Skip to content
ExponaBLOG
GIT & GITHUB FOR PROFESSIONAL DEVELOPERS · DAY 097 / 100

Git attributes for diffs and merges: a step-by-step guide

Custom behavior per file type.

Expona Admin7 min read

You've spent hours debugging a merge conflict only to realize Git tried to merge a compiled artifact or autogenerated file. You've also watched teammates accidentally commit build outputs or seen release archives bloated with test fixtures. Git attributes for diffs and merges: a step-by-step guide shows you how to configure custom behavior per file type, so Git handles generated code, binary assets, and deployment artifacts the way your team expects—not the way Git's defaults dictate.

Understanding .gitattributes and why it matters

The .gitattributes file is a configuration mechanism that tells Git how to treat specific files or patterns. Rather than applying one merge strategy or diff algorithm to everything, you can define custom rules per file type, extension, or path. This matters because:

  • Generated files (like minified bundles, transpiled code, or protobuf outputs) often create noisy diffs that obscure real changes.
  • Binary assets (images, PDFs, compiled objects) can't be merged line-by-line; you need custom handlers.
  • Certain files should never participate in merges—they should always use the version from one branch.
  • Release archives should exclude development clutter to keep deployables lean.

By the end of this guide, you'll have concrete patterns you can commit to your repository and use immediately.

Mark generated files as linguist-generated

One of the most common pain points: your frontend build pipeline generates a bundle file, but Git treats it like a real source file. Every time someone rebases or merges, Git flags conflicts in the generated code. You then have to decide whether to keep one version or rebuild it—tedious and error-prone.

GitHub's Linguist tool recognizes certain files as generated. When you mark a file as generated, GitHub's web interface will collapse diffs by default and exclude them from repository language statistics. More importantly, the linguist-generated attribute signals to your team and tools that this file is synthesized, not hand-authored.

Add this to your .gitattributes file:

bash
# Mark all build outputs as generated
dist/** linguist-generated
build/** linguist-generated
*.min.js linguist-generated
*.min.css linguist-generated

# Transpiled or compiled outputs
lib/generated/** linguist-generated
src/proto/*.pb.go linguist-generated

# Bundle analysis reports
.next/** linguist-generated
out/** linguist-generated

When a colleague opens a pull request on GitHub, they'll see these files dimmed or collapsed. More importantly, when running git log --oneline -- file.min.js or checking blame, the context is clear: this is generated, so don't spend time reviewing line-by-line changes.

In a real .NET project with an auto-generated client SDK, you'd add:

bash
src/GeneratedClient/** linguist-generated
*.generated.cs linguist-generated

This prevents the generated client from skewing your repository's language percentages on GitHub and signals to code reviewers to pay less attention to those diffs.

merge=ours for files you never want to merge

Imagine you maintain a configuration file that differs per environment: config.prod.json. When branches diverge and merge back, Git tries to merge the file content. But you never want to merge this file—production config should always come from the production branch, never from a feature branch.

The merge=ours strategy tells Git: when merging, always keep the version from the current branch and ignore changes from the incoming branch. This is perfect for environment-specific or deployment-time configuration.

bash
# Configuration files that should never merge
config.prod.json merge=ours
config.staging.json merge=ours
.env.local merge=ours
settings/local-override.xml merge=ours

# Version files managed by CI/CD
version.txt merge=ours
build.number merge=ours

When you merge develop into main, Git will automatically resolve config.prod.json by keeping main's version, with no conflict dialog:

bash
$ git merge develop
Auto-merging config.prod.json
Merge made by the 'recursive' strategy.

Compare this to without the attribute: Git would mark the file as conflicted, and you'd have to manually decide which version to keep.

A real-world scenario: your team uses package-lock.json (or yarn.lock) in npm. Different developers may install different transitive dependencies. Rather than fighting lock-file merge conflicts, you could use:

bash
package-lock.json merge=ours
yarn.lock merge=ours

Then after merge, run npm install or yarn install to regenerate the lock file properly. The merge completes cleanly, and your CI pipeline rebuilds the lock as needed.

Custom diff drivers for binary files

Binary files—PDFs, compiled objects, images—can't be diffed line-by-line. By default, Git shows: Binary files differ. That's often good enough, but for certain file types, you can plug in custom diff handlers.

For example, if your team commits PDF documentation, you might use pdftotext to extract text before diffing. Or for Microsoft Office files, you can use conversion tools. Define a custom diff driver in .gitattributes:

bash
# Custom diff driver for PDFs
*.pdf diff=pdf

# Custom diff driver for Office documents
*.docx diff=word
*.xlsx diff=excel

# Protobuf binary (show as text if available)
*.pb diff=proto

Then in your Git config (global or per-repo), define the driver:

bash
$ git config diff.pdf.textconv pdftotext
$ git config diff.word.textconv "python3 -m zipfile -e {} /tmp/word_extract && cat /tmp/word_extract/word/document.xml"

Or add to .git/config (repo-level) or ~/.gitconfig (global):

bash
[diff "pdf"]
    textconv = pdftotext

[diff "word"]
    textconv = pandoc --from docx --to markdown

[diff "proto"]
    textconv = protoc --decode_raw

Now when you run git diff or view a GitHub pull request, Git will attempt to show textual representation of the binary file. This is especially useful for design specs, generated documentation, or configuration files stored in proprietary formats.

In a trading platform, if you commit binary market data snapshots, you could register a custom driver that deserializes the binary format and displays human-readable output—making it possible to see what changed in a snapshot at a glance.

Export-ignore for release archives

When you run git archive to create a release tarball or zip file, you may want to exclude certain files: test fixtures, CI configuration, development-only scripts, or the `.gitattributes` file itself.

Use the export-ignore attribute to tell Git not to include these files when archiving:

bash
# Exclude development and testing files
.gitattributes export-ignore
.gitignore export-ignore
.github/** export-ignore
.gitlab-ci.yml export-ignore
.circleci/** export-ignore

# Test files
tests/** export-ignore
**/*.test.js export-ignore
**/*.spec.ts export-ignore

# Development utilities
scripts/dev/** export-ignore
Makefile export-ignore
docker-compose.yml export-ignore

# Documentation that's not for users
docs/architecture/** export-ignore
CONTRIBUTING.md export-ignore

Now when you create a release archive:

bash
$ git archive --format=tar.gz --output=release.tar.gz v1.2.0

The resulting release.tar.gz will not contain any of the files marked export-ignore. Your release artifact is cleaner, smaller, and focused on what users or deployments actually need.

A practical example: a NestJS API library you distribute. Your source includes:

bash
src/
tests/
.github/workflows/
scripts/benchmark.ts
README.md (for contributors)
docs/DEVELOPMENT.md

With export-ignore, the published archive includes only src/, a basic README, and the package.json—everything a consumer needs.

Common mistakes

Not committing .gitattributes to version control. If you define merge rules only in your local .git/config, teammates won't get those rules. Always commit .gitattributes to the repository root so everyone benefits.

Using merge=ours too broadly. If you mark everything as merge=ours, you'll silently lose incoming changes. Be selective: only mark truly environment-specific or infrastructure files.

Forgetting to install textconv tools. If you define a custom diff driver that calls pdftotext but that tool isn't installed on a developer's machine, Git will silently skip the conversion and show "Binary files differ." Test across your team's systems.

Setting linguist-generated on non-generated files. Don't mark hand-authored code as linguist-generated just to hide it. Use this only for truly synthesized files (build outputs, protobuf stubs, etc.). Misusing it obscures real code from reviewers.

Not testing export-ignore rules. Before shipping a release, run git archive --format=tar.gz HEAD locally and inspect the contents. Verify that excluded files are actually absent.

Key takeaways

  • Store .gitattributes in your repository root and commit it so all team members inherit the rules.
  • Use linguist-generated to mark synthesized files and reduce noise in pull request reviews on GitHub.
  • Apply merge=ours to environment-specific or deployment-time files that should never merge across branches.
  • Register custom diff drivers for binary or proprietary formats to enable meaningful diff previews.
  • Use export-ignore to exclude development clutter from release archives created with git archive.
  • Test your attributes: verify merges behave as expected, diffs render correctly, and archives contain only what you intend.
  • Document your .gitattributes rules in a team wiki or CONTRIBUTING guide so newcomers understand why certain files are handled specially.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles