🐛 fix(vscode): 同一天再切发布分支自动编号 .2 / .3,不再报已存在

This commit is contained in:
cruldra committed 2026-09-17 17:52:26 +08:00
1 parent b77f2b1b63
commit 936097757b
3 files changed
+76 -42

No files matched your search

+38 -17
View File
@@ -12,6 +12,8 @@ const DEFAULT_PREFIX = 'release/'
export interface ReleaseStatus { export interface ReleaseStatus {
devBranch: string devBranch: string
/** 下一次切发布会得到的分支全名;同一天已切过则带 .2 / .3 序号 */
next: string
/** 当期发布分支全名(含前缀);远端没有任何发布分支时为 undefined */ /** 当期发布分支全名(含前缀);远端没有任何发布分支时为 undefined */
current: string | undefined current: string | undefined
/** 未发布提交数;current 为 undefined 时为 0 */ /** 未发布提交数;current 为 undefined 时为 0 */
@@ -102,16 +104,30 @@ export function normalizeReleasePrefix(raw: string): string {
return t.endsWith('/') ? t : `${t}/` return t.endsWith('/') ? t : `${t}/`
} }
export function todayReleaseBranch(prefix: string, now = new Date()): string { /** 当天首次切不带序号;同一天再切从 .2 起,取已有最大序号加一,删过中间序号也不会倒退 */
export function nextReleaseBranch(prefix: string, remoteBranches: string[], now = new Date()): string {
const y = now.getFullYear() const y = now.getFullYear()
const m = String(now.getMonth() + 1).padStart(2, '0') const m = String(now.getMonth() + 1).padStart(2, '0')
const d = String(now.getDate()).padStart(2, '0') const d = String(now.getDate()).padStart(2, '0')
return `${prefix}${y}-${m}-${d}` const base = `${prefix}${y}-${m}-${d}`
const seqs = remoteBranches.flatMap((b) => {
if (b === base)
return [1]
if (!b.startsWith(`${base}.`))
return []
const n = Number(b.slice(base.length + 1))
return Number.isInteger(n) && n > 1 ? [n] : []
})
return seqs.length === 0 ? base : `${base}.${Math.max(...seqs) + 1}`
}
/** 先按日期再按同日序号;纯字典序会把 .10 排到 .2 前面 */
export function compareReleaseBranches(a: string, b: string): number {
return a.localeCompare(b, 'en', { numeric: true })
} }
/** 发布分支名是 ISO 日期,字典序即时间序,不解析日期 */
export function pickCurrentRelease(prefix: string, remoteBranches: string[]): string | undefined { export function pickCurrentRelease(prefix: string, remoteBranches: string[]): string | undefined {
return remoteBranches.filter(b => b.startsWith(prefix)).sort().at(-1) return remoteBranches.filter(b => b.startsWith(prefix)).sort(compareReleaseBranches).at(-1)
} }
/** Gitea 默认合并说明:Merge pull request '标题' (#260) from … */ /** Gitea 默认合并说明:Merge pull request '标题' (#260) from … */
@@ -168,15 +184,17 @@ async function remoteRefExists(cwd: string, branch: string): Promise<boolean> {
const unreleasedRange = (current: string, devBranch: string): string => `origin/${current}...origin/${devBranch}` const unreleasedRange = (current: string, devBranch: string): string => `origin/${current}...origin/${devBranch}`
export async function getReleaseStatus(o: { cwd: string, devBranch: string, prefix: string }): Promise<ReleaseStatus> { export async function getReleaseStatus(o: { cwd: string, devBranch: string, prefix: string }): Promise<ReleaseStatus> {
const base = { devBranch: o.devBranch, current: undefined, unreleased: 0 } const base = { devBranch: o.devBranch, next: nextReleaseBranch(o.prefix, []), current: undefined, unreleased: 0 }
const fetched = await runGit(o.cwd, ['fetch', 'origin', '--prune']) const fetched = await runGit(o.cwd, ['fetch', 'origin', '--prune'])
if (!fetched.ok) if (!fetched.ok)
return { ...base, unavailable: `拉取远端失败:${tail(fetched.stderr)}` } return { ...base, unavailable: `拉取远端失败:${tail(fetched.stderr)}` }
if (!await remoteRefExists(o.cwd, o.devBranch)) if (!await remoteRefExists(o.cwd, o.devBranch))
return { ...base, unavailable: `远端没有 ${o.devBranch}` } return { ...base, unavailable: `远端没有 ${o.devBranch}` }
const current = pickCurrentRelease(o.prefix, await remoteBranchNames(o.cwd, o.prefix)) const names = await remoteBranchNames(o.cwd, o.prefix)
const next = nextReleaseBranch(o.prefix, names)
const current = pickCurrentRelease(o.prefix, names)
if (!current) if (!current)
return base return { ...base, next }
const shas = await mustGit( const shas = await mustGit(
o.cwd, o.cwd,
['rev-list', '--right-only', '--cherry-pick', '--no-merges', unreleasedRange(current, o.devBranch)], ['rev-list', '--right-only', '--cherry-pick', '--no-merges', unreleasedRange(current, o.devBranch)],
@@ -184,7 +202,7 @@ export async function getReleaseStatus(o: { cwd: string, devBranch: string, pref
) )
const picked = await cherryPickedSources(o.cwd, `origin/${o.devBranch}..origin/${current}`) const picked = await cherryPickedSources(o.cwd, `origin/${o.devBranch}..origin/${current}`)
const unreleased = shas.split('\n').filter(sha => sha && !picked.has(sha)).length const unreleased = shas.split('\n').filter(sha => sha && !picked.has(sha)).length
return { devBranch: o.devBranch, current, unreleased } return { devBranch: o.devBranch, next, current, unreleased }
} }
/** /**
@@ -199,7 +217,7 @@ async function cherryPickedSources(cwd: string, range: string): Promise<Set<stri
/** 新→旧;不 fetch,调用方自己决定何时刷新远端 */ /** 新→旧;不 fetch,调用方自己决定何时刷新远端 */
export async function listReleaseBranches(o: { cwd: string, prefix: string }): Promise<string[]> { export async function listReleaseBranches(o: { cwd: string, prefix: string }): Promise<string[]> {
const names = await remoteBranchNames(o.cwd, o.prefix) const names = await remoteBranchNames(o.cwd, o.prefix)
return names.filter(b => b.startsWith(o.prefix)).sort().reverse() return names.filter(b => b.startsWith(o.prefix)).sort(compareReleaseBranches).reverse()
} }
/** 用本地 merge 记录把未发布 sha 对上 PR。不打 Gitea。 */ /** 用本地 merge 记录把未发布 sha 对上 PR。不打 Gitea。 */
@@ -460,17 +478,20 @@ async function attachMigrationVerdicts(cwd: string, current: string, commits: Un
} }
/** 纯远端切分支;返回该分支相对上一个发布点的提交数(无上一个则为 0) */ /** 纯远端切分支;返回该分支相对上一个发布点的提交数(无上一个则为 0) */
export async function cutRelease(o: { cwd: string, devBranch: string, branch: string }): Promise<number> { export async function cutRelease(o: { cwd: string, devBranch: string, prefix: string, now?: Date }): Promise<{ branch: string, count: number }> {
await mustGit(o.cwd, ['fetch', 'origin', '--prune'], '拉取远端失败') await mustGit(o.cwd, ['fetch', 'origin', '--prune'], '拉取远端失败')
if (await remoteRefExists(o.cwd, o.branch)) const names = await remoteBranchNames(o.cwd, o.prefix)
throw new ReleaseError(`发布分支 ${o.branch} 已存在`) const branch = nextReleaseBranch(o.prefix, names, o.now)
const prefix = o.branch.slice(0, o.branch.lastIndexOf('/') + 1) const prev = pickCurrentRelease(o.prefix, names)
const prev = pickCurrentRelease(prefix, await remoteBranchNames(o.cwd, prefix)) await mustGit(
await mustGit(o.cwd, ['push', 'origin', `origin/${o.devBranch}:refs/heads/${o.branch}`], `推送 ${o.branch} 失败`) o.cwd,
['push', 'origin', `--force-with-lease=refs/heads/${branch}:`, `origin/${o.devBranch}:refs/heads/${branch}`], // ← 空 lease = 远端此刻必须不存在,fetch 之后别人抢先切了也推不上去
`推送 ${branch} 失败`,
)
if (!prev) if (!prev)
return 0 return { branch, count: 0 }
const count = await mustGit(o.cwd, ['rev-list', '--count', `origin/${prev}..origin/${o.devBranch}`], '统计提交数失败') const count = await mustGit(o.cwd, ['rev-list', '--count', `origin/${prev}..origin/${o.devBranch}`], '统计提交数失败')
return Number(count) return { branch, count: Number(count) }
} }
/** 在临时 worktree 里按给定顺序(旧→新)cherry-pick,成功才推远端;无论成败都清理 worktree */ /** 在临时 worktree 里按给定顺序(旧→新)cherry-pick,成功才推远端;无论成败都清理 worktree */
+4 -5
View File
@@ -20,7 +20,7 @@ import {
indexUnreleasedByMergePr, indexUnreleasedByMergePr,
listReleaseBranches, listReleaseBranches,
listUnreleasedCommits, listUnreleasedCommits,
todayReleaseBranch, nextReleaseBranch,
} from '../../git/release' } from '../../git/release'
import { DEFAULT_WORKTREE_TEMPLATE, resolveWorktreeDir } from '../../git/worktreePath' import { DEFAULT_WORKTREE_TEMPLATE, resolveWorktreeDir } from '../../git/worktreePath'
import { triggerJenkinsBuild } from '../../jenkins/trigger' import { triggerJenkinsBuild } from '../../jenkins/trigger'
@@ -35,7 +35,7 @@ export async function handleReleaseStatus(panel: KanbanWebviewPanel): Promise<vo
const base = { const base = {
type: 'release/status' as const, type: 'release/status' as const,
devBranch: s.devBranch, devBranch: s.devBranch,
nextBranch: todayReleaseBranch(s.releaseBranchPrefix), nextBranch: nextReleaseBranch(s.releaseBranchPrefix, []),
running: panel.releaseRunning, running: panel.releaseRunning,
jenkinsConfigured: s.jenkinsJob.length > 0, jenkinsConfigured: s.jenkinsJob.length > 0,
} }
@@ -45,7 +45,7 @@ export async function handleReleaseStatus(panel: KanbanWebviewPanel): Promise<vo
} }
try { try {
const st = await getReleaseStatus({ cwd: root, devBranch: s.devBranch, prefix: s.releaseBranchPrefix }) const st = await getReleaseStatus({ cwd: root, devBranch: s.devBranch, prefix: s.releaseBranchPrefix })
panel.postMessage({ ...base, current: st.current, unreleased: st.unreleased, unavailable: st.unavailable }) panel.postMessage({ ...base, nextBranch: st.next, current: st.current, unreleased: st.unreleased, unavailable: st.unavailable })
} }
catch (err) { catch (err) {
const message = err instanceof Error ? err.message : String(err) const message = err instanceof Error ? err.message : String(err)
@@ -88,8 +88,7 @@ export function handleReleaseCut(panel: KanbanWebviewPanel): Promise<void> {
return runReleaseAction(panel, async () => { return runReleaseAction(panel, async () => {
const root = requireRoot() const root = requireRoot()
const s = getSettings(panel.context) const s = getSettings(panel.context)
const branch = todayReleaseBranch(s.releaseBranchPrefix) const { branch, count } = await cutRelease({ cwd: root, devBranch: s.devBranch, prefix: s.releaseBranchPrefix })
const count = await cutRelease({ cwd: root, devBranch: s.devBranch, branch })
toast(panel, 'success', `已切发布分支 ${branch}(${count} 个提交)`) toast(panel, 'success', `已切发布分支 ${branch}(${count} 个提交)`)
if (!s.jenkinsJob) if (!s.jenkinsJob)
return return
+34 -20
View File
@@ -11,11 +11,11 @@ import {
indexUnreleasedByMergePr, indexUnreleasedByMergePr,
listReleaseBranches, listReleaseBranches,
listUnreleasedCommits, listUnreleasedCommits,
nextReleaseBranch,
normalizeReleasePrefix, normalizeReleasePrefix,
parseAlembicRevision, parseAlembicRevision,
parseGiteaMergeSubject, parseGiteaMergeSubject,
pickCurrentRelease, pickCurrentRelease,
todayReleaseBranch,
} from '../../src/git/release' } 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 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' }
@@ -44,6 +44,8 @@ function migration(cwd: string, file: string, rev: string, down: string | null,
mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true }) mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true })
return commit(cwd, file, `revision = '${rev}'\ndown_revision = ${down === null ? 'None' : `'${down}'`}\n`, msg) return commit(cwd, file, `revision = '${rev}'\ndown_revision = ${down === null ? 'None' : `'${down}'`}\n`, msg)
} }
const AUG01 = new Date(2026, 7, 1)
const AUG31 = new Date(2026, 7, 31)
let fx: Fx | undefined let fx: Fx | undefined
afterEach(() => { afterEach(() => {
if (fx) if (fx)
@@ -57,11 +59,19 @@ describe('纯函数', () => {
expect(normalizeReleasePrefix(' rel ')).toBe('rel/') expect(normalizeReleasePrefix(' rel ')).toBe('rel/')
expect(normalizeReleasePrefix('release/')).toBe('release/') expect(normalizeReleasePrefix('release/')).toBe('release/')
}) })
it('todayReleaseBranch', () => { it('nextReleaseBranch 当天首次不带序号,再切从 .2 起,取最大序号加一', () => {
expect(todayReleaseBranch('release/', new Date(2026, 7, 31))).toBe('release/2026-08-31') expect(nextReleaseBranch('release/', ['main'], AUG31)).toBe('release/2026-08-31')
expect(nextReleaseBranch('release/', ['release/2026-08-30', 'release/2026-08-30.2'], AUG31)).toBe('release/2026-08-31')
expect(nextReleaseBranch('release/', ['release/2026-08-31'], AUG31)).toBe('release/2026-08-31.2')
expect(nextReleaseBranch('release/', ['release/2026-08-31', 'release/2026-08-31.2'], AUG31)).toBe('release/2026-08-31.3')
expect(nextReleaseBranch('release/', ['release/2026-08-31.3'], AUG31)).toBe('release/2026-08-31.4')
expect(nextReleaseBranch('release/', ['release/2026-08-31.x'], AUG31)).toBe('release/2026-08-31')
}) })
it('pickCurrentRelease 取前缀内字典序最大', () => { it('pickCurrentRelease 先按日期再按同日序号,.10 排在 .9 后', () => {
expect(pickCurrentRelease('release/', ['main', 'release/2026-08-01', 'release/2026-08-31', 'hotfix/2027'])).toBe('release/2026-08-31') expect(pickCurrentRelease('release/', ['main', 'release/2026-08-01', 'release/2026-08-31', 'hotfix/2027'])).toBe('release/2026-08-31')
expect(pickCurrentRelease('release/', ['release/2026-08-31.2', 'release/2026-08-31'])).toBe('release/2026-08-31.2')
expect(pickCurrentRelease('release/', ['release/2026-08-31.10', 'release/2026-08-31.9'])).toBe('release/2026-08-31.10')
expect(pickCurrentRelease('release/', ['release/2026-08-31.3', 'release/2026-09-01'])).toBe('release/2026-09-01')
expect(pickCurrentRelease('release/', ['main'])).toBeUndefined() expect(pickCurrentRelease('release/', ['main'])).toBeUndefined()
}) })
it('parseGiteaMergeSubject 认 Gitea 合并说明', () => { it('parseGiteaMergeSubject 认 Gitea 合并说明', () => {
@@ -98,17 +108,20 @@ describe('集成', () => {
expect(s).toMatchObject({ devBranch: 'main', current: undefined, unreleased: 0 }) expect(s).toMatchObject({ devBranch: 'main', current: undefined, unreleased: 0 })
expect(s.unavailable).toBeUndefined() expect(s.unavailable).toBeUndefined()
}) })
it('cutRelease 纯远端切分支,不建本地分支', async () => { it('cutRelease 纯远端切分支,不建本地分支;同一天再切得到 .2', async () => {
fx = await setup() fx = await setup()
const { repo, origin } = fx const { repo, origin } = fx
await cutRelease({ cwd: repo, devBranch: 'main', branch: 'release/2026-08-31' }) expect(await cutRelease({ cwd: repo, devBranch: 'main', prefix: 'release/', now: AUG31 })).toEqual({ branch: 'release/2026-08-31', count: 0 })
expect(git(origin, ['rev-parse', 'release/2026-08-31'])).toBe(git(origin, ['rev-parse', 'main'])) 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() 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(/已存在/) expect(await cutRelease({ cwd: repo, devBranch: 'main', prefix: 'release/', now: AUG31 })).toEqual({ branch: 'release/2026-08-31.2', count: 0 })
expect(git(origin, ['rev-parse', 'release/2026-08-31.2'])).toBe(git(origin, ['rev-parse', 'main']))
const s = await getReleaseStatus({ cwd: repo, devBranch: 'main', prefix: 'release/' })
expect(s.current).toBe('release/2026-08-31.2')
}) })
it('unreleased 计数忽略已 cherry-pick 的提交;cherryPickToRelease 推远端并清理 worktree', async () => { it('unreleased 计数忽略已 cherry-pick 的提交;cherryPickToRelease 推远端并清理 worktree', async () => {
fx = await setup() fx = await setup()
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' }) await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
const fix = commit(fx.peer, 'b.txt', 'b', 'fix b') const fix = commit(fx.peer, 'b.txt', 'b', 'fix b')
commit(fx.peer, 'c.txt', 'c', 'feat c') commit(fx.peer, 'c.txt', 'c', 'feat c')
git(fx.peer, ['push', 'origin', 'main']) git(fx.peer, ['push', 'origin', 'main'])
@@ -129,7 +142,7 @@ describe('集成', () => {
}) })
it('cherry-pick 冲突 → 返回冲突现场、worktree 留在中间态、远端不变;没处理完拒绝再摘,处理完复用同一 worktree', async () => { it('cherry-pick 冲突 → 返回冲突现场、worktree 留在中间态、远端不变;没处理完拒绝再摘,处理完复用同一 worktree', async () => {
fx = await setup() fx = await setup()
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' }) await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
commit(fx.peer, 'a.txt', 'a2', 'change a') commit(fx.peer, 'a.txt', 'a2', 'change a')
const conflict = commit(fx.peer, 'a.txt', 'a3', 'change a again') const conflict = commit(fx.peer, 'a.txt', 'a3', 'change a again')
const after = commit(fx.peer, 'd.txt', 'd', 'feat d') const after = commit(fx.peer, 'd.txt', 'd', 'feat d')
@@ -159,22 +172,23 @@ describe('集成', () => {
}) })
it('发布 worktree 目录解析到工作区本身 → 拒绝', async () => { it('发布 worktree 目录解析到工作区本身 → 拒绝', async () => {
fx = await setup() fx = await setup()
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' }) await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
const fix = commit(fx.peer, 'b.txt', 'b', 'fix b') const fix = commit(fx.peer, 'b.txt', 'b', 'fix b')
git(fx.peer, ['push', 'origin', 'main']) git(fx.peer, ['push', 'origin', 'main'])
await expect(cherryPickToRelease({ cwd: fx.repo, branch: 'release/2026-08-31', shas: [fix], worktreePath: fx.repo })).rejects.toThrow(/工作区本身/) await expect(cherryPickToRelease({ cwd: fx.repo, branch: 'release/2026-08-31', shas: [fix], worktreePath: fx.repo })).rejects.toThrow(/工作区本身/)
expect(git(fx.repo, ['status', '--porcelain'])).toBe('') expect(git(fx.repo, ['status', '--porcelain'])).toBe('')
}) })
it('listReleaseBranches 新→旧', async () => { it('listReleaseBranches 新→旧,同日序号大的在前', async () => {
fx = await setup() fx = await setup()
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-01' }) await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG01 })
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' }) await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
git(fx.repo, ['fetch', 'origin']) git(fx.repo, ['fetch', 'origin'])
expect(await listReleaseBranches({ cwd: fx.repo, prefix: 'release/' })).toEqual(['release/2026-08-31', 'release/2026-08-01']) expect(await listReleaseBranches({ cwd: fx.repo, prefix: 'release/' })).toEqual(['release/2026-08-31.2', 'release/2026-08-31', 'release/2026-08-01'])
}) })
it('indexUnreleasedByMergePr 用本地合并记录把分支提交对上 PR', async () => { it('indexUnreleasedByMergePr 用本地合并记录把分支提交对上 PR', async () => {
fx = await setup() fx = await setup()
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' }) await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
git(fx.peer, ['checkout', '-b', 'feature']) git(fx.peer, ['checkout', '-b', 'feature'])
const a = commit(fx.peer, 'b.txt', 'b', 'fix(admin): 会员档位隐藏') const a = commit(fx.peer, 'b.txt', 'b', 'fix(admin): 会员档位隐藏')
const b = commit(fx.peer, 'c.txt', 'c', 'fix(membership): 年付月度发卡') const b = commit(fx.peer, 'c.txt', 'c', 'fix(membership): 年付月度发卡')
@@ -193,7 +207,7 @@ describe('集成', () => {
}) })
it('发布分支把同一改动拆成两条,即使标题不同也不再列出', async () => { it('发布分支把同一改动拆成两条,即使标题不同也不再列出', async () => {
fx = await setup() fx = await setup()
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' }) await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
writeFileSync(path.join(fx.peer, 'a.txt'), 'A\n') writeFileSync(path.join(fx.peer, 'a.txt'), 'A\n')
writeFileSync(path.join(fx.peer, 'b.txt'), 'B\n') writeFileSync(path.join(fx.peer, 'b.txt'), 'B\n')
git(fx.peer, ['add', 'a.txt', 'b.txt']) git(fx.peer, ['add', 'a.txt', 'b.txt'])
@@ -215,7 +229,7 @@ describe('集成', () => {
}) })
it('解过冲突的复制品内容不同,但带 -x 尾注就算已摘,角标计数同步扣掉', async () => { it('解过冲突的复制品内容不同,但带 -x 尾注就算已摘,角标计数同步扣掉', async () => {
fx = await setup() fx = await setup()
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' }) await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
const fix = commit(fx.peer, 'a.txt', 'a2', 'fix a') const fix = commit(fx.peer, 'a.txt', 'a2', 'fix a')
commit(fx.peer, 'c.txt', 'c', 'feat: other') commit(fx.peer, 'c.txt', 'c', 'feat: other')
git(fx.peer, ['push', 'origin', 'main']) git(fx.peer, ['push', 'origin', 'main'])
@@ -233,7 +247,7 @@ describe('集成', () => {
}) })
it('标题相同但改的文件不同,仍算未发布', async () => { it('标题相同但改的文件不同,仍算未发布', async () => {
fx = await setup() fx = await setup()
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' }) await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
git(fx.peer, ['fetch']) git(fx.peer, ['fetch'])
git(fx.peer, ['checkout', '-b', 'release/2026-08-31', 'origin/release/2026-08-31']) git(fx.peer, ['checkout', '-b', 'release/2026-08-31', 'origin/release/2026-08-31'])
writeFileSync(path.join(fx.peer, 'a.txt'), 'release-a\n') writeFileSync(path.join(fx.peer, 'a.txt'), 'release-a\n')
@@ -253,7 +267,7 @@ describe('集成', () => {
fx = await setup() fx = await setup()
migration(fx.peer, 'alembic/versions/0001_init.py', '0001', null, 'feat: init schema') migration(fx.peer, 'alembic/versions/0001_init.py', '0001', null, 'feat: init schema')
git(fx.peer, ['push', 'origin', 'main']) git(fx.peer, ['push', 'origin', 'main'])
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' }) await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
migration(fx.peer, 'alembic/versions/0002_x.py', '0002', '0001', 'feat: m2') migration(fx.peer, 'alembic/versions/0002_x.py', '0002', '0001', 'feat: m2')
migration(fx.peer, 'alembic/versions/0003_y.py', '0003', '0002', 'feat: m3') migration(fx.peer, 'alembic/versions/0003_y.py', '0003', '0002', 'feat: m3')
commit(fx.peer, 'b.txt', 'b', 'fix: plain') commit(fx.peer, 'b.txt', 'b', 'fix: plain')
@@ -270,7 +284,7 @@ describe('集成', () => {
fx = await setup() fx = await setup()
migration(fx.peer, 'alembic/versions/0001_init.py', '0001', null, 'feat: init schema') migration(fx.peer, 'alembic/versions/0001_init.py', '0001', null, 'feat: init schema')
git(fx.peer, ['push', 'origin', 'main']) git(fx.peer, ['push', 'origin', 'main'])
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' }) await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
migration(fx.peer, 'alembic/versions/0002_x.py', '0002', '0001', 'feat: m2') migration(fx.peer, 'alembic/versions/0002_x.py', '0002', '0001', 'feat: m2')
git(fx.peer, ['push', 'origin', 'main']) git(fx.peer, ['push', 'origin', 'main'])
git(fx.peer, ['fetch']) git(fx.peer, ['fetch'])