[{"content":"Every repo has a .gitignore. Almost nobody has a .gitattributes. That\u0026rsquo;s a shame, because this one small file controls how Git sees, diffs, merges, normalizes, and exports your files. Once you know what it can do, you\u0026rsquo;ll want one in every project.\nWhat it is .gitattributes is a plain text file that assigns attributes to paths. Each line is a pattern followed by one or more attributes:\n*.sh text eol=lf *.png binary docs/** linguist-documentation The patterns work much like .gitignore, with two catches. Negation (!pattern) isn\u0026rsquo;t allowed. And a directory pattern like vendor/ does not apply to the files inside it, so you need vendor/**.\nEach attribute can be in one of four states:\nSet: text Unset: -text Set to a value: eol=lf Unspecified: !text, which resets it to as if you never mentioned it Where it lives Git reads attributes from several places, from highest priority to lowest:\n.git/info/attributes: local to your clone and never committed. Good for personal rules. .gitattributes files in the repo. A file in a subdirectory overrides one in a parent directory. Your global file (set with core.attributesFile, usually ~/.config/git/attributes). The system-wide file. When a file behaves strangely, ask Git what it thinks:\ngit check-attr -a path/to/file 1. End the line-ending wars This is the reason most people should add a .gitattributes today. Windows uses CRLF, everything else uses LF, and without rules you end up with diffs where every line \u0026ldquo;changed.\u0026rdquo;\n* text=auto *.sh text eol=lf *.bat text eol=crlf *.ps1 text eol=crlf text=auto lets Git detect text files and store them with LF in the repo. The eol= rules pin specific file types, which matters for shell scripts that break with CRLF and batch files that expect it.\nKnow this: adding these rules doesn\u0026rsquo;t fix files that are already committed. Run this once and commit the result:\ngit add --renormalize . To see the current line-ending state of every file:\ngit ls-files --eol 2. Mark binaries as binary *.png binary *.jpg binary *.zip binary binary is a built-in macro that expands to -text -diff -merge. Git won\u0026rsquo;t try to convert line endings, show a text diff, or attempt a text merge. That last one saves you from corrupted files after a merge that \u0026ldquo;succeeded.\u0026rdquo;\n3. Hunk headers that actually help When you run git diff, each hunk starts with a line like @@ -12,7 +12,8 @@, followed by some context Git guessed at. Often that guess is useless. Tell Git the language and it names the enclosing function or class instead:\n*.py diff=python *.rs diff=rust *.go diff=golang *.java diff=java *.md diff=markdown *.tex diff=tex These drivers are built in. There\u0026rsquo;s nothing to install. The Markdown one shows the nearest heading, which is great for docs.\n4. Diff files that aren\u0026rsquo;t text With a textconv driver, Git runs a command that turns a binary file into text, then diffs that text.\n*.png diff=exif *.pdf diff=pdf *.docx diff=docx git config --global diff.exif.textconv exiftool git config --global diff.docx.textconv \u0026#34;pandoc --from=docx --to=plain\u0026#34; Git appends the file path to the command, and the command must print text to stdout. exiftool and pandoc already do that. pdftotext doesn\u0026rsquo;t; by default it writes a .txt file next to the input. Wrap it in a tiny script:\n#!/bin/sh # save as ~/bin/pdf2txt and chmod +x it pdftotext -layout \u0026#34;$1\u0026#34; - git config --global diff.pdf.textconv pdf2txt Now git diff on an image shows which metadata changed, and git log -p on a Word doc or PDF shows actual prose changes. Add git config --global diff.pdf.cachetextconv true so Git doesn\u0026rsquo;t reconvert unchanged files every time.\n5. Silence noise you don\u0026rsquo;t care about package-lock.json -diff Cargo.lock -diff *.min.js -diff The files are still tracked and versioned normally. git diff just prints \u0026ldquo;Binary files differ\u0026rdquo; instead of four thousand lines of dependency churn.\n6. Merge strategies per file CHANGELOG.md merge=union union keeps the lines from both sides instead of creating a conflict. It\u0026rsquo;s perfect for append-only files like changelogs or lists of contributors. It\u0026rsquo;s dangerous for anything where order or structure matters, like code or JSON.\nYou can also keep your own version of a file on merge:\nconfig/local.yml merge=ours git config merge.ours.driver true Know this: a merge driver only runs when both branches changed the file. If only the other branch touched it, Git takes their version cleanly and your driver never runs. Plenty of people have been surprised by this.\n7. Clean and smudge filters This is the most powerful feature in the file. A filter has two halves:\nclean runs when a file is staged, transforming the working copy into what gets stored. smudge runs on checkout, transforming the stored version back into your working copy. *.ipynb filter=nbstrip git config filter.nbstrip.clean \u0026#34;jupyter nbconvert --clear-output --to notebook --stdout --stdin\u0026#34; git config filter.nbstrip.smudge cat git config filter.nbstrip.required true Now notebook outputs never get committed, but you keep them locally.\nReal-world tools built on filters:\nGit LFS stores large files elsewhere and commits small pointers: *.psd filter=lfs diff=lfs merge=lfs -text git-crypt transparently encrypts files in the repo and decrypts them on checkout nbstripout does the notebook trick above Know this: filter definitions live in Git config, not in the repo. .gitattributes only says \u0026ldquo;use the filter named X.\u0026rdquo; Every person who clones the repo has to set up the filter themselves. This is on purpose: if a repo could define commands that run automatically on checkout, cloning a stranger\u0026rsquo;s repo would be a security risk. Document your filters in the README.\n8. Shape your release archives git archive builds a tarball or zip of your repo. Attributes control what goes in it:\ntests/ export-ignore .github/ export-ignore .gitattributes export-ignore VERSION export-subst export-ignore leaves files out of release archives. GitHub\u0026rsquo;s \u0026ldquo;Download ZIP\u0026rdquo; and release source tarballs respect this too. It\u0026rsquo;s also one of the few attributes where a directory pattern like tests/ works, because git archive checks each directory as it walks the tree.\nexport-subst expands placeholders at archive time. Put this in VERSION:\n$Format:%H$ $Format:%cd$ The archive will contain the real commit hash and date.\n9. The GitHub extras GitHub reads a set of linguist-* attributes that change how your repo is displayed:\nvendor/** linguist-vendored dist/** linguist-generated docs/** linguist-documentation *.nx linguist-language=Rust scripts/*.txt linguist-detectable linguist-vendored and linguist-documentation exclude files from the language bar. linguist-generated also collapses those files in pull request diffs. For generated code, lockfiles, or build output, this makes reviews much faster. linguist-language fixes misdetection, or lets a custom language borrow another\u0026rsquo;s highlighting. linguist-detectable forces a language to count toward the stats when it normally wouldn\u0026rsquo;t. 10. Lesser-known attributes whitespace sets per-file rules for what git diff --check flags:\n*.md whitespace=-trailing-space *.py whitespace=trailing-space,tab-in-indent Markdown uses two trailing spaces as a line break, so you don\u0026rsquo;t want those flagged there.\nworking-tree-encoding stores UTF-16 files as UTF-8 in the repo so they diff properly, then converts them back on checkout:\n*.ps1 working-tree-encoding=UTF-16LE eol=crlf Only use it on files that really are UTF-16. With this line, git add refuses a .ps1 saved with a byte order mark (declare UTF-16LE-BOM for those) and one saved as UTF-8, which is what most editors write today.\n-delta skips delta compression for huge binaries that don\u0026rsquo;t compress well anyway, which speeds up packing:\n*.mp4 -delta ident expands $Id$ in a file to $Id: \u0026lt;blob hash\u0026gt;$ on checkout. It\u0026rsquo;s old-school, but handy for embedding a file\u0026rsquo;s exact version.\nlockable works with Git LFS file locking. Files are checked out read-only until you lock them, which prevents two people from editing the same unmergeable file:\n*.psd lockable 11. Macros In the top-level .gitattributes only, you can define your own bundles of attributes:\n[attr]lockfile -diff linguist-generated package-lock.json lockfile yarn.lock lockfile Cargo.lock lockfile One name, one place to change it later.\nKnow this: don\u0026rsquo;t put merge=ours in a macro like this. When both branches add a dependency, the merge succeeds without a conflict and the lockfile quietly loses the other branch\u0026rsquo;s entries. Regenerate lockfiles after a merge instead.\nA sane starter file If you take one thing from this post, drop this into your next project:\n# Normalize line endings * text=auto # Scripts that care about endings *.sh text eol=lf *.bat text eol=crlf *.ps1 text eol=crlf # Better diff hunk headers *.py diff=python *.md diff=markdown *.rs diff=rust # Binaries *.png binary *.jpg binary *.gif binary *.ico binary *.zip binary *.pdf binary # Quiet noisy files *.lock -diff package-lock.json -diff # Keep release archives clean .github/ export-ignore .gitattributes export-ignore Closing thought .gitignore tells Git what to leave out. .gitattributes tells Git how to understand everything you keep. It\u0026rsquo;s a few lines of text that fix line-ending chaos, make diffs readable, prevent merge disasters, and shape what your users download. Most repos never touch it. Now yours can.\n","permalink":"https://londopy.github.io/blog/posts/gitattributes/","summary":"\u003cp\u003eEvery repo has a \u003ccode\u003e.gitignore\u003c/code\u003e. Almost nobody has a \u003ccode\u003e.gitattributes\u003c/code\u003e. That\u0026rsquo;s a shame, because this one small file controls how Git sees, diffs, merges, normalizes, and exports your files. Once you know what it can do, you\u0026rsquo;ll want one in every project.\u003c/p\u003e\n\u003ch2 id=\"what-it-is\"\u003eWhat it is\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003e.gitattributes\u003c/code\u003e is a plain text file that assigns \u003cem\u003eattributes\u003c/em\u003e to paths. Each line is a pattern followed by one or more attributes:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-gitattributes\" data-lang=\"gitattributes\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s\"\u003e*.sh\u003c/span\u003e    \u003cspan class=\"na\"\u003etext\u003c/span\u003e \u003cspan class=\"na\"\u003eeol\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"l\"\u003elf\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s\"\u003e*.png\u003c/span\u003e   \u003cspan class=\"na\"\u003ebinary\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s\"\u003edocs/**\u003c/span\u003e \u003cspan class=\"na\"\u003elinguist-documentation\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\u003cp\u003eThe patterns work much like \u003ccode\u003e.gitignore\u003c/code\u003e, with two catches. Negation (\u003ccode\u003e!pattern\u003c/code\u003e) isn\u0026rsquo;t allowed. And a directory pattern like \u003ccode\u003evendor/\u003c/code\u003e does not apply to the files inside it, so you need \u003ccode\u003evendor/**\u003c/code\u003e.\u003c/p\u003e","title":"The Best Git Feature You've Never Used"},{"content":"Notes on security, systems, radio, and building things, written by Londopy.\nProjects: londopy.github.io Code: github.com/Londopy Feed: RSS Contact: message form, IRC, Discord Posts publish here first. Copies on other sites link back to the original here.\nWriting on this site is licensed CC BY 4.0. Code snippets are MIT. The source for the whole site is at Londopy/blog.\n","permalink":"https://londopy.github.io/blog/about/","summary":"\u003cp\u003eNotes on security, systems, radio, and building things, written by Londopy.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eProjects: \u003ca href=\"https://londopy.github.io/\"\u003elondopy.github.io\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eCode: \u003ca href=\"https://github.com/Londopy\"\u003egithub.com/Londopy\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eFeed: \u003ca href=\"https://londopy.github.io/blog/index.xml\"\u003eRSS\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eContact: \u003ca href=\"/blog/contact/\"\u003emessage form, IRC, Discord\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003ePosts publish here first. Copies on other sites link back to the original here.\u003c/p\u003e\n\u003cp\u003eWriting on this site is licensed \u003ca href=\"https://creativecommons.org/licenses/by/4.0/\"\u003eCC BY 4.0\u003c/a\u003e. Code snippets are \u003ca href=\"https://github.com/Londopy/blog/blob/main/LICENSE\"\u003eMIT\u003c/a\u003e. The source for the whole site is at \u003ca href=\"https://github.com/Londopy/blog\"\u003eLondopy/blog\u003c/a\u003e.\u003c/p\u003e","title":"About"},{"content":"Recruiter, collaborator, or just want to talk shop? Any of these reaches me.\nMessage form: londopy.github.io/contact. No account and no login, and your note goes only to me. Tick \u0026ldquo;encrypt my message\u0026rdquo; to encrypt it with PGP in your browser, end to end. IRC: Londopy on irc.libera.chat. Send /msg Londopy from any client, or straight from web.libera.chat with no account. I stay connected, so it reaches me even when I\u0026rsquo;m away from the keyboard. Discord: _londo. GitHub: github.com/Londopy My PGP key\u0026rsquo;s fingerprint is F010 53B8 6FE7 D60A 0A1F B790 B11E 02F1 12FA F497. The full public key is on the contact page.\n","permalink":"https://londopy.github.io/blog/contact/","summary":"\u003cp\u003eRecruiter, collaborator, or just want to talk shop? Any of these reaches me.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMessage form:\u003c/strong\u003e \u003ca href=\"https://londopy.github.io/contact/\"\u003elondopy.github.io/contact\u003c/a\u003e.\nNo account and no login, and your note goes only to me. Tick \u0026ldquo;encrypt my\nmessage\u0026rdquo; to encrypt it with PGP in your browser, end to end.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eIRC:\u003c/strong\u003e \u003ccode\u003eLondopy\u003c/code\u003e on \u003ccode\u003eirc.libera.chat\u003c/code\u003e. Send \u003ccode\u003e/msg Londopy\u003c/code\u003e from any client,\nor straight from \u003ca href=\"https://web.libera.chat\"\u003eweb.libera.chat\u003c/a\u003e with no account.\nI stay connected, so it reaches me even when I\u0026rsquo;m away from the keyboard.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDiscord:\u003c/strong\u003e \u003ccode\u003e_londo.\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eGitHub:\u003c/strong\u003e \u003ca href=\"https://github.com/Londopy\"\u003egithub.com/Londopy\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eMy PGP key\u0026rsquo;s fingerprint is \u003ccode\u003eF010 53B8 6FE7 D60A 0A1F B790 B11E 02F1 12FA F497\u003c/code\u003e.\nThe full public key is on the \u003ca href=\"https://londopy.github.io/contact/\"\u003econtact page\u003c/a\u003e.\u003c/p\u003e","title":"Contact"}]