diff --git a/src/Commands/Remove.cs b/src/Commands/Remove.cs new file mode 100644 index 000000000..17b1e2b7f --- /dev/null +++ b/src/Commands/Remove.cs @@ -0,0 +1,23 @@ +namespace SourceGit.Commands +{ + public class Remove : Command + { + public Remove(string repo) + { + WorkingDirectory = repo; + Context = repo; + } + + public Remove File(string file) + { + Args = $"rm --force --ignore-unmatch -- {file.Quoted()}"; + return this; + } + + public Remove Files(string pathspecFromFile) + { + Args = $"rm --force --ignore-unmatch --pathspec-from-file={pathspecFromFile.Quoted()}"; + return this; + } + } +} diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index ae525db93..9d1d082f6 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -247,10 +247,20 @@ public async Task SaveChangesAsPatchAsync(List changes, string sa App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } - public async Task ResetToThisRevisionAsync(string path) + public async Task ResetToThisRevisionAsync(Models.Change change) { var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'"); - await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(path, _commit.SHA); + + // If file is Deleted in this commit, it doesn't exist in this revision - remove it + if (change.Index == Models.ChangeState.Deleted) + { + await new Commands.Remove(_repo.FullPath).Use(log).File(change.Path).ExecAsync(); + } + else + { + await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.Path, _commit.SHA); + } + log.Complete(); } @@ -258,45 +268,95 @@ public async Task ResetToParentRevisionAsync(Models.Change change) { var log = _repo.CreateLog($"Reset File to '{_commit.SHA}~1'"); - if (change.Index == Models.ChangeState.Renamed) - await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.OriginalPath, $"{_commit.SHA}~1"); + // If file is Added in this commit, it doesn't exist in parent - remove it + if (change.Index == Models.ChangeState.Added) + { + await new Commands.Remove(_repo.FullPath).Use(log).File(change.Path).ExecAsync(); + } + else + { + // Handle renamed files - restore original path from parent + if (change.Index == Models.ChangeState.Renamed) + await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.OriginalPath, $"{_commit.SHA}~1"); + + await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.Path, $"{_commit.SHA}~1"); + } - await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.Path, $"{_commit.SHA}~1"); log.Complete(); } public async Task ResetMultipleToThisRevisionAsync(List changes) { - var files = new List(); + var filesToCheckout = new List(); + var filesToRemove = new List(); + + // Separate files: Deleted files don't exist in this revision, so remove them foreach (var c in changes) - files.Add(c.Path); + { + if (c.Index == Models.ChangeState.Deleted) + filesToRemove.Add(c.Path); + else + filesToCheckout.Add(c.Path); + } var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}'"); - await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(files, _commit.SHA); + + if (filesToCheckout.Count > 0) + await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(filesToCheckout, _commit.SHA); + + if (filesToRemove.Count > 0) + { + var pathSpecFile = System.IO.Path.GetTempFileName(); + await System.IO.File.WriteAllLinesAsync(pathSpecFile, filesToRemove); + await new Commands.Remove(_repo.FullPath).Use(log).Files(pathSpecFile).ExecAsync(); + System.IO.File.Delete(pathSpecFile); + } + log.Complete(); } public async Task ResetMultipleToParentRevisionAsync(List changes) { var renamed = new List(); - var modified = new List(); + var filesToCheckout = new List(); + var filesToRemove = new List(); + // Separate files by type foreach (var c in changes) { - if (c.Index == Models.ChangeState.Renamed) + if (c.Index == Models.ChangeState.Added) + { + // Added files don't exist in parent - remove them + filesToRemove.Add(c.Path); + } + else if (c.Index == Models.ChangeState.Renamed) + { + // Renamed files - restore original path from parent renamed.Add(c.OriginalPath); + } else - modified.Add(c.Path); + { + // Other files - checkout from parent + filesToCheckout.Add(c.Path); + } } var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}~1'"); - if (modified.Count > 0) - await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(modified, $"{_commit.SHA}~1"); + if (filesToCheckout.Count > 0) + await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(filesToCheckout, $"{_commit.SHA}~1"); if (renamed.Count > 0) await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(renamed, $"{_commit.SHA}~1"); + if (filesToRemove.Count > 0) + { + var pathSpecFile = System.IO.Path.GetTempFileName(); + await System.IO.File.WriteAllLinesAsync(pathSpecFile, filesToRemove); + await new Commands.Remove(_repo.FullPath).Use(log).Files(pathSpecFile).ExecAsync(); + System.IO.File.Delete(pathSpecFile); + } + log.Complete(); } diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index c3ae33ba1..bfe32ea72 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -206,7 +206,7 @@ public void Select(IList commits) 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.FullPath, _repo, start, end); } else { @@ -403,7 +403,7 @@ public async Task GetCommitFullMessageAsync(Models.Commit commit) _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.FullPath, _repo, commit, head); return null; } @@ -413,7 +413,7 @@ public async Task GetCommitFullMessageAsync(Models.Commit commit) public void CompareWithWorktree(Models.Commit commit) { - DetailContext = new RevisionCompare(_repo.FullPath, commit, null); + DetailContext = new RevisionCompare(_repo.FullPath, _repo, commit, null); } private Repository _repo = null; diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index 540338cc4..ab7c0bfb5 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -28,6 +28,8 @@ public object EndPoint public bool CanSaveAsPatch { get; } + public bool CanResetFiles => _repository != null && !_repository.IsBare; + public int TotalChanges { get => _totalChanges; @@ -77,8 +79,14 @@ public DiffContext DiffContext } public RevisionCompare(string repo, Models.Commit startPoint, Models.Commit endPoint) + : this(repo, null, startPoint, endPoint) + { + } + + public RevisionCompare(string repo, Repository repository, Models.Commit startPoint, Models.Commit endPoint) { _repo = repo; + _repository = repository; _startPoint = (object)startPoint ?? new Models.Null(); _endPoint = (object)endPoint ?? new Models.Null(); CanSaveAsPatch = startPoint != null && endPoint != null; @@ -88,6 +96,7 @@ public RevisionCompare(string repo, Models.Commit startPoint, Models.Commit endP public void Dispose() { _repo = null; + _repository = null; _startPoint = null; _endPoint = null; _changes?.Clear(); @@ -140,6 +149,116 @@ public async Task SaveChangesAsPatchAsync(List changes, string sa App.SendNotification(_repo, App.Text("SaveAsPatchSuccess")); } + public async Task ResetToSourceRevisionAsync(Models.Change change) + { + var sourceSHA = GetSHA(_startPoint); + if (string.IsNullOrEmpty(sourceSHA)) + return; + + var log = _repository?.CreateLog($"Reset File to '{sourceSHA}'"); + + // If file is Added in diff, it doesn't exist in source - remove it + if (change.Index == Models.ChangeState.Added) + { + await new Commands.Remove(_repo).Use(log).File(change.Path).ExecAsync(); + } + else + { + await new Commands.Checkout(_repo).Use(log).FileWithRevisionAsync(change.Path, sourceSHA); + } + + log?.Complete(); + } + + public async Task ResetToTargetRevisionAsync(Models.Change change) + { + var targetSHA = GetSHA(_endPoint); + if (string.IsNullOrEmpty(targetSHA)) + return; + + var log = _repository?.CreateLog($"Reset File to '{targetSHA}'"); + + // If file is Deleted in diff, it doesn't exist in target - remove it + if (change.Index == Models.ChangeState.Deleted) + { + await new Commands.Remove(_repo).Use(log).File(change.Path).ExecAsync(); + } + else + { + await new Commands.Checkout(_repo).Use(log).FileWithRevisionAsync(change.Path, targetSHA); + } + + log?.Complete(); + } + + public async Task ResetMultipleToSourceRevisionAsync(List changes) + { + var sourceSHA = GetSHA(_startPoint); + if (string.IsNullOrEmpty(sourceSHA)) + return; + + var filesToCheckout = new List(); + var filesToRemove = new List(); + + // Separate files: Added files don't exist in source, so remove them + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Added) + filesToRemove.Add(c.Path); + else + filesToCheckout.Add(c.Path); + } + + var log = _repository?.CreateLog($"Reset Files to '{sourceSHA}'"); + + if (filesToCheckout.Count > 0) + await new Commands.Checkout(_repo).Use(log).MultipleFilesWithRevisionAsync(filesToCheckout, sourceSHA); + + if (filesToRemove.Count > 0) + { + var pathSpecFile = System.IO.Path.GetTempFileName(); + await System.IO.File.WriteAllLinesAsync(pathSpecFile, filesToRemove); + await new Commands.Remove(_repo).Use(log).Files(pathSpecFile).ExecAsync(); + System.IO.File.Delete(pathSpecFile); + } + + log?.Complete(); + } + + public async Task ResetMultipleToTargetRevisionAsync(List changes) + { + var targetSHA = GetSHA(_endPoint); + if (string.IsNullOrEmpty(targetSHA)) + return; + + var filesToCheckout = new List(); + var filesToRemove = new List(); + + // Separate files: Deleted files don't exist in target, so remove them + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Deleted) + filesToRemove.Add(c.Path); + else + filesToCheckout.Add(c.Path); + } + + var log = _repository?.CreateLog($"Reset Files to '{targetSHA}'"); + + if (filesToCheckout.Count > 0) + await new Commands.Checkout(_repo).Use(log).MultipleFilesWithRevisionAsync(filesToCheckout, targetSHA); + + if (filesToRemove.Count > 0) + { + var pathSpecFile = System.IO.Path.GetTempFileName(); + await System.IO.File.WriteAllLinesAsync(pathSpecFile, filesToRemove); + await new Commands.Remove(_repo).Use(log).Files(pathSpecFile).ExecAsync(); + System.IO.File.Delete(pathSpecFile); + } + + log?.Complete(); + } + public void ClearSearchFilter() { SearchFilter = string.Empty; @@ -206,6 +325,7 @@ private string GetSHA(object obj) } private string _repo; + private Repository _repository = null; private bool _isLoading = true; private object _startPoint = null; private object _endPoint = null; diff --git a/src/Views/CommitDetail.axaml.cs b/src/Views/CommitDetail.axaml.cs index 8e5d998d4..cd1f0ad8f 100644 --- a/src/Views/CommitDetail.axaml.cs +++ b/src/Views/CommitDetail.axaml.cs @@ -335,7 +335,7 @@ public ContextMenu CreateChangeContextMenu(Models.Change change) resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); resetToThisRevision.Click += async (_, ev) => { - await vm.ResetToThisRevisionAsync(change.Path); + await vm.ResetToThisRevisionAsync(change); ev.Handled = true; }; diff --git a/src/Views/RevisionCompare.axaml.cs b/src/Views/RevisionCompare.axaml.cs index d9c830a7b..2a1238ad9 100644 --- a/src/Views/RevisionCompare.axaml.cs +++ b/src/Views/RevisionCompare.axaml.cs @@ -106,6 +106,32 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(patch); menu.Items.Add(new MenuItem() { Header = "-" }); + + if (vm.CanResetFiles) + { + var resetToSource = new MenuItem(); + resetToSource.Header = App.Text("ChangeCM.CheckoutFirstParentRevision"); + resetToSource.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToSource.Click += async (_, ev) => + { + await vm.ResetToSourceRevisionAsync(change); + ev.Handled = true; + }; + + var resetToTarget = new MenuItem(); + resetToTarget.Header = App.Text("ChangeCM.CheckoutThisRevision"); + resetToTarget.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToTarget.Click += async (_, ev) => + { + await vm.ResetToTargetRevisionAsync(change); + ev.Handled = true; + }; + + menu.Items.Add(resetToSource); + menu.Items.Add(resetToTarget); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + menu.Items.Add(copyPath); menu.Items.Add(copyFullPath); } @@ -141,6 +167,32 @@ private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e menu.Items.Add(patch); menu.Items.Add(new MenuItem() { Header = "-" }); + + if (vm.CanResetFiles) + { + var resetToSource = new MenuItem(); + resetToSource.Header = App.Text("ChangeCM.CheckoutFirstParentRevision"); + resetToSource.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToSource.Click += async (_, ev) => + { + await vm.ResetMultipleToSourceRevisionAsync(selected); + ev.Handled = true; + }; + + var resetToTarget = new MenuItem(); + resetToTarget.Header = App.Text("ChangeCM.CheckoutThisRevision"); + resetToTarget.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToTarget.Click += async (_, ev) => + { + await vm.ResetMultipleToTargetRevisionAsync(selected); + ev.Handled = true; + }; + + menu.Items.Add(resetToSource); + menu.Items.Add(resetToTarget); + menu.Items.Add(new MenuItem() { Header = "-" }); + } + menu.Items.Add(copyPath); menu.Items.Add(copyFullPath); } diff --git a/src/Views/RevisionFileTreeView.axaml.cs b/src/Views/RevisionFileTreeView.axaml.cs index 9141f3b0d..6d2f3353e 100644 --- a/src/Views/RevisionFileTreeView.axaml.cs +++ b/src/Views/RevisionFileTreeView.axaml.cs @@ -604,12 +604,14 @@ private ContextMenu CreateRevisionFileContextMenu(ViewModels.Repository repo, Vi if (!repo.IsBare) { + var change = vm.Changes.Find(x => x.Path == file.Path) ?? new Models.Change() { Index = Models.ChangeState.None, Path = file.Path }; + var resetToThisRevision = new MenuItem(); resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); resetToThisRevision.Click += async (_, ev) => { - await vm.ResetToThisRevisionAsync(file.Path); + await vm.ResetToThisRevisionAsync(change); ev.Handled = true; };