Skip to content

Commit a86a9cf

Browse files
committed
enhance: file rename detection and path resolution in file history
1 parent 46231a7 commit a86a9cf

File tree

4 files changed

+199
-24
lines changed

4 files changed

+199
-24
lines changed

src/Commands/CompareRevisions.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public partial class CompareRevisions : Command
88
{
99
[GeneratedRegex(@"^([MADC])\s+(.+)$")]
1010
private static partial Regex REG_FORMAT();
11-
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")]
11+
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)\s+(.+)$")]
1212
private static partial Regex REG_RENAME_FORMAT();
1313

1414
public CompareRevisions(string repo, string start, string end)
@@ -51,7 +51,11 @@ private void ParseLine(string line)
5151
match = REG_RENAME_FORMAT().Match(line);
5252
if (match.Success)
5353
{
54-
var renamed = new Models.Change() { Path = match.Groups[1].Value };
54+
var renamed = new Models.Change()
55+
{
56+
OriginalPath = match.Groups[1].Value,
57+
Path = match.Groups[2].Value
58+
};
5559
renamed.Set(Models.ChangeState.Renamed);
5660
_changes.Add(renamed);
5761
}

src/Commands/QueryCommits.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public QueryCommits(string repo, string filter, Models.CommitSearchMethod method
4343
}
4444
else if (method == Models.CommitSearchMethod.ByFile)
4545
{
46-
search += $"-- \"{filter}\"";
46+
search += $"--follow -- \"{filter}\"";
4747
}
4848
else
4949
{
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.RegularExpressions;
4+
5+
namespace SourceGit.Commands
6+
{
7+
public partial class QueryFilePathInRevision : Command
8+
{
9+
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)\s+(.+)$")]
10+
private static partial Regex REG_RENAME_FORMAT();
11+
12+
public QueryFilePathInRevision(string repo, string revision, string currentPath)
13+
{
14+
WorkingDirectory = repo;
15+
Context = repo;
16+
_revision = revision;
17+
_currentPath = currentPath;
18+
}
19+
20+
public string Result()
21+
{
22+
if (CheckPathExistsInRevision(_currentPath))
23+
return _currentPath;
24+
25+
string mappedPath = FindRenameHistory();
26+
return mappedPath ?? _currentPath;
27+
}
28+
29+
private bool CheckPathExistsInRevision(string path)
30+
{
31+
Args = $"ls-tree -r {_revision} -- \"{path}\"";
32+
var rs = ReadToEnd();
33+
return rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut);
34+
}
35+
36+
private string FindRenameHistory()
37+
{
38+
var fileHistory = BuildFileHistory();
39+
if (fileHistory == null || fileHistory.Count == 0)
40+
return null;
41+
42+
foreach (var entry in fileHistory)
43+
{
44+
if (!IsTargetRevisionBefore(entry.CommitSHA))
45+
continue;
46+
47+
if (CheckPathExistsInRevision(entry.OldPath))
48+
return entry.OldPath;
49+
}
50+
51+
if (fileHistory.Count > 0)
52+
{
53+
var oldestPath = fileHistory[^1].OldPath;
54+
if (CheckPathExistsInRevision(oldestPath))
55+
return oldestPath;
56+
}
57+
58+
return null;
59+
}
60+
61+
private bool IsTargetRevisionBefore(string commitSHA)
62+
{
63+
Args = $"merge-base --is-ancestor {_revision} {commitSHA}";
64+
var rs = ReadToEnd();
65+
return rs.IsSuccess;
66+
}
67+
68+
private List<RenameHistoryEntry> BuildFileHistory()
69+
{
70+
Args = $"log --follow --name-status --pretty=format:\"commit %H\" -M -- \"{_currentPath}\"";
71+
var rs = ReadToEnd();
72+
if (!rs.IsSuccess)
73+
return null;
74+
75+
var result = new List<RenameHistoryEntry>();
76+
var lines = rs.StdOut.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
77+
78+
string currentCommit = null;
79+
string currentPath = _currentPath;
80+
81+
foreach (var t in lines)
82+
{
83+
var line = t.Trim();
84+
85+
if (line.StartsWith("commit ", StringComparison.Ordinal))
86+
{
87+
currentCommit = line.Substring("commit ".Length);
88+
continue;
89+
}
90+
91+
var match = REG_RENAME_FORMAT().Match(line);
92+
if (match.Success && currentCommit != null)
93+
{
94+
var oldPath = match.Groups[1].Value;
95+
var newPath = match.Groups[2].Value;
96+
97+
if (newPath == currentPath)
98+
{
99+
result.Add(new RenameHistoryEntry
100+
{
101+
CommitSHA = currentCommit,
102+
OldPath = oldPath,
103+
NewPath = newPath
104+
});
105+
106+
currentPath = oldPath;
107+
}
108+
}
109+
}
110+
111+
return result;
112+
}
113+
114+
private class RenameHistoryEntry
115+
{
116+
public string CommitSHA { get; set; }
117+
public string OldPath { get; set; }
118+
public string NewPath { get; set; }
119+
}
120+
121+
private readonly string _revision;
122+
private readonly string _currentPath;
123+
}
124+
}

src/ViewModels/FileHistories.cs

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public FileHistoriesSingleRevision(Repository repo, string file, Models.Commit r
4949

5050
public void ResetToSelectedRevision()
5151
{
52-
new Commands.Checkout(_repo.FullPath).FileWithRevision(_file, $"{_revision.SHA}");
52+
var revisionFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _revision.SHA, _file).Result();
53+
new Commands.Checkout(_repo.FullPath).FileWithRevision(revisionFilePath, $"{_revision.SHA}");
5354
}
5455

5556
private void RefreshViewContent()
@@ -62,10 +63,12 @@ private void RefreshViewContent()
6263

6364
private void SetViewContentAsRevisionFile()
6465
{
65-
var objs = new Commands.QueryRevisionObjects(_repo.FullPath, _revision.SHA, _file).Result();
66+
var revisionFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _revision.SHA, _file).Result();
67+
68+
var objs = new Commands.QueryRevisionObjects(_repo.FullPath, _revision.SHA, revisionFilePath).Result();
6669
if (objs.Count == 0)
6770
{
68-
ViewContent = new FileHistoriesRevisionFile(_file, null);
71+
ViewContent = new FileHistoriesRevisionFile(revisionFilePath, null);
6972
return;
7073
}
7174

@@ -75,50 +78,49 @@ private void SetViewContentAsRevisionFile()
7578
case Models.ObjectType.Blob:
7679
Task.Run(() =>
7780
{
78-
var isBinary = new Commands.IsBinary(_repo.FullPath, _revision.SHA, _file).Result();
81+
var isBinary = new Commands.IsBinary(_repo.FullPath, _revision.SHA, revisionFilePath).Result();
7982
if (isBinary)
8083
{
81-
var ext = Path.GetExtension(_file);
84+
var ext = Path.GetExtension(revisionFilePath);
8285
if (IMG_EXTS.Contains(ext))
8386
{
84-
var stream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, _file);
87+
var stream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, revisionFilePath);
8588
var fileSize = stream.Length;
8689
var bitmap = fileSize > 0 ? new Bitmap(stream) : null;
87-
var imageType = Path.GetExtension(_file).TrimStart('.').ToUpper(CultureInfo.CurrentCulture);
90+
var imageType = Path.GetExtension(revisionFilePath).TrimStart('.').ToUpper(CultureInfo.CurrentCulture);
8891
var image = new Models.RevisionImageFile() { Image = bitmap, FileSize = fileSize, ImageType = imageType };
89-
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, image));
92+
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, image));
9093
}
9194
else
9295
{
93-
var size = new Commands.QueryFileSize(_repo.FullPath, _file, _revision.SHA).Result();
96+
var size = new Commands.QueryFileSize(_repo.FullPath, revisionFilePath, _revision.SHA).Result();
9497
var binaryFile = new Models.RevisionBinaryFile() { Size = size };
95-
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, binaryFile));
98+
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, binaryFile));
9699
}
97-
98100
return;
99101
}
100102

101-
var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, _file);
103+
var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, revisionFilePath);
102104
var content = new StreamReader(contentStream).ReadToEnd();
103105
var matchLFS = REG_LFS_FORMAT().Match(content);
104106
if (matchLFS.Success)
105107
{
106108
var lfs = new Models.RevisionLFSObject() { Object = new Models.LFSObject() };
107109
lfs.Object.Oid = matchLFS.Groups[1].Value;
108110
lfs.Object.Size = long.Parse(matchLFS.Groups[2].Value);
109-
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, lfs));
111+
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, lfs));
110112
}
111113
else
112114
{
113115
var txt = new Models.RevisionTextFile() { FileName = obj.Path, Content = content };
114-
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, txt));
116+
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, txt));
115117
}
116118
});
117119
break;
118120
case Models.ObjectType.Commit:
119121
Task.Run(() =>
120122
{
121-
var submoduleRoot = Path.Combine(_repo.FullPath, _file);
123+
var submoduleRoot = Path.Combine(_repo.FullPath, revisionFilePath);
122124
var commit = new Commands.QuerySingleCommit(submoduleRoot, obj.SHA).Result();
123125
if (commit != null)
124126
{
@@ -128,7 +130,7 @@ private void SetViewContentAsRevisionFile()
128130
Commit = commit,
129131
FullMessage = new Models.CommitFullMessage { Message = message }
130132
};
131-
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, module));
133+
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, module));
132134
}
133135
else
134136
{
@@ -137,19 +139,21 @@ private void SetViewContentAsRevisionFile()
137139
Commit = new Models.Commit() { SHA = obj.SHA },
138140
FullMessage = null
139141
};
140-
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, module));
142+
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, module));
141143
}
142144
});
143145
break;
144146
default:
145-
ViewContent = new FileHistoriesRevisionFile(_file, null);
147+
ViewContent = new FileHistoriesRevisionFile(revisionFilePath, null);
146148
break;
147149
}
148150
}
149151

150152
private void SetViewContentAsDiff()
151153
{
152-
var option = new Models.DiffOption(_revision, _file);
154+
var revisionFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _revision.SHA, _file).Result();
155+
156+
var option = new Models.DiffOption(_revision, revisionFilePath);
153157
ViewContent = new DiffContext(_repo.FullPath, option, _viewContent as DiffContext);
154158
}
155159

@@ -216,7 +220,50 @@ private void RefreshViewContent()
216220
{
217221
Task.Run(() =>
218222
{
219-
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, _file).Result();
223+
var startFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _startPoint.SHA, _file).Result();
224+
var endFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _endPoint.SHA, _file).Result();
225+
226+
var allChanges = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA).Result();
227+
228+
Models.Change renamedChange = null;
229+
foreach (var change in allChanges)
230+
{
231+
if (change.WorkTree != Models.ChangeState.Renamed && change.Index != Models.ChangeState.Renamed)
232+
continue;
233+
if (change.Path != endFilePath && change.OriginalPath != startFilePath)
234+
continue;
235+
236+
renamedChange = change;
237+
break;
238+
}
239+
240+
if (renamedChange != null)
241+
{
242+
_changes = [renamedChange];
243+
}
244+
else
245+
{
246+
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, startFilePath).Result();
247+
248+
if (_changes.Count == 0 && startFilePath != endFilePath)
249+
{
250+
var renamed = new Models.Change()
251+
{
252+
OriginalPath = startFilePath,
253+
Path = endFilePath
254+
};
255+
renamed.Set(Models.ChangeState.Renamed);
256+
_changes = [renamed];
257+
}
258+
else if (_changes.Count == 0)
259+
{
260+
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, endFilePath).Result();
261+
262+
if (_changes.Count == 0)
263+
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, _file).Result();
264+
}
265+
}
266+
220267
if (_changes.Count == 0)
221268
{
222269
Dispatcher.UIThread.Invoke(() => ViewContent = null);
@@ -270,7 +317,7 @@ public FileHistories(Repository repo, string file, string commit = null)
270317
Task.Run(() =>
271318
{
272319
var based = commit ?? string.Empty;
273-
var commits = new Commands.QueryCommits(_repo.FullPath, $"--date-order -n 10000 {based} -- \"{file}\"", false).Result();
320+
var commits = new Commands.QueryCommits(_repo.FullPath, $"--date-order --follow -n 10000 {based} -- \"{file}\"", false).Result();
274321
Dispatcher.UIThread.Invoke(() =>
275322
{
276323
IsLoading = false;

0 commit comments

Comments
 (0)