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', () => {
|
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', () => {
|
it('should run git fetch, resolve origin/<branch> commit hash, and reset --hard in the clone', () => {
|
||||||
// Given: autoFetch is enabled in global config.
|
// Given: autoFetch is enabled in global config.
|
||||||
|
|||||||
@ -122,12 +122,23 @@ export class CloneManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static branchExists(projectDir: string, branch: string): boolean {
|
private static branchExists(projectDir: string, branch: string): boolean {
|
||||||
|
// Local branch
|
||||||
try {
|
try {
|
||||||
execFileSync('git', ['rev-parse', '--verify', branch], {
|
execFileSync('git', ['rev-parse', '--verify', branch], {
|
||||||
cwd: projectDir,
|
cwd: projectDir,
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
});
|
});
|
||||||
return true;
|
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 {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user