Skip to content

Commit 7bf78a9

Browse files
committed
refactor: rewrite file-history to support detecting renames (--follow) (#2174)
Signed-off-by: leo <longshuang@msn.cn>
1 parent 9a8498b commit 7bf78a9

File tree

6 files changed

+210
-73
lines changed

6 files changed

+210
-73
lines changed

src/Commands/QueryFileHistory.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Text.RegularExpressions;
5+
using System.Threading.Tasks;
6+
7+
namespace SourceGit.Commands
8+
{
9+
public partial class QueryFileHistory : Command
10+
{
11+
[GeneratedRegex(@"^([MADC])\s+(.+)$")]
12+
private static partial Regex REG_FORMAT();
13+
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")]
14+
private static partial Regex REG_RENAME_FORMAT();
15+
16+
public QueryFileHistory(string repo, string path, string head)
17+
{
18+
WorkingDirectory = repo;
19+
Context = repo;
20+
RaiseError = false;
21+
22+
var builder = new StringBuilder();
23+
builder.Append("log --follow --no-show-signature --date-order -n 10000 --decorate=no --format=\"@%H%x00%P%x00%aN±%aE%x00%at%x00%s\" --name-status ");
24+
if (!string.IsNullOrEmpty(head))
25+
builder.Append(head).Append(" ");
26+
builder.Append("-- ").Append(path.Quoted());
27+
28+
Args = builder.ToString();
29+
}
30+
31+
public async Task<List<Models.FileVersion>> GetResultAsync()
32+
{
33+
var versions = new List<Models.FileVersion>();
34+
var rs = await ReadToEndAsync().ConfigureAwait(false);
35+
if (!rs.IsSuccess)
36+
return versions;
37+
38+
var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
39+
if (lines.Length == 0)
40+
return versions;
41+
42+
Models.FileVersion last = null;
43+
foreach (var line in lines)
44+
{
45+
if (line.StartsWith('@'))
46+
{
47+
var parts = line.Split('\0');
48+
if (parts.Length != 5)
49+
continue;
50+
51+
last = new Models.FileVersion();
52+
last.SHA = parts[0].Substring(1);
53+
last.HasParent = !string.IsNullOrEmpty(parts[1]);
54+
last.Author = Models.User.FindOrAdd(parts[2]);
55+
last.AuthorTime = ulong.Parse(parts[3]);
56+
last.Subject = parts[4];
57+
versions.Add(last);
58+
}
59+
else if (last != null)
60+
{
61+
var match = REG_FORMAT().Match(line);
62+
if (!match.Success)
63+
{
64+
match = REG_RENAME_FORMAT().Match(line);
65+
if (match.Success)
66+
{
67+
last.Change.Path = match.Groups[1].Value;
68+
last.Change.Set(Models.ChangeState.Renamed);
69+
}
70+
71+
continue;
72+
}
73+
74+
last.Change.Path = match.Groups[2].Value;
75+
76+
var status = match.Groups[1].Value;
77+
switch (status[0])
78+
{
79+
case 'M':
80+
last.Change.Set(Models.ChangeState.Modified);
81+
break;
82+
case 'A':
83+
last.Change.Set(Models.ChangeState.Added);
84+
break;
85+
case 'D':
86+
last.Change.Set(Models.ChangeState.Deleted);
87+
break;
88+
case 'C':
89+
last.Change.Set(Models.ChangeState.Copied);
90+
break;
91+
}
92+
}
93+
}
94+
95+
return versions;
96+
}
97+
}
98+
}

src/Models/DiffOption.cs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Text;
34

45
namespace SourceGit.Models
@@ -79,6 +80,53 @@ public DiffOption(Commit commit, string file)
7980
_path = file;
8081
}
8182

83+
/// <summary>
84+
/// Used to diff in `FileHistory`
85+
/// </summary>
86+
/// <param name="ver"></param>
87+
public DiffOption(FileVersion ver)
88+
{
89+
_revisions.Add(ver.HasParent ? $"{ver.SHA}^" : Commit.EmptyTreeSHA1);
90+
_revisions.Add(ver.SHA);
91+
_path = ver.Path;
92+
_orgPath = ver.Change.OriginalPath;
93+
}
94+
95+
/// <summary>
96+
/// Used to diff two revisions in `FileHistory`
97+
/// </summary>
98+
/// <param name="start"></param>
99+
/// <param name="end"></param>
100+
public DiffOption(FileVersion start, FileVersion end)
101+
{
102+
if (start.Change.Index == ChangeState.Deleted)
103+
{
104+
_revisions.Add(Commit.EmptyTreeSHA1);
105+
_revisions.Add(end.SHA);
106+
_path = end.Path;
107+
}
108+
else if (end.Change.Index == ChangeState.Deleted)
109+
{
110+
_revisions.Add(start.SHA);
111+
_revisions.Add(Commit.EmptyTreeSHA1);
112+
_path = start.Path;
113+
}
114+
else if (!end.Path.Equals(start.Path, StringComparison.Ordinal))
115+
{
116+
_revisions.Add($"{start.SHA}:{start.Path.Quoted()}");
117+
_revisions.Add($"{end.SHA}:{end.Path.Quoted()}");
118+
_path = end.Path;
119+
_orgPath = start.Path;
120+
_ignorePaths = true;
121+
}
122+
else
123+
{
124+
_revisions.Add(start.SHA);
125+
_revisions.Add(end.SHA);
126+
_path = start.Path;
127+
}
128+
}
129+
82130
/// <summary>
83131
/// Used to show differences between two revisions.
84132
/// </summary>
@@ -104,6 +152,9 @@ public override string ToString()
104152
foreach (var r in _revisions)
105153
builder.Append($"{r} ");
106154

155+
if (_ignorePaths)
156+
return builder.ToString();
157+
107158
builder.Append("-- ");
108159
if (!string.IsNullOrEmpty(_orgPath))
109160
builder.Append($"{_orgPath.Quoted()} ");
@@ -118,5 +169,6 @@ public override string ToString()
118169
private readonly string _orgPath = string.Empty;
119170
private readonly string _extra = string.Empty;
120171
private readonly List<string> _revisions = [];
172+
private readonly bool _ignorePaths = false;
121173
}
122174
}

src/Models/FileVersion.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
3+
namespace SourceGit.Models
4+
{
5+
public class FileVersion
6+
{
7+
public string SHA { get; set; } = string.Empty;
8+
public bool HasParent { get; set; } = false;
9+
public User Author { get; set; } = User.Invalid;
10+
public ulong AuthorTime { get; set; } = 0;
11+
public string Subject { get; set; } = string.Empty;
12+
public Change Change { get; set; } = new();
13+
14+
public string Path => Change.Path;
15+
public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime);
16+
public string AuthorTimeShortStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateOnly);
17+
}
18+
}

src/ViewModels/FileHistories.cs

Lines changed: 29 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4-
using System.Text;
54
using System.Threading.Tasks;
65

76
using Avalonia.Collections;
@@ -36,10 +35,10 @@ public object ViewContent
3635
set => SetProperty(ref _viewContent, value);
3736
}
3837

39-
public FileHistoriesSingleRevision(string repo, string file, Models.Commit revision, bool prevIsDiffMode)
38+
public FileHistoriesSingleRevision(string repo, Models.FileVersion revision, bool prevIsDiffMode)
4039
{
4140
_repo = repo;
42-
_file = file;
41+
_file = revision.Path;
4342
_revision = revision;
4443
_isDiffMode = prevIsDiffMode;
4544
_viewContent = null;
@@ -155,26 +154,25 @@ private async Task<object> GetRevisionFileContentAsync(Models.Object obj)
155154

156155
private void SetViewContentAsDiff()
157156
{
158-
var option = new Models.DiffOption(_revision, _file);
159-
ViewContent = new DiffContext(_repo, option, _viewContent as DiffContext);
157+
ViewContent = new DiffContext(_repo, new Models.DiffOption(_revision), _viewContent as DiffContext);
160158
}
161159

162160
private string _repo = null;
163161
private string _file = null;
164-
private Models.Commit _revision = null;
162+
private Models.FileVersion _revision = null;
165163
private bool _isDiffMode = false;
166164
private object _viewContent = null;
167165
}
168166

169167
public class FileHistoriesCompareRevisions : ObservableObject
170168
{
171-
public Models.Commit StartPoint
169+
public Models.FileVersion StartPoint
172170
{
173171
get => _startPoint;
174172
set => SetProperty(ref _startPoint, value);
175173
}
176174

177-
public Models.Commit EndPoint
175+
public Models.FileVersion EndPoint
178176
{
179177
get => _endPoint;
180178
set => SetProperty(ref _endPoint, value);
@@ -186,19 +184,18 @@ public DiffContext ViewContent
186184
set => SetProperty(ref _viewContent, value);
187185
}
188186

189-
public FileHistoriesCompareRevisions(string repo, string file, Models.Commit start, Models.Commit end)
187+
public FileHistoriesCompareRevisions(string repo, Models.FileVersion start, Models.FileVersion end)
190188
{
191189
_repo = repo;
192-
_file = file;
193190
_startPoint = start;
194191
_endPoint = end;
195-
RefreshViewContent();
192+
_viewContent = new(_repo, new(start, end));
196193
}
197194

198195
public void Swap()
199196
{
200197
(StartPoint, EndPoint) = (_endPoint, _startPoint);
201-
RefreshViewContent();
198+
ViewContent = new(_repo, new(_startPoint, _endPoint), _viewContent);
202199
}
203200

204201
public async Task<bool> SaveAsPatch(string saveTo)
@@ -208,27 +205,9 @@ public async Task<bool> SaveAsPatch(string saveTo)
208205
.ConfigureAwait(false);
209206
}
210207

211-
private void RefreshViewContent()
212-
{
213-
Task.Run(async () =>
214-
{
215-
_changes = await new Commands.CompareRevisions(_repo, _startPoint.SHA, _endPoint.SHA, _file).ReadAsync().ConfigureAwait(false);
216-
if (_changes.Count == 0)
217-
{
218-
Dispatcher.UIThread.Post(() => ViewContent = null);
219-
}
220-
else
221-
{
222-
var option = new Models.DiffOption(_startPoint.SHA, _endPoint.SHA, _changes[0]);
223-
Dispatcher.UIThread.Post(() => ViewContent = new DiffContext(_repo, option, _viewContent));
224-
}
225-
});
226-
}
227-
228208
private string _repo = null;
229-
private string _file = null;
230-
private Models.Commit _startPoint = null;
231-
private Models.Commit _endPoint = null;
209+
private Models.FileVersion _startPoint = null;
210+
private Models.FileVersion _endPoint = null;
232211
private List<Models.Change> _changes = [];
233212
private DiffContext _viewContent = null;
234213
}
@@ -246,13 +225,13 @@ public bool IsLoading
246225
private set => SetProperty(ref _isLoading, value);
247226
}
248227

249-
public List<Models.Commit> Commits
228+
public List<Models.FileVersion> Revisions
250229
{
251-
get => _commits;
252-
set => SetProperty(ref _commits, value);
230+
get => _revisions;
231+
set => SetProperty(ref _revisions, value);
253232
}
254233

255-
public AvaloniaList<Models.Commit> SelectedCommits
234+
public AvaloniaList<Models.FileVersion> SelectedRevisions
256235
{
257236
get;
258237
set;
@@ -275,41 +254,34 @@ public FileHistories(string repo, string file, string commit = null)
275254

276255
Task.Run(async () =>
277256
{
278-
var argsBuilder = new StringBuilder();
279-
argsBuilder
280-
.Append("--date-order -n 10000 ")
281-
.Append(commit ?? string.Empty)
282-
.Append(" -- ")
283-
.Append(file.Quoted());
284-
285-
var commits = await new Commands.QueryCommits(_repo, argsBuilder.ToString(), false)
257+
var revisions = await new Commands.QueryFileHistory(_repo, file, commit)
286258
.GetResultAsync()
287259
.ConfigureAwait(false);
288260

289261
Dispatcher.UIThread.Post(() =>
290262
{
291263
IsLoading = false;
292-
Commits = commits;
293-
if (Commits.Count > 0)
294-
SelectedCommits.Add(Commits[0]);
264+
Revisions = revisions;
265+
if (revisions.Count > 0)
266+
SelectedRevisions.Add(revisions[0]);
295267
});
296268
});
297269

298-
SelectedCommits.CollectionChanged += (_, _) =>
270+
SelectedRevisions.CollectionChanged += (_, _) =>
299271
{
300272
if (_viewContent is FileHistoriesSingleRevision singleRevision)
301273
_prevIsDiffMode = singleRevision.IsDiffMode;
302274

303-
ViewContent = SelectedCommits.Count switch
275+
ViewContent = SelectedRevisions.Count switch
304276
{
305-
1 => new FileHistoriesSingleRevision(_repo, file, SelectedCommits[0], _prevIsDiffMode),
306-
2 => new FileHistoriesCompareRevisions(_repo, file, SelectedCommits[0], SelectedCommits[1]),
307-
_ => SelectedCommits.Count,
277+
1 => new FileHistoriesSingleRevision(_repo, SelectedRevisions[0], _prevIsDiffMode),
278+
2 => new FileHistoriesCompareRevisions(_repo, SelectedRevisions[0], SelectedRevisions[1]),
279+
_ => SelectedRevisions.Count,
308280
};
309281
};
310282
}
311283

312-
public void NavigateToCommit(Models.Commit commit)
284+
public void NavigateToCommit(Models.FileVersion revision)
313285
{
314286
var launcher = App.GetLauncher();
315287
if (launcher != null)
@@ -318,16 +290,16 @@ public void NavigateToCommit(Models.Commit commit)
318290
{
319291
if (page.Data is Repository repo && repo.FullPath.Equals(_repo, StringComparison.Ordinal))
320292
{
321-
repo.NavigateToCommit(commit.SHA);
293+
repo.NavigateToCommit(revision.SHA);
322294
break;
323295
}
324296
}
325297
}
326298
}
327299

328-
public string GetCommitFullMessage(Models.Commit commit)
300+
public string GetCommitFullMessage(Models.FileVersion revision)
329301
{
330-
var sha = commit.SHA;
302+
var sha = revision.SHA;
331303
if (_fullCommitMessages.TryGetValue(sha, out var msg))
332304
return msg;
333305

@@ -339,7 +311,7 @@ public string GetCommitFullMessage(Models.Commit commit)
339311
private readonly string _repo = null;
340312
private bool _isLoading = true;
341313
private bool _prevIsDiffMode = true;
342-
private List<Models.Commit> _commits = null;
314+
private List<Models.FileVersion> _revisions = null;
343315
private Dictionary<string, string> _fullCommitMessages = new();
344316
private object _viewContent = null;
345317
}

0 commit comments

Comments
 (0)