diff --git a/src/Models/Change.cs b/src/Models/Change.cs index 6e507051..125935db 100644 --- a/src/Models/Change.cs +++ b/src/Models/Change.cs @@ -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; + } } } diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index 2d1defd6..7c0dc2ab 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -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 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; diff --git a/src/Views/ChangeStatusIcon.cs b/src/Views/ChangeStatusIcon.cs index c1185174..f4065034 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,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 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 }, + }; - 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 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 TIPS = new Dictionary() + { + { 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 IsUnstagedChangeProperty = AvaloniaProperty.Register(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(); } }