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
118 changes: 118 additions & 0 deletions src/Commands/QueryHistoryQuickSearch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace SourceGit.Commands
{
// Search only the commits currently loaded in history, including their full messages.
public class QueryHistoryQuickSearch : Command
{
public QueryHistoryQuickSearch(string repo, IReadOnlyList<Models.Commit> commits, string filter, CancellationToken cancellationToken)
{
WorkingDirectory = repo;
Context = repo;
Args = "log --no-walk=unsorted --stdin --no-show-signature --format=%H%x00%aN%x00%aE%x00%B%x00";
CancellationToken = cancellationToken;
_commits = commits;
_filter = filter;
}

public async Task<HashSet<string>> GetResultAsync()
{
var matches = new HashSet<string>(StringComparer.Ordinal);
if (_filter.Length == 0 || _commits.Count == 0)
return matches;

CancellationToken.ThrowIfCancellationRequested();
using var proc = new Process();
proc.StartInfo = CreateGitStartInfo(true);
proc.StartInfo.RedirectStandardInput = true;
proc.Start();
var input = WriteCommitsAsync(proc);
var errors = proc.StandardError.ReadToEndAsync(CancellationToken);
try
{
// Stream NUL-delimited fields as Mixed search does. One Git process handles
// the loaded history, without retaining every full message in memory.
var fields = new string[4];
var fieldIndex = 0;
var field = new StringBuilder();
var buffer = new char[4096];
int count;
while ((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;

var sha = fields[0].TrimStart('\r', '\n');
// Same literal, case-insensitive OR matching as Commit Search's Mixed mode.
if (sha.StartsWith(_filter, StringComparison.OrdinalIgnoreCase) ||
(fields[1] + "±" + fields[2]).Contains(_filter, StringComparison.OrdinalIgnoreCase) ||
fields[3].Contains(_filter, StringComparison.OrdinalIgnoreCase))
matches.Add(sha);
fieldIndex = 0;
}
}
await proc.WaitForExitAsync(CancellationToken).ConfigureAwait(false);
var error = await errors.ConfigureAwait(false);
if (proc.ExitCode != 0)
throw new InvalidOperationException(error);
await input.ConfigureAwait(false);
}
finally
{
try
{
if (!proc.HasExited)
proc.Kill(true);
}
catch (InvalidOperationException)
{
// The process may have exited between HasExited and Kill.
}
await proc.WaitForExitAsync().ConfigureAwait(false);
// Observe the input/error tasks even when Git exits early or the query is cancelled.
try
{
await input.ConfigureAwait(false);
}
catch (Exception)
{
// Preserve the original query error.
}
try
{
await errors.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// The query was superseded.
}
}
return matches;
}

private async Task WriteCommitsAsync(Process proc)
{
foreach (var commit in _commits)
await proc.StandardInput.WriteLineAsync(commit.SHA.AsMemory(), CancellationToken).ConfigureAwait(false);
proc.StandardInput.Close();
}

private readonly IReadOnlyList<Models.Commit> _commits;
private readonly string _filter;
}
}
111 changes: 100 additions & 11 deletions src/Models/CommitGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,95 @@ public class Dot
public List<Link> Links { get; } = [];
public List<Dot> Dots { get; } = [];

public static CommitGraph GenerateFiltered(List<Commit> allCommits, List<Commit> visibleCommits, bool firstParentOnlyEnabled, CommitGraphHighlighting highlighting, HashSet<string> highlightExtraCommits)
{
var visible = new HashSet<string>();
foreach (var commit in visibleCommits)
visible.Add(commit.SHA);

var visibleOrder = new Dictionary<string, int>();
for (var i = 0; i < visibleCommits.Count; i++)
visibleOrder[visibleCommits[i].SHA] = i;

var nearest = new Dictionary<string, List<string>>();
var parents = new Dictionary<string, List<string>>();
// History is ordered children before parents. Resolve hidden ancestors from
// oldest to newest, stopping each path at its first visible commit.
for (var i = allCommits.Count - 1; i >= 0; i--)
{
var commit = allCommits[i];
var parentCount = firstParentOnlyEnabled ? Math.Min(1, commit.Parents.Count) : commit.Parents.Count;
List<string> resolved = [];
if (parentCount == 1)
{
if (nearest.TryGetValue(commit.Parents[0], out var ancestors))
resolved = ancestors;
}
else if (parentCount > 1)
{
var seen = new HashSet<string>();
for (var j = 0; j < parentCount; j++)
{
if (!nearest.TryGetValue(commit.Parents[j], out var ancestors))
continue;

foreach (var ancestor in ancestors)
{
if (seen.Add(ancestor))
resolved.Add(ancestor);
}
}
}

if (parentCount > 1 && resolved.Count > 1)
{
// Hidden merges can expose both a visible commit and its ancestors.
// Keep only the nearest targets: the older targets are already
// reachable through the nearer ones and would add redundant lanes.
var candidates = new HashSet<string>(resolved);
var redundant = new HashSet<string>();
var visited = new HashSet<string>();
var pending = new Stack<string>();
var oldest = 0;
foreach (var target in resolved)
{
oldest = Math.Max(oldest, visibleOrder[target]);
foreach (var parent in parents[target])
pending.Push(parent);
}
while (pending.TryPop(out var ancestor))
{
if (visibleOrder[ancestor] > oldest || !visited.Add(ancestor))
continue;

if (candidates.Contains(ancestor))
redundant.Add(ancestor);
foreach (var parent in parents[ancestor])
pending.Push(parent);
}
resolved.RemoveAll(redundant.Contains);
}

if (visible.Contains(commit.SHA))
{
parents[commit.SHA] = resolved;
nearest[commit.SHA] = [commit.SHA];
}
else
{
nearest[commit.SHA] = resolved;
}
}

return Generate(visibleCommits, firstParentOnlyEnabled, highlighting, highlightExtraCommits, parents);
}

public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEnabled, CommitGraphHighlighting highlighting, HashSet<string> highlightExtraCommits)
{
return Generate(commits, firstParentOnlyEnabled, highlighting, highlightExtraCommits, null);
}

private static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEnabled, CommitGraphHighlighting highlighting, HashSet<string> highlightExtraCommits, Dictionary<string, List<string>> parentOverrides)
{
const double unitWidth = 12;
const double halfWidth = 6;
Expand All @@ -87,6 +175,7 @@ public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEna

foreach (var commit in commits)
{
var parents = parentOverrides != null && parentOverrides.TryGetValue(commit.SHA, out var projected) ? projected : commit.Parents;
PathHelper major = null;

// Update current y offset
Expand All @@ -106,9 +195,9 @@ public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEna
major = l;
isHighlighted = major.IsHighlighted;

if (commit.Parents.Count > 0)
if (parents.Count > 0)
{
major.Next = commit.Parents[0];
major.Next = parents[0];
major.Goto(offsetX, offsetY, halfHeight);
}
else
Expand Down Expand Up @@ -155,8 +244,8 @@ public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEna
{
isHighlighted = true;
// Highlight first parent, other parents are dealt with later
if (commit.Parents.Count > 0)
highlightExtraCommits.Add(commit.Parents[0]);
if (parents.Count > 0)
highlightExtraCommits.Add(parents[0]);
}
break;
default: // CommitGraphHighlighting.CurrentBranchAndSelectedCommits
Expand All @@ -168,8 +257,8 @@ public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEna
{
isHighlighted = true;
// Highlight first parent, other parents are dealt with later
if (commit.Parents.Count > 0)
highlightExtraCommits.Add(commit.Parents[0]);
if (parents.Count > 0)
highlightExtraCommits.Add(parents[0]);
}
break;
}
Expand All @@ -182,14 +271,14 @@ public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEna
{
offsetX += unitWidth;

if (commit.Parents.Count > 0)
if (parents.Count > 0)
{
major = new PathHelper(commit.Parents[0], isHighlighted, colorPicker.Next(), new Point(offsetX, offsetY));
major = new PathHelper(parents[0], isHighlighted, colorPicker.Next(), new Point(offsetX, offsetY));
unsolved.Add(major);
temp.Paths.Add(major.Path);
}
}
else if (isHighlighted && !major.IsHighlighted && commit.Parents.Count > 0)
else if (isHighlighted && !major.IsHighlighted && parents.Count > 0)
{
major.Highlight();
temp.Paths.Add(major.Path);
Expand All @@ -213,9 +302,9 @@ public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEna
if (highlighting == CommitGraphHighlighting.SelectedCommitsOnlyFirstParent)
isHighlighted = false;

for (int j = 1; j < commit.Parents.Count; j++)
for (int j = 1; j < parents.Count; j++)
{
var parentHash = commit.Parents[j];
var parentHash = parents[j];
var parent = unsolved.Find(x => x.Next.Equals(parentHash, StringComparison.Ordinal));
if (parent != null)
{
Expand Down
4 changes: 4 additions & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,10 @@
<x:String x:Key="Text.Histories.HighlightsInGraph.CurrentBranchAndSelectedCommits" xml:space="preserve">Current Branch &amp; Selected Commits</x:String>
<x:String x:Key="Text.Histories.HighlightsInGraph.SelectedCommitsOnly" xml:space="preserve">Selected Commits Only</x:String>
<x:String x:Key="Text.Histories.HighlightsInGraph.SelectedCommitsOnlyFirstParent" xml:space="preserve">Selected Commits (First-parent Only)</x:String>
<x:String x:Key="Text.Histories.QuickSearch" xml:space="preserve">Quick search (Mixed)</x:String>
<x:String x:Key="Text.Histories.QuickSearch.Filter" xml:space="preserve">Show matching commits only</x:String>
<x:String x:Key="Text.Histories.QuickSearch.Next" xml:space="preserve">Next match (F3 / Enter)</x:String>
<x:String x:Key="Text.Histories.QuickSearch.Previous" xml:space="preserve">Previous match (Shift+F3 / Shift+Enter)</x:String>
<x:String x:Key="Text.Histories.Selected" xml:space="preserve">SELECTED {0} COMMITS</x:String>
<x:String x:Key="Text.Histories.ShowColumns" xml:space="preserve">SHOW COLUMNS</x:String>
<x:String x:Key="Text.Histories.Tips" xml:space="preserve">Hold 'Ctrl' or 'Shift' to select multiple commits.</x:String>
Expand Down
10 changes: 10 additions & 0 deletions src/Resources/Styles.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,16 @@
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Accent}"/>
<Setter Property="Background" Value="{DynamicResource Brush.Contents}"/>
</Style>
<Style Selector="Window[IsActive=False] TextBox">
<Setter Property="CaretBrush" Value="Transparent"/>
<Setter Property="SelectionBrush" Value="{DynamicResource Brush.Border2}"/>
<Setter Property="SelectionForegroundBrush" Value="{DynamicResource Brush.FG1}"/>
</Style>
<Style Selector="Window[IsActive=False] TextBox:not(:disabled) /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="{Binding $parent[TextBox].BorderBrush}"/>
<Setter Property="BorderThickness" Value="{Binding $parent[TextBox].BorderThickness}"/>
<Setter Property="Background" Value="{Binding $parent[TextBox].Background}"/>
</Style>
<Style Selector="TextBox:disabled /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Opacity" Value=".6"/>
Expand Down
3 changes: 3 additions & 0 deletions src/Resources/Themes.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<Color x:Key="Color.Popup">#FFFAFAFA</Color>
<Color x:Key="Color.PopupBorder">#FFFFFFFF</Color>
<Color x:Key="Color.Contents">#FFFFFFFF</Color>
<Color x:Key="Color.Histories.FilteredBackground">#FFFFF9E6</Color>
<Color x:Key="Color.Badge">#FFB0CEE8</Color>
<Color x:Key="Color.BadgeFG">#FF1F1F1F</Color>
<Color x:Key="Color.Conflict">#FF836C2E</Color>
Expand Down Expand Up @@ -43,6 +44,7 @@
<Color x:Key="Color.Popup">#FF2B2B2B</Color>
<Color x:Key="Color.PopupBorder">#FF393939</Color>
<Color x:Key="Color.Contents">#FF1C1C1C</Color>
<Color x:Key="Color.Histories.FilteredBackground">#FF302D20</Color>
<Color x:Key="Color.Badge">#FF8F8F8F</Color>
<Color x:Key="Color.BadgeFG">#FFE0E0E0</Color>
<Color x:Key="Color.Conflict">#FFFAFAD2</Color>
Expand Down Expand Up @@ -77,6 +79,7 @@
<SolidColorBrush x:Key="Brush.Popup" Color="{DynamicResource Color.Popup}"/>
<SolidColorBrush x:Key="Brush.PopupBorder" Color="{DynamicResource Color.PopupBorder}"/>
<SolidColorBrush x:Key="Brush.Contents" Color="{DynamicResource Color.Contents}"/>
<SolidColorBrush x:Key="Brush.Histories.FilteredBackground" Color="{DynamicResource Color.Histories.FilteredBackground}"/>
<SolidColorBrush x:Key="Brush.Badge" Color="{DynamicResource Color.Badge}"/>
<SolidColorBrush x:Key="Brush.BadgeFG" Color="{DynamicResource Color.BadgeFG}"/>
<SolidColorBrush x:Key="Brush.Conflict" Color="{DynamicResource Color.Conflict}"/>
Expand Down
Loading