Skip to content

Commit 77dfc49

Browse files
committed
feat(history): better git commit history management and views
The release will include: - Git-Flow Support branch functionality (EXPERIMENTAL) - Refresh buttons for Local Branches and History menus - Various bug fixes and performance improvements - Memory optimization utilities for large repositories
2 parents 74d89c3 + bf191fa commit 77dfc49

23 files changed

+1380
-87
lines changed

.claude/settings.local.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
"Bash(git cherry-pick:*)",
1717
"Bash(find:*)",
1818
"Bash(dotnet test:*)",
19-
"Bash(git add:*)"
19+
"Bash(git add:*)",
20+
"Bash(dotnet clean:*)",
21+
"WebSearch",
22+
"Bash(./check_before_tag.sh:*)"
2023
],
2124
"deny": [],
2225
"ask": [],

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2025.34.11
1+
2025.34.11

src/Commands/GitFlow.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace SourceGit.Commands
55
{
66
public static class GitFlow
77
{
8-
public static async Task<bool> InitAsync(string repo, string master, string develop, string feature, string release, string hotfix, string version, Models.ICommandLog log)
8+
public static async Task<bool> InitAsync(string repo, string master, string develop, string feature, string release, string hotfix, string support, string version, Models.ICommandLog log)
99
{
1010
var config = new Config(repo);
1111
await config.SetAsync("gitflow.branch.master", master).ConfigureAwait(false);
@@ -14,7 +14,7 @@ public static async Task<bool> InitAsync(string repo, string master, string deve
1414
await config.SetAsync("gitflow.prefix.bugfix", "bugfix/").ConfigureAwait(false);
1515
await config.SetAsync("gitflow.prefix.release", release).ConfigureAwait(false);
1616
await config.SetAsync("gitflow.prefix.hotfix", hotfix).ConfigureAwait(false);
17-
await config.SetAsync("gitflow.prefix.support", "support/").ConfigureAwait(false);
17+
await config.SetAsync("gitflow.prefix.support", support).ConfigureAwait(false);
1818
await config.SetAsync("gitflow.prefix.versiontag", version, true).ConfigureAwait(false);
1919

2020
var init = new Command();
@@ -41,6 +41,9 @@ public static async Task<bool> StartAsync(string repo, Models.GitFlowBranchType
4141
case Models.GitFlowBranchType.Hotfix:
4242
start.Args = $"flow hotfix start {name}";
4343
break;
44+
case Models.GitFlowBranchType.Support:
45+
start.Args = $"flow support start {name}";
46+
break;
4447
default:
4548
App.RaiseException(repo, "Bad git-flow branch type!!!");
4649
return false;
@@ -65,6 +68,9 @@ public static async Task<bool> FinishAsync(string repo, Models.GitFlowBranchType
6568
case Models.GitFlowBranchType.Hotfix:
6669
builder.Append("hotfix");
6770
break;
71+
case Models.GitFlowBranchType.Support:
72+
builder.Append("support");
73+
break;
6874
default:
6975
App.RaiseException(repo, "Bad git-flow branch type!!!");
7076
return false;
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace SourceGit.Commands
8+
{
9+
/// <summary>
10+
/// Optimized QueryCommits implementation with batching, memory optimization, and progress feedback
11+
/// Designed for handling large repositories (1000+ commits) without UI freezing
12+
/// </summary>
13+
public class QueryCommitsOptimized : Command
14+
{
15+
private const int BATCH_SIZE = 50; // Process commits in batches
16+
private const int YIELD_FREQUENCY = 100; // Yield control every N commits
17+
18+
public QueryCommitsOptimized(string repo, string limits, bool needFindHead = true)
19+
{
20+
WorkingDirectory = repo;
21+
Context = repo;
22+
Args = $"log --no-show-signature --decorate=full --format=%H%x00%P%x00%D%x00%aN±%aE%x00%at%x00%cN±%cE%x00%ct%x00%s {limits}";
23+
_findFirstMerged = needFindHead;
24+
}
25+
26+
public QueryCommitsOptimized(string repo, string filter, Models.CommitSearchMethod method, bool onlyCurrentBranch)
27+
{
28+
string search = onlyCurrentBranch ? string.Empty : "--branches --remotes ";
29+
30+
if (method == Models.CommitSearchMethod.ByAuthor)
31+
{
32+
search += $"-i --author={filter.Quoted()}";
33+
}
34+
else if (method == Models.CommitSearchMethod.ByCommitter)
35+
{
36+
search += $"-i --committer={filter.Quoted()}";
37+
}
38+
else if (method == Models.CommitSearchMethod.ByMessage)
39+
{
40+
var argsBuilder = new StringBuilder();
41+
argsBuilder.Append(search);
42+
43+
var words = filter.Split([' ', '\t', '\r'], StringSplitOptions.RemoveEmptyEntries);
44+
foreach (var word in words)
45+
argsBuilder.Append("--grep=").Append(word.Trim().Quoted()).Append(' ');
46+
argsBuilder.Append("--all-match -i");
47+
48+
search = argsBuilder.ToString();
49+
}
50+
else if (method == Models.CommitSearchMethod.ByPath)
51+
{
52+
search += $"-- {filter.Quoted()}";
53+
}
54+
else
55+
{
56+
search = $"-G{filter.Quoted()}";
57+
}
58+
59+
WorkingDirectory = repo;
60+
Context = repo;
61+
Args = $"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 {search}";
62+
_findFirstMerged = false;
63+
}
64+
65+
public async Task<List<Models.Commit>> GetResultAsync()
66+
{
67+
// Use memory-optimized collection
68+
var commits = Models.MemoryOptimizer.RentCommitList();
69+
var stopwatch = Stopwatch.StartNew();
70+
71+
try
72+
{
73+
using var proc = new Process();
74+
proc.StartInfo = CreateGitStartInfo(true);
75+
proc.Start();
76+
77+
var rawLines = Models.MemoryOptimizer.RentStringList();
78+
var lineCount = 0;
79+
80+
// Read all lines first with batching
81+
while (await proc.StandardOutput.ReadLineAsync() is { } line)
82+
{
83+
rawLines.Add(line);
84+
lineCount++;
85+
86+
// Process in batches to prevent memory buildup
87+
if (rawLines.Count >= BATCH_SIZE)
88+
{
89+
await ProcessCommitBatch(rawLines, commits);
90+
rawLines.Clear();
91+
92+
// Yield control periodically to keep UI responsive
93+
if (lineCount % YIELD_FREQUENCY == 0)
94+
{
95+
await Task.Delay(1); // Allow other tasks to run
96+
97+
// Suggest GC every 500 commits to manage memory
98+
if (lineCount % 500 == 0)
99+
{
100+
Models.MemoryOptimizer.SuggestGarbageCollection();
101+
}
102+
}
103+
}
104+
}
105+
106+
// Process remaining commits
107+
if (rawLines.Count > 0)
108+
{
109+
await ProcessCommitBatch(rawLines, commits);
110+
}
111+
112+
// Return string list to pool
113+
Models.MemoryOptimizer.ReturnStringList(rawLines);
114+
115+
await proc.WaitForExitAsync().ConfigureAwait(false);
116+
117+
if (_findFirstMerged && !_isHeadFound && commits.Count > 0)
118+
await MarkFirstMergedAsync(commits).ConfigureAwait(false);
119+
120+
System.Diagnostics.Debug.WriteLine($"[PERF] QueryCommitsOptimized processed {commits.Count} commits in {stopwatch.ElapsedMilliseconds}ms");
121+
122+
// Create a new list to return (can't return pooled list)
123+
var result = new List<Models.Commit>(commits);
124+
Models.MemoryOptimizer.ReturnCommitList(commits);
125+
126+
return result;
127+
}
128+
catch (Exception e)
129+
{
130+
// Make sure to return pooled resources on error
131+
Models.MemoryOptimizer.ReturnCommitList(commits);
132+
App.RaiseException(Context, $"Failed to query commits. Reason: {e.Message}");
133+
return new List<Models.Commit>();
134+
}
135+
}
136+
137+
private static async Task ProcessCommitBatch(List<string> lines, List<Models.Commit> commits)
138+
{
139+
await Task.Run(() =>
140+
{
141+
foreach (var line in lines)
142+
{
143+
var parts = line.Split('\0');
144+
if (parts.Length != 8)
145+
continue;
146+
147+
var commit = CreateCommitFromParts(parts);
148+
commits.Add(commit);
149+
}
150+
});
151+
}
152+
153+
private static Models.Commit CreateCommitFromParts(string[] parts)
154+
{
155+
var commit = new Models.Commit() { SHA = parts[0] };
156+
157+
// Optimize memory allocation by reusing existing User objects
158+
commit.ParseParents(parts[1]);
159+
commit.ParseDecorators(parts[2]);
160+
commit.Author = Models.User.FindOrAdd(parts[3]);
161+
commit.AuthorTime = ulong.Parse(parts[4]);
162+
commit.Committer = Models.User.FindOrAdd(parts[5]);
163+
commit.CommitterTime = ulong.Parse(parts[6]);
164+
commit.Subject = parts[7];
165+
166+
return commit;
167+
}
168+
169+
private async Task MarkFirstMergedAsync(List<Models.Commit> commits)
170+
{
171+
Args = $"log --since={commits[^1].CommitterTimeStr.Quoted()} --format=\"%H\"";
172+
173+
var rs = await ReadToEndAsync().ConfigureAwait(false);
174+
var shas = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
175+
if (shas.Length == 0)
176+
return;
177+
178+
var set = new HashSet<string>(shas);
179+
180+
foreach (var c in commits)
181+
{
182+
if (set.Contains(c.SHA))
183+
{
184+
c.IsMerged = true;
185+
break;
186+
}
187+
}
188+
}
189+
190+
private bool _findFirstMerged = false;
191+
private bool _isHeadFound = false;
192+
}
193+
}

src/Commands/Restore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public Restore(string repo, string pathspecFile, bool isStaged)
3636

3737
var builder = new StringBuilder();
3838
builder.Append("restore ");
39-
builder.Append(isStaged ? "--staged " : "--worktree --recurse-submodules ");
39+
builder.Append(isStaged ? "--staged " : "--source=HEAD --worktree --recurse-submodules ");
4040
builder.Append("--pathspec-from-file=").Append(pathspecFile.Quoted());
4141

4242
Args = builder.ToString();

src/Converters/IntConverters.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ public static class IntConverters
2222
public static readonly FuncValueConverter<int, bool> IsNotOne =
2323
new FuncValueConverter<int, bool>(v => v != 1);
2424

25+
public static readonly FuncValueConverter<int, bool> IsNotInfinite =
26+
new FuncValueConverter<int, bool>(v => v != 0 && v < int.MaxValue);
27+
2528
public static readonly FuncValueConverter<int, bool> IsSubjectLengthBad =
2629
new FuncValueConverter<int, bool>(v => v > ViewModels.Preferences.Instance.SubjectGuideLength);
2730

0 commit comments

Comments
 (0)