fix(#2523): normalize --files to repo-relative; reject out-of-repo; gate push on git-add exit (#2638)
* test(#2523): absolute + mixed + out-of-repo --files paths * fix(#2523): normalize --files to repo-relative; reject out-of-repo; gate push on git-add exit * chore(#2523): backfill changeset pr to 2638
This commit is contained in:
@@ -769,6 +769,28 @@ function cmdCommit(cwd: string, message: string | undefined, files: string[] | u
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize --files to repo-relative paths (#2523). path.join(cwd, absPath)
|
||||
// concatenates instead of resetting (path.resolve does), so absolute phase_dir
|
||||
// paths emitted by init phase-op (#2428) were joined to cwd+absPath (non-existent)
|
||||
// and silently dropped by the existence check — and a mixed relative/absolute list
|
||||
// then committed the relative entries while reporting committed:true. Relative-izing
|
||||
// also keeps the phase-branch detection below from matching digit-hyphen runs in the
|
||||
// absolute prefix (e.g. /proj-7-decoy2/ → wrong phase branch).
|
||||
const filesRel = files && files.length > 0
|
||||
? files.map(f => toPosixPath(path.relative(cwd, path.resolve(cwd, f))))
|
||||
: [];
|
||||
// Reject --files entries resolving OUTSIDE the project root: fail loudly rather
|
||||
// than silently skip or reach `git add` (which rejects them and would pollute the
|
||||
// index via an unconditional stagedPaths.push) (#2523). `toPosixPath` only flips
|
||||
// backslashes, so a `..` escape is still detectable.
|
||||
for (const rel of filesRel) {
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
const result = { committed: false, hash: null, reason: 'path_outside_repo', path: rel };
|
||||
output(result, raw, 'failed');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure branching strategy branch exists before first commit (#1278).
|
||||
// Pre-execution workflows (discuss, plan, research) commit artifacts but the branch
|
||||
// was previously only created during execute-phase — too late.
|
||||
@@ -777,7 +799,7 @@ function cmdCommit(cwd: string, message: string | undefined, files: string[] | u
|
||||
let branchName: string | null = null;
|
||||
if (branchingStrategy === 'phase') {
|
||||
// Determine which phase we're committing for from the file paths
|
||||
const phaseMatch = (files || []).join(' ').match(/(\d+(?:\.\d+)*)-/);
|
||||
const phaseMatch = (filesRel || []).join(' ').match(/(\d+(?:\.\d+)*)-/);
|
||||
if (phaseMatch) {
|
||||
const phaseNum = phaseMatch[1];
|
||||
const phaseInfo = findPhaseInternal(cwd, phaseNum) as Record<string, unknown> | null;
|
||||
@@ -809,7 +831,7 @@ function cmdCommit(cwd: string, message: string | undefined, files: string[] | u
|
||||
|
||||
// Stage files
|
||||
const explicitFiles = files && files.length > 0;
|
||||
const filesToStage = explicitFiles ? files : ['.planning/'];
|
||||
const filesToStage = explicitFiles ? filesRel : ['.planning/'];
|
||||
const stagedPaths: string[] = [];
|
||||
for (const file of filesToStage) {
|
||||
const fullPath = path.join(cwd, file);
|
||||
@@ -824,8 +846,13 @@ function cmdCommit(cwd: string, message: string | undefined, files: string[] | u
|
||||
// removed planning files are not left dangling in the index.
|
||||
execGit(['rm', '--cached', '--ignore-unmatch', file], { cwd });
|
||||
} else {
|
||||
execGit(['add', file], { cwd });
|
||||
stagedPaths.push(file);
|
||||
const addResult = execGit(['add', file], { cwd });
|
||||
// Only record paths that actually staged — a failed `git add` (permissions,
|
||||
// out-of-repo edge) must not enter the commit pathspec (#2523). Mirrors
|
||||
// cmdCommitToSubrepo's exitCode-gated push.
|
||||
if (addResult.exitCode === 0) {
|
||||
stagedPaths.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user