Skip to content
Merged
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
68 changes: 67 additions & 1 deletion cloudflare-gastown/container/src/git-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,45 @@ async function cloneRepoInner(
`Cloning repo for rig ${options.rigId}: hasAuth=${hasAuth} envKeys=[${Object.keys(options.envVars ?? {}).join(',')}]`
);

// Omit --branch: on empty repos (no commits) the default branch doesn't
// exist yet, so `git clone --branch <branch>` would fail with
// "Remote branch <branch> not found in upstream origin".
await mkdir(dir, { recursive: true });
await exec('git', ['clone', '--no-checkout', '--branch', options.defaultBranch, authUrl, dir]);
await exec('git', ['clone', '--no-checkout', authUrl, dir]);
await configureRepoCredentials(dir, options.gitUrl, options.envVars);

// Detect empty repo: git rev-parse HEAD fails when there are no commits.
const isEmpty = await exec('git', ['rev-parse', 'HEAD'], dir)
.then(() => false)
.catch(() => true);

if (isEmpty) {
console.log(`Detected empty repo for rig ${options.rigId}, creating initial commit`);
// Create an initial empty commit so branches/worktrees can be created.
// Use -c flags for user identity (the repo has no config yet and the
// container may not have GIT_AUTHOR_NAME set).
await exec(
'git',
[
'-c',
'user.name=Gastown',
'-c',
'user.email=gastown@kilo.ai',
'commit',
'--allow-empty',
'-m',
'Initial commit',
],
dir
);
await exec('git', ['push', 'origin', `HEAD:${options.defaultBranch}`], dir);
// Best-effort: set remote HEAD so future operations know the default branch
await exec('git', ['remote', 'set-head', 'origin', options.defaultBranch], dir).catch(() => {});
// Fetch so origin/<defaultBranch> ref is available locally
await exec('git', ['fetch', 'origin'], dir);
console.log(`Created initial commit on empty repo for rig ${options.rigId}`);
}

console.log(`Cloned repo for rig ${options.rigId}`);
return dir;
}
Expand All @@ -303,6 +339,18 @@ async function createWorktreeInner(options: WorktreeOptions): Promise<string> {
return dir;
}

// Verify the repo has at least one commit. If cloneRepoInner's initial
// commit push failed, there's no HEAD and we can't create branches.
const hasHead = await exec('git', ['rev-parse', '--verify', 'HEAD'], repo)
.then(() => true)
.catch(() => false);

if (!hasHead) {
throw new Error(
`Cannot create worktree: repo has no commits. Push an initial commit first or re-connect the rig.`
);
}

// When a startPoint is provided (e.g. a convoy feature branch), create
// the new branch from that ref so the agent begins with the latest
// merged work from upstream. Without a startPoint, try to track the
Expand Down Expand Up @@ -398,6 +446,24 @@ async function setupBrowseWorktreeInner(rigId: string, defaultBranch: string): P
return browseDir;
}

// Check whether origin/<defaultBranch> exists. On a repo that was just
// initialized with an empty commit in cloneRepoInner the ref should
// exist, but if the push failed (network, permissions) it may not.
const hasRemoteBranch = await exec(
'git',
['rev-parse', '--verify', `origin/${defaultBranch}`],
repo
)
.then(() => true)
.catch(() => false);

if (!hasRemoteBranch) {
console.log(
`Skipping browse worktree for rig ${rigId}: origin/${defaultBranch} not found (repo may be empty), will create on next fetch`
);
return browseDir;
}

// Create a worktree on the default branch for browsing.
// Force-create (or reset) the tracking branch to origin/<defaultBranch>
// so a recreated browse worktree always starts from the latest remote
Expand Down
Loading