# fix\(server\): checkpoints no longer rehash every file when one entry is skip-worktree or assume-unchanged · PR \#23624 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23624)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: open

Source repository: pingdotgg/t3code

Source branch: fix/issue-13741

Target branch: main

Head commit: cfee52fc24f56169ed4c0ce48759ad1ff9a7f655

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/23624)

[Commits](https://git.cafe/pingdotgg/t3code/pull/23624/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/23624/diffs?format=markdown)

## Description

---

## Problem

In a repository that is not a sparse checkout, a single tracked entry marked skip-worktree or assume-unchanged made checkpoint capture throw away the copied index. Capture then rebuilt the index from `read-tree HEAD` with no stat cache, so `git add -A` rehashed every tracked file. On large worktrees or slow filesystems (the reporter's case: about 25k entries on an SMB drive with 6 skip-worktree entries), every capture hit `VCS process timed out in GitVcsDriver.checkpoints.captureCheckpoint ... after 30000ms`.

Reproduction on `main` (ed3f965e07), calling the real `GitVcsDriver.checkpoints.captureCheckpoint` on Linux with git 2.43. The fixture has 25,000 tracked files, a clean filter that counts every file Git rehashes, and one edited flagged file:

| flags | `main` | this branch |
|---|---|---|
| none | 505 ms, 2 files rehashed | 417 ms, 1 file rehashed |
| 1 skip-worktree | **timed out after 30000 ms** | 523 ms, 1 file rehashed |
| 1 assume-unchanged | **timed out after 30000 ms** | 731 ms, 2 files rehashed |

With 500 files on `main`, the same flagged capture took 28.5 s and rehashed all 501 files. `GIT_TRACE` shows the extra fresh `git read-tree HEAD`.

## Change

Capture now keeps the copied index and rebuilds only the flagged entries in the private index:

- **Scan.** The existing `ls-files` scan adds `--stage`, so each flagged record already has the `<mode> <oid> <stage>\t<path>` shape that `update-index --index-info` reads. The scan collects only flagged records: lowercase tags, `S` outside sparse checkout, and in-cone `S` selected by `sparse-checkout check-rules`.
- **Rebuild.** `update-index -z --index-info` on `GIT_INDEX_FILE` re-adds the flagged entries. They keep their mode, OID and stage, lose the flag bits, and get an empty stat cache, so `add -A` rehashes only those paths. That includes same-size, same-mtime edits. The write runs with `-c core.ignorestat=false`, because `core.ignorestat` would otherwise mark the re-added entries assume-unchanged.
- **Racy check.** The conservative index timestamp is restored after `update-index`, so the racy-git check from https://git.cafe/pingdotgg/t3code/pulls/10792 still applies.
- **Unchanged.** Ordinary out-of-cone `S` bits are left alone (https://git.cafe/pingdotgg/t3code/pulls/12154). The real index and Git config are never touched.
- **Fallback.** Capture still falls back to the fresh index on an invalid UTF-8 path, a flagged gitlink, an `s` tag on a sparse checkout (`--index-info` cannot drop assume-unchanged while keeping skip-worktree), more than 1 MiB of flagged records, or any command failure.

## Scope and approval

Fixes #13741
GitCafe: https://git.cafe/pingdotgg/t3code/issues/13741
Triaged bug via the automated issue flow. The maintainer's triage comment on the issue sets this direction: selectively rebuild flagged entries with `--index-info`, restore the timestamp afterwards, and keep the fallback.

## Verification

- `GitVcsDriver.test.ts`, `checkpoint index inspection handles entries beyond the output cap`:
  - The `flags` case is split into `index=skip-worktree` and `index=assume-unchanged`.
  - Every mode now asserts that no fresh `read-tree` runs and that the unflagged file is not rehashed.
  - The flagged modes also assert that a same-size, same-mtime edit and a deletion of a flagged file are captured, and that the real index stays byte-identical.
  - On `main` both new cases fail with `expected 1 to equal +0` (one fresh `read-tree`). They pass on this branch.
  - The flagged modes also set `core.ignorestat true`. They fail without the `-c core.ignorestat=false` override and pass with it.
- `sparse checkpoint preserves two captures` now counts fresh `read-tree` calls. The `flags` and `manual-skip` cases (in-cone manual flags on a cone sparse checkout) fail on `main` and pass here.
- `vp test run apps/server/src/vcs/GitVcsDriver.test.ts apps/server/src/checkpointing`: 70/70 pass with `GIT_CONFIG_GLOBAL=/dev/null`. On the test VM, `lists the fetch and push URLs of a partial clone remote` fails on `main` and here alike, because of a global `url.insteadof` rewrite on that VM.
- `vp run check:changed`: format, lint, knip, graphql and typecheck pass.
- Reviewers also checked these cases by hand with the real capture:
  - a flagged entry staged differently from HEAD
  - split index
  - cone sparse checkouts, with and without a sparse index
  - non-cone sparse checkout
  - flagged paths containing a newline, a tab, a quote or non-ASCII characters
  - a flagged gitlink
  - invalid UTF-8 paths

  In every case the snapshot had the working-tree content and the real index stayed byte-identical.
- Not checked: Windows or macOS at runtime, and Git versions older than 2.43. The change has no platform-specific code.

Server-only fix with no visible surface. The proof is the failing tests above plus the runtime table.

Claude Opus 5.5 (implementation, tests), Claude Sonnet 5.5 and Claude Opus 5.5 (review), Claude Code in T3 Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


---

## Independent verification (maintainer takeover, head `cfee52fc24`)

**Reviews** (fresh, read-only, full diff against `main`):
- Claude Sonnet 5.5: `VERDICT: MERGE`. No case found where a change goes missing. One low note: an unflagged file whose size, mtime and ctime all stay unchanged is trusted by its stat data. That is the same trust `main` gives a repo with no flags, and it can only happen with a forged mtime and `core.trustctime=false`.
- Claude Opus 5.5: `VERDICT: MERGE`. No defects found. The real index and `sharedindex.*` stayed byte-identical in every fixture.

**Proof audit:** `VERDICT: SUFFICIENT`. Audit pass 1 ([matrix.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/3d307ef1d2114f84/matrix.md)) found 16 cases the existing tests don't cover. Each was captured with the real `GitVcsDriver` capture and restore on synthetic repos, before (`main` 8d3b061121) and after (`cfee52fc24`), with git 2.43.0 on Linux x86_64. Audit pass 2 ([audit2.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/6864591a8fd33dd6/audit2.md), [rerun.log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/592dd10606b39c00/rerun.log)) checked every record against its table row, checked the SHAs and the driver hash, and independently reran R11.

- Driver: [ev23624.driver.test.ts](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/255b9e4524dad92c/ev23624.driver.test.ts), sha256 `95c72e8b34420ee159a4e39dc49fdaab52a7b19aa41bdf4368092ceca0f732c4`
- Results: [captures.md](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/973f20d5a7706bda/captures.md) · [results-before.jsonl](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e1724d8ded45374b/results-before.jsonl) · [results-after.jsonl](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/c7d721b4777464f2/results-after.jsonl) · [run-before.log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/addcdf46939e8158/run-before.log) · [run-after.log](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e0869f5fb943bdf1/run-after.log)

"Tree" compares the checkpoint tree with the working tree. "Index" is the real `.git/index` sha256 before and after capture.

| row | case | `main` | this PR | real index |
|---|---|---|---|---|
| R1b, R11 | cone sparse (with / without sparse index), same-size edit, assume-unchanged | tree matches (full rebuild) | tree matches (copied index kept) | unchanged |
| R1b, R11, R12, R19 | cone and non-cone sparse, *present* in-cone file flagged skip-worktree by hand, edit keeps size, mtime and ctime | **edit missing** | **edit missing**, same as `main` | unchanged |
| R2a / R2b | `chmod +x` on a flagged / unflagged file | tree matches | tree matches | unchanged |
| R4 | flagged file renamed | tree matches | tree matches | unchanged |
| R5 | flagged entry staged ≠ HEAD ≠ working tree | tree matches | tree matches | unchanged |
| R9 | split index | tree matches | tree matches | unchanged, `sharedindex.*` unchanged |
| R12 | non-cone sparse, assume-unchanged | **capture fails** | tree matches | unchanged |
| R13 / R15 / R16 | flagged gitlink / invalid UTF-8 path / >1 MiB of flagged records | tree matches | tree matches (full rebuild, by design) | unchanged |
| R14 | flagged paths with a newline, tab, quote or non-ASCII characters | tree matches | tree matches | unchanged |
| R17 | `s` (both bits) on sparse and non-sparse checkouts | tree matches | tree matches | unchanged |
| R19 | restore from a new-path checkpoint, non-sparse; cone sparse with assume-unchanged | restore matches | restore matches | unchanged |
| R21 | racy-timestamp file next to a flagged file | tree matches | tree matches | unchanged |

R20 measures capture on 25,000 files with one edited flagged file. The per-file clean filter here is slower than the one behind the table above.

| | no flags | skip-worktree | assume-unchanged |
|---|---|---|---|
| `main` | 342–472 ms | 138.7–171.3 s, 25,001 rehashed | 154.5 s to over 180 s, 25,001 rehashed |
| this PR | 294–380 ms | 358–455 ms, 1–2 reads | 400–630 ms, 1–2 reads |

"2 reads" means `add -A` reads the same edited file twice: once to compare it, once to stage it. It is not a second file. No unflagged file was rehashed on this branch.

**Corrections to the description above.** These are notes only; the author's text is left unchanged.
- The bullet "cone sparse checkouts, with and without a sparse index … the snapshot had the working-tree content" does not hold in one case. Take an in-cone file that is still present, marked skip-worktree by hand, with `sparse.expectFilesOutsideOfPatterns=true`. Capture reads the private index with `sparse.expectFilesOutsideOfPatterns=false`, which clears that `S` bit. The entry then keeps its old stat data, so an edit that leaves size, mtime and ctime unchanged (for example under `core.trustctime=false`) is missed.
  - This is **not a regression**: `main` misses the same edit the same way.
  - It holds for assume-unchanged entries and for deleted in-cone skip-worktree files.
  - It is a narrow pre-existing gap, worth a follow-up.
- An `s` tag on a cone sparse checkout reuses the copied index rather than taking the fallback, because Git clears the in-cone `S` first. The capture is still correct (R17).

<details><summary>Verification log</summary>

- Unverified: Windows, macOS, SMB and other network filesystems (the reporter's setup), and git versions other than 2.43.0.
- The R2 rows set `core.filemode=true` explicitly, because the test setup forces `core.filemode=false`.
- The R20 run of `main` with old mtimes and assume-unchanged was killed by vitest's 180 s timeout, so it has no record.
- Every fixture is synthetic, run with `GIT_CONFIG_GLOBAL=/dev/null` on Linux 6.1.102 x86_64.
- Model/harness: Claude Opus 5.5 (lead, audits, capture) and Claude Sonnet 5.5 (review A), Claude Code in T3 Code.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

