diff --git a/src/Commands/CompareRevisions.cs b/src/Commands/CompareRevisions.cs index 7b4a496de..b13839114 100644 --- a/src/Commands/CompareRevisions.cs +++ b/src/Commands/CompareRevisions.cs @@ -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); } diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 9e1d9918e..53c8b370a 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -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 { diff --git a/src/Commands/QueryFilePathInRevision.cs b/src/Commands/QueryFilePathInRevision.cs new file mode 100644 index 000000000..3edd199e0 --- /dev/null +++ b/src/Commands/QueryFilePathInRevision.cs @@ -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 BuildFileHistory() + { + Args = $"log --follow --name-status --pretty=format:\"commit %H\" -M -- \"{_currentPath}\""; + var rs = ReadToEnd(); + if (!rs.IsSuccess) + return null; + + var result = new List(); + 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; + } +} diff --git a/src/Models/Change.cs b/src/Models/Change.cs index 129678be6..f5d8eaab8 100644 --- a/src/Models/Change.cs +++ b/src/Models/Change.cs @@ -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 TYPE_DESCS = new Dictionary + { + { 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, diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index 9f91205ee..7c0dc2ab0 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -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,30 +78,29 @@ 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) @@ -106,19 +108,19 @@ private void SetViewContentAsRevisionFile() 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 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; diff --git a/src/Views/ChangeStatusIcon.cs b/src/Views/ChangeStatusIcon.cs index d66ac11df..6bcfc807f 100644 --- a/src/Views/ChangeStatusIcon.cs +++ b/src/Views/ChangeStatusIcon.cs @@ -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 INDICATOR = new Dictionary() + { + { 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 BACKGROUNDS = new Dictionary() + { + { 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 IsUnstagedChangeProperty = AvaloniaProperty.Register(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(