Reset a rewritten branch during the sync [IFC-3281]

Merged
GM
1mo ago
+1732-209

Why

A developer rebases a tracked branch and force-pushes it. The next sync failed with "there are
conflicts that must be resolved". No conflict existed, and the branch stayed on its old commit for
ever.

Second item of IFC-3210, and the one that fixes the reported bug. It builds on #10829, which is
merged into develop.

Fixes #6299

What changed

  • The sync collector compares each branch twice. The commit in the graph against the remote head
    gives the classification. The worktree of this worker against the remote head decides the reset.
  • Every move of a worktree is a hard reset onto the remote head that the cycle classified. A
    worktree behind the remote fast-forwards. A worktree that does not lead to the remote head loses
    its commits. The commit is then recorded and the branch imported again.
  • The graph commits are still read once per cycle in sync_remote_repositories. They now go down
    through both sync flows and RepositorySyncer.sync to the collector, for the branches that can
    still record a commit.
  • The collector also selects the local branches whose graph commit differs from the remote head.
    A worktree already on the remote head is reset onto the same commit, because pull would return
    before it records anything.
  • A branch new to this worker is classified too. The cron runs on any worker, and the graph can hold
    a commit that another worker imported.
  • Each reset or lineage break logs the branch, the discarded commit and the new commit.
  • A diverged pull names a diverged history. Only "unmerged files" keeps the conflict message.
  • git_branch_create writes the commit of the new branch. When that write fails, it logs a warning
    and still sends the broadcast.

Not in this PR: the reset in pull, the wider broadcast, the stored record and the trunk signal.
target_changed stays false until IFC-3286. T019 stays open, because the per-branch read must still
report an inherited trunk commit as absent.

Most of this diff is tests

About two thirds of the added lines are tests. The code change is about 400 lines,
most of them in backend/infrahub/git/repository.py. The rest is internal docs and the spec set.

  • tests/unit/git/test_branch_reconciliation.py (444 lines) runs the collector against a local git
    remote and a recording SDK client, with no database. Each test holds one row of the reset contract
    in contracts/internal-interfaces.md section 1, plus the failure cases.
  • The live-remote tests and their Gogs helpers (269 lines) run whole sync cycles against a remote: a
    force-pushed branch, a fast-forward, a graph that lags behind, and a branch created in Infrahub.
  • The rest covers the branch-create broadcast, the branch filter, the error messages, and existing
    tests that the change touched.

So the code is the smaller part to read. The tests mostly restate the contract table, one case each.

How to review

Start with _advance_branch and _queue_advanced_branch in backend/infrahub/git/repository.py.
Three points need attention:

  • The collector reads the branch listing when something moved, for an active repository or a
    staging trunk, and takes the branch ids from it. An updated branch that Infrahub no longer lists is
    skipped.
  • On a staging repository, the reconciled entry names the branch the trunk maps onto, not the
    staging branch. The later broadcast and recorder read it that way.
  • The first cycle after deploy imports once more each branch whose graph commit is an inherited
    trunk commit that differs from its head.

How to test

GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null uv run pytest backend/tests/unit/git/
uv run pytest backend/tests/component/git/ --neo4j
INFRAHUB_USE_TEST_CONTAINERS=1 uv run pytest backend/tests/integration/git/test_git_live_remote.py backend/tests/integration/git/test_repository.py --neo4j

The collector tests fail when the reset, the graph comparison or the classification of new branches
is reverted. The branch-create tests fail when the commit write or the caught error is reverted.

Checklist

  • Tests added
  • Changelog entry
  • Internal docs updated (git-integration.md, git-sync.md, spec set)
  • I have reviewed AI generated content