fix: file history comparison with empty and renamed revisions

This commit is contained in:
Gadfly 2025-05-29 15:48:32 +08:00
parent 70ffe9ac71
commit a418880656
No known key found for this signature in database
3 changed files with 222 additions and 77 deletions

View file

@ -9,17 +9,18 @@ namespace SourceGit.Models
Tree,
}
[Flags]
public enum ChangeState
{
None,
Modified,
TypeChanged,
Added,
Deleted,
Renamed,
Copied,
Untracked,
Conflicted,
None = 0,
Modified = 1 << 0,
TypeChanged = 1 << 1,
Added = 1 << 2,
Deleted = 1 << 3,
Renamed = 1 << 4,
Copied = 1 << 5,
Untracked = 1 << 6,
Conflicted = 1 << 7,
}
public enum ConflictReason
@ -81,5 +82,29 @@ namespace SourceGit.Models
if (!string.IsNullOrEmpty(OriginalPath) && OriginalPath[0] == '"')
OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2);
}
public static ChangeState GetPrimaryState(ChangeState state)
{
if (state == ChangeState.None)
return ChangeState.None;
if ((state & ChangeState.Conflicted) != 0)
return ChangeState.Conflicted;
if ((state & ChangeState.Untracked) != 0)
return ChangeState.Untracked;
if ((state & ChangeState.Renamed) != 0)
return ChangeState.Renamed;
if ((state & ChangeState.Copied) != 0)
return ChangeState.Copied;
if ((state & ChangeState.Deleted) != 0)
return ChangeState.Deleted;
if ((state & ChangeState.Added) != 0)
return ChangeState.Added;
if ((state & ChangeState.TypeChanged) != 0)
return ChangeState.TypeChanged;
if ((state & ChangeState.Modified) != 0)
return ChangeState.Modified;
return ChangeState.None;
}
}
}

View file

@ -238,46 +238,101 @@ namespace SourceGit.ViewModels
{
var startFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _startPoint.SHA, _file).Result();
var endFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _endPoint.SHA, _file).Result();
var allChanges = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA).Result();
var startCommand = new Commands.QueryRevisionObjects(_repo.FullPath, _startPoint.SHA, startFilePath);
var startResult = startCommand.Result();
bool startFileExists = startResult.Count > 0;
var endCommand = new Commands.QueryRevisionObjects(_repo.FullPath, _endPoint.SHA, endFilePath);
var endResult = endCommand.Result();
bool endFileExists = endResult.Count > 0;
Models.Change renamedChange = null;
foreach (var change in allChanges)
{
if (change.WorkTree != Models.ChangeState.Renamed && change.Index != Models.ChangeState.Renamed)
continue;
if (change.Path != endFilePath && change.OriginalPath != startFilePath)
continue;
renamedChange = change;
break;
if ((change.WorkTree & Models.ChangeState.Renamed) != 0 ||
(change.Index & Models.ChangeState.Renamed) != 0)
{
if (change.Path == endFilePath || change.OriginalPath == startFilePath)
{
renamedChange = change;
break;
}
}
}
bool hasChanges = false;
if (renamedChange != null)
{
if (string.IsNullOrEmpty(renamedChange.OriginalPath))
renamedChange.OriginalPath = startFilePath;
if (string.IsNullOrEmpty(renamedChange.Path))
renamedChange.Path = endFilePath;
bool hasContentChange = (!startFileExists || IsEmptyFile(_repo.FullPath, _startPoint.SHA, startFilePath)) &&
endFileExists && !IsEmptyFile(_repo.FullPath, _endPoint.SHA, endFilePath);
if (!hasContentChange)
hasContentChange = ContainsContentChanges(allChanges, startFilePath, endFilePath);
if (hasContentChange)
{
renamedChange.Index |= Models.ChangeState.Modified;
renamedChange.WorkTree |= Models.ChangeState.Modified;
}
_changes = [renamedChange];
hasChanges = true;
}
else
else if (startFilePath != endFilePath)
{
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, startFilePath).Result();
if (_changes.Count == 0 && startFilePath != endFilePath)
if (_changes.Count == 0)
{
var renamed = new Models.Change()
{
OriginalPath = startFilePath,
Path = endFilePath
};
renamed.Set(Models.ChangeState.Renamed);
_changes = [renamed];
}
else if (_changes.Count == 0)
{
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, endFilePath).Result();
if (_changes.Count == 0)
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, _file).Result();
bool hasContentChange = (!startFileExists || IsEmptyFile(_repo.FullPath, _startPoint.SHA, startFilePath)) &&
endFileExists && !IsEmptyFile(_repo.FullPath, _endPoint.SHA, endFilePath);
if (hasContentChange)
renamed.Set(Models.ChangeState.Modified | Models.ChangeState.Renamed);
else
renamed.Set(Models.ChangeState.Renamed);
_changes = [renamed];
hasChanges = true;
}
else
{
foreach (var change in _changes)
{
if (string.IsNullOrEmpty(change.OriginalPath) && change.Path == startFilePath)
{
change.OriginalPath = startFilePath;
change.Path = endFilePath;
change.Index |= Models.ChangeState.Renamed;
change.WorkTree |= Models.ChangeState.Renamed;
}
}
hasChanges = true;
}
}
if (!hasChanges)
{
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, endFilePath).Result();
if (_changes.Count == 0)
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, _file).Result();
}
if (_changes.Count == 0)
@ -291,6 +346,38 @@ namespace SourceGit.ViewModels
});
}
private bool ContainsContentChanges(List<Models.Change> changes, string startPath, string endPath)
{
foreach (var change in changes)
{
if (change.Path == endPath || change.OriginalPath == startPath)
{
bool hasContentChanges =
(change.WorkTree == Models.ChangeState.Modified ||
change.WorkTree == Models.ChangeState.Added ||
change.Index == Models.ChangeState.Modified ||
change.Index == Models.ChangeState.Added);
if (hasContentChanges)
return true;
}
}
return false;
}
private bool IsEmptyFile(string repoPath, string revision, string filePath)
{
try
{
var contentStream = Commands.QueryFileContent.Run(repoPath, revision, filePath);
return contentStream != null && contentStream.Length == 0;
}
catch
{
return true;
}
}
private Repository _repo = null;
private string _file = null;
private Models.Commit _startPoint = null;

View file

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
@ -9,55 +9,86 @@ namespace SourceGit.Views
{
public class ChangeStatusIcon : Control
{
private static readonly IBrush[] BACKGROUNDS = [
Brushes.Transparent,
new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
private static readonly Dictionary<Models.ChangeState, IBrush> BACKGROUNDS = new Dictionary<Models.ChangeState, IBrush>()
{
{ Models.ChangeState.None, Brushes.Transparent },
{ Models.ChangeState.Modified, new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
}
},
new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
{ Models.ChangeState.TypeChanged, new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
}
},
new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
{ Models.ChangeState.Added, new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
}
},
new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
{ Models.ChangeState.Deleted, new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Colors.Tomato, 0), new GradientStop(Color.FromRgb(252, 165, 150), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
}
},
new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
{ Models.ChangeState.Renamed, new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Colors.Orchid, 0), new GradientStop(Color.FromRgb(248, 161, 245), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
}
},
new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
{ Models.ChangeState.Copied, new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(238, 160, 14), 0), new GradientStop(Color.FromRgb(228, 172, 67), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
}
},
new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
{ Models.ChangeState.Untracked, new LinearGradientBrush
{
GradientStops = new GradientStops() { new GradientStop(Color.FromRgb(47, 185, 47), 0), new GradientStop(Color.FromRgb(75, 189, 75), 1) },
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0, 1, RelativeUnit.Relative),
}
},
Brushes.OrangeRed,
];
{ Models.ChangeState.Conflicted, Brushes.OrangeRed },
};
private static readonly string[] INDICATOR = ["?", "±", "T", "+", "", "➜", "❏", "★", "!"];
private static readonly string[] TIPS = ["Unknown", "Modified", "Type Changed", "Added", "Deleted", "Renamed", "Copied", "Untracked", "Conflict"];
private static readonly Dictionary<Models.ChangeState, string> INDICATOR = new Dictionary<Models.ChangeState, string>()
{
{ Models.ChangeState.None, "?" },
{ Models.ChangeState.Modified, "±" },
{ Models.ChangeState.TypeChanged, "T" },
{ Models.ChangeState.Added, "+" },
{ Models.ChangeState.Deleted, "" },
{ Models.ChangeState.Renamed, "➜" },
{ Models.ChangeState.Copied, "❏" },
{ Models.ChangeState.Untracked, "★" },
{ Models.ChangeState.Conflicted, "!" }
};
private static readonly Dictionary<Models.ChangeState, string> TIPS = new Dictionary<Models.ChangeState, string>()
{
{ Models.ChangeState.None, "Unknown" },
{ Models.ChangeState.Modified, "Modified" },
{ Models.ChangeState.TypeChanged, "Type Changed" },
{ Models.ChangeState.Added, "Added" },
{ Models.ChangeState.Deleted, "Deleted" },
{ Models.ChangeState.Renamed, "Renamed" },
{ Models.ChangeState.Copied, "Copied" },
{ Models.ChangeState.Untracked, "Untracked" },
{ Models.ChangeState.Conflicted, "Conflict" }
};
public static readonly StyledProperty<bool> IsUnstagedChangeProperty =
AvaloniaProperty.Register<ChangeStatusIcon, bool>(nameof(IsUnstagedChange));
@ -88,13 +119,15 @@ namespace SourceGit.Views
string indicator;
if (IsUnstagedChange)
{
background = BACKGROUNDS[(int)Change.WorkTree];
indicator = INDICATOR[(int)Change.WorkTree];
var status = Models.Change.GetPrimaryState(Change.WorkTree);
background = BACKGROUNDS[status];
indicator = INDICATOR[status];
}
else
{
background = BACKGROUNDS[(int)Change.Index];
indicator = INDICATOR[(int)Change.Index];
var status = Models.Change.GetPrimaryState(Change.Index);
background = BACKGROUNDS[status];
indicator = INDICATOR[status];
}
var txt = new FormattedText(
@ -125,11 +158,11 @@ namespace SourceGit.Views
return;
}
if (isUnstaged)
ToolTip.SetTip(this, TIPS[(int)c.WorkTree]);
else
ToolTip.SetTip(this, TIPS[(int)c.Index]);
var status = isUnstaged ?
Models.Change.GetPrimaryState(c.WorkTree) :
Models.Change.GetPrimaryState(c.Index);
ToolTip.SetTip(this, TIPS[status]);
InvalidateVisual();
}
}