🐛 fix(vscode): 实施启动改为复用已有 feature 分支
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
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 { ensureWorktree } from '../../src/git/worktree'
|
||||
|
||||
interface Fixture {
|
||||
root: string
|
||||
repo: string
|
||||
templatePath: string
|
||||
}
|
||||
|
||||
const gitEnv: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
GIT_CONFIG_NOSYSTEM: '1',
|
||||
GIT_CONFIG_GLOBAL: '/dev/null',
|
||||
GIT_TERMINAL_PROMPT: '0',
|
||||
GIT_AUTHOR_NAME: 'ensure-test',
|
||||
GIT_AUTHOR_EMAIL: 'ensure@test.local',
|
||||
GIT_COMMITTER_NAME: 'ensure-test',
|
||||
GIT_COMMITTER_EMAIL: 'ensure@test.local',
|
||||
}
|
||||
|
||||
function git(cwd: string, args: string[]): string {
|
||||
return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8', env: gitEnv }).trim()
|
||||
}
|
||||
|
||||
async function setupRepo(): Promise<Fixture> {
|
||||
const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sw-ensure-'))
|
||||
const repo = path.join(root, 'sw-ensure', 'repo')
|
||||
await fsp.mkdir(repo, { recursive: true })
|
||||
git(repo, ['init', '-b', 'main'])
|
||||
git(repo, ['config', 'user.email', 'ensure@test.local'])
|
||||
git(repo, ['config', 'user.name', 'ensure-test'])
|
||||
writeFileSync(path.join(repo, 'README.md'), 'hello\n')
|
||||
git(repo, ['add', 'README.md'])
|
||||
git(repo, ['commit', '-m', 'init'])
|
||||
const templatePath = path.join(root, 'sw-ensure', 'wt', 'feature-x')
|
||||
await fsp.mkdir(path.dirname(templatePath), { recursive: true })
|
||||
return { root, repo, templatePath }
|
||||
}
|
||||
|
||||
async function cleanup(fx: Fixture | undefined): Promise<void> {
|
||||
if (!fx)
|
||||
return
|
||||
try {
|
||||
const out = git(fx.repo, ['worktree', 'list', '--porcelain'])
|
||||
const extras: string[] = []
|
||||
for (const line of out.split('\n')) {
|
||||
if (!line.startsWith('worktree '))
|
||||
continue
|
||||
const p = line.slice('worktree '.length)
|
||||
if (path.resolve(p) !== path.resolve(fx.repo))
|
||||
extras.push(p)
|
||||
}
|
||||
for (const p of extras) {
|
||||
try {
|
||||
git(fx.repo, ['worktree', 'remove', '--force', p])
|
||||
}
|
||||
catch {
|
||||
rmSync(p, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// repo 可能已残,下面整棵 tmp 仍要删
|
||||
}
|
||||
rmSync(fx.root, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
describe('ensureWorktree', () => {
|
||||
let fx: Fixture | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanup(fx)
|
||||
fx = undefined
|
||||
})
|
||||
|
||||
it('本地已有 feature 分支且无 worktree 时挂上该分支并保留 commit', async () => {
|
||||
fx = await setupRepo()
|
||||
git(fx.repo, ['checkout', '-b', 'feature/x'])
|
||||
writeFileSync(path.join(fx.repo, 'README.md'), 'hello\nextra-1\n')
|
||||
git(fx.repo, ['add', 'README.md'])
|
||||
git(fx.repo, ['commit', '-m', 'extra 1'])
|
||||
writeFileSync(path.join(fx.repo, 'README.md'), 'hello\nextra-1\nextra-2\n')
|
||||
git(fx.repo, ['add', 'README.md'])
|
||||
git(fx.repo, ['commit', '-m', 'extra 2'])
|
||||
writeFileSync(path.join(fx.repo, 'README.md'), 'hello\nextra-1\nextra-2\nextra-3\n')
|
||||
git(fx.repo, ['add', 'README.md'])
|
||||
git(fx.repo, ['commit', '-m', 'extra 3'])
|
||||
git(fx.repo, ['checkout', 'main'])
|
||||
const ahead = git(fx.repo, ['log', '--oneline', 'main..feature/x'])
|
||||
expect(ahead.split('\n').filter(Boolean).length).toBe(3)
|
||||
|
||||
const actual = await ensureWorktree({
|
||||
workspaceRoot: fx.repo,
|
||||
worktreePath: fx.templatePath,
|
||||
branch: 'feature/x',
|
||||
})
|
||||
|
||||
expect(path.resolve(actual)).toBe(path.resolve(fx.templatePath))
|
||||
expect(git(actual, ['rev-parse', '--abbrev-ref', 'HEAD'])).toBe('feature/x')
|
||||
expect(git(fx.repo, ['log', '--oneline', 'main..feature/x'])).toBe(ahead)
|
||||
expect(git(actual, ['rev-parse', 'HEAD'])).toBe(git(fx.repo, ['rev-parse', 'feature/x']))
|
||||
})
|
||||
|
||||
it('分支和 worktree 都不存在时创建新分支 worktree', async () => {
|
||||
fx = await setupRepo()
|
||||
|
||||
const actual = await ensureWorktree({
|
||||
workspaceRoot: fx.repo,
|
||||
worktreePath: fx.templatePath,
|
||||
branch: 'feature/fresh',
|
||||
})
|
||||
|
||||
expect(path.resolve(actual)).toBe(path.resolve(fx.templatePath))
|
||||
expect(git(actual, ['rev-parse', '--abbrev-ref', 'HEAD'])).toBe('feature/fresh')
|
||||
expect(git(fx.repo, ['show-ref', '--verify', '--quiet', 'refs/heads/feature/fresh'])).toBe('')
|
||||
})
|
||||
|
||||
it('该 branch 已有 live worktree 时返回已有 path,不建第二个', async () => {
|
||||
fx = await setupRepo()
|
||||
git(fx.repo, ['branch', 'feature/x'])
|
||||
const existing = path.join(fx.root, 'sw-ensure', 'wt', 'already-here')
|
||||
await fsp.mkdir(path.dirname(existing), { recursive: true })
|
||||
git(fx.repo, ['worktree', 'add', existing, 'feature/x'])
|
||||
|
||||
const actual = await ensureWorktree({
|
||||
workspaceRoot: fx.repo,
|
||||
worktreePath: fx.templatePath,
|
||||
branch: 'feature/x',
|
||||
})
|
||||
|
||||
expect(path.resolve(actual)).toBe(path.resolve(existing))
|
||||
expect(existsSync(fx.templatePath)).toBe(false)
|
||||
expect(git(existing, ['rev-parse', '--abbrev-ref', 'HEAD'])).toBe('feature/x')
|
||||
const listed = git(fx.repo, ['worktree', 'list', '--porcelain'])
|
||||
const wtLines = listed.split('\n').filter(l => l.startsWith('worktree '))
|
||||
expect(wtLines).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('模板 path 是垃圾目录且分支不存在时删掉后 add -b 成功', async () => {
|
||||
fx = await setupRepo()
|
||||
await fsp.mkdir(fx.templatePath, { recursive: true })
|
||||
writeFileSync(path.join(fx.templatePath, 'junk.txt'), 'garbage')
|
||||
|
||||
const actual = await ensureWorktree({
|
||||
workspaceRoot: fx.repo,
|
||||
worktreePath: fx.templatePath,
|
||||
branch: 'feature/fresh',
|
||||
})
|
||||
|
||||
expect(path.resolve(actual)).toBe(path.resolve(fx.templatePath))
|
||||
expect(git(actual, ['rev-parse', '--abbrev-ref', 'HEAD'])).toBe('feature/fresh')
|
||||
expect(existsSync(path.join(fx.templatePath, 'junk.txt'))).toBe(false)
|
||||
expect(existsSync(path.join(fx.templatePath, 'README.md'))).toBe(true)
|
||||
})
|
||||
|
||||
it('无 leftover 时可重复调用,第二次返回同一 path', async () => {
|
||||
fx = await setupRepo()
|
||||
|
||||
const first = await ensureWorktree({
|
||||
workspaceRoot: fx.repo,
|
||||
worktreePath: fx.templatePath,
|
||||
branch: 'feature/x',
|
||||
})
|
||||
const second = await ensureWorktree({
|
||||
workspaceRoot: fx.repo,
|
||||
worktreePath: fx.templatePath,
|
||||
branch: 'feature/x',
|
||||
})
|
||||
|
||||
expect(path.resolve(second)).toBe(path.resolve(first))
|
||||
expect(git(second, ['rev-parse', '--abbrev-ref', 'HEAD'])).toBe('feature/x')
|
||||
const wtLines = git(fx.repo, ['worktree', 'list', '--porcelain'])
|
||||
.split('\n')
|
||||
.filter(l => l.startsWith('worktree '))
|
||||
expect(wtLines).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user