Skip to content

Colocated index write leaves a stale gix cache-tree; external git add produces duplicateEntries #9711

Description

@petejm

Description

In a colocated jj+git repo, a jj snapshot writes .git/index carrying the git cache-tree (TREE extension) it loaded earlier, without invalidating it after mutating entries. The written index is self-inconsistent: a subtree node is marked valid but its cached entry_count under-counts the entries it spans.

git write-tree directly over that index is clean (git trusts a node marked valid). But a later git add that invalidates only the root — e.g. staging an unrelated sibling path, or a commit hook touching another dir — forces git to rebuild the cache-tree, trust the stale child count, and emit the same subtree entry twice → git fsck reports duplicateEntries and the push is rejected.

Root cause: GitoxideLabs/gitoxide#2421 (gix doesn't invalidate the cache-tree on write); jj-side fix in the linked PR.

Steps to Reproduce the Problem

Deterministic by construction — it forces git write-tree immediately after the sibling git add, removing the hook-timing dependence that made the original end-to-end path intermittent. Trips at iteration 1 on stock jj.

#!/usr/bin/env bash
set -eu
R="$(mktemp -d)"; cd "$R"
git init -q .
git config user.name r; git config user.email r@e
jj --config user.name=r --config user.email=r@e git init --colocate >/dev/null 2>&1
mkdir -p d/e/f sib
: > d/e/f/w0.txt; : > d/e/f/w1.txt; : > d/e/y.txt; : > sib/seed.txt
git add d/e/f/w0.txt d/e/f/w1.txt d/e/y.txt sib/seed.txt
git write-tree >/dev/null            # bake a valid, fully-populated cache-tree into .git/index
: > d/e/f/w2.txt                      # new file INSIDE the existing subtree
jj --config user.name=r --config user.email=r@e status >/dev/null 2>&1   # snapshot
: > sib/g.txt                         # unrelated sibling -> invalidates ROOT only
git add sib/g.txt
git write-tree >/dev/null            # rebuild trusts the stale subtree count
git fsck --no-dangling --no-reflogs  # -> error ... duplicateEntries

Expected Behavior

After a jj snapshot, .git/index is consistent: a later external git add + git write-tree produces a valid tree, and git fsck is clean.

Actual Behavior

git fsck reports error in tree <oid>: duplicateEntries: contains duplicate file entries; the rebuilt root tree lists the affected top-level directory twice with the same subtree oid. The push is rejected.

Specifications

  • Platform: macOS arm64, git 2.50.1
  • Version: jj 0.42.0

The mechanism is OS-independent — git's cache-tree rebuild (cache-tree.c) and gix's TREE-extension serialization are not platform-specific; the deterministic repro above was run on macOS arm64.


Possibly the same corruption reported in #8884, but via a different mechanism: #8884 theorizes jj's tree_merge.rs merge path; this is a colocated index-write defect and reproduces deterministically on 0.42. #8884's reporter was on 0.38 (untested here), so I can't rule out that they hit this same path — framing it as a distinct mechanism, not a different bug. Cross-ref gitoxide root cause: GitoxideLabs/gitoxide#2421

Activity

  1. added a commit that references this issue on Jun 27, 2026
    0c5970d
  2. added 2 commits that reference this issue on Jun 27, 2026
    8488515
    c8c344e
  3. added 2 commits that reference this issue on Aug 10, 2026
    8c6364f
    5bd306b
  4. martinvonz commented on Aug 11, 2026

    @martinvonz
    Contributor

    It seems likely that this is the same issue as #8884. I would rather close one of them as a duplicate than leave one of them open after your #9712.

  5. added a commit that references this issue on Aug 15, 2026
    97e70ab
  6. petejm commented on Aug 15, 2026

    @petejm
    Author

    Disclosure: posted by an AI agent (Claude) on behalf of @petejm, who reviewed and approved this comment.

    Agreed that both should not stay open. My suggestion is to close #8884 as the duplicate and let #9712 close this one, but the call is yours, and I want to be explicit about what I did and did not verify.

    Why they look like the same bug: the corruption in #8884 is doubled subtree entries, with the same hash on both rows.

    040000 tree c3a484a7a4c1d2e2c0f03311eb2a74e3c266a9df    internal
    040000 tree c3a484a7a4c1d2e2c0f03311eb2a74e3c266a9df    internal
    

    That is the signature this mechanism produces: git rebuilds the cache-tree, trusts a stale child entry_count, and emits the subtree entry twice. A generic duplicateEntries report could be several different things, but doubled subtree rows with matching hashes is specific to this one.

    Why I would keep this issue rather than #8884: this one carries the deterministic reproducer, which trips at iteration 1 on stock jj, plus the mechanism write up. #8884 says "Exact steps are unknown" and has had no reporter activity since 2026-02-15. #9712 already carries Fixes #9711, so this issue closes on merge either way.

    What I have not verified: whether the stale cache-tree was the only source of corruption in that repo. #8884 has no steps to reproduce, so I could not confirm it from the report itself. That is why #9712 links #8884 in the CHANGELOG rather than adding a Fixes: trailer for it. If the symptom recurs on a build containing this fix, that would be a genuinely different cause and worth a fresh issue rather than reopening.

    If you would rather do it the other way round, closing this one as a duplicate of the older report, that works too. Tell me which you prefer and I will take care of it.

  7. added a commit that references this issue on Aug 25, 2026
    b647d4b
  8. added a commit that references this issue on Aug 25, 2026
    8ef9bee
  9. petejm commented on Aug 25, 2026

    @petejm
    Author

    Disclosure: posted by an AI agent (Claude) on behalf of @petejm, who reviewed and approved this comment.

    Closing this as fixed by #9712, which merged in 8ef9bee.

    The commit carries a Fixes #9711 trailer, but the auto-close did not fire when it landed through the merge queue, so this stayed open. Closing it by hand.

    For anyone arriving here from #8884: the jj side fix is a State::remove_tree() before the colocated index write, in update_intent_to_add_impl(). The gitoxide root cause is GitoxideLabs/gitoxide#2421, which was closed in July as documented rather than fixed, so the contract still stands: remove the tree cache before writing whenever entries changed. If duplicateEntries reappears on a build containing 8ef9bee, that is a different cause and worth a fresh issue rather than reopening this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    🐛bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions