From 7f2e22def6c0c9bdc8147f1da99fc9862b0698f9 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 11 Jun 2024 15:30:06 +0800 Subject: [PATCH] feature: supports branch compare (#174) --- src/Commands/QuerySingleCommit.cs | 1 - src/Resources/Locales/en_US.axaml | 2 + src/Resources/Locales/zh_CN.axaml | 2 + src/Resources/Locales/zh_TW.axaml | 2 + src/ViewModels/BranchCompare.cs | 222 +++++++++++++++++++++++++++ src/ViewModels/Repository.cs | 145 ++++++++++-------- src/Views/BranchCompare.axaml | 240 ++++++++++++++++++++++++++++++ src/Views/BranchCompare.axaml.cs | 58 ++++++++ 8 files changed, 608 insertions(+), 64 deletions(-) create mode 100644 src/ViewModels/BranchCompare.cs create mode 100644 src/Views/BranchCompare.axaml create mode 100644 src/Views/BranchCompare.axaml.cs diff --git a/src/Commands/QuerySingleCommit.cs b/src/Commands/QuerySingleCommit.cs index 4a553817..a3b13929 100644 --- a/src/Commands/QuerySingleCommit.cs +++ b/src/Commands/QuerySingleCommit.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; namespace SourceGit.Commands { diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 856577d7..fd962463 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -33,6 +33,7 @@ Blame BLAME ON THIS FILE IS NOT SUPPORTED!!! Checkout${0}$ + Compare with Branch Compare with HEAD Compare with Worktree Copy Branch Name @@ -49,6 +50,7 @@ Rename${0}$ Tracking ... Unset Upstream + Branch Compare Bytes CANCEL CHANGE DISPLAY MODE diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index e5b46759..60868f16 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -36,6 +36,7 @@ 逐行追溯(blame) 选中文件不支持该操作!!! 检出(checkout)${0}$ + 与其他分支对比 与当前HEAD比较 与本地工作树比较 复制分支名 @@ -52,6 +53,7 @@ 重命名${0}$ 切换上游分支... 取消追踪 + 分支比较 字节 取 消 切换变更显示模式 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index ae7a6d47..4ad5108d 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -36,6 +36,7 @@ 逐行追溯(blame) 選中檔案不支援該操作!!! 檢出(checkout)${0}$ + 與其他分支比較 與當前HEAD比較 與本地工作樹比較 複製分支名 @@ -52,6 +53,7 @@ 重新命名${0}$ 切換上游分支... 取消追蹤 + 分支比較 位元組 取 消 切換變更顯示模式 diff --git a/src/ViewModels/BranchCompare.cs b/src/ViewModels/BranchCompare.cs new file mode 100644 index 00000000..6b19d249 --- /dev/null +++ b/src/ViewModels/BranchCompare.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +using Avalonia.Controls; +using Avalonia.Threading; + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + public class BranchCompare : ObservableObject + { + public Models.Branch Base + { + get; + private set; + } + + public Models.Branch To + { + get; + private set; + } + + public Models.Commit BaseHead + { + get => _baseHead; + private set => SetProperty(ref _baseHead, value); + } + + public Models.Commit ToHead + { + get => _toHead; + private set => SetProperty(ref _toHead, value); + } + + public List VisibleChanges + { + get => _visibleChanges; + private set => SetProperty(ref _visibleChanges, value); + } + + public List SelectedChanges + { + get => _selectedChanges; + set + { + if (SetProperty(ref _selectedChanges, value)) + { + if (value != null && value.Count == 1) + DiffContext = new DiffContext(_repo, new Models.DiffOption(Base.Head, To.Head, value[0]), _diffContext); + else + DiffContext = null; + } + } + } + + public string SearchFilter + { + get => _searchFilter; + set + { + if (SetProperty(ref _searchFilter, value)) + { + RefreshVisible(); + } + } + } + + public DiffContext DiffContext + { + get => _diffContext; + private set => SetProperty(ref _diffContext, value); + } + + public BranchCompare(string repo, Models.Branch baseBranch, Models.Branch toBranch) + { + _repo = repo; + + Base = baseBranch; + To = toBranch; + + Task.Run(() => + { + var baseHead = new Commands.QuerySingleCommit(_repo, Base.Head).Result(); + var toHead = new Commands.QuerySingleCommit(_repo, To.Head).Result(); + _changes = new Commands.CompareRevisions(_repo, Base.Head, To.Head).Result(); + + var visible = _changes; + if (!string.IsNullOrWhiteSpace(_searchFilter)) + { + visible = new List(); + foreach (var c in _changes) + { + if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) + visible.Add(c); + } + } + + Dispatcher.UIThread.Invoke(() => + { + BaseHead = baseHead; + ToHead = toHead; + VisibleChanges = visible; + }); + }); + } + + public void NavigateTo(string commitSHA) + { + var repo = Preference.FindRepository(_repo); + if (repo != null) + repo.NavigateToCommit(commitSHA); + } + + public void ClearSearchFilter() + { + SearchFilter = string.Empty; + } + + public ContextMenu CreateChangeContextMenu() + { + if (_selectedChanges == null || _selectedChanges.Count != 1) + return null; + + var change = _selectedChanges[0]; + var menu = new ContextMenu(); + + var diffWithMerger = new MenuItem(); + diffWithMerger.Header = App.Text("DiffWithMerger"); + diffWithMerger.Icon = App.CreateMenuIcon("Icons.Diff"); + diffWithMerger.Click += (_, ev) => + { + var opt = new Models.DiffOption(Base.Head, To.Head, change); + var type = Preference.Instance.ExternalMergeToolType; + var exec = Preference.Instance.ExternalMergeToolPath; + + var tool = Models.ExternalMerger.Supported.Find(x => x.Type == type); + if (tool == null || !File.Exists(exec)) + { + App.RaiseException(_repo, "Invalid merge tool in preference setting!"); + return; + } + + var args = tool.Type != 0 ? tool.DiffCmd : Preference.Instance.ExternalMergeToolDiffCmd; + Task.Run(() => Commands.MergeTool.OpenForDiff(_repo, exec, args, opt)); + ev.Handled = true; + }; + menu.Items.Add(diffWithMerger); + + if (change.Index != Models.ChangeState.Deleted) + { + var full = Path.GetFullPath(Path.Combine(_repo, change.Path)); + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = App.CreateMenuIcon("Icons.Folder.Open"); + explore.IsEnabled = File.Exists(full); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(full, true); + ev.Handled = true; + }; + menu.Items.Add(explore); + } + + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyPath.Click += (_, ev) => + { + App.CopyText(change.Path); + ev.Handled = true; + }; + menu.Items.Add(copyPath); + + var copyFileName = new MenuItem(); + copyFileName.Header = App.Text("CopyFileName"); + copyFileName.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFileName.Click += (_, e) => + { + App.CopyText(Path.GetFileName(change.Path)); + e.Handled = true; + }; + menu.Items.Add(copyFileName); + + return menu; + } + + private void RefreshVisible() + { + if (_changes == null) + return; + + if (string.IsNullOrEmpty(_searchFilter)) + { + VisibleChanges = _changes; + } + else + { + var visible = new List(); + foreach (var c in _changes) + { + if (c.Path.Contains(_searchFilter, StringComparison.OrdinalIgnoreCase)) + visible.Add(c); + } + + VisibleChanges = visible; + } + } + + private string _repo = string.Empty; + private Models.Commit _baseHead = null; + private Models.Commit _toHead = null; + private List _changes = null; + private List _visibleChanges = null; + private List _selectedChanges = null; + private string _searchFilter = string.Empty; + private DiffContext _diffContext = null; + } +} diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index bbb0d6be..d564c15a 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -909,6 +909,13 @@ namespace SourceGit.ViewModels } menu.Items.Add(push); + + var compareWithBranch = CreateMenuItemToCompareBranches(branch); + if (compareWithBranch != null) + { + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(compareWithBranch); + } } else { @@ -968,24 +975,6 @@ namespace SourceGit.ViewModels menu.Items.Add(merge); menu.Items.Add(rebase); - var compare = new MenuItem(); - compare.Header = App.Text("BranchCM.CompareWithHead"); - compare.Icon = App.CreateMenuIcon("Icons.Compare"); - compare.Click += (o, e) => - { - SearchResultSelectedCommit = null; - - if (_histories != null) - { - var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); - var head = new Commands.QuerySingleCommit(FullPath, current.Head).Result(); - _histories.AutoSelectedCommit = null; - _histories.DetailContext = new RevisionCompare(FullPath, target, head); - } - - e.Handled = true; - }; - if (WorkingCopyChangesCount > 0) { var compareWithWorktree = new MenuItem(); @@ -1002,11 +991,18 @@ namespace SourceGit.ViewModels _histories.DetailContext = new RevisionCompare(FullPath, target, null); } }; + menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(compareWithWorktree); } - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(compare); + var compareWithBranch = CreateMenuItemToCompareBranches(branch); + if (compareWithBranch != null) + { + if (WorkingCopyChangesCount == 0) + menu.Items.Add(new MenuItem() { Header = "-" }); + + menu.Items.Add(compareWithBranch); + } } var type = GitFlow.GetBranchType(branch.Name); @@ -1263,51 +1259,39 @@ namespace SourceGit.ViewModels menu.Items.Add(merge); menu.Items.Add(rebase); menu.Items.Add(new MenuItem() { Header = "-" }); - - if (current.Head != branch.Head) - { - var compare = new MenuItem(); - compare.Header = App.Text("BranchCM.CompareWithHead"); - compare.Icon = App.CreateMenuIcon("Icons.Compare"); - compare.Click += (o, e) => - { - SearchResultSelectedCommit = null; - - if (_histories != null) - { - var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); - var head = new Commands.QuerySingleCommit(FullPath, current.Head).Result(); - _histories.AutoSelectedCommit = null; - _histories.DetailContext = new RevisionCompare(FullPath, target, head); - } - - e.Handled = true; - }; - menu.Items.Add(compare); - - if (WorkingCopyChangesCount > 0) - { - var compareWithWorktree = new MenuItem(); - compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); - compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); - compareWithWorktree.Click += (o, e) => - { - SearchResultSelectedCommit = null; - - if (_histories != null) - { - var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); - _histories.AutoSelectedCommit = null; - _histories.DetailContext = new RevisionCompare(FullPath, target, null); - } - }; - menu.Items.Add(compareWithWorktree); - } - - menu.Items.Add(new MenuItem() { Header = "-" }); - } } + var hasCompare = false; + if (WorkingCopyChangesCount > 0) + { + var compareWithWorktree = new MenuItem(); + compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); + compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); + compareWithWorktree.Click += (o, e) => + { + SearchResultSelectedCommit = null; + + if (_histories != null) + { + var target = new Commands.QuerySingleCommit(FullPath, branch.Head).Result(); + _histories.AutoSelectedCommit = null; + _histories.DetailContext = new RevisionCompare(FullPath, target, null); + } + }; + menu.Items.Add(compareWithWorktree); + hasCompare = true; + } + + var compareWithBranch = CreateMenuItemToCompareBranches(branch); + if (compareWithBranch != null) + { + menu.Items.Add(compareWithBranch); + hasCompare = true; + } + + if (hasCompare) + menu.Items.Add(new MenuItem() { Header = "-" }); + var delete = new MenuItem(); delete.Header = new Views.NameHighlightedTextBlock("BranchCM.Delete", $"{branch.Remote}/{branch.Name}"); delete.Icon = App.CreateMenuIcon("Icons.Clear"); @@ -1485,6 +1469,41 @@ namespace SourceGit.ViewModels return menu; } + private MenuItem CreateMenuItemToCompareBranches(Models.Branch branch) + { + if (Branches.Count == 1) + return null; + + var compare = new MenuItem(); + compare.Header = App.Text("BranchCM.CompareWithBranch"); + compare.Icon = App.CreateMenuIcon("Icons.Compare"); + + foreach (var b in Branches) + { + if (b.FullName != branch.FullName) + { + var dup = b; + var target = new MenuItem(); + target.Header = b.IsLocal ? b.Name : $"{b.Remote}/{b.Name}"; + target.Icon = App.CreateMenuIcon(b.IsCurrent ? "Icons.Check" : "Icons.Branch"); + target.Click += (_, e) => + { + var wnd = new Views.BranchCompare() + { + DataContext = new BranchCompare(FullPath, branch, dup) + }; + + wnd.Show(App.GetTopLevel() as Window); + e.Handled = true; + }; + + compare.Items.Add(target); + } + } + + return compare; + } + private BranchTreeNode.Builder BuildBranchTree(List branches, List remotes) { var builder = new BranchTreeNode.Builder(); diff --git a/src/Views/BranchCompare.axaml b/src/Views/BranchCompare.axaml new file mode 100644 index 00000000..faaf26a3 --- /dev/null +++ b/src/Views/BranchCompare.axaml @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/BranchCompare.axaml.cs b/src/Views/BranchCompare.axaml.cs new file mode 100644 index 00000000..7888caf5 --- /dev/null +++ b/src/Views/BranchCompare.axaml.cs @@ -0,0 +1,58 @@ +using Avalonia.Controls; +using Avalonia.Input; + +namespace SourceGit.Views +{ + public partial class BranchCompare : Window + { + public BranchCompare() + { + InitializeComponent(); + } + + private void MaximizeOrRestoreWindow(object sender, TappedEventArgs e) + { + if (WindowState == WindowState.Maximized) + WindowState = WindowState.Normal; + else + WindowState = WindowState.Maximized; + + e.Handled = true; + } + + private void CustomResizeWindow(object sender, PointerPressedEventArgs e) + { + if (sender is Border border) + { + if (border.Tag is WindowEdge edge) + { + BeginResizeDrag(edge, e); + } + } + } + + private void BeginMoveWindow(object sender, PointerPressedEventArgs e) + { + BeginMoveDrag(e); + } + + private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) + { + if (DataContext is ViewModels.BranchCompare vm && sender is ChangeCollectionView view) + { + var menu = vm.CreateChangeContextMenu(); + view.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OnPressedSHA(object sender, PointerPressedEventArgs e) + { + if (DataContext is ViewModels.BranchCompare vm && sender is TextBlock block) + vm.NavigateTo(block.Text); + + e.Handled = true; + } + } +}