Skip to content

Commit a418880

Browse files
committed
fix: file history comparison with empty and renamed revisions
1 parent 70ffe9a commit a418880

File tree

3 files changed

+219
-74
lines changed

3 files changed

+219
-74
lines changed

src/Models/Change.cs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ public enum ChangeViewMode
99
Tree,
1010
}
1111

12+
[Flags]
1213
public enum ChangeState
1314
{
14-
None,
15-
Modified,
16-
TypeChanged,
17-
Added,
18-
Deleted,
19-
Renamed,
20-
Copied,
21-
Untracked,
22-
Conflicted,
15+
None = 0,
16+
Modified = 1 << 0,
17+
TypeChanged = 1 << 1,
18+
Added = 1 << 2,
19+
Deleted = 1 << 3,
20+
Renamed = 1 << 4,
21+
Copied = 1 << 5,
22+
Untracked = 1 << 6,
23+
Conflicted = 1 << 7,
2324
}
2425

2526
public enum ConflictReason
@@ -81,5 +82,29 @@ public void Set(ChangeState index, ChangeState workTree = ChangeState.None)
8182
if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"')
8283
OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2);
8384
}
85+
86+
public static ChangeState GetPrimaryState(ChangeState state)
87+
{
88+
if (state == ChangeState.None)
89+
return ChangeState.None;
90+
if ((state & ChangeState.Conflicted) != 0)
91+
return ChangeState.Conflicted;
92+
if ((state & ChangeState.Untracked) != 0)
93+
return ChangeState.Untracked;
94+
if ((state & ChangeState.Renamed) != 0)
95+
return ChangeState.Renamed;
96+
if ((state & ChangeState.Copied) != 0)
97+
return ChangeState.Copied;
98+
if ((state & ChangeState.Deleted) != 0)
99+
return ChangeState.Deleted;
100+
if ((state & ChangeState.Added) != 0)
101+
return ChangeState.Added;
102+
if ((state & ChangeState.TypeChanged) != 0)
103+
return ChangeState.TypeChanged;
104+
if ((state & ChangeState.Modified) != 0)
105+
return ChangeState.Modified;
106+
107+
return ChangeState.None;
108+
}
84109
}
85110
}

src/ViewModels/FileHistories.cs

Lines changed: 102 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -238,48 +238,103 @@ private void RefreshViewContent()
238238
{
239239
var startFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _startPoint.SHA, _file).Result();
240240
var endFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _endPoint.SHA, _file).Result();
241-
242241
var allChanges = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA).Result();
243242

243+
var startCommand = new Commands.QueryRevisionObjects(_repo.FullPath, _startPoint.SHA, startFilePath);
244+
var startResult = startCommand.Result();
245+
bool startFileExists = startResult.Count > 0;
246+
247+
var endCommand = new Commands.QueryRevisionObjects(_repo.FullPath, _endPoint.SHA, endFilePath);
248+
var endResult = endCommand.Result();
249+
bool endFileExists = endResult.Count > 0;
250+
244251
Models.Change renamedChange = null;
245252
foreach (var change in allChanges)
246253
{
247-
if (change.WorkTree != Models.ChangeState.Renamed && change.Index != Models.ChangeState.Renamed)
248-
continue;
249-
if (change.Path != endFilePath && change.OriginalPath != startFilePath)
250-
continue;
251-
252-
renamedChange = change;
253-
break;
254+
if ((change.WorkTree & Models.ChangeState.Renamed) != 0 ||
255+
(change.Index & Models.ChangeState.Renamed) != 0)
256+
{
257+
if (change.Path == endFilePath || change.OriginalPath == startFilePath)
258+
{
259+
renamedChange = change;
260+
break;
261+
}
262+
}
254263
}
255264

265+
bool hasChanges = false;
266+
256267
if (renamedChange != null)
257268
{
269+
if (string.IsNullOrEmpty(renamedChange.OriginalPath))
270+
renamedChange.OriginalPath = startFilePath;
271+
272+
if (string.IsNullOrEmpty(renamedChange.Path))
273+
renamedChange.Path = endFilePath;
274+
275+
bool hasContentChange = (!startFileExists || IsEmptyFile(_repo.FullPath, _startPoint.SHA, startFilePath)) &&
276+
endFileExists && !IsEmptyFile(_repo.FullPath, _endPoint.SHA, endFilePath);
277+
278+
if (!hasContentChange)
279+
hasContentChange = ContainsContentChanges(allChanges, startFilePath, endFilePath);
280+
281+
if (hasContentChange)
282+
{
283+
renamedChange.Index |= Models.ChangeState.Modified;
284+
renamedChange.WorkTree |= Models.ChangeState.Modified;
285+
}
286+
258287
_changes = [renamedChange];
288+
hasChanges = true;
259289
}
260-
else
290+
else if (startFilePath != endFilePath)
261291
{
262292
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, startFilePath).Result();
263293

264-
if (_changes.Count == 0 && startFilePath != endFilePath)
294+
if (_changes.Count == 0)
265295
{
266296
var renamed = new Models.Change()
267297
{
268298
OriginalPath = startFilePath,
269299
Path = endFilePath
270300
};
271-
renamed.Set(Models.ChangeState.Renamed);
301+
302+
bool hasContentChange = (!startFileExists || IsEmptyFile(_repo.FullPath, _startPoint.SHA, startFilePath)) &&
303+
endFileExists && !IsEmptyFile(_repo.FullPath, _endPoint.SHA, endFilePath);
304+
305+
if (hasContentChange)
306+
renamed.Set(Models.ChangeState.Modified | Models.ChangeState.Renamed);
307+
else
308+
renamed.Set(Models.ChangeState.Renamed);
309+
272310
_changes = [renamed];
311+
hasChanges = true;
273312
}
274-
else if (_changes.Count == 0)
313+
else
275314
{
276-
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, endFilePath).Result();
315+
foreach (var change in _changes)
316+
{
317+
if (string.IsNullOrEmpty(change.OriginalPath) && change.Path == startFilePath)
318+
{
319+
change.OriginalPath = startFilePath;
320+
change.Path = endFilePath;
277321

278-
if (_changes.Count == 0)
279-
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, _file).Result();
322+
change.Index |= Models.ChangeState.Renamed;
323+
change.WorkTree |= Models.ChangeState.Renamed;
324+
}
325+
}
326+
hasChanges = true;
280327
}
281328
}
282329

330+
if (!hasChanges)
331+
{
332+
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, endFilePath).Result();
333+
334+
if (_changes.Count == 0)
335+
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, _file).Result();
336+
}
337+
283338
if (_changes.Count == 0)
284339
{
285340
Dispatcher.UIThread.Invoke(() => ViewContent = null);
@@ -291,6 +346,38 @@ private void RefreshViewContent()
291346
});
292347
}
293348

349+
private bool ContainsContentChanges(List<Models.Change> changes, string startPath, string endPath)
350+
{
351+
foreach (var change in changes)
352+
{
353+
if (change.Path == endPath || change.OriginalPath == startPath)
354+
{
355+
bool hasContentChanges =
356+
(change.WorkTree == Models.ChangeState.Modified ||
357+
change.WorkTree == Models.ChangeState.Added ||
358+
change.Index == Models.ChangeState.Modified ||
359+
change.Index == Models.ChangeState.Added);
360+
361+
if (hasContentChanges)
362+
return true;
363+
}
364+
}
365+
return false;
366+
}
367+
368+
private bool IsEmptyFile(string repoPath, string revision, string filePath)
369+
{
370+
try
371+
{
372+
var contentStream = Commands.QueryFileContent.Run(repoPath, revision, filePath);
373+
return contentStream != null && contentStream.Length == 0;
374+
}
375+
catch
376+
{
377+
return true;
378+
}
379+
}
380+
294381
private Repository _repo = null;
295382
private string _file = null;
296383
private Models.Commit _startPoint = null;

src/Views/ChangeStatusIcon.cs

Lines changed: 83 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Globalization;
3-
44
using Avalonia;
55
using Avalonia.Controls;
66
using Avalonia.Media;
@@ -9,55 +9,86 @@ namespace SourceGit.Views
99
{
1010
public class ChangeStatusIcon : Control
1111
{
12-
private static readonly IBrush[] BACKGROUNDS = [
13-
Brushes.Transparent,
14-
new LinearGradientBrush
15-
{
16-
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
17-
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
18-
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
12+
private static readonly Dictionary<Models.ChangeState, IBrush> BACKGROUNDS = new Dictionary<Models.ChangeState, IBrush>()
13+
{
14+
{ Models.ChangeState.None, Brushes.Transparent },
15+
{ Models.ChangeState.Modified, new LinearGradientBrush
16+
{
17+
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
18+
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
19+
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
20+
}
1921
},
20-
new LinearGradientBrush
21-
{
22-
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
23-
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
24-
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
22+
{ Models.ChangeState.TypeChanged, new LinearGradientBrush
23+
{
24+
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
25+
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
26+
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
27+
}
2528
},
26-
new LinearGradientBrush
27-
{
28-
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) },
29-
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
30-
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
29+
{ Models.ChangeState.Added, new LinearGradientBrush
30+
{
31+
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) },
32+
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
33+
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
34+
}
3135
},
32-
new LinearGradientBrush
33-
{
34-
GradientStops = new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) },
35-
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
36-
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
36+
{ Models.ChangeState.Deleted, new LinearGradientBrush
37+
{
38+
GradientStops = new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) },
39+
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
40+
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
41+
}
3742
},
38-
new LinearGradientBrush
39-
{
40-
GradientStops = new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) },
41-
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
42-
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
43+
{ Models.ChangeState.Renamed, new LinearGradientBrush
44+
{
45+
GradientStops = new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) },
46+
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
47+
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
48+
}
4349
},
44-
new LinearGradientBrush
45-
{
46-
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
47-
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
48-
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
50+
{ Models.ChangeState.Copied, new LinearGradientBrush
51+
{
52+
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
53+
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
54+
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
55+
}
4956
},
50-
new LinearGradientBrush
51-
{
52-
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) },
53-
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
54-
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
57+
{ Models.ChangeState.Untracked, new LinearGradientBrush
58+
{
59+
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) },
60+
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
61+
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
62+
}
5563
},
56-
Brushes.OrangeRed,
57-
];
64+
{ Models.ChangeState.Conflicted, Brushes.OrangeRed },
65+
};
5866

59-
private static readonly string[] INDICATOR = ["?", "±", "T", "+", "−", "➜", "❏", "★", "!"];
60-
private static readonly string[] TIPS = ["Unknown", "Modified", "Type Changed", "Added", "Deleted", "Renamed", "Copied", "Untracked", "Conflict"];
67+
private static readonly Dictionary<Models.ChangeState, string> INDICATOR = new Dictionary<Models.ChangeState, string>()
68+
{
69+
{ Models.ChangeState.None, "?" },
70+
{ Models.ChangeState.Modified, "±" },
71+
{ Models.ChangeState.TypeChanged, "T" },
72+
{ Models.ChangeState.Added, "+" },
73+
{ Models.ChangeState.Deleted, "−" },
74+
{ Models.ChangeState.Renamed, "➜" },
75+
{ Models.ChangeState.Copied, "❏" },
76+
{ Models.ChangeState.Untracked, "★" },
77+
{ Models.ChangeState.Conflicted, "!" }
78+
};
79+
80+
private static readonly Dictionary<Models.ChangeState, string> TIPS = new Dictionary<Models.ChangeState, string>()
81+
{
82+
{ Models.ChangeState.None, "Unknown" },
83+
{ Models.ChangeState.Modified, "Modified" },
84+
{ Models.ChangeState.TypeChanged, "Type Changed" },
85+
{ Models.ChangeState.Added, "Added" },
86+
{ Models.ChangeState.Deleted, "Deleted" },
87+
{ Models.ChangeState.Renamed, "Renamed" },
88+
{ Models.ChangeState.Copied, "Copied" },
89+
{ Models.ChangeState.Untracked, "Untracked" },
90+
{ Models.ChangeState.Conflicted, "Conflict" }
91+
};
6192

6293
public static readonly StyledProperty<bool> IsUnstagedChangeProperty =
6394
AvaloniaProperty.Register<ChangeStatusIcon, bool>(nameof(IsUnstagedChange));
@@ -88,13 +119,15 @@ public override void Render(DrawingContext context)
88119
string indicator;
89120
if (IsUnstagedChange)
90121
{
91-
background = BACKGROUNDS[(int)Change.WorkTree];
92-
indicator = INDICATOR[(int)Change.WorkTree];
122+
var status = Models.Change.GetPrimaryState(Change.WorkTree);
123+
background = BACKGROUNDS[status];
124+
indicator = INDICATOR[status];
93125
}
94126
else
95127
{
96-
background = BACKGROUNDS[(int)Change.Index];
97-
indicator = INDICATOR[(int)Change.Index];
128+
var status = Models.Change.GetPrimaryState(Change.Index);
129+
background = BACKGROUNDS[status];
130+
indicator = INDICATOR[status];
98131
}
99132

100133
var txt = new FormattedText(
@@ -125,11 +158,11 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
125158
return;
126159
}
127160

128-
if (isUnstaged)
129-
ToolTip.SetTip(this, TIPS[(int)c.WorkTree]);
130-
else
131-
ToolTip.SetTip(this, TIPS[(int)c.Index]);
161+
var status = isUnstaged ?
162+
Models.Change.GetPrimaryState(c.WorkTree) :
163+
Models.Change.GetPrimaryState(c.Index);
132164

165+
ToolTip.SetTip(this, TIPS[status]);
133166
InvalidateVisual();
134167
}
135168
}

0 commit comments

Comments
 (0)