fix(worktree): auto-bootstrap commit-less repos before creating worktrees
Covers autonomous runs, interactive ConPTY sessions (GetInteractiveLaunchSpec), planning sessions, and improvement/planning children — every path that needs a base commit now self-heals on a fresh 'git init' repo instead of surfacing "ambiguous argument 'HEAD'" as a HubException. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit ec16a85c495ceceaa22f922e077a27dcda54f035)
This commit is contained in:
@@ -98,6 +98,9 @@ public sealed class PlanningSessionManager
|
||||
if (!await _git.IsGitRepoAsync(listWorkingDir, ct))
|
||||
throw new InvalidOperationException($"Working directory is not a git repository: {listWorkingDir}");
|
||||
|
||||
// A fresh `git init` repo has no HEAD commit to base the planning worktree on.
|
||||
await _git.EnsureHeadCommitAsync(listWorkingDir, ct);
|
||||
|
||||
var appSettings = await settings.GetAsync(ct);
|
||||
var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir);
|
||||
var branchName = BranchNameFor(taskId);
|
||||
|
||||
@@ -46,6 +46,10 @@ public sealed class WorktreeManager
|
||||
if (!await _git.IsGitRepoAsync(workingDir, ct))
|
||||
throw new InvalidOperationException($"working_dir is not a git repository: {workingDir}");
|
||||
|
||||
// A fresh `git init` repo has no HEAD commit to base a worktree on — bootstrap one.
|
||||
if (await _git.EnsureHeadCommitAsync(workingDir, ct))
|
||||
_logger.LogInformation("Repository at {Dir} had no commits; created an empty bootstrap commit", workingDir);
|
||||
|
||||
var baseCommit = await ResolveBaseCommitAsync(task, workingDir, ct);
|
||||
// Use the full task id (dashes stripped) in the branch name so
|
||||
// two GUIDs sharing an 8-char prefix cannot collide on the same branch.
|
||||
|
||||
@@ -191,6 +191,34 @@ public class WorktreeManagerTests : IDisposable
|
||||
Assert.Contains("hello.txt", row.DiffStat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_EmptyRepo_BootstrapsInitialCommit()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
// Fresh `git init`, zero commits, no identity configured — the cross-PC failure
|
||||
// that surfaced as "git rev-parse HEAD failed: ambiguous argument 'HEAD'".
|
||||
var repoDir = Path.Combine(Path.GetTempPath(), $"claudedo_emptyrepo_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(repoDir);
|
||||
GitRepoFixture.RunGit(repoDir, "init", "-b", "main");
|
||||
_tempDirs.Add(repoDir);
|
||||
|
||||
var (task, list) = MakeEntities(repoDir);
|
||||
var (mgr, db) = await CreateManagerAsync(task, list);
|
||||
|
||||
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_worktreeCleanups.Add((repoDir, ctx.WorktreePath));
|
||||
|
||||
Assert.True(Directory.Exists(ctx.WorktreePath));
|
||||
// The base commit is the auto-created bootstrap commit.
|
||||
var head = GitRepoFixture.RunGit(repoDir, "rev-parse", "HEAD").Trim();
|
||||
Assert.Equal(head, ctx.BaseCommit);
|
||||
|
||||
using var readCtx = db.CreateContext();
|
||||
var row = await new WorktreeRepository(readCtx).GetByTaskIdAsync(task.Id);
|
||||
Assert.Equal(WorktreeState.Active, row!.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_NonGitDir_Throws_NoRow()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user