clone時に既存ブランチのcheckoutが失敗する問題を修正

cloneAndIsolateがgit remote remove originした後、リモート追跡refが
全て消えるため、default以外の既存ブランチをcheckoutできなかった。

git clone --branchでclone時にローカルブランチを作成するように変更。
併せてブランチ名フォーマットからgit非互換の#を除去。
This commit is contained in:
nrslib 2026-02-12 10:22:43 +09:00
parent 0c9d7658f8
commit b54fbe32b2
3 changed files with 22 additions and 13 deletions

View File

@ -46,7 +46,7 @@ function setupRepoForIssue167(options?: { disableReflog?: boolean; firstBranchCo
writeAndCommit(repoDir, 'develop-takt.txt', 'develop takt\n', 'takt: old instruction on develop');
writeAndCommit(repoDir, 'develop-b.txt', 'develop b\n', 'develop commit B');
const taktBranch = 'takt/#167/fix-original-instruction';
const taktBranch = 'takt/167/fix-original-instruction';
runGit(repoDir, ['checkout', '-b', taktBranch]);
const firstBranchCommitMessage = options?.firstBranchCommitMessage ?? 'takt: github-issue-167-fix-original-instruction';
writeAndCommit(repoDir, 'task-1.txt', 'task1\n', firstBranchCommitMessage);

View File

@ -207,7 +207,7 @@ describe('branch and worktree path formatting with issue numbers', () => {
});
}
it('should format branch as takt/#{issue}/{slug} when issue number is provided', () => {
it('should format branch as takt/{issue}/{slug} when issue number is provided', () => {
// Given: issue number 99 with slug
setupMockForPathTest();
@ -219,7 +219,7 @@ describe('branch and worktree path formatting with issue numbers', () => {
});
// Then: branch should use issue format
expect(result.branch).toBe('takt/#99/fix-login-timeout');
expect(result.branch).toBe('takt/99/fix-login-timeout');
});
it('should format branch as takt/{timestamp}-{slug} when no issue number', () => {

View File

@ -77,7 +77,7 @@ export class CloneManager {
const slug = slugify(options.taskSlug);
if (options.issueNumber !== undefined && slug) {
return `takt/#${options.issueNumber}/${slug}`;
return `takt/${options.issueNumber}/${slug}`;
}
const timestamp = CloneManager.generateTimestamp();
@ -124,13 +124,24 @@ export class CloneManager {
return projectDir;
}
/** Clone a repository and remove origin to isolate from the main repo */
private static cloneAndIsolate(projectDir: string, clonePath: string): void {
/** Clone a repository and remove origin to isolate from the main repo.
* When `branch` is specified, `--branch` is passed to `git clone` so the
* branch is checked out as a local branch *before* origin is removed.
* Without this, non-default branches are lost when `git remote remove origin`
* deletes the remote-tracking refs.
*/
private static cloneAndIsolate(projectDir: string, clonePath: string, branch?: string): void {
const referenceRepo = CloneManager.resolveMainRepo(projectDir);
fs.mkdirSync(path.dirname(clonePath), { recursive: true });
execFileSync('git', ['clone', '--reference', referenceRepo, '--dissociate', projectDir, clonePath], {
const cloneArgs = ['clone', '--reference', referenceRepo, '--dissociate'];
if (branch) {
cloneArgs.push('--branch', branch);
}
cloneArgs.push(projectDir, clonePath);
execFileSync('git', cloneArgs, {
cwd: projectDir,
stdio: 'pipe',
});
@ -174,11 +185,10 @@ export class CloneManager {
log.info('Creating shared clone', { path: clonePath, branch });
CloneManager.cloneAndIsolate(projectDir, clonePath);
if (CloneManager.branchExists(clonePath, branch)) {
execFileSync('git', ['checkout', branch], { cwd: clonePath, stdio: 'pipe' });
if (CloneManager.branchExists(projectDir, branch)) {
CloneManager.cloneAndIsolate(projectDir, clonePath, branch);
} else {
CloneManager.cloneAndIsolate(projectDir, clonePath);
execFileSync('git', ['checkout', '-b', branch], { cwd: clonePath, stdio: 'pipe' });
}
@ -195,8 +205,7 @@ export class CloneManager {
log.info('Creating temp clone for branch', { path: clonePath, branch });
CloneManager.cloneAndIsolate(projectDir, clonePath);
execFileSync('git', ['checkout', branch], { cwd: clonePath, stdio: 'pipe' });
CloneManager.cloneAndIsolate(projectDir, clonePath, branch);
this.saveCloneMeta(projectDir, branch, clonePath);
log.info('Temp clone created', { path: clonePath, branch });