From 14db2d5cb053335ca0a93128389d78621a82e0e2 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 16 Mar 2026 11:52:48 +0800 Subject: [PATCH 01/30] feature: supports to disable `Mica` effect on Windows 11 from theme overrides (#2191) Add `UseMicaOnWindows11` to custom theme schema Signed-off-by: leo --- src/App.axaml.cs | 2 ++ src/Models/ThemeOverrides.cs | 1 + src/Native/OS.cs | 7 +++++++ src/Views/Launcher.axaml.cs | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/App.axaml.cs b/src/App.axaml.cs index a4c25193d..9ab107fb7 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -286,6 +286,8 @@ public static void SetTheme(string theme, string themeOverridesFile) else Models.CommitGraph.SetDefaultPens(overrides.GraphPenThickness); + Native.OS.UseMicaOnWindows11 = overrides.UseMicaOnWindows11; + app.Resources.MergedDictionaries.Add(resDic); app._themeOverrides = resDic; } diff --git a/src/Models/ThemeOverrides.cs b/src/Models/ThemeOverrides.cs index ccd9f57e8..531cbccdd 100644 --- a/src/Models/ThemeOverrides.cs +++ b/src/Models/ThemeOverrides.cs @@ -9,6 +9,7 @@ public class ThemeOverrides public Dictionary BasicColors { get; set; } = new Dictionary(); public double GraphPenThickness { get; set; } = 2; public double OpacityForNotMergedCommits { get; set; } = 0.5; + public bool UseMicaOnWindows11 { get; set; } = true; public List GraphColors { get; set; } = new List(); } } diff --git a/src/Native/OS.cs b/src/Native/OS.cs index 159656f67..1044887a2 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -107,6 +107,12 @@ public static string ExternalDiffArgs set; } = string.Empty; + public static bool UseMicaOnWindows11 + { + get => OperatingSystem.IsWindows() && OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) && _enableMicaOnWindows11; + set => _enableMicaOnWindows11 = value; + } + public static bool UseSystemWindowFrame { get => OperatingSystem.IsLinux() && _enableSystemWindowFrame; @@ -294,5 +300,6 @@ private static void UpdateGitVersion() private static IBackend _backend = null; private static string _gitExecutable = string.Empty; private static bool _enableSystemWindowFrame = false; + private static bool _enableMicaOnWindows11 = true; } } diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index a1cd3d927..985c60fb9 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -62,7 +62,7 @@ public Launcher() InitializeComponent(); PositionChanged += OnPositionChanged; - if (OperatingSystem.IsWindows() && OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) + if (Native.OS.UseMicaOnWindows11) { Background = Brushes.Transparent; TransparencyLevelHint = [WindowTransparencyLevel.Mica]; From 39360083bb6cd1da70af7e1315a56984874918e8 Mon Sep 17 00:00:00 2001 From: GEV <67133971+geviraydev@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:14:57 +0800 Subject: [PATCH 02/30] feature: add added/removed line counts to diff view (#2194) --- src/Commands/Diff.cs | 2 ++ src/Models/DiffResult.cs | 2 ++ src/ViewModels/TextDiffContext.cs | 2 ++ src/Views/DiffView.axaml | 24 +++++++++++++++++++++++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index 680aff63d..abb9fd423 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -194,6 +194,7 @@ private void ParseLine(string line) return; } + _result.TextDiff.DeletedLines++; _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0); _deleted.Add(_last); _oldLine++; @@ -207,6 +208,7 @@ private void ParseLine(string line) return; } + _result.TextDiff.AddedLines++; _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine); _added.Add(_last); _newLine++; diff --git a/src/Models/DiffResult.cs b/src/Models/DiffResult.cs index df8f204c0..32fff76ce 100644 --- a/src/Models/DiffResult.cs +++ b/src/Models/DiffResult.cs @@ -55,6 +55,8 @@ public partial class TextDiff { public List Lines { get; set; } = new List(); public int MaxLineNumber = 0; + public int AddedLines { get; set; } = 0; + public int DeletedLines { get; set; } = 0; public TextDiffSelection MakeSelection(int startLine, int endLine, bool isCombined, bool isOldSide) { diff --git a/src/ViewModels/TextDiffContext.cs b/src/ViewModels/TextDiffContext.cs index fd1be431b..1804bfd43 100644 --- a/src/ViewModels/TextDiffContext.cs +++ b/src/ViewModels/TextDiffContext.cs @@ -28,6 +28,8 @@ public class TextDiffContext : ObservableObject { public Models.DiffOption Option => _option; public Models.TextDiff Data => _data; + public int AddedLines => _data?.AddedLines ?? 0; + public int DeletedLines => _data?.DeletedLines ?? 0; public Vector ScrollOffset { diff --git a/src/Views/DiffView.axaml b/src/Views/DiffView.axaml index f734a983a..3697ea10e 100644 --- a/src/Views/DiffView.axaml +++ b/src/Views/DiffView.axaml @@ -67,7 +67,29 @@ - + + + + + + + + + From bf5e12325faa6eb70c65ecbf1ebaa4f8344edcf1 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 16 Mar 2026 13:32:34 +0800 Subject: [PATCH 03/30] code_review: PR #2194 Re-design the diff view toolbar Signed-off-by: leo --- src/ViewModels/TextDiffContext.cs | 2 -- src/Views/DiffView.axaml | 53 ++++++++++++++++--------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/ViewModels/TextDiffContext.cs b/src/ViewModels/TextDiffContext.cs index 1804bfd43..fd1be431b 100644 --- a/src/ViewModels/TextDiffContext.cs +++ b/src/ViewModels/TextDiffContext.cs @@ -28,8 +28,6 @@ public class TextDiffContext : ObservableObject { public Models.DiffOption Option => _option; public Models.TextDiff Data => _data; - public int AddedLines => _data?.AddedLines ?? 0; - public int DeletedLines => _data?.DeletedLines ?? 0; public Vector ScrollOffset { diff --git a/src/Views/DiffView.axaml b/src/Views/DiffView.axaml index 3697ea10e..3c52a7d57 100644 --- a/src/Views/DiffView.axaml +++ b/src/Views/DiffView.axaml @@ -13,7 +13,7 @@ - + @@ -28,12 +28,35 @@ ToolTip.Tip="{DynamicResource Text.Diff.FileModeChanged}"> + + + + + + + + + + + + - + - + - diff --git a/src/Views/RepositoryToolbar.axaml b/src/Views/RepositoryToolbar.axaml index f857ffee6..72ebb9dc5 100644 --- a/src/Views/RepositoryToolbar.axaml +++ b/src/Views/RepositoryToolbar.axaml @@ -80,11 +80,7 @@ VerticalAlignment="Center" Fill="{DynamicResource Brush.Border2}"/> - - - From 130e4f72ed6138697d68419cec24d492e7392dac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Mar 2026 07:52:32 +0000 Subject: [PATCH 05/30] doc: Update translation status and sort locale files --- TRANSLATION.md | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 994f52a3e..fd4125995 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,7 +6,7 @@ This document shows the translation status of each locale file in the repository ### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen) -### ![de__DE](https://img.shields.io/badge/de__DE-98.46%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-98.35%25-yellow)
Missing keys in de_DE.axaml @@ -18,6 +18,7 @@ This document shows the translation status of each locale file in the repository - Text.CommitMessageTextBox.Column - Text.ConfirmEmptyCommit.StageSelectedThenCommit - Text.GotoRevisionSelector +- Text.Hotkeys.Repo.CreateBranch - Text.Hotkeys.Repo.GoToChild - Text.Init.CommandTip - Text.Init.ErrorMessageTip @@ -29,16 +30,17 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-99.90%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.79%25-yellow)
Missing keys in es_ES.axaml +- Text.Hotkeys.Repo.CreateBranch - Text.Preferences.General.Use24Hours
-### ![fr__FR](https://img.shields.io/badge/fr__FR-92.28%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-92.18%25-yellow)
Missing keys in fr_FR.axaml @@ -70,6 +72,7 @@ This document shows the translation status of each locale file in the repository - Text.Histories.ShowColumns - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.Zoom +- Text.Hotkeys.Repo.CreateBranch - Text.Hotkeys.Repo.GoToChild - Text.Hotkeys.Repo.GoToParent - Text.Init.CommandTip @@ -121,7 +124,7 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-90.22%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-90.12%25-yellow)
Missing keys in id_ID.axaml @@ -166,6 +169,7 @@ This document shows the translation status of each locale file in the repository - Text.Histories.ShowColumns - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.Zoom +- Text.Hotkeys.Repo.CreateBranch - Text.Hotkeys.Repo.GoToChild - Text.Hotkeys.Repo.GoToParent - Text.Hotkeys.Repo.OpenCommandPalette @@ -224,7 +228,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-97.84%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-97.74%25-yellow)
Missing keys in it_IT.axaml @@ -239,6 +243,7 @@ This document shows the translation status of each locale file in the repository - Text.GotoRevisionSelector - Text.Histories.Header.DateTime - Text.Histories.ShowColumns +- Text.Hotkeys.Repo.CreateBranch - Text.Hotkeys.Repo.GoToChild - Text.Hotkeys.Repo.GoToParent - Text.Init.CommandTip @@ -253,7 +258,7 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-98.87%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-98.77%25-yellow)
Missing keys in ja_JP.axaml @@ -263,6 +268,7 @@ This document shows the translation status of each locale file in the repository - Text.CommandPalette.RepositoryActions - Text.CommandPalette.RevisionFiles - Text.ConfirmEmptyCommit.StageSelectedThenCommit +- Text.Hotkeys.Repo.CreateBranch - Text.Init.CommandTip - Text.Init.ErrorMessageTip - Text.Preferences.General.Use24Hours @@ -272,7 +278,7 @@ This document shows the translation status of each locale file in the repository
-### ![ko__KR](https://img.shields.io/badge/ko__KR-90.53%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-90.43%25-yellow)
Missing keys in ko_KR.axaml @@ -312,6 +318,7 @@ This document shows the translation status of each locale file in the repository - Text.Histories.ShowColumns - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.Zoom +- Text.Hotkeys.Repo.CreateBranch - Text.Hotkeys.Repo.GoToChild - Text.Hotkeys.Repo.GoToParent - Text.Hotkeys.Repo.OpenCommandPalette @@ -372,7 +379,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-68.49%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-68.42%25-red)
Missing keys in pt_BR.axaml @@ -509,6 +516,7 @@ This document shows the translation status of each locale file in the repository - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab - Text.Hotkeys.Global.Zoom +- Text.Hotkeys.Repo.CreateBranch - Text.Hotkeys.Repo.GoToChild - Text.Hotkeys.Repo.GoToParent - Text.Hotkeys.Repo.OpenCommandPalette @@ -686,7 +694,7 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-99.18%25-yellow) +### ![ru__RU](https://img.shields.io/badge/ru__RU-99.07%25-yellow)
Missing keys in ru_RU.axaml @@ -696,13 +704,14 @@ This document shows the translation status of each locale file in the repository - Text.CommandPalette.RepositoryActions - Text.CommandPalette.RevisionFiles - Text.ConfirmEmptyCommit.StageSelectedThenCommit +- Text.Hotkeys.Repo.CreateBranch - Text.Init.CommandTip - Text.Init.ErrorMessageTip - Text.Preferences.General.Use24Hours
-### ![ta__IN](https://img.shields.io/badge/ta__IN-70.75%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-70.68%25-red)
Missing keys in ta_IN.axaml @@ -850,6 +859,7 @@ This document shows the translation status of each locale file in the repository - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab - Text.Hotkeys.Global.Zoom +- Text.Hotkeys.Repo.CreateBranch - Text.Hotkeys.Repo.GoToChild - Text.Hotkeys.Repo.GoToParent - Text.Hotkeys.Repo.OpenCommandPalette @@ -994,7 +1004,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-71.58%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-71.50%25-red)
Missing keys in uk_UA.axaml @@ -1138,6 +1148,7 @@ This document shows the translation status of each locale file in the repository - Text.Hotkeys.Global.ShowWorkspaceDropdownMenu - Text.Hotkeys.Global.SwitchTab - Text.Hotkeys.Global.Zoom +- Text.Hotkeys.Repo.CreateBranch - Text.Hotkeys.Repo.GoToChild - Text.Hotkeys.Repo.GoToParent - Text.Hotkeys.Repo.OpenCommandPalette From 1a3a366f29eed27a80c18d726a4790d57d6fde9c Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 16 Mar 2026 17:18:55 +0800 Subject: [PATCH 06/30] feature: supports git SHA-256 object hash Signed-off-by: leo --- src/App.axaml.cs | 2 +- src/Commands/Diff.cs | 2 +- src/Commands/IsBinary.cs | 4 ++-- src/Commands/QueryCommitChildren.cs | 2 +- src/Commands/QueryStagedChangesWithAmend.cs | 4 ++-- src/Models/Commit.cs | 3 +-- src/Models/DiffOption.cs | 12 +++++------- src/Models/EmptyTreeHash.cs | 13 +++++++++++++ src/Models/Stash.cs | 1 + src/ViewModels/CommitDetail.cs | 14 +++++++++----- src/ViewModels/StashesPage.cs | 14 +++++++------- src/ViewModels/WorkingCopy.cs | 2 +- src/Views/StashSubjectPresenter.cs | 2 +- 13 files changed, 45 insertions(+), 30 deletions(-) create mode 100644 src/Models/EmptyTreeHash.cs diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 9ab107fb7..e986f4e43 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -806,7 +806,7 @@ private string FixFontFamilyName(string input) return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty; } - [GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,40})(\s+.*)?$")] + [GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,64})(\s+.*)?$")] private static partial Regex REG_REBASE_TODO(); private Models.IpcChannel _ipcChannel = null; diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index abb9fd423..4d0cc72ac 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -12,7 +12,7 @@ public partial class Diff : Command [GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")] private static partial Regex REG_INDICATOR(); - [GeneratedRegex(@"^index\s([0-9a-f]{6,40})\.\.([0-9a-f]{6,40})(\s[1-9]{6})?")] + [GeneratedRegex(@"^index\s([0-9a-f]{6,64})\.\.([0-9a-f]{6,64})(\s[1-9]{6})?")] private static partial Regex REG_HASH_CHANGE(); private const string PREFIX_LFS_NEW = "+version https://git-lfs.github.com/spec/"; diff --git a/src/Commands/IsBinary.cs b/src/Commands/IsBinary.cs index 087e71c7b..9dbe05459 100644 --- a/src/Commands/IsBinary.cs +++ b/src/Commands/IsBinary.cs @@ -8,11 +8,11 @@ public partial class IsBinary : Command [GeneratedRegex(@"^\-\s+\-\s+.*$")] private static partial Regex REG_TEST(); - public IsBinary(string repo, string commit, string path) + public IsBinary(string repo, string revision, string path) { WorkingDirectory = repo; Context = repo; - Args = $"diff --no-color --no-ext-diff --numstat {Models.Commit.EmptyTreeSHA1} {commit} -- {path.Quoted()}"; + Args = $"diff --no-color --no-ext-diff --numstat {Models.EmptyTreeHash.Guess(revision)} {revision} -- {path.Quoted()}"; RaiseError = false; } diff --git a/src/Commands/QueryCommitChildren.cs b/src/Commands/QueryCommitChildren.cs index 6af0abb73..7e7e88877 100644 --- a/src/Commands/QueryCommitChildren.cs +++ b/src/Commands/QueryCommitChildren.cs @@ -24,7 +24,7 @@ public async Task> GetResultAsync() foreach (var line in lines) { if (line.Contains(_commit)) - outs.Add(line.Substring(0, 40)); + outs.Add(line.Substring(0, _commit.Length)); } } diff --git a/src/Commands/QueryStagedChangesWithAmend.cs b/src/Commands/QueryStagedChangesWithAmend.cs index cff939e2e..78109ce6b 100644 --- a/src/Commands/QueryStagedChangesWithAmend.cs +++ b/src/Commands/QueryStagedChangesWithAmend.cs @@ -6,9 +6,9 @@ namespace SourceGit.Commands { public partial class QueryStagedChangesWithAmend : Command { - [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([ADMT])\d{0,6}\t(.*)$")] + [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{4,64}) [0-9a-f]{4,64} ([ADMT])\d{0,6}\t(.*)$")] private static partial Regex REG_FORMAT1(); - [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{40}) [0-9a-f]{40} ([RC])\d{0,6}\t(.*\t.*)$")] + [GeneratedRegex(@"^:[\d]{6} ([\d]{6}) ([0-9a-f]{4,64}) [0-9a-f]{4,64} ([RC])\d{0,6}\t(.*\t.*)$")] private static partial Regex REG_FORMAT2(); public QueryStagedChangesWithAmend(string repo, string parent) diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 60501ba4a..7f55e31f8 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -15,8 +15,6 @@ public enum CommitSearchMethod public class Commit { - public const string EmptyTreeSHA1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; - public string SHA { get; set; } = string.Empty; public User Author { get; set; } = User.Invalid; public ulong AuthorTime { get; set; } = 0; @@ -33,6 +31,7 @@ public class Commit 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 HasDecorators => Decorators.Count > 0; + public string FirstParentToCompare => Parents.Count > 0 ? $"{SHA}^" : EmptyTreeHash.Guess(SHA); public string GetFriendlyName() { diff --git a/src/Models/DiffOption.cs b/src/Models/DiffOption.cs index 0dc8bc31f..def59bedd 100644 --- a/src/Models/DiffOption.cs +++ b/src/Models/DiffOption.cs @@ -60,8 +60,7 @@ public DiffOption(Change change, bool isUnstaged) /// public DiffOption(Commit commit, Change change) { - var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^"; - _revisions.Add(baseRevision); + _revisions.Add(commit.FirstParentToCompare); _revisions.Add(commit.SHA); _path = change.Path; _orgPath = change.OriginalPath; @@ -74,8 +73,7 @@ public DiffOption(Commit commit, Change change) /// public DiffOption(Commit commit, string file) { - var baseRevision = commit.Parents.Count == 0 ? Commit.EmptyTreeSHA1 : $"{commit.SHA}^"; - _revisions.Add(baseRevision); + _revisions.Add(commit.FirstParentToCompare); _revisions.Add(commit.SHA); _path = file; } @@ -88,7 +86,7 @@ public DiffOption(FileVersion ver) { if (string.IsNullOrEmpty(ver.OriginalPath)) { - _revisions.Add(ver.HasParent ? $"{ver.SHA}^" : Commit.EmptyTreeSHA1); + _revisions.Add(ver.HasParent ? $"{ver.SHA}^" : EmptyTreeHash.Guess(ver.SHA)); _revisions.Add(ver.SHA); _path = ver.Path; } @@ -111,14 +109,14 @@ public DiffOption(FileVersion start, FileVersion end) { if (start.Change.Index == ChangeState.Deleted) { - _revisions.Add(Commit.EmptyTreeSHA1); + _revisions.Add(EmptyTreeHash.Guess(end.SHA)); _revisions.Add(end.SHA); _path = end.Path; } else if (end.Change.Index == ChangeState.Deleted) { _revisions.Add(start.SHA); - _revisions.Add(Commit.EmptyTreeSHA1); + _revisions.Add(EmptyTreeHash.Guess(start.SHA)); _path = start.Path; } else if (!end.Path.Equals(start.Path, StringComparison.Ordinal)) diff --git a/src/Models/EmptyTreeHash.cs b/src/Models/EmptyTreeHash.cs new file mode 100644 index 000000000..bf1445a0f --- /dev/null +++ b/src/Models/EmptyTreeHash.cs @@ -0,0 +1,13 @@ +namespace SourceGit.Models +{ + public static class EmptyTreeHash + { + public static string Guess(string revision) + { + return revision.Length == 40 ? SHA1 : SHA256; + } + + private const string SHA1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; + private const string SHA256 = "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321"; + } +} diff --git a/src/Models/Stash.cs b/src/Models/Stash.cs index 4f5ad6922..93439a40a 100644 --- a/src/Models/Stash.cs +++ b/src/Models/Stash.cs @@ -10,5 +10,6 @@ public class Stash public ulong Time { get; set; } = 0; public string Message { get; set; } = ""; public string Subject => Message.Split('\n', 2)[0].Trim(); + public string UntrackedParent => EmptyTreeHash.Guess(SHA); } } diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 8cffb0ae9..6ccfba0ae 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -240,8 +240,13 @@ public async Task SaveChangesAsPatchAsync(List changes, string sa if (_commit == null) return; - var baseRevision = _commit.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : _commit.Parents[0]; - var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo.FullPath, changes, baseRevision, _commit.SHA, saveTo); + var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync( + _repo.FullPath, + changes, + _commit.FirstParentToCompare, + _commit.SHA, + saveTo); + if (succ) App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess")); } @@ -535,8 +540,7 @@ private void Refresh() Task.Run(async () => { - var parent = _commit.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : $"{_commit.SHA}^"; - var cmd = new Commands.CompareRevisions(_repo.FullPath, parent, _commit.SHA) { CancellationToken = token }; + var cmd = new Commands.CompareRevisions(_repo.FullPath, _commit.FirstParentToCompare, _commit.SHA) { CancellationToken = token }; var changes = await cmd.ReadAsync().ConfigureAwait(false); var visible = changes; if (!string.IsNullOrWhiteSpace(_searchChangeFilter)) @@ -757,7 +761,7 @@ private async Task SetViewingCommitAsync(Models.Object file) [GeneratedRegex(@"\b(https?://|ftp://)[\w\d\._/\-~%@()+:?&=#!]*[\w\d/]")] private static partial Regex REG_URL_FORMAT(); - [GeneratedRegex(@"\b([0-9a-fA-F]{6,40})\b")] + [GeneratedRegex(@"\b([0-9a-fA-F]{6,64})\b")] private static partial Regex REG_SHA_FORMAT(); [GeneratedRegex(@"`.*?`")] diff --git a/src/ViewModels/StashesPage.cs b/src/ViewModels/StashesPage.cs index a2a47c274..a484b0225 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -63,7 +63,7 @@ public Models.Stash SelectedStash var untracked = new List(); if (value.Parents.Count == 3) { - untracked = await new Commands.CompareRevisions(_repo.FullPath, Models.Commit.EmptyTreeSHA1, value.Parents[2]) + untracked = await new Commands.CompareRevisions(_repo.FullPath, value.UntrackedParent, value.Parents[2]) .ReadAsync() .ConfigureAwait(false); @@ -107,7 +107,7 @@ public List SelectedChanges if (value is not { Count: 1 }) DiffContext = null; else if (_untracked.Contains(value[0])) - DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(Models.Commit.EmptyTreeSHA1, _selectedStash.Parents[2], value[0]), _diffContext); + DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_selectedStash.UntrackedParent, _selectedStash.Parents[2], value[0]), _diffContext); else DiffContext = new DiffContext(_repo.FullPath, new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, value[0]), _diffContext); } @@ -167,16 +167,16 @@ public async Task SaveStashAsPatchAsync(Models.Stash stash, string saveTo) .ConfigureAwait(false); foreach (var c in changes) - opts.Add(new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, c)); + opts.Add(new Models.DiffOption(stash.Parents[0], stash.SHA, c)); if (stash.Parents.Count == 3) { - var untracked = await new Commands.CompareRevisions(_repo.FullPath, Models.Commit.EmptyTreeSHA1, stash.Parents[2]) + var untracked = await new Commands.CompareRevisions(_repo.FullPath, stash.UntrackedParent, stash.Parents[2]) .ReadAsync() .ConfigureAwait(false); foreach (var c in untracked) - opts.Add(new Models.DiffOption(Models.Commit.EmptyTreeSHA1, _selectedStash.Parents[2], c)); + opts.Add(new Models.DiffOption(stash.UntrackedParent, stash.Parents[2], c)); changes.AddRange(untracked); } @@ -190,7 +190,7 @@ public void OpenChangeWithExternalDiffTool(Models.Change change) { Models.DiffOption opt; if (_untracked.Contains(change)) - opt = new Models.DiffOption(Models.Commit.EmptyTreeSHA1, _selectedStash.Parents[2], change); + opt = new Models.DiffOption(_selectedStash.UntrackedParent, _selectedStash.Parents[2], change); else opt = new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, change); @@ -242,7 +242,7 @@ public async Task ApplySelectedChanges(List changes) foreach (var c in changes) { if (_untracked.Contains(c) && _selectedStash.Parents.Count == 3) - opts.Add(new Models.DiffOption(Models.Commit.EmptyTreeSHA1, _selectedStash.Parents[2], c)); + opts.Add(new Models.DiffOption(_selectedStash.UntrackedParent, _selectedStash.Parents[2], c)); else opts.Add(new Models.DiffOption(_selectedStash.Parents[0], _selectedStash.SHA, c)); } diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 6b68a22dd..f716a8d4b 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -739,7 +739,7 @@ public async Task CommitAsync(bool autoStage, bool autoPush) if (_useAmend) { var head = new Commands.QuerySingleCommit(_repo.FullPath, "HEAD").GetResult(); - return new Commands.QueryStagedChangesWithAmend(_repo.FullPath, head.Parents.Count == 0 ? Models.Commit.EmptyTreeSHA1 : $"{head.SHA}^").GetResult(); + return new Commands.QueryStagedChangesWithAmend(_repo.FullPath, head.FirstParentToCompare).GetResult(); } var rs = new List(); diff --git a/src/Views/StashSubjectPresenter.cs b/src/Views/StashSubjectPresenter.cs index 8f47a350e..e9c1bac91 100644 --- a/src/Views/StashSubjectPresenter.cs +++ b/src/Views/StashSubjectPresenter.cs @@ -127,7 +127,7 @@ protected override Size MeasureOverride(Size availableSize) [GeneratedRegex(@"^On ([^\s]+)\: ")] private static partial Regex REG_KEYWORD_ON(); - [GeneratedRegex(@"^WIP on ([^\s]+)\: ([a-f0-9]{6,40}) ")] + [GeneratedRegex(@"^WIP on ([^\s]+)\: ([a-f0-9]{6,64}) ")] private static partial Regex REG_KEYWORD_WIP(); } } From 54ea6cd71ba427faa9373236c720ce2768dfeb5e Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 16 Mar 2026 17:44:27 +0800 Subject: [PATCH 07/30] code_style: move some codes from `App` to `Models.InteractiveRebaseJobCollection` Signed-off-by: leo --- src/App.axaml.cs | 45 ++-------------------------- src/Models/InteractiveRebase.cs | 52 +++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/src/App.axaml.cs b/src/App.axaml.cs index e986f4e43..f9c6117cc 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Text; using System.Text.Json; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -488,23 +487,7 @@ private static bool TryLaunchAsRebaseTodoEditor(string[] args, out int exitCode) using var stream = File.OpenRead(jobsFile); var collection = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.InteractiveRebaseJobCollection); - using var writer = new StreamWriter(file); - foreach (var job in collection.Jobs) - { - var code = job.Action switch - { - Models.InteractiveRebaseAction.Pick => 'p', - Models.InteractiveRebaseAction.Edit => 'e', - Models.InteractiveRebaseAction.Reword => 'r', - Models.InteractiveRebaseAction.Squash => 's', - Models.InteractiveRebaseAction.Fixup => 'f', - _ => 'd' - }; - writer.WriteLine($"{code} {job.SHA}"); - } - - writer.Flush(); - + collection.WriteTodoList(file); exitCode = 0; return true; } @@ -535,27 +518,8 @@ private static bool TryLaunchAsRebaseMessageEditor(string[] args, out int exitCo var onto = File.ReadAllText(ontoFile).Trim(); using var stream = File.OpenRead(jobsFile); var collection = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.InteractiveRebaseJobCollection); - if (!collection.Onto.Equals(onto) || !collection.OrigHead.Equals(origHead)) - return true; - - var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - if (done.Length == 0) - return true; - - var current = done[^1].Trim(); - var match = REG_REBASE_TODO().Match(current); - if (!match.Success) - return true; - - var sha = match.Groups[1].Value; - foreach (var job in collection.Jobs) - { - if (job.SHA.StartsWith(sha)) - { - File.WriteAllText(file, job.Message); - break; - } - } + if (collection.Onto.StartsWith(onto, StringComparison.OrdinalIgnoreCase) && collection.OrigHead.StartsWith(origHead, StringComparison.OrdinalIgnoreCase)) + collection.WriteCommitMessage(doneFile, file); return true; } @@ -806,9 +770,6 @@ private string FixFontFamilyName(string input) return trimmed.Count > 0 ? string.Join(',', trimmed) : string.Empty; } - [GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,64})(\s+.*)?$")] - private static partial Regex REG_REBASE_TODO(); - private Models.IpcChannel _ipcChannel = null; private ViewModels.Launcher _launcher = null; private ResourceDictionary _activeLocale = null; diff --git a/src/Models/InteractiveRebase.cs b/src/Models/InteractiveRebase.cs index bae99ac52..ac7e29d4f 100644 --- a/src/Models/InteractiveRebase.cs +++ b/src/Models/InteractiveRebase.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; namespace SourceGit.Models { @@ -34,10 +37,55 @@ public class InteractiveRebaseJob public string Message { get; set; } = string.Empty; } - public class InteractiveRebaseJobCollection + public partial class InteractiveRebaseJobCollection { public string OrigHead { get; set; } = string.Empty; public string Onto { get; set; } = string.Empty; public List Jobs { get; set; } = new List(); + + public void WriteTodoList(string todoFile) + { + using var writer = new StreamWriter(todoFile); + foreach (var job in Jobs) + { + var code = job.Action switch + { + InteractiveRebaseAction.Pick => 'p', + InteractiveRebaseAction.Edit => 'e', + InteractiveRebaseAction.Reword => 'r', + InteractiveRebaseAction.Squash => 's', + InteractiveRebaseAction.Fixup => 'f', + _ => 'd' + }; + writer.WriteLine($"{code} {job.SHA}"); + } + + writer.Flush(); + } + + public void WriteCommitMessage(string doneFile, string msgFile) + { + var done = File.ReadAllText(doneFile).Trim().Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + if (done.Length == 0) + return; + + var current = done[^1].Trim(); + var match = REG_REBASE_TODO().Match(current); + if (!match.Success) + return; + + var sha = match.Groups[1].Value; + foreach (var job in Jobs) + { + if (job.SHA.StartsWith(sha)) + { + File.WriteAllText(msgFile, job.Message); + return; + } + } + } + + [GeneratedRegex(@"^[a-z]+\s+([a-fA-F0-9]{4,64})(\s+.*)?$")] + private static partial Regex REG_REBASE_TODO(); } } From 9d63078becb734e9131ff330915b53b462ff96d9 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 16 Mar 2026 18:39:03 +0800 Subject: [PATCH 08/30] enhance: manually update local branch tree after checking out a exiting local branch (#2169) Since the remote branch tree did not changed after checkout, and the changes in the local branch tree are completely predictable, update it manually. Signed-off-by: leo --- src/ViewModels/Checkout.cs | 31 ++++++++++++++--------------- src/ViewModels/Repository.cs | 38 +++++++++++++++++++++++++++++++++++- src/Views/Checkout.axaml | 2 +- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index ee760331f..8da89a138 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -4,9 +4,9 @@ namespace SourceGit.ViewModels { public class Checkout : Popup { - public string Branch + public string BranchName { - get; + get => _branch.Name; } public bool DiscardLocalChanges @@ -15,10 +15,10 @@ public bool DiscardLocalChanges set; } - public Checkout(Repository repo, string branch) + public Checkout(Repository repo, Models.Branch branch) { _repo = repo; - Branch = branch; + _branch = branch; DiscardLocalChanges = false; } @@ -30,9 +30,10 @@ public override bool CanStartDirectly() public override async Task Sure() { using var lockWatcher = _repo.LockWatcher(); - ProgressDescription = $"Checkout '{Branch}' ..."; + var branchName = BranchName; + ProgressDescription = $"Checkout '{branchName}' ..."; - var log = _repo.CreateLog($"Checkout '{Branch}'"); + var log = _repo.CreateLog($"Checkout '{branchName}'"); Use(log); if (_repo.CurrentBranch is { IsDetachedHead: true }) @@ -70,7 +71,7 @@ public override async Task Sure() succ = await new Commands.Checkout(_repo.FullPath) .Use(log) - .BranchAsync(Branch, DiscardLocalChanges); + .BranchAsync(branchName, DiscardLocalChanges); if (succ) { @@ -80,21 +81,19 @@ public override async Task Sure() await new Commands.Stash(_repo.FullPath) .Use(log) .PopAsync("stash@{0}"); + + _repo.FastRefreshBranchesAfterCheckout(_branch); + } + else + { + _repo.MarkWorkingCopyDirtyManually(); } log.Complete(); - - var b = _repo.Branches.Find(x => x.IsLocal && x.Name == Branch); - if (b != null && _repo.HistoryFilterMode == Models.FilterMode.Included) - _repo.SetBranchFilterMode(b, Models.FilterMode.Included, false, false); - - _repo.MarkBranchesDirtyManually(); - - ProgressDescription = "Waiting for branch updated..."; - await Task.Delay(400); return succ; } private readonly Repository _repo = null; + private readonly Models.Branch _branch = null; } } diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 7123a2e0c..6fa078a9d 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -788,6 +788,42 @@ public IDisposable LockWatcher() return _watcher?.Lock(); } + public void FastRefreshBranchesAfterCheckout(Models.Branch checkouted) + { + _watcher?.MarkBranchUpdated(); + _watcher?.MarkWorkingCopyUpdated(); + + if (_currentBranch.IsDetachedHead) + { + _branches.Remove(_currentBranch); + } + else + { + _currentBranch.IsCurrent = false; + _currentBranch.WorktreePath = null; + } + + checkouted.IsCurrent = true; + checkouted.WorktreePath = FullPath; + if (_historyFilterMode == Models.FilterMode.Included) + SetBranchFilterMode(checkouted, Models.FilterMode.Included, false, false); + + List locals = []; + foreach (var b in _branches) + { + if (b.IsLocal) + locals.Add(b); + } + + var builder = BuildBranchTree(locals, []); + LocalBranchTrees = builder.Locals; + CurrentBranch = checkouted; + + RefreshCommits(); + RefreshWorkingCopyChanges(); + RefreshWorktrees(); + } + public void MarkBranchesDirtyManually() { _watcher?.MarkBranchUpdated(); @@ -1304,7 +1340,7 @@ public async Task CheckoutBranchAsync(Models.Branch branch) if (branch.IsLocal) { - await ShowAndStartPopupAsync(new Checkout(this, branch.Name)); + await ShowAndStartPopupAsync(new Checkout(this, branch)); } else { diff --git a/src/Views/Checkout.axaml b/src/Views/Checkout.axaml index 0d2f2ecae..26fafe815 100644 --- a/src/Views/Checkout.axaml +++ b/src/Views/Checkout.axaml @@ -30,7 +30,7 @@ Text="{DynamicResource Text.Checkout.Target}"/> - + Date: Mon, 16 Mar 2026 19:41:47 +0800 Subject: [PATCH 09/30] feature: only create a new `FileHistoriesSingleRevision` instance if it is necessary after selecting a single item in `File History` (#2192) Signed-off-by: leo --- src/ViewModels/FileHistories.cs | 88 +++++++++++++++++++++----------- src/Views/FileHistories.axaml | 5 +- src/Views/FileHistories.axaml.cs | 30 +++++++++++ 3 files changed, 92 insertions(+), 31 deletions(-) diff --git a/src/ViewModels/FileHistories.cs b/src/ViewModels/FileHistories.cs index 722e450f4..17d1fc05d 100644 --- a/src/ViewModels/FileHistories.cs +++ b/src/ViewModels/FileHistories.cs @@ -2,10 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; - -using Avalonia.Collections; using Avalonia.Threading; - using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -17,15 +14,27 @@ public class FileHistoriesRevisionFile(string path, object content = null, bool public bool CanOpenWithDefaultEditor { get; set; } = canOpenWithDefaultEditor; } + public class FileHistoriesSingleRevisionViewMode + { + public bool IsDiff + { + get; + set; + } = true; + } + public class FileHistoriesSingleRevision : ObservableObject { public bool IsDiffMode { - get => _isDiffMode; + get => _viewMode.IsDiff; set { - if (SetProperty(ref _isDiffMode, value)) + if (_viewMode.IsDiff != value) + { + _viewMode.IsDiff = value; RefreshViewContent(); + } } } @@ -35,17 +44,24 @@ public object ViewContent set => SetProperty(ref _viewContent, value); } - public FileHistoriesSingleRevision(string repo, Models.FileVersion revision, bool prevIsDiffMode) + public FileHistoriesSingleRevision(string repo, Models.FileVersion revision, FileHistoriesSingleRevisionViewMode viewMode) { _repo = repo; _file = revision.Path; _revision = revision; - _isDiffMode = prevIsDiffMode; + _viewMode = viewMode; _viewContent = null; RefreshViewContent(); } + public void SetRevision(Models.FileVersion revision) + { + _file = revision.Path; + _revision = revision; + RefreshViewContent(); + } + public async Task ResetToSelectedRevisionAsync() { return await new Commands.Checkout(_repo) @@ -72,7 +88,7 @@ await Commands.SaveRevisionFile private void RefreshViewContent() { - if (_isDiffMode) + if (_viewMode.IsDiff) { ViewContent = new DiffContext(_repo, new(_revision), _viewContent as DiffContext); return; @@ -155,7 +171,7 @@ private async Task GetRevisionFileContentAsync(Models.Object obj) private string _repo = null; private string _file = null; private Models.FileVersion _revision = null; - private bool _isDiffMode = false; + private FileHistoriesSingleRevisionViewMode _viewMode = null; private object _viewContent = null; } @@ -226,11 +242,15 @@ public List Revisions set => SetProperty(ref _revisions, value); } - public AvaloniaList SelectedRevisions + public List SelectedRevisions { - get; - set; - } = []; + get => _selectedRevisions; + set + { + if (SetProperty(ref _selectedRevisions, value)) + RefreshViewContent(); + } + } public object ViewContent { @@ -257,23 +277,8 @@ public FileHistories(string repo, string file, string commit = null) { IsLoading = false; Revisions = revisions; - if (revisions.Count > 0) - SelectedRevisions.Add(revisions[0]); }); }); - - SelectedRevisions.CollectionChanged += (_, _) => - { - if (_viewContent is FileHistoriesSingleRevision singleRevision) - _prevIsDiffMode = singleRevision.IsDiffMode; - - ViewContent = SelectedRevisions.Count switch - { - 1 => new FileHistoriesSingleRevision(_repo, SelectedRevisions[0], _prevIsDiffMode), - 2 => new FileHistoriesCompareRevisions(_repo, SelectedRevisions[0], SelectedRevisions[1]), - _ => SelectedRevisions.Count, - }; - }; } public void NavigateToCommit(Models.FileVersion revision) @@ -303,10 +308,35 @@ public string GetCommitFullMessage(Models.FileVersion revision) return msg; } + private void RefreshViewContent() + { + var count = _selectedRevisions?.Count ?? 0; + if (count == 0) + { + ViewContent = null; + } + else if (count == 1) + { + if (_viewContent is FileHistoriesSingleRevision single) + single.SetRevision(_selectedRevisions[0]); + else + ViewContent = new FileHistoriesSingleRevision(_repo, _selectedRevisions[0], _viewMode); + } + else if (count == 2) + { + ViewContent = new FileHistoriesCompareRevisions(_repo, _selectedRevisions[0], _selectedRevisions[1]); + } + else + { + ViewContent = _selectedRevisions.Count; + } + } + private readonly string _repo = null; private bool _isLoading = true; - private bool _prevIsDiffMode = true; + private FileHistoriesSingleRevisionViewMode _viewMode = new(); private List _revisions = null; + private List _selectedRevisions = []; private Dictionary _fullCommitMessages = new(); private object _viewContent = null; } diff --git a/src/Views/FileHistories.axaml b/src/Views/FileHistories.axaml index f929b0a68..8b21e197f 100644 --- a/src/Views/FileHistories.axaml +++ b/src/Views/FileHistories.axaml @@ -59,9 +59,10 @@ BorderThickness="1" Margin="8,4,4,8" BorderBrush="{DynamicResource Brush.Border2}" - ItemsSource="{Binding Revisions}" - SelectedItems="{Binding SelectedRevisions, Mode=TwoWay}" + ItemsSource="{Binding Revisions, Mode=OneWay}" SelectionMode="Multiple" + SelectionChanged="OnRevisionsSelectionChanged" + PropertyChanged="OnRevisionsPropertyChanged" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto"> diff --git a/src/Views/FileHistories.axaml.cs b/src/Views/FileHistories.axaml.cs index 749b48627..252543274 100644 --- a/src/Views/FileHistories.axaml.cs +++ b/src/Views/FileHistories.axaml.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -14,6 +16,34 @@ public FileHistories() InitializeComponent(); } + private void OnRevisionsPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == ListBox.ItemsSourceProperty && + sender is ListBox { Items: { Count: > 0 } } listBox) + listBox.SelectedIndex = 0; + } + + private void OnRevisionsSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is ListBox listBox && DataContext is ViewModels.FileHistories vm) + { + if (listBox.SelectedItems is { } selected) + { + var revs = new List(); + foreach (var item in listBox.SelectedItems) + { + if (item is Models.FileVersion ver) + revs.Add(ver); + } + vm.SelectedRevisions = revs; + } + else + { + vm.SelectedRevisions = []; + } + } + } + private void OnPressCommitSHA(object sender, PointerPressedEventArgs e) { if (sender is TextBlock { DataContext: Models.FileVersion ver } && From c47f819ac2d968ef26c774c572698ddfdcffb27c Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 17 Mar 2026 10:35:14 +0800 Subject: [PATCH 10/30] enhance: support remote url with whitespaces (#2195) Signed-off-by: leo --- src/Commands/Clone.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/Clone.cs b/src/Commands/Clone.cs index b0323528c..ffd11bda9 100644 --- a/src/Commands/Clone.cs +++ b/src/Commands/Clone.cs @@ -14,7 +14,7 @@ public Clone(string ctx, string path, string url, string localName, string sshKe builder.Append("clone --progress --verbose "); if (!string.IsNullOrEmpty(extraArgs)) builder.Append(extraArgs).Append(' '); - builder.Append(url).Append(' '); + builder.Append(url.Quoted()).Append(' '); if (!string.IsNullOrEmpty(localName)) builder.Append(localName.Quoted()); From 5fdd4e3189500f9bca1a91756ecec932c068c3b0 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 17 Mar 2026 10:49:43 +0800 Subject: [PATCH 11/30] enhance: manually update local branch tree after checking out and fast-forward a exiting local branch (#2169) Signed-off-by: leo --- src/ViewModels/CheckoutAndFastForward.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/ViewModels/CheckoutAndFastForward.cs b/src/ViewModels/CheckoutAndFastForward.cs index 197701c33..1b2ef3818 100644 --- a/src/ViewModels/CheckoutAndFastForward.cs +++ b/src/ViewModels/CheckoutAndFastForward.cs @@ -80,17 +80,19 @@ public override async Task Sure() await new Commands.Stash(_repo.FullPath) .Use(log) .PopAsync("stash@{0}"); - } - - log.Complete(); - if (_repo.HistoryFilterMode == Models.FilterMode.Included) - _repo.SetBranchFilterMode(LocalBranch, Models.FilterMode.Included, false, false); + LocalBranch.Behind.Clear(); + LocalBranch.Head = RemoteBranch.Head; + LocalBranch.CommitterDate = RemoteBranch.CommitterDate; - _repo.MarkBranchesDirtyManually(); + _repo.FastRefreshBranchesAfterCheckout(LocalBranch); + } + else + { + _repo.MarkWorkingCopyDirtyManually(); + } - ProgressDescription = "Waiting for branch updated..."; - await Task.Delay(400); + log.Complete(); return succ; } From 11158d098599048f45646f9731d60cff7c0928c0 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 17 Mar 2026 11:10:47 +0800 Subject: [PATCH 12/30] enhance: manually update local branch tree after renaming a local branch (#2169) Signed-off-by: leo --- src/ViewModels/RenameBranch.cs | 10 +---- src/ViewModels/Repository.cs | 24 ++++++++++++ src/Views/BranchTree.axaml.cs | 68 ++++++++++++++++++---------------- 3 files changed, 61 insertions(+), 41 deletions(-) diff --git a/src/ViewModels/RenameBranch.cs b/src/ViewModels/RenameBranch.cs index e15a96b90..4a9235d4e 100644 --- a/src/ViewModels/RenameBranch.cs +++ b/src/ViewModels/RenameBranch.cs @@ -60,17 +60,9 @@ public override async Task Sure() .RenameAsync(_name); if (succ) - _repo.UIStates.RenameBranchFilter(Target.FullName, _name); + _repo.FastRefreshBranchesAfterRenaming(Target, _name); log.Complete(); - _repo.MarkBranchesDirtyManually(); - - if (isCurrent) - { - ProgressDescription = "Waiting for branch updated..."; - await Task.Delay(400); - } - return succ; } diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 6fa078a9d..dacafd556 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -824,6 +824,30 @@ public void FastRefreshBranchesAfterCheckout(Models.Branch checkouted) RefreshWorktrees(); } + public void FastRefreshBranchesAfterRenaming(Models.Branch b, string newName) + { + _watcher?.MarkBranchUpdated(); + + var newFullName = $"refs/heads/{newName}"; + _uiStates.RenameBranchFilter(b.FullName, newFullName); + + b.Name = newName; + b.FullName = newFullName; + + List locals = []; + foreach (var branch in _branches) + { + if (branch.IsLocal) + locals.Add(branch); + } + + var builder = BuildBranchTree(locals, []); + LocalBranchTrees = builder.Locals; + + RefreshCommits(); + RefreshWorktrees(); + } + public void MarkBranchesDirtyManually() { _watcher?.MarkBranchUpdated(); diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index 651133402..367e54240 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -887,37 +887,45 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, } } - var rename = new MenuItem(); - rename.Header = App.Text("BranchCM.Rename", branch.Name); - rename.Icon = App.CreateMenuIcon("Icons.Rename"); - rename.Click += (_, e) => + if (!branch.IsDetachedHead) { - if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.RenameBranch(repo, branch)); - e.Handled = true; - }; + var editDescription = new MenuItem(); + editDescription.Header = App.Text("BranchCM.EditDescription", branch.Name); + editDescription.Icon = App.CreateMenuIcon("Icons.Edit"); + editDescription.Click += async (_, e) => + { + var desc = await new Commands.Config(repo.FullPath).GetAsync($"branch.{branch.Name}.description"); + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.EditBranchDescription(repo, branch, desc)); + e.Handled = true; + }; - var editDescription = new MenuItem(); - editDescription.Header = App.Text("BranchCM.EditDescription", branch.Name); - editDescription.Icon = App.CreateMenuIcon("Icons.Edit"); - editDescription.Click += async (_, e) => - { - var desc = await new Commands.Config(repo.FullPath).GetAsync($"branch.{branch.Name}.description"); - if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.EditBranchDescription(repo, branch, desc)); - e.Handled = true; - }; + var rename = new MenuItem(); + rename.Header = App.Text("BranchCM.Rename", branch.Name); + rename.Icon = App.CreateMenuIcon("Icons.Rename"); + rename.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.RenameBranch(repo, branch)); + e.Handled = true; + }; - var delete = new MenuItem(); - delete.Header = App.Text("BranchCM.Delete", branch.Name); - delete.Icon = App.CreateMenuIcon("Icons.Clear"); - delete.IsEnabled = !branch.IsCurrent; - delete.Click += (_, e) => - { - if (repo.CanCreatePopup()) - repo.ShowPopup(new ViewModels.DeleteBranch(repo, branch)); - e.Handled = true; - }; + var delete = new MenuItem(); + delete.Header = App.Text("BranchCM.Delete", branch.Name); + delete.Icon = App.CreateMenuIcon("Icons.Clear"); + delete.IsEnabled = !branch.IsCurrent; + delete.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.DeleteBranch(repo, branch)); + e.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(editDescription); + menu.Items.Add(rename); + menu.Items.Add(delete); + } var createBranch = new MenuItem(); createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); @@ -939,10 +947,6 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo, e.Handled = true; }; - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(editDescription); - menu.Items.Add(rename); - menu.Items.Add(delete); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(createBranch); menu.Items.Add(createTag); From 418d4a3827898563dd5a0c7014b3a56e460e1d6b Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 17 Mar 2026 12:12:25 +0800 Subject: [PATCH 13/30] enhance: manually update local branch tree after creating a new local branch (#2169) Signed-off-by: leo --- src/ViewModels/Checkout.cs | 2 +- src/ViewModels/CheckoutAndFastForward.cs | 2 +- src/ViewModels/CreateBranch.cs | 51 +++++++++++++----------- src/ViewModels/RenameBranch.cs | 2 +- src/ViewModels/Repository.cs | 51 +++++++++++++++++++++++- 5 files changed, 80 insertions(+), 28 deletions(-) diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index 8da89a138..71a324c67 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -82,7 +82,7 @@ public override async Task Sure() .Use(log) .PopAsync("stash@{0}"); - _repo.FastRefreshBranchesAfterCheckout(_branch); + _repo.RefreshAfterCheckoutBranch(_branch); } else { diff --git a/src/ViewModels/CheckoutAndFastForward.cs b/src/ViewModels/CheckoutAndFastForward.cs index 1b2ef3818..120d6c4ad 100644 --- a/src/ViewModels/CheckoutAndFastForward.cs +++ b/src/ViewModels/CheckoutAndFastForward.cs @@ -85,7 +85,7 @@ public override async Task Sure() LocalBranch.Head = RemoteBranch.Head; LocalBranch.CommitterDate = RemoteBranch.CommitterDate; - _repo.FastRefreshBranchesAfterCheckout(LocalBranch); + _repo.RefreshAfterCheckoutBranch(LocalBranch); } else { diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index b5d4d7125..832f56268 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -58,6 +58,8 @@ public CreateBranch(Repository repo, Models.Branch branch) { _repo = repo; _baseOnRevision = branch.Head; + _committerDate = branch.CommitterDate; + _head = branch.Head; if (!branch.IsLocal) Name = branch.Name; @@ -70,6 +72,8 @@ public CreateBranch(Repository repo, Models.Commit commit) { _repo = repo; _baseOnRevision = commit.SHA; + _committerDate = commit.CommitterTime; + _head = commit.SHA; BasedOn = commit; DiscardLocalChanges = false; @@ -79,6 +83,8 @@ public CreateBranch(Repository repo, Models.Tag tag) { _repo = repo; _baseOnRevision = tag.SHA; + _committerDate = tag.CreatorDate; + _head = tag.SHA; BasedOn = tag; DiscardLocalChanges = false; @@ -125,6 +131,15 @@ public override async Task Sure() } } + Models.Branch created = new() + { + Name = _name, + FullName = $"refs/heads/{_name}", + CommitterDate = _committerDate, + Head = _head, + IsLocal = true, + }; + bool succ; if (CheckoutAfterCreated && !_repo.IsBare) { @@ -168,43 +183,33 @@ public override async Task Sure() .CreateAsync(_baseOnRevision, _allowOverwrite); } - if (succ && BasedOn is Models.Branch { IsLocal: false } basedOn && _name.Equals(basedOn.Name, StringComparison.Ordinal)) + if (succ) { - await new Commands.Branch(_repo.FullPath, _name) + if (BasedOn is Models.Branch { IsLocal: false } basedOn && _name.Equals(basedOn.Name, StringComparison.Ordinal)) + { + await new Commands.Branch(_repo.FullPath, _name) .Use(log) .SetUpstreamAsync(basedOn); - } - - log.Complete(); - - if (succ && CheckoutAfterCreated) - { - var fake = new Models.Branch() { IsLocal = true, FullName = $"refs/heads/{_name}" }; - if (BasedOn is Models.Branch { IsLocal: false } based) - fake.Upstream = based.FullName; - var folderEndIdx = fake.FullName.LastIndexOf('/'); - if (folderEndIdx > 10) - _repo.UIStates.ExpandedBranchNodesInSideBar.Add(fake.FullName.Substring(0, folderEndIdx)); + created.Upstream = basedOn.FullName; + } - if (_repo.HistoryFilterMode == Models.FilterMode.Included) - _repo.SetBranchFilterMode(fake, Models.FilterMode.Included, false, false); + _repo.RefreshAfterCreateBranch(created, CheckoutAfterCreated); } - - _repo.MarkBranchesDirtyManually(); - - if (CheckoutAfterCreated) + else { - ProgressDescription = "Waiting for branch updated..."; - await Task.Delay(400); + _repo.MarkWorkingCopyDirtyManually(); } + log.Complete(); return true; } private readonly Repository _repo = null; - private string _name = null; private readonly string _baseOnRevision = null; + private readonly ulong _committerDate = 0; + private readonly string _head = string.Empty; + private string _name = null; private bool _allowOverwrite = false; } } diff --git a/src/ViewModels/RenameBranch.cs b/src/ViewModels/RenameBranch.cs index 4a9235d4e..dbca651e7 100644 --- a/src/ViewModels/RenameBranch.cs +++ b/src/ViewModels/RenameBranch.cs @@ -60,7 +60,7 @@ public override async Task Sure() .RenameAsync(_name); if (succ) - _repo.FastRefreshBranchesAfterRenaming(Target, _name); + _repo.RefreshAfterRenameBranch(Target, _name); log.Complete(); return succ; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index dacafd556..bbd25f17e 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -788,7 +788,54 @@ public IDisposable LockWatcher() return _watcher?.Lock(); } - public void FastRefreshBranchesAfterCheckout(Models.Branch checkouted) + public void RefreshAfterCreateBranch(Models.Branch created, bool checkout) + { + _watcher?.MarkBranchUpdated(); + _watcher?.MarkWorkingCopyUpdated(); + + _branches.Add(created); + + if (checkout) + { + if (_currentBranch.IsDetachedHead) + { + _branches.Remove(_currentBranch); + } + else + { + _currentBranch.IsCurrent = false; + _currentBranch.WorktreePath = null; + } + + created.IsCurrent = true; + created.WorktreePath = FullPath; + + var folderEndIdx = created.FullName.LastIndexOf('/'); + if (folderEndIdx > 10) + _uiStates.ExpandedBranchNodesInSideBar.Add(created.FullName.Substring(0, folderEndIdx)); + + if (_historyFilterMode == Models.FilterMode.Included) + SetBranchFilterMode(created, Models.FilterMode.Included, false, false); + + CurrentBranch = created; + } + + List locals = []; + foreach (var b in _branches) + { + if (b.IsLocal) + locals.Add(b); + } + + var builder = BuildBranchTree(locals, []); + LocalBranchTrees = builder.Locals; + + RefreshCommits(); + RefreshWorkingCopyChanges(); + RefreshWorktrees(); + } + + public void RefreshAfterCheckoutBranch(Models.Branch checkouted) { _watcher?.MarkBranchUpdated(); _watcher?.MarkWorkingCopyUpdated(); @@ -824,7 +871,7 @@ public void FastRefreshBranchesAfterCheckout(Models.Branch checkouted) RefreshWorktrees(); } - public void FastRefreshBranchesAfterRenaming(Models.Branch b, string newName) + public void RefreshAfterRenameBranch(Models.Branch b, string newName) { _watcher?.MarkBranchUpdated(); From 5cdb8364a914613e3d8d5b1e3865fcb04f508e94 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 17 Mar 2026 13:50:18 +0800 Subject: [PATCH 14/30] enhance: run checking out automatically when all the local changes are new files Signed-off-by: leo --- src/ViewModels/Checkout.cs | 5 ----- src/ViewModels/Repository.cs | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index 71a324c67..aca5cb1ef 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -22,11 +22,6 @@ public Checkout(Repository repo, Models.Branch branch) DiscardLocalChanges = false; } - public override bool CanStartDirectly() - { - return _repo.LocalChangesCount == 0; - } - public override async Task Sure() { using var lockWatcher = _repo.LockWatcher(); diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index bbd25f17e..bf31a3d56 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1332,12 +1332,23 @@ public void RefreshWorkingCopyChanges() changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path)); _workingCopy.SetData(changes, token); + var hasModified = false; + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Added || c.WorkTree == Models.ChangeState.Untracked) + continue; + + hasModified = true; + break; + } + Dispatcher.UIThread.Invoke(() => { if (token.IsCancellationRequested) return; LocalChangesCount = changes.Count; + _canCheckoutDirectly = !hasModified; OnPropertyChanged(nameof(InProgressContext)); GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasLocalChanges, changes.Count == 0); }); @@ -1411,7 +1422,10 @@ public async Task CheckoutBranchAsync(Models.Branch branch) if (branch.IsLocal) { - await ShowAndStartPopupAsync(new Checkout(this, branch)); + if (_canCheckoutDirectly) + await ShowAndStartPopupAsync(new Checkout(this, branch)); + else + ShowPopup(new Checkout(this, branch)); } else { @@ -1965,6 +1979,7 @@ private async Task AutoFetchOnUIThread() private List _submodules = []; private object _visibleSubmodules = null; private string _navigateToCommitDelayed = string.Empty; + private bool _canCheckoutDirectly = false; private bool _isAutoFetching = false; private Timer _autoFetchTimer = null; From a04a64d05d61360539510f91b2318e97cb152bcd Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 17 Mar 2026 16:57:51 +0800 Subject: [PATCH 15/30] code_style: remove unused code Signed-off-by: leo --- src/Models/Remote.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index 1ef697052..ce1e6cbd6 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -97,7 +97,6 @@ public bool TryGetCreatePullRequestURL(out string url, string mergeBranch) var uri = new Uri(baseURL); var host = uri.Host; - var route = uri.AbsolutePath.TrimStart('/'); var encodedBranch = HttpUtility.UrlEncode(mergeBranch); if (host.Contains("github.com", StringComparison.Ordinal)) From 668439a17db33a6c84136908fe56614ab52b6d89 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 17 Mar 2026 17:06:30 +0800 Subject: [PATCH 16/30] fix: cancel prev searching request before starting a new one (#2197) Signed-off-by: leo --- src/ViewModels/SearchCommitContext.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/ViewModels/SearchCommitContext.cs b/src/ViewModels/SearchCommitContext.cs index 374336e5e..da4514e91 100644 --- a/src/ViewModels/SearchCommitContext.cs +++ b/src/ViewModels/SearchCommitContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -105,6 +106,12 @@ public void StartSearch() IsQuerying = true; + if (_cancellation is { IsCancellationRequested: false }) + _cancellation.Cancel(); + + _cancellation = new(); + var token = _cancellation.Token; + Task.Run(async () => { var result = new List(); @@ -158,17 +165,23 @@ public void StartSearch() Dispatcher.UIThread.Post(() => { - IsQuerying = false; + if (token.IsCancellationRequested) + return; + IsQuerying = false; if (_repo.IsSearchingCommits) Results = result; }); - }); + }, token); } public void EndSearch() { + if (_cancellation is { IsCancellationRequested: false }) + _cancellation.Cancel(); + _worktreeFiles = null; + IsQuerying = false; Suggestions = null; Results = null; GC.Collect(); @@ -228,6 +241,7 @@ private void UpdateSuggestions() } private Repository _repo = null; + private CancellationTokenSource _cancellation = null; private int _method = (int)Models.CommitSearchMethod.ByMessage; private string _filter = string.Empty; private bool _onlySearchCurrentBranch = false; From 6f16791f514480715e1f5c4562fe09443bb50795 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 17 Mar 2026 18:23:39 +0800 Subject: [PATCH 17/30] feature: supports to switch between `HTTPS` and `SSH` protocol (#1734) Signed-off-by: leo --- src/Views/AddRemote.axaml | 28 +++++- src/Views/Clone.axaml | 7 +- src/Views/EditRemote.axaml | 29 +++++- src/Views/RemoteProtocolSwitcher.axaml | 20 +++++ src/Views/RemoteProtocolSwitcher.axaml.cs | 102 ++++++++++++++++++++++ 5 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 src/Views/RemoteProtocolSwitcher.axaml create mode 100644 src/Views/RemoteProtocolSwitcher.axaml.cs diff --git a/src/Views/AddRemote.axaml b/src/Views/AddRemote.axaml index eb8d8b9f2..2b1b49574 100644 --- a/src/Views/AddRemote.axaml +++ b/src/Views/AddRemote.axaml @@ -29,7 +29,14 @@ VerticalAlignment="Center" CornerRadius="2" Watermark="{DynamicResource Text.Remote.Name.Placeholder}" - Text="{Binding Name, Mode=TwoWay}"/> + Text="{Binding Name, Mode=TwoWay}"> + + + + + Text="{Binding Url, Mode=TwoWay}"> + + + + + + + + + + + + diff --git a/src/Views/RemoteProtocolSwitcher.axaml.cs b/src/Views/RemoteProtocolSwitcher.axaml.cs new file mode 100644 index 000000000..c496ea877 --- /dev/null +++ b/src/Views/RemoteProtocolSwitcher.axaml.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace SourceGit.Views +{ + public partial class RemoteProtocolSwitcher : UserControl + { + public static readonly StyledProperty UrlProperty = + AvaloniaProperty.Register(nameof(Url)); + + public string Url + { + get => GetValue(UrlProperty); + set => SetValue(UrlProperty, value); + } + + public static readonly StyledProperty ActiveProtocolProperty = + AvaloniaProperty.Register(nameof(ActiveProtocol)); + + public string ActiveProtocol + { + get => GetValue(ActiveProtocolProperty); + set => SetValue(ActiveProtocolProperty, value); + } + + public RemoteProtocolSwitcher() + { + InitializeComponent(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == UrlProperty) + { + _protocols.Clear(); + + var url = Url ?? string.Empty; + if (url.StartsWith("https://", StringComparison.Ordinal) && Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + var host = uri.Host; + var serverName = uri.Port == 443 ? host : $"{host}:{uri.Port}"; + var route = uri.AbsolutePath.TrimStart('/'); + + _protocols.Add(url); + _protocols.Add($"git@{serverName}:{route}"); + + SetCurrentValue(ActiveProtocolProperty, "HTTPS"); + SetCurrentValue(IsVisibleProperty, true); + return; + } + + var match = REG_SSH_FORMAT().Match(url); + if (match.Success) + { + var host = match.Groups[1].Value; + var repo = match.Groups[2].Value; + + _protocols.Add($"https://{host}/{repo}"); + _protocols.Add(url); + + SetCurrentValue(ActiveProtocolProperty, "SSH"); + SetCurrentValue(IsVisibleProperty, true); + return; + } + + SetCurrentValue(IsVisibleProperty, false); + } + } + + private void OnOpenDropdownMenu(object sender, RoutedEventArgs e) + { + if (sender is Button btn && _protocols.Count > 0) + { + var menu = new ContextMenu(); + menu.Placement = PlacementMode.BottomEdgeAlignedLeft; + + foreach (var protocol in _protocols) + { + var dup = protocol; + var item = new MenuItem() { Header = dup }; + item.Click += (_, _) => Url = protocol; + menu.Items.Add(item); + } + + menu.Open(btn); + } + + e.Handled = true; + } + + [GeneratedRegex(@"^git@([\w\.\-]+):(.+)$")] + private static partial Regex REG_SSH_FORMAT(); + private List _protocols = []; + } +} From 16a66d4dff90a66c08ee18b404ca65ceca06ce5c Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 17 Mar 2026 19:24:01 +0800 Subject: [PATCH 18/30] refactor: do not handle port of remote URL since we can not know if the ssh port changed by a changed https port Signed-off-by: leo --- src/Models/Remote.cs | 9 ++------- src/Views/RemoteProtocolSwitcher.axaml.cs | 3 +-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index ce1e6cbd6..18b57c414 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -64,14 +64,9 @@ public bool TryGetVisitURL(out string url) { url = null; - if (URL.StartsWith("http", StringComparison.Ordinal)) + if (URL.StartsWith("http://", StringComparison.Ordinal) || URL.StartsWith("https://", StringComparison.Ordinal)) { - var uri = new Uri(URL.EndsWith(".git", StringComparison.Ordinal) ? URL.Substring(0, URL.Length - 4) : URL); - if (uri.Port != 80 && uri.Port != 443) - url = $"{uri.Scheme}://{uri.Host}:{uri.Port}{uri.LocalPath}"; - else - url = $"{uri.Scheme}://{uri.Host}{uri.LocalPath}"; - + url = URL.EndsWith(".git", StringComparison.Ordinal) ? URL.Substring(0, URL.Length - 4) : URL; return true; } diff --git a/src/Views/RemoteProtocolSwitcher.axaml.cs b/src/Views/RemoteProtocolSwitcher.axaml.cs index c496ea877..343dbc133 100644 --- a/src/Views/RemoteProtocolSwitcher.axaml.cs +++ b/src/Views/RemoteProtocolSwitcher.axaml.cs @@ -45,11 +45,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (url.StartsWith("https://", StringComparison.Ordinal) && Uri.TryCreate(url, UriKind.Absolute, out var uri)) { var host = uri.Host; - var serverName = uri.Port == 443 ? host : $"{host}:{uri.Port}"; var route = uri.AbsolutePath.TrimStart('/'); _protocols.Add(url); - _protocols.Add($"git@{serverName}:{route}"); + _protocols.Add($"git@{host}:{route}"); SetCurrentValue(ActiveProtocolProperty, "HTTPS"); SetCurrentValue(IsVisibleProperty, true); From 71e34ecb4dcf25e1453a49c8183f9e51f37fb9be Mon Sep 17 00:00:00 2001 From: CrabNickolson Date: Wed, 18 Mar 2026 03:18:32 +0100 Subject: [PATCH 19/30] feature: allow enabling 3-way merge when applying a patch (#2200) --- src/Commands/Apply.cs | 5 ++++- src/Resources/Locales/en_US.axaml | 1 + src/ViewModels/Apply.cs | 9 ++++++++- src/ViewModels/StashesPage.cs | 2 +- src/Views/Apply.axaml | 7 ++++++- src/Views/TextDiffView.axaml.cs | 6 +++--- 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Commands/Apply.cs b/src/Commands/Apply.cs index ca6ffe8d9..544cf4d98 100644 --- a/src/Commands/Apply.cs +++ b/src/Commands/Apply.cs @@ -4,7 +4,7 @@ namespace SourceGit.Commands { public class Apply : Command { - public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode, string extra) + public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode, bool threeWayMerge, string extra) { WorkingDirectory = repo; Context = repo; @@ -17,6 +17,9 @@ public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceM else builder.Append("--whitespace=").Append(whitespaceMode).Append(' '); + if (threeWayMerge) + builder.Append("--3way "); + if (!string.IsNullOrEmpty(extra)) builder.Append(extra).Append(' '); diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 994a8804a..f71f46f19 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -27,6 +27,7 @@ Patch File: Select .patch file to apply Ignore whitespace changes + 3-Way Merge Apply Patch Whitespace: Apply Stash diff --git a/src/ViewModels/Apply.cs b/src/ViewModels/Apply.cs index 3eab5ef7a..817d56210 100644 --- a/src/ViewModels/Apply.cs +++ b/src/ViewModels/Apply.cs @@ -26,6 +26,12 @@ public Models.ApplyWhiteSpaceMode SelectedWhiteSpaceMode set; } + public bool ThreeWayMerge + { + get => _threeWayMerge; + set => SetProperty(ref _threeWayMerge, value); + } + public Apply(Repository repo) { _repo = repo; @@ -49,7 +55,7 @@ public override async Task Sure() var log = _repo.CreateLog("Apply Patch"); Use(log); - var succ = await new Commands.Apply(_repo.FullPath, _patchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg, null) + var succ = await new Commands.Apply(_repo.FullPath, _patchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg, _threeWayMerge, null) .Use(log) .ExecAsync(); @@ -60,5 +66,6 @@ public override async Task Sure() private readonly Repository _repo = null; private string _patchFile = string.Empty; private bool _ignoreWhiteSpace = true; + private bool _threeWayMerge = false; } } diff --git a/src/ViewModels/StashesPage.cs b/src/ViewModels/StashesPage.cs index a484b0225..d1834d636 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -253,7 +253,7 @@ public async Task ApplySelectedChanges(List changes) return; var log = _repo.CreateLog($"Apply changes from '{_selectedStash.Name}'"); - await new Commands.Apply(_repo.FullPath, saveTo, true, string.Empty, string.Empty) + await new Commands.Apply(_repo.FullPath, saveTo, true, string.Empty, false, string.Empty) .Use(log) .ExecAsync(); diff --git a/src/Views/Apply.axaml b/src/Views/Apply.axaml index d1265a5a8..d64972dee 100644 --- a/src/Views/Apply.axaml +++ b/src/Views/Apply.axaml @@ -18,7 +18,7 @@ Text="{DynamicResource Text.Apply.Title}"/> - + + + diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 2f844b683..81322c723 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -1508,7 +1508,7 @@ private async void OnStageChunk(object _1, RoutedEventArgs _2) diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, false, chunk.IsOldSide, tmpFile); } - await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", "--cache --index").ExecAsync(); + await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", false, "--cache --index").ExecAsync(); File.Delete(tmpFile); vm.BlockNavigation.UpdateByChunk(chunk); @@ -1539,7 +1539,7 @@ private async void OnUnstageChunk(object _1, RoutedEventArgs _2) else diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, chunk.IsOldSide, tmpFile); - await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", "--cache --index --reverse").ExecAsync(); + await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", false, "--cache --index --reverse").ExecAsync(); File.Delete(tmpFile); vm.BlockNavigation.UpdateByChunk(chunk); @@ -1577,7 +1577,7 @@ private async void OnDiscardChunk(object _1, RoutedEventArgs _2) diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, chunk.IsOldSide, tmpFile); } - await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", "--reverse").ExecAsync(); + await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", false, "--reverse").ExecAsync(); File.Delete(tmpFile); vm.BlockNavigation.UpdateByChunk(chunk); From 987f4c0ab5923e19f17b216987876feb0ae74204 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 18 Mar 2026 10:26:59 +0800 Subject: [PATCH 20/30] code_review: PR #2200 - Use `string extra` parameter instead of adding a new `bool threeWayMerge` in `Commands.Apply`. - Add missing translations for Chinese Signed-off-by: leo --- src/Commands/Apply.cs | 5 +---- src/Resources/Locales/en_US.axaml | 2 +- src/Resources/Locales/zh_CN.axaml | 1 + src/Resources/Locales/zh_TW.axaml | 1 + src/ViewModels/Apply.cs | 8 ++++---- src/ViewModels/StashesPage.cs | 2 +- src/Views/TextDiffView.axaml.cs | 6 +++--- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Commands/Apply.cs b/src/Commands/Apply.cs index 544cf4d98..ca6ffe8d9 100644 --- a/src/Commands/Apply.cs +++ b/src/Commands/Apply.cs @@ -4,7 +4,7 @@ namespace SourceGit.Commands { public class Apply : Command { - public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode, bool threeWayMerge, string extra) + public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode, string extra) { WorkingDirectory = repo; Context = repo; @@ -17,9 +17,6 @@ public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceM else builder.Append("--whitespace=").Append(whitespaceMode).Append(' '); - if (threeWayMerge) - builder.Append("--3way "); - if (!string.IsNullOrEmpty(extra)) builder.Append(extra).Append(' '); diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index f71f46f19..3514fbef6 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -24,10 +24,10 @@ Hide SourceGit Show All Patch + 3-Way Merge Patch File: Select .patch file to apply Ignore whitespace changes - 3-Way Merge Apply Patch Whitespace: Apply Stash diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 235cfe164..3f1b8c55b 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -28,6 +28,7 @@ 隐藏 SourceGit 显示所有窗口 应用补丁(apply) + 尝试三路合并 补丁文件 : 选择补丁文件 忽略空白符号 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 9f1545c1a..fa4e3d2bd 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -28,6 +28,7 @@ 隱藏 SourceGit 顯示所有 套用修補檔 (apply patch) + 嘗試三路合併 修補檔: 選擇修補檔 忽略空白符號 diff --git a/src/ViewModels/Apply.cs b/src/ViewModels/Apply.cs index 817d56210..3578c12f2 100644 --- a/src/ViewModels/Apply.cs +++ b/src/ViewModels/Apply.cs @@ -28,8 +28,8 @@ public Models.ApplyWhiteSpaceMode SelectedWhiteSpaceMode public bool ThreeWayMerge { - get => _threeWayMerge; - set => SetProperty(ref _threeWayMerge, value); + get; + set; } public Apply(Repository repo) @@ -55,7 +55,8 @@ public override async Task Sure() var log = _repo.CreateLog("Apply Patch"); Use(log); - var succ = await new Commands.Apply(_repo.FullPath, _patchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg, _threeWayMerge, null) + var extra = ThreeWayMerge ? "--3way" : string.Empty; + var succ = await new Commands.Apply(_repo.FullPath, _patchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg, extra) .Use(log) .ExecAsync(); @@ -66,6 +67,5 @@ public override async Task Sure() private readonly Repository _repo = null; private string _patchFile = string.Empty; private bool _ignoreWhiteSpace = true; - private bool _threeWayMerge = false; } } diff --git a/src/ViewModels/StashesPage.cs b/src/ViewModels/StashesPage.cs index d1834d636..a484b0225 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -253,7 +253,7 @@ public async Task ApplySelectedChanges(List changes) return; var log = _repo.CreateLog($"Apply changes from '{_selectedStash.Name}'"); - await new Commands.Apply(_repo.FullPath, saveTo, true, string.Empty, false, string.Empty) + await new Commands.Apply(_repo.FullPath, saveTo, true, string.Empty, string.Empty) .Use(log) .ExecAsync(); diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 81322c723..2f844b683 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -1508,7 +1508,7 @@ private async void OnStageChunk(object _1, RoutedEventArgs _2) diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, false, chunk.IsOldSide, tmpFile); } - await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", false, "--cache --index").ExecAsync(); + await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", "--cache --index").ExecAsync(); File.Delete(tmpFile); vm.BlockNavigation.UpdateByChunk(chunk); @@ -1539,7 +1539,7 @@ private async void OnUnstageChunk(object _1, RoutedEventArgs _2) else diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, chunk.IsOldSide, tmpFile); - await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", false, "--cache --index --reverse").ExecAsync(); + await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", "--cache --index --reverse").ExecAsync(); File.Delete(tmpFile); vm.BlockNavigation.UpdateByChunk(chunk); @@ -1577,7 +1577,7 @@ private async void OnDiscardChunk(object _1, RoutedEventArgs _2) diff.GeneratePatchFromSelectionSingleSide(change, treeGuid, selection, true, chunk.IsOldSide, tmpFile); } - await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", false, "--reverse").ExecAsync(); + await new Commands.Apply(repo.FullPath, tmpFile, true, "nowarn", "--reverse").ExecAsync(); File.Delete(tmpFile); vm.BlockNavigation.UpdateByChunk(chunk); From 13207e8d57411f211c2d62686e15cccf04f1b225 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Mar 2026 02:27:48 +0000 Subject: [PATCH 21/30] doc: Update translation status and sort locale files --- TRANSLATION.md | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index fd4125995..bc2c2dc4c 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,11 +6,12 @@ This document shows the translation status of each locale file in the repository ### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen) -### ![de__DE](https://img.shields.io/badge/de__DE-98.35%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-98.25%25-yellow)
Missing keys in de_DE.axaml +- Text.Apply.3Way - Text.CommandPalette.Branches - Text.CommandPalette.BranchesAndTags - Text.CommandPalette.RepositoryActions @@ -30,22 +31,24 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-99.79%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.69%25-yellow)
Missing keys in es_ES.axaml +- Text.Apply.3Way - Text.Hotkeys.Repo.CreateBranch - Text.Preferences.General.Use24Hours
-### ![fr__FR](https://img.shields.io/badge/fr__FR-92.18%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-92.09%25-yellow)
Missing keys in fr_FR.axaml - Text.About.ReleaseDate +- Text.Apply.3Way - Text.Blame.IgnoreWhitespace - Text.BranchCM.CompareTwo - Text.BranchCM.CompareWith @@ -124,13 +127,14 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-90.12%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-90.03%25-yellow)
Missing keys in id_ID.axaml - Text.About.ReleaseDate - Text.About.ReleaseNotes +- Text.Apply.3Way - Text.Blame.BlameOnPreviousRevision - Text.Blame.IgnoreWhitespace - Text.BranchCM.CompareTwo @@ -228,11 +232,12 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-97.74%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-97.64%25-yellow)
Missing keys in it_IT.axaml +- Text.Apply.3Way - Text.ChangeCM.ResetFileTo - Text.CommandPalette.Branches - Text.CommandPalette.BranchesAndTags @@ -258,11 +263,12 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-98.77%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-98.66%25-yellow)
Missing keys in ja_JP.axaml +- Text.Apply.3Way - Text.CommandPalette.Branches - Text.CommandPalette.BranchesAndTags - Text.CommandPalette.RepositoryActions @@ -278,12 +284,13 @@ This document shows the translation status of each locale file in the repository
-### ![ko__KR](https://img.shields.io/badge/ko__KR-90.43%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-90.34%25-yellow)
Missing keys in ko_KR.axaml - Text.About.ReleaseDate +- Text.Apply.3Way - Text.Blame.BlameOnPreviousRevision - Text.Blame.IgnoreWhitespace - Text.Blame.TypeNotSupported @@ -379,11 +386,12 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-68.42%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-68.35%25-red)
Missing keys in pt_BR.axaml +- Text.Apply.3Way - Text.Blame.BlameOnPreviousRevision - Text.BranchCM.InteractiveRebase.Manually - Text.BranchTree.AheadBehind @@ -694,11 +702,12 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-99.07%25-yellow) +### ![ru__RU](https://img.shields.io/badge/ru__RU-98.97%25-yellow)
Missing keys in ru_RU.axaml +- Text.Apply.3Way - Text.CommandPalette.Branches - Text.CommandPalette.BranchesAndTags - Text.CommandPalette.RepositoryActions @@ -711,7 +720,7 @@ This document shows the translation status of each locale file in the repository
-### ![ta__IN](https://img.shields.io/badge/ta__IN-70.68%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-70.61%25-red)
Missing keys in ta_IN.axaml @@ -723,6 +732,7 @@ This document shows the translation status of each locale file in the repository - Text.AddToIgnore.Storage - Text.App.Hide - Text.App.ShowAll +- Text.Apply.3Way - Text.Askpass.Passphrase - Text.Avatar.Load - Text.Bisect @@ -1004,7 +1014,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-71.50%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-71.43%25-red)
Missing keys in uk_UA.axaml @@ -1016,6 +1026,7 @@ This document shows the translation status of each locale file in the repository - Text.AddToIgnore.Storage - Text.App.Hide - Text.App.ShowAll +- Text.Apply.3Way - Text.Askpass.Passphrase - Text.Avatar.Load - Text.Bisect From aad08fe3cc4cf1cc8f0d0ee77b1d997d869fa2d4 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 18 Mar 2026 11:51:35 +0800 Subject: [PATCH 22/30] code_style: rewrite editing repository node in welcome page Signed-off-by: leo --- src/ViewModels/EditRepositoryNode.cs | 30 ++++++++++++---------------- src/Views/EditRepositoryNode.axaml | 24 ++++++++++++++++++---- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/ViewModels/EditRepositoryNode.cs b/src/ViewModels/EditRepositoryNode.cs index d7176c786..cb83668e7 100644 --- a/src/ViewModels/EditRepositoryNode.cs +++ b/src/ViewModels/EditRepositoryNode.cs @@ -6,17 +6,14 @@ namespace SourceGit.ViewModels { public class EditRepositoryNode : Popup { - public string Id + public string Target { - get => _id; - set => SetProperty(ref _id, value); + get; } - [Required(ErrorMessage = "Name is required!")] - public string Name + public bool IsRepository { - get => _name; - set => SetProperty(ref _name, value, true); + get; } public List Bookmarks @@ -24,26 +21,27 @@ public List Bookmarks get; } - public int Bookmark + [Required(ErrorMessage = "Name is required!")] + public string Name { - get => _bookmark; - set => SetProperty(ref _bookmark, value); + get => _name; + set => SetProperty(ref _name, value, true); } - public bool IsRepository + public int Bookmark { - get => _isRepository; - set => SetProperty(ref _isRepository, value); + get => _bookmark; + set => SetProperty(ref _bookmark, value); } public EditRepositoryNode(RepositoryNode node) { _node = node; - _id = node.Id; _name = node.Name; - _isRepository = node.IsRepository; _bookmark = node.Bookmark; + Target = node.IsRepository ? node.Id : node.Name; + IsRepository = node.IsRepository; Bookmarks = new List(); for (var i = 0; i < Models.Bookmarks.Brushes.Length; i++) Bookmarks.Add(i); @@ -65,9 +63,7 @@ public override Task Sure() } private RepositoryNode _node = null; - private string _id = null; private string _name = null; - private bool _isRepository = false; private int _bookmark = 0; } } diff --git a/src/Views/EditRepositoryNode.axaml b/src/Views/EditRepositoryNode.axaml index 0ec9e7c58..a5016eebc 100644 --- a/src/Views/EditRepositoryNode.axaml +++ b/src/Views/EditRepositoryNode.axaml @@ -24,10 +24,26 @@ IsVisible="{Binding IsRepository}"/> - - - - + + + + + + + From 2a3ac514e5a9ae941c62e3c71b64b8ec888a86b8 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 18 Mar 2026 12:13:11 +0800 Subject: [PATCH 23/30] feature: add remote url protocol switcher to new submodule popup and changing submodule url popup Signed-off-by: leo --- src/Views/AddSubmodule.axaml | 23 +++++++++++++++++++++-- src/Views/ChangeSubmoduleUrl.axaml | 14 +++++++++++++- src/Views/Clone.axaml | 14 ++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/Views/AddSubmodule.axaml b/src/Views/AddSubmodule.axaml index fed513bfb..686a6dfcc 100644 --- a/src/Views/AddSubmodule.axaml +++ b/src/Views/AddSubmodule.axaml @@ -3,6 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.AddSubmodule" x:DataType="vm:AddSubmodule"> @@ -27,7 +28,18 @@ VerticalAlignment="Center" CornerRadius="2" Watermark="{DynamicResource Text.RepositoryURL}" - Text="{Binding Url, Mode=TwoWay}"/> + Text="{Binding Url, Mode=TwoWay}"> + + + + + + + + + Text="{Binding RelativePath, Mode=TwoWay}"> + + + + @@ -36,7 +37,18 @@ VerticalAlignment="Center" CornerRadius="2" Watermark="{DynamicResource Text.RepositoryURL}" - Text="{Binding Url, Mode=TwoWay}"/> + Text="{Binding Url, Mode=TwoWay}"> + + + + + + + + diff --git a/src/Views/Clone.axaml b/src/Views/Clone.axaml index 884b44ad5..79289ed2f 100644 --- a/src/Views/Clone.axaml +++ b/src/Views/Clone.axaml @@ -28,6 +28,13 @@ Height="28" CornerRadius="3" Text="{Binding Remote, Mode=TwoWay}"> + + + + @@ -45,6 +52,13 @@ Watermark="{DynamicResource Text.SSHKey.Placeholder}" Text="{Binding SSHKey, Mode=TwoWay}" IsVisible="{Binding UseSSH}"> + + + +
-### ![ru__RU](https://img.shields.io/badge/ru__RU-98.97%25-yellow) - -
-Missing keys in ru_RU.axaml - -- Text.Apply.3Way -- Text.CommandPalette.Branches -- Text.CommandPalette.BranchesAndTags -- Text.CommandPalette.RepositoryActions -- Text.CommandPalette.RevisionFiles -- Text.ConfirmEmptyCommit.StageSelectedThenCommit -- Text.Hotkeys.Repo.CreateBranch -- Text.Init.CommandTip -- Text.Init.ErrorMessageTip -- Text.Preferences.General.Use24Hours - -
+### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) ### ![ta__IN](https://img.shields.io/badge/ta__IN-70.61%25-red) From 98ad8cdb5d1d114e0a3e42c748196f7188198647 Mon Sep 17 00:00:00 2001 From: Adam Stachowicz Date: Sun, 22 Mar 2026 02:12:38 +0100 Subject: [PATCH 29/30] ci: update GitHub Actions versions (#2209) --- .github/workflows/build.yml | 6 +++--- .github/workflows/ci.yml | 2 +- .github/workflows/format-check.yml | 4 ++-- .github/workflows/localization-check.yml | 6 +++--- .github/workflows/package.yml | 24 ++++++++++++------------ .github/workflows/release.yml | 4 ++-- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e97d0b1b6..b3dd9d5ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,11 +39,11 @@ jobs: apt-get install -y sudo sudo apt-get install -y curl wget git unzip zip libicu66 tzdata clang - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: true - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x - name: Configure arm64 packages @@ -75,7 +75,7 @@ jobs: rm -r publish/* mv "sourcegit.${{ matrix.runtime }}.tar" publish - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sourcegit.${{ matrix.runtime }} path: publish/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50e02dc95..3204df528 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Output version string id: version run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index adbeab527..0640d19e9 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -13,12 +13,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: true - name: Set up .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x diff --git a/.github/workflows/localization-check.yml b/.github/workflows/localization-check.yml index c5970870b..76d7be77f 100644 --- a/.github/workflows/localization-check.yml +++ b/.github/workflows/localization-check.yml @@ -13,12 +13,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: '20.x' + node-version: '24.x' - name: Install dependencies run: npm install fs-extra@11.2.0 path@0.12.7 xml2js@0.6.2 diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index d203dd2e2..0845774fb 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -15,9 +15,9 @@ jobs: runtime: [win-x64, win-arm64] steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download build - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: sourcegit.${{ matrix.runtime }} path: build/SourceGit @@ -28,12 +28,12 @@ jobs: RUNTIME: ${{ matrix.runtime }} run: ./build/scripts/package.win.ps1 - name: Upload package artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: package.${{ matrix.runtime }} path: build/sourcegit_*.zip - name: Delete temp artifacts - uses: geekyeggo/delete-artifact@v5 + uses: geekyeggo/delete-artifact@v6 with: name: sourcegit.${{ matrix.runtime }} osx-app: @@ -44,9 +44,9 @@ jobs: runtime: [osx-x64, osx-arm64] steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download build - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: sourcegit.${{ matrix.runtime }} path: build @@ -59,12 +59,12 @@ jobs: tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit ./build/scripts/package.osx-app.sh - name: Upload package artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: package.${{ matrix.runtime }} path: build/sourcegit_*.zip - name: Delete temp artifacts - uses: geekyeggo/delete-artifact@v5 + uses: geekyeggo/delete-artifact@v6 with: name: sourcegit.${{ matrix.runtime }} linux: @@ -76,7 +76,7 @@ jobs: runtime: [linux-x64, linux-arm64] steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download package dependencies run: | export DEBIAN_FRONTEND=noninteractive @@ -84,7 +84,7 @@ jobs: apt-get update apt-get install -y curl wget git dpkg-dev fakeroot tzdata zip unzip desktop-file-utils rpm libfuse2 file build-essential binutils - name: Download build - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: sourcegit.${{ matrix.runtime }} path: build @@ -98,7 +98,7 @@ jobs: tar -xf "build/sourcegit.${{ matrix.runtime }}.tar" -C build/SourceGit ./build/scripts/package.linux.sh - name: Upload package artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: package.${{ matrix.runtime }} path: | @@ -106,6 +106,6 @@ jobs: build/sourcegit_*.deb build/sourcegit-*.rpm - name: Delete temp artifacts - uses: geekyeggo/delete-artifact@v5 + uses: geekyeggo/delete-artifact@v6 with: name: sourcegit.${{ matrix.runtime }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e61e608b0..816870a02 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: contents: write steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -40,7 +40,7 @@ jobs: VERSION: ${{ needs.version.outputs.version }} run: gh release create "$TAG" -t "$VERSION" --notes-from-tag - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: package.* path: packages From dae8dcbe252d54f064b44f021f431143048ff972 Mon Sep 17 00:00:00 2001 From: Saibamen Date: Sun, 22 Mar 2026 11:09:30 +0100 Subject: [PATCH 30/30] Add master to GH Actions triggers --- .github/workflows/ci.yml | 4 ++-- .github/workflows/format-check.yml | 4 ++-- .github/workflows/localization-check.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3204df528..acd7dcde1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,9 @@ name: Continuous Integration on: push: - branches: [develop] + branches: [develop, master] pull_request: - branches: [develop] + branches: [develop, master] workflow_dispatch: workflow_call: jobs: diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index 0640d19e9..43d5edf91 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -1,9 +1,9 @@ name: Format Check on: push: - branches: [develop] + branches: [develop, master] pull_request: - branches: [develop] + branches: [develop, master] workflow_dispatch: workflow_call: diff --git a/.github/workflows/localization-check.yml b/.github/workflows/localization-check.yml index 76d7be77f..84274977a 100644 --- a/.github/workflows/localization-check.yml +++ b/.github/workflows/localization-check.yml @@ -1,7 +1,7 @@ name: Localization Check on: push: - branches: [develop] + branches: [develop, master] paths: - 'src/Resources/Locales/**' workflow_dispatch: