✨ feat(vscode): 同一天再切发布直接把 main 新提交推进到当天分支,不再另起编号分支
This commit is contained in:
1 parent
4aae4df1f1
commit
17570749dd
4 files changed
+128
-94
No files matched your search
+39
-39
@@ -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 */
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
indexUnreleasedByMergePr,
|
||||
listReleaseBranches,
|
||||
listUnreleasedCommits,
|
||||
nextReleaseBranch,
|
||||
todayReleaseBranch,
|
||||
} from '../../git/release'
|
||||
import { DEFAULT_WORKTREE_TEMPLATE, resolveWorktreeDir } from '../../git/worktreePath'
|
||||
import { triggerJenkinsBuild } from '../../jenkins/trigger'
|
||||
@@ -35,7 +35,7 @@ export async function handleReleaseStatus(panel: KanbanWebviewPanel): Promise<vo
|
||||
const base = {
|
||||
type: 'release/status' as const,
|
||||
devBranch: s.devBranch,
|
||||
nextBranch: nextReleaseBranch(s.releaseBranchPrefix, []),
|
||||
nextBranch: todayReleaseBranch(s.releaseBranchPrefix),
|
||||
running: panel.releaseRunning,
|
||||
jenkinsConfigured: s.jenkinsJob.length > 0,
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export async function handleReleaseStatus(panel: KanbanWebviewPanel): Promise<vo
|
||||
}
|
||||
try {
|
||||
const st = await getReleaseStatus({ cwd: root, devBranch: s.devBranch, prefix: s.releaseBranchPrefix })
|
||||
panel.postMessage({ ...base, nextBranch: st.next, current: st.current, unreleased: st.unreleased, unavailable: st.unavailable })
|
||||
panel.postMessage({ ...base, current: st.current, unreleased: st.unreleased, unavailable: st.unavailable })
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
@@ -88,13 +88,26 @@ export function handleReleaseCut(panel: KanbanWebviewPanel): Promise<void> {
|
||||
return runReleaseAction(panel, async () => {
|
||||
const root = requireRoot()
|
||||
const s = getSettings(panel.context)
|
||||
const { branch, count } = await cutRelease({ cwd: root, devBranch: s.devBranch, prefix: s.releaseBranchPrefix })
|
||||
toast(panel, 'success', `已切发布分支 ${branch}(${count} 个提交)`)
|
||||
const r = await cutRelease({ cwd: root, devBranch: s.devBranch, prefix: s.releaseBranchPrefix })
|
||||
if (r.kind === 'diverged') {
|
||||
const picked = await pickUnreleased(panel, { root, devBranch: s.devBranch, current: r.branch })
|
||||
if (picked !== 'done')
|
||||
return
|
||||
}
|
||||
else if (r.kind === 'advanced' && r.count === 0) {
|
||||
toast(panel, 'info', `${r.branch} 已经和 ${s.devBranch} 一致,没有新提交`)
|
||||
return
|
||||
}
|
||||
else {
|
||||
toast(panel, 'success', r.kind === 'created'
|
||||
? `已切发布分支 ${r.branch}(${r.count} 个提交)`
|
||||
: `已把 ${s.devBranch} 上 ${r.count} 个新提交推进到 ${r.branch}`)
|
||||
}
|
||||
if (!s.jenkinsJob)
|
||||
return
|
||||
const pick = await window.showInformationMessage(`现在部署 ${branch} 到${s.jenkinsProdHost}?`, '部署')
|
||||
const pick = await window.showInformationMessage(`现在部署 ${r.branch} 到${s.jenkinsProdHost}?`, '部署')
|
||||
if (pick === '部署')
|
||||
await deploy(panel, branch)
|
||||
await deploy(panel, r.branch)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -140,35 +153,48 @@ export function handleReleaseCherryPickConfirm(panel: KanbanWebviewPanel, shas:
|
||||
const status = await getReleaseStatus({ cwd: root, devBranch: s.devBranch, prefix: s.releaseBranchPrefix })
|
||||
if (status.unavailable)
|
||||
throw new Error(status.unavailable)
|
||||
const current = status.current
|
||||
if (!current)
|
||||
if (!status.current)
|
||||
throw new Error('尚无发布分支,先切新发布')
|
||||
const commits = await listUnreleasedCommits({ cwd: root, devBranch: s.devBranch, current })
|
||||
const wanted = new Set(shas)
|
||||
const blocked = commits.find(c => wanted.has(c.sha) && c.migration?.blocked)
|
||||
if (blocked)
|
||||
throw new Error(`${blocked.sha.slice(0, 7)} ${blocked.subject}:${blocked.migration!.blocked}`)
|
||||
const order = new Map(commits.map((c, i) => [c.sha, i]))
|
||||
const ordered = [...shas].sort((a, b) => (order.get(b) ?? 0) - (order.get(a) ?? 0))
|
||||
// 发布 worktree 走和实施 worktree 同一套目录模板,分支名里的 / 换成 - 当目录名。
|
||||
const worktreePath = resolveWorktreeDir(s.worktreeDirectory || DEFAULT_WORKTREE_TEMPLATE, root, current.replace(/\//g, '-'))
|
||||
const spinner = toast(panel, 'info', `正在摘 ${ordered.length} 个提交到 ${current}…`, { spinner: true, dismissOnTimer: 0 })
|
||||
let conflict: CherryPickConflict | undefined
|
||||
try {
|
||||
conflict = await cherryPickToRelease({ cwd: root, branch: current, shas: ordered, worktreePath })
|
||||
}
|
||||
finally {
|
||||
dismiss(panel, spinner)
|
||||
}
|
||||
if (!conflict) {
|
||||
toast(panel, 'success', `已摘 ${ordered.length} 个提交到 ${current}`)
|
||||
return
|
||||
}
|
||||
openReleaseConflictSession(panel, { branch: current, conflict, subjectOf: new Map(commits.map(c => [c.sha, c.subject])) })
|
||||
toast(panel, 'info', `${conflict.sha.slice(0, 7)} ${conflict.subject} 冲突,已开 ${current} 冲突解决会话接手`, { dismissOnTimer: 10_000 })
|
||||
await pickUnreleased(panel, { root, devBranch: s.devBranch, current: status.current, shas })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 devBranch 上未发布的提交摘到 current,shas 不传就全摘。
|
||||
* 冲突时开会话接手并返回 'conflict',调用方据此决定后续(比如要不要问部署)。
|
||||
*/
|
||||
async function pickUnreleased(panel: KanbanWebviewPanel, o: { root: string, devBranch: string, current: string, shas?: string[] }): Promise<'done' | 'conflict' | 'nothing'> {
|
||||
const s = getSettings(panel.context)
|
||||
const commits = await listUnreleasedCommits({ cwd: o.root, devBranch: o.devBranch, current: o.current })
|
||||
const wanted = o.shas && new Set(o.shas)
|
||||
const chosen = wanted ? commits.filter(c => wanted.has(c.sha)) : commits
|
||||
if (chosen.length === 0) {
|
||||
toast(panel, 'info', '没有待摘的提交')
|
||||
return 'nothing'
|
||||
}
|
||||
const blocked = chosen.find(c => c.migration?.blocked)
|
||||
if (blocked)
|
||||
throw new Error(`${blocked.sha.slice(0, 7)} ${blocked.subject}:${blocked.migration!.blocked}`)
|
||||
const ordered = chosen.map(c => c.sha).reverse() // ← 列表是新→旧,摘要旧→新
|
||||
// 发布 worktree 走和实施 worktree 同一套目录模板,分支名里的 / 换成 - 当目录名。
|
||||
const worktreePath = resolveWorktreeDir(s.worktreeDirectory || DEFAULT_WORKTREE_TEMPLATE, o.root, o.current.replace(/\//g, '-'))
|
||||
const spinner = toast(panel, 'info', `正在摘 ${ordered.length} 个提交到 ${o.current}…`, { spinner: true, dismissOnTimer: 0 })
|
||||
let conflict: CherryPickConflict | undefined
|
||||
try {
|
||||
conflict = await cherryPickToRelease({ cwd: o.root, branch: o.current, shas: ordered, worktreePath })
|
||||
}
|
||||
finally {
|
||||
dismiss(panel, spinner)
|
||||
}
|
||||
if (!conflict) {
|
||||
toast(panel, 'success', `已摘 ${ordered.length} 个提交到 ${o.current}`)
|
||||
return 'done'
|
||||
}
|
||||
openReleaseConflictSession(panel, { branch: o.current, conflict, subjectOf: new Map(commits.map(c => [c.sha, c.subject])) })
|
||||
toast(panel, 'info', `${conflict.sha.slice(0, 7)} ${conflict.subject} 冲突,已开 ${o.current} 冲突解决会话接手`, { dismissOnTimer: 10_000 })
|
||||
return 'conflict'
|
||||
}
|
||||
|
||||
/**
|
||||
* 摘修复冲突后的接力:worktree 里 cherry-pick 停在中间态,开一个 cc 会话进去解决、
|
||||
* 按原顺序摘完剩余提交并推远端。同名终端还活着就把提示词直接打进去,不再新开。
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
indexUnreleasedByMergePr,
|
||||
listReleaseBranches,
|
||||
listUnreleasedCommits,
|
||||
nextReleaseBranch,
|
||||
normalizeReleasePrefix,
|
||||
parseAlembicRevision,
|
||||
parseGiteaMergeSubject,
|
||||
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' }
|
||||
@@ -59,19 +59,11 @@ describe('纯函数', () => {
|
||||
expect(normalizeReleasePrefix(' rel ')).toBe('rel/')
|
||||
expect(normalizeReleasePrefix('release/')).toBe('release/')
|
||||
})
|
||||
it('nextReleaseBranch 当天首次不带序号,再切从 .2 起,取最大序号加一', () => {
|
||||
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('todayReleaseBranch', () => {
|
||||
expect(todayReleaseBranch('release/', AUG31)).toBe('release/2026-08-31')
|
||||
})
|
||||
it('pickCurrentRelease 先按日期再按同日序号,.10 排在 .9 后', () => {
|
||||
it('pickCurrentRelease 取前缀内字典序最大', () => {
|
||||
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()
|
||||
})
|
||||
it('parseGiteaMergeSubject 认 Gitea 合并说明', () => {
|
||||
@@ -108,16 +100,33 @@ describe('集成', () => {
|
||||
expect(s).toMatchObject({ devBranch: 'main', current: undefined, unreleased: 0 })
|
||||
expect(s.unavailable).toBeUndefined()
|
||||
})
|
||||
it('cutRelease 纯远端切分支,不建本地分支;同一天再切得到 .2', async () => {
|
||||
it('cutRelease 纯远端切分支,不建本地分支;同一天再切 = 快进到 main,不另起分支', async () => {
|
||||
fx = await setup()
|
||||
const { repo, origin } = fx
|
||||
expect(await cutRelease({ cwd: repo, devBranch: 'main', prefix: 'release/', now: AUG31 })).toEqual({ branch: 'release/2026-08-31', count: 0 })
|
||||
const { repo, origin, peer } = fx
|
||||
expect(await cutRelease({ cwd: repo, devBranch: 'main', prefix: 'release/', now: AUG31 })).toEqual({ kind: 'created', branch: 'release/2026-08-31', count: 0 })
|
||||
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(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')
|
||||
expect(await cutRelease({ cwd: repo, devBranch: 'main', prefix: 'release/', now: AUG31 })).toEqual({ kind: 'advanced', branch: 'release/2026-08-31', count: 0 })
|
||||
commit(peer, 'b.txt', 'b', 'feat b')
|
||||
git(peer, ['push', 'origin', 'main'])
|
||||
expect(await cutRelease({ cwd: repo, devBranch: 'main', prefix: 'release/', now: AUG31 })).toEqual({ kind: 'advanced', branch: 'release/2026-08-31', count: 1 })
|
||||
expect(git(origin, ['rev-parse', 'release/2026-08-31'])).toBe(git(origin, ['rev-parse', 'main']))
|
||||
expect(git(origin, ['for-each-ref', '--format=%(refname:short)', 'refs/heads/release/'])).toBe('release/2026-08-31')
|
||||
})
|
||||
it('同一天再切但发布分支上已有单独摘的提交 → diverged,远端不动', async () => {
|
||||
fx = await setup()
|
||||
const { repo, origin, peer } = fx
|
||||
await cutRelease({ cwd: repo, devBranch: 'main', prefix: 'release/', now: AUG31 })
|
||||
const fix = commit(peer, 'b.txt', 'b', 'fix b')
|
||||
commit(peer, 'c.txt', 'c', 'feat c')
|
||||
git(peer, ['push', 'origin', 'main'])
|
||||
git(peer, ['fetch', 'origin'])
|
||||
git(peer, ['checkout', '-b', 'rel', 'origin/release/2026-08-31'])
|
||||
git(peer, ['cherry-pick', '-x', fix])
|
||||
git(peer, ['push', 'origin', 'HEAD:release/2026-08-31'])
|
||||
const before = git(origin, ['rev-parse', 'release/2026-08-31'])
|
||||
expect(await cutRelease({ cwd: repo, devBranch: 'main', prefix: 'release/', now: AUG31 })).toEqual({ kind: 'diverged', branch: 'release/2026-08-31' })
|
||||
expect(git(origin, ['rev-parse', 'release/2026-08-31'])).toBe(before)
|
||||
})
|
||||
it('unreleased 计数忽略已 cherry-pick 的提交;cherryPickToRelease 推远端并清理 worktree', async () => {
|
||||
fx = await setup()
|
||||
@@ -178,13 +187,12 @@ describe('集成', () => {
|
||||
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('')
|
||||
})
|
||||
it('listReleaseBranches 新→旧,同日序号大的在前', async () => {
|
||||
it('listReleaseBranches 新→旧', async () => {
|
||||
fx = await setup()
|
||||
await cutRelease({ cwd: fx.repo, devBranch: 'main', prefix: 'release/', now: AUG01 })
|
||||
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'])
|
||||
expect(await listReleaseBranches({ cwd: fx.repo, prefix: 'release/' })).toEqual(['release/2026-08-31.2', 'release/2026-08-31', 'release/2026-08-01'])
|
||||
expect(await listReleaseBranches({ cwd: fx.repo, prefix: 'release/' })).toEqual(['release/2026-08-31', 'release/2026-08-01'])
|
||||
})
|
||||
it('indexUnreleasedByMergePr 用本地合并记录把分支提交对上 PR', async () => {
|
||||
fx = await setup()
|
||||
|
||||
@@ -140,7 +140,7 @@ export function PanelHeader({
|
||||
className="absolute right-0 top-7 z-20 min-w-44 rounded border border-[var(--vscode-menu-border)] bg-[var(--vscode-menu-background)] py-1 text-xs text-[var(--vscode-menu-foreground)] shadow"
|
||||
>
|
||||
<button type="button" role="menuitem" className={MENU_ITEM_CLASS} onClick={() => pickRelease('cut')}>
|
||||
{`切新发布(${release.nextBranch})`}
|
||||
{release.current === release.nextBranch ? `更新今日发布(${release.nextBranch})` : `切新发布(${release.nextBranch})`}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in new issue
Block a user