From c41b14145d524bb26c310198a3163ba957828ca7 Mon Sep 17 00:00:00 2001 From: AlanIWBFT Date: Fri, 26 Dec 2025 20:21:40 +0800 Subject: [PATCH] enhance: implement two-pass status query and auto stash --- src/Commands/Config.cs | 21 ++++++++++ src/Commands/QueryLocalChanges.cs | 63 ++++++++++++++++++++++++----- src/ViewModels/DropHead.cs | 2 +- src/ViewModels/Repository.cs | 34 ++++++++++++++-- src/ViewModels/Reword.cs | 2 +- src/ViewModels/SquashOrFixupHead.cs | 2 +- src/ViewModels/StashChanges.cs | 2 +- 7 files changed, 110 insertions(+), 16 deletions(-) diff --git a/src/Commands/Config.cs b/src/Commands/Config.cs index 52fc021cb..dfd712c89 100644 --- a/src/Commands/Config.cs +++ b/src/Commands/Config.cs @@ -74,6 +74,27 @@ public async Task GetAsync(string key) return rs.StdOut.Trim(); } + // Get config value with type canonicalization + // Weird values will be converted by git, like "000" -> "false", "010" -> "true" + // git will report bad values like "fatal: bad boolean config value 'kkk' for 'core.untrackedcache'" + public async Task GetBoolAsync(string key) + { + Args = $"config get --bool {key}"; + + var rs = await ReadToEndAsync().ConfigureAwait(false); + var stdout = rs.StdOut.Trim(); + switch (rs.StdOut.Trim()) + { + case "true": + return true; + case "false": + return false; + default: + // Illegal values behave as if they are not set + return null; + } + } + public async Task SetAsync(string key, string value, bool allowEmpty = false) { var scope = _isLocal ? "--local" : "--global"; diff --git a/src/Commands/QueryLocalChanges.cs b/src/Commands/QueryLocalChanges.cs index 385a95f27..d41f9a3b8 100644 --- a/src/Commands/QueryLocalChanges.cs +++ b/src/Commands/QueryLocalChanges.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -9,18 +10,21 @@ public partial class QueryLocalChanges : Command { [GeneratedRegex(@"^(\s?[\w\?]{1,4})\s+(.+)$")] private static partial Regex REG_FORMAT(); - private static readonly string[] UNTRACKED = ["no", "all"]; + private bool _includeUntracked; + private bool _useFastPathForUntrackedFiles; - public QueryLocalChanges(string repo, bool includeUntracked = true) + public QueryLocalChanges(string repo, bool includeUntracked = true, bool useFastPathForUntrackedFiles = false) { WorkingDirectory = repo; Context = repo; - Args = $"--no-optional-locks status -u{UNTRACKED[includeUntracked ? 1 : 0]} --ignore-submodules=dirty --porcelain"; + _includeUntracked = includeUntracked; + _useFastPathForUntrackedFiles = useFastPathForUntrackedFiles; } - public async Task> GetResultAsync() + private async Task<(List, List)> RunGitAndParseOutput() { - var outs = new List(); + var outChanges = new List(); + var outUntrackedDirs = new List(); try { @@ -153,8 +157,15 @@ public QueryLocalChanges(string repo, bool includeUntracked = true) break; } - if (change.Index != Models.ChangeState.None || change.WorkTree != Models.ChangeState.None) - outs.Add(change); + if (change.WorkTree == Models.ChangeState.Untracked && change.Path.EndsWith("/")) + { + outUntrackedDirs.Add(change.Path); + } + else + { + if (change.Index != Models.ChangeState.None || change.WorkTree != Models.ChangeState.None) + outChanges.Add(change); + } } } catch @@ -162,7 +173,41 @@ public QueryLocalChanges(string repo, bool includeUntracked = true) // Ignore exceptions. } - return outs; + return (outChanges, outUntrackedDirs); + } + public async Task> GetResultAsync() + { + if (!_useFastPathForUntrackedFiles) + { + Args = $"--no-optional-locks status -u{(_includeUntracked ? "all" : "no")} --ignore-submodules=dirty --porcelain"; + var (changes, _) = await RunGitAndParseOutput().ConfigureAwait(false); + return changes; + } + else + { + // Collect untracked dirs + Args = $"--no-optional-locks status --ignore-submodules=dirty --porcelain"; + var (changes, untrackedDirs) = await RunGitAndParseOutput().ConfigureAwait(false); + + // 'git status' does not support pathspec-from-file + for (int i = 0; i < untrackedDirs.Count; i += 32) + { + var count = Math.Min(32, untrackedDirs.Count - i); + var step = untrackedDirs.GetRange(i, count); + + Args = $"--no-optional-locks status -uall --ignore-submodules=dirty --porcelain --"; + foreach (var dir in step) + { + Args += $" \"{dir}\""; + } + + var (stepChanges, dirs) = await RunGitAndParseOutput().ConfigureAwait(false); + Debug.Assert(dirs.Count == 0); + changes.AddRange(stepChanges); + } + + return changes; + } } } } diff --git a/src/ViewModels/DropHead.cs b/src/ViewModels/DropHead.cs index b5baed06f..e16d30978 100644 --- a/src/ViewModels/DropHead.cs +++ b/src/ViewModels/DropHead.cs @@ -29,7 +29,7 @@ public override async Task Sure() var log = _repo.CreateLog($"Drop '{Target.SHA}'"); Use(log); - var changes = await new Commands.QueryLocalChanges(_repo.FullPath, true).GetResultAsync(); + var changes = await new Commands.QueryLocalChanges(_repo.FullPath, true, _repo.UseFastPathForUntrackedFiles).GetResultAsync(); var needAutoStash = changes.Count > 0; var succ = false; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 5ab2d1c27..98bef28c4 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -10,6 +10,7 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; +using SourceGit.Commands; namespace SourceGit.ViewModels { @@ -57,6 +58,11 @@ public bool HasAllowedSignersFile get => _hasAllowedSignersFile; } + public bool UseFastPathForUntrackedFiles + { + get => _useFastPathForUntrackedFiles; + } + public int SelectedViewIndex { get => _selectedViewIndex; @@ -509,8 +515,29 @@ public void Open() } _lastFetchTime = DateTime.Now; - _autoFetchTimer = new Timer(AutoFetchByTimer, null, 5000, 5000); - RefreshAll(); + + Task.Run(async () => + { + var canonicalizedConfig = new Commands.Config(FullPath); + Task fsmonitor = canonicalizedConfig.GetBoolAsync("core.fsmonitor"); + Task untrackedCache = canonicalizedConfig.GetBoolAsync("core.untrackedCache"); + Task manyFiles = canonicalizedConfig.GetBoolAsync("feature.manyFiles"); + _useFastPathForUntrackedFiles = (await fsmonitor) == true && ((await untrackedCache) == true || ((await untrackedCache) == null && (await manyFiles) == true)); + + // Run a normal, index-locking 'git status' to populate/update untracked cache + // Slow for the first time + if (_useFastPathForUntrackedFiles) + { + var status = new Command(); + status.WorkingDirectory = FullPath; + status.Context = FullPath; + status.Args = "status"; + await status.ExecAsync(); + } + + _autoFetchTimer = new Timer(AutoFetchByTimer, null, 5000, 5000); + RefreshAll(); + }); } public void Close() @@ -1308,7 +1335,7 @@ public void RefreshWorkingCopyChanges() Task.Run(async () => { - var changes = await new Commands.QueryLocalChanges(FullPath, _settings.IncludeUntrackedInLocalChanges) + var changes = await new Commands.QueryLocalChanges(FullPath, _settings.IncludeUntrackedInLocalChanges, UseFastPathForUntrackedFiles) .GetResultAsync() .ConfigureAwait(false); @@ -1901,6 +1928,7 @@ private async Task AutoFetchOnUIThread() private Models.HistoryFilterCollection _historyFilterCollection = null; private Models.FilterMode _historyFilterMode = Models.FilterMode.None; private bool _hasAllowedSignersFile = false; + private bool _useFastPathForUntrackedFiles = false; private Models.Watcher _watcher = null; private Histories _histories = null; diff --git a/src/ViewModels/Reword.cs b/src/ViewModels/Reword.cs index efea03e8d..0659c0428 100644 --- a/src/ViewModels/Reword.cs +++ b/src/ViewModels/Reword.cs @@ -37,7 +37,7 @@ public override async Task Sure() var log = _repo.CreateLog("Reword HEAD"); Use(log); - var changes = await new Commands.QueryLocalChanges(_repo.FullPath, false).GetResultAsync(); + var changes = await new Commands.QueryLocalChanges(_repo.FullPath, false, _repo.UseFastPathForUntrackedFiles).GetResultAsync(); var signOff = _repo.Settings.EnableSignOffForCommit; var noVerify = _repo.Settings.NoVerifyOnCommit; var needAutoStash = false; diff --git a/src/ViewModels/SquashOrFixupHead.cs b/src/ViewModels/SquashOrFixupHead.cs index 18b5b6f91..9c9b133f9 100644 --- a/src/ViewModels/SquashOrFixupHead.cs +++ b/src/ViewModels/SquashOrFixupHead.cs @@ -39,7 +39,7 @@ public override async Task Sure() var log = _repo.CreateLog(IsFixupMode ? "Fixup" : "Squash"); Use(log); - var changes = await new Commands.QueryLocalChanges(_repo.FullPath, false).GetResultAsync(); + var changes = await new Commands.QueryLocalChanges(_repo.FullPath, false, _repo.UseFastPathForUntrackedFiles).GetResultAsync(); var signOff = _repo.Settings.EnableSignOffForCommit; var noVerify = _repo.Settings.NoVerifyOnCommit; var needAutoStash = false; diff --git a/src/ViewModels/StashChanges.cs b/src/ViewModels/StashChanges.cs index 154c89b2c..dbaaa5354 100644 --- a/src/ViewModels/StashChanges.cs +++ b/src/ViewModels/StashChanges.cs @@ -73,7 +73,7 @@ public override async Task Sure() } else { - var all = await new Commands.QueryLocalChanges(_repo.FullPath, false) + var all = await new Commands.QueryLocalChanges(_repo.FullPath, false, _repo.UseFastPathForUntrackedFiles) .Use(log) .GetResultAsync();