Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 109 additions & 8 deletions src/Commands/QueryCommits.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ public QueryCommits(string repo, string limits, bool markMerged = true)

public QueryCommits(string repo, string filter, Models.CommitSearchMethod method, bool onlyCurrentBranch)
{
if (method == Models.CommitSearchMethod.Mixed)
{
WorkingDirectory = repo;
Context = repo;
_mixedFilter = filter;
// NUL-delimited fields preserve multiline messages. Limit matching results,
// rather than limiting the history before testing author/message/SHA.
Args = "log --date-order --no-show-signature --decorate=full --format=%H%x00%P%x00%D%x00%aN±%aE%x00%at%x00%cN±%cE%x00%ct%x00%s%x00%B%x00 " +
(onlyCurrentBranch ? "HEAD" : "--branches --remotes HEAD");
return;
}

var builder = new StringBuilder();
builder.Append("log -1000 --date-order --no-show-signature --decorate=full --format=%H%x00%P%x00%D%x00%aN±%aE%x00%at%x00%cN±%cE%x00%ct%x00%s ");

Expand Down Expand Up @@ -52,6 +64,9 @@ public QueryCommits(string repo, string filter, Models.CommitSearchMethod method

public async Task<List<Models.Commit>> GetResultAsync()
{
if (_mixedFilter != null)
return await GetMixedResultAsync().ConfigureAwait(false);

var commits = new List<Models.Commit>();
try
{
Expand All @@ -66,14 +81,7 @@ public QueryCommits(string repo, string filter, Models.CommitSearchMethod method
if (parts.Length != 8)
continue;

var commit = new Models.Commit() { SHA = parts[0] };
commit.ParseParents(parts[1]);
commit.ParseDecorators(parts[2]);
commit.Author = Models.User.FindOrAdd(parts[3]);
commit.AuthorTime = ulong.Parse(parts[4]);
commit.Committer = Models.User.FindOrAdd(parts[5]);
commit.CommitterTime = ulong.Parse(parts[6]);
commit.Subject = parts[7];
var commit = ParseCommit(parts);
commits.Add(commit);

if (!findHead && commit.IsMerged)
Expand Down Expand Up @@ -106,6 +114,99 @@ public QueryCommits(string repo, string filter, Models.CommitSearchMethod method
return commits;
}

private async Task<List<Models.Commit>> GetMixedResultAsync()
{
var commits = new List<Models.Commit>();
if (string.IsNullOrEmpty(_mixedFilter) || CancellationToken.IsCancellationRequested)
return commits;

try
{
using var proc = new Process();
proc.StartInfo = CreateGitStartInfo(true);
proc.Start();
var errors = proc.StandardError.ReadToEndAsync();
try
{
var fields = new string[9];
var fieldIndex = 0;
var field = new StringBuilder();
var buffer = new char[4096];
var completed = false;
int count;
while (!completed && (count = await proc.StandardOutput.ReadAsync(buffer.AsMemory(), CancellationToken).ConfigureAwait(false)) > 0)
{
for (var i = 0; i < count; i++)
{
if (buffer[i] != '\0')
{
field.Append(buffer[i]);
continue;
}

fields[fieldIndex++] = field.ToString();
field.Clear();
if (fieldIndex < fields.Length)
continue;

// git inserts a newline between formatted records.
fields[0] = fields[0].TrimStart('\r', '\n');
if (fields[0].StartsWith(_mixedFilter, StringComparison.OrdinalIgnoreCase) ||
fields[3].Contains(_mixedFilter, StringComparison.OrdinalIgnoreCase) ||
fields[8].Contains(_mixedFilter, StringComparison.OrdinalIgnoreCase))
{
commits.Add(ParseCommit(fields));
if (commits.Count == 1000)
{
completed = true;
break;
}
}
fieldIndex = 0;
}
}
}
finally
{
// Stop promptly on cancellation or when enough matches have been found.
try
{
if (!proc.HasExited)
proc.Kill(true);
}
catch (InvalidOperationException)
{
// The process can exit between HasExited and Kill.
}
await proc.WaitForExitAsync().ConfigureAwait(false);
await errors.ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
// A newer search or closing the search panel cancels this read-only query.
}
catch (Exception e)
{
RaiseException($"Failed to query commits. Reason: {e.Message}");
}
return commits;
}

private static Models.Commit ParseCommit(string[] parts)
{
var commit = new Models.Commit() { SHA = parts[0] };
commit.ParseParents(parts[1]);
commit.ParseDecorators(parts[2]);
commit.Author = Models.User.FindOrAdd(parts[3]);
commit.AuthorTime = ulong.Parse(parts[4]);
commit.Committer = Models.User.FindOrAdd(parts[5]);
commit.CommitterTime = ulong.Parse(parts[6]);
commit.Subject = parts[7];
return commit;
}

private string _mixedFilter = null;
private bool _markMerged = false;
}
}
1 change: 1 addition & 0 deletions src/Models/Commit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum CommitSearchMethod
ByMessage,
ByPath,
ByContent,
Mixed,
}

public class Commit : ObservableObject
Expand Down
2 changes: 2 additions & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,8 @@
<x:String x:Key="Text.Repository.Search.ByContent" xml:space="preserve">Content</x:String>
<x:String x:Key="Text.Repository.Search.ByMessage" xml:space="preserve">Message</x:String>
<x:String x:Key="Text.Repository.Search.ByPath" xml:space="preserve">Path</x:String>
<x:String x:Key="Text.Repository.Search.Mixed" xml:space="preserve">Mixed</x:String>
<x:String x:Key="Text.Repository.Search.Mixed.Tip" xml:space="preserve">Search SHA prefixes, author, and full commit message</x:String>
<x:String x:Key="Text.Repository.Search.BySHA" xml:space="preserve">SHA</x:String>
<x:String x:Key="Text.Repository.Search.InCurrentBranch" xml:space="preserve">Current Branch</x:String>
<x:String x:Key="Text.Repository.Search.Method" xml:space="preserve">SEARCH BY</x:String>
Expand Down
31 changes: 19 additions & 12 deletions src/ViewModels/SearchCommitContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ public SearchCommitContext(string repo, Histories histories)
public void ClearFilter()
{
Filter = string.Empty;
Selected = null;
Results = null;
StartSearch();
}

public void ClearSuggestions()
Expand All @@ -90,6 +89,10 @@ public void ClearSuggestions()

public void StartSearch()
{
if (_cancellation is { IsCancellationRequested: false })
_cancellation.Cancel();

IsQuerying = false;
Results = null;
Selected = null;
Suggestions = null;
Expand All @@ -99,26 +102,25 @@ public void StartSearch()

IsQuerying = true;

if (_cancellation is { IsCancellationRequested: false })
_cancellation.Cancel();

_cancellation = new();
var token = _cancellation.Token;

var method = (Models.CommitSearchMethod)_method;
var filter = _filter;
var onlyCurrentBranch = _onlySearchCurrentBranch;
Task.Run(async () =>
{
var result = new List<Models.Commit>();
var method = (Models.CommitSearchMethod)_method;

if (method == Models.CommitSearchMethod.BySHA)
{
var isCommitSHA = await new Commands.IsCommitSHA(_repo, _filter)
var isCommitSHA = await new Commands.IsCommitSHA(_repo, filter)
.GetResultAsync()
.ConfigureAwait(false);

if (isCommitSHA)
{
var commit = await new Commands.QuerySingleCommit(_repo, _filter)
var commit = await new Commands.QuerySingleCommit(_repo, filter)
.GetResultAsync()
.ConfigureAwait(false);

Expand All @@ -129,9 +131,10 @@ public void StartSearch()
result.Add(commit);
}
}
else if (_onlySearchCurrentBranch)
else if (onlyCurrentBranch)
{
result = await new Commands.QueryCommits(_repo, _filter, method, true)
result = await new Commands.QueryCommits(_repo, filter, method, true)
.WithCancellation(token)
.GetResultAsync()
.ConfigureAwait(false);

Expand All @@ -140,10 +143,14 @@ public void StartSearch()
}
else
{
result = await new Commands.QueryCommits(_repo, _filter, method, false)
result = await new Commands.QueryCommits(_repo, filter, method, false)
.WithCancellation(token)
.GetResultAsync()
.ConfigureAwait(false);

if (token.IsCancellationRequested)
return;

if (result.Count > 0)
{
var set = await new Commands.QueryCurrentBranchCommitHashes(_repo, result[^1].CommitterTime)
Expand Down Expand Up @@ -288,7 +295,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.Mixed;
private string _filter = string.Empty;
private bool _onlySearchCurrentBranch = false;
private bool _isQuerying = false;
Expand Down
1 change: 1 addition & 0 deletions src/Views/CommitSearchView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
<TextBlock Text="{DynamicResource Text.Repository.Search.ByMessage}"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByPath}"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.ByContent}"/>
<TextBlock Text="{DynamicResource Text.Repository.Search.Mixed}" ToolTip.Tip="{DynamicResource Text.Repository.Search.Mixed.Tip}"/>
</ComboBox.Items>
</ComboBox>

Expand Down
10 changes: 10 additions & 0 deletions tests/MixedSearchChecks/MixedSearchChecks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../src/SourceGit.csproj" />
</ItemGroup>
</Project>
Loading