A well-honed Git configuration
By Christophe Porteneuve • Published on 3 April 2013 • 12 min

Cette page est également disponible en français.

Git offers 3 levels of configuration:

  1. Local. Specific to each local repository, it is stored in the .git/config. It includes branch tracking, remote definition, etc.
  2. Global. This is user-specific and is stored at the root of the user account, in the ~/.gitconfig file. This is the one we are interested in here.
  3. System. Usually stored in /etc/gitconfig, it is shared by all users. It is seldom used.

Priorities run in the usual order: from local to system.

Most users just put in their name, email and editor preferences into their .gitconfig. This is an appalling under-use. There are many, many aspects of Git that can be configured, and therefore improved, through configuration.

This article explores my personal configuration to show you many useful tips.

First of all, make sure you are using a recent Git (1.8+). Many of the configuration options discussed below have only recently been introduced. If an option seems to be ignored in your system, check the name and value carefully, and then look at the release notes to find the version in which the option appeared.

I maintain a generated public version of my configuration in this Gist. By “generalized” I mean that I remove, or comment out, some processing that I think is tricky to enable by default, because it involves subtle Git behaviors that you should know about before applying them.

A Git configuration file is similar in syntax to a good old-fashioned INI file: sections are delimited by a [name.section] header, and the property lines that follow belong to the section. Indentation is recommended but not required. The linearised representation of a property is section.name, e.g. core.whitespace for the whitespace property within the [core] section.

Here is the global gist already:

# Put this in your ~/.gitconfig or ~/.config/git/config
# Windows users: "~" is your profile's home directory, e.g. C:\Users\<YourName>
[user]
name = Your Full Name
email = your@email.tld
[color]
# Enable colors in color-supporting terminals
ui = auto
[alias]
# List available aliases
aliases = !git config --get-regexp alias | sed -re 's/alias\\.(\\S*)\\s(.*)$/\\1 = \\2/g'
# Command shortcuts
ci = commit
co = checkout
st = status
# Display tree-like log, because default log is a pain…
lg = log --graph --date=relative --pretty=tformat:'%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%an %ad)%Creset'
# Useful when you have to update your last commit
# with staged files without editing the commit message.
oops = commit --amend --no-edit
# Ensure that force-pushing won't lose someone else's work (only mine).
push-with-lease = push --force-with-lease
# Rebase won’t trigger hooks on each "replayed" commit.
# This is an ugly hack that will replay each commit during rebase with the
# standard `commit` command which will trigger hooks.
rebase-with-hooks = rebase -x 'git reset --soft HEAD~1 && git commit -C HEAD@{1}'
# List local commits that were not pushed to remote repository
review-local = "!git lg @{push}.."
# Edit last commit message
reword = commit --amend
# Undo last commit but keep changed files in stage
uncommit = reset --soft HEAD~1
# Remove file(s) from Git but not from disk
untrack = rm --cache --
[core]
# Don't paginate output by default
pager = cat
# Global ignore file (not shared)
excludesfile = ~/.gitignore_global
# Global shared hooks directory
# hooksPath = ~/.githooks
#
# Out of luck: on Windows w/o msysGit? You may have Notepad++…
# editor = 'C:/Program Files (x86)/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin
#
# If you want to use Sublime Text 2's subl wrapper:
# editor = subl -w
#
# Sublime Text 3 on Windows:
# editor = 'c:/Program Files/Sublime Text 3/subl.exe' -w
#
# VSCode
editor = code --wait
# Don't consider trailing space change as a cause for merge conflicts
whitespace = -trailing-space
[diff]
# Use better, descriptive initials (c, i, w) instead of a/b.
mnemonicPrefix = true
# Show renames/moves as such
renames = true
# When using --word-diff, assume --word-diff-regex=.
wordRegex = .
# Display submodule-related information (commit listings)
submodule = log
# Use VSCode as default diff tool when running `git diff-tool`
tool = vscode
[difftool "vscode"]
cmd = code --wait --diff $LOCAL $REMOTE
[fetch]
# Auto-fetch submodule changes (sadly, won't auto-update)
recurseSubmodules = on-demand
[grep]
break = true
heading = true
lineNumber = true
# Consider most regexes to be ERE
extendedRegexp = true
[log]
# Use abbrev SHAs whenever possible/relevant instead of full 40 chars
abbrevCommit = true
# Automatically --follow when given a single path
follow = true
# Disable decorate for reflog
# (because there is no dedicated `reflog` section available)
decorate = false
[merge]
# Display common-ancestor blocks in conflict hunks
# conflictStyle = diff3
# Disable fast-forward merges as default merge strategy.
# Force explicit call of `git merge --ff …`.
ff = false
# Custom/dedicated merge drive for npm lock files (`package-lock.json`).
# Also works with Yarn.
# Use Git attributes to set targeted files (located at `~/.config/git/attributes``
# when installed with npm).
# See https://www.npmjs.com/package/npm-merge-driver
# [merge "npm-merge-driver"]
# name = automatically merge npm lockfiles
# driver = npx npm-merge-driver merge %A %O %B %P
[mergetool]
# Clean up backup files created by merge tools on tool exit
keepBackup = false
# Clean up temp files created by merge tools on tool exit
keepTemporaries = false
# Put the temp files in a dedicated dir anyway
writeToTemp = true
# Auto-accept file prompts when launching merge tools
prompt = false
# [page]
# Use custom pager to get an better log on terminal.
# As this is an external tool it has to be installed.
# See https://github.com/so-fancy/diff-so-fancy
# show = diff-so-fancy | less --tabs=4 -RFX
[pull]
# This is GREAT… when you know what you're doing and are careful
# not to pull --no-rebase over a local line containing a true merge.
# rebase = true
# This option, which does away with the one gotcha of
# auto-rebasing on pulls, is only available from 1.8.5 onwards.
# rebase = preserve
# WARNING! This option, which is the latest variation, is only
# available from 2.18 onwards.
rebase = merges
[push]
# Default push should only push the current branch to its push target, regardless of its remote name
default = upstream
# When pushing, also push tags whose commit-ishs are now reachable upstream
followTags = true
# [rebase]
# Rebase advanced usage.
# Automagically reorder and prefix your commands while doing an interactive
# rebase. This has to be used with `--fixup` and/or `--squash` options for
# `git commit`.
# autoSquash = true
# Automatically stash current WD and stage when running rebase
# then you won't have to manually `git stash push` and `git stash pop`
# after and before your rebase.
# autoStash = true
# [rerere]
# If, like me, you like rerere, uncomment these
# autoupdate = true
# enabled = true
[status]
# Display submodule rev change summaries in status
submoduleSummary = true
# Recursively traverse untracked directories to display all contents
showUntrackedFiles = all
[color "branch"]
# Blue on black is hard to read in git branch -vv: use cyan instead
upstream = cyan
[tag]
# Sort tags as version numbers whenever applicable, so 1.10.2 is AFTER 1.2.0.
sort = version:refname
[versionsort]
prereleaseSuffix = -pre
prereleaseSuffix = .pre
prereleaseSuffix = -beta
prereleaseSuffix = .beta
prereleaseSuffix = -rc
prereleaseSuffix = .rc

Let’s now look at these settings one by one.

Default identity for commits

This is as basic as it gets: attributing your future commits. We use user.name and user.email for this. However, don’t mess up the Git config of a deployment user on a production server with this kind of stuff, if there are several people who can deploy. Instead, attribute your commit with git commit --author=... so you don’t alter the settings.

Coloring

Many Git commands are able to colorize their output using ANSI VT codes when the terminal allows it. The auto value has the same meaning as in many other Linux commands (e.g. ls): Git will detect whether it is being used by a VT terminal capable of handling color codes, and if so, will leverage that capability.

The color.ui property is an umbrella setting for all more specific color-handling properties, e.g. color.branch, color.diff, color.grep, color.interactive, etc. Basically, we want colors!

Aliases

Git only allows full command names by default (commit, checkout, etc.), which sometimes makes Subversion users cringe when starting with Git, searching in vain for their git ci. Apart from the fact that a properly configured prompt will offer advanced completion (that’s a topic for another post), the real reason is that Git lets you create as many aliases as you’d like, using the settings in the [alias] section. Each property is named after the alias, and its value is the Git command line without the leading git.

My configuration provides 3 aliases I couldn’t work without:

  • ci for commit
  • st for status
  • lg for advanced contextual log which, as far as I’m concerned, is just as good as graphical logs (branch graph and merges, symbolic references, tags and branch heads, SHAs, authors and relative timestamps). As a bonus, it works in a terminal, hence via SSH…

git lg display example

Pagination, whitespace and other “core” settings

By default, Git paginates everything it displays: if it exceeds the height of the terminal, it uses less. Personally, I can’t stand that: if I want to paginate, I | less myself! By specifying via core.pager to go through cat rather than less, I de facto disable paging.

When opening an editor (especially for commit messages or setting up an interactive rebase), Git chooses the editor like this:

  1. The GIT_EDITOR environment variable
  2. The core.editor setting (my favorite way)
  3. The VISUAL environment variable
  4. The EDITOR environment variable
  5. The default binary set at Git compilation time; usually vi.

If the default behavior isn’t your cup of tea, you need to specify the command line to run. When using a graphical editor, you’ll need to invoke it so that it’s in wait mode, meaning that it doesn’t yield back control until the relevant file is closed. Not all graphical editors let you do this.

Here is a list of some command lines!

☝️ If you are on Windows: if your terminal does not recognize the editor opening command, you should consider passing the full path to the executable (this usually depends on the terminal used).

  • VSCode: code -w
  • SublimeText: subl -w.
  • GEdit: gedit -w -s
  • GVim: gvim --nofork
  • Coda: you will need to setup coda-cli (usually through the always-handy Homebrew) then coda -w.
  • Notepad++: you may need to adjust path depending on where you installed Notepad++, but here is the idea: 'C:/Program Files (x86)/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin.

Finally, the core.whitespace setting prevents certain types of whitespace differences from being detected or causing problems for commands like git diff, git add -p, git apply or git rebase. There is a whole range of possible combinations; trailing-space is a combo for blank-at-eol (whitespace at the end of a line) and blank-at-eof (whitespace, especially empty lines, at the end of a file). As I am indicating this value here as a negative (note the - prefix), I am making it clear that this type of deviation should not be a concern or reported.

Enhanced diffs

By default, any variant of diff identifies the two versions as a and b :

[15:54] tdd@CodeWeaver:git-attitude (master *+%) $ git diff 2013-04-03-configuration-git.markdown

diff --git a/source/_posts/2013-04-03-configuration-git.markdown b/source/_posts/2013-04-03-configuration-git.markdown
index e69de29..44bd5ae 100644
--- a/source/_posts/2013-04-03-configuration-git.markdown
+++ b/source/_posts/2013-04-03-configuration-git.markdown

It is not always clear what a or b are… The HEAD? The stage? The working directory? Recently, Git provided a setting, diff.mnemonicPrefix, which when set to true will replace, where applicable, these anonymous letters with three others:

  • c for Commit (usually HEAD)
  • i for Index (our stage)
  • w for Working Directory

Thus, a regular diff explicitly states that it is performed between the stage (the index) and the working directory:

[16:02] tdd@CodeWeaver:_posts (master *+) $ git diff 2013-04-03-configuration-git.markdown
diff --git i/source/_posts/2013-04-03-configuration-git.markdown w/source/_posts/2013-04-03-configuration-git.markdown
index 202f830..cde050e 100644
--- i/source/_posts/2013-04-03-configuration-git.markdown
+++ w/source/_posts/2013-04-03-configuration-git.markdown

A diff --staged does indicate that it is done between the HEAD (a commit) and the stage (the index):

[16:02] tdd@CodeWeaver:_posts (master *+) $ git diff --staged 2013-04-03-configuration-git.markdown
diff --git c/source/_posts/2013-04-03-configuration-git.markdown i/source/_posts/2013-04-03-configuration-git.markdown
index cde050e..202f830 100644
--- c/source/_posts/2013-04-03-configuration-git.markdown
+++ i/source/_posts/2013-04-03-configuration-git.markdown

As for a diff HEAD, which makes its reference commit explicit, it confirms that it is comparing a commit and the working directory:

[16:02] tdd@CodeWeaver:_posts (master *+) $ git diff HEAD 2013-04-03-configuration-git.markdown
diff --git c/source/_posts/2013-04-03-configuration-git.markdown w/source/_posts/2013-04-03-configuration-git.markdown
index cde050e..3f85496 100644
--- c/source/_posts/2013-04-03-configuration-git.markdown
+++ w/source/_posts/2013-04-03-configuration-git.markdown

Another slightly unfortunate aspect of git diff is the default behavior of git diff --word-diff. Whilst it’s nice to have a default word diff mode, which lets you target the area of difference between two arguably very similar lines (much like the contrast change in graphical diff tools), Git defaults to considering any non-whitespace character sequence as “word”. In a block of code, that often creates a long display, since delimiters and operators will also be counted. For example, @twitterClient.directMessage(contact, is considered a word. Sigh.

A standard git diff --word-diff

This is why git diff provides another option, --word-diff-regex=..., which lets you specify a regex representing a “word.” Naturally, reducing this regex to . will attempt the minimal match (down to a single character). In practice, when I do a word-level diff, I always do this.

That’s the whole point of the diff.wordRegex setting: it allows a simple --word-diff to be treated as a --word-diff-regex=.... See instead:

A git diff --word-diff using the regex mode

Submodules

For those of you who are caught up in the day-to-day hassles of submodules (there are plenty of pitfalls in their day-to-day use), you can add a few safeguards through configuration:

  • fetch.recurseSubmodules automatically detects during a fetch (and thus a pull) if the submodule references have moved, and offers to retrieve them automatically if this is the case. Beware, it doesn’t call submodule update.
  • status.submoduleSummary reports within git status the submodules whose references have moved, listing the relevant commit deltas.

Extended regex as default

There are many use cases in Git where you can use regular expressions (regex). These include git log --grep and git log -G, to name a few. By default, however, only BRE (Basic Regular Expressions) are processed, i.e. most special characters such as {}, (), + and ? are ignored. In practice, when I use a regex, I always want to have Extended Regular Expressions (ERE), which enables all possible syntaxes.

Rather than having to add -E to my command line, I enable the grep.extendedRegexp setting in the global configuration.

Beware though: your regexes will not be case-insensitive. If you want to ensure this, you will usually have to add -i to your command lines.

Full SHAs are useless

Most Git commands and documentation pages use abbreviated SHA1s, or abbrevs, the first 7 characters only. The probability of colliding 7-character SHA1 values is infinitesimal (I’m not an expert, but it’s probably as low as 2−132, which means you’re only slightly less likely to run into a particular atom on Earth, including us; sounds like an acceptable risk).

I enabled log.abbrevCommit so that even a bare-bones git log uses that shortened form.

More convenient merges

Whilst Git does provide us with a plethora of mechanisms to best resolve a merge conflict, it is still possible to improve on the default behavior.

For example, Git injects classic conflict markers into text files. For example :

<body>
<<<<<<< HEAD
<h1>This is great</h1>
=======
<h1>This is beautiful</h1>
>>>>>>> feature
</body>

It is already possible to get more information at this stage, by displaying between the two (the HEAD branch and the merged branch) the version of the text in the common ancestor (divergence point, or merge base). This is the diff3 mode, which I automatically activate with the merge.conflictStyle setting:

<body>
<<<<<<< HEAD
<h1>This is great</h1>
||||||| merged common ancestors
<h1>This is nice</h1>
=======
<h1>This is beautiful</h1>
>>>>>>> feature
</body>

Related: when using a conflict resolution tool (a mergetool), it can generate temporary or backup files (often with extensions like .rej, .orig, .local, .tmp, .bak…), which happily smear your working directory. Even if these extensions are protected from being inadvertently added with a .gitignore or equivalent, the files remain there.

You can ensure that your tool will clean up after itself by disabling the mergetool.keepBackup and mergetool.keepTemporaries settings.

Finally, when you run git mergetool, it prompts by default for confirmation as to which file to open/check. To avoid this often-unnecessary question, disable mergetool.prompt.

Another important topic, on which we also have a detailed article, is ReReRe. This advanced Git feature allows you to save (locally) conflict fingerprints and the manual resolutions you made to them, and then automatically re-use those resolutions later. It’s extremely handy and enables very powerful workflows. You can explore the docs on this topic, and if you wish to use it, just enable rerere.enabled and, if you wish to automatically stage these automated resolutions, rerere.autoupdate.

Rebase your pulls by default

I wrote another article on why I think the vast majority of pulls should use a rebase rather than a merge. Basically, it’s to avoid messing up the history when several people commit to the same shared branch in parallel.

The git pull command is actually a sequence of two more specific commands: git fetch followed by git merge. For example, if you are on master with a default remote of origin, git pull is like doing:

  1. git fetch
  2. git merge origin/master

When you work with several people on that branch, it is usually best to do the equivalent of:

  1. git fetch
  2. git rebase origin/master

Git anticipates this scenario and lets you do this with git pull --rebase. However, I think this should be the general case, not the special case. It has indeed become possible to work this way by enabling the pull.rebase option.

The only pitfall here is that a rebase, by default, “inline” merges. This can be customized when you git rebase manually, but for a long time could not be tuned in the context of a pull. Let’s look at an example.

Let’s say you’ve just done a legit merge on your local branch (for example, the feature branch was finished and you merged it into master using a git merge feature).

The push is denied because it turns out that the remote master (origin/master) moved. You then must do a pull before continuing. If this is in rebase mode, that will automatically rebase the recent local history (commits from your old origin/master reference, including your merge commit) on top of the newer origin/master. In a “normal” situation this is fine, but here, it will inline the merge by replacing the true merge with a rebase. The feature branch, until then clearly visible in the log graph, now disappears.

Happily, you can now solve this using the rebase merges option (git pull --rebase=merges or pull.rebase = merges in your configuration).

Better-targeted pushes

By default, git push attempts to publish all your same-name branches to the remote. Whilst I very rarely have new work on multiple branches locally that are tracked on the remote, and thus need this, I do regularly run into the following scenario:

  1. I need to collaborate on the feature branch for a while: I git switch feature, which sets up a local branch tracked on the remote one.
  2. I work, I commit, I push, everything works…
  3. After a while, either I’ve only been working on feature for a while and my master falls behind the remote, or I’m done with feature until further notice, switch back to master and after a while my local feature falls behind the remote.

At that point, every push will balk, rejecting the lagging branch(es). It’s quite annoying, because I’m not actually trying to push those branches, I just have them locally and will probably work on them again later. This is the matching mode, which as the doc says is not appropriate for a multi-developer environment, but was (until Git 2.0 anyway) the default mode.

By setting push.default to upstream, I change the default behavior of push so that it only cares about the active branch. This is a more flexible variant of simple mode, which would require the local and remote branches to be not only connected (tracking) but homonymous. Note that this simple mode has been the default mode since Git 2.0.

Wrapping up…

Well, that’s a good thing done! Remember to look at the release notes when you update your Git, and especially to look at the git help config section from time to time, which lists all the available settings, or simply to see if the git help section of your current command doesn’t provide a CONFIGURATION section at the end, which details the relevant settings. You always learn a lot…

Good config to all!

More tips and tricks?

We’ve got a whole bunch of existing articles and more to come. Also check out our 🔥 killer Git training course: 360° Git!