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
This commit is contained in:
leo 2024-07-31 12:04:29 +08:00
parent 3c5a661fa0
commit f55a576013
No known key found for this signature in database
9 changed files with 261 additions and 86 deletions

View file

@ -11,7 +11,7 @@ namespace SourceGit.Commands
new Clean(repo).Exec(); new Clean(repo).Exec();
} }
public static void ChangesInWorkTree(string repo, List<Models.Change> changes) public static void Changes(string repo, List<Models.Change> changes)
{ {
var needClean = new List<string>(); var needClean = new List<string>();
var needCheckout = new List<string>(); var needCheckout = new List<string>();
@ -19,13 +19,9 @@ namespace SourceGit.Commands
foreach (var c in changes) foreach (var c in changes)
{ {
if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added) if (c.WorkTree == Models.ChangeState.Untracked || c.WorkTree == Models.ChangeState.Added)
{
needClean.Add(c.Path); needClean.Add(c.Path);
}
else else
{
needCheckout.Add(c.Path); needCheckout.Add(c.Path);
}
} }
for (int i = 0; i < needClean.Count; i += 10) 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(); new Restore(repo, needCheckout.GetRange(i, count), "--worktree --recurse-submodules").Exec();
} }
} }
public static void ChangesInStaged(string repo, List<Models.Change> changes)
{
for (int i = 0; i < changes.Count; i += 10)
{
var count = Math.Min(10, changes.Count - i);
var files = new List<string>();
for (int j = 0; j < count; j++)
files.Add(changes[i + j].Path);
new Restore(repo, files, "--staged --worktree --recurse-submodules").Exec();
}
}
} }
} }

View file

@ -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<Models.Change> Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess)
{
var changes = new List<Models.Change>();
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 [];
}
}
}

View file

@ -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<Models.Change> 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();
}
}

View file

@ -22,12 +22,19 @@ namespace SourceGit.Models
Untracked Untracked
} }
public class ChangeDataForAmend
{
public string FileMode { get; set; } = "";
public string ObjectHash { get; set; } = "";
}
public class Change public class Change
{ {
public ChangeState Index { get; set; } public ChangeState Index { get; set; } = ChangeState.None;
public ChangeState WorkTree { get; set; } = ChangeState.None; public ChangeState WorkTree { get; set; } = ChangeState.None;
public string Path { get; set; } = ""; public string Path { get; set; } = "";
public string OriginalPath { get; set; } = ""; public string OriginalPath { get; set; } = "";
public ChangeDataForAmend DataForAmend { get; set; } = null;
public bool IsConflit public bool IsConflit
{ {

View file

@ -39,7 +39,11 @@ namespace SourceGit.Models
} }
else else
{ {
_extra = "--cached"; if (change.DataForAmend != null)
_extra = "--cached HEAD^";
else
_extra = "--cached";
_path = change.Path; _path = change.Path;
_orgPath = change.OriginalPath; _orgPath = change.OriginalPath;
} }

View file

@ -19,11 +19,10 @@ namespace SourceGit.ViewModels
View = new Views.Discard { DataContext = this }; View = new Views.Discard { DataContext = this };
} }
public Discard(Repository repo, List<Models.Change> changes, bool isUnstaged) public Discard(Repository repo, List<Models.Change> changes)
{ {
_repo = repo; _repo = repo;
_changes = changes; _changes = changes;
_isUnstaged = isUnstaged;
if (_changes == null) if (_changes == null)
Mode = new Models.Null(); Mode = new Models.Null();
@ -44,10 +43,8 @@ namespace SourceGit.ViewModels
{ {
if (_changes == null) if (_changes == null)
Commands.Discard.All(_repo.FullPath); Commands.Discard.All(_repo.FullPath);
else if (_isUnstaged)
Commands.Discard.ChangesInWorkTree(_repo.FullPath, _changes);
else else
Commands.Discard.ChangesInStaged(_repo.FullPath, _changes); Commands.Discard.Changes(_repo.FullPath, _changes);
CallUIThread(() => CallUIThread(() =>
{ {
@ -61,6 +58,5 @@ namespace SourceGit.ViewModels
private readonly Repository _repo = null; private readonly Repository _repo = null;
private readonly List<Models.Change> _changes = null; private readonly List<Models.Change> _changes = null;
private readonly bool _isUnstaged = true;
} }
} }

View file

@ -88,21 +88,26 @@ namespace SourceGit.ViewModels
get => _useAmend; get => _useAmend;
set set
{ {
if (SetProperty(ref _useAmend, value) && value) if (SetProperty(ref _useAmend, value))
{ {
var currentBranch = _repo.CurrentBranch; if (value)
if (currentBranch == null)
{ {
App.RaiseException(_repo.FullPath, "No commits to amend!!!"); var currentBranch = _repo.CurrentBranch;
_useAmend = false; if (currentBranch == null)
OnPropertyChanged(); {
return; 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<Models.Change> changes) public bool SetData(List<Models.Change> changes)
{ {
_cached = changes;
var unstaged = new List<Models.Change>(); var unstaged = new List<Models.Change>();
var staged = new List<Models.Change>(); var staged = new List<Models.Change>();
var selectedUnstaged = new List<Models.Change>(); var selectedUnstaged = new List<Models.Change>();
@ -237,17 +244,6 @@ namespace SourceGit.ViewModels
var hasConflict = false; var hasConflict = false;
foreach (var c in changes) 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) if (c.WorkTree != Models.ChangeState.None)
{ {
unstaged.Add(c); 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; _count = changes.Count;
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
@ -358,7 +361,11 @@ namespace SourceGit.ViewModels
SetDetail(null); SetDetail(null);
IsUnstaging = true; IsUnstaging = true;
_repo.SetWatcherEnabled(false); _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()); await Task.Run(() => new Commands.Reset(_repo.FullPath).Exec());
} }
@ -376,24 +383,14 @@ namespace SourceGit.ViewModels
IsUnstaging = false; IsUnstaging = false;
} }
public void Discard(List<Models.Change> changes, bool isUnstaged) public void Discard(List<Models.Change> changes)
{ {
if (PopupHost.CanCreatePopup()) if (PopupHost.CanCreatePopup())
{ {
if (isUnstaged) if (changes.Count == _unstaged.Count && _staged.Count == 0)
{ PopupHost.ShowPopup(new Discard(_repo));
if (changes.Count == _unstaged.Count && _staged.Count == 0)
PopupHost.ShowPopup(new Discard(_repo));
else
PopupHost.ShowPopup(new Discard(_repo, changes, true));
}
else else
{ PopupHost.ShowPopup(new Discard(_repo, changes));
if (changes.Count == _staged.Count && _unstaged.Count == 0)
PopupHost.ShowPopup(new Discard(_repo));
else
PopupHost.ShowPopup(new Discard(_repo, changes, false));
}
} }
} }
@ -491,7 +488,7 @@ namespace SourceGit.ViewModels
discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) => discard.Click += (_, e) =>
{ {
Discard(_selectedUnstaged, true); Discard(_selectedUnstaged);
e.Handled = true; e.Handled = true;
}; };
@ -815,7 +812,7 @@ namespace SourceGit.ViewModels
discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) => discard.Click += (_, e) =>
{ {
Discard(_selectedUnstaged, true); Discard(_selectedUnstaged);
e.Handled = true; e.Handled = true;
}; };
@ -904,15 +901,6 @@ namespace SourceGit.ViewModels
e.Handled = true; 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(); var stash = new MenuItem();
stash.Header = App.Text("FileCM.Stash"); stash.Header = App.Text("FileCM.Stash");
stash.Icon = App.CreateMenuIcon("Icons.Stashes"); stash.Icon = App.CreateMenuIcon("Icons.Stashes");
@ -971,7 +959,6 @@ namespace SourceGit.ViewModels
menu.Items.Add(openWith); menu.Items.Add(openWith);
menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(unstage); menu.Items.Add(unstage);
menu.Items.Add(discard);
menu.Items.Add(stash); menu.Items.Add(stash);
menu.Items.Add(patch); menu.Items.Add(patch);
menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(new MenuItem() { Header = "-" });
@ -1071,15 +1058,6 @@ namespace SourceGit.ViewModels
e.Handled = true; 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(); var stash = new MenuItem();
stash.Header = App.Text("FileCM.StashMulti", _selectedStaged.Count); stash.Header = App.Text("FileCM.StashMulti", _selectedStaged.Count);
stash.Icon = App.CreateMenuIcon("Icons.Stashes"); stash.Icon = App.CreateMenuIcon("Icons.Stashes");
@ -1118,7 +1096,6 @@ namespace SourceGit.ViewModels
}; };
menu.Items.Add(unstage); menu.Items.Add(unstage);
menu.Items.Add(discard);
menu.Items.Add(stash); menu.Items.Add(stash);
menu.Items.Add(patch); menu.Items.Add(patch);
} }
@ -1162,6 +1139,25 @@ namespace SourceGit.ViewModels
return menu; return menu;
} }
private List<Models.Change> GetStagedChanges()
{
if (_useAmend)
{
return new Commands.QueryStagedChangesWithAmend(_repo.FullPath).Result();
}
else
{
var rs = new List<Models.Change>();
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) private void SetDetail(Models.Change change)
{ {
if (_isLoadingData) if (_isLoadingData)
@ -1287,6 +1283,7 @@ namespace SourceGit.ViewModels
private bool _isCommitting = false; private bool _isCommitting = false;
private bool _useAmend = false; private bool _useAmend = false;
private bool _canCommitWithPush = false; private bool _canCommitWithPush = false;
private List<Models.Change> _cached = [];
private List<Models.Change> _unstaged = []; private List<Models.Change> _unstaged = [];
private List<Models.Change> _staged = []; private List<Models.Change> _staged = [];
private List<Models.Change> _selectedUnstaged = []; private List<Models.Change> _selectedUnstaged = [];

View file

@ -77,7 +77,7 @@
<StackPanel x:Name="Popup" IsVisible="False" Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Right" Effect="drop-shadow(0 0 6 #40000000)"> <StackPanel x:Name="Popup" IsVisible="False" Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Right" Effect="drop-shadow(0 0 6 #40000000)">
<Button Classes="flat" Content="{DynamicResource Text.Hunk.Stage}" Click="OnStageChunk" IsVisible="{Binding #ThisControl.IsUnstagedChange}"/> <Button Classes="flat" Content="{DynamicResource Text.Hunk.Stage}" Click="OnStageChunk" IsVisible="{Binding #ThisControl.IsUnstagedChange}"/>
<Button Classes="flat" Content="{DynamicResource Text.Hunk.Unstage}" Click="OnUnstageChunk" IsVisible="{Binding #ThisControl.IsUnstagedChange, Converter={x:Static BoolConverters.Not}}"/> <Button Classes="flat" Content="{DynamicResource Text.Hunk.Unstage}" Click="OnUnstageChunk" IsVisible="{Binding #ThisControl.IsUnstagedChange, Converter={x:Static BoolConverters.Not}}"/>
<Button Classes="flat" Content="{DynamicResource Text.Hunk.Discard}" Margin="8,0,0,0" Click="OnDiscardChunk"/> <Button Classes="flat" Content="{DynamicResource Text.Hunk.Discard}" Margin="8,0,0,0" Click="OnDiscardChunk" IsVisible="{Binding #ThisControl.IsUnstagedChange}"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View file

@ -1273,7 +1273,7 @@ namespace SourceGit.Views
return; return;
var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy; var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy;
workcopy?.Discard(new List<Models.Change> { change }, diff.Option.IsUnstaged); workcopy?.Discard(new List<Models.Change> { change });
} }
else else
{ {
@ -1303,7 +1303,7 @@ namespace SourceGit.Views
diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, chunk.IsOldSide, tmpFile); diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, chunk.IsOldSide, tmpFile);
} }
new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", diff.Option.IsUnstaged ? "--reverse" : "--index --reverse").Exec(); new Commands.Apply(diff.Repo, tmpFile, true, "nowarn", "--reverse").Exec();
File.Delete(tmpFile); File.Delete(tmpFile);
repo.MarkWorkingCopyDirtyManually(); repo.MarkWorkingCopyDirtyManually();