Skip to content

Commit 63f2d3e

Browse files
committed
tools: add helper tool about verify backports
Add an (AI-assisted) tool to help users verify a backport. The tool scans all the commits in a backport branch, summarize the changes, flags the language-agnostic mechanical changes (e.g. file rename) and code movements, and helps prioritize the review effort. The tool intentionally builds on `git` tooling, acting an orchestrator and custom-built porcelain layer. The tool includes a LLM-optimized JSON output. This can help LLM-driven review in the folling ways: 1. Reducing orchestration (single command vs multi-step git pipelines) 2. Returning pre-correlated commit relationships 3. Providing stable machine-parseable output Token savings are possible but scenario-dependent and are not guaranteed for every run; benchmarking and measuring over time and multiple usages are the only way to know for sure. See the included README.md for the full details. AI-Attribution: AIA PAI Nc Hin R claude-4.6-opus-1M v1.0 Signed-off-by: Francesco Romani <fromani@redhat.com>
1 parent cd4dc0e commit 63f2d3e

2 files changed

Lines changed: 1235 additions & 0 deletions

File tree

tools/verify-backport/README.md

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
# verify-backport
2+
3+
Verifies that a backport branch carries commits whose content is identical to their
4+
counterparts on the main branch.
5+
6+
## Problem
7+
8+
When commits are cherry-picked or rebased onto a release branch, the commit hashes change.
9+
Cherry-pick trailers (`cherry picked from commit ...`) may be missing or incorrect.
10+
This tool provides **ex-post verification** that the backport commits match their originals
11+
by comparing the actual patch content, not metadata.
12+
13+
## How it works
14+
15+
The tool uses `git patch-id --stable` to compute a content-based hash of each commit's diff.
16+
Two commits that introduce the same code changes produce the same patch-id, regardless of
17+
their commit hash, surrounding context line numbers, or whitespace differences.
18+
19+
For each backport commit:
20+
1. Compute its patch-id
21+
2. Find a main-branch commit with the same patch-id
22+
3. Verify the backport commit message **contains** the original commit message
23+
(the backport may add extra text such as backport-specific notes)
24+
25+
When a backport commit doesn't match by patch-id, the tool tries to find a likely
26+
counterpart on main by matching the commit subject line, and reports it as a
27+
closest match (`~>`) so the user knows which main commit to compare against.
28+
29+
For unmatched commits, the tool performs a file-level comparison and classifies
30+
each file by the type of review effort required (see "Change classification" below).
31+
32+
## Usage
33+
34+
### Branch mode
35+
36+
Point the tool at a local branch that is a checkout of a backport PR:
37+
38+
```bash
39+
# Fetch the PR locally
40+
git fetch origin pull/1234/head:pr-1234
41+
42+
# Verify the backport (targeting release-4.18)
43+
bin/verify-backport \
44+
--backport-branch pr-1234 \
45+
--base-branch origin/release-4.18
46+
```
47+
48+
This derives backport commits as `origin/release-4.18..pr-1234` and searches
49+
the last 100 commits on main for matches.
50+
51+
### Range mode
52+
53+
Specify explicit git revision ranges for full control:
54+
55+
```bash
56+
bin/verify-backport \
57+
--backport-range release-4.18~5..release-4.18 \
58+
--main-range main~20..main
59+
```
60+
61+
### Flags
62+
63+
| Flag | Default | Description |
64+
|------|---------|-------------|
65+
| `--backport-branch` | | Local branch name (pseudo-PR mode) |
66+
| `--base-branch` | | Branch the backport is based on (e.g. `release-4.18`); required with `--backport-branch` |
67+
| `--backport-range` | | Git revision range for backport commits |
68+
| `--main-range` | | Git revision range for main commits |
69+
| `--main-branch` | `main` | Name of the main branch where originals live |
70+
| `--main-depth` | `100` | How many main commits to search (branch mode) |
71+
| `--check-missing` | `false` | Report main commits with no backport counterpart |
72+
| `--verbose` | `false` | Show structured diff for unmatched commits |
73+
| `--color` | `true` | Enable colored output (auto-disabled when not a terminal) |
74+
| `--json` | `false` | Output results as JSON (for scripting and LLM consumption) |
75+
76+
Exactly one of `--backport-branch` or `--backport-range` must be provided.
77+
When using `--backport-branch`, `--base-branch` is required.
78+
When using `--backport-range`, `--main-range` is required.
79+
80+
## Output
81+
82+
### Default mode
83+
84+
Each backport commit gets one status line:
85+
86+
```
87+
MATCH: abc1234 Fix the widget <--> def5678
88+
MISMATCH-MESSAGE: abc1234 Update docs <--> def5678
89+
UNMATCHED: abc1234 Adapted change (~> def5678) [2 identical, 1 moved, 1 additive, 1 edits]
90+
identical (2):
91+
internal/controller/nro_controller.go -> controllers/nro_controller.go (renamed)
92+
pkg/status/status_test.go
93+
moved (1):
94+
machineconfigpool.go -> rte.go (95% overlap)
95+
additive (1):
96+
pkg/status/status.go (+1 only in backport)
97+
edits (1):
98+
test/e2e/serial/tests/configuration.go (+4 only in backport, +2 only in main)
99+
```
100+
101+
The `~>` arrow indicates a closest match found by commit subject. The bracketed tag
102+
shows the reviewer triage summary. Files are grouped by change type (see below).
103+
104+
With `--check-missing`, main commits with no backport counterpart are also shown:
105+
106+
```
107+
MISSING-BACKPORT: def5678 Original commit subject
108+
```
109+
110+
### Verbose mode (`--verbose`)
111+
112+
Adds the actual differing lines for `additive` and `edits` files, and residual (non-overlapping)
113+
lines for `moved` files.
114+
115+
```
116+
edits (1):
117+
test/e2e/serial/tests/configuration.go (+4 only in backport, +2 only in main)
118+
only in backport:
119+
+ import "github.com/k8stopologyawareschedwg/deployer/pkg/deployer/platform"
120+
+ cond := status.FindCondition(updatedOperObj.Status.Conditions, status.ConditionAvailable)
121+
...
122+
only in main:
123+
+ dss, err := objects.GetDaemonSetsByNamespacedName(fxt.Client, ctx, ...)
124+
...
125+
```
126+
127+
### JSON mode (`--json`)
128+
129+
Outputs a single JSON object to stdout, suitable for scripting or LLM consumption.
130+
No color codes, no padding, no verbose preamble — just structured data.
131+
132+
```json
133+
{
134+
"summary": {
135+
"matched": 3,
136+
"total": 4,
137+
"unmatched_main": 1
138+
},
139+
"results": [
140+
{
141+
"status": "MATCH",
142+
"backport": {"hash": "abc1234...", "subject": "Fix the widget"},
143+
"main": {"hash": "def5678...", "subject": "Fix the widget"}
144+
},
145+
{
146+
"status": "UNMATCHED",
147+
"backport": {"hash": "aaa1111...", "subject": "Adapted change"},
148+
"closest": {"hash": "bbb2222...", "subject": "Adapted change"},
149+
"patch_comparison": {
150+
"identical": 2,
151+
"moved": 1,
152+
"additive": 1,
153+
"edits": 0
154+
}
155+
}
156+
],
157+
"missing_backports": [
158+
{"hash": "ccc3333...", "subject": "Original commit subject"}
159+
]
160+
}
161+
```
162+
163+
Field presence rules:
164+
- `main`: present only for `MATCH` and `MISMATCH-MESSAGE` results
165+
- `closest` and `patch_comparison`: present only for `UNMATCHED` results with a subject match
166+
- `missing_backports`: present only when `--check-missing` is set and there are unmatched main commits
167+
168+
#### LLM token efficiency estimate
169+
170+
Without this tool, an LLM verifying a 10-commit backport against 100 main commits needs
171+
to run ~4 git commands (log + patch-id for each side), ingest their output (~4000-6000
172+
tokens of raw git data), and cross-reference patch-ids and messages in-context.
173+
174+
With `--json`, it is a single command producing ~500-800 tokens of pre-correlated output.
175+
The estimated saving is **3-5x fewer tokens** for the ingestion step, plus elimination of
176+
the multi-step reasoning needed to correlate commits manually. The exit code alone (0 vs 2)
177+
answers "is this backport correct?" without parsing any output at all.
178+
179+
These estimates are back-of-envelope (May 2026) and should be verified against actual
180+
token counts in representative backport reviews.
181+
182+
### Change classification
183+
184+
Files are classified by the type of review effort they require, from lowest to highest bandwidth:
185+
186+
| Category | What it means | How to review | Color |
187+
|----------|---------------|---------------|-------|
188+
| **identical** | No differing lines (with or without rename) | Skip entirely | dim |
189+
| **moved** | File relocated between releases (>=50% content overlap) | Glance at residual lines (shown in verbose) | dim |
190+
| **additive** | Only one side has extra lines | Check that additions/deletions make sense for the target branch | yellow |
191+
| **edits** | Both sides have differing lines | Full review: substantive changes were made | yellow |
192+
193+
This classification tells you **how** to review each file, not just whether to review it.
194+
195+
## Exit codes
196+
197+
| Code | Meaning |
198+
|------|---------|
199+
| 0 | All backport commits match their main counterparts |
200+
| 1 | Usage error or runtime failure (bad flags, git command error) |
201+
| 2 | Verification failure (mismatches, unmatched, or missing backports) |
202+
203+
## Design notes
204+
205+
### Architecture
206+
207+
The tool operates in three phases:
208+
209+
1. **Commit-level matching**: Compute patch-ids for backport and main commits; match by patch-id or fall back to subject-line matching
210+
2. **File-level comparison**: For each unmatched commit pair, parse both patches and extract per-file added/removed lines. **Whitespace is intentionally treated as non-semantic**: lines are trimmed before comparison, so pure whitespace/indentation differences are not reported as edits. This matches the behavior of `git patch-id --stable` and avoids drowning reviewers in noise from reformatting or tab/space changes between branches.
211+
3. **Change classification**: Classify each file by change type (identical/moved/additive/edits) and present triage-friendly output
212+
213+
### Code movement detection
214+
215+
After pairing files by path and basename, leftover ONLY-IN-BACKPORT and ONLY-IN-MAIN files
216+
are tested for code movement using set intersection of their added lines:
217+
218+
```
219+
overlap = |intersection(bpAdded, mainAdded)| / min(|bpAdded|, |mainAdded|)
220+
```
221+
222+
If overlap >= 50%, the files are classified as a `moved` pair. The `min` denominator (not union)
223+
ensures that a small file fully contained in a larger one still registers as movement.
224+
225+
**Why this is reliable:**
226+
- Compares actual code content, not filenames or heuristics
227+
- False positives (unrelated files sharing 50%+ of added lines) are extremely unlikely in practice
228+
- Real-world backports show near-100% overlap for genuine code movement (e.g., refactoring between releases)
229+
230+
**Scope constraint:** Code movement is detected only within each commit. If code moves across
231+
commits within the same backport, that's a red flag indicating the backport diverged from the
232+
original structure, which itself is useful signal.
233+
234+
### Why patch-id for commit matching
235+
236+
`git patch-id` is purpose-built for detecting equivalent patches across rebases and cherry-picks.
237+
It normalizes away:
238+
- Context line numbers (hunks can shift without changing the patch-id)
239+
- Whitespace differences
240+
- Commit metadata (author, date, hash)
241+
242+
But it remains sensitive to:
243+
- Actual code changes (even one character difference produces a different patch-id)
244+
- File renames (a renamed file produces a different patch)
245+
246+
This is the right level of strictness: we want to catch any substantive changes while allowing
247+
mechanical differences like directory restructuring or import path updates.
248+
249+
### Design constraints
250+
251+
- **Local-only, no network access**: The tool **never contacts remote servers**. All git operations run against the local clone using `exec.Command`. It does not fetch, push, or call any API (GitHub or otherwise). This is a deliberate trust boundary: the tool's output depends solely on what is already checked out locally, so there is no risk of leaking credentials, hitting rate limits, or trusting data from an external service. The user controls what is in the local clone; the tool only reads it.
252+
- **No external dependencies beyond git**: The tool uses only stdlib + `golang.org/x/term` (for color detection). No go-git, no GitHub API.
253+
- **Stateless**: Each run is independent; no caching or persistent state between runs.
254+
- **Colored by default**: Color is a high-ROI UX improvement for dense output. Auto-disabled when not a terminal or when `--color=false`.
255+
256+
### Future iteration notes
257+
258+
- **Cross-commit movement detection**: Currently not implemented (by design). If needed, pool all ONLY-IN-BACKPORT and ONLY-IN-MAIN files across all commits and match globally. But this may hide backport structure divergence, so default should remain within-commit.
259+
- **Mechanical change detection**: Import path substitutions (e.g., `api/numaresourcesoperator/v1``api/v1`) are currently shown as diffs. Could detect and label these as "mechanical" to further reduce reviewer bandwidth.
260+
- **Diff output format**: Currently shows +/- lines inline. Could add side-by-side or unified diff views.
261+
- **Performance**: For very large backport PRs (50+ commits), computing patch-ids and parsing patches is the bottleneck. Could parallelize per-commit processing if needed.
262+
263+
## Key assumptions
264+
265+
- **Local clone required, no network access**: The tool operates exclusively on a local git
266+
repository and **never makes network calls**. Both the backport branch/range and the main
267+
branch must be fetched locally before running the tool. This eliminates any need to trust
268+
remote services or configure authentication.
269+
- **No merge commits**: Merge commits are excluded from comparison (`--no-merges`) since
270+
they don't carry meaningful patch content.
271+
- **Message containment, not equality**: The backport commit message must *contain* the
272+
main commit message as a substring. This allows backport-specific additions (e.g.,
273+
"Backport to release-4.18" notes) while ensuring the original message is preserved.
274+
- **Whitespace normalization**: Trailing whitespace per line and trailing newlines are
275+
normalized before message comparison.
276+
- **Patch-id stability**: `git patch-id --stable` is used for reproducible results
277+
across git versions.
278+
- **Closest match by subject**: When patch-ids don't match, the tool falls back to
279+
matching by commit subject line to suggest a likely counterpart for investigation.
280+
281+
## Building
282+
283+
```bash
284+
make bin/verify-backport
285+
```
286+
287+
## Requirements
288+
289+
- Git (any reasonably modern version with `patch-id --stable` support, i.e., >= 2.0)
290+
- Go 1.25+ (for building)

0 commit comments

Comments
 (0)