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))
|
if (!await _git.IsGitRepoAsync(listWorkingDir, ct))
|
||||||
throw new InvalidOperationException($"Working directory is not a git repository: {listWorkingDir}");
|
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 appSettings = await settings.GetAsync(ct);
|
||||||
var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir);
|
var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir);
|
||||||
var branchName = BranchNameFor(taskId);
|
var branchName = BranchNameFor(taskId);
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ public sealed class WorktreeManager
|
|||||||
if (!await _git.IsGitRepoAsync(workingDir, ct))
|
if (!await _git.IsGitRepoAsync(workingDir, ct))
|
||||||
throw new InvalidOperationException($"working_dir is not a git repository: {workingDir}");
|
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);
|
var baseCommit = await ResolveBaseCommitAsync(task, workingDir, ct);
|
||||||
// Use the full task id (dashes stripped) in the branch name so
|
// 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.
|
// 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);
|
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]
|
[Fact]
|
||||||
public async Task CreateAsync_NonGitDir_Throws_NoRow()
|
public async Task CreateAsync_NonGitDir_Throws_NoRow()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user