Configuration, Identity and Commit Craft

Which config level wins, the settings that prevent whole categories of problem, and how to write commits that are still useful during a bisect three years later.

beginner 18 min lesson hands-on task included

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

CONFIG IS LAYERED — THE NARROWEST SCOPE WINS system /etc/gitconfig every user on the machine global ~/.gitconfig you, in every repository local .git/config this repository only worktree .git/config.worktree this worktree only ← WINS WHEN IN DOUBT, ASK WHERE A VALUE CAME FROM git config --show-origin --get user.email · git config --list --show-scope
Four scopes, narrowest wins. The command at the bottom is the one to reach for whenever a setting is not doing what you expect — it names the file the value came from.
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

A COMMIT MESSAGE IS WRITTEN ONCE AND READ FOR YEARS 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 Co-authored-by: … THE RULES THAT EARN THEIR KEEP · ≤ 50 chars, imperative mood· blank line — tooling depends on it· body says WHY, not what· the diff already says what· wrap at 72 columns· reference the ticket, not instead· of an explanation MESSAGES THAT COST SOMEBODY AN HOUR "fix" · "updates" · "wip" · "asdf" · "review comments" Every one of these is unreadable at the moment you need it: during a bisect. CONVENTIONAL COMMITS BUY AUTOMATION feat: · fix: · chore: · feat!: (breaking) → changelogs and semver bumps generated, not written.
Subject, blank line, body. The blank line is not style — `git log --oneline`, GitHub, and every changelog generator use it to separate the subject from the rest.

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.