From f55a5760135c1bd12749b4db5d8a18c7dc7d428a Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 31 Jul 2024 12:04:29 +0800 Subject: [PATCH] refactor: rewrite amend behaviour (#300) * toggle amend will show changes in HEAD commit * since discard is not compatible with staged changes in `amend` mode, we only allows user to discard unstaged changes --- src/Commands/Discard.cs | 18 +--- src/Commands/QueryStagedChangesWithAmend.cs | 90 ++++++++++++++++ src/Commands/UnstageChangesForAmend.cs | 97 +++++++++++++++++ src/Models/Change.cs | 9 +- src/Models/DiffOption.cs | 6 +- src/ViewModels/Discard.cs | 8 +- src/ViewModels/WorkingCopy.cs | 113 ++++++++++---------- src/Views/TextDiffView.axaml | 2 +- src/Views/TextDiffView.axaml.cs | 4 +- 9 files changed, 261 insertions(+), 86 deletions(-) create mode 100644 src/Commands/QueryStagedChangesWithAmend.cs create mode 100644 src/Commands/UnstageChangesForAmend.cs diff --git a/src/Commands/Discard.cs b/src/Commands/Discard.cs index 22b579a3..63fcaa8e 100644 --- a/src/Commands/Discard.cs +++ b/src/Commands/Discard.cs @@ -11,7 +11,7 @@ namespace SourceGit.Commands new Clean(repo).Exec(); } - public static void ChangesInWorkTree(string repo, List changes) + public static void Changes(string repo, List changes) { var needClean = new List(); var needCheckout = new List(); @@ -19,13 +19,9 @@ namespace SourceGit.Commands foreach (var c in changes) { if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) - { needClean.Add(c.Path); - } else - { needCheckout.Add(c.Path); - } } for (int i = 0; i < needClean.Count; i += 10) @@ -40,17 +36,5 @@ namespace SourceGit.Commands new Restore(repo, needCheckout.GetRange(i, count), "--worktree --recurse-submodules").Exec(); } } - - public static void ChangesInStaged(string repo, List changes) - { - for (int i = 0; i < changes.Count; i += 10) - { - var count = Math.Min(10, changes.Count - i); - var files = new List(); - for (int j = 0; j < count; j++) - files.Add(changes[i + j].Path); - new Restore(repo, files, "--staged --worktree --recurse-submodules").Exec(); - } - } } } diff --git a/src/Commands/QueryStagedChangesWithAmend.cs b/src/Commands/QueryStagedChangesWithAmend.cs new file mode 100644 index 00000000..93db31cb --- /dev/null +++ b/src/Commands/QueryStagedChangesWithAmend.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace SourceGit.Commands +{ + public partial class QueryStagedChangesWithAmend : Command + { + [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ACDMTUX])\d{0,6}\t(.*)$")] + private static partial Regex REG_FORMAT1(); + [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} R\d{0,6}\t(.*\t.*)$")] + private static partial Regex REG_FORMAT2(); + + public QueryStagedChangesWithAmend(string repo) + { + WorkingDirectory = repo; + Context = repo; + Args = "diff-index --cached -M HEAD^"; + } + + public List Result() + { + var rs = ReadToEnd(); + if (rs.IsSuccess) + { + var changes = new List(); + var lines = rs.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var match = REG_FORMAT2().Match(line); + if (match.Success) + { + var change = new Models.Change() { + Path = match.Groups[3].Value, + DataForAmend = new Models.ChangeDataForAmend() + { + FileMode = match.Groups[1].Value, + ObjectHash = match.Groups[2].Value, + }, + }; + change.Set(Models.ChangeState.Renamed); + changes.Add(change); + continue; + } + + match = REG_FORMAT1().Match(line); + if (match.Success) + { + var change = new Models.Change() { + Path = match.Groups[4].Value, + DataForAmend = new Models.ChangeDataForAmend() + { + FileMode = match.Groups[1].Value, + ObjectHash = match.Groups[2].Value, + }, + }; + + var type = match.Groups[3].Value; + switch (type) + { + case "A": + change.Set(Models.ChangeState.Added); + break; + case "C": + change.Set(Models.ChangeState.Copied); + break; + case "D": + change.Set(Models.ChangeState.Deleted); + break; + case "M": + change.Set(Models.ChangeState.Modified); + break; + case "T": + change.Set(Models.ChangeState.TypeChanged); + break; + case "U": + change.Set(Models.ChangeState.Unmerged); + break; + } + changes.Add(change); + } + } + + return changes; + } + + return []; + } + } +} diff --git a/src/Commands/UnstageChangesForAmend.cs b/src/Commands/UnstageChangesForAmend.cs new file mode 100644 index 00000000..c930f136 --- /dev/null +++ b/src/Commands/UnstageChangesForAmend.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +using Avalonia.Threading; + +namespace SourceGit.Commands +{ + public class UnstageChangesForAmend + { + public UnstageChangesForAmend(string repo, List changes) + { + _repo = repo; + + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Renamed) + { + _patchBuilder.Append("0 0000000000000000000000000000000000000000\t"); + _patchBuilder.Append(c.Path); + _patchBuilder.Append("\0100644 "); + _patchBuilder.Append(c.DataForAmend.ObjectHash); + _patchBuilder.Append("\t"); + _patchBuilder.Append(c.OriginalPath); + _patchBuilder.Append("\n"); + } + else if (c.Index == Models.ChangeState.Added) + { + _patchBuilder.Append("0 0000000000000000000000000000000000000000\t"); + _patchBuilder.Append(c.Path); + _patchBuilder.Append("\n"); + } + else if (c.Index == Models.ChangeState.Deleted) + { + _patchBuilder.Append("100644 "); + _patchBuilder.Append(c.DataForAmend.ObjectHash); + _patchBuilder.Append("\t"); + _patchBuilder.Append(c.Path); + _patchBuilder.Append("\n"); + } + else + { + _patchBuilder.Append(c.DataForAmend.FileMode); + _patchBuilder.Append(" "); + _patchBuilder.Append(c.DataForAmend.ObjectHash); + _patchBuilder.Append("\t"); + _patchBuilder.Append(c.Path); + _patchBuilder.Append("\n"); + } + } + } + + public bool Exec() + { + var starter = new ProcessStartInfo(); + starter.WorkingDirectory = _repo; + starter.FileName = Native.OS.GitExecutable; + starter.Arguments = "-c core.editor=true update-index --index-info"; + starter.UseShellExecute = false; + starter.CreateNoWindow = true; + starter.WindowStyle = ProcessWindowStyle.Hidden; + starter.RedirectStandardInput = true; + starter.RedirectStandardOutput = false; + starter.RedirectStandardError = true; + + try + { + var proc = new Process() { StartInfo = starter }; + proc.Start(); + proc.StandardInput.Write(_patchBuilder.ToString()); + proc.StandardInput.Close(); + + var err = proc.StandardError.ReadToEnd(); + proc.WaitForExit(); + var rs = proc.ExitCode == 0; + proc.Close(); + + if (!rs) + Dispatcher.UIThread.Invoke(() => App.RaiseException(_repo, err)); + + return rs; + } + catch (Exception e) + { + Dispatcher.UIThread.Invoke(() => + { + App.RaiseException(_repo, "Failed to unstage changes: " + e.Message); + }); + return false; + } + } + + private string _repo = ""; + private StringBuilder _patchBuilder = new StringBuilder(); + } +} diff --git a/src/Models/Change.cs b/src/Models/Change.cs index 07ce4e2b..36fe20ac 100644 --- a/src/Models/Change.cs +++ b/src/Models/Change.cs @@ -22,12 +22,19 @@ namespace SourceGit.Models Untracked } + public class ChangeDataForAmend + { + public string FileMode { get; set; } = ""; + public string ObjectHash { get; set; } = ""; + } + public class Change { - public ChangeState Index { get; set; } + public ChangeState Index { get; set; } = ChangeState.None; public ChangeState WorkTree { get; set; } = ChangeState.None; public string Path { get; set; } = ""; public string OriginalPath { get; set; } = ""; + public ChangeDataForAmend DataForAmend { get; set; } = null; public bool IsConflit { diff --git a/src/Models/DiffOption.cs b/src/Models/DiffOption.cs index e122ad67..98387e7f 100644 --- a/src/Models/DiffOption.cs +++ b/src/Models/DiffOption.cs @@ -39,7 +39,11 @@ namespace SourceGit.Models } else { - _extra = "--cached"; + if (change.DataForAmend != null) + _extra = "--cached HEAD^"; + else + _extra = "--cached"; + _path = change.Path; _orgPath = change.OriginalPath; } diff --git a/src/ViewModels/Discard.cs b/src/ViewModels/Discard.cs index f37d5f0c..916c3b86 100644 --- a/src/ViewModels/Discard.cs +++ b/src/ViewModels/Discard.cs @@ -19,11 +19,10 @@ namespace SourceGit.ViewModels View = new Views.Discard { DataContext = this }; } - public Discard(Repository repo, List changes, bool isUnstaged) + public Discard(Repository repo, List changes) { _repo = repo; _changes = changes; - _isUnstaged = isUnstaged; if (_changes == null) Mode = new Models.Null(); @@ -44,10 +43,8 @@ namespace SourceGit.ViewModels { if (_changes == null) Commands.Discard.All(_repo.FullPath); - else if (_isUnstaged) - Commands.Discard.ChangesInWorkTree(_repo.FullPath, _changes); else - Commands.Discard.ChangesInStaged(_repo.FullPath, _changes); + Commands.Discard.Changes(_repo.FullPath, _changes); CallUIThread(() => { @@ -61,6 +58,5 @@ namespace SourceGit.ViewModels private readonly Repository _repo = null; private readonly List _changes = null; - private readonly bool _isUnstaged = true; } } diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 4a5c7c4e..813be025 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -88,21 +88,26 @@ namespace SourceGit.ViewModels get => _useAmend; set { - if (SetProperty(ref _useAmend, value) && value) + if (SetProperty(ref _useAmend, value)) { - var currentBranch = _repo.CurrentBranch; - if (currentBranch == null) + if (value) { - App.RaiseException(_repo.FullPath, "No commits to amend!!!"); - _useAmend = false; - OnPropertyChanged(); - return; + var currentBranch = _repo.CurrentBranch; + if (currentBranch == null) + { + App.RaiseException(_repo.FullPath, "No commits to amend!!!"); + _useAmend = false; + OnPropertyChanged(); + return; + } + + CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, currentBranch.Head).Result(); } - CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, currentBranch.Head).Result(); + Staged = GetStagedChanges(); + SelectedStaged = []; + OnPropertyChanged(nameof(IsCommitWithPushVisible)); } - - OnPropertyChanged(nameof(IsCommitWithPushVisible)); } } @@ -216,6 +221,8 @@ namespace SourceGit.ViewModels public bool SetData(List changes) { + _cached = changes; + var unstaged = new List(); var staged = new List(); var selectedUnstaged = new List(); @@ -237,17 +244,6 @@ namespace SourceGit.ViewModels var hasConflict = false; foreach (var c in changes) { - if (c.Index == Models.ChangeState.Modified - || c.Index == Models.ChangeState.Added - || c.Index == Models.ChangeState.Deleted - || c.Index == Models.ChangeState.Renamed) - { - staged.Add(c); - - if (lastSelectedStaged.Contains(c.Path)) - selectedStaged.Add(c); - } - if (c.WorkTree != Models.ChangeState.None) { unstaged.Add(c); @@ -258,6 +254,13 @@ namespace SourceGit.ViewModels } } + staged = GetStagedChanges(); + foreach (var c in staged) + { + if (lastSelectedStaged.Contains(c.Path)) + selectedStaged.Add(c); + } + _count = changes.Count; Dispatcher.UIThread.Invoke(() => @@ -358,7 +361,11 @@ namespace SourceGit.ViewModels SetDetail(null); IsUnstaging = true; _repo.SetWatcherEnabled(false); - if (changes.Count == _staged.Count) + if (_useAmend) + { + await Task.Run(() => new Commands.UnstageChangesForAmend(_repo.FullPath, changes).Exec()); + } + else if (changes.Count == _staged.Count) { await Task.Run(() => new Commands.Reset(_repo.FullPath).Exec()); } @@ -376,24 +383,14 @@ namespace SourceGit.ViewModels IsUnstaging = false; } - public void Discard(List changes, bool isUnstaged) + public void Discard(List changes) { if (PopupHost.CanCreatePopup()) { - if (isUnstaged) - { - if (changes.Count == _unstaged.Count && _staged.Count == 0) - PopupHost.ShowPopup(new Discard(_repo)); - else - PopupHost.ShowPopup(new Discard(_repo, changes, true)); - } + if (changes.Count == _unstaged.Count && _staged.Count == 0) + PopupHost.ShowPopup(new Discard(_repo)); else - { - if (changes.Count == _staged.Count && _unstaged.Count == 0) - PopupHost.ShowPopup(new Discard(_repo)); - else - PopupHost.ShowPopup(new Discard(_repo, changes, false)); - } + PopupHost.ShowPopup(new Discard(_repo, changes)); } } @@ -491,7 +488,7 @@ namespace SourceGit.ViewModels discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Click += (_, e) => { - Discard(_selectedUnstaged, true); + Discard(_selectedUnstaged); e.Handled = true; }; @@ -815,7 +812,7 @@ namespace SourceGit.ViewModels discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Click += (_, e) => { - Discard(_selectedUnstaged, true); + Discard(_selectedUnstaged); e.Handled = true; }; @@ -904,15 +901,6 @@ namespace SourceGit.ViewModels e.Handled = true; }; - var discard = new MenuItem(); - discard.Header = App.Text("FileCM.Discard"); - discard.Icon = App.CreateMenuIcon("Icons.Undo"); - discard.Click += (_, e) => - { - Discard(_selectedStaged, false); - e.Handled = true; - }; - var stash = new MenuItem(); stash.Header = App.Text("FileCM.Stash"); stash.Icon = App.CreateMenuIcon("Icons.Stashes"); @@ -971,7 +959,6 @@ namespace SourceGit.ViewModels menu.Items.Add(openWith); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(unstage); - menu.Items.Add(discard); menu.Items.Add(stash); menu.Items.Add(patch); menu.Items.Add(new MenuItem() { Header = "-" }); @@ -1071,15 +1058,6 @@ namespace SourceGit.ViewModels e.Handled = true; }; - var discard = new MenuItem(); - discard.Header = App.Text("FileCM.DiscardMulti", _selectedStaged.Count); - discard.Icon = App.CreateMenuIcon("Icons.Undo"); - discard.Click += (_, e) => - { - Discard(_selectedStaged, false); - e.Handled = true; - }; - var stash = new MenuItem(); stash.Header = App.Text("FileCM.StashMulti", _selectedStaged.Count); stash.Icon = App.CreateMenuIcon("Icons.Stashes"); @@ -1118,7 +1096,6 @@ namespace SourceGit.ViewModels }; menu.Items.Add(unstage); - menu.Items.Add(discard); menu.Items.Add(stash); menu.Items.Add(patch); } @@ -1162,6 +1139,25 @@ namespace SourceGit.ViewModels return menu; } + private List GetStagedChanges() + { + if (_useAmend) + { + return new Commands.QueryStagedChangesWithAmend(_repo.FullPath).Result(); + } + else + { + var rs = new List(); + foreach (var c in _cached) + { + if (c.Index != Models.ChangeState.None && + c.Index != Models.ChangeState.Untracked) + rs.Add(c); + } + return rs; + } + } + private void SetDetail(Models.Change change) { if (_isLoadingData) @@ -1287,6 +1283,7 @@ namespace SourceGit.ViewModels private bool _isCommitting = false; private bool _useAmend = false; private bool _canCommitWithPush = false; + private List _cached = []; private List _unstaged = []; private List _staged = []; private List _selectedUnstaged = []; diff --git a/src/Views/TextDiffView.axaml b/src/Views/TextDiffView.axaml index 4b1cad3d..b49faa7d 100644 --- a/src/Views/TextDiffView.axaml +++ b/src/Views/TextDiffView.axaml @@ -77,7 +77,7 @@