Rewrite diff viewer

This commit is contained in:
leo 2020-07-09 17:36:43 +08:00
parent dbd7a13705
commit 04ca0a9236
8 changed files with 2336 additions and 2200 deletions

233
SourceGit/Git/Diff.cs Normal file
View 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;
}
}
}

View file

@ -827,26 +827,6 @@ namespace SourceGit.Git {
return stashes; 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> /// <summary>
/// Blame file. /// Blame file.
/// </summary> /// </summary>

View file

@ -1,468 +1,456 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Navigation; using System.Windows.Navigation;
namespace SourceGit.UI { namespace SourceGit.UI {
/// <summary> /// <summary>
/// Commit detail viewer /// Commit detail viewer
/// </summary> /// </summary>
public partial class CommitViewer : UserControl { public partial class CommitViewer : UserControl {
private Git.Repository repo = null; private Git.Repository repo = null;
private Git.Commit commit = null; private Git.Commit commit = null;
private List<Git.Change> cachedChanges = new List<Git.Change>(); private List<Git.Change> cachedChanges = new List<Git.Change>();
private List<Git.Change> displayChanges = new List<Git.Change>(); private List<Git.Change> displayChanges = new List<Git.Change>();
private string changeFilter = null; private string changeFilter = null;
/// <summary> /// <summary>
/// Node for file tree. /// Node for file tree.
/// </summary> /// </summary>
public class Node { public class Node {
public string FilePath { get; set; } = ""; public string FilePath { get; set; } = "";
public string OriginalPath { get; set; } = ""; public string OriginalPath { get; set; } = "";
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public bool IsFile { get; set; } = false; public bool IsFile { get; set; } = false;
public bool IsNodeExpanded { get; set; } = true; public bool IsNodeExpanded { get; set; } = true;
public Git.Change Change { get; set; } = null; public Git.Change Change { get; set; } = null;
public List<Node> Children { get; set; } = new List<Node>(); public List<Node> Children { get; set; } = new List<Node>();
} }
/// <summary> /// <summary>
/// Constructor. /// Constructor.
/// </summary> /// </summary>
public CommitViewer() { public CommitViewer() {
InitializeComponent(); InitializeComponent();
} }
#region DATA #region DATA
public void SetData(Git.Repository opened, Git.Commit selected) { public void SetData(Git.Repository opened, Git.Commit selected) {
repo = opened; repo = opened;
commit = selected; commit = selected;
SetBaseInfo(commit); SetBaseInfo(commit);
Task.Run(() => { Task.Run(() => {
cachedChanges.Clear(); cachedChanges.Clear();
cachedChanges = commit.GetChanges(repo); cachedChanges = commit.GetChanges(repo);
Dispatcher.Invoke(() => { Dispatcher.Invoke(() => {
changeList1.ItemsSource = null; changeList1.ItemsSource = null;
changeList1.ItemsSource = cachedChanges; changeList1.ItemsSource = cachedChanges;
}); });
LayoutChanges(); LayoutChanges();
SetRevisionFiles(commit.GetFiles(repo)); SetRevisionFiles(commit.GetFiles(repo));
}); });
} }
private void Cleanup(object sender, RoutedEventArgs e) { private void Cleanup(object sender, RoutedEventArgs e) {
fileTree.ItemsSource = null; fileTree.ItemsSource = null;
changeList1.ItemsSource = null; changeList1.ItemsSource = null;
changeList2.ItemsSource = null; changeList2.ItemsSource = null;
displayChanges.Clear(); displayChanges.Clear();
cachedChanges.Clear(); cachedChanges.Clear();
diffViewer.Reset(); diffViewer.Reset();
} }
#endregion #endregion
#region BASE_INFO #region BASE_INFO
private void SetBaseInfo(Git.Commit commit) { private void SetBaseInfo(Git.Commit commit) {
var parentIds = new List<string>(); var parentIds = new List<string>();
foreach (var p in commit.Parents) parentIds.Add(p.Substring(0, 8)); foreach (var p in commit.Parents) parentIds.Add(p.Substring(0, 8));
SHA.Text = commit.SHA; SHA.Text = commit.SHA;
refs.ItemsSource = commit.Decorators; refs.ItemsSource = commit.Decorators;
parents.ItemsSource = parentIds; parents.ItemsSource = parentIds;
author.Text = $"{commit.Author.Name} <{commit.Author.Email}>"; author.Text = $"{commit.Author.Name} <{commit.Author.Email}>";
authorTime.Text = commit.Author.Time; authorTime.Text = commit.Author.Time;
committer.Text = $"{commit.Committer.Name} <{commit.Committer.Email}>"; committer.Text = $"{commit.Committer.Name} <{commit.Committer.Email}>";
committerTime.Text = commit.Committer.Time; committerTime.Text = commit.Committer.Time;
subject.Text = commit.Subject; subject.Text = commit.Subject;
message.Text = commit.Message.Trim(); message.Text = commit.Message.Trim();
if (commit.Decorators.Count == 0) lblRefs.Visibility = Visibility.Collapsed; if (commit.Decorators.Count == 0) lblRefs.Visibility = Visibility.Collapsed;
else lblRefs.Visibility = Visibility.Visible; else lblRefs.Visibility = Visibility.Visible;
if (commit.Committer.Email == commit.Author.Email && commit.Committer.Time == commit.Author.Time) { if (commit.Committer.Email == commit.Author.Email && commit.Committer.Time == commit.Author.Time) {
committerRow.Height = new GridLength(0); committerRow.Height = new GridLength(0);
} else { } else {
committerRow.Height = GridLength.Auto; committerRow.Height = GridLength.Auto;
} }
} }
private void NavigateParent(object sender, RequestNavigateEventArgs e) { private void NavigateParent(object sender, RequestNavigateEventArgs e) {
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString); repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
e.Handled = true; e.Handled = true;
} }
#endregion #endregion
#region CHANGES #region CHANGES
private void LayoutChanges() { private void LayoutChanges() {
displayChanges.Clear(); displayChanges.Clear();
if (string.IsNullOrEmpty(changeFilter)) { if (string.IsNullOrEmpty(changeFilter)) {
displayChanges.AddRange(cachedChanges); displayChanges.AddRange(cachedChanges);
} else { } else {
foreach (var c in cachedChanges) { foreach (var c in cachedChanges) {
if (c.Path.ToUpper().Contains(changeFilter)) displayChanges.Add(c); if (c.Path.ToUpper().Contains(changeFilter)) displayChanges.Add(c);
} }
} }
List<Node> changeTreeSource = new List<Node>(); List<Node> changeTreeSource = new List<Node>();
Dictionary<string, Node> folders = new Dictionary<string, Node>(); Dictionary<string, Node> folders = new Dictionary<string, Node>();
bool isDefaultExpanded = displayChanges.Count < 50; bool isDefaultExpanded = displayChanges.Count < 50;
foreach (var c in displayChanges) { foreach (var c in displayChanges) {
var sepIdx = c.Path.IndexOf('/'); var sepIdx = c.Path.IndexOf('/');
if (sepIdx == -1) { if (sepIdx == -1) {
Node node = new Node(); Node node = new Node();
node.FilePath = c.Path; node.FilePath = c.Path;
node.IsFile = true; node.IsFile = true;
node.Name = c.Path; node.Name = c.Path;
node.Change = c; node.Change = c;
node.IsNodeExpanded = isDefaultExpanded; node.IsNodeExpanded = isDefaultExpanded;
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath; if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
changeTreeSource.Add(node); changeTreeSource.Add(node);
} else { } else {
Node lastFolder = null; Node lastFolder = null;
var start = 0; var start = 0;
while (sepIdx != -1) { while (sepIdx != -1) {
var folder = c.Path.Substring(0, sepIdx); var folder = c.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) { if (folders.ContainsKey(folder)) {
lastFolder = folders[folder]; lastFolder = folders[folder];
} else if (lastFolder == null) { } else if (lastFolder == null) {
lastFolder = new Node(); lastFolder = new Node();
lastFolder.FilePath = folder; lastFolder.FilePath = folder;
lastFolder.Name = folder.Substring(start); lastFolder.Name = folder.Substring(start);
lastFolder.IsNodeExpanded = isDefaultExpanded; lastFolder.IsNodeExpanded = isDefaultExpanded;
changeTreeSource.Add(lastFolder); changeTreeSource.Add(lastFolder);
folders.Add(folder, lastFolder); folders.Add(folder, lastFolder);
} else { } else {
var folderNode = new Node(); var folderNode = new Node();
folderNode.FilePath = folder; folderNode.FilePath = folder;
folderNode.Name = folder.Substring(start); folderNode.Name = folder.Substring(start);
folderNode.IsNodeExpanded = isDefaultExpanded; folderNode.IsNodeExpanded = isDefaultExpanded;
folders.Add(folder, folderNode); folders.Add(folder, folderNode);
lastFolder.Children.Add(folderNode); lastFolder.Children.Add(folderNode);
lastFolder = folderNode; lastFolder = folderNode;
} }
start = sepIdx + 1; start = sepIdx + 1;
sepIdx = c.Path.IndexOf('/', start); sepIdx = c.Path.IndexOf('/', start);
} }
Node node = new Node(); Node node = new Node();
node.FilePath = c.Path; node.FilePath = c.Path;
node.Name = c.Path.Substring(start); node.Name = c.Path.Substring(start);
node.IsFile = true; node.IsFile = true;
node.Change = c; node.Change = c;
if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath; if (c.OriginalPath != null) node.OriginalPath = c.OriginalPath;
lastFolder.Children.Add(node); lastFolder.Children.Add(node);
} }
} }
folders.Clear(); folders.Clear();
SortTreeNodes(changeTreeSource); SortTreeNodes(changeTreeSource);
Dispatcher.Invoke(() => { Dispatcher.Invoke(() => {
changeList2.ItemsSource = null; changeList2.ItemsSource = null;
changeList2.ItemsSource = displayChanges; changeList2.ItemsSource = displayChanges;
changeTree.ItemsSource = changeTreeSource; changeTree.ItemsSource = changeTreeSource;
diffViewer.Reset(); diffViewer.Reset();
}); });
} }
private void SearchChangeFileTextChanged(object sender, TextChangedEventArgs e) { private void SearchChangeFileTextChanged(object sender, TextChangedEventArgs e) {
changeFilter = txtChangeFilter.Text.ToUpper(); changeFilter = txtChangeFilter.Text.ToUpper();
Task.Run(() => LayoutChanges()); Task.Run(() => LayoutChanges());
} }
private async void ChangeTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) { private void ChangeTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
diffViewer.Reset(); diffViewer.Reset();
var node = e.NewValue as Node; var node = e.NewValue as Node;
if (node == null || !node.IsFile) return; if (node == null || !node.IsFile) return;
var start = $"{commit.SHA}^"; var start = $"{commit.SHA}^";
if (commit.Parents.Count == 0) { if (commit.Parents.Count == 0) {
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
} }
List<string> data = new List<string>(); diffViewer.Diff(repo, $"{start} {commit.SHA}", node.FilePath, node.OriginalPath);
}
await Task.Run(() => {
data = repo.Diff(start, commit.SHA, node.FilePath, node.OriginalPath); private void ChangeListSelectionChanged(object sender, SelectionChangedEventArgs e) {
}); if (e.AddedItems.Count != 1) return;
diffViewer.SetData(data, node.FilePath, node.OriginalPath); var change = e.AddedItems[0] as Git.Change;
} if (change == null) return;
private async void ChangeListSelectionChanged(object sender, SelectionChangedEventArgs e) { var start = $"{commit.SHA}^";
if (e.AddedItems.Count != 1) return; if (commit.Parents.Count == 0) {
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
var change = e.AddedItems[0] as Git.Change; }
if (change == null) return;
diffViewer.Diff(repo, $"{start} {commit.SHA}", change.Path, change.OriginalPath);
var start = $"{commit.SHA}^"; }
if (commit.Parents.Count == 0) {
start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; private void ChangeListContextMenuOpening(object sender, ContextMenuEventArgs e) {
} var row = sender as DataGridRow;
if (row == null) return;
List<string> data = new List<string>();
var change = row.DataContext as Git.Change;
await Task.Run(() => { if (change == null) return;
data = repo.Diff(start, commit.SHA, change.Path, change.OriginalPath);
}); var path = change.Path;
var menu = new ContextMenu();
diffViewer.SetData(data, change.Path, change.OriginalPath); if (change.Index != Git.Change.Status.Deleted) {
} MenuItem history = new MenuItem();
history.Header = "File History";
private void ChangeListContextMenuOpening(object sender, ContextMenuEventArgs e) { history.Click += (o, ev) => {
var row = sender as DataGridRow; var viewer = new FileHistories(repo, path);
if (row == null) return; viewer.Show();
};
var change = row.DataContext as Git.Change; menu.Items.Add(history);
if (change == null) return;
MenuItem blame = new MenuItem();
var path = change.Path; blame.Header = "Blame";
var menu = new ContextMenu(); blame.Click += (obj, ev) => {
if (change.Index != Git.Change.Status.Deleted) { Blame viewer = new Blame(repo, path, commit.SHA);
MenuItem history = new MenuItem(); viewer.Show();
history.Header = "File History"; };
history.Click += (o, ev) => { menu.Items.Add(blame);
var viewer = new FileHistories(repo, path);
viewer.Show(); MenuItem explore = new MenuItem();
}; explore.Header = "Reveal in File Explorer";
menu.Items.Add(history); explore.Click += (o, ev) => {
var absPath = Path.GetFullPath(repo.Path + "\\" + path);
MenuItem blame = new MenuItem(); Process.Start("explorer", $"/select,{absPath}");
blame.Header = "Blame"; e.Handled = true;
blame.Click += (obj, ev) => { };
Blame viewer = new Blame(repo, path, commit.SHA); menu.Items.Add(explore);
viewer.Show();
}; MenuItem saveAs = new MenuItem();
menu.Items.Add(blame); saveAs.Header = "Save As ...";
saveAs.Click += (obj, ev) => {
MenuItem explore = new MenuItem(); var dialog = new System.Windows.Forms.FolderBrowserDialog();
explore.Header = "Reveal in File Explorer"; dialog.Description = change.Path;
explore.Click += (o, ev) => { dialog.ShowNewFolderButton = true;
var absPath = Path.GetFullPath(repo.Path + "\\" + path);
Process.Start("explorer", $"/select,{absPath}"); if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
e.Handled = true; var savePath = Path.Combine(dialog.SelectedPath, Path.GetFileName(path));
}; repo.RunAndRedirect($"show {commit.SHA}:\"{path}\"", savePath);
menu.Items.Add(explore); }
};
MenuItem saveAs = new MenuItem(); menu.Items.Add(saveAs);
saveAs.Header = "Save As ..."; }
saveAs.Click += (obj, ev) => {
var dialog = new System.Windows.Forms.FolderBrowserDialog(); MenuItem copyPath = new MenuItem();
dialog.Description = change.Path; copyPath.Header = "Copy Path";
dialog.ShowNewFolderButton = true; copyPath.Click += (obj, ev) => {
Clipboard.SetText(path);
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { };
var savePath = Path.Combine(dialog.SelectedPath, Path.GetFileName(path)); menu.Items.Add(copyPath);
repo.RunAndRedirect($"show {commit.SHA}:\"{path}\"", savePath); menu.IsOpen = true;
} e.Handled = true;
}; }
menu.Items.Add(saveAs); #endregion
}
#region FILES
MenuItem copyPath = new MenuItem(); private void SetRevisionFiles(List<string> files) {
copyPath.Header = "Copy Path"; List<Node> fileTreeSource = new List<Node>();
copyPath.Click += (obj, ev) => { Dictionary<string, Node> folders = new Dictionary<string, Node>();
Clipboard.SetText(path);
}; foreach (var path in files) {
menu.Items.Add(copyPath); var sepIdx = path.IndexOf("/");
menu.IsOpen = true; if (sepIdx == -1) {
e.Handled = true; Node node = new Node();
} node.FilePath = path;
#endregion node.Name = path;
node.IsFile = true;
#region FILES node.IsNodeExpanded = false;
private void SetRevisionFiles(List<string> files) { fileTreeSource.Add(node);
List<Node> fileTreeSource = new List<Node>(); } else {
Dictionary<string, Node> folders = new Dictionary<string, Node>(); Node lastFolder = null;
var start = 0;
foreach (var path in files) {
var sepIdx = path.IndexOf("/"); while (sepIdx != -1) {
if (sepIdx == -1) { var folder = path.Substring(0, sepIdx);
Node node = new Node(); if (folders.ContainsKey(folder)) {
node.FilePath = path; lastFolder = folders[folder];
node.Name = path; } else if (lastFolder == null) {
node.IsFile = true; lastFolder = new Node();
node.IsNodeExpanded = false; lastFolder.FilePath = folder;
fileTreeSource.Add(node); lastFolder.Name = folder.Substring(start);
} else { lastFolder.IsNodeExpanded = false;
Node lastFolder = null; fileTreeSource.Add(lastFolder);
var start = 0; folders.Add(folder, lastFolder);
} else {
while (sepIdx != -1) { var folderNode = new Node();
var folder = path.Substring(0, sepIdx); folderNode.FilePath = folder;
if (folders.ContainsKey(folder)) { folderNode.Name = folder.Substring(start);
lastFolder = folders[folder]; folderNode.IsNodeExpanded = false;
} else if (lastFolder == null) { folders.Add(folder, folderNode);
lastFolder = new Node(); lastFolder.Children.Add(folderNode);
lastFolder.FilePath = folder; lastFolder = folderNode;
lastFolder.Name = folder.Substring(start); }
lastFolder.IsNodeExpanded = false;
fileTreeSource.Add(lastFolder); start = sepIdx + 1;
folders.Add(folder, lastFolder); sepIdx = path.IndexOf('/', start);
} else { }
var folderNode = new Node();
folderNode.FilePath = folder; Node node = new Node();
folderNode.Name = folder.Substring(start); node.FilePath = path;
folderNode.IsNodeExpanded = false; node.Name = path.Substring(start);
folders.Add(folder, folderNode); node.IsFile = true;
lastFolder.Children.Add(folderNode); node.IsNodeExpanded = false;
lastFolder = folderNode; lastFolder.Children.Add(node);
} }
}
start = sepIdx + 1;
sepIdx = path.IndexOf('/', start); folders.Clear();
} SortTreeNodes(fileTreeSource);
Node node = new Node(); Dispatcher.Invoke(() => {
node.FilePath = path; fileTree.ItemsSource = fileTreeSource;
node.Name = path.Substring(start); filePreview.Text = "";
node.IsFile = true; });
node.IsNodeExpanded = false; }
lastFolder.Children.Add(node);
} private async void FileTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
} filePreview.Text = "";
folders.Clear(); var node = e.NewValue as Node;
SortTreeNodes(fileTreeSource); if (node == null || !node.IsFile) return;
Dispatcher.Invoke(() => { await Task.Run(() => {
fileTree.ItemsSource = fileTreeSource; var data = commit.GetTextFileContent(repo, node.FilePath);
filePreview.Text = ""; Dispatcher.Invoke(() => filePreview.Text = data);
}); });
} }
#endregion
private async void FileTreeItemSelected(object sender, RoutedPropertyChangedEventArgs<object> e) {
filePreview.Text = ""; #region TREE_COMMON
private void SortTreeNodes(List<Node> list) {
var node = e.NewValue as Node; list.Sort((l, r) => {
if (node == null || !node.IsFile) return; if (l.IsFile) {
return r.IsFile ? l.Name.CompareTo(r.Name) : 1;
await Task.Run(() => { } else {
var data = commit.GetTextFileContent(repo, node.FilePath); return r.IsFile ? -1 : l.Name.CompareTo(r.Name);
Dispatcher.Invoke(() => filePreview.Text = data); }
}); });
}
#endregion foreach (var sub in list) {
if (sub.Children.Count > 0) SortTreeNodes(sub.Children);
#region TREE_COMMON }
private void SortTreeNodes(List<Node> list) { }
list.Sort((l, r) => {
if (l.IsFile) { private ScrollViewer GetScrollViewer(FrameworkElement owner) {
return r.IsFile ? l.Name.CompareTo(r.Name) : 1; if (owner == null) return null;
} else { if (owner is ScrollViewer) return owner as ScrollViewer;
return r.IsFile ? -1 : l.Name.CompareTo(r.Name);
} int n = VisualTreeHelper.GetChildrenCount(owner);
}); for (int i = 0; i < n; i++) {
var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement;
foreach (var sub in list) { var deep = GetScrollViewer(child);
if (sub.Children.Count > 0) SortTreeNodes(sub.Children); if (deep != null) return deep;
} }
}
return null;
private ScrollViewer GetScrollViewer(FrameworkElement owner) { }
if (owner == null) return null;
if (owner is ScrollViewer) return owner as ScrollViewer; private void TreeMouseWheel(object sender, MouseWheelEventArgs e) {
var scroll = GetScrollViewer(sender as TreeView);
int n = VisualTreeHelper.GetChildrenCount(owner); if (scroll == null) return;
for (int i = 0; i < n; i++) {
var child = VisualTreeHelper.GetChild(owner, i) as FrameworkElement; if (e.Delta > 0) {
var deep = GetScrollViewer(child); scroll.LineUp();
if (deep != null) return deep; } else {
} scroll.LineDown();
}
return null;
} e.Handled = true;
}
private void TreeMouseWheel(object sender, MouseWheelEventArgs e) {
var scroll = GetScrollViewer(sender as TreeView); private void TreeContextMenuOpening(object sender, ContextMenuEventArgs e) {
if (scroll == null) return; var item = sender as TreeViewItem;
if (item == null) return;
if (e.Delta > 0) {
scroll.LineUp(); var node = item.DataContext as Node;
} else { if (node == null || !node.IsFile) return;
scroll.LineDown();
} item.IsSelected = true;
e.Handled = true; ContextMenu menu = new ContextMenu();
} if (node.Change == null || node.Change.Index != Git.Change.Status.Deleted) {
MenuItem history = new MenuItem();
private void TreeContextMenuOpening(object sender, ContextMenuEventArgs e) { history.Header = "File History";
var item = sender as TreeViewItem; history.Click += (o, ev) => {
if (item == null) return; var viewer = new FileHistories(repo, node.FilePath);
viewer.Show();
var node = item.DataContext as Node; };
if (node == null || !node.IsFile) return; menu.Items.Add(history);
item.IsSelected = true; MenuItem blame = new MenuItem();
blame.Header = "Blame";
ContextMenu menu = new ContextMenu(); blame.Click += (obj, ev) => {
if (node.Change == null || node.Change.Index != Git.Change.Status.Deleted) { Blame viewer = new Blame(repo, node.FilePath, commit.SHA);
MenuItem history = new MenuItem(); viewer.Show();
history.Header = "File History"; };
history.Click += (o, ev) => { menu.Items.Add(blame);
var viewer = new FileHistories(repo, node.FilePath);
viewer.Show(); MenuItem explore = new MenuItem();
}; explore.Header = "Reveal in File Explorer";
menu.Items.Add(history); explore.Click += (o, ev) => {
var path = Path.GetFullPath(repo.Path + "\\" + node.FilePath);
MenuItem blame = new MenuItem(); Process.Start("explorer", $"/select,{path}");
blame.Header = "Blame"; e.Handled = true;
blame.Click += (obj, ev) => { };
Blame viewer = new Blame(repo, node.FilePath, commit.SHA); menu.Items.Add(explore);
viewer.Show();
}; MenuItem saveAs = new MenuItem();
menu.Items.Add(blame); saveAs.Header = "Save As ...";
saveAs.Click += (obj, ev) => {
MenuItem explore = new MenuItem(); var dialog = new System.Windows.Forms.FolderBrowserDialog();
explore.Header = "Reveal in File Explorer"; dialog.Description = node.FilePath;
explore.Click += (o, ev) => { dialog.ShowNewFolderButton = true;
var path = Path.GetFullPath(repo.Path + "\\" + node.FilePath);
Process.Start("explorer", $"/select,{path}"); if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
e.Handled = true; var path = Path.Combine(dialog.SelectedPath, node.Name);
}; repo.RunAndRedirect($"show {commit.SHA}:\"{node.FilePath}\"", path);
menu.Items.Add(explore); }
};
MenuItem saveAs = new MenuItem(); menu.Items.Add(saveAs);
saveAs.Header = "Save As ..."; }
saveAs.Click += (obj, ev) => {
var dialog = new System.Windows.Forms.FolderBrowserDialog(); MenuItem copyPath = new MenuItem();
dialog.Description = node.FilePath; copyPath.Header = "Copy Path";
dialog.ShowNewFolderButton = true; copyPath.Click += (obj, ev) => {
Clipboard.SetText(node.FilePath);
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { };
var path = Path.Combine(dialog.SelectedPath, node.Name); menu.Items.Add(copyPath);
repo.RunAndRedirect($"show {commit.SHA}:\"{node.FilePath}\"", path); menu.IsOpen = true;
} e.Handled = true;
}; }
menu.Items.Add(saveAs); #endregion
} }
}
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
}
}

View file

@ -1,139 +1,143 @@
<UserControl x:Class="SourceGit.UI.DiffViewer" <UserControl x:Class="SourceGit.UI.DiffViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" mc:Ignorable="d"
FontFamily="Consolas"> FontFamily="Consolas">
<Border BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}"> <Border BorderThickness="1" BorderBrush="{StaticResource Brush.Border2}">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="0,0,0,1"> <Border Grid.Row="0" BorderBrush="{StaticResource Brush.Border2}" BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,4"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,4">
<StackPanel x:Name="orgFileNamePanel" Orientation="Horizontal"> <StackPanel x:Name="orgFileNamePanel" Orientation="Horizontal">
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/> <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 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}"/> <TextBlock Margin="8,0" VerticalAlignment="Center" Text="→" Foreground="{StaticResource Brush.FG}"/>
</StackPanel> </StackPanel>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/> <Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.File}"/>
<TextBlock x:Name="fileName" Margin="4,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/> <TextBlock x:Name="fileName" Margin="4,0" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG}"/>
</StackPanel> </StackPanel>
</Border> </Border>
<Grid Grid.Row="1" ClipToBounds="True"> <Grid Grid.Row="1" ClipToBounds="True">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="100"/> <ColumnDefinition Width="*" MinWidth="100"/>
<ColumnDefinition Width="2"/> <ColumnDefinition Width="2"/>
<ColumnDefinition Width="*" MinWidth="100"/> <ColumnDefinition Width="*" MinWidth="100"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid Grid.Column="0"> <Grid Grid.Column="0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1"/> <ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBox <TextBox
x:Name="leftLineNumber" x:Name="leftLineNumber"
Grid.Column="0" Grid.Column="0"
AcceptsReturn="True" AcceptsReturn="True"
AcceptsTab="True" AcceptsTab="True"
BorderThickness="0" BorderThickness="0"
Background="Transparent" Background="Transparent"
IsReadOnly="True" IsReadOnly="True"
Margin="4,0,4,0" Margin="4,0,4,0"
FontSize="13" FontSize="13"
HorizontalContentAlignment="Right" HorizontalContentAlignment="Right"
VerticalAlignment="Stretch"/> VerticalAlignment="Stretch"/>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/> <Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox <RichTextBox
x:Name="leftText" x:Name="leftText"
Grid.Column="2" Grid.Column="2"
AcceptsReturn="True" AcceptsReturn="True"
AcceptsTab="True" AcceptsTab="True"
IsReadOnly="True" IsReadOnly="True"
BorderThickness="0" BorderThickness="0"
Background="Transparent" Background="Transparent"
Foreground="{StaticResource Brush.FG}" Foreground="{StaticResource Brush.FG}"
Height="Auto" Height="Auto"
FontSize="13" FontSize="13"
HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled" RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="OnViewerScroll" ScrollViewer.ScrollChanged="OnViewerScroll"
PreviewMouseWheel="OnViewerMouseWheel" PreviewMouseWheel="OnViewerMouseWheel"
SizeChanged="LeftSizeChanged" SizeChanged="LeftSizeChanged"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<RichTextBox.Document> <RichTextBox.Document>
<FlowDocument PageWidth="0"/> <FlowDocument PageWidth="0"/>
</RichTextBox.Document> </RichTextBox.Document>
</RichTextBox> </RichTextBox>
</Grid> </Grid>
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{StaticResource Brush.Border2}"/> <GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{StaticResource Brush.Border2}"/>
<Grid Grid.Column="2"> <Grid Grid.Column="2">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1"/> <ColumnDefinition Width="1"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBox <TextBox
x:Name="rightLineNumber" x:Name="rightLineNumber"
Grid.Column="0" Grid.Column="0"
AcceptsReturn="True" AcceptsReturn="True"
AcceptsTab="True" AcceptsTab="True"
IsReadOnly="True" IsReadOnly="True"
BorderThickness="0" BorderThickness="0"
Background="Transparent" Background="Transparent"
Margin="4,0,4,0" Margin="4,0,4,0"
FontSize="13" FontSize="13"
HorizontalContentAlignment="Right" HorizontalContentAlignment="Right"
VerticalAlignment="Stretch"/> VerticalAlignment="Stretch"/>
<Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/> <Rectangle Grid.Column="1" Width="1" Fill="{StaticResource Brush.Border2}"/>
<RichTextBox <RichTextBox
x:Name="rightText" x:Name="rightText"
Grid.Column="2" Grid.Column="2"
AcceptsReturn="True" AcceptsReturn="True"
AcceptsTab="True" AcceptsTab="True"
IsReadOnly="True" IsReadOnly="True"
BorderThickness="0" BorderThickness="0"
Background="Transparent" Background="Transparent"
Foreground="{StaticResource Brush.FG}" Foreground="{StaticResource Brush.FG}"
Height="Auto" Height="Auto"
FontSize="13" FontSize="13"
HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
RenderOptions.ClearTypeHint="Enabled" RenderOptions.ClearTypeHint="Enabled"
ScrollViewer.ScrollChanged="OnViewerScroll" ScrollViewer.ScrollChanged="OnViewerScroll"
PreviewMouseWheel="OnViewerMouseWheel" PreviewMouseWheel="OnViewerMouseWheel"
SizeChanged="RightSizeChanged" SizeChanged="RightSizeChanged"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<RichTextBox.Document> <RichTextBox.Document>
<FlowDocument PageWidth="0"/> <FlowDocument PageWidth="0"/>
</RichTextBox.Document> </RichTextBox.Document>
</RichTextBox> </RichTextBox>
</Grid> </Grid>
</Grid> </Grid>
<Border x:Name="mask" Grid.RowSpan="2" Background="{StaticResource Brush.BG3}" Visibility="Collapsed"> <Border x:Name="mask" Grid.RowSpan="2" Background="{StaticResource Brush.BG3}" Visibility="Collapsed">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2"> <StackPanel Orientation="Vertical" VerticalAlignment="Center" Opacity=".2">
<Path Width="64" Height="64" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Diff}"/> <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"/> <Label Margin="0,8,0,0" Content="SELECT FILE TO VIEW CHANGES" FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center"/>
</StackPanel> </StackPanel>
</Border> </Border>
</Grid>
</Border> <Border x:Name="loading" Grid.RowSpan="2" Background="{StaticResource Brush.BG3}" Visibility="Collapsed">
</UserControl> <Label Content="LOADING ..." FontSize="18" FontWeight="UltraBold" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="{StaticResource Brush.FG2}"/>
</Border>
</Grid>
</Border>
</UserControl>

View file

@ -1,294 +1,238 @@
using System; using System;
using System.Collections.Generic; using System.Globalization;
using System.Globalization; using System.Threading.Tasks;
using System.Text.RegularExpressions; using System.Windows;
using System.Windows; using System.Windows.Controls;
using System.Windows.Controls; using System.Windows.Documents;
using System.Windows.Documents; using System.Windows.Input;
using System.Windows.Input; using System.Windows.Media;
using System.Windows.Media;
namespace SourceGit.UI {
namespace SourceGit.UI {
/// <summary>
/// <summary> /// Viewer for git diff
/// Viewer for git diff /// </summary>
/// </summary> public partial class DiffViewer : UserControl {
public partial class DiffViewer : UserControl { private double minWidth = 0;
private double minWidth = 0;
/// <summary>
/// <summary> /// Constructor
/// Line mode. /// </summary>
/// </summary> public DiffViewer() {
public enum LineMode { InitializeComponent();
Normal, Reset();
Indicator, }
Empty,
Added, /// <summary>
Deleted, /// Reset data.
} /// </summary>
public void Reset() {
/// <summary> mask.Visibility = Visibility.Visible;
/// Constructor }
/// </summary>
public DiffViewer() { /// <summary>
InitializeComponent(); /// Diff with options.
Reset(); /// </summary>
} /// <param name="repo"></param>
/// <param name="options"></param>
/// <summary> /// <param name="path"></param>
/// /// <param name="orgPath"></param>
/// </summary> public void Diff(Git.Repository repo, string options, string path, string orgPath = null) {
/// <param name="lines"></param> SetTitle(path, orgPath);
/// <param name="file"></param> Task.Run(() => {
/// <param name="orgFile"></param> var args = $"{options} -- ";
public void SetData(List<string> lines, string file, string orgFile = null) { if (!string.IsNullOrEmpty(orgPath)) args += $"{orgPath} ";
minWidth = Math.Max(leftText.ActualWidth, rightText.ActualWidth) - 16; args += $"\"{path}\"";
fileName.Text = file; var rs = Git.Diff.Run(repo, args);
if (!string.IsNullOrEmpty(orgFile)) { SetData(rs);
orgFileNamePanel.Visibility = Visibility.Visible; });
orgFileName.Text = orgFile; }
} else {
orgFileNamePanel.Visibility = Visibility.Collapsed; #region LAYOUT
} /// <summary>
/// Show diff title
leftText.Document.Blocks.Clear(); /// </summary>
rightText.Document.Blocks.Clear(); /// <param name="file"></param>
/// <param name="orgFile"></param>
leftLineNumber.Text = ""; private void SetTitle(string file, string orgFile) {
rightLineNumber.Text = ""; fileName.Text = file;
if (!string.IsNullOrEmpty(orgFile)) {
Regex regex = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@", RegexOptions.None); orgFileNamePanel.Visibility = Visibility.Visible;
bool started = false; orgFileName.Text = orgFile;
} else {
List<Paragraph> leftData = new List<Paragraph>(); orgFileNamePanel.Visibility = Visibility.Collapsed;
List<Paragraph> rightData = new List<Paragraph>(); }
List<string> leftNumbers = new List<string>(); }
List<string> rightNumbers = new List<string>();
/// <summary>
int leftLine = 0; /// Show diff content.
int rightLine = 0; /// </summary>
bool bLastLeft = true; /// <param name="rs"></param>
private void SetData(Git.Diff.Result rs) {
foreach (var line in lines) { Dispatcher.Invoke(() => {
if (!started) { loading.Visibility = Visibility.Collapsed;
var match = regex.Match(line); mask.Visibility = Visibility.Collapsed;
if (!match.Success) continue;
minWidth = Math.Max(leftText.ActualWidth, rightText.ActualWidth) - 16;
MakeParagraph(leftData, line, LineMode.Indicator);
MakeParagraph(rightData, line, LineMode.Indicator); leftLineNumber.Text = "";
leftNumbers.Add(""); rightLineNumber.Text = "";
rightNumbers.Add(""); leftText.Document.Blocks.Clear();
rightText.Document.Blocks.Clear();
leftLine = int.Parse(match.Groups[1].Value);
rightLine = int.Parse(match.Groups[2].Value); foreach (var b in rs.Blocks) ShowBlock(b);
started = true;
continue; leftText.Document.PageWidth = minWidth + 16;
} rightText.Document.PageWidth = minWidth + 16;
leftText.ScrollToHome();
if (line[0] == '-') { });
MakeParagraph(leftData, line.Substring(1), LineMode.Deleted); }
leftNumbers.Add(leftLine.ToString());
leftLine++; /// <summary>
bLastLeft = true; /// Make paragraph.
} else if (line[0] == '+') { /// </summary>
MakeParagraph(rightData, line.Substring(1), LineMode.Added); /// <param name="b"></param>
rightNumbers.Add(rightLine.ToString()); private void ShowBlock(Git.Diff.Block b) {
rightLine++; var content = b.Builder.ToString();
bLastLeft = false;
} else if (line[0] == '\\') { Paragraph p = new Paragraph(new Run(content));
if (bLastLeft) { p.Margin = new Thickness(0);
MakeParagraph(leftData, line.Substring(1), LineMode.Indicator); p.Padding = new Thickness();
leftNumbers.Add(""); p.LineHeight = 1;
} else { p.Background = Brushes.Transparent;
MakeParagraph(rightData, line.Substring(1), LineMode.Indicator); p.Foreground = FindResource("Brush.FG") as SolidColorBrush;
rightNumbers.Add(""); p.FontStyle = FontStyles.Normal;
}
} else { switch (b.Mode) {
FitBothSide(leftData, leftNumbers, rightData, rightNumbers); case Git.Diff.LineMode.Normal:
bLastLeft = true; break;
case Git.Diff.LineMode.Indicator:
var match = regex.Match(line); p.Foreground = Brushes.Gray;
if (match.Success) { p.FontStyle = FontStyles.Italic;
MakeParagraph(leftData, line, LineMode.Indicator); break;
MakeParagraph(rightData, line, LineMode.Indicator); case Git.Diff.LineMode.Empty:
leftNumbers.Add(""); p.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
rightNumbers.Add(""); break;
case Git.Diff.LineMode.Added:
leftLine = int.Parse(match.Groups[1].Value); p.Background = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0));
rightLine = int.Parse(match.Groups[2].Value); break;
} else { case Git.Diff.LineMode.Deleted:
var data = line.Substring(1); p.Background = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0));
MakeParagraph(leftData, data, LineMode.Normal); break;
MakeParagraph(rightData, data, LineMode.Normal); }
leftNumbers.Add(leftLine.ToString());
rightNumbers.Add(rightLine.ToString()); var formatter = new FormattedText(
leftLine++; content,
rightLine++; CultureInfo.CurrentUICulture,
} FlowDirection.LeftToRight,
} new Typeface(leftText.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch),
} leftText.FontSize,
Brushes.Black,
FitBothSide(leftData, leftNumbers, rightData, rightNumbers); new NumberSubstitution(),
TextFormattingMode.Ideal);
if (leftData.Count == 0) {
MakeParagraph(leftData, "NOT SUPPORTED OR NO DATA", LineMode.Indicator); if (minWidth < formatter.Width) minWidth = formatter.Width;
MakeParagraph(rightData, "NOT SUPPORTED OR NO DATA", LineMode.Indicator);
leftNumbers.Add(""); switch (b.Side) {
rightNumbers.Add(""); case Git.Diff.Side.Left:
} leftText.Document.Blocks.Add(p);
for (int i = 0; i < b.Count; i++) {
leftLineNumber.Text = string.Join("\n", leftNumbers); if (b.CanShowNumber) leftLineNumber.AppendText($"{i + b.LeftStart}\n");
rightLineNumber.Text = string.Join("\n", rightNumbers); else leftLineNumber.AppendText("\n");
leftText.Document.PageWidth = minWidth + 16; }
rightText.Document.PageWidth = minWidth + 16; break;
leftText.Document.Blocks.AddRange(leftData); case Git.Diff.Side.Right:
rightText.Document.Blocks.AddRange(rightData); rightText.Document.Blocks.Add(p);
leftText.ScrollToHome(); for (int i = 0; i < b.Count; i++) {
if (b.CanShowNumber) rightLineNumber.AppendText($"{i + b.RightStart}\n");
mask.Visibility = Visibility.Collapsed; else rightLineNumber.AppendText("\n");
} }
break;
/// <summary> default:
/// Reset data. leftText.Document.Blocks.Add(p);
/// </summary>
public void Reset() { var cp = new Paragraph(new Run(content));
mask.Visibility = Visibility.Visible; cp.Margin = new Thickness(0);
} cp.Padding = new Thickness();
cp.LineHeight = 1;
/// <summary> cp.Background = p.Background;
/// Make paragraph. cp.Foreground = p.Foreground;
/// </summary> cp.FontStyle = p.FontStyle;
/// <param name="collection"></param> rightText.Document.Blocks.Add(cp);
/// <param name="content"></param>
/// <param name="mode"></param> for (int i = 0; i < b.Count; i++) {
private void MakeParagraph(List<Paragraph> collection, string content, LineMode mode) { if (b.Mode != Git.Diff.LineMode.Indicator) {
Paragraph p = new Paragraph(new Run(content)); leftLineNumber.AppendText($"{i + b.LeftStart}\n");
p.Margin = new Thickness(0); rightLineNumber.AppendText($"{i + b.RightStart}\n");
p.Padding = new Thickness(); } else {
p.LineHeight = 1; leftLineNumber.AppendText("\n");
p.Background = Brushes.Transparent; rightLineNumber.AppendText("\n");
p.Foreground = FindResource("Brush.FG") as SolidColorBrush; }
p.FontStyle = FontStyles.Normal; }
break;
switch (mode) { }
case LineMode.Normal: }
break; #endregion
case LineMode.Indicator:
p.Foreground = Brushes.Gray; #region EVENTS
p.FontStyle = FontStyles.Italic; /// <summary>
break; /// Sync scroll both sides.
case LineMode.Empty: /// </summary>
p.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0)); /// <param name="sender"></param>
break; /// <param name="e"></param>
case LineMode.Added: private void OnViewerScroll(object sender, ScrollChangedEventArgs e) {
p.Background = new SolidColorBrush(Color.FromArgb(60, 0, 255, 0)); if (e.VerticalChange != 0) {
break; if (leftText.VerticalOffset != e.VerticalOffset) {
case LineMode.Deleted: leftText.ScrollToVerticalOffset(e.VerticalOffset);
p.Background = new SolidColorBrush(Color.FromArgb(60, 255, 0, 0)); }
break;
} if (rightText.VerticalOffset != e.VerticalOffset) {
rightText.ScrollToVerticalOffset(e.VerticalOffset);
var formatter = new FormattedText( }
content,
CultureInfo.CurrentUICulture, leftLineNumber.Margin = new Thickness(4, -e.VerticalOffset, 4, 0);
FlowDirection.LeftToRight, rightLineNumber.Margin = new Thickness(4, -e.VerticalOffset, 4, 0);
new Typeface(leftText.FontFamily, p.FontStyle, p.FontWeight, p.FontStretch), } else {
leftText.FontSize, if (leftText.HorizontalOffset != e.HorizontalOffset) {
Brushes.Black, leftText.ScrollToHorizontalOffset(e.HorizontalOffset);
new NumberSubstitution(), }
TextFormattingMode.Ideal);
if (rightText.HorizontalOffset != e.HorizontalOffset) {
if (minWidth < formatter.Width) minWidth = formatter.Width; rightText.ScrollToHorizontalOffset(e.HorizontalOffset);
collection.Add(p); }
} }
}
/// <summary>
/// Fit both side with empty lines. /// <summary>
/// </summary> /// Scroll using mouse wheel.
/// <param name="left"></param> /// </summary>
/// <param name="leftNumbers"></param> /// <param name="sender"></param>
/// <param name="right"></param> /// <param name="e"></param>
/// <param name="rightNumbers"></param> private void OnViewerMouseWheel(object sender, MouseWheelEventArgs e) {
private void FitBothSide(List<Paragraph> left, List<string> leftNumbers, List<Paragraph> right, List<string> rightNumbers) { var text = sender as RichTextBox;
int leftCount = left.Count; if (text == null) return;
int rightCount = right.Count;
int diff = 0; if (e.Delta > 0) {
List<Paragraph> fitContent = null; text.LineUp();
List<string> fitNumber = null; } else {
text.LineDown();
if (leftCount > rightCount) { }
diff = leftCount - rightCount;
fitContent = right; e.Handled = true;
fitNumber = rightNumbers; }
} else if (rightCount > leftCount) {
diff = rightCount - leftCount; private void LeftSizeChanged(object sender, SizeChangedEventArgs e) {
fitContent = left; if (leftText.Document.PageWidth < leftText.ActualWidth) {
fitNumber = leftNumbers; leftText.Document.PageWidth = leftText.ActualWidth;
} }
}
for (int i = 0; i < diff; i++) {
MakeParagraph(fitContent, "", LineMode.Empty); private void RightSizeChanged(object sender, SizeChangedEventArgs e) {
fitNumber.Add(""); if (rightText.Document.PageWidth < rightText.ActualWidth) {
} rightText.Document.PageWidth = rightText.ActualWidth;
} }
}
/// <summary> #endregion
/// 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;
}
}
}
}

View file

@ -1,122 +1,118 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Animation; using System.Windows.Media.Animation;
using System.Windows.Navigation; using System.Windows.Navigation;
namespace SourceGit.UI { namespace SourceGit.UI {
/// <summary> /// <summary>
/// File histories panel. /// File histories panel.
/// </summary> /// </summary>
public partial class FileHistories : Window { public partial class FileHistories : Window {
private Git.Repository repo = null; private Git.Repository repo = null;
private string file = null; private string file = null;
/// <summary> /// <summary>
/// Constructor. /// Constructor.
/// </summary> /// </summary>
/// <param name="repo"></param> /// <param name="repo"></param>
/// <param name="file"></param> /// <param name="file"></param>
public FileHistories(Git.Repository repo, string file) { public FileHistories(Git.Repository repo, string file) {
this.repo = repo; this.repo = repo;
this.file = file; this.file = file;
InitializeComponent(); InitializeComponent();
// Move to center // Move to center
var parent = App.Current.MainWindow; var parent = App.Current.MainWindow;
Left = parent.Left + (parent.Width - Width) * 0.5; Left = parent.Left + (parent.Width - Width) * 0.5;
Top = parent.Top + (parent.Height - Height) * 0.5; Top = parent.Top + (parent.Height - Height) * 0.5;
// Show loading // Show loading
DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1)); DoubleAnimation anim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(1));
anim.RepeatBehavior = RepeatBehavior.Forever; anim.RepeatBehavior = RepeatBehavior.Forever;
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim); loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
loading.Visibility = Visibility.Visible; loading.Visibility = Visibility.Visible;
// Load commits // Load commits
Task.Run(() => { Task.Run(() => {
var commits = repo.Commits($"-n 10000 -- \"{file}\""); var commits = repo.Commits($"-n 10000 -- \"{file}\"");
Dispatcher.Invoke(() => { Dispatcher.Invoke(() => {
commitList.ItemsSource = commits; commitList.ItemsSource = commits;
commitList.SelectedIndex = 0; commitList.SelectedIndex = 0;
loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null); loading.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, null);
loading.Visibility = Visibility.Collapsed; loading.Visibility = Visibility.Collapsed;
}); });
}); });
} }
/// <summary> /// <summary>
/// Logo click /// Logo click
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) { private void LogoMouseButtonDown(object sender, MouseButtonEventArgs e) {
var element = e.OriginalSource as FrameworkElement; var element = e.OriginalSource as FrameworkElement;
if (element == null) return; if (element == null) return;
var pos = PointToScreen(new Point(0, 33)); var pos = PointToScreen(new Point(0, 33));
SystemCommands.ShowSystemMenu(this, pos); SystemCommands.ShowSystemMenu(this, pos);
} }
/// <summary> /// <summary>
/// Minimize /// Minimize
/// </summary> /// </summary>
private void Minimize(object sender, RoutedEventArgs e) { private void Minimize(object sender, RoutedEventArgs e) {
SystemCommands.MinimizeWindow(this); SystemCommands.MinimizeWindow(this);
} }
/// <summary> /// <summary>
/// Maximize/Restore /// Maximize/Restore
/// </summary> /// </summary>
private void MaximizeOrRestore(object sender, RoutedEventArgs e) { private void MaximizeOrRestore(object sender, RoutedEventArgs e) {
if (WindowState == WindowState.Normal) { if (WindowState == WindowState.Normal) {
SystemCommands.MaximizeWindow(this); SystemCommands.MaximizeWindow(this);
} else { } else {
SystemCommands.RestoreWindow(this); SystemCommands.RestoreWindow(this);
} }
} }
/// <summary> /// <summary>
/// Quit /// Quit
/// </summary> /// </summary>
private void Quit(object sender, RoutedEventArgs e) { private void Quit(object sender, RoutedEventArgs e) {
Close(); Close();
} }
/// <summary> /// <summary>
/// Commit selection change event. /// Commit selection change event.
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private async void CommitSelectionChanged(object sender, SelectionChangedEventArgs e) { private void CommitSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (e.AddedItems.Count != 1) return; if (e.AddedItems.Count != 1) return;
var commit = e.AddedItems[0] as Git.Commit; var commit = e.AddedItems[0] as Git.Commit;
var start = $"{commit.SHA}^"; var start = $"{commit.SHA}^";
if (commit.Parents.Count == 0) start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; if (commit.Parents.Count == 0) start = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
List<string> data = new List<string>(); diff.Diff(repo, $"{start} {commit.SHA}", file);
await Task.Run(() => { }
data = repo.Diff(start, commit.SHA, file);
}); /// <summary>
diff.SetData(data, $"{file} @ {commit.ShortSHA}"); /// Navigate to given string
} /// </summary>
/// <param name="sender"></param>
/// <summary> /// <param name="e"></param>
/// Navigate to given string private void NavigateToCommit(object sender, RequestNavigateEventArgs e) {
/// </summary> repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString);
/// <param name="sender"></param> e.Handled = true;
/// <param name="e"></param> }
private void NavigateToCommit(object sender, RequestNavigateEventArgs e) { }
repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString); }
e.Handled = true;
}
}
}

View file

@ -1,118 +1,117 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace SourceGit.UI { namespace SourceGit.UI {
/// <summary> /// <summary>
/// Stashes viewer. /// Stashes viewer.
/// </summary> /// </summary>
public partial class Stashes : UserControl { public partial class Stashes : UserControl {
private Git.Repository repo = null; private Git.Repository repo = null;
private string selectedStash = null; private string selectedStash = null;
/// <summary> /// <summary>
/// File tree node. /// File tree node.
/// </summary> /// </summary>
public class Node { public class Node {
public string FilePath { get; set; } = ""; public string FilePath { get; set; } = "";
public string OriginalPath { get; set; } = ""; public string OriginalPath { get; set; } = "";
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public bool IsFile { get; set; } = false; public bool IsFile { get; set; } = false;
public bool IsNodeExpanded { get; set; } = true; public bool IsNodeExpanded { get; set; } = true;
public Git.Change.Status Status { get; set; } = Git.Change.Status.None; public Git.Change.Status Status { get; set; } = Git.Change.Status.None;
public List<Node> Children { get; set; } = new List<Node>(); public List<Node> Children { get; set; } = new List<Node>();
} }
/// <summary> /// <summary>
/// Constructor. /// Constructor.
/// </summary> /// </summary>
public Stashes() { public Stashes() {
InitializeComponent(); InitializeComponent();
} }
/// <summary> /// <summary>
/// Cleanup /// Cleanup
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private void Cleanup(object sender, RoutedEventArgs e) { private void Cleanup(object sender, RoutedEventArgs e) {
stashList.ItemsSource = null; stashList.ItemsSource = null;
changeList.ItemsSource = null; changeList.ItemsSource = null;
diff.Reset(); diff.Reset();
} }
/// <summary> /// <summary>
/// Set data. /// Set data.
/// </summary> /// </summary>
/// <param name="opened"></param> /// <param name="opened"></param>
/// <param name="stashes"></param> /// <param name="stashes"></param>
public void SetData(Git.Repository opened, List<Git.Stash> stashes) { public void SetData(Git.Repository opened, List<Git.Stash> stashes) {
repo = opened; repo = opened;
selectedStash = null; selectedStash = null;
stashList.ItemsSource = stashes; stashList.ItemsSource = stashes;
changeList.ItemsSource = null; changeList.ItemsSource = null;
diff.Reset(); diff.Reset();
} }
/// <summary> /// <summary>
/// Stash list selection changed event. /// Stash list selection changed event.
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private void StashSelectionChanged(object sender, SelectionChangedEventArgs e) { private void StashSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (e.AddedItems.Count != 1) return; if (e.AddedItems.Count != 1) return;
var stash = e.AddedItems[0] as Git.Stash; var stash = e.AddedItems[0] as Git.Stash;
if (stash == null) return; if (stash == null) return;
selectedStash = stash.SHA; selectedStash = stash.SHA;
diff.Reset(); diff.Reset();
changeList.ItemsSource = stash.GetChanges(repo); changeList.ItemsSource = stash.GetChanges(repo);
} }
/// <summary> /// <summary>
/// File selection changed in TreeView. /// File selection changed in TreeView.
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>
private void FileSelectionChanged(object sender, SelectionChangedEventArgs e) { private void FileSelectionChanged(object sender, SelectionChangedEventArgs e) {
if (e.AddedItems.Count != 1) return; if (e.AddedItems.Count != 1) return;
var change = e.AddedItems[0] as Git.Change; var change = e.AddedItems[0] as Git.Change;
if (change == null) return; if (change == null) return;
var data = repo.Diff($"{selectedStash}^", selectedStash, change.Path, change.OriginalPath); diff.Diff(repo, $"{selectedStash}^ {selectedStash}", change.Path, change.OriginalPath);
diff.SetData(data, change.Path, change.OriginalPath); }
}
/// <summary>
/// <summary> /// Stash context menu.
/// Stash context menu. /// </summary>
/// </summary> /// <param name="sender"></param>
/// <param name="sender"></param> /// <param name="ev"></param>
/// <param name="ev"></param> private void StashContextMenuOpening(object sender, ContextMenuEventArgs ev) {
private void StashContextMenuOpening(object sender, ContextMenuEventArgs ev) { var stash = (sender as ListViewItem).DataContext as Git.Stash;
var stash = (sender as ListViewItem).DataContext as Git.Stash; if (stash == null) return;
if (stash == null) return;
var apply = new MenuItem();
var apply = new MenuItem(); apply.Header = "Apply";
apply.Header = "Apply"; apply.Click += (o, e) => stash.Apply(repo);
apply.Click += (o, e) => stash.Apply(repo);
var pop = new MenuItem();
var pop = new MenuItem(); pop.Header = "Pop";
pop.Header = "Pop"; pop.Click += (o, e) => stash.Pop(repo);
pop.Click += (o, e) => stash.Pop(repo);
var delete = new MenuItem();
var delete = new MenuItem(); delete.Header = "Drop";
delete.Header = "Drop"; delete.Click += (o, e) => stash.Drop(repo);
delete.Click += (o, e) => stash.Drop(repo);
var menu = new ContextMenu();
var menu = new ContextMenu(); menu.Items.Add(apply);
menu.Items.Add(apply); menu.Items.Add(pop);
menu.Items.Add(pop); menu.Items.Add(delete);
menu.Items.Add(delete); menu.IsOpen = true;
menu.IsOpen = true; ev.Handled = true;
ev.Handled = true; }
} }
} }
}

File diff suppressed because it is too large Load diff