mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-27 11:22:03 +00:00
Open checked-out branches, switch the primary checkout for the default branch, and create linked worktrees only for non-trunk free branches.
214 lines
7.7 KiB
JavaScript
214 lines
7.7 KiB
JavaScript
'use strict'
|
|
|
|
const assert = require('node:assert/strict')
|
|
const { execFileSync } = require('node:child_process')
|
|
const fs = require('node:fs')
|
|
const os = require('node:os')
|
|
const path = require('node:path')
|
|
const test = require('node:test')
|
|
|
|
const {
|
|
addWorktree,
|
|
ensureGitRepo,
|
|
listBranches,
|
|
parseWorktrees,
|
|
sanitizeBranch,
|
|
switchBranch
|
|
} = require('./git-worktree-ops.cjs')
|
|
|
|
test('sanitizeBranch: spaces → hyphens, forbidden chars dropped, edges trimmed', () => {
|
|
assert.equal(sanitizeBranch('beach vibes'), 'beach-vibes')
|
|
assert.equal(sanitizeBranch('feat/cool thing'), 'feat/cool-thing')
|
|
assert.equal(sanitizeBranch(' wip~^:? '), 'wip')
|
|
assert.equal(sanitizeBranch('///'), '')
|
|
})
|
|
|
|
test('parseWorktrees: main checkout + linked worktree', () => {
|
|
const out = [
|
|
'worktree /repo',
|
|
'HEAD abc123',
|
|
'branch refs/heads/main',
|
|
'',
|
|
'worktree /repo/.worktrees/feat',
|
|
'HEAD def456',
|
|
'branch refs/heads/hermes/feat',
|
|
''
|
|
].join('\n')
|
|
|
|
const trees = parseWorktrees(out)
|
|
|
|
assert.equal(trees.length, 2)
|
|
assert.equal(trees[0].path, '/repo')
|
|
assert.equal(trees[0].branch, 'main')
|
|
assert.equal(trees[1].path, '/repo/.worktrees/feat')
|
|
assert.equal(trees[1].branch, 'hermes/feat')
|
|
})
|
|
|
|
test('parseWorktrees: detached + locked flags', () => {
|
|
const out = ['worktree /repo/wt', 'HEAD abc', 'detached', 'locked reason', ''].join('\n')
|
|
const trees = parseWorktrees(out)
|
|
|
|
assert.equal(trees.length, 1)
|
|
assert.equal(trees[0].detached, true)
|
|
assert.equal(trees[0].locked, true)
|
|
assert.equal(trees[0].branch, null)
|
|
})
|
|
|
|
test('parseWorktrees: empty input', () => {
|
|
assert.deepEqual(parseWorktrees(''), [])
|
|
})
|
|
|
|
test('ensureGitRepo: inits a plain dir with a root commit so worktrees branch', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-wt-'))
|
|
const git = (...args) => execFileSync('git', args, { cwd: dir }).toString().trim()
|
|
|
|
try {
|
|
await ensureGitRepo('git', dir)
|
|
assert.match(git('rev-parse', '--verify', 'HEAD'), /^[0-9a-f]{7,}$/)
|
|
|
|
// The whole point: a worktree can now branch off the seeded root commit.
|
|
execFileSync('git', ['worktree', 'add', '-b', 'wt', path.join(dir, '.worktrees', 'wt')], { cwd: dir })
|
|
assert.ok(fs.existsSync(path.join(dir, '.worktrees', 'wt')))
|
|
|
|
// Idempotent: an already-committed repo gets no extra commit.
|
|
await ensureGitRepo('git', dir)
|
|
assert.equal(git('rev-list', '--count', 'HEAD'), '1')
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('switchBranch: switches a normal checkout branch', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-switch-'))
|
|
const git = (...args) => execFileSync('git', args, { cwd: dir }).toString().trim()
|
|
|
|
try {
|
|
await ensureGitRepo('git', dir)
|
|
execFileSync('git', ['branch', 'feature'], { cwd: dir })
|
|
|
|
await switchBranch(dir, 'feature', 'git')
|
|
|
|
assert.equal(git('branch', '--show-current'), 'feature')
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('listBranches: lists locals and flags the checked-out branch', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-branches-'))
|
|
|
|
try {
|
|
await ensureGitRepo('git', dir)
|
|
const current = execFileSync('git', ['branch', '--show-current'], { cwd: dir }).toString().trim()
|
|
execFileSync('git', ['branch', 'feature'], { cwd: dir })
|
|
|
|
const branches = await listBranches(dir, 'git')
|
|
const names = branches.map(b => b.name).sort()
|
|
|
|
assert.deepEqual(names, [current, 'feature'].sort())
|
|
// The repo's own checkout is flagged; the unused branch is convertible.
|
|
assert.equal(branches.find(b => b.name === current).checkedOut, true)
|
|
assert.equal(branches.find(b => b.name === current).isDefault, true)
|
|
assert.equal(fs.realpathSync(branches.find(b => b.name === current).worktreePath), fs.realpathSync(dir))
|
|
assert.equal(branches.find(b => b.name === 'feature').checkedOut, false)
|
|
assert.equal(branches.find(b => b.name === 'feature').isDefault, false)
|
|
assert.equal(branches.find(b => b.name === 'feature').worktreePath, null)
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('listBranches: flags a free default branch as default, not checked out', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-branches-default-'))
|
|
const git = (...args) => execFileSync('git', args, { cwd: dir }).toString().trim()
|
|
|
|
try {
|
|
await ensureGitRepo('git', dir)
|
|
const trunk = git('branch', '--show-current')
|
|
execFileSync('git', ['switch', '-c', 'rawr'], { cwd: dir })
|
|
|
|
const branches = await listBranches(dir, 'git')
|
|
const defaultBranch = branches.find(b => b.name === trunk)
|
|
|
|
assert.equal(defaultBranch.checkedOut, false)
|
|
assert.equal(defaultBranch.isDefault, true)
|
|
assert.equal(defaultBranch.worktreePath, null)
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('listBranches: a branch claimed by a worktree is flagged checked out', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-branches-wt-'))
|
|
|
|
try {
|
|
await ensureGitRepo('git', dir)
|
|
execFileSync('git', ['branch', 'feature'], { cwd: dir })
|
|
// addWorktree converts the existing "feature" branch into a worktree.
|
|
const result = await addWorktree(dir, { existingBranch: 'feature' }, 'git')
|
|
|
|
assert.equal(result.branch, 'feature')
|
|
assert.ok(fs.existsSync(result.path))
|
|
|
|
const branches = await listBranches(dir, 'git')
|
|
|
|
assert.equal(branches.find(b => b.name === 'feature').checkedOut, true)
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('listBranches: empty on a non-repo path', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-nonrepo-'))
|
|
|
|
try {
|
|
assert.deepEqual(await listBranches(dir, 'git'), [])
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('addWorktree: existingBranch checks the branch out without a new branch', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-convert-'))
|
|
const git = (...args) => execFileSync('git', args, { cwd: dir }).toString().trim()
|
|
|
|
try {
|
|
await ensureGitRepo('git', dir)
|
|
execFileSync('git', ['branch', 'cool/feature'], { cwd: dir })
|
|
|
|
const before = git('branch', '--list').split('\n').length
|
|
const result = await addWorktree(dir, { existingBranch: 'cool/feature' }, 'git')
|
|
|
|
// No new branch was created — only the existing one is checked out.
|
|
assert.equal(git('branch', '--list').split('\n').length, before)
|
|
assert.equal(result.branch, 'cool/feature')
|
|
// Dir is named off the branch slug, nested under the main repo's .worktrees.
|
|
assert.match(result.path, /[/\\]\.worktrees[/\\]cool-feature/)
|
|
assert.equal(
|
|
execFileSync('git', ['branch', '--show-current'], { cwd: result.path }).toString().trim(),
|
|
'cool/feature'
|
|
)
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test('addWorktree: existing default branch switches the main checkout, not .worktrees/main', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-convert-default-'))
|
|
const git = (...args) => execFileSync('git', args, { cwd: dir }).toString().trim()
|
|
|
|
try {
|
|
await ensureGitRepo('git', dir)
|
|
const trunk = git('branch', '--show-current')
|
|
execFileSync('git', ['switch', '-c', 'rawr'], { cwd: dir })
|
|
|
|
const result = await addWorktree(dir, { existingBranch: trunk }, 'git')
|
|
|
|
assert.equal(result.branch, trunk)
|
|
assert.equal(fs.realpathSync(result.path), fs.realpathSync(dir))
|
|
assert.equal(git('branch', '--show-current'), trunk)
|
|
assert.equal(fs.existsSync(path.join(dir, '.worktrees', trunk)), false)
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|