Repo-root-relative untracked (and not-ignored) path → mtime_ns, or {} on error. Used at UPS to snapshot the pre-turn untracked set so the Stop hook can exclude unchanged pre-existing untracked files from review. mtime is captured so an in-place edit during the turn is still reviewed.
(cwd)
| 317 | |
| 318 | |
| 319 | def _list_untracked(cwd): |
| 320 | """Repo-root-relative untracked (and not-ignored) path → mtime_ns, or {} |
| 321 | on error. Used at UPS to snapshot the pre-turn untracked set so the Stop |
| 322 | hook can exclude unchanged pre-existing untracked files from review. |
| 323 | mtime is captured so an in-place edit during the turn is still reviewed. |
| 324 | |
| 325 | Uses ls-files (not status) for the UPS path: the index diff isn't needed, |
| 326 | and ls-files --others only walks the worktree against .gitignore.""" |
| 327 | try: |
| 328 | repo = _git_toplevel(cwd) or cwd |
| 329 | r = subprocess.run( |
| 330 | [*GIT_CMD, "-c", "core.quotePath=false", "ls-files", |
| 331 | "--others", "--exclude-standard", "-z"], |
| 332 | cwd=repo, capture_output=True, text=True, timeout=15, |
| 333 | ) |
| 334 | if r.returncode != 0: |
| 335 | debug_log(f"_list_untracked rc={r.returncode}: {r.stderr[:200]}") |
| 336 | return {} |
| 337 | out = {} |
| 338 | for p in r.stdout.split("\0"): |
| 339 | if not p: |
| 340 | continue |
| 341 | try: |
| 342 | out[p] = os.stat(os.path.join(repo, p)).st_mtime_ns |
| 343 | except OSError: |
| 344 | out[p] = 0 |
| 345 | if len(out) >= UNTRACKED_BASELINE_CAP: |
| 346 | debug_log(f"_list_untracked: capped at {UNTRACKED_BASELINE_CAP}") |
| 347 | break |
| 348 | return out |
| 349 | except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e: |
| 350 | debug_log(f"_list_untracked error: {e}") |
| 351 | return {} |
| 352 | |
| 353 | def compute_v2_review_set(cwd, baseline_sha, head_at_capture, untracked_at_baseline=None): |
| 354 | """v2 diff strategy: derive the review set from git state alone. |
no test coverage detected