import { execFileSync } from 'node:child_process' import { existsSync, promises as fsp, rmSync, writeFileSync } from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { ensureWorktreeFromRemote } from '../../src/git/worktree' const gitEnv: NodeJS.ProcessEnv = { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null', GIT_TERMINAL_PROMPT: '0', GIT_AUTHOR_NAME: 'remote-test', GIT_AUTHOR_EMAIL: 'remote@test.local', GIT_COMMITTER_NAME: 'remote-test', GIT_COMMITTER_EMAIL: 'remote@test.local', } function git(cwd: string, args: string[]): string { return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8', env: gitEnv }).trim() } interface Fixture { root: string, origin: string, repo: string, templatePath: string } /** origin(bare)← 同事的 clone push feature 分支;repo 是接管方的 clone,只有 main。 */ async function setup(): Promise { const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sw-remote-')) const origin = path.join(root, 'origin.git') git(root, ['init', '--bare', '-b', 'main', origin]) const peer = path.join(root, 'peer') git(root, ['clone', origin, peer]) writeFileSync(path.join(peer, 'README.md'), 'hello\n') git(peer, ['add', 'README.md']) git(peer, ['commit', '-m', 'init']) git(peer, ['push', '-u', 'origin', 'main']) git(peer, ['checkout', '-b', 'feature/x']) writeFileSync(path.join(peer, 'x.txt'), 'x\n') git(peer, ['add', 'x.txt']) git(peer, ['commit', '-m', 'feat x']) git(peer, ['push', '-u', 'origin', 'feature/x']) const repo = path.join(root, 'repo') git(root, ['clone', origin, repo]) const templatePath = path.join(root, 'wt', 'x') return { root, origin, repo, templatePath } } let fx: Fixture | undefined afterEach(() => { if (fx) rmSync(fx.root, { recursive: true, force: true }) fx = undefined }) describe('ensureWorktreeFromRemote', () => { it('从 origin 分支建 worktree,本地分支跟踪远端', async () => { fx = await setup() const out = await ensureWorktreeFromRemote({ workspaceRoot: fx.repo, worktreePath: fx.templatePath, branch: 'feature/x' }) expect(out).toBe(fx.templatePath) expect(existsSync(path.join(fx.templatePath, 'x.txt'))).toBe(true) expect(git(fx.templatePath, ['rev-parse', '--abbrev-ref', 'HEAD'])).toBe('feature/x') expect(git(fx.templatePath, ['rev-parse', '--abbrev-ref', '@{u}'])).toBe('origin/feature/x') }) it('该分支已有 live worktree 时直接复用', async () => { fx = await setup() const first = await ensureWorktreeFromRemote({ workspaceRoot: fx.repo, worktreePath: fx.templatePath, branch: 'feature/x' }) const other = path.join(fx.root, 'wt', 'other') const second = await ensureWorktreeFromRemote({ workspaceRoot: fx.repo, worktreePath: other, branch: 'feature/x' }) expect(second).toBe(first) expect(existsSync(other)).toBe(false) }) it('模板路径被非 worktree 目录占着 → 删掉重建', async () => { fx = await setup() await fsp.mkdir(fx.templatePath, { recursive: true }) writeFileSync(path.join(fx.templatePath, 'junk'), '') await ensureWorktreeFromRemote({ workspaceRoot: fx.repo, worktreePath: fx.templatePath, branch: 'feature/x' }) expect(existsSync(path.join(fx.templatePath, 'junk'))).toBe(false) expect(existsSync(path.join(fx.templatePath, 'x.txt'))).toBe(true) }) it('远端没有该分支 → 抛错', async () => { fx = await setup() await expect( ensureWorktreeFromRemote({ workspaceRoot: fx.repo, worktreePath: fx.templatePath, branch: 'feature/none' }), ).rejects.toThrow(/feature\/none/) }) })