6.5.2 Comparing Bazel Graphs Across Revisions

A file diff tells you where text changed. It does not tell you which requested Bazel targets may observe that change. To answer the second question, compare two explicitly identified views of the build graph and preserve the path from changed evidence to each selected root. This turns revision comparison into a reproducible explanation rather than a guess based on directory names.

Compare Two Snapshots of the Same Universe

The contract from 6.5.1 Affected-Target Service Contract names the base revision, head revision, requested roots, and supported configurations. Revision comparison implements that contract by producing one snapshot for each repository state:

base revision ──► graph snapshot A ─┐
                                    ├─► comparison ─► affected roots + explanations
head revision ──► graph snapshot B ─┘

The two snapshots must describe the same universe. If snapshot A contains //app/... under one set of flags while snapshot B contains the whole repository under another, a difference between them is ambiguous: it may come from the source change, the universe change, or both.

Record enough identity with each snapshot to reject such a comparison. At minimum, that includes:

  • the immutable repository revision;
  • the requested target universe;
  • whether the snapshot represents the unconfigured target graph or configured targets;
  • the configuration, platform, and toolchain dimensions the mode claims to capture;
  • the Bazel binary/version, effective startup, rc, and command options, plus any declared environment or repository inputs the selector claims to capture; and
  • the comparison implementation and schema version.

If a required identity dimension is missing or incompatible, reject the comparison or return unknown; do not reuse a precise-looking answer from another environment. This is why a cached head snapshot alone is insufficient. A removed target, edge, or source may no longer exist in the head graph, yet its former consumers are part of the evidence needed to explain impact. The dedicated completeness checks for deletes, renames, and stale data appear in 6.5.5 Incomplete Selector Evidence; the comparison model here begins by retaining both sides.

Choose Evidence That Matches the Question

There is no single mandatory representation for a revision snapshot. A selector can combine several kinds of evidence:

EvidenceWhat it contributesWhat it cannot establish alone
file-to-target ownershipconnects a changed source to targets that consume ittransitive impact on requested roots
reverse dependenciestraces consumers from a changed target toward rootseffects not represented by the chosen graph
target attributes and edgesdetects changes to declared target structureconfiguration-specific edges when using an unconfigured graph
configured-target datarepresents one analyzed configuration more preciselyconfigurations and roots that were never analyzed
recursive fingerprintscondenses a node's own data and dependency fingerprintscompleteness beyond the inputs included by that algorithm
generated metadatacarries organization-specific ownership or invalidation evidencecorrectness unless its production and freshness are part of the contract

query and cquery therefore support different comparison claims. query supplies an unconfigured graph; cquery observes configured targets for particular top-level requests and configurations. bazel-diff supports both modes because their precision and cost differ.1 5.2.7 Query in CI Pipelines develops how to choose query evidence in CI, while 6.5.4 Selecting Affected Targets Across Configurations handles the completeness problem when more than one configuration must be protected.

A fingerprint is useful as a change detector, not as a universal proof of affectedness. bazel-diff's workflow generates hashes for the graph at two revisions and compares them to emit impacted targets.2 The durable model is broader than that implementation: a fingerprint is trustworthy only for the node data, dependency edges, external inputs, and configuration identity that its algorithm actually includes.

Configured-target comparison is another concrete implementation shape: Target Determinator compares configured-target information across Git revisions.3 Its output does not make unexamined roots or configurations part of the evidence; the same declared-universe rule still applies.

Trace Impact from Evidence to Requested Roots

Consider this simplified base graph:

//lib:parser  ──►  //app:binary
      │
      └─────────►  //app:parser_test

Suppose parser.cc changes and the ownership mapping associates it with //lib:parser. The selector first establishes the directly changed target. It then follows reverse consumers until it reaches roots in the request's universe. If the requested roots are //app:binary and //app:parser_test, both are selected.

The explanation should preserve that reasoning in an inspectable form:

{
  "root": "//app:parser_test",
  "reason": "reverse dependency of changed target",
  "path": ["//lib:parser", "//app:parser_test"],
  "base_revision": "<immutable-base>",
  "head_revision": "<immutable-head>"
}

This is an interface sketch, not a required schema. A fingerprint-based implementation may instead show that //lib:parser changed fingerprint and that the dependency contribution changed the test's fingerprint. What matters is that the explanation identifies the comparison evidence and connects it to a requested root.

An omitted root needs an explanation too. Useful omission reasons include:

  • the root was compared and no changed evidence reached it;
  • the label was outside the declared universe;
  • the required configuration was not represented;
  • ownership or snapshot evidence was missing, so the overall result is unknown rather than unaffected.

Without those distinctions, “not returned” conflates a negative result, an out-of-scope label, and a failed comparison.

Separate Graph Change from File Change

A reliable comparison proceeds in layers:

  1. Identify changed repository evidence on both sides of the revision boundary.
  2. Map files or metadata to the targets or graph inputs they influence.
  3. Compare the relevant target or configured-target representation.
  4. Propagate differences through reverse consumers to the requested roots.
  5. Emit the selected roots together with the snapshot identities and explanations.

The first layer cannot substitute for the rest. A source file may affect several owners, a changed dependency can affect consumers far outside its directory, and a documentation file may affect no modeled build target. Conversely, BUILD, Starlark, module, platform, or toolchain changes can alter the graph more broadly than ordinary source ownership indicates. 6.5.3 Global Target-Selection Invalidators classifies those special changes and decides when ordinary comparison must broaden or yield unknown.

Spotify's published bazel-diff workflow demonstrates the two-revision shape: generate a hash map at the starting revision, generate another after changing revision, then compare them for impacted targets.4 It also shows why the snapshot algorithm must have a declared boundary: changes outside the represented Bazel graph, such as Bazel version or rc changes, need separate invalidation handling rather than being assumed to appear automatically.5

The runnable base/head deletion fixture keeps an unrelated same-basename file out of the ownership match, queries each revision separately, and preserves the base-side reverse impact in the union. Its selector assertions make the bounded evidence and expected omission explicit.

Test the Comparison, Not Just the Happy Path

Build a revision fixture whose expected path is small enough to inspect. A useful minimum has:

  • two immutable revisions;
  • at least two requested roots, only one of which consumes the changed target;
  • a recorded snapshot identity for each side;
  • an expected selected root and an expected omitted root;
  • an explanation path from changed evidence to the selected root.

Run the comparison twice from the same inputs and require equivalent results. Then change one identity dimension at a time. A different target universe, comparison mode, Bazel binary or effective options, schema version, or supported configuration must not silently reuse the previous answer.

The fixture proves a bounded claim: under this algorithm and universe, this evidence reaches this root. It does not prove that the algorithm models every possible repository change. That broader safety work belongs to invalidator classification, multi-configuration coverage, stale-evidence detection, and conservative fallback in the following articles.

think

Trace: The base snapshot contains //lib:parser -> //app:binary. In the head revision, //lib:parser was deleted, and the selector loads only the head graph before examining the file diff. Can it safely conclude that //app:binary is unaffected because no current target owns the deleted source?

Reveal

No. The missing target and edge are themselves comparison evidence, but a head-only lookup has discarded them. The selector needs the base snapshot to recover the former ownership and consumer path. If that evidence is unavailable, the contract requires unknown, not an empty affected set.

key takeaway

Cross-revision target selection compares two snapshots of the same declared universe, not merely two lists of changed files. Choose graph, configured-target, ownership, fingerprint, or generated-metadata evidence according to the claim the service must support; record the identity and limits of that evidence; then trace each difference through reverse consumers to requested roots. Selected and omitted roots are trustworthy only when their explanations preserve how the base and head evidence produced the decision.

Check your understanding · 4 questions

1.Which facts must a selector record and check for compatibility before it treats two graph snapshots as the same comparison universe?

Select all that apply

2.A deletion removes //legacy:codec and its source from the head snapshot. The base snapshot still shows //app:server as a reverse consumer. What should the comparison return for the requested root //app:server?

Select one answer

3.Match each kind of comparison evidence to the claim it supports most directly:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
Reverse dependencies
Unconfigured query data
Configured-target data
Recursive fingerprints

4.Classify these outcomes from a revision comparison:

Choose True or False for each sentence

A root that was compared and had no changed evidence reach it can be reported as unaffected.
A root outside the declared target universe is another name for an unaffected root.
Missing or incompatible required snapshot identity should cause rejection or an unknown result.
A changed fingerprint proves affectedness for configurations that were never represented.
0 of 4 answered

Footnotes

  1. Precision CI at Scale: Target-Aware Workflows with Bazel Diff - Maxwell Elliott & Connor Wybranowski — query and cquery modes and their precision/cost trade-off

  2. bazel-diff — hash-based affected-target selection — two-revision graph hashing and impacted-target output

  3. Target Determinator — cquery-based affected-target analysis — configured-target information compared across Git revisions

  4. Improving CI efficiency with Bazel querying and bazel-diff — starting and final graph-hash snapshots followed by impacted-target comparison

  5. Improving CI efficiency with Bazel querying and bazel-diff — query limitations for changes outside the represented graph and seed-file invalidation