feat: answer repository branch drift with a row per branch [IFC-3147]
Why
Infrahub knows which commit it has imported for a repository on each branch, but until now there was
no way to ask for that per branch. InfrahubRepositoryBranchDrift was published with the contract in
#10566, and it answered with no rows at all, because the resolver had no per-branch value to put in
one.
This pull request gives it those rows.
Background on the larger effort (IFC-3101). The feature makes a repository's Git state visible in
the UI: the commit log of a repository, and how far each branch has drifted from its remote. It is
being built in slices. #10566 published the API shape so the frontend could start. This slice fills
in the branch rows. Later slices add the worker that reads the real Git clone, which is what turns
"unavailable" into an actual comparison against the remote.
Non-goals. Nothing here talks to Git. Every row still reports its comparison with the remote as
unavailable, because no worker reads a clone yet. remote_head stays null.
Part of IFC-3147, in the IFC-3101 epic.
What changed
What a user sees. InfrahubRepositoryBranchDrift now returns one row per branch, each carrying
the ref that branch follows and the commit it has imported. A branch that tracks nothing reports
NOT_TRACKED. Branches that are on their way out (merged or deleting) and the internal global branch
get no row. For a read-write repository, only branches synchronised with Git appear; for a read-only
repository, every branch does.
How the values are read. A new RepositoryBranchValuesQuery resolves the values for every branch
in one database query, not one query per branch. It reproduces the same rule a normal read uses:
a branch sees its own value if it has one, otherwise it inherits the value its origin branch had when
the branch was created.
Two design points worth a reviewer's attention.
-
BranchScopecarries a list of read windows rather than a branch list and two timestamps. A
branch reads its origin branch at the point it was created, and reads itself at the current time.
Those are two different windows. Flattening them into one list at one time would either break
branch isolation or let a branch see its own edges at the wrong time.BranchScope.from_branch
copies the windows straight out ofBranch.get_branches_and_times_to_query_global, so the rule
stays owned byBranchand is not restated here. -
The repository kind is checked in exactly one place,
_drift_read. It returns the branches to
report, the attributes to read, and whether the ref comes from each branch. Checking the kind in
several places would let a future third kind be handled correctly in some of them and silently
wrongly in the others.
A bug fixed along the way. The resolver used the current time instead of the time the request
asked for, so a query with ?at= answered as of now. It now passes graphql_context.at, which every
other query resolver in this package already did. There is a test that fails without the fix.
What did not change. No schema change and no migration. get_repositories_commit_per_branch is
untouched. The published GraphQL contract is unchanged, so schema/schema.graphql regenerates
identically.
Suggested review order
backend/infrahub/core/query/repository.py- the new query, and the only new Cypher.backend/tests/component/core/query/test_repository_branch_values.py- how the inheritance rule
is meant to behave. Reading this before the resolver helps.backend/infrahub/graphql/queries/repository_git_state.py- the resolver, which is now a
consumer of the query above.- The remaining test and documentation changes.
How to review
Focus on the Cypher in RepositoryBranchValuesQuery, especially the window predicate and the two
edge elections. That is where a mistake would be quiet: a wrong window gives a plausible but wrong
commit rather than an error.
Please check the deferred work is acceptable (see dev/specs/ifc-3101-repo-commit-visibility/tasks.md):
- T101 - PR #10606 adds the same new file,
backend/infrahub/core/query/repository.py, with an
equivalent query built for the neighbouring epic. Neither branch can see the other: #10606 sits on
an epic branch whose own pull request still has open children, so waiting for it would hold this
work behind two unrelated frontend pull requests. Both branches therefore add the file, and
whichever merges second resolves an add/add conflict. T101 records how to resolve it and what has
to be reconciled first. The two agree on every branch that can exist today. - T097 - the scope agreement with the neighbouring epic's owner. It was written to happen before
this pull request merged, and is deferred, because the other side has already built its half. It is
now the same conversation as T101.
Alternative considered: adopting #10606's query instead of adding this one. Rejected for the
timing reason above, not on merit.
How to test
INFRAHUB_USE_TEST_CONTAINERS=true uv run pytest \
backend/tests/component/core/query/ \
backend/tests/component/graphql/queries/test_repository_git_state.py -q
34 tests. The wider sweep used while developing this (backend/tests/component/core/query/ plus all
of backend/tests/component/graphql/) is 975 tests and also passes.
uv run invoke lint is clean (ruff, ty, mypy), and uv run invoke schema.generate-graphqlschema
produces no diff.
Impact & rollout
- Backward compatibility: no breaking change. The query returned an empty row list before and
returns rows now, which is what the published contract always described. - Performance: one database query per request, regardless of branch count. Measured on a
worst-case fixture where every branch has imported its own distinct commit: 500 branches takes
148 ms, 2000 branches takes 998 ms. Cost grows with the number of branches only; the query
reads a single repository, so the number of repositories does not affect it. - Config/env changes: none.
- Deployment notes: safe to deploy. No coordinated release needed.
Checklist
- Tests added/updated
- Changelog entry added (the existing IFC-3101 fragment is updated; it previously said this query
returns no rows) - External docs updated (if user-facing or ops-facing change)
- Internal .md docs updated (internal knowledge and AI code tools knowledge) โ nothing under
dev/knowledge/or.agents/needed a change.dev/knowledge/backend/git-sync.mdalready records
thatsync_with_gitalone does not answer whether a branch is imported. The spec edit under
dev/specs/is tracked by its own commit. - I have reviewed AI generated content