Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions lib/domains/archive.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ export function execute(r, o, x, b) {
write(b)
const dest = dirname(xp(r, n))
mkdirSync(dirname(dest), { recursive: true })
renameSync(dirname(b.path), dest)
const source = dirname(b.path)
// 旧目录仅在被 git 追踪时才需要暂存删除:change 从未入 commit(如 release 文件清单遗漏)时
// 移动后旧路径无物可加,硬加会 pathspec fatal 并把归档卡在半落状态
const tracked = git(r, ['ls-files', '--', source]).trim().length > 0
renameSync(source, dest)
writeFileSync(join(r, 'shadow-docs', 'INDEX.md'), buildIndex(r))
// 归档落本地 commit:半落状态(INDEX 改 + 旧目录删 + 新目录未跟踪)对 AI 与人都易漏提交
git(r, ['add', '--', 'shadow-docs/INDEX.md', `shadow-docs/changes/${n}`, `shadow-docs/changes/archive/${n}`])
const staged = ['shadow-docs/INDEX.md', `shadow-docs/changes/archive/${n}`]
if (tracked) staged.unshift(`shadow-docs/changes/${n}`)
git(r, ['add', '--', ...staged])
git(r, ['commit', '-m', `docs(shadow): 归档 ${n}——PR #${x.pullRequest} 已合入 main,brief 移入 archive 并重建 INDEX`])
return { path: xp(r, n) }
}
27 changes: 27 additions & 0 deletions test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,33 @@ test('archive execute lands the archive move as a local commit', () => {
}
})

test('archive execute survives an untracked change dir and lands the local commit', () => {
const root = fixture()
const head = () => execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root }).toString().trim()
// brief 保持未追踪(如 release 文件清单遗漏了 change 目录),旧路径在移动后无物可加
updateBrief(root, (d) => {
d.status = 'published'
d.github = { repository: 'owner/repo', issue: null, issueUrl: null, pullRequest: 9, pullRequestUrl: 'https://github.test/pull/9' }
d.review = { conclusion: 'passed', verifiedCommit: head(), verifiedAt: '2026-01-01T00:00:00.000Z' }
})
const api = apiStub([{ method: 'GET', path: '/repos/owner/repo/pulls/9', body: { number: 9, merged: true, state: 'closed' } }])
try {
const planned = run(['archive', 'plan', '--name', 'sample', '--json'], root, { GITHUB_TOKEN: 'token', SHADOW_GITHUB_API_URL: api.url })
assert.equal(planned.status, 0, planned.stderr)
const before = head()
const result = run(['archive', 'execute', '--name', 'sample', '--plan-hash', JSON.parse(planned.stdout).planHash, '--confirm', '--json'], root, { GITHUB_TOKEN: 'token', SHADOW_GITHUB_API_URL: api.url })
assert.equal(result.status, 0, result.stderr)
const after = head()
assert.notEqual(after, before, 'archive must land a local commit')
const inspect = run(['repo', 'inspect', '--json'], root)
assert.deepEqual(JSON.parse(inspect.stdout).data.changedFiles, [], 'archive commit must leave the tree clean')
assert.ok(!existsSync(join(root, 'shadow-docs', 'changes', 'sample')), 'source dir must be gone')
assert.ok(existsSync(join(root, 'shadow-docs', 'changes', 'archive', 'sample', 'brief.md')), 'brief must land in archive')
} finally {
api.close()
}
})

test('commit execute reuses persisted files and message without re-passing them', () => {
const root = fixture()
writeFileSync(join(root, 'a.js'), 'a\n')
Expand Down
Loading