An AI assistant suggests removing a check from a small line-reading function. “The caller already supplies valid line numbers,” it says. The function does look simple. Before accepting the change, you look at Git history—and the line points to a commit called “Compact comparison spacing.”

That gives you another place to look. The useful question is what input the check protects, when that behavior appeared, and whether today’s callers still need it. A small history you can rebuild makes those questions concrete.

Start with the suspicious line

Suppose a source viewer uses one-based line numbers. Its current function looks like this:

def read_line(lines, line_number):
    """Read a source line using a one-based line number."""
    if line_number<1:
        raise ValueError("line number must be positive")
    if line_number > len(lines):
        raise IndexError("line number is past the end")
    return lines[line_number - 1]

For the two lines ["alpha", "omega"], line 1 should return alpha and line 2 should return omega. In Python, though, an index of -1 reads the last element. Without the lower check, a requested line number of 0 becomes index -1 and returns omega. A line number of -1 becomes index -2 and returns alpha.

You can explain that from the current code. History answers a different question: when did the rejection become part of this function, and what did the change include?

Rebuild the four commits

Download git-history-lab.py into an empty working folder. You need Git 2.29+ and Python 3.9+; there are no package dependencies. Run this in your own terminal:

python3 git-history-lab.py create history-lab --record history-check
cd history-lab
git log --reverse --format='%h %s' HEAD

On Windows, use your Python 3 launcher in place of python3 if needed. The workflow uses Git and Python, with no macOS APIs. The recorded run used Git 2.50.1 and Python 3.9.6 on macOS; Windows and Linux were not separately tested.

The script creates a new local repository, runs the checks, and saves their output in history-check. It refuses an existing destination, uses a synthetic author only for its own commits, and doesn’t change your global Git configuration. All names, dates, commit messages, and the viewer scenario are teaching fixtures.

ce4c0cf Add one-based source line lookup
e87088e Reject nonpositive source line numbers
7d41ebc Rename line reader module
27a0dc5 Compact comparison spacing

Each commit also has a readable tag:

Tag Change
baseline Adds line_reader.py; tests cover the first line, last line, and an input past the end
fix-positive-lines Adds the lower check and tests for 0, -1, and an empty input list
rename-module Renames the file to source_lines.py and updates the test import
format-spacing Changes line_number < 1 to line_number<1

The compact spacing is deliberately conspicuous so the last edit is easy to recognize.

Let blame point you to the next diff

Inspect line 3:

git blame -L 3,3 HEAD -- source_lines.py
git show --format=fuller format-spacing -- source_lines.py

Blame attributes the line to 27a0dc57, the spacing commit. Its patch contains:

-    if line_number < 1:
+    if line_number<1:

Now ask Git to ignore whitespace while tracing the line:

git blame -w -L 3,4 HEAD -- source_lines.py

The result points to e87088e0 and the old path, line_reader.py. Git blame follows whole-file renames automatically; -w ignores whitespace in attribution. The rename itself did not cause the misleading result here—the later spacing edit did. See the official git-blame documentation.

For this particular edit, the verifier also compares Python syntax trees and runs the tests before and after. Both agree. Ignoring whitespace is a way to investigate attribution, not a test of behavior: indentation and spaces inside strings can matter in Python.

After inspecting a known formatting commit, you can also exclude it explicitly:

git blame --ignore-rev format-spacing -L 3,3 HEAD -- source_lines.py

That also reaches the fix in this lab. Don’t exclude a commit just because its title says “format”; read its patch first. Blame records line attribution, not a developer’s reasoning or responsibility for a bug.

Search for the text that survives the rewrite

The error message stayed the same across the rename and spacing edit. Search for its introduction:

git log --format='%h %s' -p -S 'line number must be positive' HEAD -- '*.py'

This returns the fix and its patch in line_reader.py. Quoting *.py leaves the path matching to Git and includes the old filename. This repository has only one commit adding or removing that message.

-S selects changes in the number of occurrences of a string. It doesn’t select every edit near a match. See Git’s pickaxe explanation.

Try a string that did change during formatting:

git log --format='%h %s' -p -S 'line_number < 1' HEAD -- '*.py'

Now the output includes both the spacing commit, which removes that exact spelling, and the fix, which adds it. The extra match is useful: read what changed, then choose a more stable fragment if necessary. A missing result also needs interpretation; text can change while its occurrence count stays the same.

For a known file, this lab also works with:

git log --follow --format='%h %s' -p -S 'line number must be positive' HEAD -- source_lines.py

--follow handles one file’s rename history. It is not a general cross-file search. In a real investigation, check older paths or widen the path selection when a narrow search stops too early.

Follow the lines through time with log -L

If you already know the relevant range, ask for its patches through history:

git log --format='%h %s' -L 3,4:source_lines.py HEAD

In this repository, the output includes the whitespace edit and the original addition of the guard. It crosses the pure file rename, but the unchanged renamed block doesn’t get a separate patch. Use the earlier four-commit log when you need the whole sequence.

For the whole function, run:

git log --format='%h %s' -L :read_line:source_lines.py HEAD

This also reaches the original indexing expression at baseline. The lab declares *.py diff=python in .gitattributes so Git uses its Python function-header rules. Function recognition depends on the language and diff configuration; see defining a custom hunk header. A numeric fallback for this file is:

git log --format='%h %s' -L 1,7:source_lines.py HEAD

With log -L, the path belongs inside the range argument; don’t append a -- path limiter. The range must exist in the starting revision, and the walk starts from a single revision. These restrictions are in the official git-log documentation.

Real histories may contain merges, copied code, rewritten functions, or missing older commits in a shallow clone. A line trace is a starting point for reading patches; it cannot recover rationale that was never recorded.

Test the explanation against both versions

Read the implementation and tests together at the discovered fix:

git show --format=fuller fix-positive-lines -- line_reader.py test_lines.py

The patch adds the lower check and a test that expects ValueError for 0 and -1. The lab verifier extracts the committed source and tests into temporary folders. It runs the same regression suite against the baseline implementation, then runs each commit’s own tests. It doesn’t switch your checkout.

Observation from the recorded run What it supports
Baseline returns omega for 0 and alpha for -1 These inputs reach Python’s negative indexing
The fix’s suite fails on baseline for both inputs The added assertions detect the old behavior
The fix, rename, and spacing versions each pass five tests Those tested cases behave as intended after the fix
Rename preserves source bytes; spacing preserves the syntax tree Those later edits preserve this function’s implementation

The baseline’s three original tests also pass. A passing suite can simply leave out the input you care about. The full expected results describe the checks; your history-check/transcript.txt contains the actual commands and outputs, including the expected two failures.

The sample commit message describes a viewer returning the last line for line zero. The tests reproduce that function behavior. Before removing the check in your project, inspect how current callers reject zero and negative line numbers. This example assumes integer inputs; other types need separate validation.

Give the AI a question it can check

Once you have the revision IDs, the patch, and the test output, ask the assistant to connect them:

Explain why this lower-bound check exists. Use the supplied fix and its parent, show the result of inputs 0 and -1, and identify the regression assertions. Separate what the diff and executed tests show from what the commit message claims. Then inspect current callers before recommending removal. If those callers or historical reports aren’t available, say which conclusion remains open.

The useful answer has three parts: the behavior change, the reported motivation, and what remains unknown about today’s callers. If it claims an incident happened solely because the message says so, ask it to narrow the claim. If it proposes deleting the check, ask it to show how every supported entry path rules out the failing inputs. Following one value through unfamiliar code is a good next step for that caller investigation; reviewing an AI-written patch shows how to turn a proposed explanation into a focused regression test.