skip to content

A Laravel pull request is drowning in formatting noise because Pint reformatted files nobody touched; how would you use --dirty, --diff and a pre-commit hook to stop it?

level: seniorimportance: should knowfreq 28%

answer

  1. one formatting-only baseline commit
  2. --dirty: files with uncommitted changes
  3. --diff=origin/main: since the merge base
  4. lock the Pint version in composer.lock
  5. a depth-1 clone breaks --diff

basics

~20 s

Land one formatting-only commit that runs Pint over the whole app, then scope every later run: --dirty locally or in a pre-commit hook, --diff=origin/main --test in CI, with one committed pint.json and a locked Pint version.

solid answer

~40 s

The noise comes from running bare `./vendor/bin/pint` over a partly unformatted codebase, or with different presets or Pint versions, so each PR drags in unrelated rewrites. First land a separate formatting-only commit that runs Pint across the whole app, and tell `git blame` to skip it with an ignore-revs file. Then scope every run: `pint --dirty` touches only PHP files with uncommitted changes (staged, unstaged or untracked), which suits a pre-commit hook, and `pint --diff=origin/main --test` in CI checks only files changed since the branch left main. Commit `pint.json` and `composer.lock` so every machine applies the same preset and Pint release. Finally, give the CI checkout enough history to find the merge base, or `--diff` aborts.

code

bash · 3 lines
bash
# CI job: the checkout must contain origin/main and the merge base
composer install --no-interaction
./vendor/bin/pint --diff=origin/main --test

go deeper

for a junior

Recall that --dirty formats only the files you changed and that formatting belongs in its own commit, never mixed into a feature.

for a middle

Explain what --dirty and --diff each select from Git, that neither changes the mode, and why --test is still needed for a gate.

for a senior

Lay out the rollout: a baseline commit ignored by blame, locked Pint version, a hook that respects the index, and a CI --diff --test gate with enough clone history.

for a principal

Treat formatting as a team contract: one preset, one version, one mass reformat scheduled with the team, and upgrades landed as their own reviewable commits.

## Why the noise appears A pull request that should change twelve lines shows four hundred, most of them whitespace and import order. With **Laravel Pint**, the usual causes are: - the codebase was **never formatted as a whole**, so anyone who runs bare `./vendor/bin/pint` reformats every file it touches, related or not; - developers use **different presets** because `pint.json` is missing or not committed; - developers run **different Pint versions**; a new release can change what a rule does (Pint 1.28, for example, improved `fully_qualified_strict_types`), so two machines disagree; - an editor formats on save with **another formatter** whose opinions differ from the preset. Reviewers then skim past the noise, and real changes hide inside it. ## Step 1: a one-time baseline 1. Agree on the preset and commit `pint.json`. 2. On a quiet day, run `./vendor/bin/pint` over the whole project in its own branch and merge it as a **formatting-only commit**, with no behaviour changes mixed in. 3. Add that commit's hash to a `.git-blame-ignore-revs` file and point `git blame` at it (`blame.ignoreRevsFile`), so history still shows who wrote each line. 4. Ask open branches to rebase onto it and run Pint once, so they absorb the reformat before review. After this, every file is already in the preset's style, and a correct run changes only what a developer actually wrote. ## Step 2: scope every run Pint has two options that pick files from Git instead of scanning the tree: | Option | Files selected | Where it fits | |---|---|---| | `--dirty` | PHP files with uncommitted changes: staged, unstaged or untracked; deleted files skipped | a developer's machine, a pre-commit hook | | `--diff=<branch>` | PHP files added, modified or renamed since the merge base with `<branch>`, plus staged, unstaged and untracked ones | CI, or before opening a pull request | Details worth knowing: - Both lists are **filtered through Pint's finder**, so `exclude`, `notName` and `notPath` in `pint.json` still apply. - `--diff` compares against the **merge base** (`git diff <branch>...HEAD`), not the current tip of `main`, so commits that landed on `main` after you branched do not appear. - When nothing matches, Pint prints `No dirty files found.` or `No files have changed since branching off of main.` and exits 0. - If both are passed, `--dirty` wins. - Neither option changes the mode: `--diff=origin/main` alone still **fixes** files and exits 0. Add `--test` for a gate. ## Step 3: a pre-commit hook A hook runs before Git records the commit. The trap is that Git commits the **index** (what is staged), while Pint rewrites the **working tree**. A hook that runs plain `pint --dirty` fixes the files on disk, but the commit still records the unformatted staged version, and it may also reformat unstaged work you never meant to commit. Two safe shapes: - `./vendor/bin/pint --dirty --test` blocks the commit and tells the developer to run Pint and re-stage; - `./vendor/bin/pint --dirty --repair` fixes the files and still fails, so the developer reviews, runs `git add` and commits again. ```bash #!/bin/sh ./vendor/bin/pint --dirty --test || { echo "Pint: run ./vendor/bin/pint --dirty, re-stage, commit again." exit 1 } ``` ## Step 4: the CI gate In CI, run `./vendor/bin/pint --diff=origin/main --test`. Two conditions matter: 1. **History.** `--diff` needs `origin/main` and the merge base in the checkout. A depth-1 clone has neither, the underlying `git diff` fails, and Pint aborts with `The [--diff] option is only available when using Git.`, a message that sends people looking in the wrong place. 2. **The same Pint.** Install from the committed `composer.lock` so CI runs the exact release developers ran. Once the baseline has landed, some teams run the full-tree `pint --test` instead, since a formatted codebase makes a full check cheap and it also catches files that bypassed the hooks. ## What this buys The combination works because each piece closes one source of noise: the baseline removes old drift, the locked version removes tool drift, the hook stops new drift at the desk, and the CI gate catches whatever slipped past the hook. - Pull requests show only the lines a developer changed. - Reviewers stop commenting on style, because the gate already failed or passed on it. - `git blame` stays useful after the mass reformat. - A Pint upgrade becomes its own small, reviewable formatting commit instead of leaking into the next feature branch.

  • Why is plain pint --dirty a poor fixing pre-commit hook?
    Git commits the index, but Pint rewrites the working tree. The hook fixes the files on disk while the commit still records the unformatted staged version, and it may also reformat unstaged or untracked files you did not mean to commit. Use `--dirty --test` to block, or `--dirty --repair` and re-stage.
  • Why does locking the Pint version matter for formatting noise?
    Pint releases change how rules behave; Pint 1.28, for instance, improved `fully_qualified_strict_types`. If developers and CI run different releases, each reformats the other's code. Installing from a committed `composer.lock` makes every machine run the same release, and an upgrade becomes one deliberate formatting commit.
  • Does --diff=main check only the commits on the branch?
    No. It collects PHP files added, modified or renamed since the merge base with `main`, plus staged, unstaged and untracked PHP files in the working tree, then drops anything Pint's finder excludes. Deleted files are skipped.

saying these in an interview costs you the question

  • --dirty only checks the files already staged for the commit.
  • --diff=main compares against main's latest commit, so new main commits show up.
  • Running pint --diff in CI fails the build on drift without --test.
  • A depth-1 CI clone is enough for pint --diff to work.
  • Sharing pint.json is enough even if everyone runs a different Pint release.