Skip to content

Commit b3d0ba4

Browse files
ybeappsclaude
andauthored
Add reset file commands to compare view (#3)
* Add reset file commands to compare view Enable "Reset to Parent Revision" and "Reset to This Revision" commands in the compare view when selecting two commits. These commands now work for both single and multiple file selections in the compare view. - Add ResetToSourceRevisionAsync and ResetToTargetRevisionAsync methods - Add ResetMultipleToSourceRevisionAsync and ResetMultipleToTargetRevisionAsync methods - Update RevisionCompare constructor to accept Repository object for logging - Add CanResetFiles property to check if reset operations are available - Add reset menu items to context menu in RevisionCompare view - Parent Revision resets to compare source (StartPoint) - Current Revision resets to compare target (EndPoint) * Fix reset file commands for deleted files in compare view When resetting a file to a revision where it is deleted, use 'git rm' instead of 'git checkout' to properly remove the file from both the working directory and the index. - Add new Remove command for git rm operations - Update ResetToSourceRevisionAsync to handle Added files (don't exist in source) - Update ResetToTargetRevisionAsync to handle Deleted files (don't exist in target) - Update multiple file reset methods to separate files into checkout vs remove lists - Pass Change object instead of just path string to enable state checking - Use --force and --ignore-unmatch flags for robust file removal Fixes the "pathspec did not match any file(s) known to git" error when trying to reset deleted files to the revision where they are deleted. * Apply deleted file fix to single commit view reset commands Apply the same deleted/added file handling to CommitDetail (single commit view) that was previously applied to RevisionCompare. - Update ResetToThisRevisionAsync to handle Deleted files - Update ResetToParentRevisionAsync to handle Added files - Update ResetMultipleToThisRevisionAsync to separate deleted files - Update ResetMultipleToParentRevisionAsync to separate added files - Change method signature to accept Change object instead of path string - Update all call sites to pass Change object instead of path This fixes the same "pathspec did not match any file(s) known to git" error when resetting deleted files in single commit view. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7cc4f9b commit b3d0ba4

File tree

7 files changed

+275
-18
lines changed

7 files changed

+275
-18
lines changed

src/Commands/Remove.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace SourceGit.Commands
2+
{
3+
public class Remove : Command
4+
{
5+
public Remove(string repo)
6+
{
7+
WorkingDirectory = repo;
8+
Context = repo;
9+
}
10+
11+
public Remove File(string file)
12+
{
13+
Args = $"rm --force --ignore-unmatch -- {file.Quoted()}";
14+
return this;
15+
}
16+
17+
public Remove Files(string pathspecFromFile)
18+
{
19+
Args = $"rm --force --ignore-unmatch --pathspec-from-file={pathspecFromFile.Quoted()}";
20+
return this;
21+
}
22+
}
23+
}

src/ViewModels/CommitDetail.cs

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -247,56 +247,116 @@ public async Task SaveChangesAsPatchAsync(List<Models.Change> changes, string sa
247247
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
248248
}
249249

250-
public async Task ResetToThisRevisionAsync(string path)
250+
public async Task ResetToThisRevisionAsync(Models.Change change)
251251
{
252252
var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'");
253-
await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(path, _commit.SHA);
253+
254+
// If file is Deleted in this commit, it doesn't exist in this revision - remove it
255+
if (change.Index == Models.ChangeState.Deleted)
256+
{
257+
await new Commands.Remove(_repo.FullPath).Use(log).File(change.Path).ExecAsync();
258+
}
259+
else
260+
{
261+
await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.Path, _commit.SHA);
262+
}
263+
254264
log.Complete();
255265
}
256266

257267
public async Task ResetToParentRevisionAsync(Models.Change change)
258268
{
259269
var log = _repo.CreateLog($"Reset File to '{_commit.SHA}~1'");
260270

261-
if (change.Index == Models.ChangeState.Renamed)
262-
await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.OriginalPath, $"{_commit.SHA}~1");
271+
// If file is Added in this commit, it doesn't exist in parent - remove it
272+
if (change.Index == Models.ChangeState.Added)
273+
{
274+
await new Commands.Remove(_repo.FullPath).Use(log).File(change.Path).ExecAsync();
275+
}
276+
else
277+
{
278+
// Handle renamed files - restore original path from parent
279+
if (change.Index == Models.ChangeState.Renamed)
280+
await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.OriginalPath, $"{_commit.SHA}~1");
281+
282+
await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.Path, $"{_commit.SHA}~1");
283+
}
263284

264-
await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(change.Path, $"{_commit.SHA}~1");
265285
log.Complete();
266286
}
267287

268288
public async Task ResetMultipleToThisRevisionAsync(List<Models.Change> changes)
269289
{
270-
var files = new List<string>();
290+
var filesToCheckout = new List<string>();
291+
var filesToRemove = new List<string>();
292+
293+
// Separate files: Deleted files don't exist in this revision, so remove them
271294
foreach (var c in changes)
272-
files.Add(c.Path);
295+
{
296+
if (c.Index == Models.ChangeState.Deleted)
297+
filesToRemove.Add(c.Path);
298+
else
299+
filesToCheckout.Add(c.Path);
300+
}
273301

274302
var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}'");
275-
await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(files, _commit.SHA);
303+
304+
if (filesToCheckout.Count > 0)
305+
await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(filesToCheckout, _commit.SHA);
306+
307+
if (filesToRemove.Count > 0)
308+
{
309+
var pathSpecFile = System.IO.Path.GetTempFileName();
310+
await System.IO.File.WriteAllLinesAsync(pathSpecFile, filesToRemove);
311+
await new Commands.Remove(_repo.FullPath).Use(log).Files(pathSpecFile).ExecAsync();
312+
System.IO.File.Delete(pathSpecFile);
313+
}
314+
276315
log.Complete();
277316
}
278317

279318
public async Task ResetMultipleToParentRevisionAsync(List<Models.Change> changes)
280319
{
281320
var renamed = new List<string>();
282-
var modified = new List<string>();
321+
var filesToCheckout = new List<string>();
322+
var filesToRemove = new List<string>();
283323

324+
// Separate files by type
284325
foreach (var c in changes)
285326
{
286-
if (c.Index == Models.ChangeState.Renamed)
327+
if (c.Index == Models.ChangeState.Added)
328+
{
329+
// Added files don't exist in parent - remove them
330+
filesToRemove.Add(c.Path);
331+
}
332+
else if (c.Index == Models.ChangeState.Renamed)
333+
{
334+
// Renamed files - restore original path from parent
287335
renamed.Add(c.OriginalPath);
336+
}
288337
else
289-
modified.Add(c.Path);
338+
{
339+
// Other files - checkout from parent
340+
filesToCheckout.Add(c.Path);
341+
}
290342
}
291343

292344
var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}~1'");
293345

294-
if (modified.Count > 0)
295-
await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(modified, $"{_commit.SHA}~1");
346+
if (filesToCheckout.Count > 0)
347+
await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(filesToCheckout, $"{_commit.SHA}~1");
296348

297349
if (renamed.Count > 0)
298350
await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(renamed, $"{_commit.SHA}~1");
299351

352+
if (filesToRemove.Count > 0)
353+
{
354+
var pathSpecFile = System.IO.Path.GetTempFileName();
355+
await System.IO.File.WriteAllLinesAsync(pathSpecFile, filesToRemove);
356+
await new Commands.Remove(_repo.FullPath).Use(log).Files(pathSpecFile).ExecAsync();
357+
System.IO.File.Delete(pathSpecFile);
358+
}
359+
300360
log.Complete();
301361
}
302362

src/ViewModels/Histories.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public void Select(IList commits)
206206

207207
var end = commits[0] as Models.Commit;
208208
var start = commits[1] as Models.Commit;
209-
DetailContext = new RevisionCompare(_repo.FullPath, start, end);
209+
DetailContext = new RevisionCompare(_repo.FullPath, _repo, start, end);
210210
}
211211
else
212212
{
@@ -403,7 +403,7 @@ public async Task<string> GetCommitFullMessageAsync(Models.Commit commit)
403403
_repo.SearchCommitContext.Selected = null;
404404
head = await new Commands.QuerySingleCommit(_repo.FullPath, "HEAD").GetResultAsync();
405405
if (head != null)
406-
DetailContext = new RevisionCompare(_repo.FullPath, commit, head);
406+
DetailContext = new RevisionCompare(_repo.FullPath, _repo, commit, head);
407407

408408
return null;
409409
}
@@ -413,7 +413,7 @@ public async Task<string> GetCommitFullMessageAsync(Models.Commit commit)
413413

414414
public void CompareWithWorktree(Models.Commit commit)
415415
{
416-
DetailContext = new RevisionCompare(_repo.FullPath, commit, null);
416+
DetailContext = new RevisionCompare(_repo.FullPath, _repo, commit, null);
417417
}
418418

419419
private Repository _repo = null;

src/ViewModels/RevisionCompare.cs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public object EndPoint
2828

2929
public bool CanSaveAsPatch { get; }
3030

31+
public bool CanResetFiles => _repository != null && !_repository.IsBare;
32+
3133
public int TotalChanges
3234
{
3335
get => _totalChanges;
@@ -77,8 +79,14 @@ public DiffContext DiffContext
7779
}
7880

7981
public RevisionCompare(string repo, Models.Commit startPoint, Models.Commit endPoint)
82+
: this(repo, null, startPoint, endPoint)
83+
{
84+
}
85+
86+
public RevisionCompare(string repo, Repository repository, Models.Commit startPoint, Models.Commit endPoint)
8087
{
8188
_repo = repo;
89+
_repository = repository;
8290
_startPoint = (object)startPoint ?? new Models.Null();
8391
_endPoint = (object)endPoint ?? new Models.Null();
8492
CanSaveAsPatch = startPoint != null && endPoint != null;
@@ -88,6 +96,7 @@ public RevisionCompare(string repo, Models.Commit startPoint, Models.Commit endP
8896
public void Dispose()
8997
{
9098
_repo = null;
99+
_repository = null;
91100
_startPoint = null;
92101
_endPoint = null;
93102
_changes?.Clear();
@@ -140,6 +149,116 @@ public async Task SaveChangesAsPatchAsync(List<Models.Change> changes, string sa
140149
App.SendNotification(_repo, App.Text("SaveAsPatchSuccess"));
141150
}
142151

152+
public async Task ResetToSourceRevisionAsync(Models.Change change)
153+
{
154+
var sourceSHA = GetSHA(_startPoint);
155+
if (string.IsNullOrEmpty(sourceSHA))
156+
return;
157+
158+
var log = _repository?.CreateLog($"Reset File to '{sourceSHA}'");
159+
160+
// If file is Added in diff, it doesn't exist in source - remove it
161+
if (change.Index == Models.ChangeState.Added)
162+
{
163+
await new Commands.Remove(_repo).Use(log).File(change.Path).ExecAsync();
164+
}
165+
else
166+
{
167+
await new Commands.Checkout(_repo).Use(log).FileWithRevisionAsync(change.Path, sourceSHA);
168+
}
169+
170+
log?.Complete();
171+
}
172+
173+
public async Task ResetToTargetRevisionAsync(Models.Change change)
174+
{
175+
var targetSHA = GetSHA(_endPoint);
176+
if (string.IsNullOrEmpty(targetSHA))
177+
return;
178+
179+
var log = _repository?.CreateLog($"Reset File to '{targetSHA}'");
180+
181+
// If file is Deleted in diff, it doesn't exist in target - remove it
182+
if (change.Index == Models.ChangeState.Deleted)
183+
{
184+
await new Commands.Remove(_repo).Use(log).File(change.Path).ExecAsync();
185+
}
186+
else
187+
{
188+
await new Commands.Checkout(_repo).Use(log).FileWithRevisionAsync(change.Path, targetSHA);
189+
}
190+
191+
log?.Complete();
192+
}
193+
194+
public async Task ResetMultipleToSourceRevisionAsync(List<Models.Change> changes)
195+
{
196+
var sourceSHA = GetSHA(_startPoint);
197+
if (string.IsNullOrEmpty(sourceSHA))
198+
return;
199+
200+
var filesToCheckout = new List<string>();
201+
var filesToRemove = new List<string>();
202+
203+
// Separate files: Added files don't exist in source, so remove them
204+
foreach (var c in changes)
205+
{
206+
if (c.Index == Models.ChangeState.Added)
207+
filesToRemove.Add(c.Path);
208+
else
209+
filesToCheckout.Add(c.Path);
210+
}
211+
212+
var log = _repository?.CreateLog($"Reset Files to '{sourceSHA}'");
213+
214+
if (filesToCheckout.Count > 0)
215+
await new Commands.Checkout(_repo).Use(log).MultipleFilesWithRevisionAsync(filesToCheckout, sourceSHA);
216+
217+
if (filesToRemove.Count > 0)
218+
{
219+
var pathSpecFile = System.IO.Path.GetTempFileName();
220+
await System.IO.File.WriteAllLinesAsync(pathSpecFile, filesToRemove);
221+
await new Commands.Remove(_repo).Use(log).Files(pathSpecFile).ExecAsync();
222+
System.IO.File.Delete(pathSpecFile);
223+
}
224+
225+
log?.Complete();
226+
}
227+
228+
public async Task ResetMultipleToTargetRevisionAsync(List<Models.Change> changes)
229+
{
230+
var targetSHA = GetSHA(_endPoint);
231+
if (string.IsNullOrEmpty(targetSHA))
232+
return;
233+
234+
var filesToCheckout = new List<string>();
235+
var filesToRemove = new List<string>();
236+
237+
// Separate files: Deleted files don't exist in target, so remove them
238+
foreach (var c in changes)
239+
{
240+
if (c.Index == Models.ChangeState.Deleted)
241+
filesToRemove.Add(c.Path);
242+
else
243+
filesToCheckout.Add(c.Path);
244+
}
245+
246+
var log = _repository?.CreateLog($"Reset Files to '{targetSHA}'");
247+
248+
if (filesToCheckout.Count > 0)
249+
await new Commands.Checkout(_repo).Use(log).MultipleFilesWithRevisionAsync(filesToCheckout, targetSHA);
250+
251+
if (filesToRemove.Count > 0)
252+
{
253+
var pathSpecFile = System.IO.Path.GetTempFileName();
254+
await System.IO.File.WriteAllLinesAsync(pathSpecFile, filesToRemove);
255+
await new Commands.Remove(_repo).Use(log).Files(pathSpecFile).ExecAsync();
256+
System.IO.File.Delete(pathSpecFile);
257+
}
258+
259+
log?.Complete();
260+
}
261+
143262
public void ClearSearchFilter()
144263
{
145264
SearchFilter = string.Empty;
@@ -206,6 +325,7 @@ private string GetSHA(object obj)
206325
}
207326

208327
private string _repo;
328+
private Repository _repository = null;
209329
private bool _isLoading = true;
210330
private object _startPoint = null;
211331
private object _endPoint = null;

src/Views/CommitDetail.axaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ public ContextMenu CreateChangeContextMenu(Models.Change change)
335335
resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout");
336336
resetToThisRevision.Click += async (_, ev) =>
337337
{
338-
await vm.ResetToThisRevisionAsync(change.Path);
338+
await vm.ResetToThisRevisionAsync(change);
339339
ev.Handled = true;
340340
};
341341

0 commit comments

Comments
 (0)