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/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index 4d6c723a8..628132734 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -14,6 +14,9 @@ public static class IntConverters public static readonly FuncValueConverter IsZero = new(v => v == 0); + 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/Models/Commit.cs b/src/Models/Commit.cs index de299ed6b..efd9c2291 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/CommitSearchView.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 9abb3bdcc..5f9a5689b 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -839,6 +839,7 @@ Add Remote RESOLVE SEARCH COMMITS + SHA/Author/Message Author Content Message diff --git a/src/ViewModels/SearchCommitContext.cs b/src/ViewModels/SearchCommitContext.cs index f7885e809..3718b1d6a 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(_repo, _filter, method, true) - .GetResultAsync() - .ConfigureAwait(false); - - foreach (var c in result) - c.IsMerged = true; - } else { - result = await new Commands.QueryCommits(_repo, _filter, method, false) - .GetResultAsync() - .ConfigureAwait(false); + if (method == Models.CommitSearchMethod.All) + result = await SearchAllFieldsAsync().ConfigureAwait(false); + else + result = await new Commands.QueryCommits(_repo, _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(_repo, result[^1].CommitterTime) .GetResultAsync() @@ -171,6 +170,64 @@ public void StartSearch() }, token); } + private async Task> SearchAllFieldsAsync() + { + // Git ANDs `--grep`, `--author` and `--committer` within a single + // invocation, so every field needs its own `git log`. Each of those is + // a full history walk on large repositories, so they run concurrently + // instead of one after another. + var bySHA = SearchExactSHAAsync(); + var byMessage = new Commands.QueryCommits(_repo, _filter, Models.CommitSearchMethod.ByMessage, _onlySearchCurrentBranch) + .GetResultAsync(); + var byAuthor = new Commands.QueryCommits(_repo, _filter, Models.CommitSearchMethod.ByAuthor, _onlySearchCurrentBranch) + .GetResultAsync(); + var byCommitter = new Commands.QueryCommits(_repo, _filter, Models.CommitSearchMethod.ByCommitter, _onlySearchCurrentBranch) + .GetResultAsync(); + + await Task.WhenAll(bySHA, byMessage, byAuthor, byCommitter).ConfigureAwait(false); + + var result = new List(); + var seen = new HashSet(); + + if (bySHA.Result is { } exact && seen.Add(exact.SHA)) + result.Add(exact); + + foreach (var matched in new[] { byMessage.Result, byAuthor.Result, byCommitter.Result }) + { + foreach (var c in matched) + { + if (seen.Add(c.SHA)) + result.Add(c); + } + } + + result.Sort((l, r) => r.CommitterTime.CompareTo(l.CommitterTime)); + return result; + } + + private async Task SearchExactSHAAsync() + { + var isCommitSHA = await new Commands.IsCommitSHA(_repo, _filter) + .GetResultAsync() + .ConfigureAwait(false); + + if (!isCommitSHA) + return null; + + var commit = await new Commands.QuerySingleCommit(_repo, _filter) + .GetResultAsync() + .ConfigureAwait(false); + + if (commit == null || !_onlySearchCurrentBranch) + return commit; + + var isInCurrentBranch = await new Commands.IsAncestor(_repo, commit.SHA, "HEAD") + .GetResultAsync() + .ConfigureAwait(false); + + return isInCurrentBranch ? commit : null; + } + public void EndSearch() { if (_cancellation is { IsCancellationRequested: false }) @@ -288,7 +345,7 @@ private void UpdateSuggestions() private string _repo = null; private Histories _histories = 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/CommitSearchView.axaml b/src/Views/CommitSearchView.axaml index 85141f20c..7137e66cf 100644 --- a/src/Views/CommitSearchView.axaml +++ b/src/Views/CommitSearchView.axaml @@ -145,6 +145,7 @@ BorderBrush="{DynamicResource Brush.Border2}" SelectedIndex="{Binding Method, Mode=TwoWay}"> + @@ -156,7 +157,7 @@ + IsVisible="{Binding Method, Converter={x:Static c:IntConverters.IsNotOne}}">