✨ feat(vscode): 同一天再切发布直接把 main 新提交推进到当天分支,不再另起编号分支

This commit is contained in:
cruldra committed 2026-09-17 17:57:47 +08:00
1 parent 4aae4df1f1
commit 17570749dd
4 files changed
+128 -94

No files matched your search

+39 -39
View File
@@ -12,8 +12,6 @@ const DEFAULT_PREFIX = 'release/'
export interface ReleaseStatus {
devBranch: string
/** 下一次切发布会得到的分支全名;同一天已切过则带 .2 / .3 序号 */
next: string
/** 当期发布分支全名(含前缀);远端没有任何发布分支时为 undefined */
current: string | undefined
/** 未发布提交数;current 为 undefined 时为 0 */
@@ -104,30 +102,16 @@ export function normalizeReleasePrefix(raw: string): string {
return t.endsWith('/') ? t : `${t}/`
}
/** 当天首次切不带序号;同一天再切从 .2 起,取已有最大序号加一,删过中间序号也不会倒退 */
export function nextReleaseBranch(prefix: string, remoteBranches: string[], now = new Date()): string {
export function todayReleaseBranch(prefix: string, now = new Date()): string {
const y = now.getFullYear()
const m = String(now.getMonth() + 1).padStart(2, '0')
const d = String(now.getDate()).padStart(2, '0')
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 })
return `${prefix}${y}-${m}-${d}`
}
/** 发布分支名是 ISO 日期,字典序即时间序,不解析日期 */
export function pickCurrentRelease(prefix: string, remoteBranches: string[]): string | undefined {
return remoteBranches.filter(b => b.startsWith(prefix)).sort(compareReleaseBranches).at(-1)
return remoteBranches.filter(b => b.startsWith(prefix)).sort().at(-1)
}
/** Gitea 默认合并说明:Merge pull request '标题' (#260) from … */
@@ -184,17 +168,15 @@ async function remoteRefExists(cwd: string, branch: string): Promise<boolean> {
const unreleasedRange = (current: string, devBranch: string): string => `origin/${current}...origin/${devBranch}`
export async function getReleaseStatus(o: { cwd: string, devBranch: string, prefix: string }): Promise<ReleaseStatus> {
const base = { devBranch: o.devBranch, next: nextReleaseBranch(o.prefix, []), current: undefined, unreleased: 0 }
const base = { devBranch: o.devBranch, current: undefined, unreleased: 0 }
const fetched = await runGit(o.cwd, ['fetch', 'origin', '--prune'])
if (!fetched.ok)
return { ...base, unavailable: `拉取远端失败:${tail(fetched.stderr)}` }
if (!await remoteRefExists(o.cwd, o.devBranch))
return { ...base, unavailable: `远端没有 ${o.devBranch}` }
const names = await remoteBranchNames(o.cwd, o.prefix)
const next = nextReleaseBranch(o.prefix, names)
const current = pickCurrentRelease(o.prefix, names)
const current = pickCurrentRelease(o.prefix, await remoteBranchNames(o.cwd, o.prefix))
if (!current)
return { ...base, next }
return base
const shas = await mustGit(
o.cwd,
['rev-list', '--right-only', '--cherry-pick', '--no-merges', unreleasedRange(current, o.devBranch)],
@@ -202,7 +184,7 @@ export async function getReleaseStatus(o: { cwd: string, devBranch: string, pref
)
const picked = await cherryPickedSources(o.cwd, `origin/${o.devBranch}..origin/${current}`)
const unreleased = shas.split('\n').filter(sha => sha && !picked.has(sha)).length
return { devBranch: o.devBranch, next, current, unreleased }
return { devBranch: o.devBranch, current, unreleased }
}
/**
@@ -217,7 +199,7 @@ async function cherryPickedSources(cwd: string, range: string): Promise<Set<stri
/** 新→旧;不 fetch,调用方自己决定何时刷新远端 */
export async function listReleaseBranches(o: { cwd: string, prefix: string }): Promise<string[]> {
const names = await remoteBranchNames(o.cwd, o.prefix)
return names.filter(b => b.startsWith(o.prefix)).sort(compareReleaseBranches).reverse()
return names.filter(b => b.startsWith(o.prefix)).sort().reverse()
}
/** 用本地 merge 记录把未发布 sha 对上 PR。不打 Gitea。 */
@@ -478,20 +460,38 @@ async function attachMigrationVerdicts(cwd: string, current: string, commits: Un
}
/** 纯远端切分支;返回该分支相对上一个发布点的提交数(无上一个则为 0) */
export async function cutRelease(o: { cwd: string, devBranch: string, prefix: string, now?: Date }): Promise<{ branch: string, count: number }> {
export type CutOutcome
= | { kind: 'created', branch: string, count: number }
| { kind: 'advanced', branch: string, count: number }
| { kind: 'diverged', branch: string }
/**
* 切发布 = 让今天的发布分支等于 origin/devBranch 此刻的内容:
* ① 今天还没切过 → 从 origin/devBranch 新建
* ② 切过,分支上没别的提交 → 快进到 origin/devBranch,SHA 原样保留
* ③ 切过,分支上已有单独摘的提交 → 快进不了,交给调用方逐个摘
*/
export async function cutRelease(o: { cwd: string, devBranch: string, prefix: string, now?: Date }): Promise<CutOutcome> {
await mustGit(o.cwd, ['fetch', 'origin', '--prune'], '拉取远端失败')
const branch = todayReleaseBranch(o.prefix, o.now)
const names = await remoteBranchNames(o.cwd, o.prefix)
const branch = nextReleaseBranch(o.prefix, names, o.now)
const prev = pickCurrentRelease(o.prefix, names)
await mustGit(
o.cwd,
['push', 'origin', `--force-with-lease=refs/heads/${branch}:`, `origin/${o.devBranch}:refs/heads/${branch}`], // ← 空 lease = 远端此刻必须不存在,fetch 之后别人抢先切了也推不上去
`推送 ${branch} 失败`,
)
if (!prev)
return { branch, count: 0 }
const count = await mustGit(o.cwd, ['rev-list', '--count', `origin/${prev}..origin/${o.devBranch}`], '统计提交数失败')
return { branch, count: Number(count) }
if (!names.includes(branch)) { // ①
const prev = pickCurrentRelease(o.prefix, names)
await mustGit(
o.cwd,
['push', 'origin', `--force-with-lease=refs/heads/${branch}:`, `origin/${o.devBranch}:refs/heads/${branch}`], // ← 空 lease = 远端此刻必须不存在,fetch 之后别人抢先切了也推不上去
`推送 ${branch} 失败`,
)
const count = prev ? Number(await mustGit(o.cwd, ['rev-list', '--count', `origin/${prev}..origin/${o.devBranch}`], '统计提交数失败')) : 0
return { kind: 'created', branch, count }
}
const linear = await runGit(o.cwd, ['merge-base', '--is-ancestor', `origin/${branch}`, `origin/${o.devBranch}`])
if (!linear.ok) // ③
return { kind: 'diverged', branch }
const count = Number(await mustGit(o.cwd, ['rev-list', '--count', `origin/${branch}..origin/${o.devBranch}`], '统计提交数失败'))
if (count > 0) // ② 不带 force:fetch 之后远端被人推过,git 自己会拒掉非快进
await mustGit(o.cwd, ['push', 'origin', `origin/${o.devBranch}:refs/heads/${branch}`], `推送 ${branch} 失败`)
return { kind: 'advanced', branch, count }
}
/** 在临时 worktree 里按给定顺序(旧→新)cherry-pick,成功才推远端;无论成败都清理 worktree */