mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-12-23 20:47:25 -08:00
Rewrite diff viewer
This commit is contained in:
parent
dbd7a13705
commit
04ca0a9236
8 changed files with 2336 additions and 2200 deletions
233
SourceGit/Git/Diff.cs
Normal file
233
SourceGit/Git/Diff.cs
Normal file
|
@ -0,0 +1,233 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SourceGit.Git {
|
||||
|
||||
/// <summary>
|
||||
/// Diff helper.
|
||||
/// </summary>
|
||||
public class Diff {
|
||||
private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@", RegexOptions.None);
|
||||
|
||||
/// <summary>
|
||||
/// Line mode.
|
||||
/// </summary>
|
||||
public enum LineMode {
|
||||
Normal,
|
||||
Indicator,
|
||||
Empty,
|
||||
Added,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Side
|
||||
/// </summary>
|
||||
public enum Side {
|
||||
Left,
|
||||
Right,
|
||||
Both,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Block
|
||||
/// </summary>
|
||||
public class Block {
|
||||
public Side Side = Side.Both;
|
||||
public LineMode Mode = LineMode.Normal;
|
||||
public int LeftStart = 0;
|
||||
public int RightStart = 0;
|
||||
public int Count = 0;
|
||||
public StringBuilder Builder = new StringBuilder();
|
||||
|
||||
public bool IsLeftDelete => Side == Side.Left && Mode == LineMode.Deleted;
|
||||
public bool IsRightAdded => Side == Side.Right && Mode == LineMode.Added;
|
||||
public bool IsBothSideNormal => Side == Side.Both && Mode == LineMode.Normal;
|
||||
public bool CanShowNumber => Mode != LineMode.Indicator && Mode != LineMode.Empty;
|
||||
|
||||
public void Append(string data) {
|
||||
if (Count > 0) Builder.AppendLine();
|
||||
Builder.Append(data);
|
||||
Count++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diff result.
|
||||
/// </summary>
|
||||
public class Result {
|
||||
public bool IsValid = false;
|
||||
public bool IsBinary = false;
|
||||
public List<Block> Blocks = new List<Block>();
|
||||
public int LeftLineCount = 0;
|
||||
public int RightLineCount = 0;
|
||||
|
||||
public void SetBinary() {
|
||||
IsValid = true;
|
||||
IsBinary = true;
|
||||
}
|
||||
|
||||
public void Add(Block b) {
|
||||
if (b.Count == 0) return;
|
||||
|
||||
switch (b.Side) {
|
||||
case Side.Left:
|
||||
LeftLineCount += b.Count;
|
||||
break;
|
||||
case Side.Right:
|
||||
RightLineCount += b.Count;
|
||||
break;
|
||||
default:
|
||||
LeftLineCount += b.Count;
|
||||
RightLineCount += b.Count;
|
||||
break;
|
||||
}
|
||||
|
||||
Blocks.Add(b);
|
||||
}
|
||||
|
||||
public void Fit() {
|
||||
if (LeftLineCount > RightLineCount) {
|
||||
var b = new Block();
|
||||
b.Side = Side.Right;
|
||||
b.Mode = LineMode.Empty;
|
||||
|
||||
var delta = LeftLineCount - RightLineCount;
|
||||
for (int i = 0; i < delta; i++) b.Append("");
|
||||
|
||||
Add(b);
|
||||
} else if (LeftLineCount < RightLineCount) {
|
||||
var b = new Block();
|
||||
b.Side = Side.Left;
|
||||
b.Mode = LineMode.Empty;
|
||||
|
||||
var delta = RightLineCount - LeftLineCount;
|
||||
for (int i = 0; i < delta; i++) b.Append("");
|
||||
|
||||
Add(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run diff process.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
public static Result Run(Repository repo, string args) {
|
||||
var rs = new Result();
|
||||
var current = new Block();
|
||||
var left = 0;
|
||||
var right = 0;
|
||||
|
||||
repo.RunCommand($"diff --ignore-cr-at-eol {args}", line => {
|
||||
if (rs.IsBinary) return;
|
||||
|
||||
if (!rs.IsValid) {
|
||||
var match = REG_INDICATOR.Match(line);
|
||||
if (!match.Success) {
|
||||
if (line.StartsWith("Binary ")) rs.SetBinary();
|
||||
return;
|
||||
}
|
||||
|
||||
rs.IsValid = true;
|
||||
left = int.Parse(match.Groups[1].Value);
|
||||
right = int.Parse(match.Groups[2].Value);
|
||||
current.Mode = LineMode.Indicator;
|
||||
current.Append(line);
|
||||
} else {
|
||||
if (line[0] == '-') {
|
||||
if (current.IsLeftDelete) {
|
||||
current.Append(line.Substring(1));
|
||||
} else {
|
||||
rs.Add(current);
|
||||
|
||||
current = new Block();
|
||||
current.Side = Side.Left;
|
||||
current.Mode = LineMode.Deleted;
|
||||
current.LeftStart = left;
|
||||
current.Append(line.Substring(1));
|
||||
}
|
||||
|
||||
left++;
|
||||
} else if (line[0] == '+') {
|
||||
if (current.IsRightAdded) {
|
||||
current.Append(line.Substring(1));
|
||||
} else {
|
||||
rs.Add(current);
|
||||
|
||||
current = new Block();
|
||||
current.Side = Side.Right;
|
||||
current.Mode = LineMode.Added;
|
||||
current.RightStart = right;
|
||||
current.Append(line.Substring(1));
|
||||
}
|
||||
|
||||
right++;
|
||||
} else if (line[0] == '\\') {
|
||||
var tmp = new Block();
|
||||
tmp.Side = current.Side;
|
||||
tmp.Mode = LineMode.Indicator;
|
||||
tmp.Append(line.Substring(1));
|
||||
|
||||
rs.Add(current);
|
||||
rs.Add(tmp);
|
||||
rs.Fit();
|
||||
|
||||
current = new Block();
|
||||
current.LeftStart = left;
|
||||
current.RightStart = right;
|
||||
} else {
|
||||
var match = REG_INDICATOR.Match(line);
|
||||
if (match.Success) {
|
||||
rs.Add(current);
|
||||
rs.Fit();
|
||||
|
||||
left = int.Parse(match.Groups[1].Value);
|
||||
right = int.Parse(match.Groups[2].Value);
|
||||
|
||||
current = new Block();
|
||||
current.Mode = LineMode.Indicator;
|
||||
current.Append(line);
|
||||
} else {
|
||||
if (current.IsBothSideNormal) {
|
||||
current.Append(line.Substring(1));
|
||||
} else {
|
||||
rs.Add(current);
|
||||
rs.Fit();
|
||||
|
||||
current = new Block();
|
||||
current.LeftStart = left;
|
||||
current.RightStart = right;
|
||||
current.Append(line.Substring(1));
|
||||
}
|
||||
|
||||
left++;
|
||||
right++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rs.Add(current);
|
||||
rs.Fit();
|
||||
|
||||
if (rs.IsBinary) {
|
||||
var b = new Block();
|
||||
b.Mode = LineMode.Indicator;
|
||||
b.Append("BINARY FILES NOT SUPPORTED!!!");
|
||||
rs.Blocks.Clear();
|
||||
rs.Blocks.Add(b);
|
||||
} else if (rs.Blocks.Count == 0) {
|
||||
var b = new Block();
|
||||
b.Mode = LineMode.Indicator;
|
||||
b.Append("NO CHANGES OR ONLY WHITESPACE CHANGES!!!");
|
||||
rs.Blocks.Add(b);
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -827,26 +827,6 @@ namespace SourceGit.Git {
|
|||
return stashes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diff
|
||||
/// </summary>
|
||||
/// <param name="startRevision"></param>
|
||||
/// <param name="endRevision"></param>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="orgFile"></param>
|
||||
/// <returns></returns>
|
||||
public List<string> Diff(string startRevision, string endRevision, string file, string orgFile = null) {
|
||||
var args = $"diff --ignore-cr-at-eol {startRevision} {endRevision} -- ";
|
||||
if (!string.IsNullOrEmpty(orgFile)) args += $"\"{orgFile}\" ";
|
||||
args += $"\"{file}\"";
|
||||
|
||||
var data = new List<string>();
|
||||
var errs = RunCommand(args, line => data.Add(line));
|
||||
|
||||
if (errs != null) App.RaiseError(errs);
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blame file.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,468 +1,456 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Navigation;
|
||||
|
||||
namespace SourceGit.UI {
|
||||
|
||||
/// <summary>
|
||||
/// Commit detail viewer
|
||||
/// </summary>
|
||||
public partial class CommitViewer : UserControl {
|
||||
private Git.Repository repo = null;
|
||||
private Git.Commit commit = null;
|
||||
private List<Git.Change> cachedChanges = new List<Git.Change>();
|
||||
private List<Git.Change> displayChanges = new List<Git.Change>();
|
||||
private string changeFilter = null;
|
||||
|
||||
/// <summary>
|
||||
/// Node for file tree.
|
||||
/// </summary>
|
||||
public class Node {
|
||||
public string FilePath { get; set; } = "";
|
||||
public string OriginalPath { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public bool IsFile { get; set; } = false;
|
||||
public bool IsNodeExpanded { get; set; } = true;
|
||||
public Git.Change Change { get; set; } = null;
|
||||
public List<Node> Children { get; set; } = new List<Node>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public CommitViewer() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
#region DATA
|
||||
public void SetData(Git.Repository opened, Git.Commit selected) {
|
||||
repo = opened;
|
||||
commit = selected;
|
||||
|
||||
SetBaseInfo(commit);
|
||||
|
||||
Task.Run(() => {
|
||||
cachedChanges.Clear();
|
||||
cachedChanges = commit.GetChanges(repo);
|
||||
|
||||
Dispatcher.Invoke(() => {
|
||||
changeList1.ItemsSource = null;
|
||||
changeList1.ItemsSource = cachedChanges;
|
||||
});
|
||||
|
||||
LayoutChanges();
|
||||
SetRevisionFiles(commit.GetFiles(repo));
|
||||
});
|
||||
}
|
||||
|
||||
private void Cleanup(object sender, RoutedEventArgs e) {
|
||||
fileTree.ItemsSource = null;
|
||||
changeList1.ItemsSource = null;
|
||||
changeList2.ItemsSource = null;
|
||||
displayChanges.Clear();
|
||||
cachedChanges.Clear();
|
||||
diffViewer.Reset();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region BASE_INFO
|
||||
private void SetBaseInfo(Git.Commit commit) {
|
||||
var parentIds = new List<string>();
|
||||
foreach (var p in commit.Parents) parentIds.Add(p.Substring(0, 8));
|
||||
|
||||
SHA.Text = commit.SHA;
|
||||
refs.ItemsSource = commit.Decorators;
|
||||
parents.ItemsSource = parentIds;
|
||||
author.Text = $"{commit.Author.Name} <{commit.Author.Email}>";
|
||||
authorTime.Text = commit.Author.Time;
|
||||
committer.Text = $"{commit.Committer.Name} <{commit.Committer.Email}>";
|
||||
committerTime.Text = commit.Committer.Time;
|
||||
subject.Text = commit.Subject;
|
||||
message.Text = commit.Message.Trim();
|
||||
|
||||
if (commit.Decorators.Count == 0) lblRefs.Visibility = Visibility.Collapsed;
|
||||
else lblRefs.Visibility = Visibility.Visible;
|
||||
|
||||
if (commit.Committer.Email == commit.Author.Email && commit.Committer.Time == commit.Author.Time) {
|
||||
committerRow.Height = new GridLength(0);
|
||||
} else {
|
||||
committerRow.Height = GridLength.Auto;
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigateParent(object sender, RequestNavigateEventArgs e) {
|
||||
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CHANGES
|
||||
private void LayoutChanges() {
|
||||
displayChanges.Clear();
|
||||
|
||||
if (string.IsNullOrEmpty(changeFilter)) {
|
||||
displayChanges.AddRange(cachedChanges);
|
||||
} else {
|
||||
foreach (var c in cachedChanges) {
|
||||
if (c.Path.ToUpper().Contains(changeFilter)) displayChanges.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
List<Node> changeTreeSource = new List<Node>();
|
||||
Dictionary<string, Node> folders = new Dictionary<string, Node>();
|
||||
bool isDefaultExpanded = displayChanges.Count < 50;
|
||||
|
||||
foreach (var c in displayChanges) {
|
||||
var sepIdx = c.Path.IndexOf('/');
|
||||
if (sepIdx == -1) {
|
||||
Node node = new Node();
|
||||
node.FilePath = c.Path;
|
||||
node.IsFile = true;
|
||||
node.Name = c.Path;
|
||||
node.Change = c;
|
||||
node.IsNodeExpanded = isDefaultExpanded;
|
||||
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
|
||||
changeTreeSource.Add(node);
|
||||
} else {
|
||||
Node lastFolder = null;
|
||||
var start = 0;
|
||||
|
||||
while (sepIdx != -1) {
|
||||
var folder = c.Path.Substring(0, sepIdx);
|
||||
if (folders.ContainsKey(folder)) {
|
||||
lastFolder = folders[folder];
|
||||
} else if (lastFolder == null) {
|
||||
lastFolder = new Node();
|
||||
lastFolder.FilePath = folder;
|
||||
lastFolder.Name = folder.Substring(start);
|
||||
lastFolder.IsNodeExpanded = isDefaultExpanded;
|
||||
changeTreeSource.Add(lastFolder);
|
||||
folders.Add(folder, lastFolder);
|
||||
} else {
|
||||
var folderNode = new Node();
|
||||
folderNode.FilePath = folder;
|
||||
folderNode.Name = folder.Substring(start);
|
||||
folderNode.IsNodeExpanded = isDefaultExpanded;
|
||||
folders.Add(folder, folderNode);
|
||||
lastFolder.Children.Add(folderNode);
|
||||
lastFolder = folderNode;
|
||||
}
|
||||
|
||||
start = sepIdx + 1;
|
||||
sepIdx = c.Path.IndexOf('/', start);
|
||||
}
|
||||
|
||||
Node node = new Node();
|
||||
node.FilePath = c.Path;
|
||||
node.Name = c.Path.Substring(start);
|
||||
node.IsFile = true;
|
||||
node.Change = c;
|
||||
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
|
||||
lastFolder.Children.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
folders.Clear();
|
||||
SortTreeNodes(changeTreeSource);
|
||||
|
||||
Dispatcher.Invoke(() => {
|
||||
changeList2.ItemsSource = null;
|
||||
changeList2.ItemsSource = displayChanges;
|
||||
changeTree.ItemsSource = changeTreeSource;
|
||||
diffViewer.Reset();
|
||||
});
|
||||
}
|
||||
|
||||
private void SearchChangeFileTextChanged(object sender, TextChangedEventArgs e) {
|
||||
changeFilter = txtChangeFilter.Text.ToUpper();
|
||||
Task.Run(() => LayoutChanges());
|
||||
}
|
||||
|
||||
private async void ChangeTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
|
||||
diffViewer.Reset();
|
||||
|
||||
var node = e.NewValue as Node;
|
||||
if (node == null || !node.IsFile) return;
|
||||
|
||||
var start = $"{commit.SHA}^";
|
||||
if (commit.Parents.Count == 0) {
|
||||
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
||||
}
|
||||
|
||||
List<string> data = new List<string>();
|
||||
|
||||
await Task.Run(() => {
|
||||
data = repo.Diff(start, commit.SHA, node.FilePath, node.OriginalPath);
|
||||
});
|
||||
|
||||
diffViewer.SetData(data, node.FilePath, node.OriginalPath);
|
||||
}
|
||||
|
||||
private async void ChangeListSelectionChanged(object sender, SelectionChangedEventArgs e) {
|
||||
if (e.AddedItems.Count != 1) return;
|
||||
|
||||
var change = e.AddedItems[0] as Git.Change;
|
||||
if (change == null) return;
|
||||
|
||||
var start = $"{commit.SHA}^";
|
||||
if (commit.Parents.Count == 0) {
|
||||
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
||||
}
|
||||
|
||||
List<string> data = new List<string>();
|
||||
|
||||
await Task.Run(() => {
|
||||
data = repo.Diff(start, commit.SHA, change.Path, change.OriginalPath);
|
||||
});
|
||||
|
||||
diffViewer.SetData(data, change.Path, change.OriginalPath);
|
||||
}
|
||||
|
||||
private void ChangeListContextMenuOpening(object sender, ContextMenuEventArgs e) {
|
||||
var row = sender as DataGridRow;
|
||||
if (row == null) return;
|
||||
|
||||
var change = row.DataContext as Git.Change;
|
||||
if (change == null) return;
|
||||
|
||||
var path = change.Path;
|
||||
var menu = new ContextMenu();
|
||||
if (change.Index != Git.Change.Status.Deleted) {
|
||||
MenuItem history = new MenuItem();
|
||||
history.Header = "File History";
|
||||
history.Click += (o, ev) => {
|
||||
var viewer = new FileHistories(repo, path);
|
||||
viewer.Show();
|
||||
};
|
||||
menu.Items.Add(history);
|
||||
|
||||
MenuItem blame = new MenuItem();
|
||||
blame.Header = "Blame";
|
||||
blame.Click += (obj, ev) => {
|
||||
Blame viewer = new Blame(repo, path, commit.SHA);
|
||||
viewer.Show();
|
||||
};
|
||||
menu.Items.Add(blame);
|
||||
|
||||
MenuItem explore = new MenuItem();
|
||||
explore.Header = "Reveal in File Explorer";
|
||||
explore.Click += (o, ev) => {
|
||||
var absPath = Path.GetFullPath(repo.Path + "\\" + path);
|
||||
Process.Start("explorer", $"/select,{absPath}");
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(explore);
|
||||
|
||||
MenuItem saveAs = new MenuItem();
|
||||
saveAs.Header = "Save As ...";
|
||||
saveAs.Click += (obj, ev) => {
|
||||
var dialog = new System.Windows.Forms.FolderBrowserDialog();
|
||||
dialog.Description = change.Path;
|
||||
dialog.ShowNewFolderButton = true;
|
||||
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
|
||||
var savePath = Path.Combine(dialog.SelectedPath, Path.GetFileName(path));
|
||||
repo.RunAndRedirect($"show {commit.SHA}:\"{path}\"", savePath);
|
||||
}
|
||||
};
|
||||
menu.Items.Add(saveAs);
|
||||
}
|
||||
|
||||
MenuItem copyPath = new MenuItem();
|
||||
copyPath.Header = "Copy Path";
|
||||
copyPath.Click += (obj, ev) => {
|
||||
Clipboard.SetText(path);
|
||||
};
|
||||
menu.Items.Add(copyPath);
|
||||
menu.IsOpen = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region FILES
|
||||
private void SetRevisionFiles(List<string> files) {
|
||||
List<Node> fileTreeSource = new List<Node>();
|
||||
Dictionary<string, Node> folders = new Dictionary<string, Node>();
|
||||
|
||||
foreach (var path in files) {
|
||||
var sepIdx = path.IndexOf("/");
|
||||
if (sepIdx == -1) {
|
||||
Node node = new Node();
|
||||
node.FilePath = path;
|
||||
node.Name = path;
|
||||
node.IsFile = true;
|
||||
node.IsNodeExpanded = false;
|
||||
fileTreeSource.Add(node);
|
||||
} else {
|
||||
Node lastFolder = null;
|
||||
var start = 0;
|
||||
|
||||
while (sepIdx != -1) {
|
||||
var folder = path.Substring(0, sepIdx);
|
||||
if (folders.ContainsKey(folder)) {
|
||||
lastFolder = folders[folder];
|
||||
} else if (lastFolder == null) {
|
||||
lastFolder = new Node();
|
||||
lastFolder.FilePath = folder;
|
||||
lastFolder.Name = folder.Substring(start);
|
||||
lastFolder.IsNodeExpanded = false;
|
||||
fileTreeSource.Add(lastFolder);
|
||||
folders.Add(folder, lastFolder);
|
||||
} else {
|
||||
var folderNode = new Node();
|
||||
folderNode.FilePath = folder;
|
||||
folderNode.Name = folder.Substring(start);
|
||||
folderNode.IsNodeExpanded = false;
|
||||
folders.Add(folder, folderNode);
|
||||
lastFolder.Children.Add(folderNode);
|
||||
lastFolder = folderNode;
|
||||
}
|
||||
|
||||
start = sepIdx + 1;
|
||||
sepIdx = path.IndexOf('/', start);
|
||||
}
|
||||
|
||||
Node node = new Node();
|
||||
node.FilePath = path;
|
||||
node.Name = path.Substring(start);
|
||||
node.IsFile = true;
|
||||
node.IsNodeExpanded = false;
|
||||
lastFolder.Children.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
folders.Clear();
|
||||
SortTreeNodes(fileTreeSource);
|
||||
|
||||
Dispatcher.Invoke(() => {
|
||||
fileTree.ItemsSource = fileTreeSource;
|
||||
filePreview.Text = "";
|
||||
});
|
||||
}
|
||||
|
||||
private async void FileTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
|
||||
filePreview.Text = "";
|
||||
|
||||
var node = e.NewValue as Node;
|
||||
if (node == null || !node.IsFile) return;
|
||||
|
||||
await Task.Run(() => {
|
||||
var data = commit.GetTextFileContent(repo, node.FilePath);
|
||||
Dispatcher.Invoke(() => filePreview.Text = data);
|
||||
});
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region TREE_COMMON
|
||||
private void SortTreeNodes(List<Node> list) {
|
||||
list.Sort((l, r) => {
|
||||
if (l.IsFile) {
|
||||
return r.IsFile ? l.Name.CompareTo(r.Name) : 1;
|
||||
} else {
|
||||
return r.IsFile ? -1 : l.Name.CompareTo(r.Name);
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var sub in list) {
|
||||
if (sub.Children.Count > 0) SortTreeNodes(sub.Children);
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollViewer GetScrollViewer(FrameworkElement owner) {
|
||||
if (owner == null) return null;
|
||||
if (owner is ScrollViewer) return owner as ScrollViewer;
|
||||
|
||||
int n = VisualTreeHelper.GetChildrenCount(owner);
|
||||
for (int i = 0; i < n; i++) {
|
||||
var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement;
|
||||
var deep = GetScrollViewer(child);
|
||||
if (deep != null) return deep;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void TreeMouseWheel(object sender, MouseWheelEventArgs e) {
|
||||
var scroll = GetScrollViewer(sender as TreeView);
|
||||
if (scroll == null) return;
|
||||
|
||||
if (e.Delta > 0) {
|
||||
scroll.LineUp();
|
||||
} else {
|
||||
scroll.LineDown();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void TreeContextMenuOpening(object sender, ContextMenuEventArgs e) {
|
||||
var item = sender as TreeViewItem;
|
||||
if (item == null) return;
|
||||
|
||||
var node = item.DataContext as Node;
|
||||
if (node == null || !node.IsFile) return;
|
||||
|
||||
item.IsSelected = true;
|
||||
|
||||
ContextMenu menu = new ContextMenu();
|
||||
if (node.Change == null || node.Change.Index != Git.Change.Status.Deleted) {
|
||||
MenuItem history = new MenuItem();
|
||||
history.Header = "File History";
|
||||
history.Click += (o, ev) => {
|
||||
var viewer = new FileHistories(repo, node.FilePath);
|
||||
viewer.Show();
|
||||
};
|
||||
menu.Items.Add(history);
|
||||
|
||||
MenuItem blame = new MenuItem();
|
||||
blame.Header = "Blame";
|
||||
blame.Click += (obj, ev) => {
|
||||
Blame viewer = new Blame(repo, node.FilePath, commit.SHA);
|
||||
viewer.Show();
|
||||
};
|
||||
menu.Items.Add(blame);
|
||||
|
||||
MenuItem explore = new MenuItem();
|
||||
explore.Header = "Reveal in File Explorer";
|
||||
explore.Click += (o, ev) => {
|
||||
var path = Path.GetFullPath(repo.Path + "\\" + node.FilePath);
|
||||
Process.Start("explorer", $"/select,{path}");
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(explore);
|
||||
|
||||
MenuItem saveAs = new MenuItem();
|
||||
saveAs.Header = "Save As ...";
|
||||
saveAs.Click += (obj, ev) => {
|
||||
var dialog = new System.Windows.Forms.FolderBrowserDialog();
|
||||
dialog.Description = node.FilePath;
|
||||
dialog.ShowNewFolderButton = true;
|
||||
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
|
||||
var path = Path.Combine(dialog.SelectedPath, node.Name);
|
||||
repo.RunAndRedirect($"show {commit.SHA}:\"{node.FilePath}\"", path);
|
||||
}
|
||||
};
|
||||
menu.Items.Add(saveAs);
|
||||
}
|
||||
|
||||
MenuItem copyPath = new MenuItem();
|
||||
copyPath.Header = "Copy Path";
|
||||
copyPath.Click += (obj, ev) => {
|
||||
Clipboard.SetText(node.FilePath);
|
||||
};
|
||||
menu.Items.Add(copyPath);
|
||||
menu.IsOpen = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Navigation;
|
||||
|
||||
namespace SourceGit.UI {
|
||||
|
||||
/// <summary>
|
||||
/// Commit detail viewer
|
||||
/// </summary>
|
||||
public partial class CommitViewer : UserControl {
|
||||
private Git.Repository repo = null;
|
||||
private Git.Commit commit = null;
|
||||
private List<Git.Change> cachedChanges = new List<Git.Change>();
|
||||
private List<Git.Change> displayChanges = new List<Git.Change>();
|
||||
private string changeFilter = null;
|
||||
|
||||
/// <summary>
|
||||
/// Node for file tree.
|
||||
/// </summary>
|
||||
public class Node {
|
||||
public string FilePath { get; set; } = "";
|
||||
public string OriginalPath { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public bool IsFile { get; set; } = false;
|
||||
public bool IsNodeExpanded { get; set; } = true;
|
||||
public Git.Change Change { get; set; } = null;
|
||||
public List<Node> Children { get; set; } = new List<Node>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public CommitViewer() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
#region DATA
|
||||
public void SetData(Git.Repository opened, Git.Commit selected) {
|
||||
repo = opened;
|
||||
commit = selected;
|
||||
|
||||
SetBaseInfo(commit);
|
||||
|
||||
Task.Run(() => {
|
||||
cachedChanges.Clear();
|
||||
cachedChanges = commit.GetChanges(repo);
|
||||
|
||||
Dispatcher.Invoke(() => {
|
||||
changeList1.ItemsSource = null;
|
||||
changeList1.ItemsSource = cachedChanges;
|
||||
});
|
||||
|
||||
LayoutChanges();
|
||||
SetRevisionFiles(commit.GetFiles(repo));
|
||||
});
|
||||
}
|
||||
|
||||
private void Cleanup(object sender, RoutedEventArgs e) {
|
||||
fileTree.ItemsSource = null;
|
||||
changeList1.ItemsSource = null;
|
||||
changeList2.ItemsSource = null;
|
||||
displayChanges.Clear();
|
||||
cachedChanges.Clear();
|
||||
diffViewer.Reset();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region BASE_INFO
|
||||
private void SetBaseInfo(Git.Commit commit) {
|
||||
var parentIds = new List<string>();
|
||||
foreach (var p in commit.Parents) parentIds.Add(p.Substring(0, 8));
|
||||
|
||||
SHA.Text = commit.SHA;
|
||||
refs.ItemsSource = commit.Decorators;
|
||||
parents.ItemsSource = parentIds;
|
||||
author.Text = $"{commit.Author.Name} <{commit.Author.Email}>";
|
||||
authorTime.Text = commit.Author.Time;
|
||||
committer.Text = $"{commit.Committer.Name} <{commit.Committer.Email}>";
|
||||
committerTime.Text = commit.Committer.Time;
|
||||
subject.Text = commit.Subject;
|
||||
message.Text = commit.Message.Trim();
|
||||
|
||||
if (commit.Decorators.Count == 0) lblRefs.Visibility = Visibility.Collapsed;
|
||||
else lblRefs.Visibility = Visibility.Visible;
|
||||
|
||||
if (commit.Committer.Email == commit.Author.Email && commit.Committer.Time == commit.Author.Time) {
|
||||
committerRow.Height = new GridLength(0);
|
||||
} else {
|
||||
committerRow.Height = GridLength.Auto;
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigateParent(object sender, RequestNavigateEventArgs e) {
|
||||
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CHANGES
|
||||
private void LayoutChanges() {
|
||||
displayChanges.Clear();
|
||||
|
||||
if (string.IsNullOrEmpty(changeFilter)) {
|
||||
displayChanges.AddRange(cachedChanges);
|
||||
} else {
|
||||
foreach (var c in cachedChanges) {
|
||||
if (c.Path.ToUpper().Contains(changeFilter)) displayChanges.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
List<Node> changeTreeSource = new List<Node>();
|
||||
Dictionary<string, Node> folders = new Dictionary<string, Node>();
|
||||
bool isDefaultExpanded = displayChanges.Count < 50;
|
||||
|
||||
foreach (var c in displayChanges) {
|
||||
var sepIdx = c.Path.IndexOf('/');
|
||||
if (sepIdx == -1) {
|
||||
Node node = new Node();
|
||||
node.FilePath = c.Path;
|
||||
node.IsFile = true;
|
||||
node.Name = c.Path;
|
||||
node.Change = c;
|
||||
node.IsNodeExpanded = isDefaultExpanded;
|
||||
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
|
||||
changeTreeSource.Add(node);
|
||||
} else {
|
||||
Node lastFolder = null;
|
||||
var start = 0;
|
||||
|
||||
while (sepIdx != -1) {
|
||||
var folder = c.Path.Substring(0, sepIdx);
|
||||
if (folders.ContainsKey(folder)) {
|
||||
lastFolder = folders[folder];
|
||||
} else if (lastFolder == null) {
|
||||
lastFolder = new Node();
|
||||
lastFolder.FilePath = folder;
|
||||
lastFolder.Name = folder.Substring(start);
|
||||
lastFolder.IsNodeExpanded = isDefaultExpanded;
|
||||
changeTreeSource.Add(lastFolder);
|
||||
folders.Add(folder, lastFolder);
|
||||
} else {
|
||||
var folderNode = new Node();
|
||||
folderNode.FilePath = folder;
|
||||
folderNode.Name = folder.Substring(start);
|
||||
folderNode.IsNodeExpanded = isDefaultExpanded;
|
||||
folders.Add(folder, folderNode);
|
||||
lastFolder.Children.Add(folderNode);
|
||||
lastFolder = folderNode;
|
||||
}
|
||||
|
||||
start = sepIdx + 1;
|
||||
sepIdx = c.Path.IndexOf('/', start);
|
||||
}
|
||||
|
||||
Node node = new Node();
|
||||
node.FilePath = c.Path;
|
||||
node.Name = c.Path.Substring(start);
|
||||
node.IsFile = true;
|
||||
node.Change = c;
|
||||
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
|
||||
lastFolder.Children.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
folders.Clear();
|
||||
SortTreeNodes(changeTreeSource);
|
||||
|
||||
Dispatcher.Invoke(() => {
|
||||
changeList2.ItemsSource = null;
|
||||
changeList2.ItemsSource = displayChanges;
|
||||
changeTree.ItemsSource = changeTreeSource;
|
||||
diffViewer.Reset();
|
||||
});
|
||||
}
|
||||
|
||||
private void SearchChangeFileTextChanged(object sender, TextChangedEventArgs e) {
|
||||
changeFilter = txtChangeFilter.Text.ToUpper();
|
||||
Task.Run(() => LayoutChanges());
|
||||
}
|
||||
|
||||
private void ChangeTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
|
||||
diffViewer.Reset();
|
||||
|
||||
var node = e.NewValue as Node;
|
||||
if (node == null || !node.IsFile) return;
|
||||
|
||||
var start = $"{commit.SHA}^";
|
||||
if (commit.Parents.Count == 0) {
|
||||
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
||||
}
|
||||
|
||||
diffViewer.Diff(repo, $"{start} {commit.SHA}", node.FilePath, node.OriginalPath);
|
||||
}
|
||||
|
||||
private void ChangeListSelectionChanged(object sender, SelectionChangedEventArgs e) {
|
||||
if (e.AddedItems.Count != 1) return;
|
||||
|
||||
var change = e.AddedItems[0] as Git.Change;
|
||||
if (change == null) return;
|
||||
|
||||
var start = $"{commit.SHA}^";
|
||||
if (commit.Parents.Count == 0) {
|
||||
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
||||
}
|
||||
|
||||
diffViewer.Diff(repo, $"{start} {commit.SHA}", change.Path, change.OriginalPath);
|
||||
}
|
||||
|
||||
private void ChangeListContextMenuOpening(object sender, ContextMenuEventArgs e) {
|
||||
var row = sender as DataGridRow;
|
||||
if (row == null) return;
|
||||
|
||||
var change = row.DataContext as Git.Change;
|
||||
if (change == null) return;
|
||||
|
||||
var path = change.Path;
|
||||
var menu = new ContextMenu();
|
||||
if (change.Index != Git.Change.Status.Deleted) {
|
||||
MenuItem history = new MenuItem();
|
||||
history.Header = "File History";
|
||||
history.Click += (o, ev) => {
|
||||
var viewer = new FileHistories(repo, path);
|
||||
viewer.Show();
|
||||
};
|
||||
menu.Items.Add(history);
|
||||
|
||||
MenuItem blame = new MenuItem();
|
||||
blame.Header = "Blame";
|
||||
blame.Click += (obj, ev) => {
|
||||
Blame viewer = new Blame(repo, path, commit.SHA);
|
||||
viewer.Show();
|
||||
};
|
||||
menu.Items.Add(blame);
|
||||
|
||||
MenuItem explore = new MenuItem();
|
||||
explore.Header = "Reveal in File Explorer";
|
||||
explore.Click += (o, ev) => {
|
||||
var absPath = Path.GetFullPath(repo.Path + "\\" + path);
|
||||
Process.Start("explorer", $"/select,{absPath}");
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(explore);
|
||||
|
||||
MenuItem saveAs = new MenuItem();
|
||||
saveAs.Header = "Save As ...";
|
||||
saveAs.Click += (obj, ev) => {
|
||||
var dialog = new System.Windows.Forms.FolderBrowserDialog();
|
||||
dialog.Description = change.Path;
|
||||
dialog.ShowNewFolderButton = true;
|
||||
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
|
||||
var savePath = Path.Combine(dialog.SelectedPath, Path.GetFileName(path));
|
||||
repo.RunAndRedirect($"show {commit.SHA}:\"{path}\"", savePath);
|
||||
}
|
||||
};
|
||||
menu.Items.Add(saveAs);
|
||||
}
|
||||
|
||||
MenuItem copyPath = new MenuItem();
|
||||
copyPath.Header = "Copy Path";
|
||||
copyPath.Click += (obj, ev) => {
|
||||
Clipboard.SetText(path);
|
||||
};
|
||||
menu.Items.Add(copyPath);
|
||||
menu.IsOpen = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region FILES
|
||||
private void SetRevisionFiles(List<string> files) {
|
||||
List<Node> fileTreeSource = new List<Node>();
|
||||
Dictionary<string, Node> folders = new Dictionary<string, Node>();
|
||||
|
||||
foreach (var path in files) {
|
||||
var sepIdx = path.IndexOf("/");
|
||||
if (sepIdx == -1) {
|
||||
Node node = new Node();
|
||||
node.FilePath = path;
|
||||
node.Name = path;
|
||||
node.IsFile = true;
|
||||
node.IsNodeExpanded = false;
|
||||
fileTreeSource.Add(node);
|
||||
} else {
|
||||
Node lastFolder = null;
|
||||
var start = 0;
|
||||
|
||||
while (sepIdx != -1) {
|
||||
var folder = path.Substring(0, sepIdx);
|
||||
if (folders.ContainsKey(folder)) {
|
||||
lastFolder = folders[folder];
|
||||
} else if (lastFolder == null) {
|
||||
lastFolder = new Node();
|
||||
lastFolder.FilePath = folder;
|
||||
lastFolder.Name = folder.Substring(start);
|
||||
lastFolder.IsNodeExpanded = false;
|
||||
fileTreeSource.Add(lastFolder);
|
||||
folders.Add(folder, lastFolder);
|
||||
} else {
|
||||
var folderNode = new Node();
|
||||
folderNode.FilePath = folder;
|
||||
folderNode.Name = folder.Substring(start);
|
||||
folderNode.IsNodeExpanded = false;
|
||||
folders.Add(folder, folderNode);
|
||||
lastFolder.Children.Add(folderNode);
|
||||
lastFolder = folderNode;
|
||||
}
|
||||
|
||||
start = sepIdx + 1;
|
||||
sepIdx = path.IndexOf('/', start);
|
||||
}
|
||||
|
||||
Node node = new Node();
|
||||
node.FilePath = path;
|
||||
node.Name = path.Substring(start);
|
||||
node.IsFile = true;
|
||||
node.IsNodeExpanded = false;
|
||||
lastFolder.Children.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
folders.Clear();
|
||||
SortTreeNodes(fileTreeSource);
|
||||
|
||||
Dispatcher.Invoke(() => {
|
||||
fileTree.ItemsSource = fileTreeSource;
|
||||
filePreview.Text = "";
|
||||
});
|
||||
}
|
||||
|
||||
private async void FileTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
|
||||
filePreview.Text = "";
|
||||
|
||||
var node = e.NewValue as Node;
|
||||
if (node == null || !node.IsFile) return;
|
||||
|
||||
await Task.Run(() => {
|
||||
var data = commit.GetTextFileContent(repo, node.FilePath);
|
||||
Dispatcher.Invoke(() => filePreview.Text = data);
|
||||
});
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region TREE_COMMON
|
||||
private void SortTreeNodes(List<Node> list) {
|
||||
list.Sort((l, r) => {
|
||||
if (l.IsFile) {
|
||||
return r.IsFile ? l.Name.CompareTo(r.Name) : 1;
|
||||
} else {
|
||||
return r.IsFile ? -1 : l.Name.CompareTo(r.Name);
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var sub in list) {
|
||||
if (sub.Children.Count > 0) SortTreeNodes(sub.Children);
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollViewer GetScrollViewer(FrameworkElement owner) {
|
||||
if (owner == null) return null;
|
||||
if (owner is ScrollViewer) return owner as ScrollViewer;
|
||||
|
||||
int n = VisualTreeHelper.GetChildrenCount(owner);
|
||||
for (int i = 0; i < n; i++) {
|
||||
var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement;
|
||||
var deep = GetScrollViewer(child);
|
||||
if (deep != null) return deep;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void TreeMouseWheel(object sender, MouseWheelEventArgs e) {
|
||||
var scroll = GetScrollViewer(sender as TreeView);
|
||||
if (scroll == null) return;
|
||||
|
||||
if (e.Delta > 0) {
|
||||
scroll.LineUp();
|
||||
} else {
|
||||
scroll.LineDown();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void TreeContextMenuOpening(object sender, ContextMenuEventArgs e) {
|
||||
var item = sender as TreeViewItem;
|
||||
if (item == null) return;
|
||||
|
||||
var node = item.DataContext as Node;
|
||||
if (node == null || !node.IsFile) return;
|
||||
|
||||
item.IsSelected = true;
|
||||
|
||||
ContextMenu menu = new ContextMenu();
|
||||
if (node.Change == null || node.Change.Index != Git.Change.Status.Deleted) {
|
||||
MenuItem history = new MenuItem();
|
||||
history.Header = "File History";
|
||||
history.Click += (o, ev) => {
|
||||
var viewer = new FileHistories(repo, node.FilePath);
|
||||
viewer.Show();
|
||||
};
|
||||
menu.Items.Add(history);
|
||||
|
||||
MenuItem blame = new MenuItem();
|
||||
blame.Header = "Blame";
|
||||
blame.Click += (obj, ev) => {
|
||||
Blame viewer = new Blame(repo, node.FilePath, commit.SHA);
|
||||
viewer.Show();
|
||||
};
|
||||
menu.Items.Add(blame);
|
||||
|
||||
MenuItem explore = new MenuItem();
|
||||
explore.Header = "Reveal in File Explorer";
|
||||
explore.Click += (o, ev) => {
|
||||
var path = Path.GetFullPath(repo.Path + "\\" + node.FilePath);
|
||||
Process.Start("explorer", $"/select,{path}");
|
||||
e.Handled = true;
|
||||
};
|
||||
menu.Items.Add(explore);
|
||||
|
||||
MenuItem saveAs = new MenuItem();
|
||||
saveAs.Header = "Save As ...";
|
||||
saveAs.Click += (obj, ev) => {
|
||||
var dialog = new System.Windows.Forms.FolderBrowserDialog();
|
||||
dialog.Description = node.FilePath;
|
||||
dialog.ShowNewFolderButton = true;
|
||||
|
||||
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
|
||||
var path = Path.Combine(dialog.SelectedPath, node.Name);
|
||||
repo.RunAndRedirect($"show {commit.SHA}:\"{node.FilePath}\"", path);
|
||||
}
|
||||
};
|
||||
menu.Items.Add(saveAs);
|
||||
}
|
||||
|
||||
MenuItem copyPath = new MenuItem();
|
||||
copyPath.Header = "Copy Path";
|
||||
copyPath.Click += (obj, ev) => {
|
||||
Clipboard.SetText(node.FilePath);
|
||||
};
|
||||
menu.Items.Add(copyPath);
|
||||
menu.IsOpen = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,139 +1,143 @@
|
|||
<UserControl x:Class="SourceGit.UI.DiffViewer"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d"
|
||||
FontFamily="Consolas">
|
||||
<Border BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,4">
|
||||
<StackPanel x:Name="orgFileNamePanel" Orientation="Horizontal">
|
||||
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/>
|
||||
<TextBlock x:Name="orgFileName" Margin="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
|
||||
<TextBlock Margin="8,0" VerticalAlignment="Center" Text="→" Foreground="{StaticResource Brush.FG}"/>
|
||||
</StackPanel>
|
||||
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/>
|
||||
<TextBlock x:Name="fileName" Margin="4,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1" ClipToBounds="True">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="100"/>
|
||||
<ColumnDefinition Width="2"/>
|
||||
<ColumnDefinition Width="*" MinWidth="100"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Grid.Column="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="1"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox
|
||||
x:Name="leftLineNumber"
|
||||
Grid.Column="0"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="True"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
IsReadOnly="True"
|
||||
Margin="4,0,4,0"
|
||||
FontSize="13"
|
||||
HorizontalContentAlignment="Right"
|
||||
VerticalAlignment="Stretch"/>
|
||||
|
||||
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
|
||||
|
||||
<RichTextBox
|
||||
x:Name="leftText"
|
||||
Grid.Column="2"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="True"
|
||||
IsReadOnly="True"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
Foreground="{StaticResource Brush.FG}"
|
||||
Height="Auto"
|
||||
FontSize="13"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
RenderOptions.ClearTypeHint="Enabled"
|
||||
ScrollViewer.ScrollChanged="OnViewerScroll"
|
||||
PreviewMouseWheel="OnViewerMouseWheel"
|
||||
SizeChanged="LeftSizeChanged"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<RichTextBox.Document>
|
||||
<FlowDocument PageWidth="0"/>
|
||||
</RichTextBox.Document>
|
||||
</RichTextBox>
|
||||
</Grid>
|
||||
|
||||
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{StaticResource Brush.Border2}"/>
|
||||
|
||||
<Grid Grid.Column="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="1"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox
|
||||
x:Name="rightLineNumber"
|
||||
Grid.Column="0"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="True"
|
||||
IsReadOnly="True"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
Margin="4,0,4,0"
|
||||
FontSize="13"
|
||||
HorizontalContentAlignment="Right"
|
||||
VerticalAlignment="Stretch"/>
|
||||
|
||||
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
|
||||
|
||||
<RichTextBox
|
||||
x:Name="rightText"
|
||||
Grid.Column="2"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="True"
|
||||
IsReadOnly="True"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
Foreground="{StaticResource Brush.FG}"
|
||||
Height="Auto"
|
||||
FontSize="13"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
RenderOptions.ClearTypeHint="Enabled"
|
||||
ScrollViewer.ScrollChanged="OnViewerScroll"
|
||||
PreviewMouseWheel="OnViewerMouseWheel"
|
||||
SizeChanged="RightSizeChanged"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<RichTextBox.Document>
|
||||
<FlowDocument PageWidth="0"/>
|
||||
</RichTextBox.Document>
|
||||
</RichTextBox>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="mask" Grid.RowSpan="2" Background="{StaticResource Brush.BG3}" Visibility="Collapsed">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2">
|
||||
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Diff}"/>
|
||||
<Label Margin="0,8,0,0" Content="SELECT FILE TO VIEW CHANGES" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
<UserControl x:Class="SourceGit.UI.DiffViewer"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d"
|
||||
FontFamily="Consolas">
|
||||
<Border BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,4">
|
||||
<StackPanel x:Name="orgFileNamePanel" Orientation="Horizontal">
|
||||
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/>
|
||||
<TextBlock x:Name="orgFileName" Margin="4,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
|
||||
<TextBlock Margin="8,0" VerticalAlignment="Center" Text="→" Foreground="{StaticResource Brush.FG}"/>
|
||||
</StackPanel>
|
||||
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/>
|
||||
<TextBlock x:Name="fileName" Margin="4,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1" ClipToBounds="True">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="100"/>
|
||||
<ColumnDefinition Width="2"/>
|
||||
<ColumnDefinition Width="*" MinWidth="100"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Grid.Column="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="1"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox
|
||||
x:Name="leftLineNumber"
|
||||
Grid.Column="0"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="True"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
IsReadOnly="True"
|
||||
Margin="4,0,4,0"
|
||||
FontSize="13"
|
||||
HorizontalContentAlignment="Right"
|
||||
VerticalAlignment="Stretch"/>
|
||||
|
||||
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
|
||||
|
||||
<RichTextBox
|
||||
x:Name="leftText"
|
||||
Grid.Column="2"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="True"
|
||||
IsReadOnly="True"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
Foreground="{StaticResource Brush.FG}"
|
||||
Height="Auto"
|
||||
FontSize="13"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
RenderOptions.ClearTypeHint="Enabled"
|
||||
ScrollViewer.ScrollChanged="OnViewerScroll"
|
||||
PreviewMouseWheel="OnViewerMouseWheel"
|
||||
SizeChanged="LeftSizeChanged"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<RichTextBox.Document>
|
||||
<FlowDocument PageWidth="0"/>
|
||||
</RichTextBox.Document>
|
||||
</RichTextBox>
|
||||
</Grid>
|
||||
|
||||
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{StaticResource Brush.Border2}"/>
|
||||
|
||||
<Grid Grid.Column="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="1"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox
|
||||
x:Name="rightLineNumber"
|
||||
Grid.Column="0"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="True"
|
||||
IsReadOnly="True"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
Margin="4,0,4,0"
|
||||
FontSize="13"
|
||||
HorizontalContentAlignment="Right"
|
||||
VerticalAlignment="Stretch"/>
|
||||
|
||||
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
|
||||
|
||||
<RichTextBox
|
||||
x:Name="rightText"
|
||||
Grid.Column="2"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="True"
|
||||
IsReadOnly="True"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
Foreground="{StaticResource Brush.FG}"
|
||||
Height="Auto"
|
||||
FontSize="13"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
RenderOptions.ClearTypeHint="Enabled"
|
||||
ScrollViewer.ScrollChanged="OnViewerScroll"
|
||||
PreviewMouseWheel="OnViewerMouseWheel"
|
||||
SizeChanged="RightSizeChanged"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<RichTextBox.Document>
|
||||
<FlowDocument PageWidth="0"/>
|
||||
</RichTextBox.Document>
|
||||
</RichTextBox>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="mask" Grid.RowSpan="2" Background="{StaticResource Brush.BG3}" Visibility="Collapsed">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2">
|
||||
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Diff}"/>
|
||||
<Label Margin="0,8,0,0" Content="SELECT FILE TO VIEW CHANGES" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="loading" Grid.RowSpan="2" Background="{StaticResource Brush.BG3}" Visibility="Collapsed">
|
||||
<Label Content="LOADING ..." FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG2}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
|
|
@ -1,294 +1,238 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.UI {
|
||||
|
||||
/// <summary>
|
||||
/// Viewer for git diff
|
||||
/// </summary>
|
||||
public partial class DiffViewer : UserControl {
|
||||
private double minWidth = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Line mode.
|
||||
/// </summary>
|
||||
public enum LineMode {
|
||||
Normal,
|
||||
Indicator,
|
||||
Empty,
|
||||
Added,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public DiffViewer() {
|
||||
InitializeComponent();
|
||||
Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="lines"></param>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="orgFile"></param>
|
||||
public void SetData(List<string> lines, string file, string orgFile = null) {
|
||||
minWidth = Math.Max(leftText.ActualWidth, rightText.ActualWidth) - 16;
|
||||
|
||||
fileName.Text = file;
|
||||
if (!string.IsNullOrEmpty(orgFile)) {
|
||||
orgFileNamePanel.Visibility = Visibility.Visible;
|
||||
orgFileName.Text = orgFile;
|
||||
} else {
|
||||
orgFileNamePanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
leftText.Document.Blocks.Clear();
|
||||
rightText.Document.Blocks.Clear();
|
||||
|
||||
leftLineNumber.Text = "";
|
||||
rightLineNumber.Text = "";
|
||||
|
||||
Regex regex = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@", RegexOptions.None);
|
||||
bool started = false;
|
||||
|
||||
List<Paragraph> leftData = new List<Paragraph>();
|
||||
List<Paragraph> rightData = new List<Paragraph>();
|
||||
List<string> leftNumbers = new List<string>();
|
||||
List<string> rightNumbers = new List<string>();
|
||||
|
||||
int leftLine = 0;
|
||||
int rightLine = 0;
|
||||
bool bLastLeft = true;
|
||||
|
||||
foreach (var line in lines) {
|
||||
if (!started) {
|
||||
var match = regex.Match(line);
|
||||
if (!match.Success) continue;
|
||||
|
||||
MakeParagraph(leftData, line, LineMode.Indicator);
|
||||
MakeParagraph(rightData, line, LineMode.Indicator);
|
||||
leftNumbers.Add("");
|
||||
rightNumbers.Add("");
|
||||
|
||||
leftLine = int.Parse(match.Groups[1].Value);
|
||||
rightLine = int.Parse(match.Groups[2].Value);
|
||||
started = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line[0] == '-') {
|
||||
MakeParagraph(leftData, line.Substring(1), LineMode.Deleted);
|
||||
leftNumbers.Add(leftLine.ToString());
|
||||
leftLine++;
|
||||
bLastLeft = true;
|
||||
} else if (line[0] == '+') {
|
||||
MakeParagraph(rightData, line.Substring(1), LineMode.Added);
|
||||
rightNumbers.Add(rightLine.ToString());
|
||||
rightLine++;
|
||||
bLastLeft = false;
|
||||
} else if (line[0] == '\\') {
|
||||
if (bLastLeft) {
|
||||
MakeParagraph(leftData, line.Substring(1), LineMode.Indicator);
|
||||
leftNumbers.Add("");
|
||||
} else {
|
||||
MakeParagraph(rightData, line.Substring(1), LineMode.Indicator);
|
||||
rightNumbers.Add("");
|
||||
}
|
||||
} else {
|
||||
FitBothSide(leftData, leftNumbers, rightData, rightNumbers);
|
||||
bLastLeft = true;
|
||||
|
||||
var match = regex.Match(line);
|
||||
if (match.Success) {
|
||||
MakeParagraph(leftData, line, LineMode.Indicator);
|
||||
MakeParagraph(rightData, line, LineMode.Indicator);
|
||||
leftNumbers.Add("");
|
||||
rightNumbers.Add("");
|
||||
|
||||
leftLine = int.Parse(match.Groups[1].Value);
|
||||
rightLine = int.Parse(match.Groups[2].Value);
|
||||
} else {
|
||||
var data = line.Substring(1);
|
||||
MakeParagraph(leftData, data, LineMode.Normal);
|
||||
MakeParagraph(rightData, data, LineMode.Normal);
|
||||
leftNumbers.Add(leftLine.ToString());
|
||||
rightNumbers.Add(rightLine.ToString());
|
||||
leftLine++;
|
||||
rightLine++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FitBothSide(leftData, leftNumbers, rightData, rightNumbers);
|
||||
|
||||
if (leftData.Count == 0) {
|
||||
MakeParagraph(leftData, "NOT SUPPORTED OR NO DATA", LineMode.Indicator);
|
||||
MakeParagraph(rightData, "NOT SUPPORTED OR NO DATA", LineMode.Indicator);
|
||||
leftNumbers.Add("");
|
||||
rightNumbers.Add("");
|
||||
}
|
||||
|
||||
leftLineNumber.Text = string.Join("\n", leftNumbers);
|
||||
rightLineNumber.Text = string.Join("\n", rightNumbers);
|
||||
leftText.Document.PageWidth = minWidth + 16;
|
||||
rightText.Document.PageWidth = minWidth + 16;
|
||||
leftText.Document.Blocks.AddRange(leftData);
|
||||
rightText.Document.Blocks.AddRange(rightData);
|
||||
leftText.ScrollToHome();
|
||||
|
||||
mask.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset data.
|
||||
/// </summary>
|
||||
public void Reset() {
|
||||
mask.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make paragraph.
|
||||
/// </summary>
|
||||
/// <param name="collection"></param>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="mode"></param>
|
||||
private void MakeParagraph(List<Paragraph> collection, string content, LineMode mode) {
|
||||
Paragraph p = new Paragraph(new Run(content));
|
||||
p.Margin = new Thickness(0);
|
||||
p.Padding = new Thickness();
|
||||
p.LineHeight = 1;
|
||||
p.Background = Brushes.Transparent;
|
||||
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
|
||||
p.FontStyle = FontStyles.Normal;
|
||||
|
||||
switch (mode) {
|
||||
case LineMode.Normal:
|
||||
break;
|
||||
case LineMode.Indicator:
|
||||
p.Foreground = Brushes.Gray;
|
||||
p.FontStyle = FontStyles.Italic;
|
||||
break;
|
||||
case LineMode.Empty:
|
||||
p.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
|
||||
break;
|
||||
case LineMode.Added:
|
||||
p.Background = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0));
|
||||
break;
|
||||
case LineMode.Deleted:
|
||||
p.Background = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
|
||||
break;
|
||||
}
|
||||
|
||||
var formatter = new FormattedText(
|
||||
content,
|
||||
CultureInfo.CurrentUICulture,
|
||||
FlowDirection.LeftToRight,
|
||||
new Typeface(leftText.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch),
|
||||
leftText.FontSize,
|
||||
Brushes.Black,
|
||||
new NumberSubstitution(),
|
||||
TextFormattingMode.Ideal);
|
||||
|
||||
if (minWidth < formatter.Width) minWidth = formatter.Width;
|
||||
collection.Add(p);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fit both side with empty lines.
|
||||
/// </summary>
|
||||
/// <param name="left"></param>
|
||||
/// <param name="leftNumbers"></param>
|
||||
/// <param name="right"></param>
|
||||
/// <param name="rightNumbers"></param>
|
||||
private void FitBothSide(List<Paragraph> left, List<string> leftNumbers, List<Paragraph> right, List<string> rightNumbers) {
|
||||
int leftCount = left.Count;
|
||||
int rightCount = right.Count;
|
||||
int diff = 0;
|
||||
List<Paragraph> fitContent = null;
|
||||
List<string> fitNumber = null;
|
||||
|
||||
if (leftCount > rightCount) {
|
||||
diff = leftCount - rightCount;
|
||||
fitContent = right;
|
||||
fitNumber = rightNumbers;
|
||||
} else if (rightCount > leftCount) {
|
||||
diff = rightCount - leftCount;
|
||||
fitContent = left;
|
||||
fitNumber = leftNumbers;
|
||||
}
|
||||
|
||||
for (int i = 0; i < diff; i++) {
|
||||
MakeParagraph(fitContent, "", LineMode.Empty);
|
||||
fitNumber.Add("");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sync scroll both sides.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void OnViewerScroll(object sender, ScrollChangedEventArgs e) {
|
||||
if (e.VerticalChange != 0) {
|
||||
if (leftText.VerticalOffset != e.VerticalOffset) {
|
||||
leftText.ScrollToVerticalOffset(e.VerticalOffset);
|
||||
}
|
||||
|
||||
if (rightText.VerticalOffset != e.VerticalOffset) {
|
||||
rightText.ScrollToVerticalOffset(e.VerticalOffset);
|
||||
}
|
||||
|
||||
leftLineNumber.Margin = new Thickness(4, -e.VerticalOffset, 4, 0);
|
||||
rightLineNumber.Margin = new Thickness(4, -e.VerticalOffset, 4, 0);
|
||||
} else {
|
||||
if (leftText.HorizontalOffset != e.HorizontalOffset) {
|
||||
leftText.ScrollToHorizontalOffset(e.HorizontalOffset);
|
||||
}
|
||||
|
||||
if (rightText.HorizontalOffset != e.HorizontalOffset) {
|
||||
rightText.ScrollToHorizontalOffset(e.HorizontalOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scroll using mouse wheel.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void OnViewerMouseWheel(object sender, MouseWheelEventArgs e) {
|
||||
var text = sender as RichTextBox;
|
||||
if (text == null) return;
|
||||
|
||||
if (e.Delta > 0) {
|
||||
text.LineUp();
|
||||
} else {
|
||||
text.LineDown();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void LeftSizeChanged(object sender, SizeChangedEventArgs e) {
|
||||
if (leftText.Document.PageWidth < leftText.ActualWidth) {
|
||||
leftText.Document.PageWidth = leftText.ActualWidth;
|
||||
}
|
||||
}
|
||||
|
||||
private void RightSizeChanged(object sender, SizeChangedEventArgs e) {
|
||||
if (rightText.Document.PageWidth < rightText.ActualWidth) {
|
||||
rightText.Document.PageWidth = rightText.ActualWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SourceGit.UI {
|
||||
|
||||
/// <summary>
|
||||
/// Viewer for git diff
|
||||
/// </summary>
|
||||
public partial class DiffViewer : UserControl {
|
||||
private double minWidth = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public DiffViewer() {
|
||||
InitializeComponent();
|
||||
Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset data.
|
||||
/// </summary>
|
||||
public void Reset() {
|
||||
mask.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diff with options.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="orgPath"></param>
|
||||
public void Diff(Git.Repository repo, string options, string path, string orgPath = null) {
|
||||
SetTitle(path, orgPath);
|
||||
Task.Run(() => {
|
||||
var args = $"{options} -- ";
|
||||
if (!string.IsNullOrEmpty(orgPath)) args += $"{orgPath} ";
|
||||
args += $"\"{path}\"";
|
||||
|
||||
var rs = Git.Diff.Run(repo, args);
|
||||
SetData(rs);
|
||||
});
|
||||
}
|
||||
|
||||
#region LAYOUT
|
||||
/// <summary>
|
||||
/// Show diff title
|
||||
/// </summary>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="orgFile"></param>
|
||||
private void SetTitle(string file, string orgFile) {
|
||||
fileName.Text = file;
|
||||
if (!string.IsNullOrEmpty(orgFile)) {
|
||||
orgFileNamePanel.Visibility = Visibility.Visible;
|
||||
orgFileName.Text = orgFile;
|
||||
} else {
|
||||
orgFileNamePanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show diff content.
|
||||
/// </summary>
|
||||
/// <param name="rs"></param>
|
||||
private void SetData(Git.Diff.Result rs) {
|
||||
Dispatcher.Invoke(() => {
|
||||
loading.Visibility = Visibility.Collapsed;
|
||||
mask.Visibility = Visibility.Collapsed;
|
||||
|
||||
minWidth = Math.Max(leftText.ActualWidth, rightText.ActualWidth) - 16;
|
||||
|
||||
leftLineNumber.Text = "";
|
||||
rightLineNumber.Text = "";
|
||||
leftText.Document.Blocks.Clear();
|
||||
rightText.Document.Blocks.Clear();
|
||||
|
||||
foreach (var b in rs.Blocks) ShowBlock(b);
|
||||
|
||||
leftText.Document.PageWidth = minWidth + 16;
|
||||
rightText.Document.PageWidth = minWidth + 16;
|
||||
leftText.ScrollToHome();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make paragraph.
|
||||
/// </summary>
|
||||
/// <param name="b"></param>
|
||||
private void ShowBlock(Git.Diff.Block b) {
|
||||
var content = b.Builder.ToString();
|
||||
|
||||
Paragraph p = new Paragraph(new Run(content));
|
||||
p.Margin = new Thickness(0);
|
||||
p.Padding = new Thickness();
|
||||
p.LineHeight = 1;
|
||||
p.Background = Brushes.Transparent;
|
||||
p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
|
||||
p.FontStyle = FontStyles.Normal;
|
||||
|
||||
switch (b.Mode) {
|
||||
case Git.Diff.LineMode.Normal:
|
||||
break;
|
||||
case Git.Diff.LineMode.Indicator:
|
||||
p.Foreground = Brushes.Gray;
|
||||
p.FontStyle = FontStyles.Italic;
|
||||
break;
|
||||
case Git.Diff.LineMode.Empty:
|
||||
p.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
|
||||
break;
|
||||
case Git.Diff.LineMode.Added:
|
||||
p.Background = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0));
|
||||
break;
|
||||
case Git.Diff.LineMode.Deleted:
|
||||
p.Background = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
|
||||
break;
|
||||
}
|
||||
|
||||
var formatter = new FormattedText(
|
||||
content,
|
||||
CultureInfo.CurrentUICulture,
|
||||
FlowDirection.LeftToRight,
|
||||
new Typeface(leftText.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch),
|
||||
leftText.FontSize,
|
||||
Brushes.Black,
|
||||
new NumberSubstitution(),
|
||||
TextFormattingMode.Ideal);
|
||||
|
||||
if (minWidth < formatter.Width) minWidth = formatter.Width;
|
||||
|
||||
switch (b.Side) {
|
||||
case Git.Diff.Side.Left:
|
||||
leftText.Document.Blocks.Add(p);
|
||||
for (int i = 0; i < b.Count; i++) {
|
||||
if (b.CanShowNumber) leftLineNumber.AppendText($"{i + b.LeftStart}\n");
|
||||
else leftLineNumber.AppendText("\n");
|
||||
}
|
||||
break;
|
||||
case Git.Diff.Side.Right:
|
||||
rightText.Document.Blocks.Add(p);
|
||||
for (int i = 0; i < b.Count; i++) {
|
||||
if (b.CanShowNumber) rightLineNumber.AppendText($"{i + b.RightStart}\n");
|
||||
else rightLineNumber.AppendText("\n");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
leftText.Document.Blocks.Add(p);
|
||||
|
||||
var cp = new Paragraph(new Run(content));
|
||||
cp.Margin = new Thickness(0);
|
||||
cp.Padding = new Thickness();
|
||||
cp.LineHeight = 1;
|
||||
cp.Background = p.Background;
|
||||
cp.Foreground = p.Foreground;
|
||||
cp.FontStyle = p.FontStyle;
|
||||
rightText.Document.Blocks.Add(cp);
|
||||
|
||||
for (int i = 0; i < b.Count; i++) {
|
||||
if (b.Mode != Git.Diff.LineMode.Indicator) {
|
||||
leftLineNumber.AppendText($"{i + b.LeftStart}\n");
|
||||
rightLineNumber.AppendText($"{i + b.RightStart}\n");
|
||||
} else {
|
||||
leftLineNumber.AppendText("\n");
|
||||
rightLineNumber.AppendText("\n");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region EVENTS
|
||||
/// <summary>
|
||||
/// Sync scroll both sides.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void OnViewerScroll(object sender, ScrollChangedEventArgs e) {
|
||||
if (e.VerticalChange != 0) {
|
||||
if (leftText.VerticalOffset != e.VerticalOffset) {
|
||||
leftText.ScrollToVerticalOffset(e.VerticalOffset);
|
||||
}
|
||||
|
||||
if (rightText.VerticalOffset != e.VerticalOffset) {
|
||||
rightText.ScrollToVerticalOffset(e.VerticalOffset);
|
||||
}
|
||||
|
||||
leftLineNumber.Margin = new Thickness(4, -e.VerticalOffset, 4, 0);
|
||||
rightLineNumber.Margin = new Thickness(4, -e.VerticalOffset, 4, 0);
|
||||
} else {
|
||||
if (leftText.HorizontalOffset != e.HorizontalOffset) {
|
||||
leftText.ScrollToHorizontalOffset(e.HorizontalOffset);
|
||||
}
|
||||
|
||||
if (rightText.HorizontalOffset != e.HorizontalOffset) {
|
||||
rightText.ScrollToHorizontalOffset(e.HorizontalOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scroll using mouse wheel.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void OnViewerMouseWheel(object sender, MouseWheelEventArgs e) {
|
||||
var text = sender as RichTextBox;
|
||||
if (text == null) return;
|
||||
|
||||
if (e.Delta > 0) {
|
||||
text.LineUp();
|
||||
} else {
|
||||
text.LineDown();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void LeftSizeChanged(object sender, SizeChangedEventArgs e) {
|
||||
if (leftText.Document.PageWidth < leftText.ActualWidth) {
|
||||
leftText.Document.PageWidth = leftText.ActualWidth;
|
||||
}
|
||||
}
|
||||
|
||||
private void RightSizeChanged(object sender, SizeChangedEventArgs e) {
|
||||
if (rightText.Document.PageWidth < rightText.ActualWidth) {
|
||||
rightText.Document.PageWidth = rightText.ActualWidth;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,122 +1,118 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Navigation;
|
||||
|
||||
namespace SourceGit.UI {
|
||||
|
||||
/// <summary>
|
||||
/// File histories panel.
|
||||
/// </summary>
|
||||
public partial class FileHistories : Window {
|
||||
private Git.Repository repo = null;
|
||||
private string file = null;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="file"></param>
|
||||
public FileHistories(Git.Repository repo, string file) {
|
||||
this.repo = repo;
|
||||
this.file = file;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// Move to center
|
||||
var parent = App.Current.MainWindow;
|
||||
Left = parent.Left + (parent.Width - Width) * 0.5;
|
||||
Top = parent.Top + (parent.Height - Height) * 0.5;
|
||||
|
||||
// Show loading
|
||||
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
|
||||
anim.RepeatBehavior = RepeatBehavior.Forever;
|
||||
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
|
||||
loading.Visibility = Visibility.Visible;
|
||||
|
||||
// Load commits
|
||||
Task.Run(() => {
|
||||
var commits = repo.Commits($"-n 10000 -- \"{file}\"");
|
||||
Dispatcher.Invoke(() => {
|
||||
commitList.ItemsSource = commits;
|
||||
commitList.SelectedIndex = 0;
|
||||
|
||||
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
|
||||
loading.Visibility = Visibility.Collapsed;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logo click
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) {
|
||||
var element = e.OriginalSource as FrameworkElement;
|
||||
if (element == null) return;
|
||||
|
||||
var pos = PointToScreen(new Point(0, 33));
|
||||
SystemCommands.ShowSystemMenu(this, pos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimize
|
||||
/// </summary>
|
||||
private void Minimize(object sender, RoutedEventArgs e) {
|
||||
SystemCommands.MinimizeWindow(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maximize/Restore
|
||||
/// </summary>
|
||||
private void MaximizeOrRestore(object sender, RoutedEventArgs e) {
|
||||
if (WindowState == WindowState.Normal) {
|
||||
SystemCommands.MaximizeWindow(this);
|
||||
} else {
|
||||
SystemCommands.RestoreWindow(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quit
|
||||
/// </summary>
|
||||
private void Quit(object sender, RoutedEventArgs e) {
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commit selection change event.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private async void CommitSelectionChanged(object sender, SelectionChangedEventArgs e) {
|
||||
if (e.AddedItems.Count != 1) return;
|
||||
|
||||
var commit = e.AddedItems[0] as Git.Commit;
|
||||
var start = $"{commit.SHA}^";
|
||||
if (commit.Parents.Count == 0) start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
||||
|
||||
List<string> data = new List<string>();
|
||||
await Task.Run(() => {
|
||||
data = repo.Diff(start, commit.SHA, file);
|
||||
});
|
||||
diff.SetData(data, $"{file} @ {commit.ShortSHA}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to given string
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void NavigateToCommit(object sender, RequestNavigateEventArgs e) {
|
||||
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Navigation;
|
||||
|
||||
namespace SourceGit.UI {
|
||||
|
||||
/// <summary>
|
||||
/// File histories panel.
|
||||
/// </summary>
|
||||
public partial class FileHistories : Window {
|
||||
private Git.Repository repo = null;
|
||||
private string file = null;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="repo"></param>
|
||||
/// <param name="file"></param>
|
||||
public FileHistories(Git.Repository repo, string file) {
|
||||
this.repo = repo;
|
||||
this.file = file;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// Move to center
|
||||
var parent = App.Current.MainWindow;
|
||||
Left = parent.Left + (parent.Width - Width) * 0.5;
|
||||
Top = parent.Top + (parent.Height - Height) * 0.5;
|
||||
|
||||
// Show loading
|
||||
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
|
||||
anim.RepeatBehavior = RepeatBehavior.Forever;
|
||||
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
|
||||
loading.Visibility = Visibility.Visible;
|
||||
|
||||
// Load commits
|
||||
Task.Run(() => {
|
||||
var commits = repo.Commits($"-n 10000 -- \"{file}\"");
|
||||
Dispatcher.Invoke(() => {
|
||||
commitList.ItemsSource = commits;
|
||||
commitList.SelectedIndex = 0;
|
||||
|
||||
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
|
||||
loading.Visibility = Visibility.Collapsed;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logo click
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) {
|
||||
var element = e.OriginalSource as FrameworkElement;
|
||||
if (element == null) return;
|
||||
|
||||
var pos = PointToScreen(new Point(0, 33));
|
||||
SystemCommands.ShowSystemMenu(this, pos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimize
|
||||
/// </summary>
|
||||
private void Minimize(object sender, RoutedEventArgs e) {
|
||||
SystemCommands.MinimizeWindow(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maximize/Restore
|
||||
/// </summary>
|
||||
private void MaximizeOrRestore(object sender, RoutedEventArgs e) {
|
||||
if (WindowState == WindowState.Normal) {
|
||||
SystemCommands.MaximizeWindow(this);
|
||||
} else {
|
||||
SystemCommands.RestoreWindow(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quit
|
||||
/// </summary>
|
||||
private void Quit(object sender, RoutedEventArgs e) {
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commit selection change event.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void CommitSelectionChanged(object sender, SelectionChangedEventArgs e) {
|
||||
if (e.AddedItems.Count != 1) return;
|
||||
|
||||
var commit = e.AddedItems[0] as Git.Commit;
|
||||
var start = $"{commit.SHA}^";
|
||||
if (commit.Parents.Count == 0) start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
||||
|
||||
diff.Diff(repo, $"{start} {commit.SHA}", file);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to given string
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void NavigateToCommit(object sender, RequestNavigateEventArgs e) {
|
||||
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,118 +1,117 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SourceGit.UI {
|
||||
|
||||
/// <summary>
|
||||
/// Stashes viewer.
|
||||
/// </summary>
|
||||
public partial class Stashes : UserControl {
|
||||
private Git.Repository repo = null;
|
||||
private string selectedStash = null;
|
||||
|
||||
/// <summary>
|
||||
/// File tree node.
|
||||
/// </summary>
|
||||
public class Node {
|
||||
public string FilePath { get; set; } = "";
|
||||
public string OriginalPath { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public bool IsFile { get; set; } = false;
|
||||
public bool IsNodeExpanded { get; set; } = true;
|
||||
public Git.Change.Status Status { get; set; } = Git.Change.Status.None;
|
||||
public List<Node> Children { get; set; } = new List<Node>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public Stashes() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleanup
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void Cleanup(object sender, RoutedEventArgs e) {
|
||||
stashList.ItemsSource = null;
|
||||
changeList.ItemsSource = null;
|
||||
diff.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set data.
|
||||
/// </summary>
|
||||
/// <param name="opened"></param>
|
||||
/// <param name="stashes"></param>
|
||||
public void SetData(Git.Repository opened, List<Git.Stash> stashes) {
|
||||
repo = opened;
|
||||
selectedStash = null;
|
||||
stashList.ItemsSource = stashes;
|
||||
changeList.ItemsSource = null;
|
||||
diff.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stash list selection changed event.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void StashSelectionChanged(object sender, SelectionChangedEventArgs e) {
|
||||
if (e.AddedItems.Count != 1) return;
|
||||
|
||||
var stash = e.AddedItems[0] as Git.Stash;
|
||||
if (stash == null) return;
|
||||
|
||||
selectedStash = stash.SHA;
|
||||
diff.Reset();
|
||||
changeList.ItemsSource = stash.GetChanges(repo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// File selection changed in TreeView.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void FileSelectionChanged(object sender, SelectionChangedEventArgs e) {
|
||||
if (e.AddedItems.Count != 1) return;
|
||||
|
||||
var change = e.AddedItems[0] as Git.Change;
|
||||
if (change == null) return;
|
||||
|
||||
var data = repo.Diff($"{selectedStash}^", selectedStash, change.Path, change.OriginalPath);
|
||||
diff.SetData(data, change.Path, change.OriginalPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stash context menu.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="ev"></param>
|
||||
private void StashContextMenuOpening(object sender, ContextMenuEventArgs ev) {
|
||||
var stash = (sender as ListViewItem).DataContext as Git.Stash;
|
||||
if (stash == null) return;
|
||||
|
||||
var apply = new MenuItem();
|
||||
apply.Header = "Apply";
|
||||
apply.Click += (o, e) => stash.Apply(repo);
|
||||
|
||||
var pop = new MenuItem();
|
||||
pop.Header = "Pop";
|
||||
pop.Click += (o, e) => stash.Pop(repo);
|
||||
|
||||
var delete = new MenuItem();
|
||||
delete.Header = "Drop";
|
||||
delete.Click += (o, e) => stash.Drop(repo);
|
||||
|
||||
var menu = new ContextMenu();
|
||||
menu.Items.Add(apply);
|
||||
menu.Items.Add(pop);
|
||||
menu.Items.Add(delete);
|
||||
menu.IsOpen = true;
|
||||
ev.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SourceGit.UI {
|
||||
|
||||
/// <summary>
|
||||
/// Stashes viewer.
|
||||
/// </summary>
|
||||
public partial class Stashes : UserControl {
|
||||
private Git.Repository repo = null;
|
||||
private string selectedStash = null;
|
||||
|
||||
/// <summary>
|
||||
/// File tree node.
|
||||
/// </summary>
|
||||
public class Node {
|
||||
public string FilePath { get; set; } = "";
|
||||
public string OriginalPath { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public bool IsFile { get; set; } = false;
|
||||
public bool IsNodeExpanded { get; set; } = true;
|
||||
public Git.Change.Status Status { get; set; } = Git.Change.Status.None;
|
||||
public List<Node> Children { get; set; } = new List<Node>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public Stashes() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleanup
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void Cleanup(object sender, RoutedEventArgs e) {
|
||||
stashList.ItemsSource = null;
|
||||
changeList.ItemsSource = null;
|
||||
diff.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set data.
|
||||
/// </summary>
|
||||
/// <param name="opened"></param>
|
||||
/// <param name="stashes"></param>
|
||||
public void SetData(Git.Repository opened, List<Git.Stash> stashes) {
|
||||
repo = opened;
|
||||
selectedStash = null;
|
||||
stashList.ItemsSource = stashes;
|
||||
changeList.ItemsSource = null;
|
||||
diff.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stash list selection changed event.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void StashSelectionChanged(object sender, SelectionChangedEventArgs e) {
|
||||
if (e.AddedItems.Count != 1) return;
|
||||
|
||||
var stash = e.AddedItems[0] as Git.Stash;
|
||||
if (stash == null) return;
|
||||
|
||||
selectedStash = stash.SHA;
|
||||
diff.Reset();
|
||||
changeList.ItemsSource = stash.GetChanges(repo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// File selection changed in TreeView.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void FileSelectionChanged(object sender, SelectionChangedEventArgs e) {
|
||||
if (e.AddedItems.Count != 1) return;
|
||||
|
||||
var change = e.AddedItems[0] as Git.Change;
|
||||
if (change == null) return;
|
||||
|
||||
diff.Diff(repo, $"{selectedStash}^ {selectedStash}", change.Path, change.OriginalPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stash context menu.
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="ev"></param>
|
||||
private void StashContextMenuOpening(object sender, ContextMenuEventArgs ev) {
|
||||
var stash = (sender as ListViewItem).DataContext as Git.Stash;
|
||||
if (stash == null) return;
|
||||
|
||||
var apply = new MenuItem();
|
||||
apply.Header = "Apply";
|
||||
apply.Click += (o, e) => stash.Apply(repo);
|
||||
|
||||
var pop = new MenuItem();
|
||||
pop.Header = "Pop";
|
||||
pop.Click += (o, e) => stash.Pop(repo);
|
||||
|
||||
var delete = new MenuItem();
|
||||
delete.Header = "Drop";
|
||||
delete.Click += (o, e) => stash.Drop(repo);
|
||||
|
||||
var menu = new ContextMenu();
|
||||
menu.Items.Add(apply);
|
||||
menu.Items.Add(pop);
|
||||
menu.Items.Add(delete);
|
||||
menu.IsOpen = true;
|
||||
ev.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue