✨ feat(vscode): 发布分支 git 逻辑(切分支 / 未发布计数 / cherry-pick)
Claude-Session: https://claude.ai/code/session_01HTMgwaE3woqM5yvsHiuSAa
This commit is contained in:
1 parent
e10fe93bc6
commit
fa32d4a88b
2 files changed
+269
No files matched your search
@@ -0,0 +1,110 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { 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 {
|
||||
cherryPickToRelease,
|
||||
cutRelease,
|
||||
getReleaseStatus,
|
||||
listReleaseBranches,
|
||||
listUnreleasedCommits,
|
||||
normalizeReleasePrefix,
|
||||
pickCurrentRelease,
|
||||
todayReleaseBranch,
|
||||
} from '../../src/git/release'
|
||||
|
||||
const gitEnv = { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null', GIT_TERMINAL_PROMPT: '0', GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t' }
|
||||
const git = (cwd: string, args: string[]) => execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8', env: gitEnv }).trim()
|
||||
|
||||
interface Fx { root: string, origin: string, repo: string, peer: string }
|
||||
async function setup(): Promise<Fx> {
|
||||
const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sw-release-'))
|
||||
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])
|
||||
commit(peer, 'a.txt', 'a', 'init')
|
||||
git(peer, ['push', '-u', 'origin', 'main'])
|
||||
const repo = path.join(root, 'repo')
|
||||
git(root, ['clone', origin, repo])
|
||||
return { root, origin, repo, peer }
|
||||
}
|
||||
function commit(cwd: string, file: string, content: string, msg: string): string {
|
||||
writeFileSync(path.join(cwd, file), `${content}\n`)
|
||||
git(cwd, ['add', file])
|
||||
git(cwd, ['commit', '-m', msg])
|
||||
return git(cwd, ['rev-parse', 'HEAD'])
|
||||
}
|
||||
let fx: Fx | undefined
|
||||
afterEach(() => {
|
||||
if (fx)
|
||||
rmSync(fx.root, { recursive: true, force: true })
|
||||
fx = undefined
|
||||
})
|
||||
|
||||
describe('纯函数', () => {
|
||||
it('normalizeReleasePrefix', () => {
|
||||
expect(normalizeReleasePrefix('')).toBe('release/')
|
||||
expect(normalizeReleasePrefix(' rel ')).toBe('rel/')
|
||||
expect(normalizeReleasePrefix('release/')).toBe('release/')
|
||||
})
|
||||
it('todayReleaseBranch', () => {
|
||||
expect(todayReleaseBranch('release/', new Date(2026, 7, 31))).toBe('release/2026-08-31')
|
||||
})
|
||||
it('pickCurrentRelease 取前缀内字典序最大', () => {
|
||||
expect(pickCurrentRelease('release/', ['main', 'release/2026-08-01', 'release/2026-08-31', 'hotfix/2027'])).toBe('release/2026-08-31')
|
||||
expect(pickCurrentRelease('release/', ['main'])).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('集成', () => {
|
||||
it('无发布分支 → current undefined,unreleased 0', async () => {
|
||||
fx = await setup()
|
||||
const s = await getReleaseStatus({ cwd: fx.repo, devBranch: 'main', prefix: 'release/' })
|
||||
expect(s).toMatchObject({ devBranch: 'main', current: undefined, unreleased: 0 })
|
||||
expect(s.unavailable).toBeUndefined()
|
||||
})
|
||||
it('cutRelease 纯远端切分支,不建本地分支', async () => {
|
||||
fx = await setup()
|
||||
const { repo, origin } = fx
|
||||
await cutRelease({ cwd: repo, devBranch: 'main', branch: 'release/2026-08-31' })
|
||||
expect(git(origin, ['rev-parse', 'release/2026-08-31'])).toBe(git(origin, ['rev-parse', 'main']))
|
||||
expect(() => git(repo, ['rev-parse', '--verify', 'refs/heads/release/2026-08-31'])).toThrow()
|
||||
await expect(cutRelease({ cwd: repo, devBranch: 'main', branch: 'release/2026-08-31' })).rejects.toThrow(/已存在/)
|
||||
})
|
||||
it('unreleased 计数忽略已 cherry-pick 的提交;cherryPickToRelease 推远端并清理 worktree', async () => {
|
||||
fx = await setup()
|
||||
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' })
|
||||
const fix = commit(fx.peer, 'b.txt', 'b', 'fix b')
|
||||
commit(fx.peer, 'c.txt', 'c', 'feat c')
|
||||
git(fx.peer, ['push', 'origin', 'main'])
|
||||
let s = await getReleaseStatus({ cwd: fx.repo, devBranch: 'main', prefix: 'release/' })
|
||||
expect(s).toMatchObject({ current: 'release/2026-08-31', unreleased: 2 })
|
||||
const list = await listUnreleasedCommits({ cwd: fx.repo, devBranch: 'main', current: 'release/2026-08-31' })
|
||||
expect(list.map(c => c.subject)).toEqual(['feat c', 'fix b'])
|
||||
await cherryPickToRelease({ cwd: fx.repo, branch: 'release/2026-08-31', shas: [fix] })
|
||||
expect(git(fx.origin, ['log', '--format=%s', 'release/2026-08-31'])).toBe('fix b\ninit')
|
||||
expect(git(fx.repo, ['worktree', 'list']).split('\n')).toHaveLength(1)
|
||||
s = await getReleaseStatus({ cwd: fx.repo, devBranch: 'main', prefix: 'release/' })
|
||||
expect(s.unreleased).toBe(1)
|
||||
})
|
||||
it('cherry-pick 冲突 → 抛错、远端不变、worktree 已清理', async () => {
|
||||
fx = await setup()
|
||||
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' })
|
||||
commit(fx.peer, 'a.txt', 'a2', 'change a')
|
||||
const conflict = commit(fx.peer, 'a.txt', 'a3', 'change a again')
|
||||
git(fx.peer, ['push', 'origin', 'main'])
|
||||
const before = git(fx.origin, ['rev-parse', 'release/2026-08-31'])
|
||||
await expect(cherryPickToRelease({ cwd: fx.repo, branch: 'release/2026-08-31', shas: [conflict] })).rejects.toThrow(/冲突/)
|
||||
expect(git(fx.origin, ['rev-parse', 'release/2026-08-31'])).toBe(before)
|
||||
expect(git(fx.repo, ['worktree', 'list']).split('\n')).toHaveLength(1)
|
||||
})
|
||||
it('listReleaseBranches 新→旧', async () => {
|
||||
fx = await setup()
|
||||
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-01' })
|
||||
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' })
|
||||
git(fx.repo, ['fetch', 'origin'])
|
||||
expect(await listReleaseBranches({ cwd: fx.repo, prefix: 'release/' })).toEqual(['release/2026-08-31', 'release/2026-08-01'])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user