takt: github-issue-398-branchexists (#401)
This commit is contained in:
parent
ae74c0d595
commit
e256db8dea
@ -570,6 +570,173 @@ describe('clone submodule arguments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('branchExists remote tracking branch fallback', () => {
|
||||
it('should clone with existing branch when only remote tracking branch exists', () => {
|
||||
// Given: local branch does not exist, but origin/<branch> does
|
||||
const cloneCalls: string[][] = [];
|
||||
const checkoutCalls: string[][] = [];
|
||||
|
||||
mockExecFileSync.mockImplementation((_cmd, args) => {
|
||||
const argsArr = args as string[];
|
||||
|
||||
// resolveBaseBranch: detectDefaultBranch
|
||||
if (argsArr[0] === 'rev-parse' && argsArr[1] === '--abbrev-ref' && argsArr[2] === 'HEAD') {
|
||||
return 'main\n';
|
||||
}
|
||||
|
||||
// branchExists: git rev-parse --verify <branch>
|
||||
if (argsArr[0] === 'rev-parse' && argsArr[1] === '--verify') {
|
||||
const ref = argsArr[2];
|
||||
if (typeof ref === 'string' && ref.startsWith('origin/')) {
|
||||
// Remote tracking branch exists
|
||||
return Buffer.from('abc123');
|
||||
}
|
||||
// Local branch does not exist
|
||||
throw new Error('branch not found');
|
||||
}
|
||||
|
||||
if (argsArr[0] === 'clone') {
|
||||
cloneCalls.push(argsArr);
|
||||
return Buffer.from('');
|
||||
}
|
||||
if (argsArr[0] === 'remote') return Buffer.from('');
|
||||
if (argsArr[0] === 'config') {
|
||||
if (argsArr[1] === '--local') throw new Error('not set');
|
||||
return Buffer.from('');
|
||||
}
|
||||
if (argsArr[0] === 'checkout') {
|
||||
checkoutCalls.push(argsArr);
|
||||
return Buffer.from('');
|
||||
}
|
||||
|
||||
return Buffer.from('');
|
||||
});
|
||||
|
||||
// When
|
||||
const result = createSharedClone('/project', {
|
||||
worktree: '/tmp/clone-remote-branch',
|
||||
taskSlug: 'remote-branch-task',
|
||||
branch: 'feature/remote-only',
|
||||
});
|
||||
|
||||
// Then: branch is the requested branch name
|
||||
expect(result.branch).toBe('feature/remote-only');
|
||||
|
||||
// Then: cloneAndIsolate was called with --branch feature/remote-only (not base branch)
|
||||
expect(cloneCalls).toHaveLength(1);
|
||||
expect(cloneCalls[0]).toContain('--branch');
|
||||
expect(cloneCalls[0]).toContain('feature/remote-only');
|
||||
|
||||
// Then: no checkout -b was called (branch already exists on remote)
|
||||
expect(checkoutCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should create new branch when neither local nor remote tracking branch exists', () => {
|
||||
// Given: neither local nor remote tracking branch exists
|
||||
const cloneCalls: string[][] = [];
|
||||
const checkoutCalls: string[][] = [];
|
||||
|
||||
mockExecFileSync.mockImplementation((_cmd, args) => {
|
||||
const argsArr = args as string[];
|
||||
|
||||
if (argsArr[0] === 'rev-parse' && argsArr[1] === '--abbrev-ref' && argsArr[2] === 'HEAD') {
|
||||
return 'main\n';
|
||||
}
|
||||
|
||||
// Both local and remote tracking branch not found
|
||||
if (argsArr[0] === 'rev-parse' && argsArr[1] === '--verify') {
|
||||
throw new Error('branch not found');
|
||||
}
|
||||
|
||||
if (argsArr[0] === 'clone') {
|
||||
cloneCalls.push(argsArr);
|
||||
return Buffer.from('');
|
||||
}
|
||||
if (argsArr[0] === 'remote') return Buffer.from('');
|
||||
if (argsArr[0] === 'config') {
|
||||
if (argsArr[1] === '--local') throw new Error('not set');
|
||||
return Buffer.from('');
|
||||
}
|
||||
if (argsArr[0] === 'checkout') {
|
||||
checkoutCalls.push(argsArr);
|
||||
return Buffer.from('');
|
||||
}
|
||||
|
||||
return Buffer.from('');
|
||||
});
|
||||
|
||||
// When
|
||||
const result = createSharedClone('/project', {
|
||||
worktree: '/tmp/clone-no-branch',
|
||||
taskSlug: 'no-branch-task',
|
||||
branch: 'feature/brand-new',
|
||||
});
|
||||
|
||||
// Then: branch is the requested branch name
|
||||
expect(result.branch).toBe('feature/brand-new');
|
||||
|
||||
// Then: cloneAndIsolate was called with --branch main (base branch)
|
||||
expect(cloneCalls).toHaveLength(1);
|
||||
expect(cloneCalls[0]).toContain('--branch');
|
||||
expect(cloneCalls[0]).toContain('main');
|
||||
|
||||
// Then: checkout -b was called to create the new branch
|
||||
expect(checkoutCalls).toHaveLength(1);
|
||||
expect(checkoutCalls[0]).toEqual(['checkout', '-b', 'feature/brand-new']);
|
||||
});
|
||||
|
||||
it('should prefer local branch over remote tracking branch', () => {
|
||||
// Given: local branch exists
|
||||
const cloneCalls: string[][] = [];
|
||||
const checkoutCalls: string[][] = [];
|
||||
|
||||
mockExecFileSync.mockImplementation((_cmd, args) => {
|
||||
const argsArr = args as string[];
|
||||
|
||||
if (argsArr[0] === 'rev-parse' && argsArr[1] === '--abbrev-ref' && argsArr[2] === 'HEAD') {
|
||||
return 'main\n';
|
||||
}
|
||||
|
||||
// Local branch exists (first rev-parse --verify call succeeds)
|
||||
if (argsArr[0] === 'rev-parse' && argsArr[1] === '--verify') {
|
||||
return Buffer.from('def456');
|
||||
}
|
||||
|
||||
if (argsArr[0] === 'clone') {
|
||||
cloneCalls.push(argsArr);
|
||||
return Buffer.from('');
|
||||
}
|
||||
if (argsArr[0] === 'remote') return Buffer.from('');
|
||||
if (argsArr[0] === 'config') {
|
||||
if (argsArr[1] === '--local') throw new Error('not set');
|
||||
return Buffer.from('');
|
||||
}
|
||||
if (argsArr[0] === 'checkout') {
|
||||
checkoutCalls.push(argsArr);
|
||||
return Buffer.from('');
|
||||
}
|
||||
|
||||
return Buffer.from('');
|
||||
});
|
||||
|
||||
// When
|
||||
const result = createSharedClone('/project', {
|
||||
worktree: '/tmp/clone-local-branch',
|
||||
taskSlug: 'local-branch-task',
|
||||
branch: 'feature/local-exists',
|
||||
});
|
||||
|
||||
// Then: cloneAndIsolate was called with --branch feature/local-exists
|
||||
expect(result.branch).toBe('feature/local-exists');
|
||||
expect(cloneCalls).toHaveLength(1);
|
||||
expect(cloneCalls[0]).toContain('--branch');
|
||||
expect(cloneCalls[0]).toContain('feature/local-exists');
|
||||
|
||||
// Then: no checkout -b was called (branch already exists locally)
|
||||
expect(checkoutCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('autoFetch: true — fetch, rev-parse origin/<branch>, reset --hard', () => {
|
||||
it('should run git fetch, resolve origin/<branch> commit hash, and reset --hard in the clone', () => {
|
||||
// Given: autoFetch is enabled in global config.
|
||||
|
||||
@ -122,12 +122,23 @@ export class CloneManager {
|
||||
}
|
||||
|
||||
private static branchExists(projectDir: string, branch: string): boolean {
|
||||
// Local branch
|
||||
try {
|
||||
execFileSync('git', ['rev-parse', '--verify', branch], {
|
||||
cwd: projectDir,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
// not found locally — fall through to remote check
|
||||
}
|
||||
// Remote tracking branch
|
||||
try {
|
||||
execFileSync('git', ['rev-parse', '--verify', `origin/${branch}`], {
|
||||
cwd: projectDir,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user