feat(worker,data): add git worktree support and conventional commits
GitService (in ClaudeDo.Data so the UI can reuse it) wraps the git CLI:
IsGitRepo, RevParseHead, WorktreeAdd/Remove, HasChanges, AddAll, Commit
(multi-line via -F -), DiffStat, BranchDelete, MergeFfOnly. Throws with
stderr on failure.
WorktreeManager owns the per-task lifecycle: validate working_dir is a
git repo (throws if not, no DB row written), create the worktree at
<repo>/../.claudedo-worktrees/<slug>/<id>/ (or central root per config),
insert the worktrees row. CommitIfChangedAsync skips when there are no
changes, otherwise commits and updates head_commit + diff_stat.
CommitMessageBuilder produces "{type}({list-slug}): {title<=60}" with a
blank-line-separated description (truncated to 400) and a permanent
"ClaudeDo-Task: <id>" trailer. Slug normalises whitespace + strips
non-alphanumerics. Newlines hard-coded to \n so git on Windows doesn't
choke on \r\n.
TaskRunner branches on list.WorkingDir: worktree path runs Claude in the
worktree, commits on success, broadcasts WorktreeUpdated; failure leaves
the worktree row active for inspection. Sandbox path unchanged.
Tests: 38 pass (12 new). GitRepoFixture spins up a real temp repo with a
seed commit; tests skip gracefully if `git` isn't on PATH.
CommitMessageBuilder fully unit-tested. WorktreeManager covers create,
no-change skip, real-commit, and non-repo failure.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
46
src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs
Normal file
46
src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ClaudeDo.Worker.Runner;
|
||||
|
||||
public static class CommitMessageBuilder
|
||||
{
|
||||
public static string Build(string commitType, string listName, string taskTitle, string? taskDescription, string taskId)
|
||||
{
|
||||
var slug = ToSlug(listName);
|
||||
var title = Truncate(taskTitle, 60);
|
||||
var header = $"{commitType}({slug}): {title}";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(header);
|
||||
|
||||
var hasDescription = !string.IsNullOrWhiteSpace(taskDescription);
|
||||
if (hasDescription)
|
||||
{
|
||||
sb.Append("\n\n");
|
||||
sb.Append(Truncate(taskDescription!.Trim(), 400));
|
||||
}
|
||||
|
||||
// Trailer is always included.
|
||||
sb.Append("\n\n");
|
||||
sb.Append($"ClaudeDo-Task: {taskId}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string ToSlug(string name)
|
||||
{
|
||||
var lower = name.ToLowerInvariant();
|
||||
// Replace whitespace runs with a single dash.
|
||||
var dashed = Regex.Replace(lower, @"\s+", "-");
|
||||
// Remove all non-alphanumeric-and-dash characters.
|
||||
var cleaned = Regex.Replace(dashed, @"[^a-z0-9\-]", "");
|
||||
// Collapse multiple dashes.
|
||||
var collapsed = Regex.Replace(cleaned, @"-{2,}", "-");
|
||||
// Trim leading/trailing dashes.
|
||||
return collapsed.Trim('-');
|
||||
}
|
||||
|
||||
private static string Truncate(string value, int maxLength) =>
|
||||
value.Length <= maxLength ? value : value[..maxLength];
|
||||
}
|
||||
Reference in New Issue
Block a user