From ba8c6382e7d9c9282f4ff05e7fc47e54dd430428 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 26 Jul 2024 11:28:40 +0200 Subject: [PATCH] feature: Allow show uncommitted changes in commits history like TortoiseHG Workspace --- src/Models/Commit.cs | 1 + src/Models/CommitGraph.cs | 52 +++++++++++----------- src/Resources/Locales/en_US.axaml | 2 + src/Resources/Locales/zh_CN.axaml | 2 + src/Resources/Locales/zh_TW.axaml | 2 + src/ViewModels/Histories.cs | 9 +++- src/ViewModels/Preference.cs | 7 +++ src/ViewModels/Repository.cs | 71 ++++++++++++++++++------------- src/ViewModels/WorkingCopy.cs | 41 ++++++++++++++++++ src/Views/CommitRefsPresenter.cs | 10 +++++ src/Views/Histories.axaml | 5 +++ src/Views/Histories.axaml.cs | 17 +++++--- src/Views/Preference.axaml | 6 ++- src/Views/Repository.axaml | 2 +- src/Views/WorkingCopy.axaml | 4 +- 15 files changed, 166 insertions(+), 65 deletions(-) diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 38436fe3..ea5d1f5b 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -35,6 +35,7 @@ namespace SourceGit.Models public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime; public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; + public bool IsWorkCopy => string.IsNullOrWhiteSpace(SHA); public double Opacity => IsMerged ? 1 : OpacityForNotMerged; public FontWeight FontWeight => IsCurrentHead ? FontWeight.Bold : FontWeight.Regular; diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index c5c66482..b845d642 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -101,6 +101,7 @@ namespace SourceGit.Models { public Point Center; public int Color; + public bool IsWorkCopy; } public List Paths { get; set; } = new List(); @@ -156,42 +157,45 @@ namespace SourceGit.Models // Find first curves that links to this commit and marks others that links to this commit ended. double offsetX = -HALF_WIDTH; - foreach (var l in unsolved) + if (!string.IsNullOrEmpty(commit.SHA)) { - if (l.Next == commit.SHA) + foreach (var l in unsolved) { - if (major == null) + if (l.Next == commit.SHA) { - offsetX += UNIT_WIDTH; - major = l; - - if (commit.Parents.Count > 0) + if (major == null) { - major.Next = commit.Parents[0]; - if (!mapUnsolved.ContainsKey(major.Next)) - mapUnsolved.Add(major.Next, major); + offsetX += UNIT_WIDTH; + major = l; + + if (commit.Parents.Count > 0) + { + major.Next = commit.Parents[0]; + if (!mapUnsolved.ContainsKey(major.Next)) + mapUnsolved.Add(major.Next, major); + } + else + { + major.Next = "ENDED"; + ended.Add(l); + } + + major.Add(offsetX, offsetY, HALF_HEIGHT); } else { - major.Next = "ENDED"; ended.Add(l); } - major.Add(offsetX, offsetY, HALF_HEIGHT); + isMerged = isMerged || l.IsMerged; } else { - ended.Add(l); + if (!mapUnsolved.ContainsKey(l.Next)) + mapUnsolved.Add(l.Next, l); + offsetX += UNIT_WIDTH; + l.Add(offsetX, offsetY, HALF_HEIGHT); } - - isMerged = isMerged || l.IsMerged; - } - else - { - if (!mapUnsolved.ContainsKey(l.Next)) - mapUnsolved.Add(l.Next, l); - offsetX += UNIT_WIDTH; - l.Add(offsetX, offsetY, HALF_HEIGHT); } } @@ -211,11 +215,11 @@ namespace SourceGit.Models { major.IsMerged = isMerged; position = new Point(major.LastX, offsetY); - temp.Dots.Add(new Dot() { Center = position, Color = major.Path.Color }); + temp.Dots.Add(new Dot() { Center = position, Color = major.Path.Color, IsWorkCopy = commit.IsWorkCopy }); } else { - temp.Dots.Add(new Dot() { Center = position, Color = 0 }); + temp.Dots.Add(new Dot() { Center = position, Color = 0, IsWorkCopy = commit.IsWorkCopy }); } // Deal with parents diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 2aa5eb5f..6ab4141d 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -286,6 +286,7 @@ SEARCH SHA/SUBJECT/AUTHOR. PRESS ENTER TO SEARCH, ESC TO QUIT CLEAR SELECTED {0} COMMITS + ★Uncommitted local changes★ Keyboard Shortcuts Reference GLOBAL Cancel current popup @@ -380,6 +381,7 @@ Default Clone Dir User Email Global git user email + Show uncommitted changes in history Install Path Shell User Name diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index ac777530..cf33efac 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -289,6 +289,7 @@ 查询提交指纹、信息、作者。回车键开始,ESC键取消 清空 已选中 {0} 项提交 + ★未提交的本地更改★ 快捷键参考 全局快捷键 取消弹出面板 @@ -383,6 +384,7 @@ 默认克隆路径 邮箱 默认GIT用户邮箱 + 顯示歷史中未提交的更改 安装路径 终端Shell 用户名 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index dad46505..1f697df2 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -289,6 +289,7 @@ 查詢提交指紋、資訊、作者。回車鍵開始,ESC鍵取消 清空 已選中 {0} 項提交 + ★未提交的本地更改★ 快捷鍵參考 全域性快捷鍵 取消彈出面板 @@ -383,6 +384,7 @@ 預設克隆路徑 郵箱 預設GIT使用者郵箱 + 显示历史记录中未提交的更改 安裝路徑 終端Shell 使用者名稱 diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 9ec3a897..53679c34 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -116,7 +116,14 @@ namespace SourceGit.ViewModels AutoSelectedCommit = commit; NavigationId = _navigationId + 1; - if (_detailContext is CommitDetail detail) + + if (commit.IsWorkCopy) + { + var wc = _repo.WorkingCopy ??= new WorkingCopy(_repo); + DetailContext = wc; + wc.RefreshWorkingCopyChangesAsync(); + } + else if (_detailContext is CommitDetail detail) { detail.Commit = commit; } diff --git a/src/ViewModels/Preference.cs b/src/ViewModels/Preference.cs index 4142bf39..dac04793 100644 --- a/src/ViewModels/Preference.cs +++ b/src/ViewModels/Preference.cs @@ -147,6 +147,12 @@ namespace SourceGit.ViewModels set => SetProperty(ref _maxHistoryCommits, value); } + public bool ShowUncommittedChangesInHistory + { + get => _showUncommittedChangesInHistory; + set => SetProperty(ref _showUncommittedChangesInHistory, value); + } + public int SubjectGuideLength { get => _subjectGuideLength; @@ -515,6 +521,7 @@ namespace SourceGit.ViewModels private LayoutInfo _layout = new LayoutInfo(); private int _maxHistoryCommits = 20000; + private bool _showUncommittedChangesInHistory = false; private int _subjectGuideLength = 50; private bool _restoreTabs = false; private bool _useFixedTabWidth = true; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 92160b2d..de9eee9b 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -716,6 +716,7 @@ namespace SourceGit.ViewModels Dispatcher.UIThread.Invoke(() => _histories.IsLoading = true); var limits = $"-{Preference.Instance.MaxHistoryCommits} "; + var showUncommitedChangedInHistory = Preference.Instance.ShowUncommittedChangesInHistory; var validFilters = new List(); foreach (var filter in _settings.Filters) { @@ -762,6 +763,36 @@ namespace SourceGit.ViewModels } var commits = new Commands.QueryCommits(_fullpath, limits).Result(); + if (showUncommitedChangedInHistory) + { + var changes = new Commands.QueryLocalChanges(_fullpath, _includeUntracked).Result(); + if(changes.Count > 0) + { + var config = new Commands.Config(_fullpath).ListAll(); + var currentUser = new Models.User(); + if (config.TryGetValue("user.name", out var name)) + currentUser.Name = name; + if (config.TryGetValue("user.email", out var email)) + currentUser.Email = email; + var date = (ulong)DateTime.Now.ToUniversalTime().Subtract(DateTime.UnixEpoch).TotalSeconds; + commits.Insert(0,new Models.Commit() + { + Subject = App.Text("Histories.UncommittedChanges.Subject"), + CanPullFromUpstream = false, + Author = currentUser, + Committer = currentUser, + Parents = [currentBranch.Head], + AuthorTime = date, + CommitterTime = date, + Decorators = [ + new Models.Decorator() + { + Type = Models.DecoratorType.CurrentBranchHead, + Name = currentBranch.FriendlyName, + }] + }); + } + } var graph = Models.CommitGraph.Parse(commits, canPushCommits, canPullCommits); Dispatcher.UIThread.Invoke(() => @@ -781,41 +812,15 @@ namespace SourceGit.ViewModels Dispatcher.UIThread.Invoke(() => Submodules = submodules); } - public void RefreshWorkingCopyChanges() + public async void RefreshWorkingCopyChanges() { - var changes = new Commands.QueryLocalChanges(_fullpath, _includeUntracked).Result(); if (_workingCopy == null) return; - var hasUnsolvedConflict = _workingCopy.SetData(changes); - var inProgress = null as InProgressContext; + var result = await _workingCopy.RefreshWorkingCopyChangesAsync(); - var rebaseMergeFolder = Path.Combine(_gitDir, "rebase-merge"); - var rebaseApplyFolder = Path.Combine(_gitDir, "rebase-apply"); - if (File.Exists(Path.Combine(_gitDir, "CHERRY_PICK_HEAD"))) - { - inProgress = new CherryPickInProgress(_fullpath); - } - else if (File.Exists(Path.Combine(_gitDir, "REBASE_HEAD")) && Directory.Exists(rebaseMergeFolder)) - { - inProgress = new RebaseInProgress(this); - } - else if (File.Exists(Path.Combine(_gitDir, "REVERT_HEAD"))) - { - inProgress = new RevertInProgress(_fullpath); - } - else if (File.Exists(Path.Combine(_gitDir, "MERGE_HEAD"))) - { - inProgress = new MergeInProgress(_fullpath); - } - else - { - if (Directory.Exists(rebaseMergeFolder)) - Directory.Delete(rebaseMergeFolder, true); - - if (Directory.Exists(rebaseApplyFolder)) - Directory.Delete(rebaseApplyFolder, true); - } + var hasUnsolvedConflict = result.HasUnsolvedConflict; + var inProgress = result.InProgressContext; Dispatcher.UIThread.Invoke(() => { @@ -1957,6 +1962,12 @@ namespace SourceGit.ViewModels } } + public WorkingCopy WorkingCopy + { + get => _workingCopy; + set => _workingCopy = value; + } + private string _fullpath = string.Empty; private string _gitDir = string.Empty; private Models.RepositorySettings _settings = null; diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 4a5c7c4e..4e9a2a25 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -1280,6 +1280,47 @@ namespace SourceGit.ViewModels }); } + public async Task<(InProgressContext InProgressContext, bool HasUnsolvedConflict)> RefreshWorkingCopyChangesAsync() + { + return await Task.Factory.StartNew(() => + { + var gitDir = _repo.GitDir; + var fullpath = _repo.FullPath; + var changes = new Commands.QueryLocalChanges(fullpath, _repo.IncludeUntracked).Result(); + var hasUnsolvedConflict = SetData(changes); + var inProgress = null as InProgressContext; + + var rebaseMergeFolder = Path.Combine(gitDir, "rebase-merge"); + var rebaseApplyFolder = Path.Combine(gitDir, "rebase-apply"); + if (File.Exists(Path.Combine(gitDir, "CHERRY_PICK_HEAD"))) + { + inProgress = new CherryPickInProgress(fullpath); + } + else if (File.Exists(Path.Combine(gitDir, "REBASE_HEAD")) && Directory.Exists(rebaseMergeFolder)) + { + inProgress = new RebaseInProgress(_repo); + } + else if (File.Exists(Path.Combine(gitDir, "REVERT_HEAD"))) + { + inProgress = new RevertInProgress(fullpath); + } + else if (File.Exists(Path.Combine(gitDir, "MERGE_HEAD"))) + { + inProgress = new MergeInProgress(fullpath); + } + else + { + if (Directory.Exists(rebaseMergeFolder)) + Directory.Delete(rebaseMergeFolder, true); + + if (Directory.Exists(rebaseApplyFolder)) + Directory.Delete(rebaseApplyFolder, true); + } + return (inProgress, hasUnsolvedConflict); + }); + } + + private Repository _repo = null; private bool _isLoadingData = false; private bool _isStaging = false; diff --git a/src/Views/CommitRefsPresenter.cs b/src/Views/CommitRefsPresenter.cs index 21e506e2..f58089be 100644 --- a/src/Views/CommitRefsPresenter.cs +++ b/src/Views/CommitRefsPresenter.cs @@ -15,6 +15,7 @@ namespace SourceGit.Views public Geometry Icon { get; set; } = null; public FormattedText Label { get; set; } = null; public bool IsTag { get; set; } = false; + public bool IsWorkCopy { get; internal set; } = false; } public static readonly StyledProperty FontFamilyProperty = @@ -117,6 +118,14 @@ namespace SourceGit.Views using (context.PushTransform(Matrix.CreateTranslation(x + 4, 4))) context.DrawGeometry(iconFG, null, item.Icon); + if (item.IsWorkCopy) + { + var workCopyBorderRect = new RoundedRect(new Rect(x, 0, 16 + item.Label.Width + 8, 16) + , new CornerRadius(2, 0, 0, 2)); + var workCopyBorderPen = new Pen(item.IsTag ? tagBG : branchBG, 2, new DashStyle() { Dashes = [1, 1], Offset = 1 }); + context.DrawRectangle(null, workCopyBorderPen, workCopyBorderRect); + } + x += item.Label.Width + 16 + 8 + 4; } } @@ -156,6 +165,7 @@ namespace SourceGit.Views { Label = label, IsTag = decorator.Type == Models.DecoratorType.Tag, + IsWorkCopy = commit.IsWorkCopy, }; var geo = null as StreamGeometry; diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index 63d4da95..19d843d0 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -189,6 +189,11 @@ + + + + + diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index f135cbf7..a2d64789 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -263,8 +263,12 @@ namespace SourceGit.Views continue; if (dot.Center.Y > bottom) break; - - context.DrawEllipse(dotFill, Models.CommitGraph.Pens[dot.Color], dot.Center, 3, 3); + var pen = Models.CommitGraph.Pens[dot.Color]; + if (dot.IsWorkCopy) + { + pen = new Pen(pen.Brush, pen.Thickness, s_UncommittedCahngesLineDashStyle); + } + context.DrawEllipse(dotFill, pen, dot.Center, 5, 5); } } @@ -282,7 +286,6 @@ namespace SourceGit.Views var geo = new StreamGeometry(); var pen = Models.CommitGraph.Pens[line.Color]; - using (var ctx = geo.Open()) { var started = false; @@ -351,10 +354,12 @@ namespace SourceGit.Views ctx.BeginFigure(link.Start, false); ctx.QuadraticBezierTo(link.Control, link.End); } - - context.DrawGeometry(null, Models.CommitGraph.Pens[link.Color], geo); + var pen = Models.CommitGraph.Pens[link.Color]; + context.DrawGeometry(null, pen, geo); } } + + private readonly static IDashStyle s_UncommittedCahngesLineDashStyle = new DashStyle() { Dashes = [1, 1], Offset = 1 }; } public partial class Histories : UserControl @@ -403,7 +408,7 @@ namespace SourceGit.Views private void OnCommitDataGridContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.Histories histories && sender is DataGrid datagrid) + if (DataContext is ViewModels.Histories histories && sender is DataGrid datagrid && datagrid.SelectedItem is Models.Commit { IsWorkCopy: false}) { var menu = histories.MakeContextMenu(datagrid); datagrid.OpenContextMenu(menu); diff --git a/src/Views/Preference.axaml b/src/Views/Preference.axaml index 31df2fab..e96164c6 100644 --- a/src/Views/Preference.axaml +++ b/src/Views/Preference.axaml @@ -244,7 +244,7 @@ - + + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 825bb235..2d23794d 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -87,7 +87,7 @@ - + diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index 4727ce20..1fe23252 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -118,7 +118,7 @@ - + @@ -153,7 +153,7 @@ - +