Skip to content

enhance: file rename detection and path resolution in file history #1372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/Commands/CompareRevisions.cs
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ public partial class CompareRevisions : Command
{
[GeneratedRegex(@"^([MADC])\s+(.+)$")]
private static partial Regex REG_FORMAT();
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)$")]
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)\s+(.+)$")]
private static partial Regex REG_RENAME_FORMAT();

public CompareRevisions(string repo, string start, string end)
@@ -51,7 +51,11 @@ private void ParseLine(string line)
match = REG_RENAME_FORMAT().Match(line);
if (match.Success)
{
var renamed = new Models.Change() { Path = match.Groups[1].Value };
var renamed = new Models.Change()
{
OriginalPath = match.Groups[1].Value,
Path = match.Groups[2].Value
};
renamed.Set(Models.ChangeState.Renamed);
_changes.Add(renamed);
}
2 changes: 1 addition & 1 deletion src/Commands/QueryCommits.cs
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ public QueryCommits(string repo, string filter, Models.CommitSearchMethod method
}
else if (method == Models.CommitSearchMethod.ByFile)
{
search += $"-- \"{filter}\"";
search += $"--follow -- \"{filter}\"";
}
else
{
124 changes: 124 additions & 0 deletions src/Commands/QueryFilePathInRevision.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace SourceGit.Commands
{
public partial class QueryFilePathInRevision : Command
{
[GeneratedRegex(@"^R[0-9]{0,4}\s+(.+)\s+(.+)$")]
private static partial Regex REG_RENAME_FORMAT();

public QueryFilePathInRevision(string repo, string revision, string currentPath)
{
WorkingDirectory = repo;
Context = repo;
_revision = revision;
_currentPath = currentPath;
}

public string Result()
{
if (CheckPathExistsInRevision(_currentPath))
return _currentPath;

string mappedPath = FindRenameHistory();
return mappedPath ?? _currentPath;
}

private bool CheckPathExistsInRevision(string path)
{
Args = $"ls-tree -r {_revision} -- \"{path}\"";
var rs = ReadToEnd();
return rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut);
}

private string FindRenameHistory()
{
var fileHistory = BuildFileHistory();
if (fileHistory == null || fileHistory.Count == 0)
return null;

foreach (var entry in fileHistory)
{
if (!IsTargetRevisionBefore(entry.CommitSHA))
continue;

if (CheckPathExistsInRevision(entry.OldPath))
return entry.OldPath;
}

if (fileHistory.Count > 0)
{
var oldestPath = fileHistory[^1].OldPath;
if (CheckPathExistsInRevision(oldestPath))
return oldestPath;
}

return null;
}

private bool IsTargetRevisionBefore(string commitSHA)
{
Args = $"merge-base --is-ancestor {_revision} {commitSHA}";
var rs = ReadToEnd();
return rs.IsSuccess;
}

private List<RenameHistoryEntry> BuildFileHistory()
{
Args = $"log --follow --name-status --pretty=format:\"commit %H\" -M -- \"{_currentPath}\"";
var rs = ReadToEnd();
if (!rs.IsSuccess)
return null;

var result = new List<RenameHistoryEntry>();
var lines = rs.StdOut.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);

string currentCommit = null;
string currentPath = _currentPath;

foreach (var t in lines)
{
var line = t.Trim();

if (line.StartsWith("commit ", StringComparison.Ordinal))
{
currentCommit = line.Substring("commit ".Length);
continue;
}

var match = REG_RENAME_FORMAT().Match(line);
if (match.Success && currentCommit != null)
{
var oldPath = match.Groups[1].Value;
var newPath = match.Groups[2].Value;

if (newPath == currentPath)
{
result.Add(new RenameHistoryEntry
{
CommitSHA = currentCommit,
OldPath = oldPath,
NewPath = newPath
});

currentPath = oldPath;
}
}
}

return result;
}

private class RenameHistoryEntry
{
public string CommitSHA { get; set; }
public string OldPath { get; set; }
public string NewPath { get; set; }
}

private readonly string _revision;
private readonly string _currentPath;
}
}
73 changes: 50 additions & 23 deletions src/Models/Change.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

namespace SourceGit.Models
{
@@ -9,17 +10,18 @@ public enum ChangeViewMode
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
@@ -54,8 +56,8 @@ public class Change
public string ConflictMarker => CONFLICT_MARKERS[(int)ConflictReason];
public string ConflictDesc => CONFLICT_DESCS[(int)ConflictReason];

public string WorkTreeDesc => TYPE_DESCS[(int)WorkTree];
public string IndexDesc => TYPE_DESCS[(int)Index];
public string WorkTreeDesc => TYPE_DESCS[GetPrimaryState(WorkTree)];
public string IndexDesc => TYPE_DESCS[GetPrimaryState(Index)];

public void Set(ChangeState index, ChangeState workTree = ChangeState.None)
{
@@ -88,18 +90,43 @@ public void Set(ChangeState index, ChangeState workTree = ChangeState.None)
OriginalPath = OriginalPath.Substring(1, OriginalPath.Length - 2);
}

private static readonly string[] TYPE_DESCS =
[
"Unknown",
"Modified",
"Type Changed",
"Added",
"Deleted",
"Renamed",
"Copied",
"Untracked",
"Conflict"
];
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;
}

private static readonly Dictionary<ChangeState, string> TYPE_DESCS = new Dictionary<ChangeState, string>
{
{ ChangeState.None, "Unknown" },
{ ChangeState.Modified, "Modified" },
{ ChangeState.TypeChanged, "Type Changed" },
{ ChangeState.Added, "Added" },
{ ChangeState.Deleted, "Deleted" },
{ ChangeState.Renamed, "Renamed" },
{ ChangeState.Copied, "Copied" },
{ ChangeState.Untracked, "Untracked" },
{ ChangeState.Conflicted, "Conflict" }
};

private static readonly string[] CONFLICT_MARKERS =
[
string.Empty,
194 changes: 172 additions & 22 deletions src/ViewModels/FileHistories.cs
Original file line number Diff line number Diff line change
@@ -49,7 +49,8 @@ public FileHistoriesSingleRevision(Repository repo, string file, Models.Commit r

public void ResetToSelectedRevision()
{
new Commands.Checkout(_repo.FullPath).FileWithRevision(_file, $"{_revision.SHA}");
var revisionFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _revision.SHA, _file).Result();
new Commands.Checkout(_repo.FullPath).FileWithRevision(revisionFilePath, $"{_revision.SHA}");
}

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

private void SetViewContentAsRevisionFile()
{
var objs = new Commands.QueryRevisionObjects(_repo.FullPath, _revision.SHA, _file).Result();
var revisionFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _revision.SHA, _file).Result();

var objs = new Commands.QueryRevisionObjects(_repo.FullPath, _revision.SHA, revisionFilePath).Result();
if (objs.Count == 0)
{
ViewContent = new FileHistoriesRevisionFile(_file, null);
ViewContent = new FileHistoriesRevisionFile(revisionFilePath, null);
return;
}

@@ -75,50 +78,49 @@ private void SetViewContentAsRevisionFile()
case Models.ObjectType.Blob:
Task.Run(() =>
{
var isBinary = new Commands.IsBinary(_repo.FullPath, _revision.SHA, _file).Result();
var isBinary = new Commands.IsBinary(_repo.FullPath, _revision.SHA, revisionFilePath).Result();
if (isBinary)
{
var ext = Path.GetExtension(_file);
var ext = Path.GetExtension(revisionFilePath);
if (IMG_EXTS.Contains(ext))
{
var stream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, _file);
var stream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, revisionFilePath);
var fileSize = stream.Length;
var bitmap = fileSize > 0 ? new Bitmap(stream) : null;
var imageType = Path.GetExtension(_file).TrimStart('.').ToUpper(CultureInfo.CurrentCulture);
var imageType = Path.GetExtension(revisionFilePath).TrimStart('.').ToUpper(CultureInfo.CurrentCulture);
var image = new Models.RevisionImageFile() { Image = bitmap, FileSize = fileSize, ImageType = imageType };
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, image));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, image));
}
else
{
var size = new Commands.QueryFileSize(_repo.FullPath, _file, _revision.SHA).Result();
var size = new Commands.QueryFileSize(_repo.FullPath, revisionFilePath, _revision.SHA).Result();
var binaryFile = new Models.RevisionBinaryFile() { Size = size };
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, binaryFile));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, binaryFile));
}

return;
}

var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, _file);
var contentStream = Commands.QueryFileContent.Run(_repo.FullPath, _revision.SHA, revisionFilePath);
var content = new StreamReader(contentStream).ReadToEnd();
var matchLFS = REG_LFS_FORMAT().Match(content);
if (matchLFS.Success)
{
var lfs = new Models.RevisionLFSObject() { Object = new Models.LFSObject() };
lfs.Object.Oid = matchLFS.Groups[1].Value;
lfs.Object.Size = long.Parse(matchLFS.Groups[2].Value);
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, lfs));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, lfs));
}
else
{
var txt = new Models.RevisionTextFile() { FileName = obj.Path, Content = content };
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, txt));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, txt));
}
});
break;
case Models.ObjectType.Commit:
Task.Run(() =>
{
var submoduleRoot = Path.Combine(_repo.FullPath, _file);
var submoduleRoot = Path.Combine(_repo.FullPath, revisionFilePath);
var commit = new Commands.QuerySingleCommit(submoduleRoot, obj.SHA).Result();
if (commit != null)
{
@@ -128,7 +130,7 @@ private void SetViewContentAsRevisionFile()
Commit = commit,
FullMessage = new Models.CommitFullMessage { Message = message }
};
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, module));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, module));
}
else
{
@@ -137,20 +139,38 @@ private void SetViewContentAsRevisionFile()
Commit = new Models.Commit() { SHA = obj.SHA },
FullMessage = null
};
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(_file, module));
Dispatcher.UIThread.Invoke(() => ViewContent = new FileHistoriesRevisionFile(revisionFilePath, module));
}
});
break;
default:
ViewContent = new FileHistoriesRevisionFile(_file, null);
ViewContent = new FileHistoriesRevisionFile(revisionFilePath, null);
break;
}
}

private void SetViewContentAsDiff()
{
var option = new Models.DiffOption(_revision, _file);
ViewContent = new DiffContext(_repo.FullPath, option, _viewContent as DiffContext);
var revisionFilePath = new Commands.QueryFilePathInRevision(_repo.FullPath, _revision.SHA, _file).Result();

if (_revision.Parents.Count > 0)
{
var parentSHA = _revision.Parents[0];
var changes = new Commands.CompareRevisions(_repo.FullPath, parentSHA, _revision.SHA).Result();
foreach (var change in changes)
{
if ((change.WorkTree == Models.ChangeState.Renamed || change.Index == Models.ChangeState.Renamed)
&& change.Path == revisionFilePath)
{
var option = new Models.DiffOption(parentSHA, _revision.SHA, change);
ViewContent = new DiffContext(_repo.FullPath, option, _viewContent as DiffContext);
return;
}
}
}

var defaultOption = new Models.DiffOption(_revision, revisionFilePath);
ViewContent = new DiffContext(_repo.FullPath, defaultOption, _viewContent as DiffContext);
}

[GeneratedRegex(@"^version https://git-lfs.github.com/spec/v\d+\r?\noid sha256:([0-9a-f]+)\r?\nsize (\d+)[\r\n]*$")]
@@ -216,7 +236,105 @@ private void RefreshViewContent()
{
Task.Run(() =>
{
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, _file).Result();
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) != 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 if (startFilePath != endFilePath)
{
_changes = new Commands.CompareRevisions(_repo.FullPath, _startPoint.SHA, _endPoint.SHA, startFilePath).Result();

if (_changes.Count == 0)
{
var renamed = new Models.Change()
{
OriginalPath = startFilePath,
Path = endFilePath
};

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)
{
Dispatcher.UIThread.Invoke(() => ViewContent = null);
@@ -228,6 +346,38 @@ private void RefreshViewContent()
});
}

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;
@@ -270,7 +420,7 @@ public FileHistories(Repository repo, string file, string commit = null)
Task.Run(() =>
{
var based = commit ?? string.Empty;
var commits = new Commands.QueryCommits(_repo.FullPath, $"--date-order -n 10000 {based} -- \"{file}\"", false).Result();
var commits = new Commands.QueryCommits(_repo.FullPath, $"--date-order --follow -n 10000 {based} -- \"{file}\"", false).Result();
Dispatcher.UIThread.Invoke(() =>
{
IsLoading = false;
112 changes: 67 additions & 45 deletions src/Views/ChangeStatusIcon.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;

using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
@@ -9,53 +9,73 @@ namespace SourceGit.Views
{
public class ChangeStatusIcon : Control
{
private static readonly string[] INDICATOR = ["?", "±", "T", "+", "−", "➜", "❏", "★", "!"];
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, 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, 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 },
};

public static readonly StyledProperty<bool> IsUnstagedChangeProperty =
AvaloniaProperty.Register<ChangeStatusIcon, bool>(nameof(IsUnstagedChange));
@@ -86,13 +106,15 @@ public override void Render(DrawingContext context)
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(