parse_git_apply_output mis-attributes paths via BTreeSet::next_back()
What
parse_git_apply_output in codex-rs/git-utils/src/apply.rs classifies each path from git apply into applied / skipped / conflicted BTreeSets and tracks last_seen_path. After inserting a path it recovers "the path just inserted" with <set>.iter().next_back().cloned() (6 sites: lines ~459, 471, 483, 497, 550, 564).
next_back() returns the lexicographically greatest element of the set, not the element just inserted. Once a set holds more than one path, the cross-set remove() reconciliation and last_seen_path are applied to the wrong file — so a later THREE_WAY_FAILED / LACKS_BLOB skip can be charged to the wrong path, and applied/skipped/conflicted classification gets swapped.
Repro
Input (paths chosen so the lexicographically-greatest is inserted first):
Applied patch zzz.txt cleanly.
Applied patch aaa.txt cleanly.
Failed to perform three-way merge...
Expected: applied == ["zzz.txt"], skipped == ["aaa.txt"].
Actual: the three-way-merge failure is attributed to zzz.txt (the set max), so zzz.txt is de-applied and aaa.txt is wrongly kept as applied.
Root cause
The local add(set, raw) helper inserts but returns nothing, and callers reach back into the BTreeSet with next_back() to guess what was added. BTreeSet is ordered, so that guess is the max, not the insertion.
Suggested fix (outline)
Have the add helper return the inserted (unescaped) key, and use that returned value at every call site instead of next_back(). I tried this locally: it is a minimal change, keeps clippy (collapsible_if) and cargo fmt clean, and a regression test built from the repro above goes from failing to passing (cargo test -p codex-git-utils: 23 passed). Happy to open a PR with the fix + test if you would like — flagging via an issue first per the contributing guidelines.
3 Comments
I checked current
main(80f54d1) and this is still present incodex-rs/git-utils/src/apply.rs.The important owner path is exactly the one described here:
parse_git_apply_output()inserts intoBTreeSets and then recovers a path withiter().next_back()at multiple sites. Sincenext_back()returns the lexicographically greatest path, not the path just inserted, later reconciliation can remove/update the wrong path once the set has more than one entry.The smallest fix shape looks low-risk:
add(set, raw)to returnOption<String>for the normalized/unescaped inserted keynext_back()siteFailed to perform three-way merge...Suggested targeted validation if this gets picked up:
Independently reproduced and root-caused on current
main(ccdfb4f), with run-verified RED→GREEN and a concrete patch below. Confirming the analysis above.Reproduced (RED) on
mainAdded a regression test to
codex-rs/git-utils/src/apply.rs'smod testsusing the repro from the issue:cargo test -p codex-git-utilson unmodifiedmain:After the two
Applied patch ... cleanly.lines,applied = {aaa.txt, zzz.txt}; theadd→applied.iter().next_back().cloned()recovery picks the set max (zzz.txt) as the just-touched path, solast_seen_pathbecomeszzz.txt. The trailingTHREE_WAY_FAILEDline (apply.rs:526) then charges the skip tozzz.txt— de-applying it and wrongly leavingaaa.txtapplied. Buckets swapped, exactly as reported.Fix
Make
addreturn the key it inserted, and use that return value at all 6 call sites instead ofnext_back():Each call site (lines ~459/471/483/497/550/564) collapses from
add(...); let p = set.iter().next_back().cloned(); if let Some(p) = p { ... }to:(let-chains keep clippy's
collapsible_ifhappy.)GREEN
cargo clippy -p codex-git-utils --libis clean.I have the full patch ready and would be happy to open a PR per
docs/contributing.md(external contributions are by invitation) if a maintainer would like to invite one.Root-cause confirmation + minimal fix (fork branch ready)
Confirmed on current
mainincodex-rs/git-utils/src/apply.rs: afteradd(&mut set, path), several sites recover the inserted path with:BTreeSet::iter().next_back()is the lexicographic maximum, not the just-inserted element. With ≥2 paths,last_seen_pathand cross-setremove()hit the wrong file.Minimal repro (parser unit)
applied=["aaa.txt"],skipped=["zzz.txt"](max path wrongly de-applied)applied=["zzz.txt"],skipped=["aaa.txt"]Proposed fix
Make local
add()returnOption<String>of the inserted (unquoted) path and use that return value at every formernext_back()call site. No API surface change.Validation
Branch / compare (cannot open PR: invitation-only)
Happy to open a PR if a Codex team member invites a contribution for #30534.