Two unrelated-looking things belong in one lesson because they have the same effect: they decide how much friction everyone else experiences when working in your repository.
Topic 1: Config Levels
git config --system ... # /etc/gitconfig — the whole machine
git config --global ... # ~/.gitconfig — you, everywhere
git config --local ... # .git/config — this repository (the default)
git config --worktree ... # this worktree only
The debugging commands, which resolve nearly every config surprise:
git config --show-origin --get user.email # value + the file it came from
git config --list --show-scope # everything, labelled by level
The identity problem worth solving once. Committing to a work repository with a personal email — or the reverse — is common, awkward, and entirely preventable with a conditional include:
# ~/.gitconfig
[user]
name = Your Name
email = personal@example.com
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
# ~/.gitconfig-work
[user]
email = you@company.example
[commit]
gpgsign = true
Now anything cloned under ~/work/ uses the work identity automatically. Note the trailing slash on gitdir:~/work/ — without it the pattern does not match subdirectories, which is the one detail that makes this fail silently.
Fixing an already-wrong author on unpushed commits:
git commit --amend --author="Correct Name <correct@example.com>" --no-edit
git rebase -i HEAD~5 --exec 'git commit --amend --author="…" --no-edit'
Both rewrite history, so both are for local commits only.
Topic 2: The Settings That Prevent Problems
Beyond identity, a small set of options remove whole categories of annoyance:
# Reconcile divergent branches by rebasing rather than merging.
git config --global pull.rebase true
# ...but never rebase a merge commit you made deliberately:
git config --global rebase.autoStash true
# Remember how you resolved a conflict; replay it next time it appears.
git config --global rerere.enabled true
# Better diffs on real code.
git config --global diff.algorithm histogram
git config --global diff.colorMoved zebra
# Make `git log` readable by default in this repo's context.
git config --global log.date iso
# Sort branches by recency instead of alphabetically.
git config --global branch.sort -committerdate
# Auto-correct obvious typos after a 1s pause (gti commit → git commit).
git config --global help.autocorrect 10
rerere deserves the emphasis it rarely gets. On a long-lived branch that you rebase repeatedly, the same conflict appears every time. With rerere enabled, Git records your resolution the first time and applies it automatically thereafter — turning a recurring tax into a one-off cost.
Aliases are worth a few lines, not a hundred:
git config --global alias.st status
git config --global alias.lg "log --oneline --graph --decorate --all"
git config --global alias.last "log -1 --stat"
git config --global alias.unstage "restore --staged"
git config --global alias.amend "commit --amend --no-edit"
A shell alias someone else cannot read is a private convenience. Keep the ones that shorten commands you type twenty times a day, and write the rest out so pairing and screen-sharing still work.
Topic 3: .gitattributes — The File That Prevents Line-Ending Wars
.gitignore decides what Git tracks. .gitattributes decides how Git treats what it tracks, and it is committed, so it applies to everyone.
# Normalise line endings in the repository; check out platform-native.
* text=auto
# Some files must keep LF regardless of platform.
*.sh text eol=lf
Makefile text eol=lf
# Never touch these — they are not text.
*.png binary
*.pdf binary
*.jar binary
# Better diffs for known languages.
*.go diff=golang
*.md diff=markdown
# Do not count generated files in language statistics or default diffs.
package-lock.json linguist-generated=true -diff
# Files that must never be merged automatically.
CHANGELOG.md merge=union
The CRLF problem, since it consumes a day of someone’s life in most mixed-platform teams: without text=auto, a Windows editor writes CRLF, commits it, and every subsequent diff on a Mac or Linux machine shows the entire file as changed. With it, Git stores LF in the repository and converts on checkout. Add .gitattributes at the start of a project, not after the first “why is this whole file modified” ticket.
binary on real binaries is not cosmetic either — it stops Git from attempting textual diffs and line-ending conversion on files where both are meaningless and one is corrupting.
Topic 4: What Makes a Commit Good
Two independent properties, and both matter:
Scope — one commit, one logical change. Not “one file” and not “one day of work”. A commit that changes an API and its callers together is one change; a commit that fixes a bug and reformats the file is two, and the reformatting hides the fix.
Message — the subject says what, the body says why.
fix(auth): reject tokens issued before a password change
A token stayed valid after the user reset their password, so a stolen
token survived the reset that was meant to revoke it.
Compare the token's issued-at against the user's password_changed_at
on every verify.
Refs: SEC-4127
The conventions, and the reason each exists:
- Imperative mood (“add”, not “added”) — matches Git’s own generated messages, like “Merge branch…” and “Revert…“.
- 50 characters in the subject — anything longer is truncated in
--oneline, in the forge UI, and in blame views. - A blank second line — tooling splits subject from body here. Without it, the whole message becomes the subject.
- The body explains why. The diff already shows what changed; it cannot show what you knew at the time, what you tried, or what constraint forced the approach.
- Reference the ticket, do not delegate to it. “Fixes JIRA-123” is worthless when the tracker has been migrated twice.
The test: you are bisecting a regression at 2am and land on this commit. Does the message tell you whether this is the culprit? “fix” and “updates” fail that test, which is exactly when it matters.
Conventional Commits (feat:, fix:, chore:, feat!: for breaking) buy machine-generated changelogs and automatic semver bumps. Adopt them if you will use that automation; adopting the format without the tooling is ceremony.
Topic 5: Amending and Fixing Up
git commit --amend # change the last commit's message
git commit --amend --no-edit # add staged changes to the last commit
git commit --fixup=9f8e7d # mark a commit as fixing an earlier one
git rebase -i --autosquash HEAD~5 # apply all fixups automatically
--amend replaces the last commit with a new one — new hash, new committer date. On unpushed work it is the cleanest way to fix a typo in a message or add the file you forgot. On pushed work it is a rewrite, and the next lesson stage explains what that costs.
The --fixup + --autosquash pair is the professional version of “address review comments”: each fix is a commit marked as belonging to an earlier one, and a final autosquash rebase folds them into place with no manual todo-list editing. The reviewer sees incremental commits during the review, and main receives a clean history.
Topic 6: The .git Files You Should Add to Every New Repository
.gitignore language-specific, generated from a template
.gitattributes at minimum: * text=auto
README.md what it is, how to run it, how to test it
CODEOWNERS who must review what (if the forge supports it)
.githooks/ shared hooks + core.hooksPath, if you use them
For .gitignore, start from a template rather than inventing one — a Node .gitignore that forgets node_modules is the classic first commit of a repository that then needs a history rewrite.
One rule that saves the most pain: never commit anything generated. Build output, dependency directories, compiled assets, lock-step artifacts. They create conflicts on every merge, they inflate every clone forever, and the reason people commit them (“the build is broken without it”) is always a build problem wearing a version-control costume.
Try it yourself: run git log --format='%h %s' -20 on a repository you did not write. Count how many messages tell you why. That ratio is the single best predictor of how painful the next incident in that repository will be.
Common mistake: setting user.email only globally, then discovering months of work-repository commits carry a personal address — visible forever in the public history of an open-source project, or failing a corporate DCO check on every commit. The conditional include takes two minutes and prevents a rewrite of hundreds of commits.