noteUntrackedSkipped: fix subdir underreport + be best-effort everywhere

Two bugs in the untracked-skipped warning, both surfaced by review.

1. Wrong scope for subdir inputs. The helper accepted any path the
   caller had (sourcePath, which may be a user-supplied subdirectory)
   and ran `git -C <path> ls-files --others --exclude-standard`. Git
   scopes that output to the cwd, so pointing `vm run ./repo/sub` at
   a subdir silently underreported untracked files living elsewhere
   in the repo — exactly the files the warning exists to surface.
   Fix: resolve sourcePath to the repo root inside the helper via
   `rev-parse --show-toplevel` before counting.

2. Inconsistent failure handling. The comment said the helper should
   be silent when the count can't be determined; the body returned
   the error. vm_run.go treated the error as non-fatal (logged a
   warning, continued); workspace prepare and --dry-run aborted the
   whole operation on the same helper failure. A courtesy notice
   shouldn't kill the operation.
   Fix: make the helper best-effort in signature and body — no error
   return, swallows rev-parse + count failures, emits nothing when
   there's nothing to say. All three callers lose their error
   branches.

Regression tests:
- subdir input reports the root-level untracked file (the bug case)
- non-repo path produces silence, not a fatal error
- inspector whose runner errors on every call produces silence

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-22 12:42:33 -03:00
parent ecb18ce6ca
commit bbd187391e
No known key found for this signature in database
GPG key ID: 33112E6833C34679
4 changed files with 144 additions and 20 deletions

View file

@ -29,25 +29,33 @@ func (d *deps) runWorkspaceDryRun(ctx context.Context, out io.Writer, resolvedPa
fmt.Fprintln(out, path)
}
if !includeUntracked {
if err := d.noteUntrackedSkipped(ctx, out, spec.RepoRoot); err != nil {
return err
}
d.noteUntrackedSkipped(ctx, out, spec.RepoRoot)
}
return nil
}
// noteUntrackedSkipped prints a one-line notice to stderr-analog when
// the repo has untracked non-ignored files that will NOT be copied
// because --include-untracked was not passed. Silent when there are
// no such files, or when the count can't be determined.
func (d *deps) noteUntrackedSkipped(ctx context.Context, out io.Writer, repoRoot string) error {
count, err := d.repoInspector.CountUntrackedPaths(ctx, repoRoot)
if err != nil {
return err
// noteUntrackedSkipped prints a one-line notice when the repo holds
// untracked non-ignored files that will NOT be copied because
// --include-untracked was not passed.
//
// Best-effort: if sourcePath isn't inside a git repo, or git errors,
// or there are no untracked files, the helper stays silent. The
// notice is a courtesy — failing the whole operation over a courtesy
// would be worse than the notice being missing.
//
// Resolves sourcePath to the repo root internally via `git rev-parse
// --show-toplevel` so callers can pass whatever path the user typed.
// Before this helper normalised, subdir inputs ran `ls-files
// --others` scoped to the subdir, which silently underreported the
// skipped files the user needed to know about.
func (d *deps) noteUntrackedSkipped(ctx context.Context, out io.Writer, sourcePath string) {
repoRoot, err := d.repoInspector.GitTrimmedOutput(ctx, sourcePath, "rev-parse", "--show-toplevel")
if err != nil || repoRoot == "" {
return
}
if count == 0 {
return nil
count, err := d.repoInspector.CountUntrackedPaths(ctx, repoRoot)
if err != nil || count == 0 {
return
}
fmt.Fprintf(out, "---\nnote: %d untracked non-ignored file(s) were NOT copied (git-tracked files only by default — pass --include-untracked to include them)\n", count)
return nil
}