From 04ca0a9236cc05f97003f245f1bf60d38bd51946 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 9 Jul 2020 17:36:43 +0800 Subject: [PATCH] Rewrite diff viewer --- SourceGit/Git/Diff.cs | 233 ++++ SourceGit/Git/Repository.cs | 20 - SourceGit/UI/CommitViewer.xaml.cs | 924 ++++++------- SourceGit/UI/DiffViewer.xaml | 282 ++-- SourceGit/UI/DiffViewer.xaml.cs | 532 ++++--- SourceGit/UI/FileHistories.xaml.cs | 240 ++-- SourceGit/UI/Stashes.xaml.cs | 235 ++-- SourceGit/UI/WorkingCopy.xaml.cs | 2070 ++++++++++++++-------------- 8 files changed, 2336 insertions(+), 2200 deletions(-) create mode 100644 SourceGit/Git/Diff.cs diff --git a/SourceGit/Git/Diff.cs b/SourceGit/Git/Diff.cs new file mode 100644 index 00000000..b53b2a83 --- /dev/null +++ b/SourceGit/Git/Diff.cs @@ -0,0 +1,233 @@ +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace SourceGit.Git { + + /// + /// Diff helper. + /// + public class Diff { + private static readonly Regex REG_INDICATOR = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@", RegexOptions.None); + + /// + /// Line mode. + /// + public enum LineMode { + Normal, + Indicator, + Empty, + Added, + Deleted, + } + + /// + /// Side + /// + public enum Side { + Left, + Right, + Both, + } + + /// + /// Block + /// + 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++; + } + } + + /// + /// Diff result. + /// + public class Result { + public bool IsValid = false; + public bool IsBinary = false; + public List Blocks = new List(); + 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); + } + } + } + + /// + /// Run diff process. + /// + /// + /// + /// + 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; + } + } +} diff --git a/SourceGit/Git/Repository.cs b/SourceGit/Git/Repository.cs index 77d594dc..9a0b0383 100644 --- a/SourceGit/Git/Repository.cs +++ b/SourceGit/Git/Repository.cs @@ -827,26 +827,6 @@ namespace SourceGit.Git { return stashes; } - /// - /// Diff - /// - /// - /// - /// - /// - /// - public List 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(); - var errs = RunCommand(args, line => data.Add(line)); - - if (errs != null) App.RaiseError(errs); - return data; - } - /// /// Blame file. /// diff --git a/SourceGit/UI/CommitViewer.xaml.cs b/SourceGit/UI/CommitViewer.xaml.cs index ab350a12..916bcfad 100644 --- a/SourceGit/UI/CommitViewer.xaml.cs +++ b/SourceGit/UI/CommitViewer.xaml.cs @@ -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 { - - /// - /// Commit detail viewer - /// - public partial class CommitViewer : UserControl { - private Git.Repository repo = null; - private Git.Commit commit = null; - private List cachedChanges = new List(); - private List displayChanges = new List(); - private string changeFilter = null; - - /// - /// Node for file tree. - /// - 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 Children { get; set; } = new List(); - } - - /// - /// Constructor. - /// - 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(); - 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 changeTreeSource = new List(); - Dictionary folders = new Dictionary(); - 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 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 data = new List(); - - 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 data = new List(); - - 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 files) { - List fileTreeSource = new List(); - Dictionary folders = new Dictionary(); - - 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 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 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 { + + /// + /// Commit detail viewer + /// + public partial class CommitViewer : UserControl { + private Git.Repository repo = null; + private Git.Commit commit = null; + private List cachedChanges = new List(); + private List displayChanges = new List(); + private string changeFilter = null; + + /// + /// Node for file tree. + /// + 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 Children { get; set; } = new List(); + } + + /// + /// Constructor. + /// + 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(); + 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 changeTreeSource = new List(); + Dictionary folders = new Dictionary(); + 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 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 files) { + List fileTreeSource = new List(); + Dictionary folders = new Dictionary(); + + 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 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 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 + } +} diff --git a/SourceGit/UI/DiffViewer.xaml b/SourceGit/UI/DiffViewer.xaml index 22bbf644..e029ae3d 100644 --- a/SourceGit/UI/DiffViewer.xaml +++ b/SourceGit/UI/DiffViewer.xaml @@ -1,139 +1,143 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SourceGit/UI/DiffViewer.xaml.cs b/SourceGit/UI/DiffViewer.xaml.cs index e573b4f5..4e6145a4 100644 --- a/SourceGit/UI/DiffViewer.xaml.cs +++ b/SourceGit/UI/DiffViewer.xaml.cs @@ -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 { - - /// - /// Viewer for git diff - /// - public partial class DiffViewer : UserControl { - private double minWidth = 0; - - /// - /// Line mode. - /// - public enum LineMode { - Normal, - Indicator, - Empty, - Added, - Deleted, - } - - /// - /// Constructor - /// - public DiffViewer() { - InitializeComponent(); - Reset(); - } - - /// - /// - /// - /// - /// - /// - public void SetData(List 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 leftData = new List(); - List rightData = new List(); - List leftNumbers = new List(); - List rightNumbers = new List(); - - 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; - } - - /// - /// Reset data. - /// - public void Reset() { - mask.Visibility = Visibility.Visible; - } - - /// - /// Make paragraph. - /// - /// - /// - /// - private void MakeParagraph(List 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); - } - - /// - /// Fit both side with empty lines. - /// - /// - /// - /// - /// - private void FitBothSide(List left, List leftNumbers, List right, List rightNumbers) { - int leftCount = left.Count; - int rightCount = right.Count; - int diff = 0; - List fitContent = null; - List 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(""); - } - } - - /// - /// Sync scroll both sides. - /// - /// - /// - 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); - } - } - } - - /// - /// Scroll using mouse wheel. - /// - /// - /// - 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 { + + /// + /// Viewer for git diff + /// + public partial class DiffViewer : UserControl { + private double minWidth = 0; + + /// + /// Constructor + /// + public DiffViewer() { + InitializeComponent(); + Reset(); + } + + /// + /// Reset data. + /// + public void Reset() { + mask.Visibility = Visibility.Visible; + } + + /// + /// Diff with options. + /// + /// + /// + /// + /// + 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 + /// + /// Show diff title + /// + /// + /// + 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; + } + } + + /// + /// Show diff content. + /// + /// + 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(); + }); + } + + /// + /// Make paragraph. + /// + /// + 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 + /// + /// Sync scroll both sides. + /// + /// + /// + 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); + } + } + } + + /// + /// Scroll using mouse wheel. + /// + /// + /// + 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 + } +} diff --git a/SourceGit/UI/FileHistories.xaml.cs b/SourceGit/UI/FileHistories.xaml.cs index 47e6e76d..22ecb8b1 100644 --- a/SourceGit/UI/FileHistories.xaml.cs +++ b/SourceGit/UI/FileHistories.xaml.cs @@ -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 { - - /// - /// File histories panel. - /// - public partial class FileHistories : Window { - private Git.Repository repo = null; - private string file = null; - - /// - /// Constructor. - /// - /// - /// - 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; - }); - }); - } - - /// - /// Logo click - /// - /// - /// - 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); - } - - /// - /// Minimize - /// - private void Minimize(object sender, RoutedEventArgs e) { - SystemCommands.MinimizeWindow(this); - } - - /// - /// Maximize/Restore - /// - private void MaximizeOrRestore(object sender, RoutedEventArgs e) { - if (WindowState == WindowState.Normal) { - SystemCommands.MaximizeWindow(this); - } else { - SystemCommands.RestoreWindow(this); - } - } - - /// - /// Quit - /// - private void Quit(object sender, RoutedEventArgs e) { - Close(); - } - - /// - /// Commit selection change event. - /// - /// - /// - 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 data = new List(); - await Task.Run(() => { - data = repo.Diff(start, commit.SHA, file); - }); - diff.SetData(data, $"{file} @ {commit.ShortSHA}"); - } - - /// - /// Navigate to given string - /// - /// - /// - 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 { + + /// + /// File histories panel. + /// + public partial class FileHistories : Window { + private Git.Repository repo = null; + private string file = null; + + /// + /// Constructor. + /// + /// + /// + 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; + }); + }); + } + + /// + /// Logo click + /// + /// + /// + 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); + } + + /// + /// Minimize + /// + private void Minimize(object sender, RoutedEventArgs e) { + SystemCommands.MinimizeWindow(this); + } + + /// + /// Maximize/Restore + /// + private void MaximizeOrRestore(object sender, RoutedEventArgs e) { + if (WindowState == WindowState.Normal) { + SystemCommands.MaximizeWindow(this); + } else { + SystemCommands.RestoreWindow(this); + } + } + + /// + /// Quit + /// + private void Quit(object sender, RoutedEventArgs e) { + Close(); + } + + /// + /// Commit selection change event. + /// + /// + /// + 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); + } + + /// + /// Navigate to given string + /// + /// + /// + private void NavigateToCommit(object sender, RequestNavigateEventArgs e) { + repo.OnNavigateCommit?.Invoke(e.Uri.OriginalString); + e.Handled = true; + } + } +} diff --git a/SourceGit/UI/Stashes.xaml.cs b/SourceGit/UI/Stashes.xaml.cs index 2b982976..7051fafe 100644 --- a/SourceGit/UI/Stashes.xaml.cs +++ b/SourceGit/UI/Stashes.xaml.cs @@ -1,118 +1,117 @@ -using System.Collections.Generic; -using System.Windows; -using System.Windows.Controls; - -namespace SourceGit.UI { - - /// - /// Stashes viewer. - /// - public partial class Stashes : UserControl { - private Git.Repository repo = null; - private string selectedStash = null; - - /// - /// File tree node. - /// - 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 Children { get; set; } = new List(); - } - - /// - /// Constructor. - /// - public Stashes() { - InitializeComponent(); - } - - /// - /// Cleanup - /// - /// - /// - private void Cleanup(object sender, RoutedEventArgs e) { - stashList.ItemsSource = null; - changeList.ItemsSource = null; - diff.Reset(); - } - - /// - /// Set data. - /// - /// - /// - public void SetData(Git.Repository opened, List stashes) { - repo = opened; - selectedStash = null; - stashList.ItemsSource = stashes; - changeList.ItemsSource = null; - diff.Reset(); - } - - /// - /// Stash list selection changed event. - /// - /// - /// - 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); - } - - /// - /// File selection changed in TreeView. - /// - /// - /// - 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); - } - - /// - /// Stash context menu. - /// - /// - /// - 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 { + + /// + /// Stashes viewer. + /// + public partial class Stashes : UserControl { + private Git.Repository repo = null; + private string selectedStash = null; + + /// + /// File tree node. + /// + 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 Children { get; set; } = new List(); + } + + /// + /// Constructor. + /// + public Stashes() { + InitializeComponent(); + } + + /// + /// Cleanup + /// + /// + /// + private void Cleanup(object sender, RoutedEventArgs e) { + stashList.ItemsSource = null; + changeList.ItemsSource = null; + diff.Reset(); + } + + /// + /// Set data. + /// + /// + /// + public void SetData(Git.Repository opened, List stashes) { + repo = opened; + selectedStash = null; + stashList.ItemsSource = stashes; + changeList.ItemsSource = null; + diff.Reset(); + } + + /// + /// Stash list selection changed event. + /// + /// + /// + 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); + } + + /// + /// File selection changed in TreeView. + /// + /// + /// + 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); + } + + /// + /// Stash context menu. + /// + /// + /// + 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; + } + } +} diff --git a/SourceGit/UI/WorkingCopy.xaml.cs b/SourceGit/UI/WorkingCopy.xaml.cs index 538e3da8..4dd0ad48 100644 --- a/SourceGit/UI/WorkingCopy.xaml.cs +++ b/SourceGit/UI/WorkingCopy.xaml.cs @@ -1,1039 +1,1031 @@ -using Microsoft.Win32; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Controls.Primitives; -using System.Windows.Input; -using System.Windows.Media; - -namespace SourceGit.UI { - - /// - /// Working copy panel. - /// - public partial class WorkingCopy : UserControl { - - /// - /// Node for file tree. - /// - public class Node { - public string FilePath { 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 Children { get; set; } = new List(); - } - - /// - /// Current opened repository. - /// - public Git.Repository Repo { get; set; } - - /// - /// Just for Validation. - /// - public string CommitMessage { get; set; } - - /// - /// Has conflict object? - /// - private bool hasConflict = false; - - /// - /// Constructor. - /// - public WorkingCopy() { - InitializeComponent(); - } - - /// - /// Set display data. - /// - /// - public bool SetData(List changes) { - List staged = new List(); - List unstaged = new List(); - hasConflict = false; - - foreach (var c in changes) { - hasConflict = hasConflict || c.IsConflit; - - if (c.Index != Git.Change.Status.None && c.Index != Git.Change.Status.Untracked) { - staged.Add(c); - } - - if (c.WorkTree != Git.Change.Status.None) { - unstaged.Add(c); - } - } - - Dispatcher.Invoke(() => mergePanel.Visibility = Visibility.Collapsed); - - SetData(unstaged, true); - SetData(staged, false); - - Dispatcher.Invoke(() => { - var current = Repo.CurrentBranch(); - if (current != null && !string.IsNullOrEmpty(current.Upstream) && chkAmend.IsChecked != true) { - btnCommitAndPush.Visibility = Visibility.Visible; - } else { - btnCommitAndPush.Visibility = Visibility.Collapsed; - } - - diffViewer.Reset(); - }); - - return hasConflict; - } - - /// - /// Try to load merge message. - /// - public void LoadMergeMessage() { - if (string.IsNullOrEmpty(txtCommitMsg.Text)) { - var mergeMsgFile = Path.Combine(Repo.Path, ".git", "MERGE_MSG"); - if (!File.Exists(mergeMsgFile)) return; - - var content = File.ReadAllText(mergeMsgFile); - txtCommitMsg.Text = content; - } - } - - /// - /// Clear message. - /// - public void ClearMessage() { - txtCommitMsg.Text = ""; - Validation.ClearInvalid(txtCommitMsg.GetBindingExpression(TextBox.TextProperty)); - } - - #region UNSTAGED - private void UnstagedTreeMultiSelectionChanged(object sender, RoutedEventArgs e) { - mergePanel.Visibility = Visibility.Collapsed; - diffViewer.Reset(); - - var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); - if (selected.Count == 0) return; - - Helpers.TreeViewHelper.UnselectTree(stageTree); - stageList.SelectedItems.Clear(); - - if (selected.Count != 1) return; - - var node = selected[0].DataContext as Node; - if (!node.IsFile) return; - - if (node.Change.IsConflit) { - mergePanel.Visibility = Visibility.Visible; - return; - } - - List data; - switch (node.Change.WorkTree) { - case Git.Change.Status.Added: - case Git.Change.Status.Untracked: - data = Repo.Diff("", "--no-index", node.FilePath, "/dev/null"); - break; - default: - data = Repo.Diff("", "", node.FilePath, node.Change.OriginalPath); - break; - } - - diffViewer.SetData(data, node.FilePath, node.Change.OriginalPath); - } - - private void UnstagedListSelectionChanged(object sender, SelectionChangedEventArgs e) { - var selected = unstagedList.SelectedItems; - if (selected.Count == 0) return; - - mergePanel.Visibility = Visibility.Collapsed; - diffViewer.Reset(); - Helpers.TreeViewHelper.UnselectTree(stageTree); - stageList.SelectedItems.Clear(); - - if (selected.Count != 1) return; - - var change = selected[0] as Git.Change; - if (change.IsConflit) { - mergePanel.Visibility = Visibility.Visible; - return; - } - - List data; - switch (change.WorkTree) { - case Git.Change.Status.Added: - case Git.Change.Status.Untracked: - data = Repo.Diff("", "--no-index", change.Path, "/dev/null"); - break; - default: - data = Repo.Diff("", "", change.Path, change.OriginalPath); - break; - } - - diffViewer.SetData(data, change.Path, change.OriginalPath); - } - - private void SaveAsPatchFromUnstagedChanges(string path, List changes) { - FileStream stream = new FileStream(path, FileMode.Create); - StreamWriter writer = new StreamWriter(stream); - - foreach (var change in changes) { - if (change.WorkTree == Git.Change.Status.Added || change.WorkTree == Git.Change.Status.Untracked) { - Repo.RunCommand($"diff --no-index --no-ext-diff --find-renames -- /dev/null \"{change.Path}\"", line => { - writer.WriteLine(line); - }); - } else { - var orgFile = string.IsNullOrEmpty(change.OriginalPath) ? "" : $"\"{change.OriginalPath}\""; - Repo.RunCommand($"diff --binary --no-ext-diff --find-renames --full-index -- {orgFile} \"{change.Path}\"", line => { - writer.WriteLine(line); - }); - } - } - - writer.Flush(); - stream.Flush(); - writer.Close(); - stream.Close(); - } - - private void GetChangesFromNode(Node node, List outs) { - if (node.Change != null) { - if (!outs.Contains(node.Change)) outs.Add(node.Change); - } else if (node.Children.Count > 0) { - foreach (var sub in node.Children) GetChangesFromNode(sub, outs); - } - } - - private void UnstagedTreeContextMenuOpening(object sender, ContextMenuEventArgs ev) { - var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); - - if (selected.Count == 1) { - var item = sender as TreeViewItem; - var node = item.DataContext as Node; - if (node == null) return; - - var changes = new List(); - GetChangesFromNode(node, changes); - - var path = Path.GetFullPath(Repo.Path + "\\" + node.FilePath); - var explore = new MenuItem(); - explore.IsEnabled = File.Exists(path) || Directory.Exists(path); - explore.Header = "Reveal in File Explorer"; - explore.Click += (o, e) => { - if (node.IsFile) Process.Start("explorer", $"/select,{path}"); - else Process.Start(path); - e.Handled = true; - }; - - var stage = new MenuItem(); - stage.Header = "Stage"; - stage.Click += async (o, e) => { - await Task.Run(() => Repo.Stage(node.FilePath)); - e.Handled = true; - }; - - var discard = new MenuItem(); - discard.Header = "Discard changes ..."; - discard.Click += (o, e) => { - Discard.Show(Repo, changes); - e.Handled = true; - }; - - var stash = new MenuItem(); - stash.Header = $"Stash ..."; - stash.Click += (o, e) => { - List nodes = new List() { node.FilePath }; - Stash.Show(Repo, nodes); - e.Handled = true; - }; - - var patch = new MenuItem(); - patch.Header = $"Save as patch ..."; - patch.Click += (o, e) => { - var dialog = new SaveFileDialog(); - dialog.Filter = "Patch File|*.patch"; - dialog.Title = "Select file to store patch data."; - dialog.InitialDirectory = Repo.Path; - - if (dialog.ShowDialog() == true) { - SaveAsPatchFromUnstagedChanges(dialog.FileName, changes); - } - - e.Handled = true; - }; - - var copyPath = new MenuItem(); - copyPath.Header = "Copy full path"; - copyPath.Click += (o, e) => { - Clipboard.SetText(node.FilePath); - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(explore); - menu.Items.Add(new Separator()); - menu.Items.Add(stage); - menu.Items.Add(discard); - menu.Items.Add(stash); - menu.Items.Add(patch); - menu.Items.Add(new Separator()); - menu.Items.Add(copyPath); - menu.IsOpen = true; - } else if (selected.Count > 1) { - var changes = new List(); - var files = new List(); - - foreach (var item in selected) GetChangesFromNode(item.DataContext as Node, changes); - foreach (var c in changes) files.Add(c.Path); - - var stage = new MenuItem(); - stage.Header = $"Stage {changes.Count} files ..."; - stage.Click += async (o, e) => { - await Task.Run(() => Repo.Stage(files.ToArray())); - e.Handled = true; - }; - - var discard = new MenuItem(); - discard.Header = $"Discard {changes.Count} changes ..."; - discard.Click += (o, e) => { - Discard.Show(Repo, changes); - e.Handled = true; - }; - - var stash = new MenuItem(); - stash.Header = $"Stash {changes.Count} files ..."; - stash.Click += (o, e) => { - Stash.Show(Repo, files); - e.Handled = true; - }; - - var patch = new MenuItem(); - patch.Header = $"Save as patch ..."; - patch.Click += (o, e) => { - var dialog = new SaveFileDialog(); - dialog.Filter = "Patch File|*.patch"; - dialog.Title = "Select file to store patch data."; - dialog.InitialDirectory = Repo.Path; - - if (dialog.ShowDialog() == true) { - SaveAsPatchFromUnstagedChanges(dialog.FileName, changes); - } - - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(stage); - menu.Items.Add(discard); - menu.Items.Add(stash); - menu.Items.Add(patch); - menu.IsOpen = true; - } - - ev.Handled = true; - } - - private void UnstagedListContextMenuOpening(object sender, ContextMenuEventArgs ev) { - var row = sender as DataGridRow; - if (row == null) return; - - if (!row.IsSelected) { - unstagedList.SelectedItems.Clear(); - unstagedList.SelectedItems.Add(row.DataContext); - } - - var selected = unstagedList.SelectedItems; - var brush = new SolidColorBrush(Color.FromRgb(48, 48, 48)); - - if (selected.Count == 1) { - var change = selected[0] as Git.Change; - var path = Path.GetFullPath(Repo.Path + "\\" + change.Path); - var explore = new MenuItem(); - explore.IsEnabled = File.Exists(path) || Directory.Exists(path); - explore.Header = "Reveal in File Explorer"; - explore.Click += (o, e) => { - Process.Start("explorer", $"/select,{path}"); - e.Handled = true; - }; - - var stage = new MenuItem(); - stage.Header = "Stage"; - stage.Click += async (o, e) => { - await Task.Run(() => Repo.Stage(change.Path)); - e.Handled = true; - }; - - var discard = new MenuItem(); - discard.Header = "Discard changes ..."; - discard.Click += (o, e) => { - Discard.Show(Repo, new List() { change }); - e.Handled = true; - }; - - var stash = new MenuItem(); - stash.Header = $"Stash ..."; - stash.Click += (o, e) => { - List nodes = new List() { change.Path }; - Stash.Show(Repo, nodes); - e.Handled = true; - }; - - var patch = new MenuItem(); - patch.Header = $"Save as patch ..."; - patch.Click += (o, e) => { - var dialog = new SaveFileDialog(); - dialog.Filter = "Patch File|*.patch"; - dialog.Title = "Select file to store patch data."; - dialog.InitialDirectory = Repo.Path; - - if (dialog.ShowDialog() == true) { - SaveAsPatchFromUnstagedChanges(dialog.FileName, new List() { change }); - } - - e.Handled = true; - }; - - var copyPath = new MenuItem(); - copyPath.Header = "Copy file path"; - copyPath.Click += (o, e) => { - Clipboard.SetText(change.Path); - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(explore); - menu.Items.Add(new Separator()); - menu.Items.Add(stage); - menu.Items.Add(discard); - menu.Items.Add(stash); - menu.Items.Add(patch); - menu.Items.Add(new Separator()); - menu.Items.Add(copyPath); - menu.IsOpen = true; - } else if (selected.Count > 1) { - List files = new List(); - List changes = new List(); - foreach (var item in selected) { - files.Add((item as Git.Change).Path); - changes.Add(item as Git.Change); - } - - var stage = new MenuItem(); - stage.Header = $"Stage {changes.Count} files ..."; - stage.Click += async (o, e) => { - await Task.Run(() => Repo.Stage(files.ToArray())); - e.Handled = true; - }; - - var discard = new MenuItem(); - discard.Header = $"Discard {changes.Count} changes ..."; - discard.Click += (o, e) => { - Discard.Show(Repo, changes); - e.Handled = true; - }; - - var stash = new MenuItem(); - stash.Header = $"Stash {changes.Count} files ..."; - stash.Click += (o, e) => { - Stash.Show(Repo, files); - e.Handled = true; - }; - - var patch = new MenuItem(); - patch.Header = $"Save as patch ..."; - patch.Click += (o, e) => { - var dialog = new SaveFileDialog(); - dialog.Filter = "Patch File|*.patch"; - dialog.Title = "Select file to store patch data."; - dialog.InitialDirectory = Repo.Path; - - if (dialog.ShowDialog() == true) { - SaveAsPatchFromUnstagedChanges(dialog.FileName, changes); - } - - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(stage); - menu.Items.Add(discard); - menu.Items.Add(stash); - menu.Items.Add(patch); - menu.IsOpen = true; - } - - ev.Handled = true; - } - - private async void Stage(object sender, RoutedEventArgs e) { - var files = new List(); - - if (App.Preference.UIUseListInUnstaged) { - var selected = unstagedList.SelectedItems; - foreach (var one in selected) { - var node = one as Git.Change; - if (node != null) files.Add(node.Path); - } - } else { - var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); - foreach (var one in selected) { - var node = one.DataContext as Node; - if (node != null) files.Add(node.FilePath); - } - } - - if (files.Count == 0) return; - await Task.Run(() => Repo.Stage(files.ToArray())); - } - - private async void StageAll(object sender, RoutedEventArgs e) { - await Task.Run(() => Repo.Stage()); - } - #endregion - - #region STAGED - private void StageTreeMultiSelectionChanged(object sender, RoutedEventArgs e) { - mergePanel.Visibility = Visibility.Collapsed; - diffViewer.Reset(); - - var selected = Helpers.TreeViewHelper.GetSelectedItems(stageTree); - if (selected.Count == 0) return; - - Helpers.TreeViewHelper.UnselectTree(unstagedTree); - unstagedList.SelectedItems.Clear(); - - if (selected.Count != 1) return; - - var node = selected[0].DataContext as Node; - if (!node.IsFile) return; - - mergePanel.Visibility = Visibility.Collapsed; - List data = Repo.Diff("", "--cached", node.FilePath, node.Change.OriginalPath); - diffViewer.SetData(data, node.FilePath, node.Change.OriginalPath); - e.Handled = true; - } - - private void StagedListSelectionChanged(object sender, SelectionChangedEventArgs e) { - var selected = stageList.SelectedItems; - if (selected.Count == 0) return; - - mergePanel.Visibility = Visibility.Collapsed; - diffViewer.Reset(); - Helpers.TreeViewHelper.UnselectTree(unstagedTree); - unstagedList.SelectedItems.Clear(); - - if (selected.Count != 1) return; - - var change = selected[0] as Git.Change; - mergePanel.Visibility = Visibility.Collapsed; - List data = Repo.Diff("", "--cached", change.Path, change.OriginalPath); - diffViewer.SetData(data, change.Path, change.OriginalPath); - e.Handled = true; - } - - private void StageTreeContextMenuOpening(object sender, ContextMenuEventArgs ev) { - var selected = Helpers.TreeViewHelper.GetSelectedItems(stageTree); - var brush = new SolidColorBrush(Color.FromRgb(48, 48, 48)); - - if (selected.Count == 1) { - var item = sender as TreeViewItem; - if (item == null) return; - - var node = item.DataContext as Node; - if (node == null) return; - - var path = Path.GetFullPath(Repo.Path + "\\" + node.FilePath); - - var explore = new MenuItem(); - explore.IsEnabled = File.Exists(path) || Directory.Exists(path); - explore.Header = "Reveal in File Explorer"; - explore.Click += (o, e) => { - if (node.IsFile) Process.Start("explorer", $"/select,{path}"); - else Process.Start(path); - e.Handled = true; - }; - - var unstage = new MenuItem(); - unstage.Header = "Unstage"; - unstage.Click += async (o, e) => { - await Task.Run(() => Repo.Unstage(node.FilePath)); - e.Handled = true; - }; - - var copyPath = new MenuItem(); - copyPath.Header = "Copy full path"; - copyPath.Click += (o, e) => { - Clipboard.SetText(node.FilePath); - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(explore); - menu.Items.Add(new Separator()); - menu.Items.Add(unstage); - menu.Items.Add(new Separator()); - menu.Items.Add(copyPath); - menu.IsOpen = true; - } else if (selected.Count > 1) { - var changes = new List(); - var files = new List(); - foreach (var item in selected) GetChangesFromNode(item.DataContext as Node, changes); - foreach (var c in changes) files.Add(c.Path); - - var unstage = new MenuItem(); - unstage.Header = $"Unstage {changes.Count} files"; - unstage.Click += async (o, e) => { - await Task.Run(() => Repo.Unstage(files.ToArray())); - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(unstage); - menu.IsOpen = true; - } - - ev.Handled = true; - } - - private void StagedListContextMenuOpening(object sender, ContextMenuEventArgs ev) { - var row = sender as DataGridRow; - if (row == null) return; - - if (!row.IsSelected) { - stageList.SelectedItems.Clear(); - stageList.SelectedItems.Add(row.DataContext); - } - - var selected = stageList.SelectedItems; - var brush = new SolidColorBrush(Color.FromRgb(48, 48, 48)); - - if (selected.Count == 1) { - var change = selected[0] as Git.Change; - var path = Path.GetFullPath(Repo.Path + "\\" + change.Path); - - var explore = new MenuItem(); - explore.IsEnabled = File.Exists(path) || Directory.Exists(path); - explore.Header = "Reveal in File Explorer"; - explore.Click += (o, e) => { - Process.Start("explorer", $"/select,{path}"); - e.Handled = true; - }; - - var unstage = new MenuItem(); - unstage.Header = "Unstage"; - unstage.Click += async (o, e) => { - await Task.Run(() => Repo.Unstage(change.Path)); - e.Handled = true; - }; - - var copyPath = new MenuItem(); - copyPath.Header = "Copy full path"; - copyPath.Click += (o, e) => { - Clipboard.SetText(change.Path); - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(explore); - menu.Items.Add(new Separator()); - menu.Items.Add(unstage); - menu.Items.Add(new Separator()); - menu.Items.Add(copyPath); - menu.IsOpen = true; - } else if (selected.Count > 1) { - List files = new List(); - foreach (var one in selected) files.Add((one as Git.Change).Path); - - var unstage = new MenuItem(); - unstage.Header = $"Unstage {selected.Count} files"; - unstage.Click += async (o, e) => { - await Task.Run(() => Repo.Unstage(files.ToArray())); - e.Handled = true; - }; - - var menu = new ContextMenu(); - menu.Items.Add(unstage); - menu.IsOpen = true; - } - - ev.Handled = true; - } - - private async void Unstage(object sender, RoutedEventArgs e) { - var files = new List(); - - if (App.Preference.UIUseListInUnstaged) { - var selected = stageList.SelectedItems; - foreach (var one in selected) { - var node = one as Git.Change; - if (node != null) files.Add(node.Path); - } - } else { - var selected = Helpers.TreeViewHelper.GetSelectedItems(stageTree); - foreach (var one in selected) { - var node = one.DataContext as Node; - if (node != null) files.Add(node.FilePath); - } - } - - if (files.Count == 0) return; - await Task.Run(() => Repo.Unstage(files.ToArray())); - } - - private async void UnstageAll(object sender, RoutedEventArgs e) { - await Task.Run(() => Repo.Unstage()); - } - #endregion - - #region COMMIT_PANEL - private void OpenCommitMessageSelector(object sender, RoutedEventArgs e) { - var anchor = sender as Button; - - if (anchor.ContextMenu == null) { - anchor.ContextMenu = new ContextMenu(); - anchor.ContextMenu.PlacementTarget = anchor; - anchor.ContextMenu.Placement = PlacementMode.Top; - anchor.ContextMenu.VerticalOffset = -4; - anchor.ContextMenu.StaysOpen = false; - anchor.ContextMenu.Focusable = true; - anchor.ContextMenu.MaxWidth = 500; - } else { - anchor.ContextMenu.Items.Clear(); - } - - if (Repo.CommitMsgRecords.Count == 0) { - var tip = new MenuItem(); - tip.Header = "NO RECENT INPUT MESSAGES"; - tip.IsEnabled = false; - anchor.ContextMenu.Items.Add(tip); - } else { - var tip = new MenuItem(); - tip.Header = "RECENT INPUT MESSAGES"; - tip.IsEnabled = false; - anchor.ContextMenu.Items.Add(tip); - anchor.ContextMenu.Items.Add(new Separator()); - - foreach (var one in Repo.CommitMsgRecords) { - var dump = one; - - var item = new MenuItem(); - item.Header = dump; - item.Padding = new Thickness(0); - item.Click += (o, ev) => { - txtCommitMsg.Text = dump; - ev.Handled = true; - }; - - anchor.ContextMenu.Items.Add(item); - } - } - - anchor.ContextMenu.IsOpen = true; - e.Handled = true; - } - - private void CommitMessageChanged(object sender, TextChangedEventArgs e) { - (sender as TextBox).ScrollToEnd(); - } - - private void StartAmend(object sender, RoutedEventArgs e) { - var commits = Repo.Commits("-n 1"); - if (commits.Count == 0) { - App.RaiseError("No commit to amend!"); - chkAmend.IsChecked = false; - return; - } - - txtCommitMsg.Text = commits[0].Subject; - btnCommitAndPush.Visibility = Visibility.Collapsed; - } - - private void EndAmend(object sender, RoutedEventArgs e) { - if (!IsLoaded) return; - - var current = Repo.CurrentBranch(); - if (current != null && !string.IsNullOrEmpty(current.Upstream)) { - btnCommitAndPush.Visibility = Visibility.Visible; - } else { - btnCommitAndPush.Visibility = Visibility.Collapsed; - } - } - - private async void Commit(object sender, RoutedEventArgs e) { - var amend = chkAmend.IsChecked == true; - - Repo.RecordCommitMessage(CommitMessage); - - if (hasConflict) { - App.RaiseError("You have unsolved conflicts in your working copy!"); - return; - } - - if (stageTree.Items.Count == 0) { - App.RaiseError("Nothing to commit!"); - return; - } - - txtCommitMsg.GetBindingExpression(TextBox.TextProperty).UpdateSource(); - if (Validation.GetHasError(txtCommitMsg)) return; - - bool succ = await Task.Run(() => Repo.DoCommit(CommitMessage, amend)); - if (succ) ClearMessage(); - } - - private async void CommitAndPush(object sender, RoutedEventArgs e) { - var amend = chkAmend.IsChecked == true; - - Repo.RecordCommitMessage(CommitMessage); - - if (hasConflict) { - App.RaiseError("You have unsolved conflicts in your working copy!"); - return; - } - - if (stageTree.Items.Count == 0) { - App.RaiseError("Nothing to commit!"); - return; - } - - txtCommitMsg.GetBindingExpression(TextBox.TextProperty).UpdateSource(); - if (Validation.GetHasError(txtCommitMsg)) return; - - bool succ = await Task.Run(() => Repo.DoCommit(CommitMessage, amend)); - if (!succ) return; - - ClearMessage(); - Push.StartDirectly(Repo); - } - #endregion - - #region MERGE - private async void OpenMergeTool(object sender, RoutedEventArgs e) { - var mergeExe = App.Preference.MergeExecutable; - var mergeParam = Git.MergeTool.Supported[App.Preference.MergeTool].Parameter; - - if (!File.Exists(mergeExe) || mergeParam.IndexOf("$MERGED") < 0) { - App.RaiseError("Invalid merge tool in preference setting!"); - return; - } - - string file = null; - if (App.Preference.UIUseListInUnstaged) { - var selected = unstagedList.SelectedItems; - if (selected.Count <= 0) return; - - var change = selected[0] as Git.Change; - if (change == null) return; - - file = change.Path; - } else { - var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); - if (selected.Count <= 0) return; - - var node = selected[0].DataContext as Node; - if (node == null || !node.IsFile) return; - - file = node.FilePath; - } - - await Task.Run(() => { - Repo.RunCommand($"-c mergetool.sourcegit.cmd=\"\\\"{mergeExe}\\\" {mergeParam}\" -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit {file}", null); - }); - } - - private async void UseTheirs(object sender, RoutedEventArgs e) { - var files = new List(); - if (App.Preference.UIUseListInUnstaged) { - var selected = unstagedList.SelectedItems; - foreach (var one in selected) { - var node = one as Git.Change; - if (node != null) files.Add(node.Path); - } - } else { - var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); - foreach (var one in selected) { - var node = one.DataContext as Node; - if (node != null) files.Add(node.FilePath); - } - } - - await Task.Run(() => { - Repo.SetWatcherEnabled(false); - var errs = Repo.RunCommand($"checkout --theirs -- {string.Join(" ", files)}", null); - if (errs != null) { - Repo.SetWatcherEnabled(true); - App.RaiseError("Use theirs failed: " + errs); - return; - } - - Repo.Stage(files.ToArray()); - }); - } - - private async void UseMine(object sender, RoutedEventArgs e) { - var files = new List(); - if (App.Preference.UIUseListInUnstaged) { - var selected = unstagedList.SelectedItems; - foreach (var one in selected) { - var node = one as Git.Change; - if (node != null) files.Add(node.Path); - } - } else { - var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); - foreach (var one in selected) { - var node = one.DataContext as Node; - if (node != null) files.Add(node.FilePath); - } - } - - await Task.Run(() => { - Repo.SetWatcherEnabled(false); - var errs = Repo.RunCommand($"checkout --ours -- {string.Join(" ", files)}", null); - if (errs != null) { - Repo.SetWatcherEnabled(true); - App.RaiseError("Use mine failed: " + errs); - return; - } - - Repo.Stage(files.ToArray()); - }); - } - #endregion - - #region TREE_COMMON - private void SelectWholeTree(object sender, ExecutedRoutedEventArgs e) { - var tree = sender as TreeView; - if (tree == null) return; - - Helpers.TreeViewHelper.SelectWholeTree(tree); - } - - private void SetData(List changes, bool unstaged) { - List source = new List(); - Dictionary folders = new Dictionary(); - bool isExpendDefault = changes.Count <= 50; - - foreach (var c in changes) { - var subs = c.Path.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); - if (subs.Length == 1) { - Node node = new Node(); - node.FilePath = c.Path; - node.IsFile = true; - node.Name = c.Path; - node.Change = c; - source.Add(node); - } else { - Node lastFolder = null; - var folder = ""; - for (int i = 0; i < subs.Length - 1; i++) { - folder += (subs[i] + "/"); - if (folders.ContainsKey(folder)) { - lastFolder = folders[folder]; - } else if (lastFolder == null) { - lastFolder = new Node(); - lastFolder.FilePath = folder; - lastFolder.Name = subs[i]; - lastFolder.IsNodeExpanded = isExpendDefault; - source.Add(lastFolder); - folders.Add(folder, lastFolder); - } else { - var folderNode = new Node(); - folderNode.FilePath = folder; - folderNode.Name = subs[i]; - folderNode.IsNodeExpanded = isExpendDefault; - folders.Add(folder, folderNode); - lastFolder.Children.Add(folderNode); - lastFolder = folderNode; - } - } - - Node node = new Node(); - node.FilePath = c.Path; - node.Name = subs[subs.Length - 1]; - node.IsFile = true; - node.Change = c; - lastFolder.Children.Add(node); - } - } - - folders.Clear(); - SortTreeNodes(source); - - Dispatcher.Invoke(() => { - if (unstaged) { - unstagedList.ItemsSource = changes; - unstagedTree.ItemsSource = source; - } else { - stageList.ItemsSource = changes; - stageTree.ItemsSource = source; - } - }); - } - - private Node FindNodeByPath(List nodes, string filePath) { - foreach (var node in nodes) { - if (node.FilePath == filePath) return node; - var found = FindNodeByPath(node.Children, filePath); - if (found != null) return found; - } - return null; - } - - private void SortTreeNodes(List list) { - list.Sort((l, r) => { - if (l.IsFile) { - return r.IsFile ? l.FilePath.CompareTo(r.FilePath) : 1; - } else { - return r.IsFile ? -1 : l.FilePath.CompareTo(r.FilePath); - } - }); - - 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; - } - #endregion - - #region DATAGRID_COMMON - private void SelectWholeDataGrid(object sender, ExecutedRoutedEventArgs e) { - var grid = sender as DataGrid; - if (grid == null) return; - - var source = grid.ItemsSource; - foreach (var item in source) grid.SelectedItems.Add(item); - } - #endregion - } -} +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; + +namespace SourceGit.UI { + + /// + /// Working copy panel. + /// + public partial class WorkingCopy : UserControl { + + /// + /// Node for file tree. + /// + public class Node { + public string FilePath { 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 Children { get; set; } = new List(); + } + + /// + /// Current opened repository. + /// + public Git.Repository Repo { get; set; } + + /// + /// Just for Validation. + /// + public string CommitMessage { get; set; } + + /// + /// Has conflict object? + /// + private bool hasConflict = false; + + /// + /// Constructor. + /// + public WorkingCopy() { + InitializeComponent(); + } + + /// + /// Set display data. + /// + /// + public bool SetData(List changes) { + List staged = new List(); + List unstaged = new List(); + hasConflict = false; + + foreach (var c in changes) { + hasConflict = hasConflict || c.IsConflit; + + if (c.Index != Git.Change.Status.None && c.Index != Git.Change.Status.Untracked) { + staged.Add(c); + } + + if (c.WorkTree != Git.Change.Status.None) { + unstaged.Add(c); + } + } + + Dispatcher.Invoke(() => mergePanel.Visibility = Visibility.Collapsed); + + SetData(unstaged, true); + SetData(staged, false); + + Dispatcher.Invoke(() => { + var current = Repo.CurrentBranch(); + if (current != null && !string.IsNullOrEmpty(current.Upstream) && chkAmend.IsChecked != true) { + btnCommitAndPush.Visibility = Visibility.Visible; + } else { + btnCommitAndPush.Visibility = Visibility.Collapsed; + } + + diffViewer.Reset(); + }); + + return hasConflict; + } + + /// + /// Try to load merge message. + /// + public void LoadMergeMessage() { + if (string.IsNullOrEmpty(txtCommitMsg.Text)) { + var mergeMsgFile = Path.Combine(Repo.Path, ".git", "MERGE_MSG"); + if (!File.Exists(mergeMsgFile)) return; + + var content = File.ReadAllText(mergeMsgFile); + txtCommitMsg.Text = content; + } + } + + /// + /// Clear message. + /// + public void ClearMessage() { + txtCommitMsg.Text = ""; + Validation.ClearInvalid(txtCommitMsg.GetBindingExpression(TextBox.TextProperty)); + } + + #region UNSTAGED + private void UnstagedTreeMultiSelectionChanged(object sender, RoutedEventArgs e) { + mergePanel.Visibility = Visibility.Collapsed; + diffViewer.Reset(); + + var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); + if (selected.Count == 0) return; + + Helpers.TreeViewHelper.UnselectTree(stageTree); + stageList.SelectedItems.Clear(); + + if (selected.Count != 1) return; + + var node = selected[0].DataContext as Node; + if (!node.IsFile) return; + + if (node.Change.IsConflit) { + mergePanel.Visibility = Visibility.Visible; + return; + } + + switch (node.Change.WorkTree) { + case Git.Change.Status.Added: + case Git.Change.Status.Untracked: + diffViewer.Diff(Repo, "--no-index", node.FilePath, "/dev/null"); + break; + default: + diffViewer.Diff(Repo, "", node.FilePath, node.Change.OriginalPath); + break; + } + } + + private void UnstagedListSelectionChanged(object sender, SelectionChangedEventArgs e) { + var selected = unstagedList.SelectedItems; + if (selected.Count == 0) return; + + mergePanel.Visibility = Visibility.Collapsed; + diffViewer.Reset(); + Helpers.TreeViewHelper.UnselectTree(stageTree); + stageList.SelectedItems.Clear(); + + if (selected.Count != 1) return; + + var change = selected[0] as Git.Change; + if (change.IsConflit) { + mergePanel.Visibility = Visibility.Visible; + return; + } + + switch (change.WorkTree) { + case Git.Change.Status.Added: + case Git.Change.Status.Untracked: + diffViewer.Diff(Repo, "--no-index", change.Path, "/dev/null"); + break; + default: + diffViewer.Diff(Repo, "", change.Path, change.OriginalPath); + break; + } + } + + private void SaveAsPatchFromUnstagedChanges(string path, List changes) { + FileStream stream = new FileStream(path, FileMode.Create); + StreamWriter writer = new StreamWriter(stream); + + foreach (var change in changes) { + if (change.WorkTree == Git.Change.Status.Added || change.WorkTree == Git.Change.Status.Untracked) { + Repo.RunCommand($"diff --no-index --no-ext-diff --find-renames -- /dev/null \"{change.Path}\"", line => { + writer.WriteLine(line); + }); + } else { + var orgFile = string.IsNullOrEmpty(change.OriginalPath) ? "" : $"\"{change.OriginalPath}\""; + Repo.RunCommand($"diff --binary --no-ext-diff --find-renames --full-index -- {orgFile} \"{change.Path}\"", line => { + writer.WriteLine(line); + }); + } + } + + writer.Flush(); + stream.Flush(); + writer.Close(); + stream.Close(); + } + + private void GetChangesFromNode(Node node, List outs) { + if (node.Change != null) { + if (!outs.Contains(node.Change)) outs.Add(node.Change); + } else if (node.Children.Count > 0) { + foreach (var sub in node.Children) GetChangesFromNode(sub, outs); + } + } + + private void UnstagedTreeContextMenuOpening(object sender, ContextMenuEventArgs ev) { + var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); + + if (selected.Count == 1) { + var item = sender as TreeViewItem; + var node = item.DataContext as Node; + if (node == null) return; + + var changes = new List(); + GetChangesFromNode(node, changes); + + var path = Path.GetFullPath(Repo.Path + "\\" + node.FilePath); + var explore = new MenuItem(); + explore.IsEnabled = File.Exists(path) || Directory.Exists(path); + explore.Header = "Reveal in File Explorer"; + explore.Click += (o, e) => { + if (node.IsFile) Process.Start("explorer", $"/select,{path}"); + else Process.Start(path); + e.Handled = true; + }; + + var stage = new MenuItem(); + stage.Header = "Stage"; + stage.Click += async (o, e) => { + await Task.Run(() => Repo.Stage(node.FilePath)); + e.Handled = true; + }; + + var discard = new MenuItem(); + discard.Header = "Discard changes ..."; + discard.Click += (o, e) => { + Discard.Show(Repo, changes); + e.Handled = true; + }; + + var stash = new MenuItem(); + stash.Header = $"Stash ..."; + stash.Click += (o, e) => { + List nodes = new List() { node.FilePath }; + Stash.Show(Repo, nodes); + e.Handled = true; + }; + + var patch = new MenuItem(); + patch.Header = $"Save as patch ..."; + patch.Click += (o, e) => { + var dialog = new SaveFileDialog(); + dialog.Filter = "Patch File|*.patch"; + dialog.Title = "Select file to store patch data."; + dialog.InitialDirectory = Repo.Path; + + if (dialog.ShowDialog() == true) { + SaveAsPatchFromUnstagedChanges(dialog.FileName, changes); + } + + e.Handled = true; + }; + + var copyPath = new MenuItem(); + copyPath.Header = "Copy full path"; + copyPath.Click += (o, e) => { + Clipboard.SetText(node.FilePath); + e.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(explore); + menu.Items.Add(new Separator()); + menu.Items.Add(stage); + menu.Items.Add(discard); + menu.Items.Add(stash); + menu.Items.Add(patch); + menu.Items.Add(new Separator()); + menu.Items.Add(copyPath); + menu.IsOpen = true; + } else if (selected.Count > 1) { + var changes = new List(); + var files = new List(); + + foreach (var item in selected) GetChangesFromNode(item.DataContext as Node, changes); + foreach (var c in changes) files.Add(c.Path); + + var stage = new MenuItem(); + stage.Header = $"Stage {changes.Count} files ..."; + stage.Click += async (o, e) => { + await Task.Run(() => Repo.Stage(files.ToArray())); + e.Handled = true; + }; + + var discard = new MenuItem(); + discard.Header = $"Discard {changes.Count} changes ..."; + discard.Click += (o, e) => { + Discard.Show(Repo, changes); + e.Handled = true; + }; + + var stash = new MenuItem(); + stash.Header = $"Stash {changes.Count} files ..."; + stash.Click += (o, e) => { + Stash.Show(Repo, files); + e.Handled = true; + }; + + var patch = new MenuItem(); + patch.Header = $"Save as patch ..."; + patch.Click += (o, e) => { + var dialog = new SaveFileDialog(); + dialog.Filter = "Patch File|*.patch"; + dialog.Title = "Select file to store patch data."; + dialog.InitialDirectory = Repo.Path; + + if (dialog.ShowDialog() == true) { + SaveAsPatchFromUnstagedChanges(dialog.FileName, changes); + } + + e.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(stage); + menu.Items.Add(discard); + menu.Items.Add(stash); + menu.Items.Add(patch); + menu.IsOpen = true; + } + + ev.Handled = true; + } + + private void UnstagedListContextMenuOpening(object sender, ContextMenuEventArgs ev) { + var row = sender as DataGridRow; + if (row == null) return; + + if (!row.IsSelected) { + unstagedList.SelectedItems.Clear(); + unstagedList.SelectedItems.Add(row.DataContext); + } + + var selected = unstagedList.SelectedItems; + var brush = new SolidColorBrush(Color.FromRgb(48, 48, 48)); + + if (selected.Count == 1) { + var change = selected[0] as Git.Change; + var path = Path.GetFullPath(Repo.Path + "\\" + change.Path); + var explore = new MenuItem(); + explore.IsEnabled = File.Exists(path) || Directory.Exists(path); + explore.Header = "Reveal in File Explorer"; + explore.Click += (o, e) => { + Process.Start("explorer", $"/select,{path}"); + e.Handled = true; + }; + + var stage = new MenuItem(); + stage.Header = "Stage"; + stage.Click += async (o, e) => { + await Task.Run(() => Repo.Stage(change.Path)); + e.Handled = true; + }; + + var discard = new MenuItem(); + discard.Header = "Discard changes ..."; + discard.Click += (o, e) => { + Discard.Show(Repo, new List() { change }); + e.Handled = true; + }; + + var stash = new MenuItem(); + stash.Header = $"Stash ..."; + stash.Click += (o, e) => { + List nodes = new List() { change.Path }; + Stash.Show(Repo, nodes); + e.Handled = true; + }; + + var patch = new MenuItem(); + patch.Header = $"Save as patch ..."; + patch.Click += (o, e) => { + var dialog = new SaveFileDialog(); + dialog.Filter = "Patch File|*.patch"; + dialog.Title = "Select file to store patch data."; + dialog.InitialDirectory = Repo.Path; + + if (dialog.ShowDialog() == true) { + SaveAsPatchFromUnstagedChanges(dialog.FileName, new List() { change }); + } + + e.Handled = true; + }; + + var copyPath = new MenuItem(); + copyPath.Header = "Copy file path"; + copyPath.Click += (o, e) => { + Clipboard.SetText(change.Path); + e.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(explore); + menu.Items.Add(new Separator()); + menu.Items.Add(stage); + menu.Items.Add(discard); + menu.Items.Add(stash); + menu.Items.Add(patch); + menu.Items.Add(new Separator()); + menu.Items.Add(copyPath); + menu.IsOpen = true; + } else if (selected.Count > 1) { + List files = new List(); + List changes = new List(); + foreach (var item in selected) { + files.Add((item as Git.Change).Path); + changes.Add(item as Git.Change); + } + + var stage = new MenuItem(); + stage.Header = $"Stage {changes.Count} files ..."; + stage.Click += async (o, e) => { + await Task.Run(() => Repo.Stage(files.ToArray())); + e.Handled = true; + }; + + var discard = new MenuItem(); + discard.Header = $"Discard {changes.Count} changes ..."; + discard.Click += (o, e) => { + Discard.Show(Repo, changes); + e.Handled = true; + }; + + var stash = new MenuItem(); + stash.Header = $"Stash {changes.Count} files ..."; + stash.Click += (o, e) => { + Stash.Show(Repo, files); + e.Handled = true; + }; + + var patch = new MenuItem(); + patch.Header = $"Save as patch ..."; + patch.Click += (o, e) => { + var dialog = new SaveFileDialog(); + dialog.Filter = "Patch File|*.patch"; + dialog.Title = "Select file to store patch data."; + dialog.InitialDirectory = Repo.Path; + + if (dialog.ShowDialog() == true) { + SaveAsPatchFromUnstagedChanges(dialog.FileName, changes); + } + + e.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(stage); + menu.Items.Add(discard); + menu.Items.Add(stash); + menu.Items.Add(patch); + menu.IsOpen = true; + } + + ev.Handled = true; + } + + private async void Stage(object sender, RoutedEventArgs e) { + var files = new List(); + + if (App.Preference.UIUseListInUnstaged) { + var selected = unstagedList.SelectedItems; + foreach (var one in selected) { + var node = one as Git.Change; + if (node != null) files.Add(node.Path); + } + } else { + var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); + foreach (var one in selected) { + var node = one.DataContext as Node; + if (node != null) files.Add(node.FilePath); + } + } + + if (files.Count == 0) return; + await Task.Run(() => Repo.Stage(files.ToArray())); + } + + private async void StageAll(object sender, RoutedEventArgs e) { + await Task.Run(() => Repo.Stage()); + } + #endregion + + #region STAGED + private void StageTreeMultiSelectionChanged(object sender, RoutedEventArgs e) { + mergePanel.Visibility = Visibility.Collapsed; + diffViewer.Reset(); + + var selected = Helpers.TreeViewHelper.GetSelectedItems(stageTree); + if (selected.Count == 0) return; + + Helpers.TreeViewHelper.UnselectTree(unstagedTree); + unstagedList.SelectedItems.Clear(); + + if (selected.Count != 1) return; + + var node = selected[0].DataContext as Node; + if (!node.IsFile) return; + + mergePanel.Visibility = Visibility.Collapsed; + diffViewer.Diff(Repo, "--cached", node.FilePath, node.Change.OriginalPath); + e.Handled = true; + } + + private void StagedListSelectionChanged(object sender, SelectionChangedEventArgs e) { + var selected = stageList.SelectedItems; + if (selected.Count == 0) return; + + mergePanel.Visibility = Visibility.Collapsed; + diffViewer.Reset(); + Helpers.TreeViewHelper.UnselectTree(unstagedTree); + unstagedList.SelectedItems.Clear(); + + if (selected.Count != 1) return; + + var change = selected[0] as Git.Change; + mergePanel.Visibility = Visibility.Collapsed; + diffViewer.Diff(Repo, "--cached", change.Path, change.OriginalPath); + e.Handled = true; + } + + private void StageTreeContextMenuOpening(object sender, ContextMenuEventArgs ev) { + var selected = Helpers.TreeViewHelper.GetSelectedItems(stageTree); + var brush = new SolidColorBrush(Color.FromRgb(48, 48, 48)); + + if (selected.Count == 1) { + var item = sender as TreeViewItem; + if (item == null) return; + + var node = item.DataContext as Node; + if (node == null) return; + + var path = Path.GetFullPath(Repo.Path + "\\" + node.FilePath); + + var explore = new MenuItem(); + explore.IsEnabled = File.Exists(path) || Directory.Exists(path); + explore.Header = "Reveal in File Explorer"; + explore.Click += (o, e) => { + if (node.IsFile) Process.Start("explorer", $"/select,{path}"); + else Process.Start(path); + e.Handled = true; + }; + + var unstage = new MenuItem(); + unstage.Header = "Unstage"; + unstage.Click += async (o, e) => { + await Task.Run(() => Repo.Unstage(node.FilePath)); + e.Handled = true; + }; + + var copyPath = new MenuItem(); + copyPath.Header = "Copy full path"; + copyPath.Click += (o, e) => { + Clipboard.SetText(node.FilePath); + e.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(explore); + menu.Items.Add(new Separator()); + menu.Items.Add(unstage); + menu.Items.Add(new Separator()); + menu.Items.Add(copyPath); + menu.IsOpen = true; + } else if (selected.Count > 1) { + var changes = new List(); + var files = new List(); + foreach (var item in selected) GetChangesFromNode(item.DataContext as Node, changes); + foreach (var c in changes) files.Add(c.Path); + + var unstage = new MenuItem(); + unstage.Header = $"Unstage {changes.Count} files"; + unstage.Click += async (o, e) => { + await Task.Run(() => Repo.Unstage(files.ToArray())); + e.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(unstage); + menu.IsOpen = true; + } + + ev.Handled = true; + } + + private void StagedListContextMenuOpening(object sender, ContextMenuEventArgs ev) { + var row = sender as DataGridRow; + if (row == null) return; + + if (!row.IsSelected) { + stageList.SelectedItems.Clear(); + stageList.SelectedItems.Add(row.DataContext); + } + + var selected = stageList.SelectedItems; + var brush = new SolidColorBrush(Color.FromRgb(48, 48, 48)); + + if (selected.Count == 1) { + var change = selected[0] as Git.Change; + var path = Path.GetFullPath(Repo.Path + "\\" + change.Path); + + var explore = new MenuItem(); + explore.IsEnabled = File.Exists(path) || Directory.Exists(path); + explore.Header = "Reveal in File Explorer"; + explore.Click += (o, e) => { + Process.Start("explorer", $"/select,{path}"); + e.Handled = true; + }; + + var unstage = new MenuItem(); + unstage.Header = "Unstage"; + unstage.Click += async (o, e) => { + await Task.Run(() => Repo.Unstage(change.Path)); + e.Handled = true; + }; + + var copyPath = new MenuItem(); + copyPath.Header = "Copy full path"; + copyPath.Click += (o, e) => { + Clipboard.SetText(change.Path); + e.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(explore); + menu.Items.Add(new Separator()); + menu.Items.Add(unstage); + menu.Items.Add(new Separator()); + menu.Items.Add(copyPath); + menu.IsOpen = true; + } else if (selected.Count > 1) { + List files = new List(); + foreach (var one in selected) files.Add((one as Git.Change).Path); + + var unstage = new MenuItem(); + unstage.Header = $"Unstage {selected.Count} files"; + unstage.Click += async (o, e) => { + await Task.Run(() => Repo.Unstage(files.ToArray())); + e.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(unstage); + menu.IsOpen = true; + } + + ev.Handled = true; + } + + private async void Unstage(object sender, RoutedEventArgs e) { + var files = new List(); + + if (App.Preference.UIUseListInStaged) { + var selected = stageList.SelectedItems; + foreach (var one in selected) { + var node = one as Git.Change; + if (node != null) files.Add(node.Path); + } + } else { + var selected = Helpers.TreeViewHelper.GetSelectedItems(stageTree); + foreach (var one in selected) { + var node = one.DataContext as Node; + if (node != null) files.Add(node.FilePath); + } + } + + if (files.Count == 0) return; + await Task.Run(() => Repo.Unstage(files.ToArray())); + } + + private async void UnstageAll(object sender, RoutedEventArgs e) { + await Task.Run(() => Repo.Unstage()); + } + #endregion + + #region COMMIT_PANEL + private void OpenCommitMessageSelector(object sender, RoutedEventArgs e) { + var anchor = sender as Button; + + if (anchor.ContextMenu == null) { + anchor.ContextMenu = new ContextMenu(); + anchor.ContextMenu.PlacementTarget = anchor; + anchor.ContextMenu.Placement = PlacementMode.Top; + anchor.ContextMenu.VerticalOffset = -4; + anchor.ContextMenu.StaysOpen = false; + anchor.ContextMenu.Focusable = true; + anchor.ContextMenu.MaxWidth = 500; + } else { + anchor.ContextMenu.Items.Clear(); + } + + if (Repo.CommitMsgRecords.Count == 0) { + var tip = new MenuItem(); + tip.Header = "NO RECENT INPUT MESSAGES"; + tip.IsEnabled = false; + anchor.ContextMenu.Items.Add(tip); + } else { + var tip = new MenuItem(); + tip.Header = "RECENT INPUT MESSAGES"; + tip.IsEnabled = false; + anchor.ContextMenu.Items.Add(tip); + anchor.ContextMenu.Items.Add(new Separator()); + + foreach (var one in Repo.CommitMsgRecords) { + var dump = one; + + var item = new MenuItem(); + item.Header = dump; + item.Padding = new Thickness(0); + item.Click += (o, ev) => { + txtCommitMsg.Text = dump; + ev.Handled = true; + }; + + anchor.ContextMenu.Items.Add(item); + } + } + + anchor.ContextMenu.IsOpen = true; + e.Handled = true; + } + + private void CommitMessageChanged(object sender, TextChangedEventArgs e) { + (sender as TextBox).ScrollToEnd(); + } + + private void StartAmend(object sender, RoutedEventArgs e) { + var commits = Repo.Commits("-n 1"); + if (commits.Count == 0) { + App.RaiseError("No commit to amend!"); + chkAmend.IsChecked = false; + return; + } + + txtCommitMsg.Text = commits[0].Subject; + btnCommitAndPush.Visibility = Visibility.Collapsed; + } + + private void EndAmend(object sender, RoutedEventArgs e) { + if (!IsLoaded) return; + + var current = Repo.CurrentBranch(); + if (current != null && !string.IsNullOrEmpty(current.Upstream)) { + btnCommitAndPush.Visibility = Visibility.Visible; + } else { + btnCommitAndPush.Visibility = Visibility.Collapsed; + } + } + + private async void Commit(object sender, RoutedEventArgs e) { + var amend = chkAmend.IsChecked == true; + + Repo.RecordCommitMessage(CommitMessage); + + if (hasConflict) { + App.RaiseError("You have unsolved conflicts in your working copy!"); + return; + } + + if (stageTree.Items.Count == 0) { + App.RaiseError("Nothing to commit!"); + return; + } + + txtCommitMsg.GetBindingExpression(TextBox.TextProperty).UpdateSource(); + if (Validation.GetHasError(txtCommitMsg)) return; + + bool succ = await Task.Run(() => Repo.DoCommit(CommitMessage, amend)); + if (succ) ClearMessage(); + } + + private async void CommitAndPush(object sender, RoutedEventArgs e) { + var amend = chkAmend.IsChecked == true; + + Repo.RecordCommitMessage(CommitMessage); + + if (hasConflict) { + App.RaiseError("You have unsolved conflicts in your working copy!"); + return; + } + + if (stageTree.Items.Count == 0) { + App.RaiseError("Nothing to commit!"); + return; + } + + txtCommitMsg.GetBindingExpression(TextBox.TextProperty).UpdateSource(); + if (Validation.GetHasError(txtCommitMsg)) return; + + bool succ = await Task.Run(() => Repo.DoCommit(CommitMessage, amend)); + if (!succ) return; + + ClearMessage(); + Push.StartDirectly(Repo); + } + #endregion + + #region MERGE + private async void OpenMergeTool(object sender, RoutedEventArgs e) { + var mergeExe = App.Preference.MergeExecutable; + var mergeParam = Git.MergeTool.Supported[App.Preference.MergeTool].Parameter; + + if (!File.Exists(mergeExe) || mergeParam.IndexOf("$MERGED") < 0) { + App.RaiseError("Invalid merge tool in preference setting!"); + return; + } + + string file = null; + if (App.Preference.UIUseListInUnstaged) { + var selected = unstagedList.SelectedItems; + if (selected.Count <= 0) return; + + var change = selected[0] as Git.Change; + if (change == null) return; + + file = change.Path; + } else { + var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); + if (selected.Count <= 0) return; + + var node = selected[0].DataContext as Node; + if (node == null || !node.IsFile) return; + + file = node.FilePath; + } + + await Task.Run(() => { + Repo.RunCommand($"-c mergetool.sourcegit.cmd=\"\\\"{mergeExe}\\\" {mergeParam}\" -c mergetool.keepBackup=false -c mergetool.trustExitCode=true mergetool --tool=sourcegit {file}", null); + }); + } + + private async void UseTheirs(object sender, RoutedEventArgs e) { + var files = new List(); + if (App.Preference.UIUseListInUnstaged) { + var selected = unstagedList.SelectedItems; + foreach (var one in selected) { + var node = one as Git.Change; + if (node != null) files.Add(node.Path); + } + } else { + var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); + foreach (var one in selected) { + var node = one.DataContext as Node; + if (node != null) files.Add(node.FilePath); + } + } + + await Task.Run(() => { + Repo.SetWatcherEnabled(false); + var errs = Repo.RunCommand($"checkout --theirs -- {string.Join(" ", files)}", null); + if (errs != null) { + Repo.SetWatcherEnabled(true); + App.RaiseError("Use theirs failed: " + errs); + return; + } + + Repo.Stage(files.ToArray()); + }); + } + + private async void UseMine(object sender, RoutedEventArgs e) { + var files = new List(); + if (App.Preference.UIUseListInUnstaged) { + var selected = unstagedList.SelectedItems; + foreach (var one in selected) { + var node = one as Git.Change; + if (node != null) files.Add(node.Path); + } + } else { + var selected = Helpers.TreeViewHelper.GetSelectedItems(unstagedTree); + foreach (var one in selected) { + var node = one.DataContext as Node; + if (node != null) files.Add(node.FilePath); + } + } + + await Task.Run(() => { + Repo.SetWatcherEnabled(false); + var errs = Repo.RunCommand($"checkout --ours -- {string.Join(" ", files)}", null); + if (errs != null) { + Repo.SetWatcherEnabled(true); + App.RaiseError("Use mine failed: " + errs); + return; + } + + Repo.Stage(files.ToArray()); + }); + } + #endregion + + #region TREE_COMMON + private void SelectWholeTree(object sender, ExecutedRoutedEventArgs e) { + var tree = sender as TreeView; + if (tree == null) return; + + Helpers.TreeViewHelper.SelectWholeTree(tree); + } + + private void SetData(List changes, bool unstaged) { + List source = new List(); + Dictionary folders = new Dictionary(); + bool isExpendDefault = changes.Count <= 50; + + foreach (var c in changes) { + var subs = c.Path.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); + if (subs.Length == 1) { + Node node = new Node(); + node.FilePath = c.Path; + node.IsFile = true; + node.Name = c.Path; + node.Change = c; + source.Add(node); + } else { + Node lastFolder = null; + var folder = ""; + for (int i = 0; i < subs.Length - 1; i++) { + folder += (subs[i] + "/"); + if (folders.ContainsKey(folder)) { + lastFolder = folders[folder]; + } else if (lastFolder == null) { + lastFolder = new Node(); + lastFolder.FilePath = folder; + lastFolder.Name = subs[i]; + lastFolder.IsNodeExpanded = isExpendDefault; + source.Add(lastFolder); + folders.Add(folder, lastFolder); + } else { + var folderNode = new Node(); + folderNode.FilePath = folder; + folderNode.Name = subs[i]; + folderNode.IsNodeExpanded = isExpendDefault; + folders.Add(folder, folderNode); + lastFolder.Children.Add(folderNode); + lastFolder = folderNode; + } + } + + Node node = new Node(); + node.FilePath = c.Path; + node.Name = subs[subs.Length - 1]; + node.IsFile = true; + node.Change = c; + lastFolder.Children.Add(node); + } + } + + folders.Clear(); + SortTreeNodes(source); + + Dispatcher.Invoke(() => { + if (unstaged) { + unstagedList.ItemsSource = changes; + unstagedTree.ItemsSource = source; + } else { + stageList.ItemsSource = changes; + stageTree.ItemsSource = source; + } + }); + } + + private Node FindNodeByPath(List nodes, string filePath) { + foreach (var node in nodes) { + if (node.FilePath == filePath) return node; + var found = FindNodeByPath(node.Children, filePath); + if (found != null) return found; + } + return null; + } + + private void SortTreeNodes(List list) { + list.Sort((l, r) => { + if (l.IsFile) { + return r.IsFile ? l.FilePath.CompareTo(r.FilePath) : 1; + } else { + return r.IsFile ? -1 : l.FilePath.CompareTo(r.FilePath); + } + }); + + 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; + } + #endregion + + #region DATAGRID_COMMON + private void SelectWholeDataGrid(object sender, ExecutedRoutedEventArgs e) { + var grid = sender as DataGrid; + if (grid == null) return; + + var source = grid.ItemsSource; + foreach (var item in source) grid.SelectedItems.Add(item); + } + #endregion + } +}