feat: add removeWorktreeForJob helper
Removes worktree dir via `git worktree remove --force` and deletes
the local branch by default; keepBranch=true preserves the branch.
Returns { removed: false } when the worktree path doesn't exist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e7bb3c82ba
commit
b20e297851
2 changed files with 131 additions and 1 deletions
|
|
@ -4,7 +4,7 @@ import { promisify } from 'node:util'
|
|||
import * as os from 'node:os'
|
||||
import * as fs from 'node:fs/promises'
|
||||
import * as path from 'node:path'
|
||||
import { createWorktreeForJob } from '../../src/git/worktree.js'
|
||||
import { createWorktreeForJob, removeWorktreeForJob } from '../../src/git/worktree.js'
|
||||
|
||||
const exec = promisify(execFile)
|
||||
|
||||
|
|
@ -112,3 +112,93 @@ describe('createWorktreeForJob', () => {
|
|||
).rejects.toThrow('Worktree path already exists')
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeWorktreeForJob', () => {
|
||||
const tmpDirs: string[] = []
|
||||
const originalWorktreeDir = process.env.SCRUM4ME_AGENT_WORKTREE_DIR
|
||||
|
||||
afterEach(async () => {
|
||||
if (originalWorktreeDir === undefined) {
|
||||
delete process.env.SCRUM4ME_AGENT_WORKTREE_DIR
|
||||
} else {
|
||||
process.env.SCRUM4ME_AGENT_WORKTREE_DIR = originalWorktreeDir
|
||||
}
|
||||
for (const dir of tmpDirs.splice(0)) {
|
||||
await fs.rm(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
async function makeWorktreeParent(): Promise<string> {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'scrum4me-worktrees-'))
|
||||
tmpDirs.push(dir)
|
||||
process.env.SCRUM4ME_AGENT_WORKTREE_DIR = dir
|
||||
return dir
|
||||
}
|
||||
|
||||
it('removes worktree directory and deletes branch by default', async () => {
|
||||
const { repoDir, originDir } = await setupRepo()
|
||||
tmpDirs.push(repoDir, originDir)
|
||||
const wtParent = await makeWorktreeParent()
|
||||
|
||||
const { worktreePath, branchName } = await createWorktreeForJob({
|
||||
repoRoot: repoDir,
|
||||
jobId: 'job-rm-01',
|
||||
branchName: 'feat/job-rm-01',
|
||||
baseRef: 'origin/main',
|
||||
})
|
||||
|
||||
const result = await removeWorktreeForJob({ repoRoot: repoDir, jobId: 'job-rm-01' })
|
||||
|
||||
expect(result.removed).toBe(true)
|
||||
await expect(fs.access(worktreePath)).rejects.toThrow()
|
||||
await expect(fs.access(path.join(wtParent, 'job-rm-01'))).rejects.toThrow()
|
||||
|
||||
// Branch should be deleted
|
||||
await expect(
|
||||
exec('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`], {
|
||||
cwd: repoDir,
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('removes worktree directory but keeps branch when keepBranch=true', async () => {
|
||||
const { repoDir, originDir } = await setupRepo()
|
||||
tmpDirs.push(repoDir, originDir)
|
||||
const wtParent = await makeWorktreeParent()
|
||||
|
||||
const { worktreePath, branchName } = await createWorktreeForJob({
|
||||
repoRoot: repoDir,
|
||||
jobId: 'job-rm-02',
|
||||
branchName: 'feat/job-rm-02',
|
||||
baseRef: 'origin/main',
|
||||
})
|
||||
|
||||
const result = await removeWorktreeForJob({
|
||||
repoRoot: repoDir,
|
||||
jobId: 'job-rm-02',
|
||||
keepBranch: true,
|
||||
})
|
||||
|
||||
expect(result.removed).toBe(true)
|
||||
await expect(fs.access(worktreePath)).rejects.toThrow()
|
||||
await expect(fs.access(path.join(wtParent, 'job-rm-02'))).rejects.toThrow()
|
||||
|
||||
// Branch should still exist
|
||||
const { stdout } = await exec(
|
||||
'git',
|
||||
['show-ref', '--verify', `refs/heads/${branchName}`],
|
||||
{ cwd: repoDir },
|
||||
)
|
||||
expect(stdout).toContain(branchName)
|
||||
})
|
||||
|
||||
it('returns { removed: false } when worktree does not exist', async () => {
|
||||
const { repoDir, originDir } = await setupRepo()
|
||||
tmpDirs.push(repoDir, originDir)
|
||||
await makeWorktreeParent()
|
||||
|
||||
const result = await removeWorktreeForJob({ repoRoot: repoDir, jobId: 'job-rm-nonexistent' })
|
||||
|
||||
expect(result.removed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -55,3 +55,43 @@ export async function createWorktreeForJob(opts: {
|
|||
|
||||
return { worktreePath, branchName }
|
||||
}
|
||||
|
||||
export async function removeWorktreeForJob(opts: {
|
||||
repoRoot: string
|
||||
jobId: string
|
||||
keepBranch?: boolean
|
||||
}): Promise<{ removed: boolean }> {
|
||||
const { repoRoot, jobId, keepBranch = false } = opts
|
||||
|
||||
const parent =
|
||||
process.env.SCRUM4ME_AGENT_WORKTREE_DIR ??
|
||||
path.join(os.homedir(), '.scrum4me-agent-worktrees')
|
||||
|
||||
const worktreePath = path.join(parent, jobId)
|
||||
|
||||
try {
|
||||
await fs.access(worktreePath)
|
||||
} catch {
|
||||
return { removed: false }
|
||||
}
|
||||
|
||||
let branchName: string | undefined
|
||||
if (!keepBranch) {
|
||||
try {
|
||||
const { stdout } = await exec('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
||||
cwd: worktreePath,
|
||||
})
|
||||
branchName = stdout.trim()
|
||||
} catch {
|
||||
// worktree HEAD unreadable — skip branch deletion
|
||||
}
|
||||
}
|
||||
|
||||
await exec('git', ['worktree', 'remove', '--force', worktreePath], { cwd: repoRoot })
|
||||
|
||||
if (!keepBranch && branchName && (await branchExists(repoRoot, branchName))) {
|
||||
await exec('git', ['branch', '-D', branchName], { cwd: repoRoot })
|
||||
}
|
||||
|
||||
return { removed: true }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue