Skip to content

Add gap fixes for temporary index plumbing, 3-way merge, and intent-t… - #1486

Open
czha168 wants to merge 1 commit into
libgit2:masterfrom
czha168:master
Open

Add gap fixes for temporary index plumbing, 3-way merge, and intent-t…#1486
czha168 wants to merge 1 commit into
libgit2:masterfrom
czha168:master

Conversation

@czha168

@czha168 czha168 commented Sep 1, 2026

Copy link
Copy Markdown

…o-add

  • Gap 1: Add Repository.set_index() to swap repo's index with a temporary Index object via git_repository_set_index(). This enables atomic operations on temp indices without modifying the working tree.

  • Gap 2: Add Repository.merge_diff() for 3-way merge support. Accepts theirs and ancestor trees and performs git_merge_trees() internally, returning an Index with merged content and optional conflicts.

  • Gap 3: Add Index.add_intent() for intent-to-add entries (null OID) like git add -N. Creates a valid entry then zeroes the OID bytes via FFI.

  • Add test_gap_fixes.py with 8 tests covering all three fixes.

  • Add docs/gap-fix-plan.md documenting the investigation and fixes.

Assisted-by: Claude Sonnet 4.6

…o-add

- Gap 1: Add Repository.set_index() to swap repo's index with a
  temporary Index object via git_repository_set_index(). This enables
  atomic operations on temp indices without modifying the working tree.

- Gap 2: Add Repository.merge_diff() for 3-way merge support. Accepts
  theirs and ancestor trees and performs git_merge_trees() internally,
  returning an Index with merged content and optional conflicts.

- Gap 3: Add Index.add_intent() for intent-to-add entries (null OID)
  like git add -N. Creates a valid entry then zeroes the OID bytes
  via FFI.

- Add test_gap_fixes.py with 8 tests covering all three fixes.

- Add docs/gap-fix-plan.md documenting the investigation and fixes.

Assisted-by: Claude Sonnet 4.6
@jdavid

jdavid commented Sep 3, 2026

Copy link
Copy Markdown
Member

Below a review by Kimi K2.7 Coding Highspeed model.
I will do a personal review before merging, but first please handle the feedback from Kimi. Thanks!


Overall assessment

The PR currently needs substantial revision before it can be merged. Two of the three proposed additions (set_index, merge_diff) have serious correctness or redundancy problems, and the new docs/gap-fix-plan.md file should not be part of the upstream repository.

Repository.set_index() — has an ownership/lifetime bug

pygit2/repository.py:1614

The implementation simply passes index._index to git_repository_set_index(). In libgit2, git_repository_set_index() takes ownership of the index (it frees the repo’s old index and stores the new pointer without incrementing a reference count). The Python Index object also believes it owns that same pointer and will call git_index_free() from Index.del. This creates a double-free / use-after-free once the temporary Index is garbage collected.

Example of the hazard in test/test_gap_fixes.py:78:

  original_index = testrepo.index
  testrepo.set_index(temp_index)
  ...
  testrepo.set_index(original_index)

After the first set_index(temp_index), libgit2 frees the repository’s original index. The Python object original_index still holds a pointer to that freed index; passing it back to set_index() is UB. The tests happen to pass because the memory is still mapped, but this is not safe.

To fix this, set_index() must either:

• be implemented in the C extension where the Index object can be detached/invalidated after ownership transfer, or
• in Python, after git_repository_set_index() succeeds, mark the Index object as no longer owning the pointer and guard Index.del against calling git_index_free(NULL).

Also note: the plan document describes C extension changes in src/repository.c/src/repository.h, but the actual PR only adds a CFFI declaration in
pygit2/decl/repository.h. The implementation is entirely Python/CFFI, which is fine, but the document is misleading.

Repository.merge_diff() — redundant and lower-quality

pygit2/repository.py:1641

This duplicates the existing Repository.merge_trees() method (pygit2/repository.py:956), which already performs a 3-way tree merge and returns an Index. merge_diff() is less flexible: ours is hard-coded to self.head.peel().tree, it only accepts Tree objects (not Oid/str), and it does not expose the MergeFlag options.

Additional implementation issues:

• The parameter is named flags but typed MergeFileFlag; the existing convention is flags: MergeFlag and file_flags: MergeFileFlag.
• It only assigns opts.file_flags, never opts.flags.
• _merge_trees() is declared @classmethod but called as an instance method, so cls receives the instance. It works by accident because Index.from_c() ignores cls, but it is confusing.
• merge_diff() uses a fixed GIT_MERGE_OPTIONS_VERSION init and does not reuse the existing _merge_options() helper, so argument validation is inconsistent with the rest of the module.

Recommendation: remove merge_diff() entirely and use the existing merge_trees(ancestor, ours, theirs, favor=..., flags=..., file_flags=...).

Index.add_intent() — works but needs polish

pygit2/index.py:230

The two-step approach (add() the file, then zero the OID via the returned git_index_entry *) is pragmatic because git_index_add() rejects a null OID. It correctly produces intent-to-add entries.

Requested changes:

• Accept str | PathLike[str], not just str, to match Index.add().
• Use the IndexEntry / Oid / FileMode imports already available in the module rather than hand-rolling null bytes if you refactor it later.
• The hard-coded 20 bytes for the OID is fine today but fragile if SHA-256 support ever arrives; prefer ffi.sizeof(centry.id).

docs/gap-fix-plan.md — should not be merged

This is an internal planning document that references “Athena’s PR generation workflow”, athena/pr_generator/branch.py, etc. It does not belong in the upstream pygit2 repository. Remove it before merging.

Tests — needs restructuring and fixes

test/test_gap_fixes.py

• The new tests should be split into the existing test/test_repository.py, test/test_index.py, and test/test_merge.py rather than adding a separate file named after the PR’s internal gaps.
• Two theirs_tree / ours_tree constructions are buggy:
• test_merge_diff_basic line ~153: testrepo[testrepo.TreeBuilder().write()] creates an empty tree instead of testrepo[theirs_builder.write()].
• test_merge_diff_no_conflict line ~215: testrepo[testrepo.TreeBuilder().write()] creates an empty tree instead of testrepo[ours_builder.write()].
• Unused variables: sig in test_merge_diff_basic and test_merge_diff_with_conflict; ours_tree in test_merge_diff_with_conflict and test_merge_diff_no_conflict.
• ruff check and ruff format --diff report trailing whitespace and formatting issues in the new file. CI will fail on these.

Suggested path forward

  1. Remove docs/gap-fix-plan.md.
  2. Remove Repository.merge_diff() and its helper; document that callers should use Repository.merge_trees().
  3. Either properly implement Repository.set_index() with ownership transfer in the C extension, or remove it and rely on the existing Index.write_tree(repo) workflow.
  4. Move the surviving tests into the existing test files and fix the empty-tree bugs, unused variables, and formatting issues.
  5. Run ruff format ., ruff check ., and the full test suite before updating the PR.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants