From 3cd815d4935472a8fca99056079d5d892730ed7b Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 13 Jul 2026 10:21:29 +0800 Subject: [PATCH 01/65] ux: change status icon (#2521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use `M` instead of `±` for modified files - Use `SkyBlue` instead of `LimeGreen` (which is already used for added files) for untracked files Signed-off-by: leo --- src/Views/ChangeStatusIcon.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Views/ChangeStatusIcon.cs b/src/Views/ChangeStatusIcon.cs index e7aff0106..ee48cbc89 100644 --- a/src/Views/ChangeStatusIcon.cs +++ b/src/Views/ChangeStatusIcon.cs @@ -10,7 +10,7 @@ namespace SourceGit.Views { public class ChangeStatusIcon : Control { - private static readonly string[] INDICATOR = ["?", "±", "T", "+", "−", "➜", "❏", "★", "!"]; + private static readonly string[] INDICATOR = ["?", "M", "T", "+", "−", "➜", "❏", "?", "!"]; private static readonly Color[] COLOR = [ Colors.Transparent, @@ -20,7 +20,7 @@ public class ChangeStatusIcon : Control Colors.Tomato, Colors.Orchid, Colors.Goldenrod, - Colors.LimeGreen, + Colors.SkyBlue, Colors.OrangeRed, ]; From 11748ea144335799657c8ebd5e51481af4e35c73 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 14 Jul 2026 09:34:56 +0800 Subject: [PATCH 02/65] feature: support using `ESC` to cancel confirm dialog Signed-off-by: leo --- src/Views/Confirm.axaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Views/Confirm.axaml b/src/Views/Confirm.axaml index 52b9240ab..a9055f71a 100644 --- a/src/Views/Confirm.axaml +++ b/src/Views/Confirm.axaml @@ -56,6 +56,7 @@ Width="80" Height="30" Margin="4,0" + HotKey="Escape" Click="CloseWindow" HorizontalAlignment="Center" HorizontalContentAlignment="Center" From 73f83611051403a13b8068af92b6feb172a45070 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 14 Jul 2026 13:44:31 +0800 Subject: [PATCH 03/65] refactor: use product code to check JetBrains products Signed-off-by: leo --- src/Models/ExternalTool.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/Models/ExternalTool.cs b/src/Models/ExternalTool.cs index 1ca448525..7f93cf8bd 100644 --- a/src/Models/ExternalTool.cs +++ b/src/Models/ExternalTool.cs @@ -84,30 +84,18 @@ public class VisualStudioInstance public class JetBrainsState { - [JsonPropertyName("version")] - public int Version { get; set; } = 0; - [JsonPropertyName("appVersion")] - public string AppVersion { get; set; } = string.Empty; [JsonPropertyName("tools")] public List Tools { get; set; } = new List(); } public class JetBrainsTool { - [JsonPropertyName("channelId")] - public string ChannelId { get; set; } - [JsonPropertyName("toolId")] - public string ToolId { get; set; } [JsonPropertyName("productCode")] public string ProductCode { get; set; } - [JsonPropertyName("tag")] - public string Tag { get; set; } [JsonPropertyName("displayName")] public string DisplayName { get; set; } [JsonPropertyName("displayVersion")] public string DisplayVersion { get; set; } - [JsonPropertyName("buildNumber")] - public string BuildNumber { get; set; } [JsonPropertyName("installLocation")] public string InstallLocation { get; set; } [JsonPropertyName("launchCommand")] @@ -198,8 +186,7 @@ public void Cursor(Func platformFinder) public void FindJetBrainsFromToolbox(Func platformFinder) { - var exclude = new List { "fleet", "dotmemory", "dottrace", "resharper-u", "androidstudio" }; - var supportedIcons = new List { "CL", "DB", "DL", "DS", "GO", "JB", "PC", "PS", "PY", "QA", "QD", "RD", "RM", "RR", "WRS", "WS" }; + var supported = new List { "CL", "DB", "DL", "DS", "GO", "JB", "PC", "PS", "PY", "QA", "QD", "RD", "RM", "RR", "WRS", "WS" }; var state = Path.Combine(platformFinder(), "state.json"); if (File.Exists(state)) { @@ -209,12 +196,12 @@ public void FindJetBrainsFromToolbox(Func platformFinder) var stateData = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.JetBrainsState); foreach (var tool in stateData.Tools) { - if (exclude.Contains(tool.ToolId.ToLowerInvariant())) + if (!supported.Contains(tool.ProductCode)) continue; Tools.Add(new ExternalTool( $"{tool.DisplayName} {tool.DisplayVersion}", - supportedIcons.Contains(tool.ProductCode) ? $"JetBrains/{tool.ProductCode}" : "JetBrains/JB", + $"JetBrains/{tool.ProductCode}", Path.Combine(tool.InstallLocation, tool.LaunchCommand), null, true)); From 66a758725e76b3ad5f0c9f29adae070cef50b986 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 14 Jul 2026 13:48:34 +0800 Subject: [PATCH 04/65] ux: reorder of external editors on Windows Signed-off-by: leo --- src/Native/Windows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Native/Windows.cs b/src/Native/Windows.cs index fd8f31c9b..2ec98ca40 100644 --- a/src/Native/Windows.cs +++ b/src/Native/Windows.cs @@ -127,11 +127,11 @@ public string FindTerminal(Models.ShellOrTerminal shell) finder.VSCode(FindVSCode); finder.VSCodeInsiders(FindVSCodeInsiders); finder.VSCodium(FindVSCodium); + FindVisualStudio(finder); finder.Cursor(() => Path.Combine(localAppDataDir, @"Programs\Cursor\Cursor.exe")); finder.FindJetBrainsFromToolbox(() => Path.Combine(localAppDataDir, @"JetBrains\Toolbox")); finder.SublimeText(FindSublimeText); finder.Zed(FindZed); - FindVisualStudio(finder); return finder.Tools; } From 64d41169509b24310062245640a66abb85f86d95 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 14 Jul 2026 14:02:39 +0800 Subject: [PATCH 05/65] ux: prevent scroll to last selected item when collapse/expand tree node (#2531) Signed-off-by: leo --- src/Views/ChangeCollectionView.axaml.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index 8cf0a32ce..0a271664f 100644 --- a/src/Views/ChangeCollectionView.axaml.cs +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -21,6 +21,10 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && DataContext is ViewModels.ChangeTreeNode { IsFolder: true } node) { + var container = this.FindAncestorOfType(); + if (container != null) + container.SelectedItem = node; + var tree = this.FindAncestorOfType(); tree?.ToggleNodeIsExpanded(node); } From 90aecc6d98312fdebfbcb5f16670897078d2800b Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 14 Jul 2026 16:45:06 +0800 Subject: [PATCH 06/65] feature: branch filtering in `Statistics` window (#2532) Signed-off-by: leo --- src/Commands/Statistics.cs | 17 +++- src/ViewModels/Statistics.cs | 62 +++++++++++--- src/Views/BranchSelector.axaml | 7 +- src/Views/Statistics.axaml | 145 ++++++++++++++++++--------------- 4 files changed, 150 insertions(+), 81 deletions(-) diff --git a/src/Commands/Statistics.cs b/src/Commands/Statistics.cs index 2d43c7629..24867907d 100644 --- a/src/Commands/Statistics.cs +++ b/src/Commands/Statistics.cs @@ -1,15 +1,28 @@ using System.IO; +using System.Text; using System.Threading.Tasks; namespace SourceGit.Commands { public class Statistics : Command { - public Statistics(string repo, int max) + public Statistics(string repo, int max, Models.Branch specBranch) { WorkingDirectory = repo; Context = repo; - Args = $"log --date-order --branches --remotes -{max} --format=%ct$%aN±%aE"; + + var builder = new StringBuilder(); + builder + .Append("log --date-order -") + .Append(max) + .Append(" --format=%ct$%aN±%aE "); + + if (specBranch == null) + builder.Append("--branches --remotes"); + else + builder.Append(specBranch.FullName); + + Args = builder.ToString(); } public async Task ReadAsync() diff --git a/src/ViewModels/Statistics.cs b/src/ViewModels/Statistics.cs index 6b2f9a270..add2423e2 100644 --- a/src/ViewModels/Statistics.cs +++ b/src/ViewModels/Statistics.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -12,6 +13,22 @@ public bool IsLoading private set => SetProperty(ref _isLoading, value); } + public List Branches + { + get => _branches; + private set => SetProperty(ref _branches, value); + } + + public Models.Branch SelectedBranch + { + get => _selectedBranch; + set + { + if (SetProperty(ref _selectedBranch, value)) + LoadStatistics(); + } + } + public Models.StatisticsMode ViewMode { get => _viewMode; @@ -36,9 +53,39 @@ public Models.StatisticsSamples Samples public Statistics(string repo) { + _repo = repo; + LoadBranches(); + LoadStatistics(); + } + + public void ChangeAuthor(Models.StatisticsAuthor author) + { + if (SelectedReport == null) + return; + + Samples = SelectedReport.GetSamples(author); + } + + private void LoadBranches() + { + Task.Run(async () => + { + var branches = await new Commands.QueryBranches(_repo) + .GetResultAsync() + .ConfigureAwait(false); + + branches.Insert(0, _selectedBranch); + Dispatcher.UIThread.Post(() => Branches = branches); + }); + } + + private void LoadStatistics() + { + IsLoading = true; + Task.Run(async () => { - var result = await new Commands.Statistics(repo, Preferences.Instance.MaxHistoryCommits) + var result = await new Commands.Statistics(_repo, Preferences.Instance.MaxHistoryCommits, _selectedBranch) .ReadAsync() .ConfigureAwait(false); @@ -51,14 +98,6 @@ public Statistics(string repo) }); } - public void ChangeAuthor(Models.StatisticsAuthor author) - { - if (SelectedReport == null) - return; - - Samples = SelectedReport.GetSamples(author); - } - private void RefreshReport() { if (_data == null) @@ -74,7 +113,10 @@ private void RefreshReport() Samples = SelectedReport.GetSamples(null); } + private string _repo = null; private bool _isLoading = true; + private List _branches = new List(); + private Models.Branch _selectedBranch = new Models.Branch() { Name = "--- (All)", IsLocal = true, FullName = "", Head = "---" }; // Fake branch to represent all branches private Models.Statistics _data = null; private Models.StatisticsMode _viewMode = Models.StatisticsMode.All; private Models.StatisticsReport _selectedReport = null; diff --git a/src/Views/BranchSelector.axaml b/src/Views/BranchSelector.axaml index af8b25eb5..7f12a82bf 100644 --- a/src/Views/BranchSelector.axaml +++ b/src/Views/BranchSelector.axaml @@ -14,14 +14,17 @@ + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + Date: Tue, 14 Jul 2026 16:50:36 +0800 Subject: [PATCH 07/65] fix: missing check for `FullName` of selected branch while filtering branch in `Statistic` window (#2532) Signed-off-by: leo --- src/Commands/Statistics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/Statistics.cs b/src/Commands/Statistics.cs index 24867907d..3c6ed017b 100644 --- a/src/Commands/Statistics.cs +++ b/src/Commands/Statistics.cs @@ -17,7 +17,7 @@ public Statistics(string repo, int max, Models.Branch specBranch) .Append(max) .Append(" --format=%ct$%aN±%aE "); - if (specBranch == null) + if (specBranch == null || string.IsNullOrEmpty(specBranch.FullName)) builder.Append("--branches --remotes"); else builder.Append(specBranch.FullName); From d235132c0ef2b4705f90612fb4ba8eba7b08a38b Mon Sep 17 00:00:00 2001 From: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:26:23 +0800 Subject: [PATCH 08/65] enhance: remove history filter only on successful remote branch deletion (#2533) --- src/ViewModels/DeleteBranch.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ViewModels/DeleteBranch.cs b/src/ViewModels/DeleteBranch.cs index 976e4f392..300441aa6 100644 --- a/src/ViewModels/DeleteBranch.cs +++ b/src/ViewModels/DeleteBranch.cs @@ -65,7 +65,8 @@ public override async Task Sure() else { succ = await DeleteRemoteBranchAsync(Target, log); - _repo.UIStates.RemoveHistoryFilter(Target.FullName, Models.FilterType.RemoteBranch); + if (succ) + _repo.UIStates.RemoveHistoryFilter(Target.FullName, Models.FilterType.RemoteBranch); } log.Complete(); From 2e931dd82470728af1f436f763e08ebfb90d597e Mon Sep 17 00:00:00 2001 From: JC-Chung <52159296+JC-Chung@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:10:00 +0800 Subject: [PATCH 09/65] ux: show registering progress after scanning repositories (#2534) --- src/ViewModels/ScanRepositories.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs index 0a5bf3438..f9ac3e41b 100644 --- a/src/ViewModels/ScanRepositories.cs +++ b/src/ViewModels/ScanRepositories.cs @@ -93,8 +93,10 @@ public override async Task Sure() await minDelay; var normalizedRoot = rootDir.FullName.Replace('\\', '/').TrimEnd('/'); - foreach (var f in found) + for (var i = 0; i < found.Count; i++) { + var f = found[i]; + ProgressDescription = $"Registering ({i + 1}/{found.Count}) {f}..."; var parent = new DirectoryInfo(f).Parent!.FullName.Replace('\\', '/').TrimEnd('/'); if (parent.Equals(normalizedRoot, StringComparison.OrdinalIgnoreCase)) { From ba599a8d7b661e720ec2cd1f3d13dcafc047949a Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 15 Jul 2026 10:11:01 +0800 Subject: [PATCH 10/65] ux: close `AIAssistant` dialog immediately after applying the generated message (#2535) Signed-off-by: leo --- src/Views/AIAssistant.axaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Views/AIAssistant.axaml.cs b/src/Views/AIAssistant.axaml.cs index ddd601be2..84a180360 100644 --- a/src/Views/AIAssistant.axaml.cs +++ b/src/Views/AIAssistant.axaml.cs @@ -168,6 +168,7 @@ private void OnUseClicked(object sender, RoutedEventArgs e) if (DataContext is ViewModels.AIAssistant vm && !string.IsNullOrEmpty(vm.Response)) vm.Use(vm.Response); + Close(); e.Handled = true; } From 7f1688b599e1486747bd18f8aa30b380093dea9c Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 15 Jul 2026 10:31:05 +0800 Subject: [PATCH 11/65] enhance: include current branch name in commit message generation prompt - Add `currentBranch` parameter to `Agent.GenerateCommitMessageAsync` method - Append current branch info to the prompt sent to AI service - Pass current branch name from `AIAssistant` view model to the agent Signed-off-by: leo --- src/AI/Agent.cs | 3 ++- src/ViewModels/AIAssistant.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/AI/Agent.cs b/src/AI/Agent.cs index e1b102724..22a420e2a 100644 --- a/src/AI/Agent.cs +++ b/src/AI/Agent.cs @@ -14,7 +14,7 @@ public Agent(Service service) _service = service; } - public async Task GenerateCommitMessageAsync(string repo, string changeList, Action onUpdate, CancellationToken cancellation) + public async Task GenerateCommitMessageAsync(string repo, string currentBranch, string changeList, Action onUpdate, CancellationToken cancellation) { var chatClient = _service.GetChatClient(); if (chatClient == null) @@ -28,6 +28,7 @@ public async Task GenerateCommitMessageAsync(string repo, string changeList, Act .AppendLine("- Output the conventional commit message (with detail changes in list) directly. Do not explain your output nor introduce your answer.") .AppendLine(_service.AdditionalPrompt) .Append("Repository path: ").AppendLine(repo.Quoted()) + .Append("Current branch: ").AppendLine(currentBranch.Quoted()) .AppendLine("Changed files ('A' means added, 'M' means modified, 'D' means deleted, 'T' means type changed, 'R' means renamed, 'C' means copied): ") .Append(changeList); diff --git a/src/ViewModels/AIAssistant.cs b/src/ViewModels/AIAssistant.cs index 4f54678c2..b6d154139 100644 --- a/src/ViewModels/AIAssistant.cs +++ b/src/ViewModels/AIAssistant.cs @@ -63,6 +63,7 @@ public async Task GenAsync() var responseBuilder = new StringBuilder(); var foundResponse = false; + var currentBranchName = _repo.CurrentBranch?.Name ?? "main"; Text = builder.ToString(); Response = string.Empty; @@ -70,7 +71,7 @@ public async Task GenAsync() try { - await agent.GenerateCommitMessageAsync(_repo.FullPath, _changeList, message => + await agent.GenerateCommitMessageAsync(_repo.FullPath, currentBranchName, _changeList, message => { builder.AppendLine(message); From d70e4f4608bba856650d5db9712bb7a0257a0f39 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 15 Jul 2026 19:39:51 +0800 Subject: [PATCH 12/65] fix: extra `CR` marker at the end of the last line in text diff view (#2536) Signed-off-by: leo --- src/Views/TextDiffView.axaml.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index f0b7837b7..992f002db 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -1052,7 +1052,6 @@ protected override void OnDataContextChanged(EventArgs e) builder.Append('\n'); } - builder.Length--; Text = builder.ToString(); } else @@ -1243,7 +1242,6 @@ protected override void OnDataContextChanged(EventArgs e) builder.Append('\n'); } - builder.Length--; Text = builder.ToString(); } else From 50531eff59b54294f17cd43d8d9ab7fcdc8992e5 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 16 Jul 2026 10:37:55 +0800 Subject: [PATCH 13/65] feature: support to choose the start point of `git-flow` topic branch (#2538) Signed-off-by: leo --- src/Commands/GitFlow.cs | 16 ++++++++++++---- src/ViewModels/GitFlowStart.cs | 27 +++++++++++++++++++++++---- src/Views/GitFlowStart.axaml | 13 ++++++------- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/Commands/GitFlow.cs b/src/Commands/GitFlow.cs index 56b56a923..0438d76de 100644 --- a/src/Commands/GitFlow.cs +++ b/src/Commands/GitFlow.cs @@ -47,24 +47,32 @@ public async Task InitAsync(string production, string develop, string feat return await ExecAsync().ConfigureAwait(false); } - public async Task StartAsync(Models.GitFlowBranchType type, string name) + public async Task StartAsync(Models.GitFlowBranchType type, string name, Models.Branch based) { + var builder = new StringBuilder(); + builder.Append("flow "); + switch (type) { case Models.GitFlowBranchType.Feature: - Args = $"flow feature start {name}"; + builder.Append("feature"); break; case Models.GitFlowBranchType.Release: - Args = $"flow release start {name}"; + builder.Append("release"); break; case Models.GitFlowBranchType.Hotfix: - Args = $"flow hotfix start {name}"; + builder.Append("hotfix"); break; default: RaiseException("Bad git-flow branch type!!!"); return false; } + builder.Append(" start ").Append(name); + if (based != null) + builder.Append(' ').Append(based.Name); + + Args = builder.ToString(); return await ExecAsync().ConfigureAwait(false); } diff --git a/src/ViewModels/GitFlowStart.cs b/src/ViewModels/GitFlowStart.cs index cc1f9e8a4..2fe9c0784 100644 --- a/src/ViewModels/GitFlowStart.cs +++ b/src/ViewModels/GitFlowStart.cs @@ -1,4 +1,6 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -11,12 +13,18 @@ public Models.GitFlowBranchType Type private set; } - public string StartPoint + public List LocalBranches { get; private set; } + public Models.Branch StartPoint + { + get => _startPoint; + set => SetProperty(ref _startPoint, value); + } + public string Prefix { get; @@ -38,11 +46,21 @@ public GitFlowStart(Repository repo, Models.GitFlowBranchType type) Type = type; Prefix = _repo.GitFlow.GetPrefix(type); - StartPoint = type switch + LocalBranches = new List(); + + foreach (var b in _repo.Branches) + { + if (b.IsLocal && !b.IsDetachedHead) + LocalBranches.Add(b); + } + + var defBranch = type switch { Models.GitFlowBranchType.Hotfix => _repo.GitFlow.ProductionBranch, _ => _repo.GitFlow.DevelopmentBranch, }; + + StartPoint = LocalBranches.Find(b => b.Name.Equals(defBranch, StringComparison.Ordinal)); } public static ValidationResult ValidateBranchName(string name, ValidationContext ctx) @@ -70,7 +88,7 @@ public override async Task Sure() var succ = await new Commands.GitFlow(_repo.FullPath) .Use(log) - .StartAsync(Type, _name); + .StartAsync(Type, _name, _startPoint); log.Complete(); return succ; @@ -78,5 +96,6 @@ public override async Task Sure() private readonly Repository _repo; private string _name = null; + private Models.Branch _startPoint = null; } } diff --git a/src/Views/GitFlowStart.axaml b/src/Views/GitFlowStart.axaml index 647331741..38b4be4eb 100644 --- a/src/Views/GitFlowStart.axaml +++ b/src/Views/GitFlowStart.axaml @@ -33,13 +33,12 @@ HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,8,0" Text="{DynamicResource Text.GitFlow.StartAt}"/> - - - - + Date: Thu, 16 Jul 2026 11:08:52 +0800 Subject: [PATCH 14/65] ux: default focus the new topic branch name Signed-off-by: leo --- src/Views/GitFlowStart.axaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Views/GitFlowStart.axaml b/src/Views/GitFlowStart.axaml index 38b4be4eb..51827c6d6 100644 --- a/src/Views/GitFlowStart.axaml +++ b/src/Views/GitFlowStart.axaml @@ -30,21 +30,10 @@ - - - - + + + From 31d14114aa771f234febdb1941d859e150752674 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 16 Jul 2026 14:20:03 +0800 Subject: [PATCH 15/65] ux: change focus behaviour of `DealWithLocalChangesMethod` control - Disable `Focusable` for inner radio buttons and trait the whole control as a focusable control - Support to use up and down arrow to change the value of this control Signed-off-by: leo --- src/Views/DealWithLocalChangesMethod.axaml | 25 +++++++++++++++++++ src/Views/DealWithLocalChangesMethod.axaml.cs | 24 ++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/Views/DealWithLocalChangesMethod.axaml b/src/Views/DealWithLocalChangesMethod.axaml index 9d35cb192..c0663173b 100644 --- a/src/Views/DealWithLocalChangesMethod.axaml +++ b/src/Views/DealWithLocalChangesMethod.axaml @@ -3,23 +3,48 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:m="using:SourceGit.Models" + xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.DealWithLocalChangesMethod" x:Name="ThisControl"> + + + + + + + Date: Thu, 16 Jul 2026 14:22:53 +0800 Subject: [PATCH 16/65] code_style: remove unnecessary calling for `UpdateRadioButtons` Signed-off-by: leo --- src/Views/DealWithLocalChangesMethod.axaml.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Views/DealWithLocalChangesMethod.axaml.cs b/src/Views/DealWithLocalChangesMethod.axaml.cs index ddd4227d6..a55510308 100644 --- a/src/Views/DealWithLocalChangesMethod.axaml.cs +++ b/src/Views/DealWithLocalChangesMethod.axaml.cs @@ -60,7 +60,6 @@ private void OnRadioButtonClicked(object sender, RoutedEventArgs e) if (sender is RadioButton { Tag: Models.DealWithLocalChanges way }) { Method = way; - UpdateRadioButtons(); e.Handled = true; } } From a7352aa58c56d5e8f39163a2e5584a4ad18e94a3 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 16 Jul 2026 15:05:31 +0800 Subject: [PATCH 17/65] ux: change `KeyboardNavigation.TabNavigation` to `Cycle` for popup panel Signed-off-by: leo --- src/Views/LauncherPage.axaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Views/LauncherPage.axaml b/src/Views/LauncherPage.axaml index 0a2e3144e..bcbecbeca 100644 --- a/src/Views/LauncherPage.axaml +++ b/src/Views/LauncherPage.axaml @@ -67,7 +67,7 @@ - + @@ -111,8 +111,10 @@ + IsVisible="{Binding InProgress}" + IsHitTestVisible="False"/> From 93957058a6c7c7059be6b74213dd9ad4097a4dec Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 16 Jul 2026 19:36:47 +0800 Subject: [PATCH 18/65] feature: support to collapse history filters bar (#2540) Signed-off-by: leo --- src/Models/RepositoryUIStates.cs | 6 ++++ src/Resources/Locales/en_US.axaml | 2 ++ src/Resources/Locales/zh_CN.axaml | 2 ++ src/Resources/Locales/zh_TW.axaml | 2 ++ src/ViewModels/Repository.cs | 13 +++++++ src/Views/Repository.axaml | 57 +++++++++++++++++++++++++------ 6 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/Models/RepositoryUIStates.cs b/src/Models/RepositoryUIStates.cs index 93cb6fc09..97df74d5a 100644 --- a/src/Models/RepositoryUIStates.cs +++ b/src/Models/RepositoryUIStates.cs @@ -231,6 +231,12 @@ public AvaloniaList HistoryFilters set; } = []; + public bool OnlyShowHistoryFiltersSummary + { + get; + set; + } = false; + public List RecentCommitMessages { get; diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index c997f8221..5a2ee7b72 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -800,9 +800,11 @@ Open in File Browser Search Branches/Tags/Submodules Visibility in Graph + Only show count of filters Unset Hide in commit graph Filter in commit graph + filter(s) enabled LAYOUT Horizontal Vertical diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index cf27832e7..72bde70a8 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -804,9 +804,11 @@ 在文件浏览器中打开 快速查找分支/标签/子模块 设置在列表中的可见性 + 仅显示过滤项的数量 不指定 在提交列表中隐藏 使用其对提交列表过滤 + 个过滤项已被启用 布局方式 水平排布 竖直排布 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index fcf0003dd..c27d49b01 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -804,9 +804,11 @@ 在檔案瀏覽器中開啟 快速搜尋分支/標籤/子模組 篩選以顯示或隱藏 + 僅顯示篩選項的數量 取消指定 在提交列表中隱藏 以其篩選提交列表 + 個篩選項已被啟用 版面配置 橫向顯示 縱向顯示 diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 8c08dbe7f..93d15aa2d 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -412,6 +412,19 @@ public bool IsBisectCommandRunning private set => SetProperty(ref _isBisectCommandRunning, value); } + public bool OnlyShowHistoryFiltersSummary + { + get => _uiStates.OnlyShowHistoryFiltersSummary; + set + { + if (value != _uiStates.OnlyShowHistoryFiltersSummary) + { + _uiStates.OnlyShowHistoryFiltersSummary = value; + OnPropertyChanged(); + } + } + } + public bool IsAutoFetching { get => _isAutoFetching; diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 2b6b7f9d5..213a5f639 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -922,7 +922,7 @@ - + - + @@ -951,22 +954,56 @@ BorderThickness="1" BorderBrush="{Binding Mode, Converter={x:Static c:FilterModeConverters.ToBorderBrush}}" VerticalAlignment="Center"> - - - - + + + + + + + + + + - - + - From f87974b3b8ab4a5c4c11a717089f677198af91b5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Jul 2026 11:37:06 +0000 Subject: [PATCH 19/65] doc: Update translation status and sort locale files --- TRANSLATION.md | 76 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 2297d1157..7cc9b1976 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-89.98%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-89.80%25-yellow)
Missing keys in de_DE.axaml @@ -102,6 +102,8 @@ This document shows the translation status of each locale file in the repository - Text.Rebase.Test.UnknownError - Text.Rebase.Test.WillCauseConflicts - Text.RemoteCM.EnableAutoFetch +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary - Text.StashCM.ApplyFileChanges - Text.StashCM.Branch - Text.SubmoduleRevisionCompare @@ -116,11 +118,27 @@ This document shows the translation status of each locale file in the repository
-### ![el__GR](https://img.shields.io/badge/el__GR-%E2%88%9A-brightgreen) +### ![el__GR](https://img.shields.io/badge/el__GR-99.80%25-yellow) -### ![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen) +
+Missing keys in el_GR.axaml + +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary + +
+ +### ![es__ES](https://img.shields.io/badge/es__ES-99.80%25-yellow) + +
+Missing keys in es_ES.axaml + +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary + +
-### ![fr__FR](https://img.shields.io/badge/fr__FR-95.97%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-95.78%25-yellow)
Missing keys in fr_FR.axaml @@ -162,6 +180,8 @@ This document shows the translation status of each locale file in the repository - Text.Rebase.Test.OK - Text.Rebase.Test.UnknownError - Text.Rebase.Test.WillCauseConflicts +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary - Text.TagCM.Checkout - Text.TagCM.Merge - Text.UpdateSubmodules.Recursive @@ -169,7 +189,7 @@ This document shows the translation status of each locale file in the repository
-### ![he__IL](https://img.shields.io/badge/he__IL-95.97%25-yellow) +### ![he__IL](https://img.shields.io/badge/he__IL-95.78%25-yellow)
Missing keys in he_IL.axaml @@ -211,6 +231,8 @@ This document shows the translation status of each locale file in the repository - Text.Rebase.Test.OK - Text.Rebase.Test.UnknownError - Text.Rebase.Test.WillCauseConflicts +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary - Text.TagCM.Checkout - Text.TagCM.Merge - Text.UpdateSubmodules.Recursive @@ -218,7 +240,7 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-82.91%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-82.75%25-yellow)
Missing keys in id_ID.axaml @@ -376,6 +398,8 @@ This document shows the translation status of each locale file in the repository - Text.Rebase.Test.UnknownError - Text.Rebase.Test.WillCauseConflicts - Text.RemoteCM.EnableAutoFetch +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary - Text.Repository.OpenAsFolder - Text.Repository.Resolve - Text.SelfUpdate.CurrentVersion @@ -400,7 +424,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-89.49%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-89.31%25-yellow)
Missing keys in it_IT.axaml @@ -499,6 +523,8 @@ This document shows the translation status of each locale file in the repository - Text.Rebase.Test.UnknownError - Text.Rebase.Test.WillCauseConflicts - Text.RemoteCM.EnableAutoFetch +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary - Text.SelfUpdate.CurrentVersion - Text.SelfUpdate.ReleaseDate - Text.StashCM.ApplyFileChanges @@ -515,9 +541,17 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-%E2%88%9A-brightgreen) +### ![ja__JP](https://img.shields.io/badge/ja__JP-99.80%25-yellow) -### ![ko__KR](https://img.shields.io/badge/ko__KR-97.45%25-yellow) +
+Missing keys in ja_JP.axaml + +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary + +
+ +### ![ko__KR](https://img.shields.io/badge/ko__KR-97.25%25-yellow)
Missing keys in ko_KR.axaml @@ -544,6 +578,8 @@ This document shows the translation status of each locale file in the repository - Text.GitFlow.FinishWithRebase - Text.GitFlow.StartAt - Text.GitFlow.StartName +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary - Text.TagCM.Checkout - Text.TagCM.Merge - Text.UpdateSubmodules.Recursive @@ -551,7 +587,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-62.97%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-62.84%25-red)
Missing keys in pt_BR.axaml @@ -836,6 +872,8 @@ This document shows the translation status of each locale file in the repository - Text.Repository.ClearStashes - Text.Repository.Dashboard - Text.Repository.FilterCommits +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary - Text.Repository.HistoriesLayout - Text.Repository.HistoriesLayout.Horizontal - Text.Repository.HistoriesLayout.Vertical @@ -936,9 +974,17 @@ 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.80%25-yellow) + +
+Missing keys in ru_RU.axaml + +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary + +
-### ![ta__IN](https://img.shields.io/badge/ta__IN-64.83%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-64.71%25-red)
Missing keys in ta_IN.axaml @@ -1221,6 +1267,8 @@ This document shows the translation status of each locale file in the repository - Text.Repository.BranchSort.ByName - Text.Repository.ClearStashes - Text.Repository.Dashboard +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary - Text.Repository.MoreOptions - Text.Repository.OpenAsFolder - Text.Repository.Resolve @@ -1304,7 +1352,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-65.62%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-65.49%25-red)
Missing keys in uk_UA.axaml @@ -1583,6 +1631,8 @@ This document shows the translation status of each locale file in the repository - Text.Repository.BranchSort.ByName - Text.Repository.ClearStashes - Text.Repository.Dashboard +- Text.Repository.FilterCommits.Collapse +- Text.Repository.FilterCommits.Summary - Text.Repository.MoreOptions - Text.Repository.OpenAsFolder - Text.Repository.Resolve From 7f17ba86098911621062b2b3d6b395548bb667ba Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 16 Jul 2026 21:46:37 +0800 Subject: [PATCH 20/65] ux: new style for history filter bar expander --- src/Models/RepositoryUIStates.cs | 2 +- src/Resources/Locales/en_US.axaml | 3 ++- src/Resources/Locales/zh_CN.axaml | 3 ++- src/Resources/Locales/zh_TW.axaml | 3 ++- src/Resources/Styles.axaml | 38 +++++++++++++++++++++++++++++++ src/ViewModels/Repository.cs | 26 ++++++++++----------- src/Views/Repository.axaml | 25 +++++++++----------- 7 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/Models/RepositoryUIStates.cs b/src/Models/RepositoryUIStates.cs index 97df74d5a..723fe900b 100644 --- a/src/Models/RepositoryUIStates.cs +++ b/src/Models/RepositoryUIStates.cs @@ -231,7 +231,7 @@ public AvaloniaList HistoryFilters set; } = []; - public bool OnlyShowHistoryFiltersSummary + public bool IsHistoryFiltersCollapsed { get; set; diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 5a2ee7b72..c6ca011ef 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -800,9 +800,10 @@ Open in File Browser Search Branches/Tags/Submodules Visibility in Graph - Only show count of filters + Collapse Filters Unset Hide in commit graph + Expand Filters Filter in commit graph filter(s) enabled LAYOUT diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 72bde70a8..9486664bf 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -804,9 +804,10 @@ 在文件浏览器中打开 快速查找分支/标签/子模块 设置在列表中的可见性 - 仅显示过滤项的数量 + 折叠过滤项列表 不指定 在提交列表中隐藏 + 展开过滤项列表 使用其对提交列表过滤 个过滤项已被启用 布局方式 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index c27d49b01..807c06cad 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -804,9 +804,10 @@ 在檔案瀏覽器中開啟 快速搜尋分支/標籤/子模組 篩選以顯示或隱藏 - 僅顯示篩選項的數量 + 折疊篩選項列表 取消指定 在提交列表中隱藏 + 展開篩選項列表 以其篩選提交列表 個篩選項已被啟用 版面配置 diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index bcf19edeb..27878b5cc 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -1215,6 +1215,44 @@ + + + + + + + @@ -70,10 +82,9 @@ - + From 6f670b19b8a6df8e587f678da2cd1a2f6f3774d4 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 23 Jul 2026 11:54:34 +0800 Subject: [PATCH 41/65] feature: support `open -a SourceGit ` on macOS (#2553) Signed-off-by: leo --- build/resources/app/App.plist | 15 +++++++++++++++ src/App.axaml.cs | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/build/resources/app/App.plist b/build/resources/app/App.plist index ba6f40a2b..d20efa5e2 100644 --- a/build/resources/app/App.plist +++ b/build/resources/app/App.plist @@ -20,6 +20,21 @@ APPL CFBundleShortVersionString SOURCE_GIT_VERSION + CFBundleDocumentTypes + + + CFBundleTypeName + Supported Folder + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + public.folder + + + NSHighResolutionCapable diff --git a/src/App.axaml.cs b/src/App.axaml.cs index e1b2dbc77..a22f764d6 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -11,6 +11,7 @@ using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Fonts; +using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Threading; @@ -475,6 +476,15 @@ private void TryLaunchAsNormal(IClassicDesktopStyleApplicationLifetime desktop) Native.OS.SetupExternalTools(); Models.AvatarManager.Instance.Start(); + if (this.TryGetFeature() is { } activatable) + { + activatable.Activated += (_, e) => + { + if (e is FileActivatedEventArgs { Files: { Count: > 0 } } fileArgs) + _launcher?.TryOpenRepositoryFromPath(fileArgs.Files[0].Path.LocalPath); + }; + } + string startupRepo = null; if (desktop.Args is { Length: 1 }) { From 87a506caed74fd75f56e9de00e3b11c8a5ea515f Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 23 Jul 2026 14:42:30 +0800 Subject: [PATCH 42/65] code_style: remove unnecessary code Signed-off-by: leo --- src/Native/Linux.cs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index 0309a6562..a28d88ebe 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -48,25 +48,7 @@ public string GetDataDir() // Runtime data dir: ~/.sourcegit var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var dataDir = Path.Combine(home, ".sourcegit"); - if (Directory.Exists(dataDir)) - return dataDir; - - // Migrate old data: ~/.config/SourceGit - var oldDataDir = Path.Combine(home, ".config", "SourceGit"); - if (Directory.Exists(oldDataDir)) - { - try - { - Directory.Move(oldDataDir, dataDir); - } - catch - { - // Ignore errors - } - } - - return dataDir; + return Path.Combine(home, ".sourcegit"); } public string FindGitExecutable() From cee9115f81d8c8fd14b5e956f0940e3b0af106e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20W?= <44604769+goran-w@users.noreply.github.com> Date: Sat, 25 Jul 2026 04:35:35 +0200 Subject: [PATCH 43/65] refacto: diff result parsing (#2562) * Pass unmodified 'lineBytes' into ParseChunkBodyLine(), and clarify the use of its indicator-stripped subset by adding new variable 'rawContent' (matching the existing variable 'content'). * Move assignment of '_isInChunk = true' from (callee) ParseChunkStartLine() into (caller) ParseLine(), which makes it easier to see how the variable is used (since it is also checked and set to false in this outer scope). * Clarify the phrasing of two related comments. --- src/Commands/Diff.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Commands/Diff.cs b/src/Commands/Diff.cs index 6edeff540..89248ddc1 100644 --- a/src/Commands/Diff.cs +++ b/src/Commands/Diff.cs @@ -144,20 +144,23 @@ private void ParseLine(ArraySegment lineBytes) if (line.Length == 0) return; - // If we are reading a chunk body, try to read the current line as body first (because - // the number of chunk body is greater than the number of chunk indicator in most time. + // If we are reading a chunk-body, try to read the current line as chunk-body first (because + // there are usually more chunk-body lines than chunk-indicator lines). if (_isInChunk) { - if (ParseChunkBodyLine(line, lineBytes[1..])) + if (ParseChunkBodyLine(line, lineBytes)) return; ProcessInlineHighlights(); _isInChunk = false; } - // If the current line is not a chunk body, try to parse it as chunk indicator + // If the current line is not a chunk-body, try to parse it as chunk-indicator if (ParseChunkStartLine(line)) + { + _isInChunk = true; return; + } // Fallback to diff headers to support type-changed diff (multiple headers). ParseDiffHeaderLine(line); @@ -193,7 +196,6 @@ private bool ParseChunkStartLine(string line) _newLine = int.Parse(match.Groups[2].Value); _last = new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, null, 0, 0); _result.TextDiff.Lines.Add(_last); - _isInChunk = true; return true; } @@ -204,13 +206,14 @@ private bool ParseChunkBodyLine(string line, ArraySegment lineBytes) { var prefix = line[0]; var content = line.Substring(1); + var rawContent = lineBytes[1..].ToArray(); if (ParseLFSChange(prefix, content)) return true; if (prefix == PREFIX_DELETED) { _result.TextDiff.DeletedLines++; - _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, content, lineBytes.ToArray(), _oldLine, 0); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Deleted, content, rawContent, _oldLine, 0); _deleted.Add(_last); _oldLine++; return true; @@ -219,7 +222,7 @@ private bool ParseChunkBodyLine(string line, ArraySegment lineBytes) if (prefix == PREFIX_ADDED) { _result.TextDiff.AddedLines++; - _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, content, lineBytes.ToArray(), 0, _newLine); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Added, content, rawContent, 0, _newLine); _added.Add(_last); _newLine++; return true; @@ -229,7 +232,7 @@ private bool ParseChunkBodyLine(string line, ArraySegment lineBytes) { ProcessInlineHighlights(); - _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, content, lineBytes.ToArray(), _oldLine, _newLine); + _last = new Models.TextDiffLine(Models.TextDiffLineType.Normal, content, rawContent, _oldLine, _newLine); _result.TextDiff.Lines.Add(_last); _oldLine++; _newLine++; From 4b27bd3037b9d15ab03bfc48aceb29e75490e196 Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 25 Jul 2026 10:41:30 +0800 Subject: [PATCH 44/65] ux: add a placeholder for filter-change TextBox (#2565) --- src/Resources/Locales/en_US.axaml | 1 + src/Resources/Locales/zh_CN.axaml | 1 + src/Resources/Locales/zh_TW.axaml | 1 + src/Views/WorkingCopy.axaml | 1 + 4 files changed, 4 insertions(+) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 4e49e74ed..676bd57a0 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -1009,6 +1009,7 @@ FILE CONFLICTS ARE RESOLVED USE MINE USE THEIRS + Filter Changes... INCLUDE UNTRACKED FILES NO RECENT INPUT MESSAGES NO COMMIT TEMPLATES diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 2718abe21..7c42ba4c3 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -1013,6 +1013,7 @@ 文件冲突已解决 使用 MINE 使用 THEIRS + 查找变更... 显示未跟踪文件 没有提交信息记录 没有可应用的提交信息模板 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index f86247435..4043186a5 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -1013,6 +1013,7 @@ 檔案衝突已解決 使用我方版本 (ours) 使用對方版本 (theirs) + 搜尋变更... 顯示未追蹤檔案 沒有提交訊息記錄 沒有可套用的提交訊息範本 diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index 4275460c4..fc1127053 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -33,6 +33,7 @@ CornerRadius="12" Text="{Binding Filter, Mode=TwoWay}" BorderBrush="{DynamicResource Brush.Border2}" + Watermark="{DynamicResource Text.WorkingCopy.FilterChanges}" VerticalContentAlignment="Center"> Date: Sat, 25 Jul 2026 02:41:54 +0000 Subject: [PATCH 45/65] doc: Update translation status and sort locale files --- TRANSLATION.md | 57 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 5c1f0c2d8..3d02b2139 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-89.61%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-89.52%25-yellow)
Missing keys in de_DE.axaml @@ -114,13 +114,14 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.Merge - Text.UpdateSubmodules.Recursive - Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.FilterChanges - Text.Worktree.Branch - Text.Worktree.Head - Text.Worktree.Path
-### ![el__GR](https://img.shields.io/badge/el__GR-99.61%25-yellow) +### ![el__GR](https://img.shields.io/badge/el__GR-99.51%25-yellow)
Missing keys in el_GR.axaml @@ -129,12 +130,20 @@ This document shows the translation status of each locale file in the repository - Text.Repository.FilterCommits.Collapse - Text.Repository.FilterCommits.Expand - Text.Repository.FilterCommits.Summary +- Text.WorkingCopy.FilterChanges
-### ![es__ES](https://img.shields.io/badge/es__ES-%E2%88%9A-brightgreen) +### ![es__ES](https://img.shields.io/badge/es__ES-99.90%25-yellow) -### ![fr__FR](https://img.shields.io/badge/fr__FR-95.59%25-yellow) +
+Missing keys in es_ES.axaml + +- Text.WorkingCopy.FilterChanges + +
+ +### ![fr__FR](https://img.shields.io/badge/fr__FR-95.49%25-yellow)
Missing keys in fr_FR.axaml @@ -184,10 +193,11 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.Merge - Text.UpdateSubmodules.Recursive - Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.FilterChanges
-### ![he__IL](https://img.shields.io/badge/he__IL-95.59%25-yellow) +### ![he__IL](https://img.shields.io/badge/he__IL-95.49%25-yellow)
Missing keys in he_IL.axaml @@ -237,12 +247,20 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.Merge - Text.UpdateSubmodules.Recursive - Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.FilterChanges
-### ![id__ID](https://img.shields.io/badge/id__ID-%E2%88%9A-brightgreen) +### ![id__ID](https://img.shields.io/badge/id__ID-99.90%25-yellow) + +
+Missing keys in id_ID.axaml -### ![it__IT](https://img.shields.io/badge/it__IT-89.12%25-yellow) +- Text.WorkingCopy.FilterChanges + +
+ +### ![it__IT](https://img.shields.io/badge/it__IT-89.03%25-yellow)
Missing keys in it_IT.axaml @@ -355,13 +373,14 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.Merge - Text.UpdateSubmodules.Recursive - Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.FilterChanges - Text.Worktree.Branch - Text.Worktree.Head - Text.Worktree.Path
-### ![ja__JP](https://img.shields.io/badge/ja__JP-99.61%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-99.51%25-yellow)
Missing keys in ja_JP.axaml @@ -370,10 +389,11 @@ This document shows the translation status of each locale file in the repository - Text.Repository.FilterCommits.Collapse - Text.Repository.FilterCommits.Expand - Text.Repository.FilterCommits.Summary +- Text.WorkingCopy.FilterChanges
-### ![ko__KR](https://img.shields.io/badge/ko__KR-97.06%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-96.96%25-yellow)
Missing keys in ko_KR.axaml @@ -408,10 +428,11 @@ This document shows the translation status of each locale file in the repository - Text.TagCM.Merge - Text.UpdateSubmodules.Recursive - Text.WorkingCopy.AddToGitIgnore.UntrackedInSameFolder +- Text.WorkingCopy.FilterChanges
-### ![pt__BR](https://img.shields.io/badge/pt__BR-62.84%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-62.78%25-red)
Missing keys in pt_BR.axaml @@ -787,6 +808,7 @@ This document shows the translation status of each locale file in the repository - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.FilterChanges - Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor - Text.WorkingCopy.SignOff @@ -798,9 +820,16 @@ 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.90%25-yellow) + +
+Missing keys in ru_RU.axaml + +- Text.WorkingCopy.FilterChanges + +
-### ![ta__IN](https://img.shields.io/badge/ta__IN-64.51%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-64.45%25-red)
Missing keys in ta_IN.axaml @@ -1160,6 +1189,7 @@ This document shows the translation status of each locale file in the repository - Text.WorkingCopy.Conflicts.OpenExternalMergeToolAllConflicts - Text.WorkingCopy.Conflicts.UseMine - Text.WorkingCopy.Conflicts.UseTheirs +- Text.WorkingCopy.FilterChanges - Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor - Text.Worktree.Branch @@ -1170,7 +1200,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-65.29%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-65.23%25-red)
Missing keys in uk_UA.axaml @@ -1522,6 +1552,7 @@ This document shows the translation status of each locale file in the repository - Text.WorkingCopy.ConfirmCommitWithDetachedHead - Text.WorkingCopy.Conflicts.Merge - Text.WorkingCopy.Conflicts.MergeExternal +- Text.WorkingCopy.FilterChanges - Text.WorkingCopy.NoVerify - Text.WorkingCopy.ResetAuthor - Text.Worktree.Branch From cd614c8c827bf0ad3322dea19fce9e9271c8f275 Mon Sep 17 00:00:00 2001 From: leo Date: Sun, 26 Jul 2026 11:40:34 +0800 Subject: [PATCH 46/65] feature: support to detect conflict file state with binary file and hide `MERGE` button for non-text file conflict (#2566) --- src/Commands/IsConflictResolved.cs | 27 --------- src/Commands/QueryConflictFileState.cs | 55 ++++++++++++++++++ src/Models/Conflict.cs | 8 +++ src/ViewModels/Conflict.cs | 79 +++++++++++++++----------- src/ViewModels/WorkingCopy.cs | 15 ++--- src/Views/Conflict.axaml | 13 +---- 6 files changed, 119 insertions(+), 78 deletions(-) delete mode 100644 src/Commands/IsConflictResolved.cs create mode 100644 src/Commands/QueryConflictFileState.cs diff --git a/src/Commands/IsConflictResolved.cs b/src/Commands/IsConflictResolved.cs deleted file mode 100644 index e5b752d35..000000000 --- a/src/Commands/IsConflictResolved.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Threading.Tasks; - -namespace SourceGit.Commands -{ - public class IsConflictResolved : Command - { - public IsConflictResolved(string repo, Models.Change change) - { - var opt = new Models.DiffOption(change, true); - - WorkingDirectory = repo; - Context = repo; - Args = $"diff --no-color --no-ext-diff -a --ignore-cr-at-eol --check {opt}"; - } - - public bool GetResult() - { - return ReadToEnd().IsSuccess; - } - - public async Task GetResultAsync() - { - var rs = await ReadToEndAsync().ConfigureAwait(false); - return rs.IsSuccess; - } - } -} diff --git a/src/Commands/QueryConflictFileState.cs b/src/Commands/QueryConflictFileState.cs new file mode 100644 index 000000000..02578b218 --- /dev/null +++ b/src/Commands/QueryConflictFileState.cs @@ -0,0 +1,55 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class QueryConflictFileState : Command + { + public QueryConflictFileState(string repo, Models.Change change) + { + var opt = new Models.DiffOption(change, true); + + WorkingDirectory = repo; + Context = repo; + Args = $"diff --no-color --no-ext-diff --full-index --patch {opt}"; + } + + public async Task GetResultAsync() + { + try + { + using var proc = new Process(); + proc.StartInfo = CreateGitStartInfo(true); + proc.Start(); + + var isBinary = false; + var tokenCount = 0; + + while (await proc.StandardOutput.ReadLineAsync().ConfigureAwait(false) is { } line) + { + if (isBinary) + continue; + + if (line.StartsWith("Binary files ", StringComparison.Ordinal)) + isBinary = true; + else if (line.StartsWith("++<<<<<<<", StringComparison.Ordinal) || + line.StartsWith("++=======", StringComparison.Ordinal) || + line.StartsWith("++>>>>>>>", StringComparison.Ordinal)) + tokenCount++; + } + + await proc.WaitForExitAsync().ConfigureAwait(false); + + if (isBinary) + return Models.ConflictFileState.UnmergedBinary; + + return tokenCount == 0 ? Models.ConflictFileState.Resolved : Models.ConflictFileState.UnmergedText; + } + catch + { + return Models.ConflictFileState.Unknown; + } + } + } +} diff --git a/src/Models/Conflict.cs b/src/Models/Conflict.cs index 112fdd9b7..fd9c69030 100644 --- a/src/Models/Conflict.cs +++ b/src/Models/Conflict.cs @@ -2,6 +2,14 @@ namespace SourceGit.Models { + public enum ConflictFileState + { + Unknown, + UnmergedText, + UnmergedBinary, + Resolved + } + public enum ConflictPanelType { Ours, diff --git a/src/ViewModels/Conflict.cs b/src/ViewModels/Conflict.cs index 380ad68dc..3bb947bdc 100644 --- a/src/ViewModels/Conflict.cs +++ b/src/ViewModels/Conflict.cs @@ -1,9 +1,11 @@ using System.IO; using System.Threading.Tasks; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public class Conflict + public class Conflict : ObservableObject { public string Marker { @@ -15,52 +17,57 @@ public string Description get => _change.ConflictDesc; } - public object Theirs + public Models.ConflictFileState State { - get; - private set; + get => _state; + private set => SetProperty(ref _state, value); } - public object Mine + public object Theirs { - get; - private set; + get => _theirs; + private set => SetProperty(ref _theirs, value); } - public bool IsResolved - { - get; - private set; - } = false; - - public bool CanMerge + public object Mine { - get; - private set; - } = false; + get => _mine; + private set => SetProperty(ref _mine, value); + } public Conflict(Repository repo, WorkingCopy wc, Models.Change change) { _repo = repo; _wc = wc; + _canMerge = (change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified) && !Directory.Exists(Path.Combine(repo.FullPath, change.Path)); _change = change; - CanMerge = _change.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified; - if (CanMerge) - CanMerge = !Directory.Exists(Path.Combine(repo.FullPath, change.Path)); // Cannot merge directories (submodules) + Task.Run(async () => + { + _head = new Commands.QuerySingleCommit(repo.FullPath, "HEAD").GetResult(); - if (CanMerge) - IsResolved = new Commands.IsConflictResolved(repo.FullPath, change).GetResult(); + var (mine, theirs) = wc.InProgressContext switch + { + CherryPickInProgress cherryPick => (_head, cherryPick.Head), + RebaseInProgress rebase => (rebase.Onto, rebase.StoppedAt), + RevertInProgress revert => (_head, revert.Head), + MergeInProgress merge => (_head, merge.Source), + _ => (_head, (object)"Stash or Patch"), + }; - _head = new Commands.QuerySingleCommit(repo.FullPath, "HEAD").GetResult(); - (Mine, Theirs) = wc.InProgressContext switch - { - CherryPickInProgress cherryPick => (_head, cherryPick.Head), - RebaseInProgress rebase => (rebase.Onto, rebase.StoppedAt), - RevertInProgress revert => (_head, revert.Head), - MergeInProgress merge => (_head, merge.Source), - _ => (_head, (object)"Stash or Patch"), - }; + var state = Models.ConflictFileState.Unknown; + if (_canMerge) + state = await new Commands.QueryConflictFileState(repo.FullPath, change) + .GetResultAsync() + .ConfigureAwait(false); + + Dispatcher.UIThread.Post(() => + { + State = state; + Mine = mine; + Theirs = theirs; + }); + }); } public async Task UseTheirsAsync() @@ -75,18 +82,22 @@ public async Task UseMineAsync() public MergeConflictEditor CreateOpenMergeEditorRequest() { - return CanMerge ? new MergeConflictEditor(_repo, _head, _change.Path) : null; + return _canMerge ? new MergeConflictEditor(_repo, _head, _change.Path) : null; } public async Task MergeExternalAsync() { - if (CanMerge) + if (_canMerge) await _wc.UseExternalMergeToolAsync(_change); } private Repository _repo = null; private WorkingCopy _wc = null; - private Models.Commit _head = null; + private bool _canMerge = false; private Models.Change _change = null; + private Models.Commit _head = null; + private Models.ConflictFileState _state = Models.ConflictFileState.Unknown; + private object _mine = null; + private object _theirs = null; } } diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index c4a4c6486..c0f273529 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -689,15 +689,16 @@ public async Task CommitAsync(bool autoStage, bool autoPush) { if (c.IsConflicted) { - var isResolved = c.ConflictReason switch + if (c.ConflictReason is Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified) + { + var state = await new Commands.QueryConflictFileState(_repo.FullPath, c).GetResultAsync(); + if (state != Models.ConflictFileState.Resolved) + continue; + } + else { - Models.ConflictReason.BothAdded or Models.ConflictReason.BothModified => - await new Commands.IsConflictResolved(_repo.FullPath, c).GetResultAsync(), - _ => false, - }; - - if (!isResolved) continue; + } } outs.Add(c); diff --git a/src/Views/Conflict.axaml b/src/Views/Conflict.axaml index f2702a1f1..95d475a01 100644 --- a/src/Views/Conflict.axaml +++ b/src/Views/Conflict.axaml @@ -11,7 +11,7 @@ x:DataType="vm:Conflict"> - + @@ -50,13 +50,6 @@ - - - - - - - @@ -81,7 +74,7 @@ + IsVisible="{Binding State, Mode=OneWay, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:ConflictFileState.UnmergedText}}"> + + + - + - - + + + + + - - + + + + +