From 91947365dc648f4c005a26b8836f1e23f04b7bbf Mon Sep 17 00:00:00 2001 From: Martin Dindoffer Date: Fri, 17 Jul 2026 09:59:43 +0200 Subject: [PATCH] feature: add fulltext commit search across SHA/author/committer/message (#985) Git cannot OR --grep/--author/--committer in a single invocation, so the new `All` method (now the default) runs the field queries separately, plus an exact-SHA lookup, and merges the deduplicated results sorted by commit time. Adds an internal-only `ByCommitter` query mode to power it. Co-Authored-By: Claude Fable 5 --- src/Commands/QueryCommits.cs | 4 ++ src/Models/Commit.cs | 7 ++- src/Resources/Locales/en_US.axaml | 1 + src/ViewModels/SearchCommitContext.cs | 76 ++++++++++++++++++++++----- src/Views/Repository.axaml | 3 +- 5 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 1a5073988..64a713764 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -28,6 +28,10 @@ public QueryCommits(string repo, string filter, Models.CommitSearchMethod method { builder.Append("-i --author=").Append(filter.Quoted()); } + else if (method == Models.CommitSearchMethod.ByCommitter) + { + builder.Append("-i --committer=").Append(filter.Quoted()); + } else if (method == Models.CommitSearchMethod.ByMessage) { var words = filter.Split([' ', '\t', '\r'], StringSplitOptions.RemoveEmptyEntries); diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index de299ed6b..2c7610002 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -4,13 +4,18 @@ namespace SourceGit.Models { + // Bound by index to the search-method ComboBox in Views/Repository.axaml, so + // UI-selectable methods must stay first and in dropdown order. ByCommitter is + // internal-only (used by the fulltext `All` search). public enum CommitSearchMethod { - BySHA = 0, + All = 0, + BySHA, ByAuthor, ByMessage, ByPath, ByContent, + ByCommitter, } public class Commit : ObservableObject diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index e11f101dd..1617a8e67 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -825,6 +825,7 @@ Add Remote RESOLVE Search Commit + SHA/Author/Message Author Content Message diff --git a/src/ViewModels/SearchCommitContext.cs b/src/ViewModels/SearchCommitContext.cs index e67046a9c..a12eadfc8 100644 --- a/src/ViewModels/SearchCommitContext.cs +++ b/src/ViewModels/SearchCommitContext.cs @@ -129,22 +129,21 @@ public void StartSearch() result.Add(commit); } } - else if (_onlySearchCurrentBranch) - { - result = await new Commands.QueryCommits(repoPath, _filter, method, true) - .GetResultAsync() - .ConfigureAwait(false); - - foreach (var c in result) - c.IsMerged = true; - } else { - result = await new Commands.QueryCommits(repoPath, _filter, method, false) - .GetResultAsync() - .ConfigureAwait(false); + if (method == Models.CommitSearchMethod.All) + result = await SearchAllFieldsAsync(repoPath).ConfigureAwait(false); + else + result = await new Commands.QueryCommits(repoPath, _filter, method, _onlySearchCurrentBranch) + .GetResultAsync() + .ConfigureAwait(false); - if (result.Count > 0) + if (_onlySearchCurrentBranch) + { + foreach (var c in result) + c.IsMerged = true; + } + else if (result.Count > 0) { var set = await new Commands.QueryCurrentBranchCommitHashes(repoPath, result[^1].CommitterTime) .GetResultAsync() @@ -171,6 +170,55 @@ public void StartSearch() }, token); } + private async Task> SearchAllFieldsAsync(string repoPath) + { + var result = new List(); + var seen = new HashSet(); + + var isCommitSHA = await new Commands.IsCommitSHA(repoPath, _filter) + .GetResultAsync() + .ConfigureAwait(false); + + if (isCommitSHA) + { + var commit = await new Commands.QuerySingleCommit(repoPath, _filter) + .GetResultAsync() + .ConfigureAwait(false); + + var matchesScope = commit != null; + if (matchesScope && _onlySearchCurrentBranch) + matchesScope = await new Commands.IsAncestor(repoPath, commit.SHA, "HEAD") + .GetResultAsync() + .ConfigureAwait(false); + + if (matchesScope && seen.Add(commit.SHA)) + result.Add(commit); + } + + var fields = new[] + { + Models.CommitSearchMethod.ByMessage, + Models.CommitSearchMethod.ByAuthor, + Models.CommitSearchMethod.ByCommitter, + }; + + foreach (var field in fields) + { + var matched = await new Commands.QueryCommits(repoPath, _filter, field, _onlySearchCurrentBranch) + .GetResultAsync() + .ConfigureAwait(false); + + foreach (var c in matched) + { + if (seen.Add(c.SHA)) + result.Add(c); + } + } + + result.Sort((l, r) => r.CommitterTime.CompareTo(l.CommitterTime)); + return result; + } + public void EndSearch() { if (_cancellation is { IsCancellationRequested: false }) @@ -287,7 +335,7 @@ private void UpdateSuggestions() private Repository _repo = null; private CancellationTokenSource _cancellation = null; - private int _method = (int)Models.CommitSearchMethod.ByMessage; + private int _method = (int)Models.CommitSearchMethod.All; private string _filter = string.Empty; private bool _onlySearchCurrentBranch = false; private bool _isQuerying = false; diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index cda8c45e0..b18fe777e 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -617,6 +617,7 @@ BorderThickness="0" SelectedIndex="{Binding SearchCommitContext.Method, Mode=TwoWay}"> + @@ -628,7 +629,7 @@ + IsVisible="{Binding SearchCommitContext.Method, Converter={x:Static c:IntConverters.IsNotOne}}">