From 98e5767ff6400f8d196b05e836fd7454bd605855 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 21 Sep 2026 15:56:39 +0800 Subject: [PATCH 01/51] ci: sign macOS package (#1833) Co-authored-by: helmut Signed-off-by: leo --- build/scripts/package.osx-app.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/scripts/package.osx-app.sh b/build/scripts/package.osx-app.sh index 62e9184f66..78c31114f4 100755 --- a/build/scripts/package.osx-app.sh +++ b/build/scripts/package.osx-app.sh @@ -21,5 +21,7 @@ case "$RUNTIME" in esac clang -arch "$ARCH" ../tools/setsid-macos/setsid.c -o SourceGit.app/Contents/MacOS/setsid -mmacosx-version-min=13.0 +codesign --force --deep --sign - SourceGit.app +codesign --verify --deep --strict --verbose=2 SourceGit.app zip "sourcegit_$VERSION.$RUNTIME.zip" -r SourceGit.app From 2e024612de61f7b7338cc8c77240bd67e5b6c1f2 Mon Sep 17 00:00:00 2001 From: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:58:21 +0800 Subject: [PATCH 02/51] fix: `HasPendingPullOrPush` dirty state not updated after checkout (#2734) --- src/ViewModels/Repository.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 732f064814..9957e860bd 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -815,6 +815,7 @@ public void RefreshAfterCheckoutBranch(Models.Branch checkouted) var builder = BuildBranchTree(locals, [], false); LocalBranchTrees = builder.Locals; CurrentBranch = checkouted; + GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasPendingPullOrPush, !checkouted.IsTrackStatusVisible); RefreshCommits(); RefreshWorkingCopyChanges(); From b777993a8da76fb14a4b20ea321e38d733cd9008 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 22 Sep 2026 16:01:37 +0800 Subject: [PATCH 03/51] code_style: remove unnecessary converters Signed-off-by: leo --- src/Converters/IntConverters.cs | 6 ------ src/Views/LauncherPage.axaml | 6 +++--- src/Views/Repository.axaml | 14 +++++--------- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index 566952536b..4d6c723a8d 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -14,12 +14,6 @@ public static class IntConverters public static readonly FuncValueConverter IsZero = new(v => v == 0); - public static readonly FuncValueConverter IsOne = - new(v => v == 1); - - public static readonly FuncValueConverter IsNotOne = - new(v => v != 1); - public static readonly FuncValueConverter ToTreeMargin = new(v => new Thickness(v * 16, 0, 0, 0)); diff --git a/src/Views/LauncherPage.axaml b/src/Views/LauncherPage.axaml index 92049cca3f..08107bfa90 100644 --- a/src/Views/LauncherPage.axaml +++ b/src/Views/LauncherPage.axaml @@ -43,7 +43,7 @@ - + @@ -51,11 +51,11 @@ - + - + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index e64f7d8912..b991642dc9 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -227,7 +227,7 @@ Content="{DynamicResource Text.Repository.Resolve}" Padding="8,2" Margin="0,0,8,0" Click="OnResolveInProgress" - IsVisible="{Binding SelectedViewIndex, Converter={x:Static c:IntConverters.IsNotOne}}"/> + IsVisible="{Binding !IsWorkingCopyVisible}"/> From c7db6b3e513536830d4faf3103e3503ef1af5220 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 23 Sep 2026 10:51:22 +0000 Subject: [PATCH 06/51] doc: Update translation status and sort locale files --- TRANSLATION.md | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 67becad004..ea6fb2a824 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,12 +6,13 @@ 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-95.66%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-95.57%25-yellow)
Missing keys in de_DE.axaml - Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL - Text.CommitDetail.Files.Tips - Text.Dashboard @@ -60,12 +61,13 @@ This document shows the translation status of each locale file in the repository
-### ![el__GR](https://img.shields.io/badge/el__GR-95.19%25-yellow) +### ![el__GR](https://img.shields.io/badge/el__GR-95.10%25-yellow)
Missing keys in el_GR.axaml - Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL - Text.CommitDetail.Files.Tips - Text.Dashboard @@ -119,18 +121,19 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-99.72%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.62%25-yellow)
Missing keys in es_ES.axaml +- Text.BranchCM.CheckoutAsWorktree - Text.CommitDetail.Files.Tips - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.ExpandSidebar
-### ![fr__FR](https://img.shields.io/badge/fr__FR-91.42%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-91.33%25-yellow)
Missing keys in fr_FR.axaml @@ -141,6 +144,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.WaitingForCheckoutAnother - Text.Bisect.WaitingForFirstBad - Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL - Text.CheckoutDetached - Text.CheckoutDetached.Target @@ -229,7 +233,7 @@ This document shows the translation status of each locale file in the repository
-### ![he__IL](https://img.shields.io/badge/he__IL-91.42%25-yellow) +### ![he__IL](https://img.shields.io/badge/he__IL-91.33%25-yellow)
Missing keys in he_IL.axaml @@ -240,6 +244,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.WaitingForCheckoutAnother - Text.Bisect.WaitingForFirstBad - Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL - Text.CheckoutDetached - Text.CheckoutDetached.Target @@ -328,12 +333,13 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-95.66%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-95.57%25-yellow)
Missing keys in id_ID.axaml - Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL - Text.CommitDetail.Files.Tips - Text.Dashboard @@ -382,7 +388,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-85.28%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-85.20%25-yellow)
Missing keys in it_IT.axaml @@ -396,6 +402,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.WaitingForCheckoutAnother - Text.Bisect.WaitingForFirstBad - Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchCM.CompareWithSpecial - Text.BranchTree.PushURL - Text.ChangeCM.ResetFileTo @@ -546,18 +553,19 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-99.72%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-99.62%25-yellow)
Missing keys in ja_JP.axaml +- Text.BranchCM.CheckoutAsWorktree - Text.CommitDetail.Files.Tips - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.ExpandSidebar
-### ![ko__KR](https://img.shields.io/badge/ko__KR-92.83%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-92.74%25-yellow)
Missing keys in ko_KR.axaml @@ -565,6 +573,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.WaitingForCheckoutAnother - Text.Bisect.WaitingForFirstBad - Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL - Text.CheckoutDetached - Text.CheckoutDetached.Target @@ -641,7 +650,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-60.19%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-60.13%25-red)
Missing keys in pt_BR.axaml @@ -656,6 +665,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.WaitingForFirstBad - Text.Blame.BlameOnPreviousRevision - Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchCM.CompareWithSpecial - Text.BranchCM.InteractiveRebase.Manually - Text.BranchTree.AheadBehind @@ -1071,12 +1081,13 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-98.02%25-yellow) +### ![ru__RU](https://img.shields.io/badge/ru__RU-97.93%25-yellow)
Missing keys in ru_RU.axaml - Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL - Text.CommitDetail.Files.Tips - Text.Dashboard @@ -1100,7 +1111,7 @@ This document shows the translation status of each locale file in the repository
-### ![ta__IN](https://img.shields.io/badge/ta__IN-61.79%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-61.73%25-red)
Missing keys in ta_IN.axaml @@ -1132,6 +1143,7 @@ This document shows the translation status of each locale file in the repository - Text.Blame.BlameOnPreviousRevision - Text.Blame.IgnoreWhitespace - Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchCM.CompareTwo - Text.BranchCM.CompareWith - Text.BranchCM.CompareWithHead @@ -1513,7 +1525,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-62.55%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-62.49%25-red)
Missing keys in uk_UA.axaml @@ -1545,6 +1557,7 @@ This document shows the translation status of each locale file in the repository - Text.Blame.BlameOnPreviousRevision - Text.Blame.IgnoreWhitespace - Text.Blame.Tips +- Text.BranchCM.CheckoutAsWorktree - Text.BranchCM.CompareTwo - Text.BranchCM.CompareWith - Text.BranchCM.CompareWithHead @@ -1920,11 +1933,12 @@ This document shows the translation status of each locale file in the repository ### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen) -### ![zh__TW](https://img.shields.io/badge/zh__TW-99.72%25-yellow) +### ![zh__TW](https://img.shields.io/badge/zh__TW-99.62%25-yellow)
Missing keys in zh_TW.axaml +- Text.BranchCM.CheckoutAsWorktree - Text.CommitDetail.Files.Tips - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.ExpandSidebar From f7041b9ae828729ca7529bc6b2c3a033ca2ee61b Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 23 Sep 2026 23:16:53 +0800 Subject: [PATCH 07/51] refactor: worktree - `Add Worktree` now only creates worktree based on HEAD - Supports to checkout a remote branch as worktree - Use different popups for `Add Worktree`, `Checkout as Worktree` and `Checkout as Worktree` --- src/Commands/Worktree.cs | 36 ++-- src/Resources/Locales/en_US.axaml | 13 +- src/Resources/Locales/zh_CN.axaml | 8 + src/ViewModels/AddWorktree.cs | 158 ++++-------------- src/ViewModels/CheckoutAsWorktree.cs | 72 ++++++++ .../CheckoutRemoteBranchAsWorktree.cs | 107 ++++++++++++ src/ViewModels/Repository.cs | 12 ++ src/Views/AddWorktree.axaml | 59 +------ src/Views/BranchTree.axaml.cs | 24 +-- src/Views/CheckoutAsWorktree.axaml | 49 ++++++ src/Views/CheckoutAsWorktree.axaml.cs | 41 +++++ .../CheckoutRemoteBranchAsWorktree.axaml | 65 +++++++ .../CheckoutRemoteBranchAsWorktree.axaml.cs | 41 +++++ src/Views/Histories.axaml.cs | 23 +-- 14 files changed, 493 insertions(+), 215 deletions(-) create mode 100644 src/ViewModels/CheckoutAsWorktree.cs create mode 100644 src/ViewModels/CheckoutRemoteBranchAsWorktree.cs create mode 100644 src/Views/CheckoutAsWorktree.axaml create mode 100644 src/Views/CheckoutAsWorktree.axaml.cs create mode 100644 src/Views/CheckoutRemoteBranchAsWorktree.axaml create mode 100644 src/Views/CheckoutRemoteBranchAsWorktree.axaml.cs diff --git a/src/Commands/Worktree.cs b/src/Commands/Worktree.cs index 7b70e2ab49..137bb3f7f1 100644 --- a/src/Commands/Worktree.cs +++ b/src/Commands/Worktree.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using System.Threading.Tasks; @@ -61,20 +62,33 @@ public Worktree(string repo) return worktrees; } - public async Task AddAsync(string fullpath, string name, bool createNew, string tracking) + public async Task AddAsync(string fullpath, string name) + { + if (string.IsNullOrEmpty(name)) + Args = $"worktree add {fullpath.Quoted()}"; + else + Args = $"worktree add -b {name} {fullpath.Quoted()}"; + + return await ExecAsync().ConfigureAwait(false); + } + + public async Task AddWithLocalBranchAsync(string fullpath, string localBranch) + { + Args = $"worktree add -B {localBranch} {fullpath.Quoted()} {localBranch}"; + return await ExecAsync().ConfigureAwait(false); + } + + public async Task AddWithRemoteBranchAsync(string fullpath, string name, string remoteBranch, bool tracking) { var builder = new StringBuilder(1024); builder.Append("worktree add "); - if (!string.IsNullOrEmpty(tracking)) - builder.Append("--track "); - if (!string.IsNullOrEmpty(name)) - builder.Append(createNew ? "-b " : "-B ").Append(name).Append(' '); - builder.Append(fullpath.Quoted()).Append(' '); - - if (!string.IsNullOrEmpty(tracking)) - builder.Append(tracking); - else if (!string.IsNullOrEmpty(name) && !createNew) - builder.Append(name); + + if (tracking) + builder.Append("--track -b ").Append(string.IsNullOrEmpty(name) ? Path.GetFileName(fullpath) : name).Append(' '); + else if (!string.IsNullOrEmpty(name)) + builder.Append("-b ").Append(name).Append(' '); + + builder.Append(fullpath.Quoted()).Append(' ').Append(remoteBranch); Args = builder.ToString(); return await ExecAsync().ConfigureAwait(false); diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 96b262502f..98bbb4b46d 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -12,11 +12,6 @@ Path for this worktree. Relative path is supported. Branch Name: Optional. Default is the destination folder name. - Track Branch: - Tracking remote branch - What to Checkout: - Create New Branch - Existing Branch AI Assistant MODEL RE-GENERATE @@ -120,6 +115,14 @@ The following submodules need to be updated:{0}Do you want to update them? Checkout & Fast-Forward Fast-Forward to: + Checkout Branch as Worktree + Local Branch: + New Branch Name: + Optional. Default is the destination folder name. + Remote Branch: + Tracking this remote branch. + Worktree Path: + Path for this worktree. Relative path is supported. Checkout Branch From Stash New Branch: Stash: diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 79a9778442..2a9b5844b3 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -124,6 +124,14 @@ 以下子模块需要更新:{0}是否立即更新? 检出分支并快进 上游分支 : + 检出分支为新的工作树 + 本地分支 : + 新分支名称 : + 选填。默认使用目标文件夹名称。 + 远程分支 : + 新分支跟踪此远程分支 + 工作树路径 : + 必填。可填写相对路径。 从所选贮藏检出分支 新分支 : 所选贮藏 : diff --git a/src/ViewModels/AddWorktree.cs b/src/ViewModels/AddWorktree.cs index d044c390fe..4d5eb9f1e2 100644 --- a/src/ViewModels/AddWorktree.cs +++ b/src/ViewModels/AddWorktree.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Threading.Tasks; @@ -8,98 +7,35 @@ namespace SourceGit.ViewModels { public class AddWorktree : Popup { - [Required(ErrorMessage = "Worktree path is required!")] [CustomValidation(typeof(AddWorktree), nameof(ValidateWorktreePath))] - public string Path + public string WorktreePath { - get => _path; - set => SetProperty(ref _path, value, true); + get => _worktreePath; + set => SetProperty(ref _worktreePath, value, true); } - public bool CreateNewBranch + [CustomValidation(typeof(AddWorktree), nameof(ValidateBranchName))] + public string BranchName { - get => _createNewBranch; - set - { - if (SetProperty(ref _createNewBranch, value, true)) - { - if (value) - SelectedBranch = null; - else - SelectedBranch = LocalBranches.Count > 0 ? LocalBranches[0] : null; - } - } - } - - public List LocalBranches - { - get; - private set; - } - - public Models.Branch SelectedBranch - { - get => _selectedBranch; - set => SetProperty(ref _selectedBranch, value); - } - - public string NewBranchName - { - get => _newBranchName; - set => SetProperty(ref _newBranchName, value); - } - - public bool SetTrackingBranch - { - get => _setTrackingBranch; - set - { - if (SetProperty(ref _setTrackingBranch, value)) - AutoSelectTrackingBranch(); - } - } - - public List RemoteBranches - { - get; - private set; - } - - public Models.Branch SelectedTrackingBranch - { - get => _selectedTrackingBranch; - set => SetProperty(ref _selectedTrackingBranch, value); + get => _branchName; + set => SetProperty(ref _branchName, value, true); } public AddWorktree(Repository repo) { _repo = repo; - - LocalBranches = new List(); - RemoteBranches = new List(); - foreach (var branch in repo.Branches) - { - if (branch.IsLocal) - { - if (!branch.IsCurrent && !branch.HasWorktree) - LocalBranches.Add(branch); - } - else - { - RemoteBranches.Add(branch); - } - } + _worktreePath = Path.Combine(repo.GetRecommandedWorktreeDir(), "new-worktree"); } public static ValidationResult ValidateWorktreePath(string path, ValidationContext ctx) { if (ctx.ObjectInstance is not AddWorktree creator) - return new ValidationResult("Missing runtime context to create branch!"); + return new ValidationResult("Missing runtime context to add worktree!"); if (string.IsNullOrEmpty(path)) return new ValidationResult("Worktree path is required!"); - var fullPath = System.IO.Path.IsPathRooted(path) ? path : System.IO.Path.Combine(creator._repo.FullPath, path); + var fullPath = Path.IsPathRooted(path) ? path : Path.Combine(creator._repo.FullPath, path); var info = new DirectoryInfo(fullPath); if (info.Exists) { @@ -115,70 +51,42 @@ public static ValidationResult ValidateWorktreePath(string path, ValidationConte return ValidationResult.Success; } - public override async Task Sure() + public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) { - using var lockWatcher = _repo.LockWatcher(); - ProgressDescription = "Adding worktree ..."; - - var branchName = GetBranchName(false); - var tracking = (_createNewBranch && _setTrackingBranch && _selectedTrackingBranch != null) ? _selectedTrackingBranch.FriendlyName : string.Empty; - var log = _repo.CreateLog("Add Worktree"); - - Use(log); - - var succ = await new Commands.Worktree(_repo.FullPath) - .Use(log) - .AddAsync(_path, branchName, _createNewBranch, tracking); - - log.Complete(); - return succ; - } + if (ctx.ObjectInstance is not AddWorktree creator) + return new ValidationResult("Missing runtime context to add worktree!"); - private void AutoSelectTrackingBranch() - { - if (!_setTrackingBranch || RemoteBranches.Count == 0) - return; + var test = !string.IsNullOrEmpty(name) ? name : Path.GetFileName(creator._worktreePath.TrimEnd('/').TrimEnd('\\')); + if (!Models.RefName.IsValidBranchName(test)) + return new ValidationResult($"{test} is not a valid branch name!"); - var name = GetBranchName(true); - if (!string.IsNullOrEmpty(name)) + foreach (var b in creator._repo.Branches) { - var remoteBranch = RemoteBranches.Find(b => b.Name.Equals(name, StringComparison.Ordinal)); - remoteBranch ??= RemoteBranches.Find(b => b.Name.EndsWith("/" + name, StringComparison.Ordinal)); - if (remoteBranch != null) - { - SelectedTrackingBranch = remoteBranch; - return; - } + if (b.IsLocal && b.Name.Equals(test, StringComparison.Ordinal)) + return new ValidationResult($"A branch with name '{test}' already exists!"); } - SelectedTrackingBranch = RemoteBranches[0]; + return ValidationResult.Success; } - private string GetBranchName(bool fallback) + public override async Task Sure() { - do - { - if (!_createNewBranch) - { - if (_selectedBranch != null) - return _selectedBranch.Name; + using var lockWatcher = _repo.LockWatcher(); + ProgressDescription = $"Adding a new worktree ..."; - break; - } + var log = _repo.CreateLog("Add Worktree"); + Use(log); - if (!string.IsNullOrEmpty(_newBranchName)) - return _newBranchName; - } while (false); + var succ = await new Commands.Worktree(_repo.FullPath) + .Use(log) + .AddAsync(_worktreePath, _branchName); - return fallback ? System.IO.Path.GetFileName(_path.TrimEnd('/', '\\')) : string.Empty; + log.Complete(); + return succ; } - private Repository _repo = null; - private string _path = string.Empty; - private bool _createNewBranch = true; - private Models.Branch _selectedBranch = null; - private string _newBranchName = string.Empty; - private bool _setTrackingBranch = false; - private Models.Branch _selectedTrackingBranch = null; + private readonly Repository _repo = null; + private string _worktreePath = string.Empty; + private string _branchName = string.Empty; } } diff --git a/src/ViewModels/CheckoutAsWorktree.cs b/src/ViewModels/CheckoutAsWorktree.cs new file mode 100644 index 0000000000..2963db85f0 --- /dev/null +++ b/src/ViewModels/CheckoutAsWorktree.cs @@ -0,0 +1,72 @@ +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class CheckoutAsWorktree : Popup + { + public Models.Branch Branch + { + get => _branch; + } + + [CustomValidation(typeof(CheckoutAsWorktree), nameof(ValidateWorktreePath))] + public string WorktreePath + { + get => _worktreePath; + set => SetProperty(ref _worktreePath, value, true); + } + + public CheckoutAsWorktree(Repository repo, Models.Branch branch) + { + _repo = repo; + _branch = branch; + _worktreePath = Path.Combine(repo.GetRecommandedWorktreeDir(), branch.Name.Replace('/', '-').Replace('\\', '-')); + } + + public static ValidationResult ValidateWorktreePath(string path, ValidationContext ctx) + { + if (ctx.ObjectInstance is not CheckoutAsWorktree creator) + return new ValidationResult("Missing runtime context to checkout branch as worktree!"); + + if (string.IsNullOrEmpty(path)) + return new ValidationResult("Worktree path is required!"); + + var fullPath = Path.IsPathRooted(path) ? path : Path.Combine(creator._repo.FullPath, path); + var info = new DirectoryInfo(fullPath); + if (info.Exists) + { + var files = info.GetFiles(); + if (files.Length > 0) + return new ValidationResult("Given path is not empty!!!"); + + var folders = info.GetDirectories(); + if (folders.Length > 0) + return new ValidationResult("Given path is not empty!!!"); + } + + return ValidationResult.Success; + } + + public override async Task Sure() + { + using var lockWatcher = _repo.LockWatcher(); + ProgressDescription = $"Checkout branch '{_branch.Name}' as worktree ..."; + + var log = _repo.CreateLog("Add Worktree"); + Use(log); + + var succ = await new Commands.Worktree(_repo.FullPath) + .Use(log) + .AddWithLocalBranchAsync(_worktreePath, _branch.Name); + + log.Complete(); + return succ; + } + + private readonly Repository _repo; + private readonly Models.Branch _branch; + private string _worktreePath; + } +} diff --git a/src/ViewModels/CheckoutRemoteBranchAsWorktree.cs b/src/ViewModels/CheckoutRemoteBranchAsWorktree.cs new file mode 100644 index 0000000000..9ec894ae0e --- /dev/null +++ b/src/ViewModels/CheckoutRemoteBranchAsWorktree.cs @@ -0,0 +1,107 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class CheckoutRemoteBranchAsWorktree : Popup + { + public Models.Branch RemoteBranch + { + get => _remoteBranch; + } + + [CustomValidation(typeof(CheckoutRemoteBranchAsWorktree), nameof(ValidateWorktreePath))] + public string WorktreePath + { + get => _worktreePath; + set => SetProperty(ref _worktreePath, value, true); + } + + [CustomValidation(typeof(CheckoutRemoteBranchAsWorktree), nameof(ValidateBranchName))] + public string BranchName + { + get => _branchName; + set => SetProperty(ref _branchName, value, true); + } + + public bool Tracking + { + get => _tracking; + set => SetProperty(ref _tracking, value); + } + + public CheckoutRemoteBranchAsWorktree(Repository repo, Models.Branch remoteBranch) + { + _repo = repo; + _remoteBranch = remoteBranch; + _worktreePath = Path.Combine(repo.GetRecommandedWorktreeDir(), remoteBranch.Name.Replace('/', '-').Replace('\\', '-')); + _tracking = true; + } + + public static ValidationResult ValidateWorktreePath(string path, ValidationContext ctx) + { + if (ctx.ObjectInstance is not CheckoutRemoteBranchAsWorktree creator) + return new ValidationResult("Missing runtime context to checkout branch as worktree!"); + + if (string.IsNullOrEmpty(path)) + return new ValidationResult("Worktree path is required!"); + + var fullPath = Path.IsPathRooted(path) ? path : Path.Combine(creator._repo.FullPath, path); + var info = new DirectoryInfo(fullPath); + if (info.Exists) + { + var files = info.GetFiles(); + if (files.Length > 0) + return new ValidationResult("Given path is not empty!!!"); + + var folders = info.GetDirectories(); + if (folders.Length > 0) + return new ValidationResult("Given path is not empty!!!"); + } + + return ValidationResult.Success; + } + + public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) + { + if (ctx.ObjectInstance is not CheckoutRemoteBranchAsWorktree creator) + return new ValidationResult("Missing runtime context to checkout branch as worktree!"); + + var test = !string.IsNullOrEmpty(name) ? name : Path.GetFileName(creator._worktreePath.TrimEnd('/').TrimEnd('\\')); + if (!Models.RefName.IsValidBranchName(test)) + return new ValidationResult($"{test} is not a valid branch name!"); + + foreach (var b in creator._repo.Branches) + { + if (b.IsLocal && b.Name.Equals(test, StringComparison.Ordinal)) + return new ValidationResult($"A branch with name '{test}' already exists!"); + } + + return ValidationResult.Success; + } + + public override async Task Sure() + { + using var lockWatcher = _repo.LockWatcher(); + ProgressDescription = $"Checkout branch '{_remoteBranch.FriendlyName}' as worktree ..."; + + var log = _repo.CreateLog("Add Worktree"); + Use(log); + + var succ = await new Commands.Worktree(_repo.FullPath) + .Use(log) + .AddWithRemoteBranchAsync(_worktreePath, _branchName, _remoteBranch.FriendlyName, _tracking); + + log.Complete(); + return succ; + } + + private readonly Repository _repo; + private readonly Models.Branch _remoteBranch; + private string _worktreePath; + private string _branchName; + private bool _tracking; + } +} diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 9957e860bd..3ab5b1cffc 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -606,6 +606,18 @@ public CommandLog CreateLog(string name) return log; } + public string GetRecommandedWorktreeDir() + { + var commonDirFile = Path.Combine(GitDir, "commondir"); + var isWorktree = GitDir.IndexOf("/worktrees/", StringComparison.Ordinal) > 0 && File.Exists(commonDirFile); + var parentFolder = Path.GetFullPath(Path.Combine(FullPath, "..")); + if (isWorktree) + return parentFolder; + + var dirName = $"{Path.GetFileName(FullPath)}-worktrees"; + return Path.Combine(parentFolder, dirName); + } + public void RefreshAll() { RefreshCommits(); diff --git a/src/Views/AddWorktree.axaml b/src/Views/AddWorktree.axaml index fca719e2a8..f0f5060cfe 100644 --- a/src/Views/AddWorktree.axaml +++ b/src/Views/AddWorktree.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.AddWorktree" x:DataType="vm:AddWorktree"> @@ -19,7 +18,7 @@ Text="{DynamicResource Text.AddWorktree}"/> - + + + + + + diff --git a/src/Views/CheckoutAsWorktree.axaml.cs b/src/Views/CheckoutAsWorktree.axaml.cs new file mode 100644 index 0000000000..7a4ad196ae --- /dev/null +++ b/src/Views/CheckoutAsWorktree.axaml.cs @@ -0,0 +1,41 @@ +using System; + +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views +{ + public partial class CheckoutAsWorktree : UserControl + { + public CheckoutAsWorktree() + { + InitializeComponent(); + } + + private async void SelectLocation(object _, RoutedEventArgs e) + { + var toplevel = TopLevel.GetTopLevel(this); + if (toplevel == null) + return; + + var options = new FolderPickerOpenOptions() { AllowMultiple = false }; + try + { + var selected = await toplevel.StorageProvider.OpenFolderPickerAsync(options); + if (selected.Count == 1) + { + var folder = selected[0]; + var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder.Path.ToString(); + TxtLocation.Text = folderPath.TrimEnd('\\', '/'); + } + } + catch (Exception exception) + { + Models.Notification.Send(null, $"Failed to select location: {exception.Message}", true); + } + + e.Handled = true; + } + } +} diff --git a/src/Views/CheckoutRemoteBranchAsWorktree.axaml b/src/Views/CheckoutRemoteBranchAsWorktree.axaml new file mode 100644 index 0000000000..84b68ae3ae --- /dev/null +++ b/src/Views/CheckoutRemoteBranchAsWorktree.axaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/CheckoutRemoteBranchAsWorktree.axaml.cs b/src/Views/CheckoutRemoteBranchAsWorktree.axaml.cs new file mode 100644 index 0000000000..cff9995bbe --- /dev/null +++ b/src/Views/CheckoutRemoteBranchAsWorktree.axaml.cs @@ -0,0 +1,41 @@ +using System; + +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; + +namespace SourceGit.Views +{ + public partial class CheckoutRemoteBranchAsWorktree : UserControl + { + public CheckoutRemoteBranchAsWorktree() + { + InitializeComponent(); + } + + private async void SelectLocation(object _, RoutedEventArgs e) + { + var toplevel = TopLevel.GetTopLevel(this); + if (toplevel == null) + return; + + var options = new FolderPickerOpenOptions() { AllowMultiple = false }; + try + { + var selected = await toplevel.StorageProvider.OpenFolderPickerAsync(options); + if (selected.Count == 1) + { + var folder = selected[0]; + var folderPath = folder is { Path: { IsAbsoluteUri: true } path } ? path.LocalPath : folder.Path.ToString(); + TxtLocation.Text = folderPath.TrimEnd('\\', '/'); + } + } + catch (Exception exception) + { + Models.Notification.Send(null, $"Failed to select location: {exception.Message}", true); + } + + e.Handled = true; + } + } +} diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 10aba4f5f6..846c59d1b1 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -1674,16 +1674,7 @@ private void FillOtherLocalBranchMenu(ContextMenu menu, ViewModels.Repository re checkoutAsWorktree.Click += (_, e) => { if (repo.CanCreatePopup()) - { - var worktreeFolder = $"{System.IO.Path.GetFileName(repo.FullPath)}-worktrees"; - var recommandName = branch.Name.Replace('/', '-').Replace('\\', '-'); - var recommandPath = System.IO.Path.GetFullPath(System.IO.Path.Combine(repo.FullPath, "..", worktreeFolder, recommandName)); - var addWorktree = new ViewModels.AddWorktree(repo); - addWorktree.Path = recommandPath; - addWorktree.CreateNewBranch = false; - addWorktree.SelectedBranch = branch; - repo.ShowPopup(addWorktree); - } + repo.ShowPopup(new ViewModels.CheckoutAsWorktree(repo, branch)); e.Handled = true; }; @@ -1826,6 +1817,7 @@ private void FillRemoteBranchMenu(ContextMenu menu, ViewModels.Repository repo, var checkout = new MenuItem(); checkout.Header = App.Text("BranchCM.Checkout", name); checkout.Icon = this.CreateMenuIcon("Icons.Check"); + checkout.IsEnabled = !repo.IsBare; checkout.Click += async (_, e) => { await repo.CheckoutBranchAsync(branch); @@ -1833,6 +1825,17 @@ private void FillRemoteBranchMenu(ContextMenu menu, ViewModels.Repository repo, }; submenu.Items.Add(checkout); + var checkoutAsWorktree = new MenuItem(); + checkoutAsWorktree.Header = App.Text("BranchCM.CheckoutAsWorktree", name); + checkoutAsWorktree.Icon = this.CreateMenuIcon("Icons.Worktree.Add"); + checkoutAsWorktree.Click += (_, e) => + { + if (repo.CanCreatePopup()) + repo.ShowPopup(new ViewModels.CheckoutRemoteBranchAsWorktree(repo, branch)); + e.Handled = true; + }; + submenu.Items.Add(checkoutAsWorktree); + var merge = new MenuItem(); merge.Header = App.Text("BranchCM.Merge", name, current.Name); merge.Icon = this.CreateMenuIcon("Icons.Merge"); From 8be9a66038518e366f8306053173819bc79002e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 23 Sep 2026 15:17:17 +0000 Subject: [PATCH 08/51] doc: Update translation status and sort locale files --- TRANSLATION.md | 140 +++++++++++++++++++++++++++--- src/Resources/Locales/de_DE.axaml | 5 -- src/Resources/Locales/el_GR.axaml | 5 -- src/Resources/Locales/es_ES.axaml | 5 -- src/Resources/Locales/fr_FR.axaml | 5 -- src/Resources/Locales/he_IL.axaml | 5 -- src/Resources/Locales/id_ID.axaml | 5 -- src/Resources/Locales/it_IT.axaml | 5 -- src/Resources/Locales/ja_JP.axaml | 5 -- src/Resources/Locales/ko_KR.axaml | 5 -- src/Resources/Locales/pt_BR.axaml | 5 -- src/Resources/Locales/ru_RU.axaml | 5 -- src/Resources/Locales/ta_IN.axaml | 5 -- src/Resources/Locales/uk_UA.axaml | 5 -- src/Resources/Locales/zh_CN.axaml | 5 -- src/Resources/Locales/zh_TW.axaml | 5 -- 16 files changed, 126 insertions(+), 89 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index ea6fb2a824..13ed7678f6 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-95.57%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-94.83%25-yellow)
Missing keys in de_DE.axaml @@ -14,6 +14,14 @@ This document shows the translation status of each locale file in the repository - Text.Blame.Tips - Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CommitDetail.Files.Tips - Text.Dashboard - Text.Diff.Binary.ViewContentOfNew @@ -61,7 +69,7 @@ This document shows the translation status of each locale file in the repository
-### ![el__GR](https://img.shields.io/badge/el__GR-95.10%25-yellow) +### ![el__GR](https://img.shields.io/badge/el__GR-94.36%25-yellow)
Missing keys in el_GR.axaml @@ -69,6 +77,14 @@ This document shows the translation status of each locale file in the repository - Text.Blame.Tips - Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CommitDetail.Files.Tips - Text.Dashboard - Text.Diff.Binary.ViewContentOfNew @@ -121,19 +137,27 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-99.62%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-98.87%25-yellow)
Missing keys in es_ES.axaml - Text.BranchCM.CheckoutAsWorktree +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CommitDetail.Files.Tips - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.ExpandSidebar
-### ![fr__FR](https://img.shields.io/badge/fr__FR-91.33%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-90.60%25-yellow)
Missing keys in fr_FR.axaml @@ -146,6 +170,14 @@ This document shows the translation status of each locale file in the repository - Text.Blame.Tips - Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CheckoutDetached - Text.CheckoutDetached.Target - Text.CheckoutDetached.Warning @@ -233,7 +265,7 @@ This document shows the translation status of each locale file in the repository
-### ![he__IL](https://img.shields.io/badge/he__IL-91.33%25-yellow) +### ![he__IL](https://img.shields.io/badge/he__IL-90.60%25-yellow)
Missing keys in he_IL.axaml @@ -246,6 +278,14 @@ This document shows the translation status of each locale file in the repository - Text.Blame.Tips - Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CheckoutDetached - Text.CheckoutDetached.Target - Text.CheckoutDetached.Warning @@ -333,7 +373,7 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-95.57%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-94.83%25-yellow)
Missing keys in id_ID.axaml @@ -341,6 +381,14 @@ This document shows the translation status of each locale file in the repository - Text.Blame.Tips - Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CommitDetail.Files.Tips - Text.Dashboard - Text.Diff.Binary.ViewContentOfNew @@ -388,7 +436,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-85.20%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-84.49%25-yellow)
Missing keys in it_IT.axaml @@ -406,6 +454,14 @@ This document shows the translation status of each locale file in the repository - Text.BranchCM.CompareWithSpecial - Text.BranchTree.PushURL - Text.ChangeCM.ResetFileTo +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CheckoutBranchFromStash - Text.CheckoutBranchFromStash.Branch - Text.CheckoutBranchFromStash.Stash @@ -553,19 +609,27 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-99.62%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-98.87%25-yellow)
Missing keys in ja_JP.axaml - Text.BranchCM.CheckoutAsWorktree +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CommitDetail.Files.Tips - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.ExpandSidebar
-### ![ko__KR](https://img.shields.io/badge/ko__KR-92.74%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-92.01%25-yellow)
Missing keys in ko_KR.axaml @@ -575,6 +639,14 @@ This document shows the translation status of each locale file in the repository - Text.Blame.Tips - Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CheckoutDetached - Text.CheckoutDetached.Target - Text.CheckoutDetached.Warning @@ -650,7 +722,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-60.13%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-59.49%25-red)
Missing keys in pt_BR.axaml @@ -684,6 +756,14 @@ This document shows the translation status of each locale file in the repository - Text.Checkout.WarnUpdatingSubmodules - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CheckoutBranchFromStash - Text.CheckoutBranchFromStash.Branch - Text.CheckoutBranchFromStash.Stash @@ -1081,7 +1161,7 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-97.93%25-yellow) +### ![ru__RU](https://img.shields.io/badge/ru__RU-97.18%25-yellow)
Missing keys in ru_RU.axaml @@ -1089,6 +1169,14 @@ This document shows the translation status of each locale file in the repository - Text.Blame.Tips - Text.BranchCM.CheckoutAsWorktree - Text.BranchTree.PushURL +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CommitDetail.Files.Tips - Text.Dashboard - Text.Histories.AdvancedOptions @@ -1111,7 +1199,7 @@ This document shows the translation status of each locale file in the repository
-### ![ta__IN](https://img.shields.io/badge/ta__IN-61.73%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-61.09%25-red)
Missing keys in ta_IN.axaml @@ -1174,6 +1262,14 @@ This document shows the translation status of each locale file in the repository - Text.Checkout.WarnUpdatingSubmodules - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CheckoutBranchFromStash - Text.CheckoutBranchFromStash.Branch - Text.CheckoutBranchFromStash.Stash @@ -1525,7 +1621,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-62.49%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-61.84%25-red)
Missing keys in uk_UA.axaml @@ -1588,6 +1684,14 @@ This document shows the translation status of each locale file in the repository - Text.Checkout.WarnUpdatingSubmodules - Text.Checkout.WithFastForward - Text.Checkout.WithFastForward.Upstream +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CheckoutBranchFromStash - Text.CheckoutBranchFromStash.Branch - Text.CheckoutBranchFromStash.Stash @@ -1933,12 +2037,20 @@ This document shows the translation status of each locale file in the repository ### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen) -### ![zh__TW](https://img.shields.io/badge/zh__TW-99.62%25-yellow) +### ![zh__TW](https://img.shields.io/badge/zh__TW-98.87%25-yellow)
Missing keys in zh_TW.axaml - Text.BranchCM.CheckoutAsWorktree +- Text.CheckoutAsWorktree +- Text.CheckoutAsWorktree.LocalBranch +- Text.CheckoutAsWorktree.NewBranchName +- Text.CheckoutAsWorktree.NewBranchName.Placeholder +- Text.CheckoutAsWorktree.RemoteBranch +- Text.CheckoutAsWorktree.Tracking +- Text.CheckoutAsWorktree.WorktreePath +- Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CommitDetail.Files.Tips - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.ExpandSidebar diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index fee376aad9..9c2a87c666 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -16,11 +16,6 @@ Pfad für diesen Worktree. Relative Pfade werden unterstützt. Branch-Name: Optional. Standard ist der Name des Zielordners. - Branch verfolgen: - Remote-Branch verfolgen - Was auschecken: - Neuen Branch erstellen - Existierender Branch AI-Assistent Modell NEU GENERIEREN diff --git a/src/Resources/Locales/el_GR.axaml b/src/Resources/Locales/el_GR.axaml index 6c6996121a..fa1701ecb6 100644 --- a/src/Resources/Locales/el_GR.axaml +++ b/src/Resources/Locales/el_GR.axaml @@ -16,11 +16,6 @@ Διαδρομή για αυτό το worktree. Υποστηρίζεται σχετική διαδρομή. Όνομα κλάδου: Προαιρετικό. Προεπιλογή είναι το όνομα του φακέλου προορισμού. - Παρακολούθηση κλάδου: - Παρακολούθηση απομακρυσμένου κλάδου - Τι να γίνει checkout: - Δημιουργία νέου κλάδου - Υπάρχων κλάδος Βοηθός AI ΜΟΝΤΕΛΟ ΕΠΑΝΑΔΗΜΙΟΥΡΓΙΑ diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 856153ea2f..38c0c387a7 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -16,11 +16,6 @@ Ruta para este worktree. Se admite ruta relativa. Nombre de la Rama: Opcional. Por defecto es el nombre de la carpeta de destino. - Rama de Seguimiento: - Seguimiento de rama remota - Qué Checkout: - Crear Nueva Rama - Rama Existente Asistente OpenAI Modelo RE-GENERAR diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 823b851bae..18f75729e3 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -16,11 +16,6 @@ Chemin vers ce worktree. Relatif supporté. Nom de branche: Optionnel. Nom du dossier de destination par défaut. - Suivre la branche : - Suivi de la branche distante - Que récupérer : - Créer une nouvelle branche - Branche existante Assistant IA Modèle RE-GÉNÉRER diff --git a/src/Resources/Locales/he_IL.axaml b/src/Resources/Locales/he_IL.axaml index 2ba8cc88c1..64a98f3a2d 100644 --- a/src/Resources/Locales/he_IL.axaml +++ b/src/Resources/Locales/he_IL.axaml @@ -16,11 +16,6 @@ נתיב ל־worktree זה. נתיב יחסי נתמך. שם Branch: לא חובה. ברירת המחדל היא שם תיקיית היעד. - Branch למעקב: - מעקב אחר branch מרוחק - מה לבצע לו checkout: - צור Branch חדש - Branch קיים עוזר AI מודל צור מחדש diff --git a/src/Resources/Locales/id_ID.axaml b/src/Resources/Locales/id_ID.axaml index 0ff7f394c4..fd96085540 100644 --- a/src/Resources/Locales/id_ID.axaml +++ b/src/Resources/Locales/id_ID.axaml @@ -16,11 +16,6 @@ Jalur untuk worktree ini. Jalur relatif didukung. Nama Branch: Opsional. Standar adalah nama folder tujuan. - Track Branch: - Track remote branch - Yang Akan Di-Checkout: - Buat Branch Baru - Branch Yang Ada Asisten AI Model BUAT ULANG diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index e595a13df9..3ce0bae2c9 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -16,11 +16,6 @@ Percorso per questo worktree. Supportato il percorso relativo. Nome Branch: Facoltativo. Predefinito è il nome della cartella di destinazione. - Traccia Branch: - Traccia branch remoto - Di cosa fare il checkout: - Crea nuovo branch - Branch esistente Assistente AI Modello RIGENERA diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index ec73701055..0d914e9e8f 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -16,11 +16,6 @@ この作業ツリーへのパス (相対パスも使用できます) ブランチの名前: 省略可能 - 既定では宛先のフォルダーと同じ名前が使用されます - 追跡するブランチ: - 追跡するリモートブランチを設定 - チェックアウトする内容: - 新しいブランチを作成 - 既存のブランチ AI アシスタント モデル 再生成 diff --git a/src/Resources/Locales/ko_KR.axaml b/src/Resources/Locales/ko_KR.axaml index 939f8a4c1c..d6cd3d77b8 100644 --- a/src/Resources/Locales/ko_KR.axaml +++ b/src/Resources/Locales/ko_KR.axaml @@ -16,11 +16,6 @@ 이 워크트리의 경로입니다. 상대 경로를 지원합니다. 브랜치 이름: 선택 사항. 기본값은 대상 폴더 이름입니다. - 추적할 브랜치: - 원격 브랜치 추적 - 체크아웃할 대상: - 새 브랜치 생성 - 기존 브랜치 AI 어시스턴트 모델 재생성 diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 0e1a405bcd..5084f0180b 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -16,11 +16,6 @@ Caminho para este worktree. Caminho relativo é suportado. Nome do Branch: Opcional. O padrão é o nome da pasta de destino. - Rastrear Branch: - Rastreando branch remoto - O que Checar: - Criar Novo Branch - Branch Existente Assietente IA Modelo Gerar novamente diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 7566fe27f4..6108c7cbcd 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -16,11 +16,6 @@ Путь к рабочему каталогу (поддерживается относительный путь) Имя ветки: Имя целевого каталога по умолчанию (необязательно) - Отслеживание ветки: - Отслеживание внешней ветки - Переключиться на: - Создать новую ветку - Ветку из списка Помощник OpenAI Модель ПЕРЕСОЗДАТЬ diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index f2f12bcfee..3473ff1cab 100644 --- a/src/Resources/Locales/ta_IN.axaml +++ b/src/Resources/Locales/ta_IN.axaml @@ -11,11 +11,6 @@ இந்த பணிமரத்திற்கான பாதை. தொடர்புடைய பாதை ஆதரிக்கப்படுகிறது. கிளை பெயர்: விருப்பத்தேர்வு. இயல்புநிலை இலக்கு கோப்புறை பெயர். - கிளை கண்காணி: - தொலை கிளையைக் கண்காணித்தல் - என்ன சரிபார்க்க வேண்டும்: - புதிய கிளையை உருவாக்கு - ஏற்கனவே உள்ள கிளை செநு உதவியாளர் மாதிரி மறு-உருவாக்கு diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml index a6e5b89ae5..5f44193e39 100644 --- a/src/Resources/Locales/uk_UA.axaml +++ b/src/Resources/Locales/uk_UA.axaml @@ -11,11 +11,6 @@ Шлях для цього робочого дерева. Відносний шлях підтримується. Назва гілки: Необов'язково. За замовчуванням — назва кінцевої папки. - Відстежувати гілку: - Відстежувати віддалену гілку - Що перемкнути: - Створити нову гілку - Наявна гілка AI Асистент Модель ПЕРЕГЕНЕРУВАТИ diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 2a9b5844b3..2498fc8b26 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -16,11 +16,6 @@ 填写该工作树的路径。支持相对路径。 分支名 : 选填。默认使用目标文件夹名称。 - 跟踪分支 - 设置上游跟踪分支 - 检出分支方式 : - 创建新分支 - 已有分支 AI助手 模型 重新生成 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 135596f6e9..64be88d931 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -16,11 +16,6 @@ 填寫該工作樹的路徑。支援相對路徑。 分支名稱: 選填。預設使用目標資料夾名稱。 - 追蹤分支 - 設定遠端追蹤分支 - 簽出分支方式: - 建立新分支 - 已有分支 AI 助理 模型 重新產生 From 0b253cc5242311016c8b1b29dea54cf20e037698 Mon Sep 17 00:00:00 2001 From: Daniil Tatianin <34326714+d-tatianin@users.noreply.github.com> Date: Thu, 24 Sep 2026 05:21:27 +0300 Subject: [PATCH 09/51] enhance: clear stale commit message after an external operation ends (#2741) When a rebase, merge, cherry-pick or revert is in progress, the working copy auto-fills the commit message box from `MERGE_MSG`, `rebase-merge/message` or the commit being replayed. If that operation then finishes or is aborted outside SourceGit (e.g. from the terminal), the loaded text stayed in the box, got persisted as the last commit message on close, and reappeared on every later open of the repository. Remember which message was auto-loaded and clear it once the in-progress state disappears, unless the user has edited the text in the meantime. Signed-off-by: Daniil Tatianin <99danilt@gmail.com> --- src/ViewModels/WorkingCopy.cs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 8034a6fb4b..59e02ef8cd 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -807,6 +807,12 @@ private void UpdateInProgressState() if (_inProgressContext != null && _inProgressContext.GetType() == oldType && !string.IsNullOrEmpty(_commitMessage)) return; + // The operation that filled the commit message ended outside SourceGit (for example, a + // rebase or merge finished/aborted from the terminal). Drop the stale message unless the + // user has edited it since it was loaded. + if (_inProgressContext == null && oldType != null && IsAutoLoadedCommitMessage()) + CommitMessage = string.Empty; + if (LoadCommitMessageFromFile(Path.Combine(_repo.GitDir, "MERGE_MSG"))) return; @@ -816,7 +822,7 @@ private void UpdateInProgressState() if (LoadCommitMessageFromFile(Path.Combine(_repo.GitDir, "rebase-merge", "message"))) return; - CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, rebasing.StoppedAt.SHA).GetResult(); + SetAutoLoadedCommitMessage(new Commands.QueryCommitFullMessage(_repo.FullPath, rebasing.StoppedAt.SHA).GetResult()); } private bool LoadCommitMessageFromFile(string file) @@ -828,10 +834,22 @@ private bool LoadCommitMessageFromFile(string file) if (string.IsNullOrEmpty(msg)) return false; - CommitMessage = msg; + SetAutoLoadedCommitMessage(msg); return true; } + private void SetAutoLoadedCommitMessage(string msg) + { + _autoLoadedCommitMessage = msg; + CommitMessage = msg; + } + + private bool IsAutoLoadedCommitMessage() + { + return !string.IsNullOrEmpty(_autoLoadedCommitMessage) && + string.Equals(_autoLoadedCommitMessage, _commitMessage, StringComparison.Ordinal); + } + private void SetDetail(Models.Change change, bool isUnstaged) { if (_isLoadingData) @@ -942,5 +960,6 @@ public ChangeInTree(Models.Change change) private bool _hasUnsolvedConflicts = false; private InProgressContext _inProgressContext = null; + private string _autoLoadedCommitMessage = null; } } From 5b06cd10c54c9dee9f14466b2e9e56437cac9002 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 24 Sep 2026 10:23:17 +0800 Subject: [PATCH 10/51] localization: remove unused translations Signed-off-by: leo --- src/Resources/Locales/en_US.axaml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 98bbb4b46d..4dc15a4c36 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -204,7 +204,6 @@ SHA Signer: Open in Browser - COL Enter commit message. Please use an empty-line to separate subject and description! SUBJECT Compare From 247c227238be74861f83fa0513d0b1ba066b6e8c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 24 Sep 2026 02:23:48 +0000 Subject: [PATCH 11/51] doc: Update translation status and sort locale files --- TRANSLATION.md | 18 +++++++----------- src/Resources/Locales/de_DE.axaml | 1 - src/Resources/Locales/el_GR.axaml | 1 - src/Resources/Locales/es_ES.axaml | 1 - src/Resources/Locales/fr_FR.axaml | 1 - src/Resources/Locales/he_IL.axaml | 1 - src/Resources/Locales/id_ID.axaml | 1 - src/Resources/Locales/ja_JP.axaml | 1 - src/Resources/Locales/ko_KR.axaml | 1 - src/Resources/Locales/ru_RU.axaml | 1 - src/Resources/Locales/zh_CN.axaml | 1 - src/Resources/Locales/zh_TW.axaml | 1 - 12 files changed, 7 insertions(+), 22 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 13ed7678f6..40fcc28f0c 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -157,7 +157,7 @@ This document shows the translation status of each locale file in the repository
-### ![fr__FR](https://img.shields.io/badge/fr__FR-90.60%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-90.59%25-yellow)
Missing keys in fr_FR.axaml @@ -265,7 +265,7 @@ This document shows the translation status of each locale file in the repository
-### ![he__IL](https://img.shields.io/badge/he__IL-90.60%25-yellow) +### ![he__IL](https://img.shields.io/badge/he__IL-90.59%25-yellow)
Missing keys in he_IL.axaml @@ -436,7 +436,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-84.49%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-84.57%25-yellow)
Missing keys in it_IT.axaml @@ -478,7 +478,6 @@ This document shows the translation status of each locale file in the repository - Text.CommitCM.CopyCommitterTime - Text.CommitDetail.CollapseToBottom - Text.CommitDetail.Files.Tips -- Text.CommitMessageTextBox.Column - Text.Compare.Changes - Text.Compare.Commits - Text.Compare.Commits.LeftOnly @@ -629,7 +628,7 @@ This document shows the translation status of each locale file in the repository
-### ![ko__KR](https://img.shields.io/badge/ko__KR-92.01%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-92.00%25-yellow)
Missing keys in ko_KR.axaml @@ -722,7 +721,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-59.49%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-59.55%25-red)
Missing keys in pt_BR.axaml @@ -803,7 +802,6 @@ This document shows the translation status of each locale file in the repository - Text.CommitDetail.Info.CopyNameAndEmail - Text.CommitDetail.Info.Key - Text.CommitDetail.Info.Signer -- Text.CommitMessageTextBox.Column - Text.CommitMessageTextBox.Placeholder - Text.CommitMessageTextBox.SubjectCount - Text.Compare.Changes @@ -1199,7 +1197,7 @@ This document shows the translation status of each locale file in the repository
-### ![ta__IN](https://img.shields.io/badge/ta__IN-61.09%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-61.15%25-red)
Missing keys in ta_IN.axaml @@ -1306,7 +1304,6 @@ This document shows the translation status of each locale file in the repository - Text.CommitDetail.Info.CopyNameAndEmail - Text.CommitDetail.Info.Key - Text.CommitDetail.Info.Signer -- Text.CommitMessageTextBox.Column - Text.CommitMessageTextBox.Placeholder - Text.CommitMessageTextBox.SubjectCount - Text.Compare.Changes @@ -1621,7 +1618,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-61.84%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-61.90%25-red)
Missing keys in uk_UA.axaml @@ -1728,7 +1725,6 @@ This document shows the translation status of each locale file in the repository - Text.CommitDetail.Info.CopyNameAndEmail - Text.CommitDetail.Info.Key - Text.CommitDetail.Info.Signer -- Text.CommitMessageTextBox.Column - Text.CommitMessageTextBox.Placeholder - Text.CommitMessageTextBox.SubjectCount - Text.Compare.Changes diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 9c2a87c666..9e2d348bc8 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -196,7 +196,6 @@ SHA Unterzeichner: Im Browser öffnen - SPALTE Commit-Nachricht eingeben. Leerzeile zum Trennen von Betreff und Beschreibung verwenden! BETREFF Vergleichen diff --git a/src/Resources/Locales/el_GR.axaml b/src/Resources/Locales/el_GR.axaml index fa1701ecb6..8e5e5f4dfe 100644 --- a/src/Resources/Locales/el_GR.axaml +++ b/src/Resources/Locales/el_GR.axaml @@ -196,7 +196,6 @@ SHA Υπογράφων: Άνοιγμα στον περιηγητή - ΣΤΗΛΗ Εισαγάγετε το μήνυμα commit. Χρησιμοποιήστε μια κενή γραμμή για να διαχωρίσετε το θέμα από την περιγραφή! ΘΕΜΑ Σύγκριση diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 38c0c387a7..a194095fe6 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -198,7 +198,6 @@ SHA Firmante: Abrir en Navegador - COL Ingresa el mensaje de commit. ¡Por favor usa una línea en blanco para separar el título y la descripción! ASUNTO Comparar diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 18f75729e3..e6b46a1e27 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -186,7 +186,6 @@ SHA Signataire : Ouvrir dans le navigateur - COL Saisissez le message de commit. Utilisez une ligne vide pour séparer le sujet et la description ! SUJET Comparer diff --git a/src/Resources/Locales/he_IL.axaml b/src/Resources/Locales/he_IL.axaml index 64a98f3a2d..98426b3023 100644 --- a/src/Resources/Locales/he_IL.axaml +++ b/src/Resources/Locales/he_IL.axaml @@ -186,7 +186,6 @@ SHA חותם: פתח בדפדפן - עמודה הזן הודעת commit. יש להפריד בין subject לתיאור באמצעות שורה ריקה! SUBJECT השווה diff --git a/src/Resources/Locales/id_ID.axaml b/src/Resources/Locales/id_ID.axaml index fd96085540..a50be71a66 100644 --- a/src/Resources/Locales/id_ID.axaml +++ b/src/Resources/Locales/id_ID.axaml @@ -196,7 +196,6 @@ SHA Penanda Tangan: Buka di Browser - KOL Masukkan pesan commit. Gunakan baris kosong untuk memisahkan subjek dan deskripsi! SUBJEK Perbandingan diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index 0d914e9e8f..ac9637eea9 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -198,7 +198,6 @@ SHA 署名者: ブラウザーで開く - 列 コミットメッセージを入力してください。タイトルと説明は、空行で分けて記述してください! タイトル 比較 diff --git a/src/Resources/Locales/ko_KR.axaml b/src/Resources/Locales/ko_KR.axaml index d6cd3d77b8..e0695553fb 100644 --- a/src/Resources/Locales/ko_KR.axaml +++ b/src/Resources/Locales/ko_KR.axaml @@ -191,7 +191,6 @@ SHA 서명자: 브라우저에서 열기 - 열 커밋 메시지를 입력하세요. 제목과 설명은 빈 줄로 구분하세요! 제목 비교 diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 6108c7cbcd..a84938579a 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -196,7 +196,6 @@ SHA Подписант: Открыть в браузере - СТБ Введите сообщение ревизии. Пожалуйста, используйте пустые строки для разделения субъекта и описания! СУБЪЕКТ Сравнить diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 2498fc8b26..a7f35949ba 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -208,7 +208,6 @@ 提交指纹 签名者 : 浏览器中查看 - 列 请输入提交的信息。注意:主题与具体描述中间需要空白行分隔! 主题 比较 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 64be88d931..85612abe12 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -198,7 +198,6 @@ 提交編號 簽署人: 在瀏覽器中檢視 - 欄 請輸入提交訊息,標題與詳細描述之間請使用單行空白區隔。 標題 比較 From bfd8c400ea385d1589781264c636081ed5feb6cf Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 24 Sep 2026 10:39:22 +0800 Subject: [PATCH 12/51] enhance: pre-fill branch name when checking out a remote branch as worktree Signed-off-by: leo --- src/ViewModels/CheckoutRemoteBranchAsWorktree.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ViewModels/CheckoutRemoteBranchAsWorktree.cs b/src/ViewModels/CheckoutRemoteBranchAsWorktree.cs index 9ec894ae0e..a054a30134 100644 --- a/src/ViewModels/CheckoutRemoteBranchAsWorktree.cs +++ b/src/ViewModels/CheckoutRemoteBranchAsWorktree.cs @@ -37,6 +37,7 @@ public CheckoutRemoteBranchAsWorktree(Repository repo, Models.Branch remoteBranc _repo = repo; _remoteBranch = remoteBranch; _worktreePath = Path.Combine(repo.GetRecommandedWorktreeDir(), remoteBranch.Name.Replace('/', '-').Replace('\\', '-')); + _branchName = remoteBranch.Name; _tracking = true; } From 0317ba2c62a1986353f6b7e06370f483bcb72225 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 24 Sep 2026 11:25:17 +0800 Subject: [PATCH 13/51] style: use another way to set global default font size (#2708) Signed-off-by: leo --- src/Resources/Styles.axaml | 8 +++++--- src/Views/CommitBaseInfo.axaml | 1 + src/Views/Histories.axaml | 1 + src/Views/InteractiveRebase.axaml | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 2a605b2bd6..d1ba76c821 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -21,6 +21,8 @@ + + @@ -174,9 +176,9 @@ - + + -
-### ![ru__RU](https://img.shields.io/badge/ru__RU-97.18%25-yellow) - -
-Missing keys in ru_RU.axaml - -- Text.Blame.Tips -- Text.BranchCM.CheckoutAsWorktree -- Text.BranchTree.PushURL -- Text.CheckoutAsWorktree -- Text.CheckoutAsWorktree.LocalBranch -- Text.CheckoutAsWorktree.NewBranchName -- Text.CheckoutAsWorktree.NewBranchName.Placeholder -- Text.CheckoutAsWorktree.RemoteBranch -- Text.CheckoutAsWorktree.Tracking -- Text.CheckoutAsWorktree.WorktreePath -- Text.CheckoutAsWorktree.WorktreePath.Placeholder -- Text.CommitDetail.Files.Tips -- Text.Dashboard -- Text.Histories.AdvancedOptions -- Text.Hotkeys.Repo.ToggleCommitDetailPanel -- Text.Hotkeys.Repo.ToggleSearchCommits -- Text.Hotkeys.Repo.ViewDashboard -- Text.Preferences.AI.ExtraHeaders -- Text.Remote.ResetPushURL -- Text.RemoteCM.SetPushURL -- Text.Repository.Search.Method -- Text.Repository.Search.ToolTip -- Text.SetPushUrl -- Text.SetPushUrl.PushURL -- Text.SetPushUrl.Remote -- Text.SetPushUrl.URL -- Text.Statistics.Tips -- Text.WorkingCopy.CollapseSidebar -- Text.WorkingCopy.DiscardAll -- Text.WorkingCopy.ExpandSidebar - -
+### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) ### ![ta__IN](https://img.shields.io/badge/ta__IN-61.15%25-red) diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index f638aae807..596df60287 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -1036,8 +1036,8 @@ Изменить Теперь вы можете сформировать этот файл. Очистить историю - Скрыть боковую панель Вы действительно хотите очистить всю историю сообщений ревизии? Данное действие нельзя отменить. + Скрыть боковую панель ЗАФИКСИРОВАТЬ ЗАФИКСИРОВАТЬ и ОТПРАВИТЬ Шаблон/Истории From d51cd4109ed513a64504814d4d8435454db8b1c0 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 28 Sep 2026 10:07:15 +0800 Subject: [PATCH 17/51] enhance: terminate `git status` command gracefully (#2718) Signed-off-by: leo --- src/Commands/QueryLocalChanges.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index 31d72a0959..7c2e4156ab 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -46,8 +46,8 @@ public QueryLocalChanges(string repo, bool includeUntracked = true, bool noOptio { lock (capturedLock) { - if (captured is { Process: { HasExited: false } }) - captured.Process.Kill(true); + if (captured is { Process: { HasExited: false } proc }) + Native.OS.TerminateProcess(proc); } }); } From 9752df31c1bcfb90de3937d1635b9afa9caee67b Mon Sep 17 00:00:00 2001 From: liccode <53359500+a2580vb@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:20:57 +0800 Subject: [PATCH 18/51] feature: confirm before trusting a repository refused by git (#2746) * feature: confirm before trusting a repository refused by git * refactor: keep only permanent trust for untrusted repositories --------- Co-authored-by: liccode --- src/App.axaml.cs | 4 +- src/Commands/AddSafeDirectory.cs | 11 +++ src/Commands/Command.cs | 4 +- src/Models/SafeDirectories.cs | 103 +++++++++++++++++++++ src/Resources/Locales/en_US.axaml | 6 ++ src/Resources/Locales/zh_CN.axaml | 6 ++ src/Resources/Locales/zh_TW.axaml | 6 ++ src/ViewModels/Launcher.cs | 28 +++++- src/ViewModels/LauncherPage.cs | 2 +- src/ViewModels/OpenLocalRepository.cs | 23 ++++- src/ViewModels/Popup.cs | 6 ++ src/ViewModels/ScanRepositories.cs | 9 ++ src/ViewModels/TrustRepository.cs | 125 ++++++++++++++++++++++++++ src/ViewModels/Welcome.cs | 16 +++- src/Views/LauncherPage.axaml | 1 + src/Views/TrustRepository.axaml | 54 +++++++++++ src/Views/TrustRepository.axaml.cs | 12 +++ src/Views/Welcome.axaml.cs | 2 +- 18 files changed, 403 insertions(+), 15 deletions(-) create mode 100644 src/Commands/AddSafeDirectory.cs create mode 100644 src/Models/SafeDirectories.cs create mode 100644 src/ViewModels/TrustRepository.cs create mode 100644 src/Views/TrustRepository.axaml create mode 100644 src/Views/TrustRepository.axaml.cs diff --git a/src/App.axaml.cs b/src/App.axaml.cs index eef4684cee..66882b4869 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -350,7 +350,7 @@ private bool TryLaunchAsFileHistoryViewer(IClassicDesktopStyleApplicationLifetim var test = new Commands.QueryRepositoryRootPath(dir).GetResult(); if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut)) { - Console.Out.WriteLine($"'{args[1]}' is not in a valid git repository"); + Console.Out.WriteLine(string.IsNullOrWhiteSpace(test.StdErr) ? $"'{args[1]}' is not in a valid git repository" : test.StdErr.Trim()); desktop.Shutdown(-1); return true; } @@ -395,7 +395,7 @@ private bool TryLaunchAsBlameViewer(IClassicDesktopStyleApplicationLifetime desk var test = new Commands.QueryRepositoryRootPath(dir).GetResult(); if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut)) { - Console.Out.WriteLine($"'{args[1]}' is not in a valid git repository"); + Console.Out.WriteLine(string.IsNullOrWhiteSpace(test.StdErr) ? $"'{args[1]}' is not in a valid git repository" : test.StdErr.Trim()); desktop.Shutdown(-1); return true; } diff --git a/src/Commands/AddSafeDirectory.cs b/src/Commands/AddSafeDirectory.cs new file mode 100644 index 0000000000..52e190db69 --- /dev/null +++ b/src/Commands/AddSafeDirectory.cs @@ -0,0 +1,11 @@ +namespace SourceGit.Commands +{ + public class AddSafeDirectory : Command + { + public AddSafeDirectory(string ctx, string value) + { + Context = ctx; + Args = $"config --global --add safe.directory {value.Quoted()}"; + } + } +} diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index ba68bb90da..14430805c2 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -210,8 +210,8 @@ protected ProcessStartInfo CreateGitStartInfo(bool redirect) if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey)) start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}' -o AddKeysToAgent=yes"); - // Force using en_US.UTF-8 locale - if (OperatingSystem.IsLinux()) + // Force the C locale on Unix, so that git's output (including fatal errors) can always be parsed. + if (!OperatingSystem.IsWindows()) { start.Environment.Add("LANG", "C"); start.Environment.Add("LC_ALL", "C"); diff --git a/src/Models/SafeDirectories.cs b/src/Models/SafeDirectories.cs new file mode 100644 index 0000000000..5fcf24a3af --- /dev/null +++ b/src/Models/SafeDirectories.cs @@ -0,0 +1,103 @@ +using System; +using System.Text; +using System.Text.RegularExpressions; + +namespace SourceGit.Models +{ + /// + /// Helpers for the `safe.directory` protection introduced by git 2.35.2 (CVE-2022-24765). + /// Git refuses to use a repository whose top-level directory is owned by another user (which is + /// always the case for network shares / UNC paths) unless the path is listed in the `safe.directory` config. + /// + public static partial class SafeDirectories + { + public static bool IsUntrustedRepository(string output) + { + if (string.IsNullOrWhiteSpace(output)) + return false; + + return output.Contains("detected dubious ownership", StringComparison.OrdinalIgnoreCase) || + output.Contains("unsafe repository", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Tries to get the value that should be written into the `safe.directory` config. + /// Prefers the value suggested by git itself (which knows the correct form for the current platform), + /// and falls back to building a value from the given path. + /// + public static bool TryGetSafeDirectoryValue(string path, string output, out string value) + { + value = ParseSuggestedValue(output); + if (!string.IsNullOrEmpty(value)) + return true; + + var normalized = Normalize(path); + if (string.IsNullOrEmpty(normalized)) + return false; + + // Git for Windows uses the `%(prefix)/` prefix for UNC paths. + value = OperatingSystem.IsWindows() && normalized.StartsWith("//", StringComparison.Ordinal) ? $"%(prefix)/{normalized}" : normalized; + return true; + } + + private static string Normalize(string path) + { + return path?.Replace('\\', '/').TrimEnd('/') ?? string.Empty; + } + + private static string ParseSuggestedValue(string output) + { + if (string.IsNullOrEmpty(output)) + return string.Empty; + + var match = REG_SAFE_DIRECTORY_HINT().Match(output); + if (!match.Success) + return string.Empty; + + return Dequote(match.Groups["value"].Value.Trim()); + } + + /// + /// Git quotes the suggested value with `sq_quote_buf()`, which escapes `'` and `!` as `'\''` and + /// `'\!'`. Decodes it back to the plain path (see `quote.c` of git). + /// + private static string Dequote(string value) + { + if (value.Length > 1 && value[0] == '\'') + { + var builder = new StringBuilder(value.Length); + for (var i = 1; i < value.Length; i++) + { + var c = value[i]; + if (c != '\'') + { + builder.Append(c); + continue; + } + + if (i == value.Length - 1) + return builder.ToString(); + + if (i + 3 < value.Length && value[i + 1] == '\\' && (value[i + 2] == '\'' || value[i + 2] == '!') && value[i + 3] == '\'') + { + builder.Append(value[i + 2]); + i += 3; + continue; + } + + return string.Empty; + } + + return string.Empty; + } + + if (value.Length > 1 && value[0] == '"' && value[^1] == '"') + return value[1..^1]; + + return value; + } + + [GeneratedRegex(@"--add\s+safe\.directory\s+(?[^\r\n]+)", RegexOptions.Multiline)] + private static partial Regex REG_SAFE_DIRECTORY_HINT(); + } +} diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 4dc15a4c36..d3a300dd0e 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -997,6 +997,12 @@ Merge ${0}$ into ${1}$... Push ${0}$... TERMINATE + Untrusted Repository + Will run: + Git refused to access this repository: + Path: + Trust this repository + {0} repositories were skipped because git does not trust them. Please open them manually and confirm whether to trust them. Update Submodules All submodules Initialize as needed diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index a7f35949ba..4f0ddd13d3 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -1001,6 +1001,12 @@ 合并 ${0}$ 到 ${1}$... 推送 ${0}$... 终止运行 + 不受信任的仓库 + 将执行: + Git 拒绝访问该仓库: + 路径 : + 信任此仓库 + 有 {0} 个仓库因 git 的信任检查被跳过,请手动打开它们并确认是否信任。 更新子模块 更新所有子模块 如未初始化子模块,先初始化 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 85612abe12..8286fc84c0 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -991,6 +991,12 @@ 合併 ${0}$ 到 ${1}$... 推送 ${0}$... 終止執行 + 不受信任的存放庫 + 將執行: + Git 拒絕存取該存放庫: + 路徑 : + 信任此存放庫 + 有 {0} 個存放庫因 git 的信任檢查而被略過,請手動開啟並確認是否信任。 更新子模組 更新所有子模組 如果子模組尚未初始化,則將其初始化 diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 1cf00a848c..794776f9fa 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -104,7 +104,12 @@ public bool TryOpenRepositoryFromPath(string repo) if (ActivePage is not { Data: Welcome { }, Popup: null }) AddNewTab(); - ActivePage.Popup = new Init(ActivePage.Node.Id, repo, null, 0, test.StdErr ?? "Unknown error occurred while opening the repository."); + if (Models.SafeDirectories.IsUntrustedRepository(test.StdErr) && + Models.SafeDirectories.TryGetSafeDirectoryValue(repo, test.StdErr, out var safeDirectory)) + ActivePage.Popup = new TrustRepository(ActivePage.Node.Id, repo, test.StdErr, safeDirectory, null, false, true, 0); + else + ActivePage.Popup = new Init(ActivePage.Node.Id, repo, null, 0, test.StdErr ?? "Unknown error occurred while opening the repository."); + return true; } } @@ -325,6 +330,9 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) var gitDir = isBare ? node.Id : GetRepositoryGitDir(node.Id); if (string.IsNullOrEmpty(gitDir)) { + if (TryShowTrustRepositoryPopup(node.Id, ActivePage)) + return; + ActivePage.Notifications.Add(new Models.Notification { Group = node.Id, @@ -404,6 +412,9 @@ public void OpenSubRepository(LauncherPage ownerPage, string fullpath) var gitDir = GetRepositoryGitDir(normalizedPath); if (string.IsNullOrEmpty(gitDir)) { + if (TryShowTrustRepositoryPopup(normalizedPath, ownerPage)) + return; + ownerPage.Notifications.Add(new Models.Notification { Group = ownerPage.Node.Id, @@ -439,6 +450,21 @@ public void OpenSubRepository(LauncherPage ownerPage, string fullpath) ActivePage = page; } + private bool TryShowTrustRepositoryPopup(string path, LauncherPage page) + { + if (page == null || !page.CanCreatePopup()) + return false; + + var test = new Commands.QueryRepositoryRootPath(path).GetResult(); + if (test.IsSuccess || + !Models.SafeDirectories.IsUntrustedRepository(test.StdErr) || + !Models.SafeDirectories.TryGetSafeDirectoryValue(path, test.StdErr, out var safeDirectory)) + return false; + + page.Popup = new TrustRepository(page.Node.Id, path, test.StdErr, safeDirectory, null, false, true, 0); + return true; + } + private void DispatchNotification(Models.Notification notification) { if (!Dispatcher.UIThread.CheckAccess()) diff --git a/src/ViewModels/LauncherPage.cs b/src/ViewModels/LauncherPage.cs index 370e5a25c7..73013a4889 100644 --- a/src/ViewModels/LauncherPage.cs +++ b/src/ViewModels/LauncherPage.cs @@ -80,7 +80,7 @@ public bool CanCreatePopup() public async Task ProcessPopupAsync() { - if (_popup is { InProgress: false } dump) + if (_popup is { InProgress: false } dump && dump.CanSure) { if (!dump.Check()) return; diff --git a/src/ViewModels/OpenLocalRepository.cs b/src/ViewModels/OpenLocalRepository.cs index 732c5c8acf..8636980b49 100644 --- a/src/ViewModels/OpenLocalRepository.cs +++ b/src/ViewModels/OpenLocalRepository.cs @@ -89,12 +89,27 @@ public override async Task Sure() else { var launcher = App.GetLauncher(); - foreach (var page in launcher.Pages) + if (Models.SafeDirectories.IsUntrustedRepository(test.StdErr) && + Models.SafeDirectories.TryGetSafeDirectoryValue(_repoPath, test.StdErr, out var safeDirectory)) { - if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal)) + foreach (var page in launcher.Pages) { - page.Popup = new Init(page.Node.Id, _repoPath, parent, _bookmark, test.StdErr); - break; + if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal)) + { + page.Popup = new TrustRepository(page.Node.Id, _repoPath, test.StdErr, safeDirectory, parent, true, true, _bookmark); + break; + } + } + } + else + { + foreach (var page in launcher.Pages) + { + if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal)) + { + page.Popup = new Init(page.Node.Id, _repoPath, parent, _bookmark, test.StdErr); + break; + } } } diff --git a/src/ViewModels/Popup.cs b/src/ViewModels/Popup.cs index 10bc44fe1b..325f2119ad 100644 --- a/src/ViewModels/Popup.cs +++ b/src/ViewModels/Popup.cs @@ -55,6 +55,12 @@ public virtual bool CanStartDirectly() return true; } + /// + /// Whether the `Sure` action is currently allowed. Views should bind the confirm button's + /// `IsEnabled` to this property. + /// + public virtual bool CanSure => true; + public virtual Task Sure() { return null; diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs index 544784c117..693c4ca415 100644 --- a/src/ViewModels/ScanRepositories.cs +++ b/src/ViewModels/ScanRepositories.cs @@ -115,6 +115,10 @@ public override async Task Sure() Preferences.Instance.AutoRemoveInvalidNode(); Preferences.Instance.Save(); Welcome.Instance.Refresh(); + + if (_untrusted > 0) + Models.Notification.Send(null, App.Text("TrustRepository.ScanSkipped", _untrusted)); + return true; } @@ -154,6 +158,10 @@ private async Task GetUnmanagedRepositoriesAsync(DirectoryInfo dir, List if (!IsManaged(normalized)) outs.Add(normalized); } + else if (Models.SafeDirectories.IsUntrustedRepository(test.StdErr)) + { + _untrusted++; + } continue; } @@ -182,5 +190,6 @@ private bool IsManaged(string path) private bool _useCustomDir = false; private string _customDir = string.Empty; private Models.ScanDir _selected = null; + private int _untrusted = 0; } } diff --git a/src/ViewModels/TrustRepository.cs b/src/ViewModels/TrustRepository.cs new file mode 100644 index 0000000000..ec0a4d2ca4 --- /dev/null +++ b/src/ViewModels/TrustRepository.cs @@ -0,0 +1,125 @@ +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class TrustRepository : Popup + { + public string TargetPath + { + get; + } + + public string SafeDirectory + { + get; + } + + public string Command + { + get; + } + + public string Reason + { + get; + } + + /// + /// Whether the exception should be added into the user's global git config. Disabled by default, + /// so the user has to opt in explicitly. + /// + public bool Permanent + { + get => _permanent; + set + { + if (SetProperty(ref _permanent, value)) + OnPropertyChanged(nameof(CanSure)); + } + } + + public override bool CanSure => Permanent; + + public TrustRepository(string pageId, string path, string reason, string safeDirectory, RepositoryNode parent, bool moveNode, bool open, int bookmark) + { + _pageId = pageId; + _parent = parent; + _moveNode = moveNode; + _open = open; + _bookmark = bookmark; + + TargetPath = path; + SafeDirectory = safeDirectory; + Command = $"git config --global --add safe.directory {safeDirectory.Quoted()}"; + Reason = GetReason(reason); + } + + public override async Task Sure() + { + if (!Permanent) + return false; + + var log = new CommandLog("Trust Repository"); + Use(log); + + ProgressDescription = $"Adding '{SafeDirectory}' into git global `safe.directory` ..."; + var added = await new Commands.AddSafeDirectory(_pageId, SafeDirectory).Use(log).ExecAsync(); + if (!added) + { + log.Complete(); + return false; + } + + ProgressDescription = $"Opening '{TargetPath}' ..."; + + var root = TargetPath; + var isBare = await new Commands.IsBareRepository(TargetPath).GetResultAsync(); + if (!isBare) + { + var test = await new Commands.QueryRepositoryRootPath(TargetPath).GetResultAsync(); + if (!test.IsSuccess || string.IsNullOrWhiteSpace(test.StdOut)) + { + log.Complete(); + Models.Notification.Send(_pageId, string.IsNullOrWhiteSpace(test.StdErr) ? "Failed to open the repository after trusting it." : test.StdErr, true); + return false; + } + + root = test.StdOut.Trim(); + } + + log.Complete(); + + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(root, _parent, _moveNode); + node.Bookmark = _bookmark; + await node.UpdateStatusAsync(false, null); + Welcome.Instance.Refresh(); + + if (_open) + node.Open(); + + return true; + } + + private static string GetReason(string stderr) + { + if (string.IsNullOrWhiteSpace(stderr)) + return string.Empty; + + // Only the first line (e.g. "fatal: detected dubious ownership in repository at '...'") is shown. + // The remaining lines only contain the owner SIDs and git's suggested `git config` command. + var line = stderr.Trim(); + var idx = line.IndexOf('\n'); + if (idx >= 0) + line = line.Substring(0, idx); + + return line.Trim(); + } + + private readonly string _pageId; + private readonly RepositoryNode _parent; + private readonly bool _moveNode; + private readonly bool _open; + private readonly int _bookmark; + private bool _permanent = false; + } +} diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index 49539292d7..24ce2ce31f 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -100,7 +100,7 @@ public void ToggleNodeIsExpanded(RepositoryNode node) } } - public async Task GetRepositoryRootAsync(string path) + public async Task GetRepositoryRootAsync(string path, RepositoryNode parent = null) { if (!Preferences.Instance.IsGitConfigured()) { @@ -122,10 +122,18 @@ public async Task GetRepositoryRootAsync(string path) return root; var rs = await new Commands.QueryRepositoryRootPath(root).GetResultAsync(); - if (!rs.IsSuccess || string.IsNullOrWhiteSpace(rs.StdOut)) - return null; + if (rs.IsSuccess && !string.IsNullOrWhiteSpace(rs.StdOut)) + return rs.StdOut.Trim(); - return rs.StdOut.Trim(); + if (Models.SafeDirectories.IsUntrustedRepository(rs.StdErr) && + Models.SafeDirectories.TryGetSafeDirectoryValue(root, rs.StdErr, out var safeDirectory)) + { + var launcher = App.GetLauncher(); + if (launcher?.ActivePage is { } page && page.CanCreatePopup()) + page.Popup = new TrustRepository(page.Node.Id, root, rs.StdErr, safeDirectory, parent, true, false, 0); + } + + return null; } public async Task AddRepositoryAsync(string path, RepositoryNode parent, bool moveNode, bool open) diff --git a/src/Views/LauncherPage.axaml b/src/Views/LauncherPage.axaml index 08107bfa90..a5390f327e 100644 --- a/src/Views/LauncherPage.axaml +++ b/src/Views/LauncherPage.axaml @@ -116,6 +116,7 @@ Padding="0" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" + IsEnabled="{Binding CanSure}" Content="{DynamicResource Text.Sure}" Click="OnPopupSure" ToolTip.Tip="Enter"/> diff --git a/src/Views/TrustRepository.axaml b/src/Views/TrustRepository.axaml new file mode 100644 index 0000000000..4706d1fe62 --- /dev/null +++ b/src/Views/TrustRepository.axaml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/TrustRepository.axaml.cs b/src/Views/TrustRepository.axaml.cs new file mode 100644 index 0000000000..5f624bc238 --- /dev/null +++ b/src/Views/TrustRepository.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class TrustRepository : UserControl + { + public TrustRepository() + { + InitializeComponent(); + } + } +} diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index c93c27ebb8..fbc3c193ab 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -394,7 +394,7 @@ private async void DropOnTreeNode(object sender, DragEventArgs e) foreach (var item in items) { - var path = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(item.Path.LocalPath); + var path = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(item.Path.LocalPath, to); if (!string.IsNullOrEmpty(path)) { await ViewModels.Welcome.Instance.AddRepositoryAsync(path, to, true, false); From ec12af364a5a5c064b0230f888ee1351301fc6fc Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 29 Sep 2026 11:46:11 +0800 Subject: [PATCH 19/51] code_review: PR #2764 - Remove `Trust this repository` checkbox and `CanSure` property, pressing `OK` means trust this repository - Stop processing drop item when there is an untrusted repository because `SourceGit` only supports to display one popup at a time - Remove `TryShowTrustRepositoryPopup` * Both `OpenRepositoryInTab` and `OpenSubRepository` are used for added repositories. * `GetRepositoryGitDir` uses `Directory.Exist` and `File.Exist` first to get the $GIT_DIR Signed-off-by: leo --- src/Resources/Locales/en_US.axaml | 3 +- src/Resources/Locales/zh_CN.axaml | 2 +- src/Resources/Locales/zh_TW.axaml | 2 +- src/ViewModels/Launcher.cs | 21 ------------ src/ViewModels/LauncherPage.cs | 2 +- src/ViewModels/Popup.cs | 6 ---- src/ViewModels/TrustRepository.cs | 22 +----------- src/ViewModels/Welcome.cs | 35 +++---------------- src/Views/LauncherPage.axaml | 1 - src/Views/TrustRepository.axaml | 21 +++--------- src/Views/Welcome.axaml.cs | 56 +++++++++++++++++++++++++++---- 11 files changed, 65 insertions(+), 106 deletions(-) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index d3a300dd0e..3461158a11 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -998,10 +998,9 @@ Push ${0}$... TERMINATE Untrusted Repository - Will run: + Do you want to run `git config --global --add safe.directory {0}` command to trust this path? Git refused to access this repository: Path: - Trust this repository {0} repositories were skipped because git does not trust them. Please open them manually and confirm whether to trust them. Update Submodules All submodules diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 4f0ddd13d3..ee8ac50f92 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -1002,7 +1002,7 @@ 推送 ${0}$... 终止运行 不受信任的仓库 - 将执行: + 是否执行 `git config --global --add safe.directory {0}` 命令以信任该仓库? Git 拒绝访问该仓库: 路径 : 信任此仓库 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 8286fc84c0..9b7ba96024 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -992,7 +992,7 @@ 推送 ${0}$... 終止執行 不受信任的存放庫 - 將執行: + 您是否要在該路徑執行 `git config --global --add safe.directory {0}` 命令以信任該存放庫? Git 拒絕存取該存放庫: 路徑 : 信任此存放庫 diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 794776f9fa..774fbe0e41 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -330,9 +330,6 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page) var gitDir = isBare ? node.Id : GetRepositoryGitDir(node.Id); if (string.IsNullOrEmpty(gitDir)) { - if (TryShowTrustRepositoryPopup(node.Id, ActivePage)) - return; - ActivePage.Notifications.Add(new Models.Notification { Group = node.Id, @@ -412,9 +409,6 @@ public void OpenSubRepository(LauncherPage ownerPage, string fullpath) var gitDir = GetRepositoryGitDir(normalizedPath); if (string.IsNullOrEmpty(gitDir)) { - if (TryShowTrustRepositoryPopup(normalizedPath, ownerPage)) - return; - ownerPage.Notifications.Add(new Models.Notification { Group = ownerPage.Node.Id, @@ -450,21 +444,6 @@ public void OpenSubRepository(LauncherPage ownerPage, string fullpath) ActivePage = page; } - private bool TryShowTrustRepositoryPopup(string path, LauncherPage page) - { - if (page == null || !page.CanCreatePopup()) - return false; - - var test = new Commands.QueryRepositoryRootPath(path).GetResult(); - if (test.IsSuccess || - !Models.SafeDirectories.IsUntrustedRepository(test.StdErr) || - !Models.SafeDirectories.TryGetSafeDirectoryValue(path, test.StdErr, out var safeDirectory)) - return false; - - page.Popup = new TrustRepository(page.Node.Id, path, test.StdErr, safeDirectory, null, false, true, 0); - return true; - } - private void DispatchNotification(Models.Notification notification) { if (!Dispatcher.UIThread.CheckAccess()) diff --git a/src/ViewModels/LauncherPage.cs b/src/ViewModels/LauncherPage.cs index 73013a4889..370e5a25c7 100644 --- a/src/ViewModels/LauncherPage.cs +++ b/src/ViewModels/LauncherPage.cs @@ -80,7 +80,7 @@ public bool CanCreatePopup() public async Task ProcessPopupAsync() { - if (_popup is { InProgress: false } dump && dump.CanSure) + if (_popup is { InProgress: false } dump) { if (!dump.Check()) return; diff --git a/src/ViewModels/Popup.cs b/src/ViewModels/Popup.cs index 325f2119ad..10bc44fe1b 100644 --- a/src/ViewModels/Popup.cs +++ b/src/ViewModels/Popup.cs @@ -55,12 +55,6 @@ public virtual bool CanStartDirectly() return true; } - /// - /// Whether the `Sure` action is currently allowed. Views should bind the confirm button's - /// `IsEnabled` to this property. - /// - public virtual bool CanSure => true; - public virtual Task Sure() { return null; diff --git a/src/ViewModels/TrustRepository.cs b/src/ViewModels/TrustRepository.cs index ec0a4d2ca4..96615b209d 100644 --- a/src/ViewModels/TrustRepository.cs +++ b/src/ViewModels/TrustRepository.cs @@ -24,22 +24,6 @@ public string Reason get; } - /// - /// Whether the exception should be added into the user's global git config. Disabled by default, - /// so the user has to opt in explicitly. - /// - public bool Permanent - { - get => _permanent; - set - { - if (SetProperty(ref _permanent, value)) - OnPropertyChanged(nameof(CanSure)); - } - } - - public override bool CanSure => Permanent; - public TrustRepository(string pageId, string path, string reason, string safeDirectory, RepositoryNode parent, bool moveNode, bool open, int bookmark) { _pageId = pageId; @@ -50,15 +34,12 @@ public TrustRepository(string pageId, string path, string reason, string safeDir TargetPath = path; SafeDirectory = safeDirectory; - Command = $"git config --global --add safe.directory {safeDirectory.Quoted()}"; + Command = App.Text("TrustRepository.CommandTip", safeDirectory); Reason = GetReason(reason); } public override async Task Sure() { - if (!Permanent) - return false; - var log = new CommandLog("Trust Repository"); Use(log); @@ -120,6 +101,5 @@ private static string GetReason(string stderr) private readonly bool _moveNode; private readonly bool _open; private readonly int _bookmark; - private bool _permanent = false; } } diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index 24ce2ce31f..58707d8a98 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Threading; using System.Threading.Tasks; @@ -100,40 +99,16 @@ public void ToggleNodeIsExpanded(RepositoryNode node) } } - public async Task GetRepositoryRootAsync(string path, RepositoryNode parent = null) + public async Task GetRepositoryRootAsync(string path) { if (!Preferences.Instance.IsGitConfigured()) - { - Models.Notification.Send(null, App.Text("NotConfigured"), true); - return null; - } - - var root = path; - if (!Directory.Exists(root)) - { - if (File.Exists(root)) - root = Path.GetDirectoryName(root); - else - return null; - } + return Commands.Command.Result.Failed(App.Text("NotConfigured")); - var isBare = await new Commands.IsBareRepository(root).GetResultAsync(); + var isBare = await new Commands.IsBareRepository(path).GetResultAsync(); if (isBare) - return root; - - var rs = await new Commands.QueryRepositoryRootPath(root).GetResultAsync(); - if (rs.IsSuccess && !string.IsNullOrWhiteSpace(rs.StdOut)) - return rs.StdOut.Trim(); - - if (Models.SafeDirectories.IsUntrustedRepository(rs.StdErr) && - Models.SafeDirectories.TryGetSafeDirectoryValue(root, rs.StdErr, out var safeDirectory)) - { - var launcher = App.GetLauncher(); - if (launcher?.ActivePage is { } page && page.CanCreatePopup()) - page.Popup = new TrustRepository(page.Node.Id, root, rs.StdErr, safeDirectory, parent, true, false, 0); - } + return new Commands.Command.Result { IsSuccess = true, StdOut = path }; - return null; + return await new Commands.QueryRepositoryRootPath(path).GetResultAsync(); } public async Task AddRepositoryAsync(string path, RepositoryNode parent, bool moveNode, bool open) diff --git a/src/Views/LauncherPage.axaml b/src/Views/LauncherPage.axaml index a5390f327e..08107bfa90 100644 --- a/src/Views/LauncherPage.axaml +++ b/src/Views/LauncherPage.axaml @@ -116,7 +116,6 @@ Padding="0" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" - IsEnabled="{Binding CanSure}" Content="{DynamicResource Text.Sure}" Click="OnPopupSure" ToolTip.Tip="Enter"/> diff --git a/src/Views/TrustRepository.axaml b/src/Views/TrustRepository.axaml index 4706d1fe62..a78e1a49e0 100644 --- a/src/Views/TrustRepository.axaml +++ b/src/Views/TrustRepository.axaml @@ -28,27 +28,16 @@ Text="{Binding TargetPath}"/> - - - - - - - - - - - + + diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index fbc3c193ab..65538875d6 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -335,10 +335,32 @@ private async void DropOnTreeView(object sender, DragEventArgs e) foreach (var item in items) { - var path = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(item.Path.LocalPath); - if (!string.IsNullOrEmpty(path)) + var root = item.Path.LocalPath; + if (!Directory.Exists(root)) { - await ViewModels.Welcome.Instance.AddRepositoryAsync(path, null, true, false); + if (File.Exists(root)) + root = Path.GetDirectoryName(root); + else + continue; + } + + var rs = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(root); + if (!rs.IsSuccess) + { + if (Models.SafeDirectories.IsUntrustedRepository(rs.StdErr) && + Models.SafeDirectories.TryGetSafeDirectoryValue(root, rs.StdErr, out var safeDirectory)) + { + var pageView = this.FindAncestorOfType(); + if (pageView is { DataContext: ViewModels.LauncherPage page }) + { + page.Popup = new ViewModels.TrustRepository(page.Node.Id, root, rs.StdErr, safeDirectory, null, false, false, 0); + break; // Stop processing further because `SourceGit` only supports processing one popup at a time. + } + } + } + else + { + await ViewModels.Welcome.Instance.AddRepositoryAsync(rs.StdOut.Trim(), null, true, false); refresh = true; } } @@ -394,10 +416,32 @@ private async void DropOnTreeNode(object sender, DragEventArgs e) foreach (var item in items) { - var path = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(item.Path.LocalPath, to); - if (!string.IsNullOrEmpty(path)) + var root = item.Path.LocalPath; + if (!Directory.Exists(root)) + { + if (File.Exists(root)) + root = Path.GetDirectoryName(root); + else + continue; + } + + var rs = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(root); + if (!rs.IsSuccess) + { + if (Models.SafeDirectories.IsUntrustedRepository(rs.StdErr) && + Models.SafeDirectories.TryGetSafeDirectoryValue(root, rs.StdErr, out var safeDirectory)) + { + var pageView = this.FindAncestorOfType(); + if (pageView is { DataContext: ViewModels.LauncherPage page }) + { + page.Popup = new ViewModels.TrustRepository(page.Node.Id, root, rs.StdErr, safeDirectory, to, false, false, 0); + break; // Stop processing further because `SourceGit` only supports processing one popup at a time. + } + } + } + else { - await ViewModels.Welcome.Instance.AddRepositoryAsync(path, to, true, false); + await ViewModels.Welcome.Instance.AddRepositoryAsync(rs.StdOut.Trim(), to, true, false); refresh = true; } } From a5e30ec31a4acaadaeee722b589a50b97b01c69d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 29 Sep 2026 03:49:40 +0000 Subject: [PATCH 20/51] doc: Update translation status and sort locale files --- TRANSLATION.md | 99 ++++++++++++++++++++++++++----- src/Resources/Locales/zh_CN.axaml | 1 - src/Resources/Locales/zh_TW.axaml | 1 - 3 files changed, 85 insertions(+), 16 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 9c9219166b..0b52d4422c 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-94.83%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-94.38%25-yellow)
Missing keys in de_DE.axaml @@ -63,13 +63,18 @@ This document shows the translation status of each locale file in the repository - Text.SSHKeyHelper.Title - Text.Statistics.Tips - Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.DiscardAll - Text.WorkingCopy.ExpandSidebar
-### ![el__GR](https://img.shields.io/badge/el__GR-94.36%25-yellow) +### ![el__GR](https://img.shields.io/badge/el__GR-93.91%25-yellow)
Missing keys in el_GR.axaml @@ -130,6 +135,11 @@ This document shows the translation status of each locale file in the repository - Text.SSHKeyHelper.Title - Text.Statistics.Tips - Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.DiscardAll - Text.WorkingCopy.ExpandSidebar @@ -137,7 +147,7 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-98.87%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-98.41%25-yellow)
Missing keys in es_ES.axaml @@ -152,12 +162,17 @@ This document shows the translation status of each locale file in the repository - Text.CheckoutAsWorktree.WorktreePath - Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CommitDetail.Files.Tips +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.ExpandSidebar
-### ![fr__FR](https://img.shields.io/badge/fr__FR-90.59%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-90.17%25-yellow)
Missing keys in fr_FR.axaml @@ -256,6 +271,11 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.Checkout - Text.TagCM.Merge - Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.UpdateSubmodules.Recursive - Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder - Text.WorkingCopy.CollapseSidebar @@ -265,7 +285,7 @@ This document shows the translation status of each locale file in the repository
-### ![he__IL](https://img.shields.io/badge/he__IL-90.59%25-yellow) +### ![he__IL](https://img.shields.io/badge/he__IL-90.17%25-yellow)
Missing keys in he_IL.axaml @@ -364,6 +384,11 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.Checkout - Text.TagCM.Merge - Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.UpdateSubmodules.Recursive - Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder - Text.WorkingCopy.CollapseSidebar @@ -373,7 +398,7 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-94.83%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-94.38%25-yellow)
Missing keys in id_ID.axaml @@ -429,6 +454,11 @@ This document shows the translation status of each locale file in the repository - Text.SSHKeyHelper.Title - Text.Statistics.Tips - Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.DiscardAll - Text.WorkingCopy.ExpandSidebar @@ -436,7 +466,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-84.57%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-84.18%25-yellow)
Missing keys in it_IT.axaml @@ -596,6 +626,11 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.Checkout - Text.TagCM.Merge - Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.UpdateSubmodules.Recursive - Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder - Text.WorkingCopy.CollapseSidebar @@ -608,7 +643,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.41%25-yellow)
Missing keys in ja_JP.axaml @@ -623,12 +658,17 @@ This document shows the translation status of each locale file in the repository - Text.CheckoutAsWorktree.WorktreePath - Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CommitDetail.Files.Tips +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.ExpandSidebar
-### ![ko__KR](https://img.shields.io/badge/ko__KR-92.00%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-91.57%25-yellow)
Missing keys in ko_KR.axaml @@ -712,6 +752,11 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.Checkout - Text.TagCM.Merge - Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.UpdateSubmodules.Recursive - Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder - Text.WorkingCopy.CollapseSidebar @@ -721,7 +766,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-59.55%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-59.27%25-red)
Missing keys in pt_BR.axaml @@ -1126,6 +1171,11 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.DeleteMultiple - Text.TagCM.Merge - Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.UpdateSubmodules.Recursive - Text.UpdateSubmodules.UpdateToRemoteTrackingBranch - Text.ViewLogs @@ -1159,9 +1209,20 @@ 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) +### ![ru__RU](https://img.shields.io/badge/ru__RU-99.53%25-yellow) -### ![ta__IN](https://img.shields.io/badge/ta__IN-61.15%25-red) +
+Missing keys in ru_RU.axaml + +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped + +
+ +### ![ta__IN](https://img.shields.io/badge/ta__IN-60.86%25-red)
Missing keys in ta_IN.axaml @@ -1551,6 +1612,11 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.DeleteMultiple - Text.TagCM.Merge - Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.UpdateSubmodules.Target - Text.UpdateSubmodules.Recursive - Text.UpdateSubmodules.UpdateToRemoteTrackingBranch @@ -1582,7 +1648,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-61.90%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-61.61%25-red)
Missing keys in uk_UA.axaml @@ -1968,6 +2034,11 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.DeleteMultiple - Text.TagCM.Merge - Text.Terminate +- Text.TrustRepository +- Text.TrustRepository.CommandTip +- Text.TrustRepository.Description +- Text.TrustRepository.Path +- Text.TrustRepository.ScanSkipped - Text.UpdateSubmodules.Recursive - Text.UpdateSubmodules.UpdateToRemoteTrackingBranch - Text.ViewLogs @@ -1997,7 +2068,7 @@ This document shows the translation status of each locale file in the repository ### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen) -### ![zh__TW](https://img.shields.io/badge/zh__TW-98.87%25-yellow) +### ![zh__TW](https://img.shields.io/badge/zh__TW-98.88%25-yellow)
Missing keys in zh_TW.axaml diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index ee8ac50f92..be5958e0db 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -1005,7 +1005,6 @@ 是否执行 `git config --global --add safe.directory {0}` 命令以信任该仓库? Git 拒绝访问该仓库: 路径 : - 信任此仓库 有 {0} 个仓库因 git 的信任检查被跳过,请手动打开它们并确认是否信任。 更新子模块 更新所有子模块 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 9b7ba96024..c4dc16a4f5 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -995,7 +995,6 @@ 您是否要在該路徑執行 `git config --global --add safe.directory {0}` 命令以信任該存放庫? Git 拒絕存取該存放庫: 路徑 : - 信任此存放庫 有 {0} 個存放庫因 git 的信任檢查而被略過,請手動開啟並確認是否信任。 更新子模組 更新所有子模組 From 9bdfd4fb6c655a513551b1f9107419d0caa5464b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20J=2E=20Mart=C3=ADnez=20M=2E?= <56406225+jjesus-dev@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:49:50 -0600 Subject: [PATCH 21/51] localization: update Spanish translation (#2749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + Add missing strings. + Fix intances of the word `worktree` into `árbol de trabajo`. --- src/Resources/Locales/es_ES.axaml | 36 ++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index a194095fe6..38472476d8 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -11,9 +11,9 @@ Agregar Archivo(s) Para Ignorar Patrón: Almacenar Archivo: - Agregar Worktree + Agregar Árbol de trabajo Ubicación: - Ruta para este worktree. Se admite ruta relativa. + Ruta para este árbol de trabajo. Se admite ruta relativa. Nombre de la Rama: Opcional. Por defecto es el nombre de la carpeta de destino. Asistente OpenAI @@ -61,7 +61,8 @@ Ignorar cambios de espacios en blanco Mantener presionado Ctrl (en Windows/Linux) o ⌘ (en macOS) cuando se hace click en el SHA del commit reconstruirá lo datos del blame basados en esa revisión. ¡BLAME EN ESTE ARCHIVO NO SOPORTADO! - Checkout ${0}$... + Checkout a ${0}$... + Checkout a ${0}$ como árbol de trabajo... Comparar las 2 ramas seleccionadas Comparar con... Comparar con HEAD @@ -85,7 +86,7 @@ Rebase ${0}$ en ${1}$... Renombrar ${0}$... Resetear ${0}$ a ${1}$... - Cambiar a ${0}$ (worktree) + Cambiar a ${0}$ (árbol de trabajo) Establecer Rama de Seguimiento... {0} commit(s) adelante {0} commit(s) adelante, {1} commit(s) detrás @@ -96,7 +97,7 @@ ESTADO SEGUIMIENTO URL - WORKTREE + ÁRBOL DE TRABAJO CANCELAR Resetear a Revisión Padre Resetear a Esta Revisión @@ -118,6 +119,14 @@ Los siguientes submódulos necesitan ser actualizados:{0} ¿Quieres actualizarlos? Checkout y Fast-Forward Fast-Forward a: + Checkout a Rama como Árbol de trabajo + Rama Local: + Nombre de la Nueva Rama: + Opcional. Por defecto es el nombre de la carpeta de destino. + Rama Remota: + Rastrear esta rama remota. + Ruta del Árbol de trabajo: + Ruta para este árbol de trabajo. Se puede usar una ruta relativa. Checkout a la Rama desde el Stash Nueva Rama: Stash: @@ -152,7 +161,7 @@ Cherry-Pick Este Commit Cherry-Pick ... Comparar con HEAD - Comparar con Worktree + Comparar con Árbol de trabajo Autor Fecha/Hora del Autor Mensaje @@ -182,6 +191,7 @@ Archivo LFS Buscar Archivos... Submódulo + SELECCIONAR ARCHIVO PARA VER SU CONTENIDO INFORMACIÓN AUTOR COMMITTER @@ -732,8 +742,8 @@ Shell/Terminal Podar Remoto Destino: - Podar Worktrees - Podar información de worktree en `$GIT_COMMON_DIR/worktrees` + Podar Árboles de trabajo + Podar información del árbol de trabajo en `$GIT_COMMON_DIR/worktrees` Pull Rama Remota: En: @@ -785,7 +795,7 @@ Abrir En Navegador Podar (Prune) Establecer Push URL... - Confirmar para Eliminar Worktree + Confirmar para Eliminar Árbol de trabajo Utilizar Opción `--force` Destino: Renombrar Rama @@ -859,8 +869,8 @@ Abrir en Terminal Ver Logs Visitar '{0}' en el Navegador - WORKTREES - AÑADIR WORKTREE + ÁRBOLES DE TRABAJO + AÑADIR ÁRBOL DE TRABAJO PRUNE URL del Repositorio Git Resetear Rama Actual a Revisión @@ -1027,6 +1037,7 @@ Puedes hacer stage a este archivo ahora. Limpiar Historial ¿Estás seguro de querer limpiar todo el historial de los mensajes de commit? Esta acción no se puede deshacer. + Ocultar Barra lateral COMMIT COMMIT y PUSH Plantilla/Historias @@ -1043,6 +1054,7 @@ USAR MÍOS USAR SUYOS DESCARTAR TODO + Mostrar Barra lateral Cambios de Filtro... INCLUIR ARCHIVOS SIN RASTREAR NO HAY MENSAJES DE ENTRADA RECIENTES @@ -1060,7 +1072,7 @@ Plantilla: ${0}$ ESPACIO DE TRABAJO: Configura Espacios de Trabajo... - WORKTREE + ÁRBOL DE TRABAJO RAMA Copiar Ruta HEAD From 873b7fcd5aef2a43a3f28447c2eb1b77c36c3b49 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 29 Sep 2026 03:50:00 +0000 Subject: [PATCH 22/51] doc: Update translation status and sort locale files --- TRANSLATION.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 0b52d4422c..e419b78b52 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -147,28 +147,16 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-98.41%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.53%25-yellow)
Missing keys in es_ES.axaml -- Text.BranchCM.CheckoutAsWorktree -- Text.CheckoutAsWorktree -- Text.CheckoutAsWorktree.LocalBranch -- Text.CheckoutAsWorktree.NewBranchName -- Text.CheckoutAsWorktree.NewBranchName.Placeholder -- Text.CheckoutAsWorktree.RemoteBranch -- Text.CheckoutAsWorktree.Tracking -- Text.CheckoutAsWorktree.WorktreePath -- Text.CheckoutAsWorktree.WorktreePath.Placeholder -- Text.CommitDetail.Files.Tips - Text.TrustRepository - Text.TrustRepository.CommandTip - Text.TrustRepository.Description - Text.TrustRepository.Path - Text.TrustRepository.ScanSkipped -- Text.WorkingCopy.CollapseSidebar -- Text.WorkingCopy.ExpandSidebar
From 3c4dd3062396587c2bedc7a5fecee5837c7bb2a9 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 29 Sep 2026 15:35:50 +0800 Subject: [PATCH 23/51] feature: add a pie-chart for top contributors Signed-off-by: leo --- src/Resources/Locales/en_US.axaml | 4 + src/Resources/Locales/zh_CN.axaml | 4 + src/ViewModels/Statistics.cs | 9 +- src/Views/PieChart.cs | 371 ++++++++++++++++++++++++++++++ src/Views/Statistics.axaml | 152 ++++++++---- 5 files changed, 495 insertions(+), 45 deletions(-) create mode 100644 src/Views/PieChart.cs diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 3461158a11..554791dc04 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -950,10 +950,14 @@ CHANGES STASHES Statistics + COMMITS OVERVIEW MONTH WEEK SourceGit will only get a maximum of 200,000 commits according to the target branch for analysis. + TOP AUTHORS + COMMITS + PERCENTAGE AUTHORS: COMMITS: SUBMODULES diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index be5958e0db..14c7e9de19 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -954,10 +954,14 @@ 查看变更 贮藏列表 提交统计 + 提交统计图 总览 本月 本周 SourceGit 仅会按所选分支最多取出 200000 条提交记录进行分析 + 主要贡献者 + 提交数量 + 百分比 贡献者人数: 提交次数: 子模块 diff --git a/src/ViewModels/Statistics.cs b/src/ViewModels/Statistics.cs index 30c29606b0..9d7f620bdb 100644 --- a/src/ViewModels/Statistics.cs +++ b/src/ViewModels/Statistics.cs @@ -45,6 +45,11 @@ public Models.StatisticsReport SelectedReport private set => SetProperty(ref _selectedReport, value); } + public Models.StatisticsAuthor SelectedAuthor + { + get => _selectedAuthor; + } + public Models.StatisticsSamples Samples { get => _samples; @@ -60,9 +65,10 @@ public Statistics(string repo) public void ChangeAuthor(Models.StatisticsAuthor author) { - if (SelectedReport == null) + if (SelectedReport == null || author == _selectedAuthor) return; + _selectedAuthor = author; Samples = SelectedReport.GetSamples(author); } @@ -120,6 +126,7 @@ private void RefreshReport() private Models.Statistics _data = null; private Models.StatisticsMode _viewMode = Models.StatisticsMode.All; private Models.StatisticsReport _selectedReport = null; + private Models.StatisticsAuthor _selectedAuthor = null; private Models.StatisticsSamples _samples = null; } } diff --git a/src/Views/PieChart.cs b/src/Views/PieChart.cs new file mode 100644 index 0000000000..2e9a5a3c5f --- /dev/null +++ b/src/Views/PieChart.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public record PieChartToolTip(string Title, int Count, double Percentage, IBrush Brush); + + public class PieChart : Control + { + public static readonly DirectProperty ReportProperty = + AvaloniaProperty.RegisterDirect( + nameof(Report), + o => o.Report, + (o, v) => o.Report = v); + + public Models.StatisticsReport Report + { + get => _report; + set => SetAndRaise(ReportProperty, ref _report, value); + } + + public static readonly StyledProperty FontFamilyProperty = + AvaloniaProperty.Register(nameof(FontFamily)); + + public FontFamily FontFamily + { + get => GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + public static readonly StyledProperty SecondaryFontFamilyProperty = + AvaloniaProperty.Register(nameof(SecondaryFontFamily)); + + public FontFamily SecondaryFontFamily + { + get => GetValue(SecondaryFontFamilyProperty); + set => SetValue(SecondaryFontFamilyProperty, value); + } + + public static readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground), Brushes.White); + + public IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public static readonly StyledProperty SecondaryForegroundProperty = + AvaloniaProperty.Register(nameof(SecondaryForeground), Brushes.Gray); + + public IBrush SecondaryForeground + { + get => GetValue(SecondaryForegroundProperty); + set => SetValue(SecondaryForegroundProperty, value); + } + + public PieChart() + { + ToolTip.SetShowDelay(this, 0); + ToolTip.SetPlacement(this, PlacementMode.Custom); + ToolTip.SetCustomPopupPlacementCallback(this, PlaceToolTip); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + _arcs.Clear(); + _lastHoverred = null; + + if (_report == null || _report.Authors.Count == 0) + return; + + var w = Bounds.Width; + var h = Bounds.Height; + var foreground = Foreground; + var secondaryForeground = SecondaryForeground; + var typeface = new Typeface(FontFamily); + var secondaryTypeface = new Typeface(SecondaryFontFamily); + + _center = new Point(w / 2, h / 2); + _radius = Math.Min(w, h) / 2 - 48; + + if (_report.Authors.Count == 1) + { + var single = _report.Authors[0]; + var label = new FormattedText( + single.User.Name, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + 12, + Brushes.White); + context.DrawEllipse(s_brushes[0], null, _center, _radius, _radius); + context.DrawText(label, new Point(_center.X - label.Width / 2, _center.Y - label.Height / 2)); + + _arcs.Add(new(0, 2 * Math.PI, single.User.Name, single.Count, 1.0, s_brushes[0], single)); + return; + } + + var total = _report.Total; + var renderedCount = 0; + var startAngle = -Math.PI / 2; + var remaining = 2 * Math.PI; + var brushIndex = 0; + var leftArcs = new List(); + var rightArcs = new List(); + foreach (var author in _report.Authors) + { + var percent = (double)author.Count / total; + var brush = s_brushes[brushIndex]; + if (percent > 0.01) + { + var sweepAngle = percent * 2 * Math.PI; + DrawArc(context, startAngle, sweepAngle, brush); + + var arc = new Arc(startAngle, sweepAngle, author.User.Name, author.Count, percent, brush, author); + _arcs.Add(arc); + if (arc.IsLeftSide) + leftArcs.Add(arc); + else + rightArcs.Add(arc); + + remaining -= sweepAngle; + startAngle += sweepAngle; + brushIndex = (brushIndex + 1) % s_brushes.Length; + renderedCount += author.Count; + } + else + { + var sweepAngle = remaining; + var count = total - renderedCount; + DrawArc(context, startAngle, sweepAngle, brush); + + var arc = new Arc(startAngle, sweepAngle, "Others", count, (double)count / total, brush, null); + _arcs.Add(arc); + if (arc.IsLeftSide) + leftArcs.Add(arc); + else + rightArcs.Add(arc); + break; + } + } + + if (leftArcs.Count > 0) + { + for (var i = leftArcs.Count - 1; i >= 0; i--) + { + var arc = leftArcs[i]; + var pen = new Pen(arc.Tip.Brush, 1); + var lineStartX = _center.X - _radius - 32; + + var edgePoint = new Point( + _center.X + _radius * Math.Cos(arc.MidAngle), + _center.Y + _radius * Math.Sin(arc.MidAngle)); + + var edgeLineStartPoint = new Point( + _center.X + (_radius + 32) * Math.Cos(arc.MidAngle), + _center.Y + (_radius + 32) * Math.Sin(arc.MidAngle)); + + context.DrawLine(pen, new Point(lineStartX, edgeLineStartPoint.Y), edgeLineStartPoint); + context.DrawLine(pen, edgeLineStartPoint, edgePoint); + + var title = new FormattedText( + arc.Tip.Title, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + 12, + foreground); + var percentage = new FormattedText( + $"{arc.Tip.Percentage:P1}", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + secondaryTypeface, + 12, + secondaryForeground); + + var labelStartX = lineStartX - 8 - percentage.WidthIncludingTrailingWhitespace - 4 - title.WidthIncludingTrailingWhitespace; + var labelMidY = edgeLineStartPoint.Y; + context.DrawText(title, new Point(labelStartX, labelMidY - title.Height * 0.5)); + context.DrawText(percentage, new Point(labelStartX + title.WidthIncludingTrailingWhitespace + 4, labelMidY - percentage.Height * 0.5)); + } + } + + if (rightArcs.Count > 0) + { + for (var i = 0; i < rightArcs.Count; i++) + { + var arc = rightArcs[i]; + var pen = new Pen(arc.Tip.Brush, 1); + var lineStartX = _center.X + _radius + 32; + + var edgePoint = new Point( + _center.X + _radius * Math.Cos(arc.MidAngle), + _center.Y + _radius * Math.Sin(arc.MidAngle)); + + var edgeLineStartPoint = new Point( + _center.X + (_radius + 32) * Math.Cos(arc.MidAngle), + _center.Y + (_radius + 32) * Math.Sin(arc.MidAngle)); + + context.DrawLine(pen, new Point(lineStartX, edgeLineStartPoint.Y), edgeLineStartPoint); + context.DrawLine(pen, edgeLineStartPoint, edgePoint); + + var title = new FormattedText( + arc.Tip.Title, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + 12, + foreground); + var percentage = new FormattedText( + $"{arc.Tip.Percentage:P1}", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + secondaryTypeface, + 12, + secondaryForeground); + + var labelStartX = lineStartX + 8; + var labelMidY = edgeLineStartPoint.Y; + context.DrawText(title, new Point(labelStartX, labelMidY - title.Height * 0.5)); + context.DrawText(percentage, new Point(labelStartX + title.WidthIncludingTrailingWhitespace + 4, labelMidY - percentage.Height * 0.5)); + } + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ReportProperty || + change.Property == FontFamilyProperty || + change.Property == ForegroundProperty || + change.Property == SecondaryForegroundProperty) + InvalidateVisual(); + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + + if (_report == null || _report.Authors.Count == 0) + { + ToolTip.SetTip(this, null); + return; + } + + var pos = e.GetPosition(this); + var distance = Math.Sqrt(Math.Pow(pos.X - _center.X, 2) + Math.Pow(pos.Y - _center.Y, 2)); + if (distance > _radius) + { + ToolTip.SetTip(this, null); + return; + } + + var angle = Math.Atan2(pos.Y - _center.Y, pos.X - _center.X); + if (angle < -Math.PI / 2) + angle += 2 * Math.PI; + + foreach (var arc in _arcs) + { + if (angle >= arc.StartAngle && angle < arc.EndAngle) + { + if (_lastHoverred == arc) + return; + + _lastHoverred = arc; + ToolTip.SetTip(this, arc.Tip); + ToolTip.SetIsOpen(this, true); + return; + } + } + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + if (_lastHoverred != null) + { + var statisticsView = this.FindAncestorOfType(); + if (statisticsView is { DataContext: ViewModels.Statistics vm }) + { + if (_lastHoverred.Author == null || _lastHoverred.Author == vm.SelectedAuthor) + vm.ChangeAuthor(null); + else + vm.ChangeAuthor(_lastHoverred.Author); + } + } + } + + private void DrawArc(DrawingContext context, double startAngle, double sweepAngle, IBrush brush) + { + var endAngle = startAngle + sweepAngle; + var startPoint = new Point(_center.X + _radius * Math.Cos(startAngle), _center.Y + _radius * Math.Sin(startAngle)); + var endPoint = new Point(_center.X + _radius * Math.Cos(endAngle), _center.Y + _radius * Math.Sin(endAngle)); + var geometry = new StreamGeometry(); + using (var ctx = geometry.Open()) + { + ctx.BeginFigure(_center, true); + ctx.LineTo(startPoint); + ctx.ArcTo(endPoint, new Size(_radius, _radius), 0, sweepAngle > Math.PI, SweepDirection.Clockwise); + ctx.LineTo(_center); + } + context.DrawGeometry(brush, null, geometry); + } + + private void PlaceToolTip(CustomPopupPlacement placement) + { + if (_lastHoverred == null) + return; + + var angle = (_lastHoverred.StartAngle + _lastHoverred.EndAngle) / 2; + var x = _center.X + (_radius * 0.5) * Math.Cos(angle); + var y = _center.Y + (_radius * 0.5) * Math.Sin(angle); + + placement.Anchor = PopupAnchor.TopLeft; + placement.Gravity = PopupGravity.Bottom; + placement.Offset = new Point(x, y); + } + + private class Arc + { + public double StartAngle { get; set; } + public double EndAngle { get; set; } + public PieChartToolTip Tip { get; set; } + public Models.StatisticsAuthor Author { get; set; } + + public double MidAngle => (StartAngle + EndAngle) / 2; + public bool IsLeftSide => MidAngle > Math.PI * 0.5; + + public Arc(double startAngle, double sweepAngle, string title, int count, double percentage, IBrush brush, Models.StatisticsAuthor author) + { + StartAngle = startAngle; + EndAngle = startAngle + sweepAngle; + Tip = new PieChartToolTip(title, count, percentage, brush); + Author = author; + } + } + + private static readonly IBrush[] s_brushes = new IBrush[] + { + Brushes.Orange, + Brushes.ForestGreen, + Brushes.Turquoise, + Brushes.Olive, + Brushes.Magenta, + Brushes.Red, + Brushes.Khaki, + Brushes.Lime, + Brushes.RoyalBlue, + Brushes.Teal, + }; + + private Models.StatisticsReport _report = null; + private Point _center = new Point(0, 0); + private double _radius = 0; + private List _arcs = new List(); + private Arc _lastHoverred = null; + } +} diff --git a/src/Views/Statistics.axaml b/src/Views/Statistics.axaml index bc397dbad5..0204d8f6b8 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -11,9 +11,10 @@ x:DataType="vm:Statistics" x:Name="ThisControl" Title="{DynamicResource Text.Statistics}" - Width="800" Height="500" + Width="1080" Height="720" + MinWidth="1080" MinHeight="720" WindowStartupLocation="CenterOwner" - CanResize="False"> + CanResize="True"> @@ -33,7 +34,7 @@ IsHitTestVisible="False"/> @@ -208,49 +209,112 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + From fd9c2ad4c03deecfdfb08e996547d5f028d7424c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 29 Sep 2026 07:36:07 +0000 Subject: [PATCH 24/51] doc: Update translation status and sort locale files --- TRANSLATION.md | 84 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 14 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index e419b78b52..51aad2558a 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-94.38%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-94.03%25-yellow)
Missing keys in de_DE.axaml @@ -61,7 +61,11 @@ This document shows the translation status of each locale file in the repository - Text.SSHKeyHelper.Path - Text.SSHKeyHelper.PublicKey - Text.SSHKeyHelper.Title +- Text.Statistics.Commits - Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.Terminate - Text.TrustRepository - Text.TrustRepository.CommandTip @@ -74,7 +78,7 @@ This document shows the translation status of each locale file in the repository
-### ![el__GR](https://img.shields.io/badge/el__GR-93.91%25-yellow) +### ![el__GR](https://img.shields.io/badge/el__GR-93.56%25-yellow)
Missing keys in el_GR.axaml @@ -133,7 +137,11 @@ This document shows the translation status of each locale file in the repository - Text.SSHKeyHelper.Path - Text.SSHKeyHelper.PublicKey - Text.SSHKeyHelper.Title +- Text.Statistics.Commits - Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.Terminate - Text.TrustRepository - Text.TrustRepository.CommandTip @@ -147,11 +155,15 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-99.53%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.16%25-yellow)
Missing keys in es_ES.axaml +- Text.Statistics.Commits +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.TrustRepository - Text.TrustRepository.CommandTip - Text.TrustRepository.Description @@ -160,7 +172,7 @@ This document shows the translation status of each locale file in the repository
-### ![fr__FR](https://img.shields.io/badge/fr__FR-90.17%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-89.83%25-yellow)
Missing keys in fr_FR.axaml @@ -255,7 +267,11 @@ This document shows the translation status of each locale file in the repository - Text.SSHKeyHelper.Path - Text.SSHKeyHelper.PublicKey - Text.SSHKeyHelper.Title +- Text.Statistics.Commits - Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.TagCM.Checkout - Text.TagCM.Merge - Text.Terminate @@ -273,7 +289,7 @@ This document shows the translation status of each locale file in the repository
-### ![he__IL](https://img.shields.io/badge/he__IL-90.17%25-yellow) +### ![he__IL](https://img.shields.io/badge/he__IL-89.83%25-yellow)
Missing keys in he_IL.axaml @@ -368,7 +384,11 @@ This document shows the translation status of each locale file in the repository - Text.SSHKeyHelper.Path - Text.SSHKeyHelper.PublicKey - Text.SSHKeyHelper.Title +- Text.Statistics.Commits - Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.TagCM.Checkout - Text.TagCM.Merge - Text.Terminate @@ -386,7 +406,7 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-94.38%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-94.03%25-yellow)
Missing keys in id_ID.axaml @@ -440,7 +460,11 @@ This document shows the translation status of each locale file in the repository - Text.SSHKeyHelper.Path - Text.SSHKeyHelper.PublicKey - Text.SSHKeyHelper.Title +- Text.Statistics.Commits - Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.Terminate - Text.TrustRepository - Text.TrustRepository.CommandTip @@ -454,7 +478,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-84.18%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-83.86%25-yellow)
Missing keys in it_IT.axaml @@ -608,7 +632,11 @@ This document shows the translation status of each locale file in the repository - Text.SSHKeyHelper.Title - Text.StashCM.ApplyFileChanges - Text.StashCM.Branch +- Text.Statistics.Commits - Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.SubmoduleRevisionCompare - Text.SubmoduleRevisionCompare.OpenDetails - Text.TagCM.Checkout @@ -631,7 +659,7 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-98.41%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-98.04%25-yellow)
Missing keys in ja_JP.axaml @@ -646,6 +674,10 @@ This document shows the translation status of each locale file in the repository - Text.CheckoutAsWorktree.WorktreePath - Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CommitDetail.Files.Tips +- Text.Statistics.Commits +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.TrustRepository - Text.TrustRepository.CommandTip - Text.TrustRepository.Description @@ -656,7 +688,7 @@ This document shows the translation status of each locale file in the repository
-### ![ko__KR](https://img.shields.io/badge/ko__KR-91.57%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-91.23%25-yellow)
Missing keys in ko_KR.axaml @@ -736,7 +768,11 @@ This document shows the translation status of each locale file in the repository - Text.SSHKeyHelper.Path - Text.SSHKeyHelper.PublicKey - Text.SSHKeyHelper.Title +- Text.Statistics.Commits - Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.TagCM.Checkout - Text.TagCM.Merge - Text.Terminate @@ -754,7 +790,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-59.27%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-59.05%25-red)
Missing keys in pt_BR.axaml @@ -1128,7 +1164,11 @@ This document shows the translation status of each locale file in the repository - Text.StashCM.Branch - Text.StashCM.CopyMessage - Text.StashCM.SaveAsPatch +- Text.Statistics.Commits - Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.Submodule.Branch - Text.Submodule.CopyBranch - Text.Submodule.Deinit @@ -1197,11 +1237,15 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-99.53%25-yellow) +### ![ru__RU](https://img.shields.io/badge/ru__RU-99.16%25-yellow)
Missing keys in ru_RU.axaml +- Text.Statistics.Commits +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.TrustRepository - Text.TrustRepository.CommandTip - Text.TrustRepository.Description @@ -1210,7 +1254,7 @@ This document shows the translation status of each locale file in the repository
-### ![ta__IN](https://img.shields.io/badge/ta__IN-60.86%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-60.63%25-red)
Missing keys in ta_IN.axaml @@ -1569,7 +1613,11 @@ This document shows the translation status of each locale file in the repository - Text.StashCM.ApplyFileChanges - Text.StashCM.Branch - Text.StashCM.CopyMessage +- Text.Statistics.Commits - Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.Submodule.Branch - Text.Submodule.CopyBranch - Text.Submodule.Deinit @@ -1636,7 +1684,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-61.61%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-61.38%25-red)
Missing keys in uk_UA.axaml @@ -1991,7 +2039,11 @@ This document shows the translation status of each locale file in the repository - Text.StashCM.ApplyFileChanges - Text.StashCM.Branch - Text.StashCM.CopyMessage +- Text.Statistics.Commits - Text.Statistics.Tips +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.Submodule.Branch - Text.Submodule.CopyBranch - Text.Submodule.Deinit @@ -2056,7 +2108,7 @@ This document shows the translation status of each locale file in the repository ### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen) -### ![zh__TW](https://img.shields.io/badge/zh__TW-98.88%25-yellow) +### ![zh__TW](https://img.shields.io/badge/zh__TW-98.51%25-yellow)
Missing keys in zh_TW.axaml @@ -2071,6 +2123,10 @@ This document shows the translation status of each locale file in the repository - Text.CheckoutAsWorktree.WorktreePath - Text.CheckoutAsWorktree.WorktreePath.Placeholder - Text.CommitDetail.Files.Tips +- Text.Statistics.Commits +- Text.Statistics.TopAuthors +- Text.Statistics.TopAuthors.Count +- Text.Statistics.TopAuthors.Percentage - Text.WorkingCopy.CollapseSidebar - Text.WorkingCopy.ExpandSidebar From 6b852ea3d142e3d3342f848b85a2d48bb3583621 Mon Sep 17 00:00:00 2001 From: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:31:12 +0800 Subject: [PATCH 25/51] localization: update Traditional Chinese translations (#2750) --- src/Resources/Locales/zh_TW.axaml | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index c4dc16a4f5..95a7a4644d 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -62,6 +62,7 @@ 按住 Ctrl 鍵 (Windows/Linux) 或 ⌘ 鍵 (macOS) 並點擊提交編號,將依據該次提交重新執行逐行溯源 所選擇的檔案不支援該操作! 簽出 (checkout) ${0}$... + 簽出 (checkout) ${0}$ 為工作樹... 比較所選的 2 個分支 與其他分支或標籤比較... 與目前 HEAD 比較 @@ -118,6 +119,14 @@ 以下子模組需要更新: {0},您要立即更新嗎? 簽出分支並快轉 上游分支: + 簽出分支為工作樹 + 本機分支: + 新分支名稱: + 選填。預設使用目標資料夾名稱。 + 遠端分支: + 追蹤此遠端分支 + 工作樹路徑: + 填寫該工作樹的路徑。支援相對路徑。 從所選擱置簽出分支 新分支: 所選擱置: @@ -182,6 +191,7 @@ LFS 檔案 搜尋檔案... 子模組 + 請選擇要檢視內容的檔案 基本資訊 作者 提交者 @@ -364,7 +374,7 @@ 標籤名稱: 同時刪除遠端存放庫中的此標籤 二進位檔案 - 查看新內容 + 檢視新內容 空檔案 第一個差異 忽略空白符號變化 @@ -944,10 +954,14 @@ 檢視變更 擱置變更列表 提交統計 + 提交統計圖 總覽 本月 本週 SourceGit 僅會依所選分支取出最多 200,000 筆提交記錄進行分析 + 主要貢獻者 + 提交次數 + 百分比 貢獻者人數: 提交次數: 子模組 @@ -992,9 +1006,9 @@ 推送 ${0}$... 終止執行 不受信任的存放庫 - 您是否要在該路徑執行 `git config --global --add safe.directory {0}` 命令以信任該存放庫? - Git 拒絕存取該存放庫: - 路徑 : + 您是否要執行 `git config --global --add safe.directory {0}` 命令以信任該存放庫? + Git 拒絕存取該存放庫: + 路徑: 有 {0} 個存放庫因 git 的信任檢查而被略過,請手動開啟並確認是否信任。 更新子模組 更新所有子模組 @@ -1032,6 +1046,7 @@ 現在您已可將其加入暫存區中 清除提交訊息記錄 您確定要清除所有提交訊息記錄嗎 (執行後無法復原)? + 收合側邊欄 提 交 提交並推送 提交訊息記錄/範本 @@ -1048,6 +1063,7 @@ 使用我方版本 (ours) 使用對方版本 (theirs) 捨棄所有變更 + 展開側邊欄 搜尋變更... 顯示未追蹤檔案 沒有提交訊息記錄 From e302d843f9c5ef14dcb1f907a9f55a73e196c458 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 29 Sep 2026 08:31:27 +0000 Subject: [PATCH 26/51] doc: Update translation status and sort locale files --- TRANSLATION.md | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 51aad2558a..22a20460bb 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -2108,26 +2108,4 @@ This document shows the translation status of each locale file in the repository ### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen) -### ![zh__TW](https://img.shields.io/badge/zh__TW-98.51%25-yellow) - -
-Missing keys in zh_TW.axaml - -- Text.BranchCM.CheckoutAsWorktree -- Text.CheckoutAsWorktree -- Text.CheckoutAsWorktree.LocalBranch -- Text.CheckoutAsWorktree.NewBranchName -- Text.CheckoutAsWorktree.NewBranchName.Placeholder -- Text.CheckoutAsWorktree.RemoteBranch -- Text.CheckoutAsWorktree.Tracking -- Text.CheckoutAsWorktree.WorktreePath -- Text.CheckoutAsWorktree.WorktreePath.Placeholder -- Text.CommitDetail.Files.Tips -- Text.Statistics.Commits -- Text.Statistics.TopAuthors -- Text.Statistics.TopAuthors.Count -- Text.Statistics.TopAuthors.Percentage -- Text.WorkingCopy.CollapseSidebar -- Text.WorkingCopy.ExpandSidebar - -
\ No newline at end of file +### ![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen) \ No newline at end of file From 98d4d850b480c7839aa84a65e8a7870b6248bf93 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 29 Sep 2026 17:27:05 +0800 Subject: [PATCH 27/51] ux: show page skeleton when the samples is empty in `Statistics` page Signed-off-by: leo --- src/Views/Chart.cs | 5 ++++- src/Views/PieChart.cs | 12 ++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Views/Chart.cs b/src/Views/Chart.cs index 392e271f40..627e6f0516 100644 --- a/src/Views/Chart.cs +++ b/src/Views/Chart.cs @@ -54,7 +54,7 @@ public override void Render(DrawingContext context) base.Render(context); var samples = _samples; - if (samples == null || samples.Count == 0) + if (samples == null) return; var w = Bounds.Width; @@ -94,6 +94,9 @@ public override void Render(DrawingContext context) context.DrawLine(labelPen, new Point(leftMargin, y), new Point(w, y)); } + if (count == 0) + return; + var step = Math.Max((w - leftMargin) / count, 10.0); _maxOffsetX = Math.Max(count * step - (w - leftMargin), 0); _hitBoxes.Clear(); diff --git a/src/Views/PieChart.cs b/src/Views/PieChart.cs index 2e9a5a3c5f..b65c7d091a 100644 --- a/src/Views/PieChart.cs +++ b/src/Views/PieChart.cs @@ -77,9 +77,6 @@ public override void Render(DrawingContext context) _arcs.Clear(); _lastHoverred = null; - if (_report == null || _report.Authors.Count == 0) - return; - var w = Bounds.Width; var h = Bounds.Height; var foreground = Foreground; @@ -90,7 +87,14 @@ public override void Render(DrawingContext context) _center = new Point(w / 2, h / 2); _radius = Math.Min(w, h) / 2 - 48; - if (_report.Authors.Count == 1) + if (_report == null || _report.Authors.Count == 0) + { + var label = new FormattedText("0", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 14, Brushes.White); + context.DrawEllipse(null, new Pen(s_brushes[0]), _center, _radius, _radius); + context.DrawText(label, new Point(_center.X - label.Width / 2, _center.Y - label.Height / 2)); + return; + } + else if (_report.Authors.Count == 1) { var single = _report.Authors[0]; var label = new FormattedText( From 1d330703aef2756de6f69aab741a4ffa1fd2e374 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 29 Sep 2026 17:41:18 +0800 Subject: [PATCH 28/51] ux: use theme foreground color Signed-off-by: leo --- src/Views/PieChart.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/PieChart.cs b/src/Views/PieChart.cs index b65c7d091a..ac0453e3fa 100644 --- a/src/Views/PieChart.cs +++ b/src/Views/PieChart.cs @@ -89,7 +89,7 @@ public override void Render(DrawingContext context) if (_report == null || _report.Authors.Count == 0) { - var label = new FormattedText("0", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 14, Brushes.White); + var label = new FormattedText("0", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 14, foreground); context.DrawEllipse(null, new Pen(s_brushes[0]), _center, _radius, _radius); context.DrawText(label, new Point(_center.X - label.Width / 2, _center.Y - label.Height / 2)); return; From 6401729115ffa425810c40727728f2364b985718 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 29 Sep 2026 18:24:15 +0800 Subject: [PATCH 29/51] ux: another way to show hovered part in pie-chart Signed-off-by: leo --- src/Views/PieChart.cs | 47 +++++++++++++++++--------------------- src/Views/Statistics.axaml | 22 +++++++++++++----- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/Views/PieChart.cs b/src/Views/PieChart.cs index ac0453e3fa..e1fc3cccee 100644 --- a/src/Views/PieChart.cs +++ b/src/Views/PieChart.cs @@ -4,7 +4,6 @@ using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Media; using Avalonia.VisualTree; @@ -27,6 +26,17 @@ public Models.StatisticsReport Report set => SetAndRaise(ReportProperty, ref _report, value); } + public static readonly DirectProperty HoveringTipProperty = + AvaloniaProperty.RegisterDirect( + nameof(HoveringTip), + o => o.HoveringTip); + + public PieChartToolTip HoveringTip + { + get => _hoveringTip; + private set => SetAndRaise(HoveringTipProperty, ref _hoveringTip, value); + } + public static readonly StyledProperty FontFamilyProperty = AvaloniaProperty.Register(nameof(FontFamily)); @@ -63,13 +73,6 @@ public IBrush SecondaryForeground set => SetValue(SecondaryForegroundProperty, value); } - public PieChart() - { - ToolTip.SetShowDelay(this, 0); - ToolTip.SetPlacement(this, PlacementMode.Custom); - ToolTip.SetCustomPopupPlacementCallback(this, PlaceToolTip); - } - public override void Render(DrawingContext context) { base.Render(context); @@ -86,6 +89,7 @@ public override void Render(DrawingContext context) _center = new Point(w / 2, h / 2); _radius = Math.Min(w, h) / 2 - 48; + context.FillRectangle(Brushes.Transparent, new Rect(0, 0, w, h)); if (_report == null || _report.Authors.Count == 0) { @@ -255,7 +259,8 @@ protected override void OnPointerMoved(PointerEventArgs e) if (_report == null || _report.Authors.Count == 0) { - ToolTip.SetTip(this, null); + _lastHoverred = null; + HoveringTip = null; return; } @@ -263,7 +268,8 @@ protected override void OnPointerMoved(PointerEventArgs e) var distance = Math.Sqrt(Math.Pow(pos.X - _center.X, 2) + Math.Pow(pos.Y - _center.Y, 2)); if (distance > _radius) { - ToolTip.SetTip(this, null); + _lastHoverred = null; + HoveringTip = null; return; } @@ -279,11 +285,13 @@ protected override void OnPointerMoved(PointerEventArgs e) return; _lastHoverred = arc; - ToolTip.SetTip(this, arc.Tip); - ToolTip.SetIsOpen(this, true); + HoveringTip = arc.Tip; return; } } + + _lastHoverred = null; + HoveringTip = null; } protected override void OnPointerPressed(PointerPressedEventArgs e) @@ -319,20 +327,6 @@ private void DrawArc(DrawingContext context, double startAngle, double sweepAngl context.DrawGeometry(brush, null, geometry); } - private void PlaceToolTip(CustomPopupPlacement placement) - { - if (_lastHoverred == null) - return; - - var angle = (_lastHoverred.StartAngle + _lastHoverred.EndAngle) / 2; - var x = _center.X + (_radius * 0.5) * Math.Cos(angle); - var y = _center.Y + (_radius * 0.5) * Math.Sin(angle); - - placement.Anchor = PopupAnchor.TopLeft; - placement.Gravity = PopupGravity.Bottom; - placement.Offset = new Point(x, y); - } - private class Arc { public double StartAngle { get; set; } @@ -371,5 +365,6 @@ public Arc(double startAngle, double sweepAngle, string title, int count, double private double _radius = 0; private List _arcs = new List(); private Arc _lastHoverred = null; + private PieChartToolTip _hoveringTip = null; } } diff --git a/src/Views/Statistics.axaml b/src/Views/Statistics.axaml index 0204d8f6b8..6d2fa3e102 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -218,16 +218,26 @@ Foreground="{DynamicResource Brush.FG2}"/> - + SecondaryForeground="{DynamicResource Brush.FG2}"/> + + + - - + + - - + + Date: Tue, 29 Sep 2026 18:27:18 +0800 Subject: [PATCH 30/51] ux: allow to deselect selected author in Statistics window (#2751) --- src/Views/Statistics.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/Statistics.axaml b/src/Views/Statistics.axaml index 6d2fa3e102..669709d4a4 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -135,7 +135,7 @@ Date: Tue, 29 Sep 2026 18:35:28 +0800 Subject: [PATCH 31/51] enhance: clear hovering tip after that mouse leaved Signed-off-by: leo --- src/Views/PieChart.cs | 6 ++++++ src/Views/Statistics.axaml | 1 + 2 files changed, 7 insertions(+) diff --git a/src/Views/PieChart.cs b/src/Views/PieChart.cs index e1fc3cccee..b143bcbc79 100644 --- a/src/Views/PieChart.cs +++ b/src/Views/PieChart.cs @@ -246,6 +246,12 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { base.OnPropertyChanged(change); + if (change.Property == ReportProperty) + { + _lastHoverred = null; + HoveringTip = null; + } + if (change.Property == ReportProperty || change.Property == FontFamilyProperty || change.Property == ForegroundProperty || diff --git a/src/Views/Statistics.axaml b/src/Views/Statistics.axaml index 669709d4a4..ae88d3f5b2 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -229,6 +229,7 @@ From dbf2837ee73295c52e277c319e4bbeed166f544a Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 30 Sep 2026 15:00:32 +0800 Subject: [PATCH 32/51] feature: allow to maximize `Statistics` dialog by double-clicking the titlebar Signed-off-by: leo --- src/Views/Statistics.axaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Views/Statistics.axaml b/src/Views/Statistics.axaml index ae88d3f5b2..c7d41e2a90 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -20,6 +20,7 @@ Date: Wed, 30 Sep 2026 15:48:46 +0800 Subject: [PATCH 33/51] ux: add a tooltip for top authors Signed-off-by: leo --- src/Resources/Locales/en_US.axaml | 1 + src/Resources/Locales/zh_CN.axaml | 1 + src/Views/Statistics.axaml | 20 +++++++++++++------- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 554791dc04..76ac9e4dcb 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -958,6 +958,7 @@ TOP AUTHORS COMMITS PERCENTAGE + Authors with less than 1% commits will be grouped under Others. AUTHORS: COMMITS: SUBMODULES diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 14c7e9de19..d24f14b1e8 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -962,6 +962,7 @@ 主要贡献者 提交数量 百分比 + 提交占比小于1%的作者会被统计入Others分组 贡献者人数: 提交次数: 子模块 diff --git a/src/Views/Statistics.axaml b/src/Views/Statistics.axaml index c7d41e2a90..48a26774cf 100644 --- a/src/Views/Statistics.axaml +++ b/src/Views/Statistics.axaml @@ -211,13 +211,19 @@ - - + + + + + + + Date: Wed, 30 Sep 2026 07:49:10 +0000 Subject: [PATCH 34/51] doc: Update translation status and sort locale files --- TRANSLATION.md | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 22a20460bb..4fd9c1a9f4 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-94.03%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-93.94%25-yellow)
Missing keys in de_DE.axaml @@ -66,6 +66,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.Terminate - Text.TrustRepository - Text.TrustRepository.CommandTip @@ -78,7 +79,7 @@ This document shows the translation status of each locale file in the repository
-### ![el__GR](https://img.shields.io/badge/el__GR-93.56%25-yellow) +### ![el__GR](https://img.shields.io/badge/el__GR-93.48%25-yellow)
Missing keys in el_GR.axaml @@ -142,6 +143,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.Terminate - Text.TrustRepository - Text.TrustRepository.CommandTip @@ -155,7 +157,7 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-99.16%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.07%25-yellow)
Missing keys in es_ES.axaml @@ -164,6 +166,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.TrustRepository - Text.TrustRepository.CommandTip - Text.TrustRepository.Description @@ -172,7 +175,7 @@ This document shows the translation status of each locale file in the repository
-### ![fr__FR](https://img.shields.io/badge/fr__FR-89.83%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-89.75%25-yellow)
Missing keys in fr_FR.axaml @@ -272,6 +275,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.TagCM.Checkout - Text.TagCM.Merge - Text.Terminate @@ -289,7 +293,7 @@ This document shows the translation status of each locale file in the repository
-### ![he__IL](https://img.shields.io/badge/he__IL-89.83%25-yellow) +### ![he__IL](https://img.shields.io/badge/he__IL-89.75%25-yellow)
Missing keys in he_IL.axaml @@ -389,6 +393,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.TagCM.Checkout - Text.TagCM.Merge - Text.Terminate @@ -406,7 +411,7 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-94.03%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-93.94%25-yellow)
Missing keys in id_ID.axaml @@ -465,6 +470,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.Terminate - Text.TrustRepository - Text.TrustRepository.CommandTip @@ -478,7 +484,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-83.86%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-83.78%25-yellow)
Missing keys in it_IT.axaml @@ -637,6 +643,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.SubmoduleRevisionCompare - Text.SubmoduleRevisionCompare.OpenDetails - Text.TagCM.Checkout @@ -659,7 +666,7 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-98.04%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-97.95%25-yellow)
Missing keys in ja_JP.axaml @@ -678,6 +685,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.TrustRepository - Text.TrustRepository.CommandTip - Text.TrustRepository.Description @@ -688,7 +696,7 @@ This document shows the translation status of each locale file in the repository
-### ![ko__KR](https://img.shields.io/badge/ko__KR-91.23%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-91.15%25-yellow)
Missing keys in ko_KR.axaml @@ -773,6 +781,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.TagCM.Checkout - Text.TagCM.Merge - Text.Terminate @@ -790,7 +799,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-59.05%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-58.99%25-red)
Missing keys in pt_BR.axaml @@ -1169,6 +1178,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.Submodule.Branch - Text.Submodule.CopyBranch - Text.Submodule.Deinit @@ -1237,7 +1247,7 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-99.16%25-yellow) +### ![ru__RU](https://img.shields.io/badge/ru__RU-99.07%25-yellow)
Missing keys in ru_RU.axaml @@ -1246,6 +1256,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.TrustRepository - Text.TrustRepository.CommandTip - Text.TrustRepository.Description @@ -1254,7 +1265,7 @@ This document shows the translation status of each locale file in the repository
-### ![ta__IN](https://img.shields.io/badge/ta__IN-60.63%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-60.58%25-red)
Missing keys in ta_IN.axaml @@ -1618,6 +1629,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.Submodule.Branch - Text.Submodule.CopyBranch - Text.Submodule.Deinit @@ -1684,7 +1696,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-61.38%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-61.32%25-red)
Missing keys in uk_UA.axaml @@ -2044,6 +2056,7 @@ This document shows the translation status of each locale file in the repository - Text.Statistics.TopAuthors - Text.Statistics.TopAuthors.Count - Text.Statistics.TopAuthors.Percentage +- Text.Statistics.TopAuthors.Tip - Text.Submodule.Branch - Text.Submodule.CopyBranch - Text.Submodule.Deinit @@ -2108,4 +2121,11 @@ This document shows the translation status of each locale file in the repository ### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen) -### ![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen) \ No newline at end of file +### ![zh__TW](https://img.shields.io/badge/zh__TW-99.91%25-yellow) + +
+Missing keys in zh_TW.axaml + +- Text.Statistics.TopAuthors.Tip + +
\ No newline at end of file From 06c7c539c0ae8f7a316d24d6ee2ad0c16881719c Mon Sep 17 00:00:00 2001 From: aiapienthusiast <327629069+aiapienthusiast@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:16:12 +0400 Subject: [PATCH 35/51] docs: document Cheaper Inference as an OpenAI-compatible service (#2755) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 53baa34274..ec610c1043 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,12 @@ For `OrcaRouter`: * `API Key` should be an OrcaRouter API key (prefixed with `sk-orca-`) * A good starting `Model` is `orcarouter/auto`, which routes each request to the best available model +For `Cheaper Inference`: + +* `Server` should be `https://api.cheaperinference.com/v1` +* `API Key` should be a Cheaper Inference API key (prefixed with `ci_live_`) +* A good starting `Model` is `gpt-5.4-mini` + For other AI service: * The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1`. For example, when using `Ollama`, it should be `http://localhost:11434/v1` instead of `http://localhost:11434/api/generate` From 783f0ba9f6eb993f1f592c3a79aeea67628ccfeb Mon Sep 17 00:00:00 2001 From: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:16:28 +0800 Subject: [PATCH 36/51] localization: update Traditional Chinese translations (#2756) --- src/Resources/Locales/zh_TW.axaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 95a7a4644d..97680c6103 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -185,7 +185,7 @@ 另存為修補檔 (patch)... 變更對比 個檔案已變更 - 搜尋變更... + 篩選變更... 收合詳細面板 檔案列表 LFS 檔案 @@ -817,7 +817,7 @@ 沒有自訂的動作 捨棄所有變更 在檔案瀏覽器中開啟 - 快速搜尋分支/標籤/子模組 + 篩選分支/標籤/子模組 篩選以顯示或隱藏 摺疊篩選器 取消指定 @@ -962,6 +962,7 @@ 主要貢獻者 提交次數 百分比 + 提交佔比低於 1% 的貢獻者會被歸入 Others 分組 貢獻者人數: 提交次數: 子模組 @@ -1034,7 +1035,7 @@ 開啟本機存放庫 開啟終端機 重新掃描預設複製 (clone) 目錄下的存放庫 - 快速搜尋存放庫... + 篩選存放庫... 本機變更 加入至 .gitignore 忽略清單 忽略所有 *{0} 檔案 @@ -1064,7 +1065,7 @@ 使用對方版本 (theirs) 捨棄所有變更 展開側邊欄 - 搜尋變更... + 篩選變更... 顯示未追蹤檔案 沒有提交訊息記錄 沒有可套用的提交訊息範本 From 4433baf4c9523978e06e2d69250da256c7dec924 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 8 Oct 2026 02:16:39 +0000 Subject: [PATCH 37/51] doc: Update translation status and sort locale files --- TRANSLATION.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 4fd9c1a9f4..1ddc5ac9bd 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -2121,11 +2121,4 @@ This document shows the translation status of each locale file in the repository ### ![zh__CN](https://img.shields.io/badge/zh__CN-%E2%88%9A-brightgreen) -### ![zh__TW](https://img.shields.io/badge/zh__TW-99.91%25-yellow) - -
-Missing keys in zh_TW.axaml - -- Text.Statistics.TopAuthors.Tip - -
\ No newline at end of file +### ![zh__TW](https://img.shields.io/badge/zh__TW-%E2%88%9A-brightgreen) \ No newline at end of file From dbfde1a07265f4c1742c9b991f7317ef71cc3713 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 8 Oct 2026 10:30:21 +0800 Subject: [PATCH 38/51] ux: pie-chart - Use `Brushes.Gray` for `Others` group - Rename `_lastHoverred` to `_lastHovered` - Make sure that the last slice uses a different color with the first one Co-authored-by: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Signed-off-by: leo --- src/Views/PieChart.cs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Views/PieChart.cs b/src/Views/PieChart.cs index b143bcbc79..0da216d9d9 100644 --- a/src/Views/PieChart.cs +++ b/src/Views/PieChart.cs @@ -78,7 +78,7 @@ public override void Render(DrawingContext context) base.Render(context); _arcs.Clear(); - _lastHoverred = null; + _lastHovered = null; var w = Bounds.Width; var h = Bounds.Height; @@ -119,15 +119,16 @@ public override void Render(DrawingContext context) var renderedCount = 0; var startAngle = -Math.PI / 2; var remaining = 2 * Math.PI; - var brushIndex = 0; var leftArcs = new List(); var rightArcs = new List(); - foreach (var author in _report.Authors) + for (var i = 0; i < _report.Authors.Count; i++) { + var author = _report.Authors[i]; var percent = (double)author.Count / total; - var brush = s_brushes[brushIndex]; if (percent > 0.01) { + var brushIdx = i % s_brushes.Length; + var brush = (i == _report.Authors.Count - 1 && brushIdx == 0) ? s_brushes[1] : s_brushes[brushIdx]; var sweepAngle = percent * 2 * Math.PI; DrawArc(context, startAngle, sweepAngle, brush); @@ -140,16 +141,15 @@ public override void Render(DrawingContext context) remaining -= sweepAngle; startAngle += sweepAngle; - brushIndex = (brushIndex + 1) % s_brushes.Length; renderedCount += author.Count; } else { var sweepAngle = remaining; var count = total - renderedCount; - DrawArc(context, startAngle, sweepAngle, brush); + DrawArc(context, startAngle, sweepAngle, Brushes.Gray); - var arc = new Arc(startAngle, sweepAngle, "Others", count, (double)count / total, brush, null); + var arc = new Arc(startAngle, sweepAngle, "Others", count, (double)count / total, Brushes.Gray, null); _arcs.Add(arc); if (arc.IsLeftSide) leftArcs.Add(arc); @@ -248,7 +248,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == ReportProperty) { - _lastHoverred = null; + _lastHovered = null; HoveringTip = null; } @@ -265,7 +265,7 @@ protected override void OnPointerMoved(PointerEventArgs e) if (_report == null || _report.Authors.Count == 0) { - _lastHoverred = null; + _lastHovered = null; HoveringTip = null; return; } @@ -274,7 +274,7 @@ protected override void OnPointerMoved(PointerEventArgs e) var distance = Math.Sqrt(Math.Pow(pos.X - _center.X, 2) + Math.Pow(pos.Y - _center.Y, 2)); if (distance > _radius) { - _lastHoverred = null; + _lastHovered = null; HoveringTip = null; return; } @@ -287,16 +287,16 @@ protected override void OnPointerMoved(PointerEventArgs e) { if (angle >= arc.StartAngle && angle < arc.EndAngle) { - if (_lastHoverred == arc) + if (_lastHovered == arc) return; - _lastHoverred = arc; + _lastHovered = arc; HoveringTip = arc.Tip; return; } } - _lastHoverred = null; + _lastHovered = null; HoveringTip = null; } @@ -304,15 +304,15 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); - if (_lastHoverred != null) + if (_lastHovered != null) { var statisticsView = this.FindAncestorOfType(); if (statisticsView is { DataContext: ViewModels.Statistics vm }) { - if (_lastHoverred.Author == null || _lastHoverred.Author == vm.SelectedAuthor) + if (_lastHovered.Author == null || _lastHovered.Author == vm.SelectedAuthor) vm.ChangeAuthor(null); else - vm.ChangeAuthor(_lastHoverred.Author); + vm.ChangeAuthor(_lastHovered.Author); } } } @@ -370,7 +370,7 @@ public Arc(double startAngle, double sweepAngle, string title, int count, double private Point _center = new Point(0, 0); private double _radius = 0; private List _arcs = new List(); - private Arc _lastHoverred = null; + private Arc _lastHovered = null; private PieChartToolTip _hoveringTip = null; } } From 5706d2cbe17984d8ba1cc1fb1e46fa64062392e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20J=2E=20Mart=C3=ADnez=20M=2E?= <56406225+jjesus-dev@users.noreply.github.com> Date: Wed, 7 Oct 2026 20:35:20 -0600 Subject: [PATCH 39/51] localization: update Spanish translation (#2759) + Add missing strings. --- src/Resources/Locales/es_ES.axaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 38472476d8..e5a8c77e75 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -954,10 +954,15 @@ CAMBIOS STASHES Estadísticas + COMMITS GENERAL MES SEMANA SourceGit solamente obtendrá un máximo de 200,000 commits de acuerdo a la rama destino para su análisis. + AUTORES DESTACADOS + COMMITS + PORCENTAJE + Autores con menos del 1% de commits serán agrupados bajo Otros. AUTORES: COMMITS: SUBMÓDULOS @@ -1001,6 +1006,11 @@ Hacer merge de ${0}$ a ${1}$... Hacer push a ${0}$... FINALIZAR + Repositorio No confiable + ¿Quieres correr el comando `git config --global --add safe.directory {0}` para confiar en está ruta? + Git rechazó el acceso a este repositorio: + Ruta: + {0} repositorios fueron omitidos porque git no confía en ellos. Ábrelos manualmente por favor y confirma si confías en ellos. Actualizar Submódulos Todos los submódulos Inicializar según sea necesario From dc0754a3861989d7a4fc285d4ca982c3d6261c04 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 8 Oct 2026 02:35:36 +0000 Subject: [PATCH 40/51] doc: Update translation status and sort locale files --- TRANSLATION.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 1ddc5ac9bd..75cf1c1c23 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -157,23 +157,7 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-99.07%25-yellow) - -
-Missing keys in es_ES.axaml - -- Text.Statistics.Commits -- Text.Statistics.TopAuthors -- Text.Statistics.TopAuthors.Count -- Text.Statistics.TopAuthors.Percentage -- Text.Statistics.TopAuthors.Tip -- Text.TrustRepository -- Text.TrustRepository.CommandTip -- Text.TrustRepository.Description -- Text.TrustRepository.Path -- Text.TrustRepository.ScanSkipped - -
+### ![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen) ### ![fr__FR](https://img.shields.io/badge/fr__FR-89.75%25-yellow) From b256212a27ca37ec582bacd545b9f2667d6f16f8 Mon Sep 17 00:00:00 2001 From: Flo Date: Thu, 8 Oct 2026 04:46:18 +0200 Subject: [PATCH 41/51] enhance: embed AppImage update information and publish zsync files (#2762) --- .github/workflows/package.yml | 1 + build/scripts/package.linux.sh | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 0845774fb3..9397d78e2b 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -103,6 +103,7 @@ jobs: name: package.${{ matrix.runtime }} path: | build/sourcegit-*.AppImage + build/sourcegit-*.AppImage.zsync build/sourcegit_*.deb build/sourcegit-*.rpm - name: Delete temp artifacts diff --git a/build/scripts/package.linux.sh b/build/scripts/package.linux.sh index 262f141ae8..e4645b7213 100755 --- a/build/scripts/package.linux.sh +++ b/build/scripts/package.linux.sh @@ -51,7 +51,9 @@ ln -rsf SourceGit.AppDir/opt/sourcegit/sourcegit SourceGit.AppDir/AppRun ln -rsf SourceGit.AppDir/usr/share/applications/com.sourcegit_scm.SourceGit.desktop SourceGit.AppDir cp resources/appimage/sourcegit.appdata.xml SourceGit.AppDir/usr/share/metainfo/com.sourcegit_scm.SourceGit.appdata.xml -ARCH="$appimage_arch" ./appimagetool -v SourceGit.AppDir "sourcegit-$VERSION.linux.$arch.AppImage" +ARCH="$appimage_arch" ./appimagetool -v \ + -u "gh-releases-zsync|sourcegit-scm|sourcegit|latest|sourcegit-*.linux.$arch.AppImage.zsync" \ + SourceGit.AppDir "sourcegit-$VERSION.linux.$arch.AppImage" mkdir -p resources/deb/opt/sourcegit/ mkdir -p resources/deb/usr/bin From 24bc8a5cc1aabd9f98d1c1e52ed84da5659da25f Mon Sep 17 00:00:00 2001 From: Gadfly Date: Thu, 8 Oct 2026 14:10:55 +0800 Subject: [PATCH 42/51] fix: collapse shadow padding when window touches a screen edge (#2717) The custom window frame draws its shadow inside a 12px transparent padding that belongs to the window bounds, so window managers clamp the padded bounds against the screen edge and the visible content stops short of it. Collapse the padding on the touching side to let the content sit flush against the edge. --- src/Views/ChromelessWindow.cs | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/Views/ChromelessWindow.cs b/src/Views/ChromelessWindow.cs index a06ab75656..0fc83764ba 100644 --- a/src/Views/ChromelessWindow.cs +++ b/src/Views/ChromelessWindow.cs @@ -26,6 +26,8 @@ public ChromelessWindow() { Focusable = true; Native.OS.SetupForWindow(this); + PositionChanged += (_, _) => UpdateEdgeSnapInsets(); + Resized += (_, _) => UpdateEdgeSnapInsets(); } public void BeginMoveWindow(object _, PointerPressedEventArgs e) @@ -89,6 +91,9 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (OperatingSystem.IsWindows() && change.Property == WindowStateProperty) Native.Win64Utilities.FixWindowFrame(this); + + if (change.Property == WindowStateProperty) + UpdateEdgeSnapInsets(); } protected override void OnKeyDown(KeyEventArgs e) @@ -132,5 +137,47 @@ private void OnWindowBorderPointerPressed(object sender, PointerPressedEventArgs if (sender is Border { Tag: WindowEdge edge } && CanResize) BeginResizeDrag(edge, e); } + + // The transparent padding around the window holds the self-drawn shadow + // (keep the value in sync with "Window.custom_window_frame" style). + // Collapse it on the side touching a screen edge so the content sits flush. + private void UpdateEdgeSnapInsets() + { + if (!Classes.Contains("custom_window_frame")) + return; + + if (WindowState != WindowState.Normal) + { + // Let the "WindowState=Maximized" style take over + ClearValue(PaddingProperty); + return; + } + + var screen = Screens.ScreenFromWindow(this); + if (screen == null) + return; + + var workArea = screen.WorkingArea; + var right = Position.X + Bounds.Width; + var bottom = Position.Y + Bounds.Height; + + const double tolerance = 2.0; + var snapTop = Position.Y <= workArea.Y + tolerance; + var snapLeft = Position.X <= workArea.X + tolerance; + var snapRight = right >= workArea.Right - tolerance; + var snapBottom = bottom >= workArea.Bottom - tolerance; + + if (!snapTop && !snapLeft && !snapRight && !snapBottom) + { + ClearValue(PaddingProperty); + return; + } + + Padding = new Thickness( + snapLeft ? 0 : 12, + snapTop ? 0 : 12, + snapRight ? 0 : 12, + snapBottom ? 0 : 12); + } } } From 3b952570eda5947e4ab10e1dc0b42a64f32505f3 Mon Sep 17 00:00:00 2001 From: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:26:12 +0800 Subject: [PATCH 43/51] fix: prevent leftmost x-axis label from being clipped in Statistics chart (#2556) --- src/Views/Chart.cs | 126 +++++++++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 57 deletions(-) diff --git a/src/Views/Chart.cs b/src/Views/Chart.cs index 627e6f0516..d0a9b85681 100644 --- a/src/Views/Chart.cs +++ b/src/Views/Chart.cs @@ -68,6 +68,7 @@ public override void Render(DrawingContext context) var hasUserSamples = samples.HasSpecialUser; var labelPen = new Pen(new SolidColorBrush(Colors.Gray, 0.4), .5); + var tickPen = new Pen(Brushes.Gray); var labelTypeface = new Typeface(LabelFontFamily); var corner = new CornerRadius(2, 2, 0, 0); var sampleBrush = SampleBrush; @@ -106,78 +107,89 @@ public override void Render(DrawingContext context) var sampleW = hasUserSamples ? Math.Min(step * 0.5 - 2.5, 14.0) : Math.Min(step - 3, 28.0); var maxSampleH = h - 24.0; var maxLabelEndX = w - 4.0; + var labelsToDraw = new List<(FormattedText Text, Point Position, Point TickStart, Point TickEnd)>(); - using var clip = context.PushClip(new Rect(leftMargin, 0, w - leftMargin, h)); - do + using (context.PushClip(new Rect(leftMargin, 0, w - leftMargin, h))) { - var (label, total, user) = samples.GetSample(time); - - if (x - step <= w && total > 0) + do { - if (hasUserSamples) - { - var startX = x - step * 0.5 - 1 - sampleW; - var startY = maxSampleH * (1.0 - total * 1.0 / maxValue); - var rect = new Rect(startX, startY, sampleW, maxSampleH - startY); + var (label, total, user) = samples.GetSample(time); - using (context.PushOpacity(0.2)) + if (x - step <= w && total > 0) + { + if (hasUserSamples) { - context.DrawRectangle(sampleBrush, null, new RoundedRect(rect, corner)); + var startX = x - step * 0.5 - 1 - sampleW; + var startY = maxSampleH * (1.0 - total * 1.0 / maxValue); + var rect = new Rect(startX, startY, sampleW, maxSampleH - startY); + + using (context.PushOpacity(0.2)) + { + context.DrawRectangle(sampleBrush, null, new RoundedRect(rect, corner)); + } + + if (user > 0) + { + var userStartX = startX + sampleW + 2; + var userStartY = maxSampleH * (1.0 - user * 1.0 / maxValue); + var userRect = new Rect(userStartX, userStartY, sampleW, maxSampleH - userStartY); + context.DrawRectangle(sampleBrush, null, new RoundedRect(userRect, corner)); + } + + var hitRect = new Rect(startX, startY, sampleW * 2 + 2, maxSampleH - startY); + _hitBoxes.Add(new(hitRect, new(label, true, total, user))); } - - if (user > 0) + else { - var userStartX = startX + sampleW + 2; - var userStartY = maxSampleH * (1.0 - user * 1.0 / maxValue); - var userRect = new Rect(userStartX, userStartY, sampleW, maxSampleH - userStartY); - context.DrawRectangle(sampleBrush, null, new RoundedRect(userRect, corner)); - } + var startX = x - step * 0.5 - sampleW * 0.5; + var startY = maxSampleH * (1.0 - total * 1.0 / maxValue); + var rect = new Rect(startX, startY, sampleW, maxSampleH - startY); - var hitRect = new Rect(startX, startY, sampleW * 2 + 2, maxSampleH - startY); - _hitBoxes.Add(new(hitRect, new(label, true, total, user))); - } - else - { - var startX = x - step * 0.5 - sampleW * 0.5; - var startY = maxSampleH * (1.0 - total * 1.0 / maxValue); - var rect = new Rect(startX, startY, sampleW, maxSampleH - startY); - - context.DrawRectangle(sampleBrush, null, new RoundedRect(rect, corner)); - _hitBoxes.Add(new(rect, new(label, false, total, 0))); + context.DrawRectangle(sampleBrush, null, new RoundedRect(rect, corner)); + _hitBoxes.Add(new(rect, new(label, false, total, 0))); + } } - } - if (x <= w) - { - var formattedLabel = new FormattedText( - label, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - labelTypeface, - 11, - Brushes.Gray); - - var labelCenterX = x - step * 0.5; - var labelEndX = labelCenterX + formattedLabel.Width * 0.5; - if (labelEndX <= maxLabelEndX) + if (x <= w) { - var labelStartX = labelCenterX - formattedLabel.Width * 0.5; - var labelStartY = h - formattedLabel.Height - 2.0; - context.DrawLine(new Pen(Brushes.Gray), new Point(labelCenterX, maxSampleH + 4), new Point(labelCenterX, maxSampleH)); - context.DrawText(formattedLabel, new Point(labelStartX, labelStartY)); - maxLabelEndX = labelStartX - 16.0; + var formattedLabel = new FormattedText( + label, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + labelTypeface, + 11, + Brushes.Gray); + + var labelCenterX = x - step * 0.5; + var labelEndX = labelCenterX + formattedLabel.Width * 0.5; + if (labelEndX <= maxLabelEndX) + { + var labelStartX = labelCenterX - formattedLabel.Width * 0.5; + var labelStartY = h - formattedLabel.Height - 2.0; + labelsToDraw.Add((formattedLabel, + new Point(labelStartX, labelStartY), + new Point(labelCenterX, maxSampleH + 4), + new Point(labelCenterX, maxSampleH))); + maxLabelEndX = labelStartX - 16.0; + } } - } - if (x <= leftMargin) - break; + if (x <= leftMargin) + break; + + x -= step; - x -= step; + time = samples.NextSampleTime(time); + if (time < minTime) + break; + } while (true); + } - time = samples.NextSampleTime(time); - if (time < minTime) - break; - } while (true); + foreach (var (text, position, tickStart, tickEnd) in labelsToDraw) + { + context.DrawLine(tickPen, tickStart, tickEnd); + context.DrawText(text, position); + } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) From b64730845cfb7801e593eb78608050353a7d0a6a Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 8 Oct 2026 15:45:19 +0800 Subject: [PATCH 44/51] code_style: use named class instead of tuple Signed-off-by: leo --- src/Views/Chart.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Views/Chart.cs b/src/Views/Chart.cs index d0a9b85681..583a869991 100644 --- a/src/Views/Chart.cs +++ b/src/Views/Chart.cs @@ -107,7 +107,7 @@ public override void Render(DrawingContext context) var sampleW = hasUserSamples ? Math.Min(step * 0.5 - 2.5, 14.0) : Math.Min(step - 3, 28.0); var maxSampleH = h - 24.0; var maxLabelEndX = w - 4.0; - var labelsToDraw = new List<(FormattedText Text, Point Position, Point TickStart, Point TickEnd)>(); + var labelsToDraw = new List(); using (context.PushClip(new Rect(leftMargin, 0, w - leftMargin, h))) { @@ -165,11 +165,7 @@ public override void Render(DrawingContext context) if (labelEndX <= maxLabelEndX) { var labelStartX = labelCenterX - formattedLabel.Width * 0.5; - var labelStartY = h - formattedLabel.Height - 2.0; - labelsToDraw.Add((formattedLabel, - new Point(labelStartX, labelStartY), - new Point(labelCenterX, maxSampleH + 4), - new Point(labelCenterX, maxSampleH))); + labelsToDraw.Add(new(formattedLabel, labelStartX, labelCenterX)); maxLabelEndX = labelStartX - 16.0; } } @@ -185,10 +181,10 @@ public override void Render(DrawingContext context) } while (true); } - foreach (var (text, position, tickStart, tickEnd) in labelsToDraw) + foreach (var label in labelsToDraw) { - context.DrawLine(tickPen, tickStart, tickEnd); - context.DrawText(text, position); + context.DrawLine(tickPen, new Point(label.TickPosition, maxSampleH + 4), new Point(label.TickPosition, maxSampleH)); + context.DrawText(label.Text, new Point(label.TextPosition, maxSampleH + 6)); } } @@ -288,6 +284,7 @@ protected override void OnPointerWheelChanged(PointerWheelEventArgs e) } private record HitBox(Rect Rect, ChartToolTip ToolTip); + private record XLabel(FormattedText Text, double TextPosition, double TickPosition); private Models.StatisticsSamples _samples = null; private double _offsetX = 0; From 415e13779170628a2bceebf85c94a9f5f22e1394 Mon Sep 17 00:00:00 2001 From: cdammanintopix <63319198+cdammanintopix@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:43:37 +0200 Subject: [PATCH 45/51] fix: watcher missed ref updates stored in `packed-refs` or lost on buffer overflow (#2764) The branch graph was sometimes not updated after a fetch, until a pull or a restart. - `packed-refs` was ignored. A ref updated or deleted only there (e.g. `fetch --prune`, or the `git maintenance run --auto` triggered after a fetch) never scheduled a refresh. - `FileSystemWatcher.Error` was not handled. A large fetch writes thousands of object files, which overflows the internal buffer (8 KB by default): Windows then drops events, including the ones about `refs/remotes/*`, and nothing noticed. Since we cannot know which events were lost, schedule a full reload instead. --- src/Models/Watcher.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index b881807b0c..66e246bc07 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -43,6 +43,7 @@ public Watcher(IRepository repo, string fullpath, string gitDir) combined.Renamed += OnRepositoryChanged; combined.Changed += OnRepositoryChanged; combined.Deleted += OnRepositoryChanged; + combined.Error += OnWatcherError; combined.EnableRaisingEvents = false; _watchers.Add(combined); @@ -58,6 +59,7 @@ public Watcher(IRepository repo, string fullpath, string gitDir) wc.Renamed += OnWorkingCopyChanged; wc.Changed += OnWorkingCopyChanged; wc.Deleted += OnWorkingCopyChanged; + wc.Error += OnWatcherError; wc.EnableRaisingEvents = false; var git = new FileSystemWatcher(); @@ -69,6 +71,7 @@ public Watcher(IRepository repo, string fullpath, string gitDir) git.Renamed += OnGitDirChanged; git.Changed += OnGitDirChanged; git.Deleted += OnGitDirChanged; + git.Error += OnWatcherError; git.EnableRaisingEvents = false; _watchers.Add(wc); @@ -240,6 +243,21 @@ private void OnRepositoryChanged(object o, FileSystemEventArgs e) HandleWorkingCopyFileChanged(name, e.FullPath); } + /// + /// The internal buffer overflowed (a large fetch or checkout writes thousands of files), so + /// some events were dropped and we cannot know which. Reload everything once things settle. + /// + private void OnWatcherError(object o, ErrorEventArgs e) + { + var desired = DateTime.Now.AddSeconds(1).ToFileTime(); + Interlocked.Exchange(ref _updateBranch, desired); + Interlocked.Exchange(ref _updateTags, desired); + Interlocked.Exchange(ref _updateStashes, desired); + Interlocked.Exchange(ref _updateWC, desired); + if (_repo.MayHaveSubmodules()) + Interlocked.Exchange(ref _updateSubmodules, desired); + } + private void OnGitDirChanged(object o, FileSystemEventArgs e) { if (string.IsNullOrEmpty(e.Name)) @@ -302,6 +320,12 @@ private void HandleGitDirFileChanged(string name) { Interlocked.Exchange(ref _updateBranch, DateTime.Now.AddSeconds(.5).ToFileTime()); } + else if (name.Equals("packed-refs", StringComparison.Ordinal)) + { + var desired = DateTime.Now.AddSeconds(.5).ToFileTime(); + Interlocked.Exchange(ref _updateBranch, desired); + Interlocked.Exchange(ref _updateTags, desired); + } else if (name.StartsWith("reftable/", StringComparison.Ordinal)) { var desired = DateTime.Now.AddSeconds(.5).ToFileTime(); From 554bfaa223acdb1ae24e3f84a2718ccf25671239 Mon Sep 17 00:00:00 2001 From: DennyHo0917 <149746199+DennyHo0917@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:59:19 +0800 Subject: [PATCH 46/51] docs: add API Route AI service configuration (#2767) --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index ec610c1043..f26f27d8f4 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,13 @@ For `Cheaper Inference`: * `API Key` should be a Cheaper Inference API key (prefixed with `ci_live_`) * A good starting `Model` is `gpt-5.4-mini` +For `API Route`: + +* `Server` should be `https://global.api-route.com/v1` +* `API Key` should be your API Route API key +* `Model` can be `gpt-6.1-sol` if it is available to your key; otherwise select an available chat model from the fetched catalog +* See the [API documentation](https://github.com/DennyHo0917/api-route/blob/main/API.md) for authentication and model discovery + For other AI service: * The `Server` should fill in a URL equivalent to OpenAI's `https://api.openai.com/v1`. For example, when using `Ollama`, it should be `http://localhost:11434/v1` instead of `http://localhost:11434/api/generate` From c245ee636f0ccb2a836858a569442221928eaaa7 Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 9 Oct 2026 11:41:43 +0800 Subject: [PATCH 47/51] feature: support to open repository in system file browser from context menu of repository tab Signed-off-by: leo --- src/Views/LauncherTabBar.axaml.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Views/LauncherTabBar.axaml.cs b/src/Views/LauncherTabBar.axaml.cs index 83826afcec..19bb5967ac 100644 --- a/src/Views/LauncherTabBar.axaml.cs +++ b/src/Views/LauncherTabBar.axaml.cs @@ -302,6 +302,17 @@ private void OnTabContextRequested(object sender, ContextRequestedEventArgs e) if (vm.ActivePage.Data is ViewModels.Repository repo) { + var explore = new MenuItem(); + explore.Header = App.Text("Repository.Explore"); + explore.Icon = this.CreateMenuIcon("Icons.Explore"); + explore.Tag = OperatingSystem.IsMacOS() ? "⌘+E" : "Ctrl+E"; + explore.Click += (_, e) => + { + Native.OS.OpenInFileManager(repo.FullPath); + e.Handled = true; + }; + menu.Items.Add(explore); + var refresh = new MenuItem(); refresh.Header = App.Text("PageTabBar.Tab.Refresh"); refresh.Icon = this.CreateMenuIcon("Icons.Loading"); From d92e37074e6a64ebb293c0e5803514f44d6893d2 Mon Sep 17 00:00:00 2001 From: Felix <151852644+Felixkw12@users.noreply.github.com> Date: Sat, 10 Oct 2026 11:12:02 +0200 Subject: [PATCH 48/51] docs: document Opper as an OpenAI-compatible service (#2769) Add an Opper entry to the OpenAI section of the README, after the existing service entries. Opper is an EU-hosted AI gateway with an OpenAI-compatible API at https://api.opper.ai/v3/compat. One key gives access to 700+ models from 50+ providers. Docs-only change. Disclosure: Opper is my startup. Co-authored-by: Claude Opus 5.5 (1M context) Co-authored-by: leo --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index f26f27d8f4..2ac5ba0723 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,12 @@ For `Cheaper Inference`: * `API Key` should be a Cheaper Inference API key (prefixed with `ci_live_`) * A good starting `Model` is `gpt-5.4-mini` +For `Opper`: + +* `Server` should be `https://api.opper.ai/v3/compat` +* `API Key` should be an Opper API key, created at https://platform.opper.ai +* A good starting `Model` is `gpt-5.4-mini` + For `API Route`: * `Server` should be `https://global.api-route.com/v1` From f1e381fcc8fcdc33d134db9d73eb651d32e5293c Mon Sep 17 00:00:00 2001 From: AquariusStar <48148723+AquariusStar@users.noreply.github.com> Date: Mon, 12 Oct 2026 05:00:05 +0300 Subject: [PATCH 49/51] localization: update Russian translate (#2770) --- src/Resources/Locales/ru_RU.axaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 596df60287..f9260cc4f9 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -954,10 +954,15 @@ ИЗМЕНЕНИЯ ОТЛОЖЕННЫЕ Статистика + РЕВИЗИИ ОБЗОР МЕСЯЦ НЕДЕЛЯ SourceGit проанализирует не более 200 000 ревизий в целевой ветке. + ТОП АВТОРОВ + РЕВИЗИИ + ПРОЦЕНТ + Авторы с долей менее 1% ревизий объединены в группу «Другие». АВТОРЫ: РЕВИЗИИ: ПОДМОДУЛИ @@ -1001,6 +1006,11 @@ Влить ${0}$ в ${1}$... Выложить ${0}$... ПРЕКРАТИТЬ + Недоверенный репозиторий + Вы хотите выполнить команду (git config --global --add safe.directory {0}), чтобы разрешить доступ к этому пути? + Отказ гита в доступе к этому репозиторию: + Путь: + Пропущено репозиториев из-за недоверия к ним гитом: {0}. Пожалуйста, откройте их вручную и проверьте, можно ли им доверять. Обновление подмодулей Все подмодули Создавать по необходимости From e73e2cf7f4eb0aae20da7bd98d8d1170e28fd9a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Oct 2026 02:00:15 +0000 Subject: [PATCH 50/51] doc: Update translation status and sort locale files --- TRANSLATION.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 75cf1c1c23..0e33d6f6a4 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -1231,23 +1231,7 @@ 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) - -
-Missing keys in ru_RU.axaml - -- Text.Statistics.Commits -- Text.Statistics.TopAuthors -- Text.Statistics.TopAuthors.Count -- Text.Statistics.TopAuthors.Percentage -- Text.Statistics.TopAuthors.Tip -- Text.TrustRepository -- Text.TrustRepository.CommandTip -- Text.TrustRepository.Description -- Text.TrustRepository.Path -- Text.TrustRepository.ScanSkipped - -
+### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) ### ![ta__IN](https://img.shields.io/badge/ta__IN-60.58%25-red) From 43548d306d9d485aea7475587bf83e2747509446 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 12 Oct 2026 10:02:31 +0800 Subject: [PATCH 51/51] version: Release 2026.22 Signed-off-by: leo --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 88005f0e1c..bb71f1638b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2026.21 \ No newline at end of file +2026.22 \ No newline at end of file