cmdPhasesClear hard-deleted committed phase directories (rmSync) with no archive, so browsable phase history was silently lost at a milestone switch. The #1447 dirty-tree guard was a no-op for the common committed case (a clean tree passes the guard, then rmSync destroyed the dirs, leaving orphaned uncommitted deletions and no archive). Archive-then-remove: move each non-999 phase dir to milestones/<version>-phases/ (version from getMilestoneInfo; timestamp fallback; collision-safe) using retryRenameSync — mirroring the existing archivePhases path in cmdMilestoneComplete. The #1447 uncommitted-changes guard is retained as a secondary backstop. Tests in tests/new-milestone-clear-phases.test.cjs updated: phase content is asserted to SURVIVE in milestones/*-phases/ (archived), not be destroyed — including the committed-dirs case that previously codified the no-op. Closes #1871
This commit is contained in:
@@ -26,7 +26,7 @@ import phaseIdMod = require('./phase-id.cjs');
|
||||
const { escapeRegex, normalizePhaseName, phaseTokenMatches } = phaseIdMod;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import roadmapParserMod = require('./roadmap-parser.cjs');
|
||||
const { getMilestonePhaseFilter, extractCurrentMilestone } = roadmapParserMod;
|
||||
const { getMilestonePhaseFilter, extractCurrentMilestone, getMilestoneInfo } = roadmapParserMod;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import coreUtilsMod = require('./core-utils.cjs');
|
||||
const { extractOneLinerFromBody } = coreUtilsMod;
|
||||
@@ -440,8 +440,30 @@ function cmdPhasesClear(cwd: string, raw: boolean, args: string[]): void {
|
||||
}
|
||||
|
||||
try {
|
||||
// #1871: archive phase directories instead of destroying them. Move each
|
||||
// non-999 dir to milestones/<version>-phases/ so browsable phase history
|
||||
// survives the milestone switch (previously rmSync hard-deleted committed
|
||||
// dirs, leaving orphaned uncommitted deletions and no archive).
|
||||
let archiveVersion: string | null = null;
|
||||
try {
|
||||
archiveVersion = getMilestoneInfo(cwd).version ?? null;
|
||||
} catch {
|
||||
/* ROADMAP/STATE unreadable — fall back to a dated label */
|
||||
}
|
||||
if (!archiveVersion) {
|
||||
archiveVersion = `archived-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 8)}`;
|
||||
}
|
||||
const archivePhasesDir = path.join(planningPaths(cwd).planning, 'milestones', `${archiveVersion}-phases`);
|
||||
platformEnsureDir(archivePhasesDir);
|
||||
for (const entry of dirs) {
|
||||
fs.rmSync(path.join(phasesDir, entry.name), { recursive: true, force: true });
|
||||
const src = path.join(phasesDir, entry.name);
|
||||
// Collision-safe: if a same-named archive entry exists (re-run), suffix it.
|
||||
let dest = path.join(archivePhasesDir, entry.name);
|
||||
let n = 1;
|
||||
while (fs.existsSync(dest)) {
|
||||
dest = path.join(archivePhasesDir, `${entry.name}.${n++}`);
|
||||
}
|
||||
retryRenameSync(src, dest);
|
||||
cleared++;
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user