DEV Community

Cover image for Myers, Patience, Histogram: Why Git Can Make an Agent's Moved Method Look Rewritten
Gabriel Anhaia
Gabriel Anhaia

Posted on Edited on AI-assisted

Myers, Patience, Histogram: Why Git Can Make an Agent's Moved Method Look Rewritten


A C function gets a guard clause at the top, and the condition below it flips. In Adam Johnson's published example, Git's default diff of that change and its histogram diff both come to 10 added lines and 6 removed ones when you count them. Same edit, same count. Only the histogram version reads like "added a guard clause". The default one weaves the new check through the old body, pairing a { with some other {, and leaves you to rebuild the change in your head.

Picture the edits a coding agent makes on an ordinary Tuesday. It moves a method, extracts a class, re-indents a block, reorders a file to match a style rule. Those are the edits where the choice of diff algorithm decides whether a reviewer sees a move or a rewrite. Git has offered four algorithms for years, and it still ships the oldest one as the default.

What Myers optimizes, and what it doesn't

Eugene Myers published "An O(ND) Difference Algorithm and Its Variations" in Algorithmica in 1986 (vol. 1, no. 2, pp. 251–266, cited in Wikipedia's diff article). The idea is to treat a diff as a path through an edit graph and find the shortest one. N is the combined length of the two files, D is the number of edits, and the running time grows with their product. When two versions are close, D is small and the algorithm is fast.

James Coglan's walkthrough of the algorithm puts the behaviour in two words: it is "greedy, that is trying to consume as many lines that are the same before making a change", and it prefers deletions over insertions, so removed lines appear first. Git's own git diff docs (2.56.0) call myers "the basic greedy diff algorithm" and say "Currently, this is the default." A separate minimal mode will "spend extra time to make sure the smallest possible diff is produced", so the default trades a little minimality for speed.

Myers' objective is a short edit script, and a line matches any equal line. In source code the most common lines are }, { and blank lines. Coglan's follow-up on patience diff says it plainly: "Myers can sometimes pick uninteresting lines to match up, such as all the blank lines and braces". A minimal diff can still be a misleading one.

Two stacks of code blocks side by side, with thin orange threads tying each closing brace on the left to the wrong closing brace on the right
Myers is happy to pair any equal lines, and braces and blank lines are the most equal lines in a file.

PHP makes this worse by convention. PSR-12 says a method's "opening brace MUST go on its own line", and the same goes for classes. Bram Cohen named that exact habit in 2010: the mismatch "has a particularly strong tendency to happen if you put curly brackets on lines by themselves" (his post). Every PSR-12 controller is full of identical brace-only lines for Myers to pair up.

Patience and histogram: anchor on lines that mean something

Cohen's fix was patience diff. Match the identical lines at the start and end of both files. Then find the lines that "occur exactly once on both sides", take the longest common subsequence of those, and use them as anchors. Recurse into the gaps between anchors. A method signature such as public function show(int $id) is usually unique in its file, so it becomes an anchor, and the braces around it are less likely to pair with the wrong method.

Patience has a weak spot. If a region has no unique lines at all, there is nothing to anchor on. JGit, the Java implementation of Git, extended the idea. Its HistogramDiff javadoc calls it "an extended form of Bram Cohen's patience diff algorithm" that was "further extended to support low-occurrence common elements". It counts how often each line occurs, and the source docs say the matching line "that has the lowest number of occurrences is chosen as a split point". When a unique line exists, it "behaves exactly like Bram Cohen's patience diff". When a region is too repetitive, it hands that region to a fallback, which is Myers in the code.

Git took the code in 1.7.7, released in 2011, which gave git diff a --histogram option using diff machinery "stolen from jgit".

Nugroho, Hata and Matsumoto compared Myers and histogram across Java projects in Empirical Software Engineering (arXiv preprint). On 14 projects, code churn metrics came out different in 1.7% to 8.2% of commits depending on the algorithm. For patches they found histogram "more suitable than Myers for providing the changes of code", and they recommend it for mining repositories.

Reproduce it on one agent refactor

Here is a refactor an agent might hand you on a Laravel app. It moves show() above store(), and it pulls the discount rule out of the controller into a plain class under app/Domain. No behaviour changes. The script builds a throwaway repo with the before and after as two commits. It assumes your git user.name and user.email are set.

mkdir diff-lab && cd diff-lab && git init -q
mkdir -p app/Http/Controllers app/Domain
F=app/Http/Controllers/OrderController.php
Enter fullscreen mode Exit fullscreen mode

The controller as it stands before the refactor, as the first commit:

cat > "$F" <<'PHP'
<?php

namespace App\Http\Controllers;

use App\Models\Order;
use Illuminate\Http\Request;

class OrderController
{
    public function store(Request $request)
    {
        $order = Order::create($request->all());
        $order->discount = $this->discountFor($order);
        $order->save();

        return $order;
    }

    public function show(int $id)
    {
        return Order::findOrFail($id);
    }

    private function discountFor(Order $order): int
    {
        if ($order->total > 100) {
            return 10;
        }

        return 0;
    }
}
PHP
git add -A && git commit -qm "before"
Enter fullscreen mode Exit fullscreen mode

Then the agent's version of the controller:

cat > "$F" <<'PHP'
<?php

namespace App\Http\Controllers;

use App\Domain\DiscountPolicy;
use App\Models\Order;
use Illuminate\Http\Request;

class OrderController
{
    public function __construct(
        private DiscountPolicy $discounts,
    ) {
    }

    public function show(int $id)
    {
        return Order::findOrFail($id);
    }

    public function store(Request $request)
    {
        $order = Order::create($request->all());
        $order->discount = $this->discounts->discountFor($order);
        $order->save();

        return $order;
    }
}
PHP
Enter fullscreen mode Exit fullscreen mode

And the new class the rule moved into, plus the second commit:

cat > app/Domain/DiscountPolicy.php <<'PHP'
<?php

namespace App\Domain;

use App\Models\Order;

final class DiscountPolicy
{
    public function discountFor(Order $order): int
    {
        if ($order->total > 100) {
            return 10;
        }

        return 0;
    }
}
PHP
git add -A && git commit -qm "agent: extract DiscountPolicy"
Enter fullscreen mode Exit fullscreen mode

Now diff the same commit three ways and count:

for a in myers patience histogram; do
  git diff --diff-algorithm="$a" HEAD~1 HEAD > "../$a.diff"
done
cd ..
grep -cE '^[-+]([^-+]|$)' myers.diff patience.diff histogram.diff
grep -c '^@@' myers.diff patience.diff histogram.diff
Enter fullscreen mode Exit fullscreen mode

The first grep counts added and removed lines, skipping the --- and +++ file headers. The second counts hunks. The .diff files go one level up so they never land in the repo.

I haven't run these two commits through the three algorithms myself, so I won't hand you numbers or predict a winner. Read the three files side by side. In the controller, look at what happens to show() and store(): does one of them come out as a clean block that moved, or does the diff weave old and new bodies together around shared braces? Look at which } each algorithm keeps as context. Myers is free to pair any } with any other }. Patience only anchors on lines that are unique on both sides, such as the two method signatures. Histogram starts from rare lines too, but it splits the file around the whole common run with the lowest occurrence count (JGit source), and that run can carry the braces and blank lines next to it. The JGit docs say it behaves like patience when a unique line exists. Whether Git's version gives you the same grouping on this file is the thing to check.

The line counts may come out close, or even equal: Adam Johnson's C example above had identical counts under Myers and histogram. The difference is in how the lines are grouped, and the hunk count is a rough proxy for that.

Three magnifying glasses held over the same page of code, each lens showing the highlighted lines grouped in a different pattern
One commit, three algorithms. The grouping a reviewer reads can change with the algorithm.

Moved code and whitespace: the flags that cut reading

No algorithm will show DiscountPolicy::discountFor() as a move. It is a deletion in one file and an addition in another, and a line diff sees exactly that. Git has a separate pass for this. --color-moved colours "moved lines of code" differently, per the docs. Given with no mode it means zebra: blocks of moved text "of at least 20 alphanumeric characters are detected greedily", and alternating colours mark where one moved block ends and the next starts. dimmed-zebra goes further and dims "uninteresting parts of moved code", leaving only the borders of each moved block bright.

cd diff-lab
git show --color-moved=dimmed-zebra \
  --color-moved-ws=allow-indentation-change HEAD
Enter fullscreen mode Exit fullscreen mode

--color-moved-ws=allow-indentation-change matters for agent work. Agents often move a block and change its depth at the same time, say from a method into a closure. That mode ignores whitespace at first, then groups lines into one moved block only "if the change in whitespace is the same per line". A block shifted one level still counts as moved.

--color-moved leaves the line count alone and shrinks the part you have to read closely. If move detection picks up the body of discountFor(), that body is dimmed, and what should stay bright is the part that differs: the private that became public, the new class wrapper, the constructor. The algorithm still matters here. Move detection only works on lines the diff shows as removed in one place and added in another, so if the algorithm keeps some of the old body's braces and blank lines as context against store(), less of that body comes out as one block for --color-moved to catch.

-w is the blunter tool. It will "ignore whitespace when comparing lines", so a block that was only re-indented in place drops out of the diff. That is what you want when an agent reformats a PHP file. It is the wrong flag for Python, YAML or a Makefile, where whitespace is the code. Use it to read, never as the only view before you approve.

The settings to set before you review agent work

Put these in your global config once:

git config --global diff.algorithm histogram
git config --global diff.colorMoved dimmed-zebra
git config --global diff.colorMovedWS \
  allow-indentation-change
Enter fullscreen mode Exit fullscreen mode

The diff config docs list all three. When you want the old behaviour back for a single command, the git diff docs say you "have to use --diff-algorithm=default". The evidence above favours histogram as a default, and it still won't read best on every file. When a diff still looks like a rewrite, try --diff-algorithm=patience on that one command. Recent Git also lets git blame take --diff-algorithm (docs). The commit that added it, from November 2025, makes blame respect diff.algorithm too, and notes "long-standing interest in changing the default diff algorithm" to histogram, with Git 3.0 "floated as a possible occasion". It is not in Git's list of 3.0 breaking changes yet, so don't wait for it.

These settings live on your machine. A pull request page does not read your ~/.gitconfig. On GitHub, the closest control is whitespace: since an October 22, 2026 update you can "Open Settings → Whitespace changes and choose Always show, Always hide, or Hide for this diff only". That changelog says nothing about moved code or the diff algorithm. For a PR that is mostly a move, fetch the branch and read it locally with the flags above.

I build Hermes IDE, so weigh this part with that in mind. Hermes gives each agent task its own hermes/<name> branch in its own worktree. The Review Desk (⌘G, Ctrl+Shift+G on Windows and Linux) shows the changes by turn or by file, with risk flags, and sends a line comment back to the agent that wrote the line. The Hermes docs don't say the Review Desk's viewer reads diff.algorithm, so I won't claim it does. The branches are plain git, though. When a turn looks like a rewrite and you suspect a move, run the same check from your main checkout:

git diff --diff-algorithm=histogram \
  --color-moved=dimmed-zebra main...hermes/<name>
Enter fullscreen mode Exit fullscreen mode

The Hermes Review Desk in By turn view for a Claude Code task, listing two turns with their line counts, the diff of src/db.ts from turn 1, the changed-files list and a drafted commit message
The Review Desk (⌘G) in By turn view: two turns, turn 1's diff of src/db.ts, the changed files and a commit message drafted from the turns.

Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. Download it or read the code on GitHub. It is not affiliated with Nous Research, Anthropic, OpenAI or any agent vendor.

Which one does your team use when it reviews agent diffs: Git's default, histogram, or whatever the PR page shows? Tell me in the comments.


If this was useful

The refactor in this post is the most common one in a PHP codebase that is trying to get its framework back under control: a rule leaves the controller and lands in a plain class the framework knows nothing about. Decoupled PHP is about making that move on purpose, with ports and adapters, so your business rules have a home outside the framework before an agent starts moving them around.

Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework

Available on Kindle, Paperback, and Hardcover. English, German, and Japanese editions out now — Portuguese and Spanish coming soon.

Top comments (0)