289 lines
8.6 KiB
TypeScript
289 lines
8.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('node:child_process', () => ({
|
|
execFileSync: vi.fn(),
|
|
}));
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import {
|
|
createBranchBaseResolutionCache,
|
|
findFirstTaktCommit,
|
|
resolveBranchBaseCommit,
|
|
} from '../infra/task/branchGitResolver.js';
|
|
|
|
const mockExecFileSync = vi.mocked(execFileSync);
|
|
|
|
describe('branchGitResolver performance', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should skip full ref scan when default branch candidate resolves', () => {
|
|
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
if (cmd !== 'git') {
|
|
throw new Error('unexpected command');
|
|
}
|
|
|
|
if (args[0] === 'reflog') {
|
|
throw new Error('reflog unavailable');
|
|
}
|
|
|
|
if (args[0] === 'merge-base' && args[1] === 'main') {
|
|
return 'base-main';
|
|
}
|
|
|
|
if (args[0] === 'merge-base' && args[1] === 'origin/main') {
|
|
throw new Error('origin/main not available');
|
|
}
|
|
|
|
if (args[0] === 'rev-list') {
|
|
return '1';
|
|
}
|
|
|
|
if (args[0] === 'log' && args[1] === '--format=%s') {
|
|
return 'takt: first';
|
|
}
|
|
|
|
if (args[0] === 'for-each-ref') {
|
|
throw new Error('for-each-ref should not be called');
|
|
}
|
|
|
|
throw new Error(`unexpected git args: ${args.join(' ')}`);
|
|
});
|
|
|
|
const baseCommit = resolveBranchBaseCommit('/project', 'main', 'takt/feature-a');
|
|
|
|
expect(baseCommit).toBe('base-main');
|
|
expect(mockExecFileSync).not.toHaveBeenCalledWith(
|
|
'git',
|
|
['for-each-ref', '--format=%(refname:short)', 'refs/heads', 'refs/remotes'],
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it('should skip full ref scan when default branch candidate resolves without takt prefix', () => {
|
|
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
if (cmd !== 'git') {
|
|
throw new Error('unexpected command');
|
|
}
|
|
|
|
if (args[0] === 'reflog') {
|
|
throw new Error('reflog unavailable');
|
|
}
|
|
|
|
if (args[0] === 'merge-base' && args[1] === 'main') {
|
|
return 'base-main';
|
|
}
|
|
|
|
if (args[0] === 'merge-base' && args[1] === 'origin/main') {
|
|
throw new Error('origin/main not available');
|
|
}
|
|
|
|
if (args[0] === 'rev-list') {
|
|
return '3';
|
|
}
|
|
|
|
if (args[0] === 'log' && args[1] === '--format=%s') {
|
|
return 'feat: add new feature';
|
|
}
|
|
|
|
if (args[0] === 'for-each-ref') {
|
|
throw new Error('for-each-ref should not be called');
|
|
}
|
|
|
|
throw new Error(`unexpected git args: ${args.join(' ')}`);
|
|
});
|
|
|
|
const baseCommit = resolveBranchBaseCommit('/project', 'main', 'takt/feature-a');
|
|
|
|
expect(baseCommit).toBe('base-main');
|
|
expect(mockExecFileSync).not.toHaveBeenCalledWith(
|
|
'git',
|
|
['for-each-ref', '--format=%(refname:short)', 'refs/heads', 'refs/remotes'],
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it('should reuse ref list cache across branch resolutions', () => {
|
|
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
if (cmd !== 'git') {
|
|
throw new Error('unexpected command');
|
|
}
|
|
|
|
if (args[0] === 'reflog') {
|
|
throw new Error('reflog unavailable');
|
|
}
|
|
|
|
if (args[0] === 'merge-base') {
|
|
const baseRef = args[1];
|
|
const branch = args[2];
|
|
if (baseRef === 'main' || baseRef === 'origin/main') {
|
|
throw new Error('priority refs unavailable');
|
|
}
|
|
if (baseRef === 'develop' && branch === 'takt/feature-a') {
|
|
return 'base-a';
|
|
}
|
|
if (baseRef === 'origin/develop' && branch === 'takt/feature-a') {
|
|
return 'base-a-remote';
|
|
}
|
|
if (baseRef === 'develop' && branch === 'takt/feature-b') {
|
|
return 'base-b';
|
|
}
|
|
if (baseRef === 'origin/develop' && branch === 'takt/feature-b') {
|
|
return 'base-b-remote';
|
|
}
|
|
throw new Error(`unexpected merge-base args: ${args.join(' ')}`);
|
|
}
|
|
|
|
if (args[0] === 'for-each-ref') {
|
|
return 'develop\norigin/develop\n';
|
|
}
|
|
|
|
if (args[0] === 'rev-parse' && args[1] === '--git-common-dir') {
|
|
return '/project/.git';
|
|
}
|
|
|
|
if (args[0] === 'rev-list') {
|
|
const range = args[3];
|
|
if (range === 'base-a..takt/feature-a') {
|
|
return '1';
|
|
}
|
|
if (range === 'base-a-remote..takt/feature-a') {
|
|
return '5';
|
|
}
|
|
if (range === 'base-b..takt/feature-b') {
|
|
return '1';
|
|
}
|
|
if (range === 'base-b-remote..takt/feature-b') {
|
|
return '6';
|
|
}
|
|
throw new Error(`unexpected rev-list args: ${args.join(' ')}`);
|
|
}
|
|
|
|
if (args[0] === 'log' && args[1] === '--format=%s') {
|
|
return 'takt: first';
|
|
}
|
|
|
|
throw new Error(`unexpected git args: ${args.join(' ')}`);
|
|
});
|
|
|
|
const cache = createBranchBaseResolutionCache();
|
|
const baseA = resolveBranchBaseCommit('/project', 'main', 'takt/feature-a', cache);
|
|
const baseB = resolveBranchBaseCommit('/project', 'main', 'takt/feature-b', cache);
|
|
|
|
expect(baseA).toBe('base-a');
|
|
expect(baseB).toBe('base-b');
|
|
|
|
const forEachRefCalls = mockExecFileSync.mock.calls.filter(([, args]) =>
|
|
args[0] === 'for-each-ref',
|
|
);
|
|
expect(forEachRefCalls).toHaveLength(1);
|
|
});
|
|
|
|
it('should skip reflog lookup when baseCommit is provided to findFirstTaktCommit', () => {
|
|
mockExecFileSync.mockImplementation((cmd, args) => {
|
|
if (cmd !== 'git') {
|
|
throw new Error('unexpected command');
|
|
}
|
|
|
|
if (args[0] === 'reflog') {
|
|
throw new Error('reflog should not be called');
|
|
}
|
|
|
|
if (args[0] === 'log' && args[1] === '--format=%H\t%s') {
|
|
return 'abc123\ttakt: first instruction\n';
|
|
}
|
|
|
|
throw new Error(`unexpected git args: ${args.join(' ')}`);
|
|
});
|
|
|
|
const first = findFirstTaktCommit('/project', 'main', 'takt/feature-a', { baseCommit: 'base-a' });
|
|
|
|
expect(first).toEqual({ subject: 'takt: first instruction' });
|
|
expect(mockExecFileSync).not.toHaveBeenCalledWith(
|
|
'git',
|
|
['reflog', 'show', '--format=%H', 'takt/feature-a'],
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it('should reuse ref list cache across worktrees in the same repository', () => {
|
|
mockExecFileSync.mockImplementation((cmd, args, options) => {
|
|
if (cmd !== 'git') {
|
|
throw new Error('unexpected command');
|
|
}
|
|
|
|
if (args[0] === 'reflog') {
|
|
throw new Error('reflog unavailable');
|
|
}
|
|
|
|
if (args[0] === 'rev-parse' && args[1] === '--git-common-dir') {
|
|
if (options?.cwd === '/repo/worktrees/a' || options?.cwd === '/repo/worktrees/b') {
|
|
return '/repo/.git';
|
|
}
|
|
throw new Error(`unexpected rev-parse cwd: ${String(options?.cwd)}`);
|
|
}
|
|
|
|
if (args[0] === 'merge-base') {
|
|
const baseRef = args[1];
|
|
const branch = args[2];
|
|
if (baseRef === 'main' || baseRef === 'origin/main') {
|
|
throw new Error('priority refs unavailable');
|
|
}
|
|
if (baseRef === 'develop' && branch === 'takt/feature-a') {
|
|
return 'base-a';
|
|
}
|
|
if (baseRef === 'origin/develop' && branch === 'takt/feature-a') {
|
|
return 'base-a-remote';
|
|
}
|
|
if (baseRef === 'develop' && branch === 'takt/feature-b') {
|
|
return 'base-b';
|
|
}
|
|
if (baseRef === 'origin/develop' && branch === 'takt/feature-b') {
|
|
return 'base-b-remote';
|
|
}
|
|
throw new Error(`unexpected merge-base args: ${args.join(' ')}`);
|
|
}
|
|
|
|
if (args[0] === 'for-each-ref') {
|
|
return 'develop\norigin/develop\n';
|
|
}
|
|
|
|
if (args[0] === 'rev-list') {
|
|
const range = args[3];
|
|
if (range === 'base-a..takt/feature-a') {
|
|
return '1';
|
|
}
|
|
if (range === 'base-a-remote..takt/feature-a') {
|
|
return '5';
|
|
}
|
|
if (range === 'base-b..takt/feature-b') {
|
|
return '1';
|
|
}
|
|
if (range === 'base-b-remote..takt/feature-b') {
|
|
return '6';
|
|
}
|
|
throw new Error(`unexpected rev-list args: ${args.join(' ')}`);
|
|
}
|
|
|
|
if (args[0] === 'log' && args[1] === '--format=%s') {
|
|
return 'takt: first';
|
|
}
|
|
|
|
throw new Error(`unexpected git args: ${args.join(' ')}`);
|
|
});
|
|
|
|
const cache = createBranchBaseResolutionCache();
|
|
const baseA = resolveBranchBaseCommit('/repo/worktrees/a', 'main', 'takt/feature-a', cache);
|
|
const baseB = resolveBranchBaseCommit('/repo/worktrees/b', 'main', 'takt/feature-b', cache);
|
|
|
|
expect(baseA).toBe('base-a');
|
|
expect(baseB).toBe('base-b');
|
|
|
|
const forEachRefCalls = mockExecFileSync.mock.calls.filter(([, args]) =>
|
|
args[0] === 'for-each-ref',
|
|
);
|
|
expect(forEachRefCalls).toHaveLength(1);
|
|
});
|
|
});
|