🐛 fix(vscode): 实施启动改为复用已有 feature 分支
This commit is contained in:
+111
-34
@@ -1,9 +1,6 @@
|
||||
/**
|
||||
* Thin wrapper around `git worktree add -b` for the 实施 flow.
|
||||
*
|
||||
* We invoke the system `git` binary via `execFile` rather than depend on a
|
||||
* git library — keeps the surface minimal and lets the user benefit from
|
||||
* whatever git they already have configured.
|
||||
* git worktree 的薄封装:实施流用 ensureWorktree 保证「这个 feature 有一个可用
|
||||
* worktree」,不依赖 git 库,走用户机器上的系统 git。
|
||||
*/
|
||||
|
||||
import { execFile } from 'node:child_process'
|
||||
@@ -30,43 +27,123 @@ export function resolveWorktreePath(stored: string, workspaceRoot: string): stri
|
||||
}
|
||||
|
||||
export interface WorktreeOpts {
|
||||
/** Absolute path to the main workspace root. */
|
||||
workspaceRoot: string
|
||||
/** Absolute path of the worktree to create. */
|
||||
/** 模板 path;① 已有 live worktree 时返回值可能与此不同。 */
|
||||
worktreePath: string
|
||||
/** New branch name; e.g. `feature/abcd1234`. */
|
||||
branch: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new git worktree with a freshly-created branch off current HEAD.
|
||||
* git -C <workspaceRoot> worktree add <worktreePath> -b <branch>
|
||||
*
|
||||
* Rejects with a descriptive Error if git exits non-zero. The caller should
|
||||
* surface stderr to the user via a toast.
|
||||
*/
|
||||
export async function createWorktree(opts: WorktreeOpts): Promise<void> {
|
||||
const { workspaceRoot, worktreePath, branch } = opts
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
execFile(
|
||||
'git',
|
||||
['-C', workspaceRoot, 'worktree', 'add', worktreePath, '-b', branch],
|
||||
{ timeout: 30_000 },
|
||||
(err, _stdout, stderr) => {
|
||||
if (err) {
|
||||
// execFile sets `err.code` for non-zero exits; for spawn failures
|
||||
// (e.g. git not on PATH) it's a string like 'ENOENT'.
|
||||
const code = (err as NodeJS.ErrnoException).code ?? 'unknown'
|
||||
const trimmed = (stderr ?? '').toString().trim() || err.message
|
||||
reject(new Error(`git worktree add 失败 (${code}): ${trimmed}`))
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
},
|
||||
)
|
||||
function runGit(workspaceRoot: string, args: string[], failLabel?: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile('git', ['-C', workspaceRoot, ...args], { timeout: 30_000 }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
// execFile: 非零退出是数字 code;git 不在 PATH 时是 'ENOENT'
|
||||
const code = (err as NodeJS.ErrnoException).code ?? 'unknown'
|
||||
const trimmed = (stderr ?? '').toString().trim() || err.message
|
||||
const label = failLabel ?? `git ${args.join(' ')}`
|
||||
reject(new Error(`${label} 失败 (${code}): ${trimmed}`))
|
||||
return
|
||||
}
|
||||
resolve((stdout ?? '').toString())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function pathExists(p: string): Promise<boolean> {
|
||||
try {
|
||||
await fsp.stat(p)
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function parseWorktreePorcelain(stdout: string): Array<{ path: string, branch: string | null, prunable: boolean }> {
|
||||
const entries: Array<{ path: string, branch: string | null, prunable: boolean }> = []
|
||||
let current: { path: string, branch: string | null, prunable: boolean } | null = null
|
||||
const flush = (): void => {
|
||||
if (current?.path)
|
||||
entries.push(current)
|
||||
current = null
|
||||
}
|
||||
for (const line of stdout.split('\n')) {
|
||||
if (line.startsWith('worktree ')) {
|
||||
flush()
|
||||
current = { path: line.slice('worktree '.length), branch: null, prunable: false }
|
||||
continue
|
||||
}
|
||||
if (!current)
|
||||
continue
|
||||
if (line.startsWith('branch ')) {
|
||||
const ref = line.slice('branch '.length)
|
||||
current.branch = ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : ref
|
||||
}
|
||||
else if (line === 'detached') {
|
||||
current.branch = null
|
||||
}
|
||||
else if (line === 'prunable' || line.startsWith('prunable ')) {
|
||||
current.prunable = true
|
||||
}
|
||||
else if (line === '') {
|
||||
flush()
|
||||
}
|
||||
}
|
||||
flush()
|
||||
return entries
|
||||
}
|
||||
|
||||
async function findLiveWorktreeForBranch(workspaceRoot: string, branch: string): Promise<string | undefined> {
|
||||
const stdout = await runGit(workspaceRoot, ['worktree', 'list', '--porcelain'])
|
||||
for (const entry of parseWorktreePorcelain(stdout)) {
|
||||
if (entry.prunable || entry.branch !== branch)
|
||||
continue
|
||||
if (await pathExists(entry.path))
|
||||
return entry.path
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function localBranchExists(workspaceRoot: string, branch: string): Promise<boolean> {
|
||||
try {
|
||||
await runGit(workspaceRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`])
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保证这个 feature 有一个可用 worktree。优先复用 live,绝不删 feature 分支
|
||||
* —— 拖到「进行中」是开始实施,不是销毁上次进度。
|
||||
*/
|
||||
export async function ensureWorktree(opts: WorktreeOpts): Promise<string> {
|
||||
const { workspaceRoot, branch } = opts
|
||||
const worktreePath = path.resolve(opts.worktreePath)
|
||||
|
||||
// ① 该 branch 已在某个 live worktree → 原路返回(path ≠ 模板也不搬,避免丢未提交改动)
|
||||
const live = await findLiveWorktreeForBranch(workspaceRoot, branch)
|
||||
if (live) {
|
||||
console.log(`[superpowers] 复用已有 worktree ${live} (${branch})`)
|
||||
return live
|
||||
}
|
||||
|
||||
// ② 模板 path 被垃圾占着(存在,但不是本仓库该 branch 的 live worktree)
|
||||
if (await pathExists(worktreePath)) {
|
||||
killProcessesUsingWorktree(worktreePath)
|
||||
await removeWorktreeDir(workspaceRoot, worktreePath)
|
||||
console.log(`[superpowers] 已清模板路径垃圾 ${worktreePath}`)
|
||||
}
|
||||
|
||||
const branchExists = await localBranchExists(workspaceRoot, branch)
|
||||
const addArgs = branchExists
|
||||
? ['worktree', 'add', worktreePath, branch] // ③ 有分支无 live worktree → 不加 -b,挂上已有分支,保留 commit
|
||||
: ['worktree', 'add', worktreePath, '-b', branch] // ④ 分支也不存在 → -b 新建
|
||||
await runGit(workspaceRoot, addArgs, 'git worktree add')
|
||||
return worktreePath
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个 worktree 目录并清掉 git 侧注册。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user