From 96a9e9962197129d22d6d0b36d3ec39e587d457c Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 29 Jan 2026 12:03:47 +0800 Subject: [PATCH] feature: add `Reset File(s) to ` context menu entry to selected change(s) in revision compare view (#2079) Co-authored-by: ybeapps Signed-off-by: leo --- src/Resources/Locales/en_US.axaml | 1 + src/Resources/Locales/zh_CN.axaml | 1 + src/Resources/Locales/zh_TW.axaml | 1 + src/ViewModels/Histories.cs | 6 +- src/ViewModels/RevisionCompare.cs | 218 ++++++++++++++++++++++++++--- src/Views/RevisionCompare.axaml.cs | 46 ++++++ 6 files changed, 248 insertions(+), 25 deletions(-) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index d56c3aae..81f77e94 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -95,6 +95,7 @@ Generate commit message Merge (Built-in) Merge (External) + Reset File(s) to ${0}$ CHANGE DISPLAY MODE Show as File and Dir List Show as Path List diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index fb271d20..76fd84b5 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -99,6 +99,7 @@ 生成提交信息 解决冲突(内部工具) 解决冲突(外部工具) + 重置文件到 ${0}$ 切换变更显示模式 文件名+路径列表模式 全路径列表模式 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 73e7bc43..a69e1ee0 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -99,6 +99,7 @@ 產生提交訊息 解決衝突 (內建工具) 解決衝突 (外部工具) + 重設檔案到 ${0}$ 切換變更顯示模式 檔案名稱 + 路徑列表模式 全路徑列表模式 diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index c3ae33ba..1d13b38e 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -206,7 +206,7 @@ namespace SourceGit.ViewModels var end = commits[0] as Models.Commit; var start = commits[1] as Models.Commit; - DetailContext = new RevisionCompare(_repo.FullPath, start, end); + DetailContext = new RevisionCompare(_repo, start, end); } else { @@ -403,7 +403,7 @@ namespace SourceGit.ViewModels _repo.SearchCommitContext.Selected = null; head = await new Commands.QuerySingleCommit(_repo.FullPath, "HEAD").GetResultAsync(); if (head != null) - DetailContext = new RevisionCompare(_repo.FullPath, commit, head); + DetailContext = new RevisionCompare(_repo, commit, head); return null; } @@ -413,7 +413,7 @@ namespace SourceGit.ViewModels public void CompareWithWorktree(Models.Commit commit) { - DetailContext = new RevisionCompare(_repo.FullPath, commit, null); + DetailContext = new RevisionCompare(_repo, commit, null); } private Repository _repo = null; diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index 540338cc..e3b1650a 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; + using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -26,7 +28,30 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _endPoint, value); } - public bool CanSaveAsPatch { get; } + public string LeftSideDesc + { + get => GetDesc(StartPoint); + } + + public string RightSideDesc + { + get => GetDesc(EndPoint); + } + + public bool CanResetToLeft + { + get => !_repo.IsBare && _startPoint != null; + } + + public bool CanResetToRight + { + get => !_repo.IsBare && _endPoint != null; + } + + public bool CanSaveAsPatch + { + get => _startPoint != null && _endPoint != null; + } public int TotalChanges { @@ -50,7 +75,7 @@ namespace SourceGit.ViewModels if (value is { Count: 1 }) { var option = new Models.DiffOption(GetSHA(_startPoint), GetSHA(_endPoint), value[0]); - DiffContext = new DiffContext(_repo, option, _diffContext); + DiffContext = new DiffContext(_repo.FullPath, option, _diffContext); } else { @@ -76,12 +101,11 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _diffContext, value); } - public RevisionCompare(string repo, Models.Commit startPoint, Models.Commit endPoint) + public RevisionCompare(Repository repo, Models.Commit startPoint, Models.Commit endPoint) { _repo = repo; _startPoint = (object)startPoint ?? new Models.Null(); _endPoint = (object)endPoint ?? new Models.Null(); - CanSaveAsPatch = startPoint != null && endPoint != null; Refresh(); } @@ -100,23 +124,12 @@ namespace SourceGit.ViewModels public void OpenChangeWithExternalDiffTool(Models.Change change) { var opt = new Models.DiffOption(GetSHA(_startPoint), GetSHA(_endPoint), change); - new Commands.DiffTool(_repo, opt).Open(); + new Commands.DiffTool(_repo.FullPath, opt).Open(); } public void NavigateTo(string commitSHA) { - var launcher = App.GetLauncher(); - if (launcher == null) - return; - - foreach (var page in launcher.Pages) - { - if (page.Data is Repository repo && repo.FullPath.Equals(_repo)) - { - repo.NavigateToCommit(commitSHA); - break; - } - } + _repo?.NavigateToCommit(commitSHA); } public void Swap() @@ -130,14 +143,170 @@ namespace SourceGit.ViewModels public string GetAbsPath(string path) { - return Native.OS.GetAbsPath(_repo, path); + return Native.OS.GetAbsPath(_repo.FullPath, path); + } + + public async Task ResetToLeftAsync(Models.Change change) + { + var sha = GetSHA(_startPoint); + var log = _repo.CreateLog($"Reset File to '{GetDesc(_startPoint)}'"); + + if (change.Index == Models.ChangeState.Added) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(fullpath)) + await new Commands.Remove(_repo.FullPath, [change.Path]) + .Use(log) + .ExecAsync(); + } + else if (change.Index == Models.ChangeState.Renamed) + { + var renamed = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(renamed)) + await new Commands.Remove(_repo.FullPath, [change.Path]) + .Use(log) + .ExecAsync(); + + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.OriginalPath, sha); + } + else + { + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.Path, sha); + } + + log.Complete(); + } + + public async Task ResetToRightAsync(Models.Change change) + { + var sha = GetSHA(_endPoint); + var log = _repo.CreateLog($"Reset File to '{GetDesc(_endPoint)}'"); + + if (change.Index == Models.ChangeState.Deleted) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, change.Path); + if (File.Exists(fullpath)) + await new Commands.Remove(_repo.FullPath, [change.Path]) + .Use(log) + .ExecAsync(); + } + else if (change.Index == Models.ChangeState.Renamed) + { + var old = Native.OS.GetAbsPath(_repo.FullPath, change.OriginalPath); + if (File.Exists(old)) + await new Commands.Remove(_repo.FullPath, [change.OriginalPath]) + .Use(log) + .ExecAsync(); + + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.Path, sha); + } + else + { + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .FileWithRevisionAsync(change.Path, sha); + } + + log.Complete(); + } + + public async Task ResetMultipleToLeftAsync(List changes) + { + var sha = GetSHA(_startPoint); + var checkouts = new List(); + var removes = new List(); + + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Added) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(fullpath)) + removes.Add(c.Path); + } + else if (c.Index == Models.ChangeState.Renamed) + { + var old = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(old)) + removes.Add(c.Path); + + checkouts.Add(c.OriginalPath); + } + else + { + checkouts.Add(c.Path); + } + } + + var log = _repo.CreateLog($"Reset Files to '{GetDesc(_startPoint)}'"); + + if (removes.Count > 0) + await new Commands.Remove(_repo.FullPath, removes) + .Use(log) + .ExecAsync(); + + if (checkouts.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(checkouts, sha); + + log.Complete(); + } + + public async Task ResetMultipleToRightAsync(List changes) + { + var sha = GetSHA(_endPoint); + var checkouts = new List(); + var removes = new List(); + + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Deleted) + { + var fullpath = Native.OS.GetAbsPath(_repo.FullPath, c.Path); + if (File.Exists(fullpath)) + removes.Add(c.Path); + } + else if (c.Index == Models.ChangeState.Renamed) + { + var renamed = Native.OS.GetAbsPath(_repo.FullPath, c.OriginalPath); + if (File.Exists(renamed)) + removes.Add(c.OriginalPath); + + checkouts.Add(c.Path); + } + else + { + checkouts.Add(c.Path); + } + } + + var log = _repo.CreateLog($"Reset Files to '{GetDesc(_endPoint)}'"); + + if (removes.Count > 0) + await new Commands.Remove(_repo.FullPath, removes) + .Use(log) + .ExecAsync(); + + if (checkouts.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(checkouts, sha); + + log.Complete(); } public async Task SaveChangesAsPatchAsync(List changes, string saveTo) { - var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes ?? _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo); + var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo.FullPath, changes ?? _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo); if (succ) - App.SendNotification(_repo, App.Text("SaveAsPatchSuccess")); + App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } public void ClearSearchFilter() @@ -171,7 +340,7 @@ namespace SourceGit.ViewModels { Task.Run(async () => { - _changes = await new Commands.CompareRevisions(_repo, GetSHA(_startPoint), GetSHA(_endPoint)) + _changes = await new Commands.CompareRevisions(_repo.FullPath, GetSHA(_startPoint), GetSHA(_endPoint)) .ReadAsync() .ConfigureAwait(false); @@ -205,7 +374,12 @@ namespace SourceGit.ViewModels return obj is Models.Commit commit ? commit.SHA : string.Empty; } - private string _repo; + private string GetDesc(object obj) + { + return obj is Models.Commit commit ? commit.GetFriendlyName() : App.Text("Worktree"); + } + + private Repository _repo; private bool _isLoading = true; private object _startPoint = null; private object _endPoint = null; diff --git a/src/Views/RevisionCompare.axaml.cs b/src/Views/RevisionCompare.axaml.cs index d9c830a7..f296aff5 100644 --- a/src/Views/RevisionCompare.axaml.cs +++ b/src/Views/RevisionCompare.axaml.cs @@ -83,6 +83,26 @@ namespace SourceGit.Views menu.Items.Add(explore); } + var resetToLeft = new MenuItem(); + resetToLeft.Header = App.Text("ChangeCM.ResetFileTo", vm.LeftSideDesc); + resetToLeft.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToLeft.IsEnabled = vm.CanResetToLeft; + resetToLeft.Click += async (_, ev) => + { + await vm.ResetToLeftAsync(change); + ev.Handled = true; + }; + + var resetToRight = new MenuItem(); + resetToRight.Header = App.Text("ChangeCM.ResetFileTo", vm.RightSideDesc); + resetToRight.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToRight.IsEnabled = vm.CanResetToRight; + resetToRight.Click += async (_, ev) => + { + await vm.ResetToRightAsync(change); + ev.Handled = true; + }; + var copyPath = new MenuItem(); copyPath.Header = App.Text("CopyPath"); copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); @@ -106,11 +126,34 @@ namespace SourceGit.Views menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(patch); menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(resetToLeft); + menu.Items.Add(resetToRight); + menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(copyPath); menu.Items.Add(copyFullPath); } else { + var resetToLeft = new MenuItem(); + resetToLeft.Header = App.Text("ChangeCM.ResetFileTo", vm.LeftSideDesc); + resetToLeft.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToLeft.IsEnabled = vm.CanResetToLeft; + resetToLeft.Click += async (_, ev) => + { + await vm.ResetMultipleToLeftAsync(selected); + ev.Handled = true; + }; + + var resetToRight = new MenuItem(); + resetToRight.Header = App.Text("ChangeCM.ResetFileTo", vm.RightSideDesc); + resetToRight.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToRight.IsEnabled = vm.CanResetToRight; + resetToRight.Click += async (_, ev) => + { + await vm.ResetMultipleToRightAsync(selected); + ev.Handled = true; + }; + var copyPath = new MenuItem(); copyPath.Header = App.Text("CopyPath"); copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); @@ -141,6 +184,9 @@ namespace SourceGit.Views menu.Items.Add(patch); menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(resetToLeft); + menu.Items.Add(resetToRight); + menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(copyPath); menu.Items.Add(copyFullPath); }