>
DevOps

Git’s Magic Files: The Hidden Configuration That Travels With Your Code

Git’s Magic Files: The Hidden Configuration That Travels With Your Repo

Git has a layer of files that most developers never touch. The layer is called “dotfiles” (the files that start with a dot) and the layer is configuration that travels with the repository itself, not with the developer’s machine. The layer includes .gitattributes, .gitignore, .gitconfig (in some cases), .gitkeep, and a few others. The layer controls how Git behaves in subtle ways. The layer can make merges work better, can prevent secrets from leaking, can normalize line endings across operating systems, and can speed up operations on large repositories. After working with these files for a decade, the configuration is the kind of thing that separates a polished Git workflow from a chaotic one. This is what each file does, when to use it, and the patterns that work.

What .gitignore actually does and does not

The .gitignore file is a list of patterns that Git will not track. The file lives in the repository root (or in subdirectories for scoped ignores). The file is version-controlled. The file is used for “do not track this file in the repository.” The classic use cases are: build artifacts (build/, dist/, *.o), dependency directories (node_modules/, vendor/), environment files (.env, .env.local), IDE files (.idea/, .vscode/), and OS files (.DS_Store, Thumbs.db). The file does not apply to files that are already tracked. If you accidentally committed a .env file and then added it to .gitignore, Git will still track the file. The mitigation is git rm --cached .env to untrack the file. The untracking is one command. The forgetting to untrack is a common bug. The common bug is preventable.

What .gitattributes actually does

The .gitattributes file is a list of patterns that control how Git handles specific files. The file lives in the repository root. The file is version-controlled. The file is used for “treat this file in a specific way.” The classic use cases are: line ending normalization (*.py text eol=lf, *.bat text eol=crlf), binary file detection (*.png binary), diff drivers for non-text files (*.docx diff=word), merge drivers for structured files (*.json merge=union), and export substitution (*.md export-subst). The file is the unsung hero of cross-platform development. The file prevents the “my line endings are different from yours” merge conflicts. The file prevents the “I can’t diff this Word document” problems. The file is the right call for repositories that are worked on by developers on different operating systems.

What line ending normalization actually does

The line ending story is the most common use case for .gitattributes. Windows uses rn (CRLF). macOS and Linux use n (LF). The mismatch causes problems. The fix is to tell Git which files should have which line ending. The pattern is: * text=auto (let Git detect), then specific overrides for known text files (*.py text eol=lf, *.sh text eol=lf, *.bat text eol=crlf, *.md text eol=lf). The result is that everyone sees the file with the line ending they expect. The result is that the file is stored in the repository with one consistent line ending. The result is that no one gets spurious “the whole file changed” diffs because of line ending differences. The result is the right answer. The result is well-documented. The result is not always implemented. The implementation is a 3-line file. The 3-line file is the answer.

What binary file detection actually does

Git automatically detects binary files (by checking for null bytes in the first 8000 bytes). The detection is good. The detection is not perfect. Some files look like text but are actually binary (e.g., UTF-16 encoded text, PostScript files, certain compressed files). The fix is to explicitly mark these files as binary in .gitattributes: *.psd binary, *.ai binary, *.utf16 text working-tree-encoding=UTF-16. The fix prevents Git from trying to do line-ending normalization on files that should not be normalized. The fix prevents Git from trying to do a text diff on files that should be diffed as binary. The fix is the right call for any file format Git misdetects.

What export-subst actually does

The export-subst attribute lets you put Git metadata into files at export time. The classic example is a VERSION file that contains the current commit hash. The pattern is: add VERSION export-subst to .gitattributes, then in VERSION write $Format:%H$ (or any other format string). When Git exports the repository (via git archive or via GitHub’s tarball download), the placeholder is replaced with the actual commit hash. The use cases I have used: version files, build manifests, deployment manifests, and changelogs. The use cases I have not used: anything that needs to be checked into the repository (since the export is on the consumer side). The pattern is powerful. The pattern is not commonly known. The pattern is the right call for any project that needs to embed Git metadata in files.

What .gitkeep actually does

The .gitkeep file is a convention, not a Git feature. Git does not track empty directories. If you want a directory to be tracked, it must contain at least one file. The convention is to add a file called .gitkeep to empty directories that you want to keep in the repository. The file has no special meaning to Git. The file is just a marker. The file is a workaround for a Git limitation. The file is used for “I want this empty directory to be in the repo.” The file is not the right call for “I want to track this empty directory in some special way.” If you need to track the directory contents, use a different approach (a .gitkeep file with a comment, a .gitignore file with a comment, or a .placeholder file).

What the practical patterns actually are

Five patterns I use in every repository I work on. The first is to add a .gitignore at the repository root on day one. The second is to add a .gitattributes at the repository root on day one, with at least * text=auto and the language-specific line ending overrides. The third is to never commit secrets. The .gitignore should include .env and similar files. The fourth is to use a pre-commit hook (via pre-commit or husky or a custom Git hook) to enforce the .gitignore patterns. The hook catches the “I accidentally added a .env file” bug. The fifth is to document the Git workflow in a CONTRIBUTING.md file. The documentation explains how to set up Git, how to configure line endings, and how to run the pre-commit hooks. The documentation prevents the “I didn’t know” problems. The patterns are not exotic. The patterns are the standard. The patterns are not always followed. The follow-up is the bug.

Here is the developer checklist I use before merging Git configuration:

  • Is there a .gitignore at the repository root? Add it on day one, not when secrets leak
  • Is there a .gitattributes at the repository root? Include * text=auto and language-specific overrides
  • Is the line ending story documented? Add a CONTRIBUTING.md section explaining the expected behavior
  • Is the pre-commit hook set up? Enforce the .gitignore patterns and the formatting
  • Is the audit log reviewed? At least monthly, scan the Git history for accidental commits of secrets

What this means for the day-to-day developer

The configuration files that travel with your repository are an investment. The investment pays off every time a new developer joins the project. The investment pays off every time you work on a different operating system. The investment pays off every time you wonder why a file changed. The files are small. The files are simple. The files are the right answer. The recommendation is to add the files on day one. The recommendation is to keep them up to date as the project evolves. The recommendation is to document them in CONTRIBUTING.md. The recommendation is to enforce them with hooks. The configuration is the kind of thing that does not feel important until it does. The day it does, the configuration is the difference between a smooth experience and a frustrating one. Use to invest the 30 minutes up front. The 30 minutes pays off for the life of the repository.

Filed under: #development #terminal

Leave a comment