Keeping GitLab Merge Request Diffs Readable in Large Migrations
AI generated
CI/CD
.yml
GitLab · Code Review · Migration
Large merge request diffs
kept readable: how reviewers stay on top of framework upgrades

A framework upgrade, an automated refactoring, or a formatting overhaul can change tens of thousands of lines in a single merge request, even though the actual content-relevant change makes up only a fraction of that. Without countermeasures, every reviewer drowns in noise and overlooks exactly the line that carries real risk. This article shows concrete techniques, from .gitattributes for generated files through clean commit separation to GitLab-specific diff view settings, that keep large, unavoidable diffs reviewable anyway.

16 min read Code Review · Merge Requests GitLab · Migration

1. The problem of large, unavoidable diffs

A PHP version switch, a Composer major upgrade, or introducing a new code formatter such as PHP-CS-Fixer almost inevitably produces merge requests with tens of thousands of changed lines, even though the bulk of that is purely mechanical, such as changed indentation, renamed methods, or automatically updated type declarations. A human reviewer going through such a diff line by line loses the ability to distinguish real content risk from pure formatting noise within a few minutes.

The actual risk rarely lies in the sheer volume of changes itself, but in the few lines hidden among thousands of mechanical changes that actually alter behavior, for instance an automatically generated but semantically wrong type conversion. A review process needs to reliably surface exactly those lines instead of letting them disappear into the general noise.

2. Configuring .gitattributes for generated files

Git and GitLab support the linguist-generated attribute through the .gitattributes file, which lets specific files or directories be marked as automatically generated. GitLab collapses the diff for files marked this way by default in the merge request view, so reviewers do not necessarily see them, but can expand them at any time if needed.

Typical candidates for this are automatically generated interception classes under generated/, compiled static content artifacts, or lock files such as composer.lock, whose diff is rarely relevant for a content assessment. It is important to apply the marking deliberately rather than broadly, because an overly wide linguist-generated rule can also hide changes that actually matter for review, which only relocates the real risk instead of solving it.


# .gitattributes at the project root

# Automatically generated Magento interception classes
generated/ linguist-generated=true

# Compiled static content artifacts
pub/static/ linguist-generated=true

# Lock files: mark as generated, diff stays viewable
# on demand
composer.lock linguist-generated=true
package-lock.json linguist-generated=true

# Exclude binary files from the diff entirely
*.png -diff
*.jpg -diff

3. Separate commits: keeping formatting apart from logic

The single most effective measure is packing purely mechanical changes, such as an automated reformatting run, into their own isolated commit that contains only that one operation and is never mixed with a content change. Such a commit ideally carries a clear, standardized commit message like chore: apply php-cs-fixer across src/, so it is immediately obvious that no logic change is to be expected there.

GitLab lets you view individual commits instead of the overall diff through a merge request's URL parameters, so a reviewer can deliberately skip the formatting commit and focus exclusively on the commits carrying content changes. This separation, however, only works if the commit history in the merge request is not squashed at the end, which is why squash merges should usually be deliberately disabled for large migrations.

4. GitLab-specific diff view tricks for reviewers

In GitLab's merge request diff view, pure whitespace changes can be hidden through the Show whitespace changes toggle, which by itself often eliminates the bulk of visible noise after a code formatter reformat. The file path filter at the top of the diff view likewise helps show only changes within specific directories, for instance only app/code/Mironsoft/ instead of the entire vendor tree.

For very large merge requests, GitLab additionally offers a per-commit view through the Commits tab, where every commit can be commented on and approved individually, instead of treating the entire merge request as a single, indivisible review unit. This commit-by-commit review practice noticeably spreads the cognitive load better than a single monolithic diff pass.


# Locally check which files outside generated
# directories were actually changed
git diff origin/main...HEAD --stat -- \
    ':!generated' ':!pub/static' ':!composer.lock'

# Show only commits after the formatting commit
git log --oneline <format-commit-sha>..HEAD

5. Handling Composer and package lock files sensibly in the diff

Lock files such as composer.lock or package-lock.json change almost entirely with every dependency update, because they contain hashes, version numbers, and dependency trees in a form that is barely readable for humans. A reviewer should not attempt to check this diff line by line, but instead look specifically at the top-level packages actually changed, as called out in the merge request text.

In practice it pays off to explicitly list a short summary of the most important version jumps in the merge request description, for instance symfony/console 6.2 to 6.4, guzzlehttp/guzzle 7.4 to 7.8, so the reviewer does not have to reconstruct from the unreadable lock file diff what actually changed content-wise.

6. Isolating automated refactoring tooling in its own pipeline job

Tools such as Rector for automated PHP code transformations or php-cs-fixer for formatting should run in their own, dedicatedly named CI job, whose output is introduced as a separate commit or even as its own, upstream merge request, instead of quietly getting mixed into the same commit as a functional change. That way it stays traceable at any time which part of a codebase change was machine-made and which was manual.

A proven pattern is a dedicated scheduled pipeline job that regularly runs Rector on its own branch and automatically opens a merge request containing exclusively mechanical changes. That MR can then be checked and merged with considerably less review effort, before the next content migration builds on top of it, instead of mixing both into a single, giant merge request.


rector_autofix:
  stage: maintenance
  script:
    - vendor/bin/rector process app/code/Mironsoft --dry-run=false
    - git config user.email "ci@mironsoft.de"
    - git config user.name "Rector Bot"
    - git checkout -b "rector/auto-$(date +%Y%m%d)"
    - git commit -am "chore: automated rector refactoring"
    - git push origin "rector/auto-$(date +%Y%m%d)"
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'

7. Merge request descriptions as a review guide for large migrations

For a large migration, it pays off to not only describe what was changed in the merge request description, but to explicitly state which files or directories a reviewer actually needs to check and which can safely be skipped. A GitLab merge request template with fixed sections such as Changes requiring manual review and Purely mechanical changes, no review needed gives every reviewer immediate orientation.

A link to a short review guide, for instance in the project wiki, helps further, describing in which order the commits are sensibly worked through and which automated checks, such as PHPStan or a Rector dry run, already ran ahead of time in the pipeline and therefore do not need to be re-verified manually.

8. Adapting the review process itself to large migrations

Instead of bundling an entire migration into a single merge request, a chain of several smaller, sequentially building merge requests usually proves more workable in practice, each with a clearly scoped purpose, for instance first the Composer upgrade, then the necessary code adjustments for changed signatures, then the actual new functionality. GitLab supports this through merge request chains with explicit target branches instead of a single main branch as the target.

For migrations that cannot be cleanly split over time, GitLab's draft status helps signal that a merge request is not yet review-ready, while initial comments on individual commits can already be collected in parallel. This prevents a reviewer from prematurely going through an unfinished, giant diff in full, even though parts of it will still change.

9. Checklist for the next large migration

Before opening a large migration merge request, a short self-check pays off: are generated files marked in .gitattributes, is formatting isolated in its own commit, does the description contain clear guidance for the reviewer, and is the commit history structured so it will not accidentally get squashed on merge.

The table below summarizes the key techniques once more and rates them by how much effort the setup costs and how large the benefit for review quality typically turns out to be.

Technique Setup effort Benefit for reviewers Especially useful when
.gitattributes linguist-generated Low, one-time High, diff auto-collapsed Generated directories, static content
Separate commits for formatting Medium, needs discipline Very high Framework upgrades, formatter rollout
GitLab whitespace filter None Medium Pure indentation changes
Chain of smaller merge requests High, more planning Very high Migrations plannable over time
Dedicated Rector pipeline job High, one-time High, long-term Recurring automated refactorings

Mironsoft

CI/CD pipelines, zero-downtime deployments and release automation

Deployments that run without downtime and without the nail-biting?

We review existing GitLab pipelines for fragile deployment steps and missing safeguards, then build a release process with zero-downtime deployments, automated checks and a rollback you can actually trust in an emergency.

Pipeline Review

Checking an existing .gitlab-ci.yml for fragility, missing stages and security gaps.

Zero-Downtime Deployment

Building symlink releases, health checks and rollback strategies for Magento stores.

CI/CD Automation

Connecting tests, security scans and deployments into one reliable pipeline.

10. Summary

Keeping Large Diffs Readable: The Essentials at a Glance

Core problem

Tens of thousands of mechanical line changes hide the few lines that actually change behavior.

Most important lever

Isolate purely mechanical changes into their own commits, never mix them with content changes.

GitLab tooling

.gitattributes with linguist-generated, the whitespace filter, and commit-by-commit review via the Commits tab.

Process level

Split migrations into several smaller merge requests instead of forcing a single indivisible review unit.

11. FAQ: Keeping Large Diffs Readable: The Essentials at a Glance

1What exactly does the linguist-generated attribute in .gitattributes do?
Files carrying this attribute are shown collapsed by default in the GitLab merge request view, but can be manually expanded at any time when needed. That reduces visible noise without hiding the information entirely.
2Why should I never mix formatting and logic changes in the same commit?
Mixed commits can neither be traced individually nor skipped selectively. A reviewer can no longer tell which line is purely mechanical and which is content-risky without reading the entire commit in detail.
3Does commit-by-commit review still work after a squash merge?
No, individual commit boundaries are lost in the target branch after a squash. For large migrations, squash merge should therefore be deliberately disabled, or at least turned off for that particular merge request.
4How do I hide pure whitespace changes in the GitLab diff view?
Through the Show whitespace changes toggle above the diff in the merge request view. That removes changes consisting only of altered indentation or spaces from the visible display.
5Should composer.lock be marked as linguist-generated?
Yes, generally it is worth doing, because the diff is barely readable for humans. The actually relevant information, which top-level packages changed, should instead go explicitly into the merge request description.
6How do I structure a merge request chain for a large migration?
Best with a clearly scoped purpose per merge request, for instance first the dependency upgrade, then necessary signature adjustments, then the new functionality, each targeting the previous merge request's branch instead of main directly.
7Does a dedicated pipeline job for automated refactoring actually pay off?
For recurring refactorings, yes, because it consistently keeps mechanical changes separated from content changes. For one-off migrations, a manually run dedicated commit is often enough.
8What belongs in a good merge request description for large migrations?
A short summary of the most important version jumps, a clear statement of which files actually need manual review, and a note about checks that already ran automatically, such as PHPStan or a Rector dry run.
9Can I exclude individual files from the diff entirely in GitLab?
Not directly from being shown, but -diff in .gitattributes can prevent Git from even attempting to compute a textual diff for specific file types such as binary files.
10How do I handle a merge request that is already open and unmanageably large?
Splitting it retroactively into smaller, sequentially building merge requests is effort, but often more sensible than trying to review the giant diff as is. Alternatively, at least a clear, retroactive review guide in the description helps.