From 14db2d5cb053335ca0a93128389d78621a82e0e2 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 16 Mar 2026 11:52:48 +0800 Subject: [PATCH 01/64] 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 a4c25193..9ab107fb 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -286,6 +286,8 @@ namespace SourceGit 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 ccd9f57e..531cbccd 100644 --- a/src/Models/ThemeOverrides.cs +++ b/src/Models/ThemeOverrides.cs @@ -9,6 +9,7 @@ namespace SourceGit.Models 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 159656f6..1044887a 100644 --- a/src/Native/OS.cs +++ b/src/Native/OS.cs @@ -107,6 +107,12 @@ namespace SourceGit.Native 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 @@ namespace SourceGit.Native 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 a1cd3d92..985c60fb 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -62,7 +62,7 @@ namespace SourceGit.Views 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/64] 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 680aff63..abb9fd42 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -194,6 +194,7 @@ namespace SourceGit.Commands return; } + _result.TextDiff.DeletedLines++; _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0); _deleted.Add(_last); _oldLine++; @@ -207,6 +208,7 @@ namespace SourceGit.Commands 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 df8f204c..32fff76c 100644 --- a/src/Models/DiffResult.cs +++ b/src/Models/DiffResult.cs @@ -55,6 +55,8 @@ namespace SourceGit.Models { 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 fd1be431..1804bfd4 100644 --- a/src/ViewModels/TextDiffContext.cs +++ b/src/ViewModels/TextDiffContext.cs @@ -28,6 +28,8 @@ namespace SourceGit.ViewModels { 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 f734a983..3697ea10 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/64] 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 1804bfd4..fd1be431 100644 --- a/src/ViewModels/TextDiffContext.cs +++ b/src/ViewModels/TextDiffContext.cs @@ -28,8 +28,6 @@ namespace SourceGit.ViewModels { 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 3697ea10..3c52a7d5 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 f857ffee..72ebb9dc 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/64] 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 994f52a3..fd412599 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/64] 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 9ab107fb..e986f4e4 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -806,7 +806,7 @@ namespace SourceGit 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 abb9fd42..4d0cc72a 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -12,7 +12,7 @@ namespace SourceGit.Commands [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 087e71c7..9dbe0545 100644 --- a/src/Commands/IsBinary.cs +++ b/src/Commands/IsBinary.cs @@ -8,11 +8,11 @@ namespace SourceGit.Commands [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 6af0abb7..7e7e8887 100644 --- a/src/Commands/QueryCommitChildren.cs +++ b/src/Commands/QueryCommitChildren.cs @@ -24,7 +24,7 @@ namespace SourceGit.Commands 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 cff939e2..78109ce6 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 60501ba4..7f55e31f 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -15,8 +15,6 @@ namespace SourceGit.Models 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 @@ namespace SourceGit.Models public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime; public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; public bool 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 0dc8bc31..def59bed 100644 --- a/src/Models/DiffOption.cs +++ b/src/Models/DiffOption.cs @@ -60,8 +60,7 @@ namespace SourceGit.Models /// 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 @@ namespace SourceGit.Models /// 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 @@ namespace SourceGit.Models { 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 @@ namespace SourceGit.Models { 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 00000000..bf1445a0 --- /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 4f5ad692..93439a40 100644 --- a/src/Models/Stash.cs +++ b/src/Models/Stash.cs @@ -10,5 +10,6 @@ namespace SourceGit.Models 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 8cffb0ae..6ccfba0a 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -240,8 +240,13 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels [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 a2a47c27..a484b022 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -63,7 +63,7 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels .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 @@ namespace SourceGit.ViewModels { 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 @@ namespace SourceGit.ViewModels 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 6b68a22d..f716a8d4 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -739,7 +739,7 @@ namespace SourceGit.ViewModels 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 8f47a350..e9c1bac9 100644 --- a/src/Views/StashSubjectPresenter.cs +++ b/src/Views/StashSubjectPresenter.cs @@ -127,7 +127,7 @@ namespace SourceGit.Views [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/64] 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 e986f4e4..f9c6117c 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -6,7 +6,6 @@ using System.Net.Http; 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 @@ namespace SourceGit 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 @@ namespace SourceGit 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 @@ namespace SourceGit 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 bae99ac5..ac7e29d4 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 @@ namespace SourceGit.Models 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/64] 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 ee760331..8da89a13 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 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels succ = await new Commands.Checkout(_repo.FullPath) .Use(log) - .BranchAsync(Branch, DiscardLocalChanges); + .BranchAsync(branchName, DiscardLocalChanges); if (succ) { @@ -80,21 +81,19 @@ namespace SourceGit.ViewModels 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 7123a2e0..6fa078a9 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -788,6 +788,42 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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 0d2f2eca..26fafe81 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/64] 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 722e450f..17d1fc05 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 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels private void RefreshViewContent() { - if (_isDiffMode) + if (_viewMode.IsDiff) { ViewContent = new DiffContext(_repo, new(_revision), _viewContent as DiffContext); return; @@ -155,7 +171,7 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels { 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 @@ namespace SourceGit.ViewModels 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 f929b0a6..8b21e197 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 749b4862..25254327 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 @@ namespace SourceGit.Views 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/64] 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 b0323528..ffd11bda 100644 --- a/src/Commands/Clone.cs +++ b/src/Commands/Clone.cs @@ -14,7 +14,7 @@ namespace SourceGit.Commands 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/64] 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 197701c3..1b2ef381 100644 --- a/src/ViewModels/CheckoutAndFastForward.cs +++ b/src/ViewModels/CheckoutAndFastForward.cs @@ -80,17 +80,19 @@ namespace SourceGit.ViewModels await new Commands.Stash(_repo.FullPath) .Use(log) .PopAsync("stash@{0}"); + + LocalBranch.Behind.Clear(); + LocalBranch.Head = RemoteBranch.Head; + LocalBranch.CommitterDate = RemoteBranch.CommitterDate; + + _repo.FastRefreshBranchesAfterCheckout(LocalBranch); + } + else + { + _repo.MarkWorkingCopyDirtyManually(); } log.Complete(); - - if (_repo.HistoryFilterMode == Models.FilterMode.Included) - _repo.SetBranchFilterMode(LocalBranch, Models.FilterMode.Included, false, false); - - _repo.MarkBranchesDirtyManually(); - - ProgressDescription = "Waiting for branch updated..."; - await Task.Delay(400); 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/64] 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 e15a96b9..4a9235d4 100644 --- a/src/ViewModels/RenameBranch.cs +++ b/src/ViewModels/RenameBranch.cs @@ -60,17 +60,9 @@ namespace SourceGit.ViewModels .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 6fa078a9..dacafd55 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -824,6 +824,30 @@ namespace SourceGit.ViewModels 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 65113340..367e5424 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -887,37 +887,45 @@ namespace SourceGit.Views } } - 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 @@ namespace SourceGit.Views 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/64] 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 | 57 +++++++++++++----------- src/ViewModels/RenameBranch.cs | 2 +- src/ViewModels/Repository.cs | 51 ++++++++++++++++++++- 5 files changed, 83 insertions(+), 31 deletions(-) diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index 8da89a13..71a324c6 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -82,7 +82,7 @@ namespace SourceGit.ViewModels .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 1b2ef381..120d6c4a 100644 --- a/src/ViewModels/CheckoutAndFastForward.cs +++ b/src/ViewModels/CheckoutAndFastForward.cs @@ -85,7 +85,7 @@ namespace SourceGit.ViewModels 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 b5d4d712..832f5626 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -58,6 +58,8 @@ namespace SourceGit.ViewModels { _repo = repo; _baseOnRevision = branch.Head; + _committerDate = branch.CommitterDate; + _head = branch.Head; if (!branch.IsLocal) Name = branch.Name; @@ -70,6 +72,8 @@ namespace SourceGit.ViewModels { _repo = repo; _baseOnRevision = commit.SHA; + _committerDate = commit.CommitterTime; + _head = commit.SHA; BasedOn = commit; DiscardLocalChanges = false; @@ -79,6 +83,8 @@ namespace SourceGit.ViewModels { _repo = repo; _baseOnRevision = tag.SHA; + _committerDate = tag.CreatorDate; + _head = tag.SHA; BasedOn = tag; DiscardLocalChanges = false; @@ -125,6 +131,15 @@ namespace SourceGit.ViewModels } } + 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 @@ namespace SourceGit.ViewModels .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); + + created.Upstream = basedOn.FullName; + } + + _repo.RefreshAfterCreateBranch(created, CheckoutAfterCreated); + } + else + { + _repo.MarkWorkingCopyDirtyManually(); } 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)); - - if (_repo.HistoryFilterMode == Models.FilterMode.Included) - _repo.SetBranchFilterMode(fake, Models.FilterMode.Included, false, false); - } - - _repo.MarkBranchesDirtyManually(); - - if (CheckoutAfterCreated) - { - ProgressDescription = "Waiting for branch updated..."; - await Task.Delay(400); - } - 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 4a9235d4..dbca651e 100644 --- a/src/ViewModels/RenameBranch.cs +++ b/src/ViewModels/RenameBranch.cs @@ -60,7 +60,7 @@ namespace SourceGit.ViewModels .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 dacafd55..bbd25f17 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -788,7 +788,54 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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/64] 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 71a324c6..aca5cb1e 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -22,11 +22,6 @@ namespace SourceGit.ViewModels 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 bbd25f17..bf31a3d5 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1332,12 +1332,23 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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/64] 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 1ef69705..ce1e6cbd 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -97,7 +97,6 @@ namespace SourceGit.Models 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/64] 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 374336e5..da4514e9 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 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels } 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/64] 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 eb8d8b9f..2b1b4957 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 00000000..c496ea87 --- /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/64] 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 ce1e6cbd..18b57c41 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -64,14 +64,9 @@ namespace SourceGit.Models { 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 c496ea87..343dbc13 100644 --- a/src/Views/RemoteProtocolSwitcher.axaml.cs +++ b/src/Views/RemoteProtocolSwitcher.axaml.cs @@ -45,11 +45,10 @@ namespace SourceGit.Views 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/64] 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 ca6ffe8d..544cf4d9 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 @@ namespace SourceGit.Commands 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 994a8804..f71f46f1 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 3eab5ef7..817d5621 100644 --- a/src/ViewModels/Apply.cs +++ b/src/ViewModels/Apply.cs @@ -26,6 +26,12 @@ namespace SourceGit.ViewModels set; } + public bool ThreeWayMerge + { + get => _threeWayMerge; + set => SetProperty(ref _threeWayMerge, value); + } + public Apply(Repository repo) { _repo = repo; @@ -49,7 +55,7 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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 a484b022..d1834d63 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -253,7 +253,7 @@ namespace SourceGit.ViewModels 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 d1265a5a..d64972de 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 2f844b68..81322c72 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -1508,7 +1508,7 @@ namespace SourceGit.Views 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 @@ namespace SourceGit.Views 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 @@ namespace SourceGit.Views 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/64] 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 544cf4d9..ca6ffe8d 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 @@ namespace SourceGit.Commands 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 f71f46f1..3514fbef 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 235cfe16..3f1b8c55 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 9f1545c1..fa4e3d2b 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 817d5621..3578c12f 100644 --- a/src/ViewModels/Apply.cs +++ b/src/ViewModels/Apply.cs @@ -28,8 +28,8 @@ namespace SourceGit.ViewModels public bool ThreeWayMerge { - get => _threeWayMerge; - set => SetProperty(ref _threeWayMerge, value); + get; + set; } public Apply(Repository repo) @@ -55,7 +55,8 @@ namespace SourceGit.ViewModels 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 @@ namespace SourceGit.ViewModels 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 d1834d63..a484b022 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -253,7 +253,7 @@ namespace SourceGit.ViewModels 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 81322c72..2f844b68 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -1508,7 +1508,7 @@ namespace SourceGit.Views 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 @@ namespace SourceGit.Views 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 @@ namespace SourceGit.Views 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/64] 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 fd412599..bc2c2dc4 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/64] code_style: rewrite editing repository node in welcome page Signed-off-by: leo --- src/ViewModels/EditRepositoryNode.cs | 32 ++++++++++++---------------- src/Views/EditRepositoryNode.axaml | 24 +++++++++++++++++---- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/ViewModels/EditRepositoryNode.cs b/src/ViewModels/EditRepositoryNode.cs index d7176c78..cb83668e 100644 --- a/src/ViewModels/EditRepositoryNode.cs +++ b/src/ViewModels/EditRepositoryNode.cs @@ -6,10 +6,19 @@ namespace SourceGit.ViewModels { public class EditRepositoryNode : Popup { - public string Id + public string Target { - get => _id; - set => SetProperty(ref _id, value); + get; + } + + public bool IsRepository + { + get; + } + + public List Bookmarks + { + get; } [Required(ErrorMessage = "Name is required!")] @@ -19,31 +28,20 @@ namespace SourceGit.ViewModels set => SetProperty(ref _name, value, true); } - public List Bookmarks - { - get; - } - public int Bookmark { get => _bookmark; set => SetProperty(ref _bookmark, value); } - public bool IsRepository - { - get => _isRepository; - set => SetProperty(ref _isRepository, 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 @@ namespace SourceGit.ViewModels } 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 0ec9e7c5..a5016eeb 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/64] 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 fed513bf..686a6dfc 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 884b44ad..79289ed2 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/64] 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 e97d0b1b..b3dd9d5e 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 50e02dc9..3204df52 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 adbeab52..0640d19e 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 c5970870..76d7be77 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 d203dd2e..0845774f 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 e61e608b..816870a0 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 d357bc4286441b2b6e0aa11ed33a66be9e389ae1 Mon Sep 17 00:00:00 2001 From: Chiahong <36815907+ChiahongHong@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:29:22 +0800 Subject: [PATCH 30/64] localization: update Traditional Chinese translations (#2213) --- src/Resources/Locales/zh_TW.axaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index fa4e3d2b..e347950e 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -28,7 +28,7 @@ 隱藏 SourceGit 顯示所有 套用修補檔 (apply patch) - 嘗試三路合併 + 嘗試三向合併 修補檔: 選擇修補檔 忽略空白符號 @@ -286,7 +286,7 @@ 確認繼續 未包含任何檔案變更! 您是否仍要提交 (--allow-empty)? 暫存全部變更並提交 - 僅暫存所選變更并提交 + 僅暫存所選變更並提交 未包含任何檔案變更! 您是否仍要提交 (--allow-empty) 或者自動暫存變更並提交? 系統提示 您需要重新啟動此應用程式才能套用變更! @@ -530,8 +530,8 @@ 暫存 取消暫存 初始化存放庫 - 在該路徑執行 git init 以初始化 git 倉庫? - 無法在指定路徑開啟本機儲存庫。原因: + 您是否要在該路徑執行 git init 以進行初始化? + 無法在指定路徑開啟本機存放庫。原因: 路徑: 揀選 (cherry-pick) 操作進行中。 正在處理提交 @@ -655,7 +655,7 @@ 在提交詳細資訊中顯示後續提交 在路線圖中顯示標籤 提交標題字數偵測 - 24小時制 + 24 小時制 產生 GitHub 風格的預設頭貼 Git 設定 自動換行轉換 @@ -983,7 +983,7 @@ 工作區: 設定工作區... 本機工作區 - 关联分支 + 分支 複製工作區路徑 最新提交 鎖定工作區 From 0b1545191601f620780c7929e4258005ef252511 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 23 Mar 2026 12:17:24 +0800 Subject: [PATCH 31/64] feature: add `Do Nothing` option back to all `git checkout` command (#2204) Signed-off-by: leo --- src/Models/DealWithLocalChanges.cs | 9 +++ src/Resources/Locales/de_DE.axaml | 3 + src/Resources/Locales/en_US.axaml | 9 +-- src/Resources/Locales/es_ES.axaml | 3 + src/Resources/Locales/fr_FR.axaml | 3 + src/Resources/Locales/id_ID.axaml | 2 + src/Resources/Locales/it_IT.axaml | 3 + src/Resources/Locales/ja_JP.axaml | 2 + src/Resources/Locales/ko_KR.axaml | 2 + src/Resources/Locales/pt_BR.axaml | 3 + src/Resources/Locales/ru_RU.axaml | 3 + src/Resources/Locales/ta_IN.axaml | 2 + src/Resources/Locales/uk_UA.axaml | 2 + src/Resources/Locales/zh_CN.axaml | 3 + src/Resources/Locales/zh_TW.axaml | 3 + src/ViewModels/Checkout.cs | 27 ++++++--- src/ViewModels/CheckoutAndFastForward.cs | 26 +++++++-- src/ViewModels/CheckoutCommit.cs | 27 ++++++--- src/ViewModels/CreateBranch.cs | 52 ++++++++++++++--- src/ViewModels/Pull.cs | 15 +++-- src/Views/Checkout.axaml | 22 ++++---- src/Views/CheckoutAndFastForward.axaml | 22 ++++---- src/Views/CheckoutCommit.axaml | 24 ++++---- src/Views/CreateBranch.axaml | 37 +++++++----- src/Views/DealWithLocalChangesMethod.axaml | 26 +++++++++ src/Views/DealWithLocalChangesMethod.axaml.cs | 56 +++++++++++++++++++ src/Views/Pull.axaml | 22 ++++---- 27 files changed, 304 insertions(+), 104 deletions(-) create mode 100644 src/Models/DealWithLocalChanges.cs create mode 100644 src/Views/DealWithLocalChangesMethod.axaml create mode 100644 src/Views/DealWithLocalChangesMethod.axaml.cs diff --git a/src/Models/DealWithLocalChanges.cs b/src/Models/DealWithLocalChanges.cs new file mode 100644 index 00000000..9775c61a --- /dev/null +++ b/src/Models/DealWithLocalChanges.cs @@ -0,0 +1,9 @@ +namespace SourceGit.Models +{ + public enum DealWithLocalChanges + { + DoNothing = 0, + StashAndReapply, + Discard, + } +} diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index c3f3f631..67bcc363 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -318,6 +318,9 @@ $1, $2, … Werte der Eingabe-Steuerelemente Ohne Anmerkung Halte ‚Strg‘ gedrückt, um direkt auszuführen Ausschneiden + Verwerfen + Nichts tun + Stashen & wieder anwenden Deinitialisiere Submodul Erzwinge Deinitialisierung, selbst wenn lokale Änderungen enthalten sind. Submodul: diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 3514fbef..2bd5d316 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -110,8 +110,6 @@ Commit: Warning: By doing a commit checkout, your Head will be detached Local Changes: - Discard - Stash & Reapply Branch: Your current HEAD contains commit(s) not connected to any branches/tags! Do you want to continue? The following submodules need to be updated:{0}Do you want to update them? @@ -301,8 +299,6 @@ Based On: Check out the created branch Local Changes: - Discard - Stash & Reapply New Branch Name: Enter branch name. Create Local Branch @@ -321,6 +317,9 @@ lightweight Hold Ctrl to start directly Cut + Discard + Do Nothing + Stash & Reapply De-initialize Submodule Force de-init even if it contains local changes. Submodule: @@ -689,8 +688,6 @@ Remote Branch: Into: Local Changes: - Discard - Stash & Reapply Remote: Pull (Fetch & Merge) Use rebase instead of merge diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 736cc079..87b8b75b 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -324,6 +324,9 @@ ligera Mantenga Ctrl para iniciar directamente Cortar + Descartar + No Hacer Nada + Stash & Reaplicar Desinicializar Submódulo Forzar desinicialización incluso si contiene cambios locales. Submódulo: diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 12293ff1..50829885 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -305,6 +305,9 @@ léger Maintenir Ctrl pour commencer directement Couper + Annuler + Ne rien faire + Mettre en stash et réappliquer Désinitialiser le sous-module Forcer la désinitialisation même s'il contient des modifications locales. Sous-module : diff --git a/src/Resources/Locales/id_ID.axaml b/src/Resources/Locales/id_ID.axaml index afb07fd8..1b4edd3a 100644 --- a/src/Resources/Locales/id_ID.axaml +++ b/src/Resources/Locales/id_ID.axaml @@ -290,6 +290,8 @@ lightweight Tahan Ctrl untuk memulai langsung Potong + Buang + Stash & Terapkan Ulang De-initialize Submodule Paksa de-init meski mengandung perubahan lokal. Submodule: diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index f7b6346b..9b36517a 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -317,6 +317,9 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle leggero Tieni premuto Ctrl per avviare direttamente Taglia + Scarta + Non fare nulla + Stash e Ripristina Deinizializza Sottomodulo Forza deinizializzazione anche se contiene modifiche locali. Sottomodulo: diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index f2b3f277..9b301143 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -319,6 +319,8 @@ 軽量 Ctrl キーを押しながらで直接実行できます 切り取り + 破棄 + スタッシュして再適用 サブモジュールの初期化を解除 ローカルの変更の有無に関わらず、強制的に解除 サブモジュール: diff --git a/src/Resources/Locales/ko_KR.axaml b/src/Resources/Locales/ko_KR.axaml index 0768b096..3d541dca 100644 --- a/src/Resources/Locales/ko_KR.axaml +++ b/src/Resources/Locales/ko_KR.axaml @@ -289,6 +289,8 @@ 경량 태그 Ctrl을 누른 채 클릭하면 바로 시작합니다 잘라내기 + 폐기 + 스태시 & 재적용 서브모듈 초기화 해제 로컬 변경 사항이 있어도 강제로 초기화 해제합니다. 서브모듈: diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 97218f93..6c738cc0 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -220,6 +220,9 @@ leve Pressione Ctrl para iniciar diretamente Recortar + Descartar + Nada + Stash & Reaplicar Excluir Branch Branch: Você está prestes a excluir uma branch remota!!! diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 629ac59d..d642c4ef 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -325,6 +325,9 @@ Простой Удерживайте Ctrl, чтобы сразу начать Вырезать + Отклонить + Ничего не делать + Отложить и примненить повторно Удалить подмодуль Принудительно удалить даже если содержит локальные изменения. Подмодуль: diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index e2dd7900..0e607ab3 100644 --- a/src/Resources/Locales/ta_IN.axaml +++ b/src/Resources/Locales/ta_IN.axaml @@ -201,6 +201,8 @@ குறைந்தஎடை நேரடியாகத் தொடங்க கட்டுப்பாட்டை அழுத்திப் பிடி வெட்டு + நிராகரி + பதுக்கிவை & மீண்டும் இடு கிளையை நீக்கு கிளை: நீங்கள் ஒரு தொலை கிளையை நீக்கப் போகிறீர்கள்!!! diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml index 2ba6c0ee..af0d82d8 100644 --- a/src/Resources/Locales/uk_UA.axaml +++ b/src/Resources/Locales/uk_UA.axaml @@ -205,6 +205,8 @@ легкий Утримуйте Ctrl для запуску без діалогу Вирізати + Скасувати + Сховати та Застосувати Видалити гілку Гілка: Ви збираєтеся видалити віддалену гілку!!! diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 3f1b8c55..1f0041da 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -325,6 +325,9 @@ 轻量标签 按住Ctrl键点击将以默认参数运行 剪切 + 丢弃更改 + 不做处理 + 贮藏并自动恢复 取消初始化子模块 强制取消,即使包含本地变更 子模块 : diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index e347950e..b959405b 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -325,6 +325,9 @@ 輕量標籤 按住 Ctrl 鍵將直接以預設參數執行 剪下 + 捨棄變更 + 不做處理 + 擱置變更並自動復原 取消初始化子模組 強制取消,即使包含本機變更 子模組: diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index aca5cb1e..d2fc39b9 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -9,7 +9,7 @@ namespace SourceGit.ViewModels get => _branch.Name; } - public bool DiscardLocalChanges + public Models.DealWithLocalChanges DealWithLocalChanges { get; set; @@ -19,7 +19,7 @@ namespace SourceGit.ViewModels { _repo = repo; _branch = branch; - DiscardLocalChanges = false; + DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; } public override async Task Sure() @@ -46,7 +46,13 @@ namespace SourceGit.ViewModels var succ = false; var needPopStash = false; - if (!DiscardLocalChanges) + if (DealWithLocalChanges == Models.DealWithLocalChanges.DoNothing) + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(branchName, false); + } + else if (DealWithLocalChanges == Models.DealWithLocalChanges.StashAndReapply) { var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) @@ -57,16 +63,23 @@ namespace SourceGit.ViewModels if (!succ) { log.Complete(); + _repo.MarkWorkingCopyDirtyManually(); return false; } needPopStash = true; } - } - succ = await new Commands.Checkout(_repo.FullPath) - .Use(log) - .BranchAsync(branchName, DiscardLocalChanges); + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(branchName, false); + } + else + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(branchName, true); + } if (succ) { diff --git a/src/ViewModels/CheckoutAndFastForward.cs b/src/ViewModels/CheckoutAndFastForward.cs index 120d6c4a..c80a9a8a 100644 --- a/src/ViewModels/CheckoutAndFastForward.cs +++ b/src/ViewModels/CheckoutAndFastForward.cs @@ -14,7 +14,7 @@ namespace SourceGit.ViewModels get; } - public bool DiscardLocalChanges + public Models.DealWithLocalChanges DealWithLocalChanges { get; set; @@ -25,6 +25,7 @@ namespace SourceGit.ViewModels _repo = repo; LocalBranch = localBranch; RemoteBranch = remoteBranch; + DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; } public override async Task Sure() @@ -50,7 +51,13 @@ namespace SourceGit.ViewModels var succ = false; var needPopStash = false; - if (!DiscardLocalChanges) + if (DealWithLocalChanges == Models.DealWithLocalChanges.DoNothing) + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(LocalBranch.Name, RemoteBranch.Head, false, true); + } + else if (DealWithLocalChanges == Models.DealWithLocalChanges.StashAndReapply) { var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) @@ -61,16 +68,23 @@ namespace SourceGit.ViewModels if (!succ) { log.Complete(); + _repo.MarkWorkingCopyDirtyManually(); return false; } needPopStash = true; } - } - succ = await new Commands.Checkout(_repo.FullPath) - .Use(log) - .BranchAsync(LocalBranch.Name, RemoteBranch.Head, DiscardLocalChanges, true); + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(LocalBranch.Name, RemoteBranch.Head, false, true); + } + else + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(LocalBranch.Name, RemoteBranch.Head, true, true); + } if (succ) { diff --git a/src/ViewModels/CheckoutCommit.cs b/src/ViewModels/CheckoutCommit.cs index d631f5f0..c8179c23 100644 --- a/src/ViewModels/CheckoutCommit.cs +++ b/src/ViewModels/CheckoutCommit.cs @@ -9,7 +9,7 @@ namespace SourceGit.ViewModels get; } - public bool DiscardLocalChanges + public Models.DealWithLocalChanges DealWithLocalChanges { get; set; @@ -19,7 +19,7 @@ namespace SourceGit.ViewModels { _repo = repo; Commit = commit; - DiscardLocalChanges = false; + DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; } public override async Task Sure() @@ -45,7 +45,13 @@ namespace SourceGit.ViewModels var succ = false; var needPop = false; - if (!DiscardLocalChanges) + if (DealWithLocalChanges == Models.DealWithLocalChanges.DoNothing) + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .CommitAsync(Commit.SHA, false); + } + else if (DealWithLocalChanges == Models.DealWithLocalChanges.StashAndReapply) { var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) @@ -56,16 +62,23 @@ namespace SourceGit.ViewModels if (!succ) { log.Complete(); + _repo.MarkWorkingCopyDirtyManually(); return false; } needPop = true; } - } - succ = await new Commands.Checkout(_repo.FullPath) - .Use(log) - .CommitAsync(Commit.SHA, DiscardLocalChanges); + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .CommitAsync(Commit.SHA, false); + } + else + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .CommitAsync(Commit.SHA, true); + } if (succ) { diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index 832f5626..8060f20f 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -20,7 +20,12 @@ namespace SourceGit.ViewModels get; } - public bool DiscardLocalChanges + public bool HasLocalChanges + { + get => _repo.LocalChangesCount > 0; + } + + public Models.DealWithLocalChanges DealWithLocalChanges { get; set; @@ -35,6 +40,7 @@ namespace SourceGit.ViewModels { _repo.UIStates.CheckoutBranchOnCreateBranch = value; OnPropertyChanged(); + UpdateOverrideTip(); } } } @@ -44,6 +50,12 @@ namespace SourceGit.ViewModels get => _repo.IsBare; } + public string OverrideTip + { + get => _overrideTip; + private set => SetProperty(ref _overrideTip, value); + } + public bool AllowOverwrite { get => _allowOverwrite; @@ -65,7 +77,8 @@ namespace SourceGit.ViewModels Name = branch.Name; BasedOn = branch; - DiscardLocalChanges = false; + DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; + UpdateOverrideTip(); } public CreateBranch(Repository repo, Models.Commit commit) @@ -76,7 +89,8 @@ namespace SourceGit.ViewModels _head = commit.SHA; BasedOn = commit; - DiscardLocalChanges = false; + DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; + UpdateOverrideTip(); } public CreateBranch(Repository repo, Models.Tag tag) @@ -87,7 +101,8 @@ namespace SourceGit.ViewModels _head = tag.SHA; BasedOn = tag; - DiscardLocalChanges = false; + DealWithLocalChanges = Models.DealWithLocalChanges.DoNothing; + UpdateOverrideTip(); } public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) @@ -144,7 +159,13 @@ namespace SourceGit.ViewModels if (CheckoutAfterCreated && !_repo.IsBare) { var needPopStash = false; - if (!DiscardLocalChanges) + if (DealWithLocalChanges == Models.DealWithLocalChanges.DoNothing) + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(_name, _baseOnRevision, false, _allowOverwrite); + } + else if (DealWithLocalChanges == Models.DealWithLocalChanges.StashAndReapply) { var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) @@ -155,16 +176,23 @@ namespace SourceGit.ViewModels if (!succ) { log.Complete(); + _repo.MarkWorkingCopyDirtyManually(); return false; } needPopStash = true; } - } - succ = await new Commands.Checkout(_repo.FullPath) - .Use(log) - .BranchAsync(_name, _baseOnRevision, DiscardLocalChanges, _allowOverwrite); + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(_name, _baseOnRevision, false, _allowOverwrite); + } + else + { + succ = await new Commands.Checkout(_repo.FullPath) + .Use(log) + .BranchAsync(_name, _baseOnRevision, true, _allowOverwrite); + } if (succ) { @@ -205,11 +233,17 @@ namespace SourceGit.ViewModels return true; } + private void UpdateOverrideTip() + { + OverrideTip = CheckoutAfterCreated ? "-B in `git checkout`" : "-f in `git branch`"; + } + private readonly Repository _repo = null; private readonly string _baseOnRevision = null; private readonly ulong _committerDate = 0; private readonly string _head = string.Empty; private string _name = null; + private string _overrideTip = "-B"; private bool _allowOverwrite = false; } } diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index 09388bc0..4e4fe832 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -38,11 +38,11 @@ namespace SourceGit.ViewModels set => SetProperty(ref _selectedBranch, value, true); } - public bool DiscardLocalChanges + public Models.DealWithLocalChanges DealWithLocalChanges { get; set; - } = false; + } = Models.DealWithLocalChanges.DoNothing; public bool UseRebase { @@ -111,21 +111,26 @@ namespace SourceGit.ViewModels var needPopStash = false; if (changes > 0) { - if (DiscardLocalChanges) + if (DealWithLocalChanges == Models.DealWithLocalChanges.DoNothing) { - await Commands.Discard.AllAsync(_repo.FullPath, false, false, log); + // Do nothing, just let the pull command fail and show the error to user } - else + else if (DealWithLocalChanges == Models.DealWithLocalChanges.StashAndReapply) { var succ = await new Commands.Stash(_repo.FullPath).Use(log).PushAsync("PULL_AUTO_STASH", false); if (!succ) { log.Complete(); + _repo.MarkWorkingCopyDirtyManually(); return false; } needPopStash = true; } + else + { + await Commands.Discard.AllAsync(_repo.FullPath, false, false, log); + } } bool rs = await new Commands.Pull( diff --git a/src/Views/Checkout.axaml b/src/Views/Checkout.axaml index 26fafe81..7d3d12b1 100644 --- a/src/Views/Checkout.axaml +++ b/src/Views/Checkout.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="800" d:DesignHeight="450" x:Class="SourceGit.Views.Checkout" x:DataType="vm:Checkout"> @@ -33,18 +34,15 @@ - - - - - + + + +
diff --git a/src/Views/CheckoutAndFastForward.axaml b/src/Views/CheckoutAndFastForward.axaml index 13c12d33..1a4381df 100644 --- a/src/Views/CheckoutAndFastForward.axaml +++ b/src/Views/CheckoutAndFastForward.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="800" d:DesignHeight="450" x:Class="SourceGit.Views.CheckoutAndFastForward" x:DataType="vm:CheckoutAndFastForward"> @@ -55,18 +56,15 @@ - - - - - + + + + diff --git a/src/Views/CheckoutCommit.axaml b/src/Views/CheckoutCommit.axaml index 3315a861..3b60aa31 100644 --- a/src/Views/CheckoutCommit.axaml +++ b/src/Views/CheckoutCommit.axaml @@ -3,7 +3,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" - xmlns:c="clr-namespace:SourceGit.Converters" + xmlns:v="using:SourceGit.Views" + xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.CheckoutCommit" x:DataType="vm:CheckoutCommit"> @@ -35,18 +36,15 @@ - - - - - + + + + - - - - - - + + + + + + + + + + + + + + + + + ToolTip.Tip="{Binding OverrideTip, Mode=OneWay}"/> + + + + + + diff --git a/src/Views/DealWithLocalChangesMethod.axaml.cs b/src/Views/DealWithLocalChangesMethod.axaml.cs new file mode 100644 index 00000000..bd12c1de --- /dev/null +++ b/src/Views/DealWithLocalChangesMethod.axaml.cs @@ -0,0 +1,56 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace SourceGit.Views +{ + public partial class DealWithLocalChangesMethod : UserControl + { + public static readonly StyledProperty MethodProperty = + AvaloniaProperty.Register(nameof(Method), Models.DealWithLocalChanges.DoNothing); + + public Models.DealWithLocalChanges Method + { + get => GetValue(MethodProperty); + set => SetValue(MethodProperty, value); + } + + public DealWithLocalChangesMethod() + { + InitializeComponent(); + UpdateRadioButtons(); + } + + private void OnRadioButtonClicked(object sender, RoutedEventArgs e) + { + if (sender is RadioButton { Tag: Models.DealWithLocalChanges way }) + { + Method = way; + UpdateRadioButtons(); + e.Handled = true; + } + } + + private void UpdateRadioButtons() + { + switch (Method) + { + case Models.DealWithLocalChanges.DoNothing: + RadioDoNothing.IsChecked = true; + RadioStashAndReapply.IsChecked = false; + RadioDiscard.IsChecked = false; + break; + case Models.DealWithLocalChanges.StashAndReapply: + RadioDoNothing.IsChecked = false; + RadioStashAndReapply.IsChecked = true; + RadioDiscard.IsChecked = false; + break; + default: + RadioDoNothing.IsChecked = false; + RadioStashAndReapply.IsChecked = false; + RadioDiscard.IsChecked = true; + break; + } + } + } +} diff --git a/src/Views/Pull.axaml b/src/Views/Pull.axaml index b09f280f..19246f86 100644 --- a/src/Views/Pull.axaml +++ b/src/Views/Pull.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" + xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="SourceGit.Views.Pull" x:DataType="vm:Pull"> @@ -78,18 +79,15 @@ - - - - - + + + + Date: Mon, 23 Mar 2026 04:17:43 +0000 Subject: [PATCH 32/64] doc: Update translation status and sort locale files --- TRANSLATION.md | 21 +++++++++++++-------- src/Resources/Locales/de_DE.axaml | 6 ------ src/Resources/Locales/es_ES.axaml | 6 ------ src/Resources/Locales/fr_FR.axaml | 6 ------ src/Resources/Locales/id_ID.axaml | 6 ------ src/Resources/Locales/it_IT.axaml | 6 ------ src/Resources/Locales/ja_JP.axaml | 6 ------ src/Resources/Locales/ko_KR.axaml | 6 ------ src/Resources/Locales/pt_BR.axaml | 6 ------ src/Resources/Locales/ru_RU.axaml | 6 ------ src/Resources/Locales/ta_IN.axaml | 6 ------ src/Resources/Locales/uk_UA.axaml | 6 ------ src/Resources/Locales/zh_CN.axaml | 6 ------ src/Resources/Locales/zh_TW.axaml | 6 ------ 14 files changed, 13 insertions(+), 86 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index fd1e3672..61cf10e0 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -42,7 +42,7 @@ This document shows the translation status of each locale file in the repository
-### ![fr__FR](https://img.shields.io/badge/fr__FR-92.09%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-92.06%25-yellow)
Missing keys in fr_FR.axaml @@ -127,7 +127,7 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-90.03%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-89.90%25-yellow)
Missing keys in id_ID.axaml @@ -160,6 +160,7 @@ This document shows the translation status of each locale file in the repository - Text.Configure.Git.ConventionalTypesOverride - Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfirmEmptyCommit.StageSelectedThenCommit +- Text.DealWithLocalChanges.DoNothing - Text.DropHead - Text.DropHead.Commit - Text.DropHead.NewHead @@ -232,7 +233,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-97.64%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-97.63%25-yellow)
Missing keys in it_IT.axaml @@ -263,7 +264,7 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-98.66%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-98.56%25-yellow)
Missing keys in ja_JP.axaml @@ -274,6 +275,7 @@ This document shows the translation status of each locale file in the repository - Text.CommandPalette.RepositoryActions - Text.CommandPalette.RevisionFiles - Text.ConfirmEmptyCommit.StageSelectedThenCommit +- Text.DealWithLocalChanges.DoNothing - Text.Hotkeys.Repo.CreateBranch - Text.Init.CommandTip - Text.Init.ErrorMessageTip @@ -284,7 +286,7 @@ This document shows the translation status of each locale file in the repository
-### ![ko__KR](https://img.shields.io/badge/ko__KR-90.34%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-90.21%25-yellow)
Missing keys in ko_KR.axaml @@ -315,6 +317,7 @@ This document shows the translation status of each locale file in the repository - Text.Configure.Git.ConventionalTypesOverride - Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfirmEmptyCommit.StageSelectedThenCommit +- Text.DealWithLocalChanges.DoNothing - Text.EditBranchDescription - Text.EditBranchDescription.Target - Text.FileCM.CustomAction @@ -386,7 +389,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-68.35%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-68.25%25-red)
Missing keys in pt_BR.axaml @@ -704,7 +707,7 @@ This document shows the translation status of each locale file in the repository ### ![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) +### ![ta__IN](https://img.shields.io/badge/ta__IN-70.41%25-red)
Missing keys in ta_IN.axaml @@ -817,6 +820,7 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmRestart.Title - Text.ConfirmRestart.Message - Text.CreateBranch.OverwriteExisting +- Text.DealWithLocalChanges.DoNothing - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path @@ -998,7 +1002,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-71.43%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-71.24%25-red)
Missing keys in uk_UA.axaml @@ -1107,6 +1111,7 @@ This document shows the translation status of each locale file in the repository - Text.ConfirmRestart.Title - Text.ConfirmRestart.Message - Text.CreateBranch.OverwriteExisting +- Text.DealWithLocalChanges.DoNothing - Text.DeinitSubmodule - Text.DeinitSubmodule.Force - Text.DeinitSubmodule.Path diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 67bcc363..0fa1e680 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -113,8 +113,6 @@ Commit: Warnung: Durch Auschecken eines Commits wird dein HEAD losgelöst (detached)! Lokale Änderungen: - Verwerfen - Stashen & wieder anwenden Branch: Dein aktueller HEAD enthält Commit(s) ohne Verbindung zu einem Branch / Tag. Möchtest du trotzdem fortfahren? Die folgenden Submodule müssen aktualisiert werden:{0}Möchtest du sie aktualisieren? @@ -298,8 +296,6 @@ $1, $2, … Werte der Eingabe-Steuerelemente Basierend auf: Erstellten Branch auschecken Lokale Änderungen: - Verwerfen - Stashen & wieder anwenden Neuer Branch-Name: Einen Branch-Namen eingeben Lokalen Branch erstellen @@ -683,8 +679,6 @@ $1, $2, … Werte der Eingabe-Steuerelemente Remote-Branch: Lokaler Branch: Lokale Änderungen: - Verwerfen - Stashen & wieder anwenden Remote: Pull (Fetch & Merge) Rebase anstatt Merge verwenden diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 87b8b75b..2867f5f4 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -113,8 +113,6 @@ Commit: Advertencia: Al hacer un checkout de commit, tu Head se separará Cambios Locales: - Descartar - Stash & Reaplicar Rama: ¡Tu HEAD actual contiene commit(s) que no están conectados a ningunas ramas/etiquetas! ¿Quieres continuar? Los siguientes submódulos necesitan ser actualizados:{0} ¿Quieres actualizarlos? @@ -304,8 +302,6 @@ Basado En: Checkout de la rama creada Cambios Locales: - Descartar - Stash & Reaplicar Nombre de la Nueva Rama: Introduzca el nombre de la rama. Crear Rama Local @@ -693,8 +689,6 @@ Rama Remota: En: Cambios Locales: - Descartar - Stash & Reaplicar Remoto: Pull (Fetch & Merge) Usar rebase en lugar de merge diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 50829885..5a4e0b81 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -104,8 +104,6 @@ Commit : Avertissement: une récupération vers un commit aboutiera vers un HEAD détaché Changements locaux : - Annuler - Mettre en stash et réappliquer Branche : Votre HEAD actuel contient un ou plusieurs commits non connectés à une branche/tag ! Voulez-vous continuer ? Récupérer & Fast-Forward @@ -285,8 +283,6 @@ Basé sur : Récupérer la branche créée Changements locaux : - Rejeter - Stash & Réappliquer Nom de la nouvelle branche : Entrez le nom de la branche. Créer une branche locale @@ -636,8 +632,6 @@ Branche distante : Dans : Changements locaux : - Rejeter - Stash & Réappliquer Dépôt distant : Pull (Fetch & Merge) Utiliser rebase au lieu de merge diff --git a/src/Resources/Locales/id_ID.axaml b/src/Resources/Locales/id_ID.axaml index 1b4edd3a..3d383f86 100644 --- a/src/Resources/Locales/id_ID.axaml +++ b/src/Resources/Locales/id_ID.axaml @@ -100,8 +100,6 @@ Commit: Peringatan: Dengan melakukan checkout commit, Head akan terlepas Perubahan Lokal: - Buang - Stash & Terapkan Ulang Branch: HEAD saat ini mengandung commit yang tidak terhubung ke branch/tag manapun! Lanjutkan? Checkout & Fast-Forward @@ -270,8 +268,6 @@ Berdasarkan: Checkout branch yang dibuat Perubahan Lokal: - Buang - Stash & Terapkan Ulang Nama Branch Baru: Masukkan nama branch. Buat Branch Lokal @@ -610,8 +606,6 @@ Remote Branch: Ke: Perubahan Lokal: - Buang - Stash & Terapkan Ulang Remote: Pull (Fetch & Merge) Gunakan rebase alih-alih merge diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index 9b36517a..5ef74516 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -112,8 +112,6 @@ Commit: Avviso: Effettuando un checkout del commit, la tua HEAD sarà separata Modifiche Locali: - Scarta - Stasha e Ripristina Branch: Il tuo HEAD attuale contiene commit non connessi ad alcun branch/tag! Sicuro di voler continuare? I seguenti sottomoduli devono essere aggiornati:{0}Vuoi aggiornarli? @@ -297,8 +295,6 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Basato Su: Checkout del Branch Creato Modifiche Locali: - Scarta - Stasha e Ripristina Nome Nuovo Branch: Inserisci il nome del branch. Crea Branch Locale @@ -679,8 +675,6 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Branch Remoto: In: Modifiche Locali: - Scarta - Stasha e Riapplica Remoto: Scarica (Recupera e Unisci) Riallineare anziché unire diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index 9b301143..25dd808f 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -113,8 +113,6 @@ コミット: 警告: コミットをチェックアウトすると、HEAD が切断されます ローカルの変更: - 破棄 - スタッシュして再適用 ブランチ: 現在の HEAD には、どのブランチやタグにも繋がっていないコミットが含まれています!それでも続行しますか? これらのサブモジュールを更新する必要があります:{0}更新しますか? @@ -299,8 +297,6 @@ 派生元: 作成したブランチにチェックアウト ローカルの変更: - 破棄 - スタッシュして再適用 新しいブランチ名: ブランチの名前を入力 ローカルブランチを作成 @@ -685,8 +681,6 @@ リモートブランチ: プル先: ローカルの変更: - 破棄 - スタッシュして再適用 リモート: プル (フェッチ & マージ) マージではなくリベースを使用 diff --git a/src/Resources/Locales/ko_KR.axaml b/src/Resources/Locales/ko_KR.axaml index 3d541dca..c381f223 100644 --- a/src/Resources/Locales/ko_KR.axaml +++ b/src/Resources/Locales/ko_KR.axaml @@ -97,8 +97,6 @@ 커밋: 경고: 커밋 체크아웃을 하면, HEAD가 분리됩니다(detached) 로컬 변경 사항: - 폐기 - 스태시 & 재적용 브랜치: 현재 HEAD에 브랜치/태그에 연결되지 않은 커밋이 있습니다! 계속하시겠습니까? 체크아웃 & Fast-Forward @@ -269,8 +267,6 @@ 기준: 생성된 브랜치로 체크아웃 로컬 변경 사항: - 폐기 - 스태시 & 재적용 새 브랜치 이름: 브랜치 이름을 입력하세요. 로컬 브랜치 생성 @@ -611,8 +607,6 @@ 원격 브랜치: 대상: 로컬 변경 사항: - 폐기 - 스태시 & 재적용 원격: Pull (Fetch & 병합) 병합 대신 리베이스 사용 diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 6c738cc0..bba18cea 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -100,8 +100,6 @@ Commit: Aviso: Ao fazer o checkout de um commit, seu Head ficará desanexado Alterações Locais: - Descartar - Stash & Reaplicar Branch: Cherry-Pick Adicionar origem à mensagem de commit @@ -201,8 +199,6 @@ Baseado Em: Checar o branch criado Alterações Locais: - Descartar - Guardar & Reaplicar Nome do Novo Branch: Insira o nome do branch. Criar Branch Local @@ -479,8 +475,6 @@ Branch Remoto: Para: Alterações Locais: - Descartar - Guardar & Reaplicar Remoto: Puxar (Buscar & Mesclar) Usar rebase em vez de merge diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index d642c4ef..cd78046e 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -114,8 +114,6 @@ Ревизия: Предупреждение: После переключения ревизии ваша Голова (HEAD) будет отсоединена Локальные изменения: - Отклонить - Отложить и применить повторно Ветка: Ваша текущая ГОЛОВА содержит ревизию(и), не связанные ни с к какими ветками или метками! Вы хотите продолжить? Подмодулям требуется обновление:{0}Обновить их? @@ -305,8 +303,6 @@ Основан на: Переключиться на созданную ветку Локальные изменения: - Отклонить - Отложить и применить повторно Имя новой ветки: Введите имя ветки. Создать локальную ветку @@ -696,8 +692,6 @@ Ветка внешнего репозитория: В: Локальные изменения: - Отклонить - Отложить и применить повторно Внешний репозиторий: Загрузить (Получить и слить) Использовать перемещение вместо слияния diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index 0e607ab3..94da338d 100644 --- a/src/Resources/Locales/ta_IN.axaml +++ b/src/Resources/Locales/ta_IN.axaml @@ -71,8 +71,6 @@ உறுதிமொழி: முன்னறிவிப்பு: ஒரு உறுதிமொழி சரிபார்பதன் மூலம், உங்கள் தலை பிரிக்கப்படும் உள்ளக மாற்றங்கள்: - நிராகரி - பதுக்கிவை & மீண்டும் இடு கிளை: கனி பறி உறுதிமொழி செய்திக்கு மூலத்தைச் சேர் @@ -182,8 +180,6 @@ இதன் அடிப்படையில்: உருவாக்கப்பட்ட கிளையைப் சரிபார் உள்ளக மாற்றங்கள்: - நிராகரி - பதுக்கிவை & மீண்டும் இடு புதிய கிளை பெயர்: கிளை பெயரை உள்ளிடவும். உள்ளக கிளையை உருவாக்கு @@ -482,8 +478,6 @@ தொலை கிளை: இதனுள்: உள்ளக மாற்றங்கள்: - நிராகரி - பதுக்கிவை & மீண்டும் இடு தொலை: இழு (எடுத்து ஒன்றிணை) ஒன்றிணை என்பதற்குப் பதிலாக மறுதளத்தைப் பயன்படுத்து diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml index af0d82d8..b0bcf3a4 100644 --- a/src/Resources/Locales/uk_UA.axaml +++ b/src/Resources/Locales/uk_UA.axaml @@ -71,8 +71,6 @@ Коміт: Попередження: Перехід на коміт призведе до стану "від'єднаний HEAD" Локальні зміни: - Скасувати - Сховати та Застосувати Гілка: Cherry-pick Додати джерело до повідомлення коміту @@ -186,8 +184,6 @@ На основі: Перейти на створену гілку Локальні зміни: - Скасувати - Сховати та Застосувати Назва нової гілки: Введіть назву гілки. Створити локальну гілку @@ -486,8 +482,6 @@ Віддалена гілка: В: Локальні зміни: - Скасувати - Сховати та Застосувати Віддалене сховище: Pull (Fetch & Merge) Використовувати rebase замість merge diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 1f0041da..69821511 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -114,8 +114,6 @@ 提交 : 注意:执行该操作后,当前HEAD会变为游离(detached)状态! 未提交更改 : - 丢弃更改 - 贮藏并自动恢复 目标分支 : 您当前游离的HEAD包含未被任何分支及标签引用的提交!是否继续? 以下子模块需要更新:{0}是否立即更新? @@ -305,8 +303,6 @@ 新分支基于 : 完成后切换到新分支 未提交更改 : - 丢弃更改 - 贮藏并自动恢复 新分支名 : 填写分支名称。 创建本地分支 @@ -696,8 +692,6 @@ 拉取分支 : 本地分支 : 未提交更改 : - 丢弃更改 - 贮藏并自动恢复 远程 : 拉回(拉取并合并) 使用变基方式合并分支 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index b959405b..0ff1ba8c 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -114,8 +114,6 @@ 提交: 注意: 執行該操作後,目前 HEAD 會變為分離 (detached) 狀態! 未提交變更: - 捨棄變更 - 擱置變更並自動復原 目標分支: 您目前的分離的 HEAD 包含與任何分支/標籤無關的提交! 您要繼續嗎? 以下子模組需要更新: {0},您要立即更新嗎? @@ -305,8 +303,6 @@ 新分支基於: 完成後切換到新分支 未提交變更: - 捨棄變更 - 擱置變更並自動復原 新分支名稱: 輸入分支名稱。 建立本機分支 @@ -696,8 +692,6 @@ 拉取分支: 本機分支: 未提交變更: - 捨棄變更 - 擱置變更並自動復原 遠端: 拉取 (提取並合併) 使用重定基底 (rebase) 合併分支 From 548ac66cc17a03002b00e2dd0eb4e1e22595c141 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 23 Mar 2026 12:25:41 +0800 Subject: [PATCH 33/64] ux: checkout commit popup layout Signed-off-by: leo --- src/Views/CheckoutCommit.axaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Views/CheckoutCommit.axaml b/src/Views/CheckoutCommit.axaml index 3b60aa31..fbd73d67 100644 --- a/src/Views/CheckoutCommit.axaml +++ b/src/Views/CheckoutCommit.axaml @@ -19,12 +19,16 @@ Text="{DynamicResource Text.Checkout.Commit}"/> - + + + + + Date: Mon, 23 Mar 2026 13:37:01 +0800 Subject: [PATCH 34/64] refactor: enable `CanSwitchBranchDirectly` by default Signed-off-by: leo --- src/ViewModels/WorkingCopy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index c45b79e1..215cdcb8 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -42,7 +42,7 @@ namespace SourceGit.ViewModels { get; set; - } = false; + } = true; public InProgressContext InProgressContext { From 9c15cc92e11c7b6d9a7e371ca698109160bc0c38 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 23 Mar 2026 13:59:33 +0800 Subject: [PATCH 35/64] ux: avoid bounds of item in branch tree change Signed-off-by: leo --- src/Views/BranchTree.axaml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Views/BranchTree.axaml b/src/Views/BranchTree.axaml index a627a0d9..11a08e7c 100644 --- a/src/Views/BranchTree.axaml +++ b/src/Views/BranchTree.axaml @@ -127,12 +127,16 @@ - + - - From 5f520932a2c2c7cad01386e5bebe24fd903d47d5 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 23 Mar 2026 14:26:42 +0800 Subject: [PATCH 36/64] ux: hide the row to deal with uncommitted local changes when there are no local changes Signed-off-by: leo --- src/ViewModels/Checkout.cs | 5 +++++ src/ViewModels/CheckoutAndFastForward.cs | 5 +++++ src/ViewModels/CheckoutCommit.cs | 5 +++++ src/ViewModels/Pull.cs | 5 +++++ src/Views/Checkout.axaml | 13 +++++-------- src/Views/CheckoutAndFastForward.axaml | 14 +++++--------- src/Views/CheckoutCommit.axaml | 13 +++++-------- src/Views/Pull.axaml | 16 +++++----------- 8 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index d2fc39b9..9cdbdc16 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -9,6 +9,11 @@ namespace SourceGit.ViewModels get => _branch.Name; } + public bool HasLocalChanges + { + get => _repo.LocalChangesCount > 0; + } + public Models.DealWithLocalChanges DealWithLocalChanges { get; diff --git a/src/ViewModels/CheckoutAndFastForward.cs b/src/ViewModels/CheckoutAndFastForward.cs index c80a9a8a..13d9e922 100644 --- a/src/ViewModels/CheckoutAndFastForward.cs +++ b/src/ViewModels/CheckoutAndFastForward.cs @@ -14,6 +14,11 @@ namespace SourceGit.ViewModels get; } + public bool HasLocalChanges + { + get => _repo.LocalChangesCount > 0; + } + public Models.DealWithLocalChanges DealWithLocalChanges { get; diff --git a/src/ViewModels/CheckoutCommit.cs b/src/ViewModels/CheckoutCommit.cs index c8179c23..9f74985e 100644 --- a/src/ViewModels/CheckoutCommit.cs +++ b/src/ViewModels/CheckoutCommit.cs @@ -9,6 +9,11 @@ namespace SourceGit.ViewModels get; } + public bool HasLocalChanges + { + get => _repo.LocalChangesCount > 0; + } + public Models.DealWithLocalChanges DealWithLocalChanges { get; diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index 4e4fe832..33008bf4 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -38,6 +38,11 @@ namespace SourceGit.ViewModels set => SetProperty(ref _selectedBranch, value, true); } + public bool HasLocalChanges + { + get => _repo.LocalChangesCount > 0; + } + public Models.DealWithLocalChanges DealWithLocalChanges { get; diff --git a/src/Views/Checkout.axaml b/src/Views/Checkout.axaml index 7d3d12b1..3d91c707 100644 --- a/src/Views/Checkout.axaml +++ b/src/Views/Checkout.axaml @@ -19,12 +19,7 @@ Text="{DynamicResource Text.Checkout}"/> - - - - - - + + VerticalAlignment="Top" + IsVisible="{Binding HasLocalChanges, Mode=OneWay}"> + Method="{Binding DealWithLocalChanges, Mode=TwoWay}" + IsVisible="{Binding HasLocalChanges, Mode=OneWay}"/> diff --git a/src/Views/CheckoutAndFastForward.axaml b/src/Views/CheckoutAndFastForward.axaml index 1a4381df..42fbf544 100644 --- a/src/Views/CheckoutAndFastForward.axaml +++ b/src/Views/CheckoutAndFastForward.axaml @@ -19,13 +19,7 @@ Text="{DynamicResource Text.Checkout.WithFastForward}"/> - - - - - - - + + VerticalAlignment="Top" + IsVisible="{Binding HasLocalChanges, Mode=OneWay}"> + Method="{Binding DealWithLocalChanges, Mode=TwoWay}" + IsVisible="{Binding HasLocalChanges, Mode=OneWay}"/> diff --git a/src/Views/CheckoutCommit.axaml b/src/Views/CheckoutCommit.axaml index fbd73d67..90c506a7 100644 --- a/src/Views/CheckoutCommit.axaml +++ b/src/Views/CheckoutCommit.axaml @@ -19,12 +19,7 @@ Text="{DynamicResource Text.Checkout.Commit}"/> - - - - - - + @@ -42,13 +37,15 @@ + VerticalAlignment="Top" + IsVisible="{Binding HasLocalChanges, Mode=OneWay}"> + Method="{Binding DealWithLocalChanges, Mode=TwoWay}" + IsVisible="{Binding HasLocalChanges, Mode=OneWay}"/> - - - - - - - - - + + VerticalAlignment="Top" + IsVisible="{Binding HasLocalChanges, Mode=OneWay}"> + Method="{Binding DealWithLocalChanges, Mode=TwoWay}" + IsVisible="{Binding HasLocalChanges, Mode=OneWay}"/> Date: Mon, 23 Mar 2026 18:00:43 +0800 Subject: [PATCH 37/64] localization: update English translation Signed-off-by: leo --- src/Resources/Locales/en_US.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 2bd5d316..e738e223 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -108,7 +108,7 @@ Checkout Branch Checkout Commit Commit: - Warning: By doing a commit checkout, your Head will be detached + Warning: By doing a commit checkout, HEAD will be detached Local Changes: Branch: Your current HEAD contains commit(s) not connected to any branches/tags! Do you want to continue? From 91b411ea14fa5693d801615f9e93bd6e5e92c4c5 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 24 Mar 2026 18:24:01 +0800 Subject: [PATCH 38/64] refactor: simplify AI assistant architecture with tool-based integration - Replace complex OpenAIService with AIProvider model - Implement tool calling support for AI-generated commit messages - Remove advanced AI configuration UI (prompts, streaming toggle) - Add dedicated AI namespace with ChatTools, Service, and ToolCallsBuilder - Update all view models and views to use new AI architecture - Improve code organization and maintainability Signed-off-by: leo --- src/AI/ChatTools.cs | 92 +++++++++ src/AI/Service.cs | 98 ++++++++++ src/AI/ToolCallsBuilder.cs | 119 ++++++++++++ src/Commands/GenerateCommitMessage.cs | 101 ---------- src/Models/AIProvider.cs | 11 ++ src/Models/OpenAI.cs | 239 ------------------------ src/Resources/Locales/en_US.axaml | 3 - src/SourceGit.csproj | 4 +- src/ViewModels/AIAssistant.cs | 65 +++++-- src/ViewModels/Preferences.cs | 2 +- src/ViewModels/Repository.cs | 4 +- src/Views/AIAssistant.axaml | 7 - src/Views/AIAssistant.axaml.cs | 24 ++- src/Views/CommitMessageToolBox.axaml.cs | 4 +- src/Views/Preferences.axaml | 28 +-- src/Views/Preferences.axaml.cs | 8 +- src/Views/WorkingCopy.axaml.cs | 4 +- 17 files changed, 404 insertions(+), 409 deletions(-) create mode 100644 src/AI/ChatTools.cs create mode 100644 src/AI/Service.cs create mode 100644 src/AI/ToolCallsBuilder.cs delete mode 100644 src/Commands/GenerateCommitMessage.cs create mode 100644 src/Models/AIProvider.cs delete mode 100644 src/Models/OpenAI.cs diff --git a/src/AI/ChatTools.cs b/src/AI/ChatTools.cs new file mode 100644 index 00000000..e4c1cba6 --- /dev/null +++ b/src/AI/ChatTools.cs @@ -0,0 +1,92 @@ +using System; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using OpenAI.Chat; + +namespace SourceGit.AI +{ + public static class ChatTools + { + public static readonly ChatTool Tool_GetDetailChangesInFile = ChatTool.CreateFunctionTool( + nameof(GetDetailChangesInFile), + "Get the detailed changes in the specified file in the specified repository.", + BinaryData.FromBytes(Encoding.UTF8.GetBytes(""" + { + "type": "object", + "properties": { + "repo": { + "type": "string", + "description": "The path to the repository." + }, + "file": { + "type": "string", + "description": "The path to the file." + }, + "originalFile": { + "type": "string", + "description": "The path to the original file when it has been renamed." + } + }, + "required": ["repo", "file"] + } + """)), false); + + public static async Task Process(ChatToolCall call, Action output) + { + using var doc = JsonDocument.Parse(call.FunctionArguments); + + switch (call.FunctionName) + { + case nameof(GetDetailChangesInFile): + { + var hasRepo = doc.RootElement.TryGetProperty("repo", out var repoPath); + var hasFile = doc.RootElement.TryGetProperty("file", out var filePath); + var hasOriginalFile = doc.RootElement.TryGetProperty("originalFile", out var originalFilePath); + if (!hasRepo) + throw new ArgumentException("repo", "The repo argument is required"); + if (!hasFile) + throw new ArgumentException("file", "The file argument is required"); + + output?.Invoke($"Read changes in file: {filePath.GetString()}"); + + var toolResult = await ChatTools.GetDetailChangesInFile( + repoPath.GetString(), + filePath.GetString(), + hasOriginalFile ? originalFilePath.GetString() : string.Empty); + return new ToolChatMessage(call.Id, toolResult); + } + default: + throw new NotSupportedException($"The tool {call.FunctionName} is not supported"); + } + } + + private static async Task GetDetailChangesInFile(string repo, string file, string originalFile) + { + var rs = await new GetDiffContentCommand(repo, file, originalFile).ReadAsync(); + return rs.IsSuccess ? rs.StdOut : string.Empty; + } + + private class GetDiffContentCommand : Commands.Command + { + public GetDiffContentCommand(string repo, string file, string originalFile) + { + WorkingDirectory = repo; + Context = repo; + + var builder = new StringBuilder(); + builder.Append("diff --no-color --no-ext-diff --diff-algorithm=minimal --cached -- "); + if (!string.IsNullOrEmpty(originalFile) && !file.Equals(originalFile, StringComparison.Ordinal)) + builder.Append(originalFile.Quoted()).Append(' '); + builder.Append(file.Quoted()); + + Args = builder.ToString(); + } + + public async Task ReadAsync() + { + return await ReadToEndAsync().ConfigureAwait(false); + } + } + } +} diff --git a/src/AI/Service.cs b/src/AI/Service.cs new file mode 100644 index 00000000..70e29ab6 --- /dev/null +++ b/src/AI/Service.cs @@ -0,0 +1,98 @@ +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using OpenAI; +using OpenAI.Chat; + +namespace SourceGit.AI +{ + public class Service + { + public Service(Models.AIProvider ai) + { + _ai = ai; + } + + public async Task GenerateCommitMessage(string repo, string changeList, Action onUpdate, CancellationToken cancellation) + { + var key = _ai.ReadApiKeyFromEnv ? Environment.GetEnvironmentVariable(_ai.ApiKey) : _ai.ApiKey; + var endPoint = new Uri(_ai.Server); + var credential = new ApiKeyCredential(key); + var client = _ai.Server.Contains("openai.azure.com/", StringComparison.Ordinal) + ? new AzureOpenAIClient(endPoint, credential) + : new OpenAIClient(credential, new() { Endpoint = endPoint }); + + var chatClient = client.GetChatClient(_ai.Model); + var options = new ChatCompletionOptions() { Tools = { ChatTools.Tool_GetDetailChangesInFile } }; + + var userMessageBuilder = new StringBuilder(); + userMessageBuilder + .AppendLine("Generate a commit message (follow the rule of conventional commit message) for given git repository.") + .AppendLine("- Read all given changed files before generating. Do not skip any one file.") + .Append("Reposiory path: ").AppendLine(repo.Quoted()) + .AppendLine("Changed files: ") + .Append(changeList); + + var messages = new List() { new UserChatMessage(userMessageBuilder.ToString()) }; + + do + { + var inProgress = false; + var updates = chatClient.CompleteChatStreamingAsync(messages, options).WithCancellation(cancellation); + var toolCalls = new ToolCallsBuilder(); + var contentBuilder = new StringBuilder(); + + await foreach (var update in updates) + { + foreach (var contentPart in update.ContentUpdate) + contentBuilder.Append(contentPart.Text); + + foreach (var toolCall in update.ToolCallUpdates) + toolCalls.Append(toolCall); + + switch (update.FinishReason) + { + case ChatFinishReason.Stop: + onUpdate?.Invoke(string.Empty); + onUpdate?.Invoke("[Assistant]:"); + onUpdate?.Invoke(contentBuilder.ToString()); + break; + case ChatFinishReason.Length: + throw new Exception("The response was cut off because it reached the maximum length. Consider increasing the max tokens limit."); + case ChatFinishReason.ToolCalls: + { + var calls = toolCalls.Build(); + var assistantMessage = new AssistantChatMessage(calls); + if (contentBuilder.Length > 0) + assistantMessage.Content.Add(ChatMessageContentPart.CreateTextPart(contentBuilder.ToString())); + messages.Add(assistantMessage); + + foreach (var call in calls) + { + var result = await ChatTools.Process(call, onUpdate); + messages.Add(result); + } + + inProgress = true; + break; + } + case ChatFinishReason.ContentFilter: + throw new Exception("Ommitted content due to a content filter flag"); + default: + break; + } + + } + + if (!inProgress) + break; + } while (true); + } + + private readonly Models.AIProvider _ai; + } +} diff --git a/src/AI/ToolCallsBuilder.cs b/src/AI/ToolCallsBuilder.cs new file mode 100644 index 00000000..948e3104 --- /dev/null +++ b/src/AI/ToolCallsBuilder.cs @@ -0,0 +1,119 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using OpenAI.Chat; + +namespace SourceGit.AI +{ + public class ToolCallsBuilder + { + private readonly Dictionary _indexToToolCallId = []; + private readonly Dictionary _indexToFunctionName = []; + private readonly Dictionary> _indexToFunctionArguments = []; + + public void Append(StreamingChatToolCallUpdate toolCallUpdate) + { + if (toolCallUpdate.ToolCallId != null) + { + _indexToToolCallId[toolCallUpdate.Index] = toolCallUpdate.ToolCallId; + } + + if (toolCallUpdate.FunctionName != null) + { + _indexToFunctionName[toolCallUpdate.Index] = toolCallUpdate.FunctionName; + } + + if (toolCallUpdate.FunctionArgumentsUpdate != null && !toolCallUpdate.FunctionArgumentsUpdate.ToMemory().IsEmpty) + { + if (!_indexToFunctionArguments.TryGetValue(toolCallUpdate.Index, out SequenceBuilder argumentsBuilder)) + { + argumentsBuilder = new SequenceBuilder(); + _indexToFunctionArguments[toolCallUpdate.Index] = argumentsBuilder; + } + + argumentsBuilder.Append(toolCallUpdate.FunctionArgumentsUpdate); + } + } + + public IReadOnlyList Build() + { + List toolCalls = []; + + foreach ((int index, string toolCallId) in _indexToToolCallId) + { + ReadOnlySequence sequence = _indexToFunctionArguments[index].Build(); + + ChatToolCall toolCall = ChatToolCall.CreateFunctionToolCall( + id: toolCallId, + functionName: _indexToFunctionName[index], + functionArguments: BinaryData.FromBytes(sequence.ToArray())); + + toolCalls.Add(toolCall); + } + + return toolCalls; + } + } + + public class SequenceBuilder + { + Segment _first; + Segment _last; + + public void Append(ReadOnlyMemory data) + { + if (_first == null) + { + Debug.Assert(_last == null); + _first = new Segment(data); + _last = _first; + } + else + { + _last = _last!.Append(data); + } + } + + public ReadOnlySequence Build() + { + if (_first == null) + { + Debug.Assert(_last == null); + return ReadOnlySequence.Empty; + } + + if (_first == _last) + { + Debug.Assert(_first.Next == null); + return new ReadOnlySequence(_first.Memory); + } + + return new ReadOnlySequence(_first, 0, _last!, _last!.Memory.Length); + } + + private sealed class Segment : ReadOnlySequenceSegment + { + public Segment(ReadOnlyMemory items) : this(items, 0) + { + } + + private Segment(ReadOnlyMemory items, long runningIndex) + { + Debug.Assert(runningIndex >= 0); + Memory = items; + RunningIndex = runningIndex; + } + + public Segment Append(ReadOnlyMemory items) + { + long runningIndex; + checked + { runningIndex = RunningIndex + Memory.Length; } + Segment segment = new(items, runningIndex); + Next = segment; + return segment; + } + } + } +} diff --git a/src/Commands/GenerateCommitMessage.cs b/src/Commands/GenerateCommitMessage.cs deleted file mode 100644 index bbefa34e..00000000 --- a/src/Commands/GenerateCommitMessage.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace SourceGit.Commands -{ - /// - /// A C# version of https://github.com/anjerodev/commitollama - /// - public class GenerateCommitMessage - { - public class GetDiffContent : Command - { - public GetDiffContent(string repo, Models.DiffOption opt) - { - WorkingDirectory = repo; - Context = repo; - Args = $"diff --no-color --no-ext-diff --diff-algorithm=minimal {opt}"; - } - - public async Task ReadAsync() - { - return await ReadToEndAsync().ConfigureAwait(false); - } - } - - public GenerateCommitMessage(Models.OpenAIService service, string repo, List changes, CancellationToken cancelToken, Action onResponse) - { - _service = service; - _repo = repo; - _changes = changes; - _cancelToken = cancelToken; - _onResponse = onResponse; - } - - public async Task ExecAsync() - { - try - { - _onResponse?.Invoke("Waiting for pre-file analyzing to completed...\n\n"); - - var responseBuilder = new StringBuilder(); - var summaryBuilder = new StringBuilder(); - foreach (var change in _changes) - { - if (_cancelToken.IsCancellationRequested) - return; - - responseBuilder.Append("- "); - summaryBuilder.Append("- "); - - var rs = await new GetDiffContent(_repo, new Models.DiffOption(change, false)).ReadAsync(); - if (rs.IsSuccess) - { - await _service.ChatAsync( - _service.AnalyzeDiffPrompt, - $"Here is the `git diff` output: {rs.StdOut}", - _cancelToken, - update => - { - responseBuilder.Append(update); - summaryBuilder.Append(update); - - _onResponse?.Invoke($"Waiting for pre-file analyzing to completed...\n\n{responseBuilder}"); - }); - } - - responseBuilder.AppendLine(); - summaryBuilder.Append("(file: ").Append(change.Path).AppendLine(")"); - } - - if (_cancelToken.IsCancellationRequested) - return; - - var responseBody = responseBuilder.ToString(); - var subjectBuilder = new StringBuilder(); - await _service.ChatAsync( - _service.GenerateSubjectPrompt, - $"Here are the summaries changes:\n{summaryBuilder}", - _cancelToken, - update => - { - subjectBuilder.Append(update); - _onResponse?.Invoke($"{subjectBuilder}\n\n{responseBody}"); - }); - } - catch (Exception e) - { - App.RaiseException(_repo, $"Failed to generate commit message: {e}"); - } - } - - private Models.OpenAIService _service; - private string _repo; - private List _changes; - private CancellationToken _cancelToken; - private Action _onResponse; - } -} diff --git a/src/Models/AIProvider.cs b/src/Models/AIProvider.cs new file mode 100644 index 00000000..1a39e8bd --- /dev/null +++ b/src/Models/AIProvider.cs @@ -0,0 +1,11 @@ +namespace SourceGit.Models +{ + public class AIProvider + { + public string Name { get; set; } + public string Server { get; set; } + public string Model { get; set; } + public string ApiKey { get; set; } + public bool ReadApiKeyFromEnv { get; set; } + } +} diff --git a/src/Models/OpenAI.cs b/src/Models/OpenAI.cs deleted file mode 100644 index c38eb674..00000000 --- a/src/Models/OpenAI.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using CommunityToolkit.Mvvm.ComponentModel; -using OpenAI; -using OpenAI.Chat; - -namespace SourceGit.Models -{ - public partial class OpenAIResponse - { - public OpenAIResponse(Action onUpdate) - { - _onUpdate = onUpdate; - } - - public void Append(string text) - { - var buffer = text; - - if (_thinkTail.Length > 0) - { - _thinkTail.Append(buffer); - buffer = _thinkTail.ToString(); - _thinkTail.Clear(); - } - - buffer = REG_COT().Replace(buffer, ""); - - var startIdx = buffer.IndexOf('<'); - if (startIdx >= 0) - { - if (startIdx > 0) - OnReceive(buffer.Substring(0, startIdx)); - - var endIdx = buffer.IndexOf('>', startIdx + 1); - if (endIdx <= startIdx) - { - if (buffer.Length - startIdx <= 15) - _thinkTail.Append(buffer.AsSpan(startIdx)); - else - OnReceive(buffer.Substring(startIdx)); - } - else if (endIdx < startIdx + 15) - { - var tag = buffer.Substring(startIdx + 1, endIdx - startIdx - 1); - if (_thinkTags.Contains(tag)) - _thinkTail.Append(buffer.AsSpan(startIdx)); - else - OnReceive(buffer.Substring(startIdx)); - } - else - { - OnReceive(buffer.Substring(startIdx)); - } - } - else - { - OnReceive(buffer); - } - } - - public void End() - { - if (_thinkTail.Length > 0) - { - OnReceive(_thinkTail.ToString()); - _thinkTail.Clear(); - } - } - - private void OnReceive(string text) - { - if (!_hasTrimmedStart) - { - text = text.TrimStart(); - if (string.IsNullOrEmpty(text)) - return; - - _hasTrimmedStart = true; - } - - _onUpdate?.Invoke(text); - } - - [GeneratedRegex(@"<(think|thought|thinking|thought_chain)>.*?", RegexOptions.Singleline)] - private static partial Regex REG_COT(); - - private Action _onUpdate = null; - private StringBuilder _thinkTail = new StringBuilder(); - private HashSet _thinkTags = ["think", "thought", "thinking", "thought_chain"]; - private bool _hasTrimmedStart = false; - } - - public class OpenAIService : ObservableObject - { - public string Name - { - get => _name; - set => SetProperty(ref _name, value); - } - - public string Server - { - get => _server; - set => SetProperty(ref _server, value); - } - - public string ApiKey - { - get => _apiKey; - set => SetProperty(ref _apiKey, value); - } - - public bool ReadApiKeyFromEnv - { - get => _readApiKeyFromEnv; - set => SetProperty(ref _readApiKeyFromEnv, value); - } - - public string Model - { - get => _model; - set => SetProperty(ref _model, value); - } - - public bool Streaming - { - get => _streaming; - set => SetProperty(ref _streaming, value); - } - - public string AnalyzeDiffPrompt - { - get => _analyzeDiffPrompt; - set => SetProperty(ref _analyzeDiffPrompt, value); - } - - public string GenerateSubjectPrompt - { - get => _generateSubjectPrompt; - set => SetProperty(ref _generateSubjectPrompt, value); - } - - public OpenAIService() - { - AnalyzeDiffPrompt = """ - You are an expert developer specialist in creating commits. - Provide a super concise one sentence overall changes summary of the user `git diff` output following strictly the next rules: - - Do not use any code snippets, imports, file routes or bullets points. - - Do not mention the route of file that has been change. - - Write clear, concise, and descriptive messages that explain the MAIN GOAL made of the changes. - - Use the present tense and active voice in the message, for example, "Fix bug" instead of "Fixed bug.". - - Use the imperative mood, which gives the message a sense of command, e.g. "Add feature" instead of "Added feature". - - Avoid using general terms like "update" or "change", be specific about what was updated or changed. - - Avoid using terms like "The main goal of", just output directly the summary in plain text - """; - - GenerateSubjectPrompt = """ - You are an expert developer specialist in creating commits messages. - Your only goal is to retrieve a single commit message. - Based on the provided user changes, combine them in ONE SINGLE commit message retrieving the global idea, following strictly the next rules: - - Assign the commit {type} according to the next conditions: - feat: Only when adding a new feature. - fix: When fixing a bug. - docs: When updating documentation. - style: When changing elements styles or design and/or making changes to the code style (formatting, missing semicolons, etc.) without changing the code logic. - test: When adding or updating tests. - chore: When making changes to the build process or auxiliary tools and libraries. - revert: When undoing a previous commit. - refactor: When restructuring code without changing its external behavior, or is any of the other refactor types. - - Do not add any issues numeration, explain your output nor introduce your answer. - - Output directly only one commit message in plain text with the next format: {type}: {commit_message}. - - Be as concise as possible, keep the message under 50 characters. - """; - } - - public async Task ChatAsync(string prompt, string question, CancellationToken cancellation, Action onUpdate) - { - var key = _readApiKeyFromEnv ? Environment.GetEnvironmentVariable(_apiKey) : _apiKey; - var endPoint = new Uri(_server); - var credential = new ApiKeyCredential(key); - var client = _server.Contains("openai.azure.com/", StringComparison.Ordinal) - ? new AzureOpenAIClient(endPoint, credential) - : new OpenAIClient(credential, new() { Endpoint = endPoint }); - - var chatClient = client.GetChatClient(_model); - var messages = new List() - { - _model.Equals("o1-mini", StringComparison.Ordinal) ? new UserChatMessage(prompt) : new SystemChatMessage(prompt), - new UserChatMessage(question), - }; - - try - { - var rsp = new OpenAIResponse(onUpdate); - - if (_streaming) - { - var updates = chatClient.CompleteChatStreamingAsync(messages, null, cancellation); - - await foreach (var update in updates) - { - if (update.ContentUpdate.Count > 0) - rsp.Append(update.ContentUpdate[0].Text); - } - } - else - { - var completion = await chatClient.CompleteChatAsync(messages, null, cancellation); - - if (completion.Value.Content.Count > 0) - rsp.Append(completion.Value.Content[0].Text); - } - - rsp.End(); - } - catch - { - if (!cancellation.IsCancellationRequested) - throw; - } - } - - private string _name; - private string _server; - private string _apiKey; - private bool _readApiKeyFromEnv = false; - private string _model; - private bool _streaming = true; - private string _analyzeDiffPrompt; - private string _generateSubjectPrompt; - } -} diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index e738e223..620442d0 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -610,14 +610,11 @@ Yesterday Preferences AI - Analyze Diff Prompt API Key - Generate Subject Prompt Model Name Entered value is the name to load API key from ENV Server - Enable Streaming APPEARANCE Default Font Editor Tab Width diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index f57d7ee3..735d7141 100644 --- a/src/SourceGit.csproj +++ b/src/SourceGit.csproj @@ -33,7 +33,7 @@ - + @@ -60,7 +60,7 @@ - + diff --git a/src/ViewModels/AIAssistant.cs b/src/ViewModels/AIAssistant.cs index d538ce1b..07d89c20 100644 --- a/src/ViewModels/AIAssistant.cs +++ b/src/ViewModels/AIAssistant.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -22,13 +24,17 @@ namespace SourceGit.ViewModels private set => SetProperty(ref _text, value); } - public AIAssistant(Repository repo, Models.OpenAIService service, List changes) + public AIAssistant(string repo, Models.AIProvider provider, List changes) { _repo = repo; - _service = service; - _changes = changes; + _provider = provider; _cancel = new CancellationTokenSource(); + var builder = new StringBuilder(); + foreach (var c in changes) + SerializeChange(c, builder); + _changeList = builder.ToString(); + Gen(); } @@ -40,16 +46,32 @@ namespace SourceGit.ViewModels Gen(); } - public void Apply() - { - _repo.SetCommitMessage(Text); - } - public void Cancel() { _cancel?.Cancel(); } + private void SerializeChange(Models.Change c, StringBuilder builder) + { + var status = c.Index switch + { + Models.ChangeState.Added => "A", + Models.ChangeState.Modified => "M", + Models.ChangeState.Deleted => "D", + Models.ChangeState.TypeChanged => "T", + Models.ChangeState.Renamed => "R", + Models.ChangeState.Copied => "C", + _ => " ", + }; + + builder.Append(status).Append('\t'); + + if (c.Index == Models.ChangeState.Renamed || c.Index == Models.ChangeState.Copied) + builder.Append(c.OriginalPath).Append(" -> ").Append(c.Path).AppendLine(); + else + builder.Append(c.Path).AppendLine(); + } + private void Gen() { Text = string.Empty; @@ -58,18 +80,31 @@ namespace SourceGit.ViewModels _cancel = new CancellationTokenSource(); Task.Run(async () => { - await new Commands.GenerateCommitMessage(_service, _repo.FullPath, _changes, _cancel.Token, message => + var server = new AI.Service(_provider); + var builder = new StringBuilder(); + builder.AppendLine("Asking AI to generate commit message...").AppendLine(); + Dispatcher.UIThread.Post(() => Text = builder.ToString()); + + try { - Dispatcher.UIThread.Post(() => Text = message); - }).ExecAsync().ConfigureAwait(false); + await server.GenerateCommitMessage(_repo, _changeList, message => + { + builder.AppendLine(message); + Dispatcher.UIThread.Post(() => Text = builder.ToString()); + }, _cancel.Token).ConfigureAwait(false); + } + catch (Exception e) + { + App.RaiseException(_repo, e.Message); + } Dispatcher.UIThread.Post(() => IsGenerating = false); }, _cancel.Token); } - private readonly Repository _repo = null; - private Models.OpenAIService _service = null; - private List _changes = null; + private readonly string _repo = null; + private readonly Models.AIProvider _provider = null; + private readonly string _changeList = null; private CancellationTokenSource _cancel = null; private bool _isGenerating = false; private string _text = string.Empty; diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 95817cc1..520fc560 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -480,7 +480,7 @@ namespace SourceGit.ViewModels set; } = []; - public AvaloniaList OpenAIServices + public AvaloniaList OpenAIServices { get; set; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 4588647c..eecbfc85 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1599,7 +1599,7 @@ namespace SourceGit.ViewModels log.Complete(); } - public List GetPreferredOpenAIServices() + public List GetPreferredOpenAIServices() { var services = Preferences.Instance.OpenAIServices; if (services == null || services.Count == 0) @@ -1609,7 +1609,7 @@ namespace SourceGit.ViewModels return [services[0]]; var preferred = _settings.PreferredOpenAIService; - var all = new List(); + var all = new List(); foreach (var service in services) { if (service.Name.Equals(preferred, StringComparison.Ordinal)) diff --git a/src/Views/AIAssistant.axaml b/src/Views/AIAssistant.axaml index bef13df2..2504ac3b 100644 --- a/src/Views/AIAssistant.axaml +++ b/src/Views/AIAssistant.axaml @@ -51,13 +51,6 @@ -