The next review starts at the last reviewed commit
Shipped
The Issueflow v0.10.0 tag includes the incremental review work from v0.9.0 alongside native Codex runtime support. I changed later review rounds to read a separate fix patch first, while keeping the full pull-request diff for context and inline-comment eligibility. Reviewer allocation follows the fix’s changed-line count instead of repeatedly sizing itself to the entire pull request.
After a review asks for a small correction, the next useful question is what that correction changed. Starting from the original base again makes the reviewer rediscover work it already examined.
Keep two patches with different jobs
Use Git and Python 3.11 or newer. This example builds a local review bundle; it does not call a model, post comments, or decide whether a change is correct.
Save this as review_bundle.py. The caller supplies a base commit, the previously reviewed commit, and the current commit. The tutorial requires a linear ancestry relationship and refuses rebased history so it cannot silently mislabel a rewritten change as a small fix.
Git diff compares the trees at two endpoints. That makes previous → current a different question from base → current, even when both commands run on the same checkout.
import json
import re
import subprocess
from pathlib import Path
def git(repo: Path, *args: str) -> str:
return subprocess.check_output(
["git", "-C", str(repo), *args], text=True, encoding="utf-8"
)
def commit(repo: Path, value: str) -> str:
if not re.fullmatch(r"[0-9a-f]{7,40}", value):
raise ValueError("supply a commit hash, not a branch expression")
return git(repo, "rev-parse", "--verify", value + "^{commit}").strip()
def require_ancestor(repo: Path, older: str, newer: str) -> None:
result = subprocess.run(
["git", "-C", str(repo), "merge-base", "--is-ancestor", older, newer],
check=False, capture_output=True,
)
if result.returncode == 1:
raise ValueError("history changed; request a fresh full review")
if result.returncode != 0:
raise RuntimeError("could not verify review ancestry")
def bundle(repo: Path, base: str, previous: str, current: str, out: Path) -> dict:
base, previous, current = [commit(repo, item) for item in (base, previous, current)]
require_ancestor(repo, base, previous)
require_ancestor(repo, previous, current)
options = ("--no-ext-diff", "--no-textconv", "--no-renames")
full = git(repo, "diff", *options, base, current, "--")
fix = git(repo, "diff", *options, previous, current, "--")
if not fix.strip():
raise ValueError("no fix to review")
stats = git(repo, "diff", *options, "--numstat", previous, current, "--")
changed = 0
for row in stats.splitlines():
added, deleted, _ = row.split("\t", 2)
if added == "-" or deleted == "-":
raise ValueError("binary change needs an explicit review plan")
changed += int(added) + int(deleted)
result = {
"base": base, "previous": previous, "current": current,
"fix_lines": changed,
"finders": min(3, max(1, (changed + 299) // 300)),
}
out.mkdir(parents=True, exist_ok=False)
(out / "full.patch").write_text(full, encoding="utf-8")
(out / "fix.patch").write_text(fix, encoding="utf-8")
(out / "review.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
return result
The ancestry check uses git merge-base --is-ancestor, whose exit status distinguishes ancestry from errors. That distinction is useful: a missing object is not evidence that a rebase occurred.
The allocation formula mirrors the release’s later-round finder policy. It is a work-sizing heuristic, not a claim that every few hundred lines deserve the same attention. A one-line authorization change can deserve more scrutiny than a large generated file.
The example rejects binary changes instead of counting them as zero lines. It also disables external diff helpers and text conversion, keeping bundle construction focused on repository data rather than executing a locally configured renderer.
Build a small history you can inspect
Save this as check_review.py next to the first file. It creates a disposable repository with a base, an initial feature, and a follow-up fix. All commits stay local, and the temporary repository is cleaned up when the test ends.
import tempfile
from pathlib import Path
from review_bundle import git, bundle
with tempfile.TemporaryDirectory(prefix="review-demo-") as directory:
root = Path(directory)
repo = root / "repo"
repo.mkdir()
git(repo, "init", "-q")
git(repo, "config", "user.name", "Review demo")
git(repo, "config", "user.email", "[email protected]")
git(repo, "config", "commit.gpgsign", "false")
source = repo / "service.py"
def checkpoint(message, code):
source.write_text(code, encoding="utf-8")
git(repo, "add", "service.py")
git(repo, "-c", "core.hooksPath=/dev/null", "commit", "-qm", message)
return git(repo, "rev-parse", "HEAD").strip()
base = checkpoint("base", "LIMIT = 10\n")
previous = checkpoint("feature", "LIMIT = 10\nTIMEOUT = 30\n")
current = checkpoint("fix", "LIMIT = 10\nTIMEOUT = 60\n")
out = root / "bundle"
result = bundle(repo, base, previous, current, out)
assert result["fix_lines"] == 2
assert result["finders"] == 1
assert "-TIMEOUT = 30" in (out / "fix.patch").read_text()
assert "-TIMEOUT = 30" not in (out / "full.patch").read_text()
print("fix patch: replacement included")
print("full patch: original feature context retained")
print("review allocation:", result["finders"], "finder")
try:
bundle(repo, base, current, current, root / "empty")
except ValueError as error:
assert str(error) == "no fix to review"
print("empty follow-up: refused")
else:
raise AssertionError("empty fix should be refused")
Run:
python3 check_review.py
Expected output:
fix patch: replacement included
full patch: original feature context retained
review allocation: 1 finder
empty follow-up: refused
The changed-line count includes the deleted old timeout and the added replacement. It is not a count of lines in the final file or a count of distinct affected source locations.
Hand the reviewer a reading order
Use fix.patch as the primary read. Then give the reviewer the enclosing functions, relevant callers, original requirements, and outstanding major findings. Keep full.patch available when the fix changes assumptions made by the original feature.
Persist the reviewed commit only after that review is complete. Otherwise a retry can advance the baseline past work no reviewer examined. The bundle records all three endpoints so a later process can see exactly what the round covered.
Inline comments have a separate coordinate system. GitHub’s review-comment API expects a path, commit, side, and line in the pull-request diff. Do not take a line’s position in fix.patch and assume it is a valid GitHub anchor. The release keeps the full diff as the authority for inline eligibility for that reason.
Gotchas
Re-reviewing the full change can keep expanding the work. The release’s changelog records repeated full-PR review rounds and growing fixes as the reason for the delta-first policy. The escape is to review the fix first and constrain the fixer to named failures, while retaining context for cross-file effects. The release does not establish a measured post-change speedup, and this guide claims none.
A fix patch is not enough context on its own. A local change can invalidate a caller outside its hunks. The tagged briefs explicitly retain the full patch and ask reviewers to read enclosing functions and callers. Delta-first changes reading priority, not the standard of correctness.
History rewrites need a deliberate reset. This tutorial refuses a non-ancestor previous head. Without that guard, a rebase can create a large or misleading endpoint diff. Fetch the needed objects, verify the new history, and explicitly schedule a full review instead of quietly moving the baseline.
An empty delta is not another review round. The release refuses a round with no fix. The local harness proves that refusal. If findings remain open with no code change, resolve the disagreement or request a fix; spawning the same review again does not create new evidence.
Sources
- Git diff — endpoint comparisons and numeric change statistics.
- Git merge-base — ancestry verification and exit statuses.
- GitHub review comments — inline-comment coordinates.