Git on Roids
An overview of .gitconfig
adjustments and Git add-ons that can improve
workflows.
With the exception of few popular projects, the industry has unanimously agreed to Git being the de facto standard version control system. While Git comes with batteries included and usually doesn’t require a lot of configuration upfront to be usable, its git-config documentation shows the overwhelming amount of possible adjustments a user can make to change and optimize it.
In this write-up I’ll go through what I believe are some of the most useful
settings and extensions that will make #gitlife
easier. The demonstrated Git
config is certainly not comprehensive but might provide a good start to
beginners, as well as some valuable hints for advanced users.
The basics
My .gitconfig
resides under ~/.config/git/config
, instead of
~/.gitconfig
. This allows me to add additional files, like
~/.config/git/attributes
, without cluttering ~/
too much.
Let’s take a look at the general setup:
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ General ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[user]
name = マリウス
email = marius@xn--gckvb8fzb.com
signingkey = 4D3899AF73E7F5FE9B39C822272ED814BF63261F
[core]
fileMode = true
ignoreCase = false
symlinks = true
compression = 9
excludesFile = ~/.config/git/ignore
attributesFile = ~/.config/git/attributes
hooksPath = ~/.config/git/hooks
pager = delta
editor = nvim
[format]
signOff = yes
[gpg]
program = gpg
format = openpgp
[init]
defaultBranch = master
[commit]
gpgSign = true
verbose = true
[fetch]
prune = true
parallel = 3
[submodule]
fetchJobs = 3
[pull]
rebase = true
[push]
gpgSign = if-asked
default = simple
autoSetupRemote = true
followTags = yes
[help]
autoCorrect = prompt
The [user]
section should be obvious. signingkey
is the GPG key ID to be
used for signing commits/pushes. I always recommend signing commits
(commit.gpgSign
) to proof authenticity of the committed code. While
anyone could configure user.name
and user.email
to appear as
someone else, the GPG signature cannot be faked without possessing the private
GPG key.push.gpgSign
can be set to if-asked
, so that pushes are also signed if the
server supports it. Depending on your setup, it might also make sense to set
gpg.minTrustLevel
globally if you’re predominantly
working with critical repos. Alternatively, this can be set on a per-repo basis.
The gpg
section tells Git where to look for the GPG program and what format to
use when signing (openpgp
, x509
, ssh
).
format.signOff
enables the -s/--signoff
flag globally, adding
Signed-Off-by
to commits by default and thereby certifying the rights to
submit this work, under the specific project’s assigned meaning for this
developer certification. Since many open source projects require
sign-offs it makes sense to have it enabled to spare the -s
flag. For projects that don’t have an explicit DCO it’s simply a meaningless
line at the end of the commit message.
pull.rebase
is probably something that’s worth debating, as it might have
implications on your own workflow. I’m not going to go down the rabbit hole
here, but feel free to check online for the arguments against
it. This also is a convenience setting that you could use on a per-repo or
per-commit (--rebase
) basis instead.
push.default
, “defines the action git push
should take
if no refspec is given”. This is also depending on your workflow, but generally
the simple
setting has turned out to be the safest option and is the
default since Git 2.0.
help.autoCorrect
is a neat setting that allows you to mistype Git commands and
get a suggestion for what you probably meant to type. You can either set it to
prompt
, which will ask for confirmation before running what Git thinks you
intended to run; You can set it to a positive number that describes the
deciseconds (0.1 second) to wait before automatically running the corrected
command – allowing you to ctrl + c
in case it’s wrong; You can set it to
immediate
to run the corrected command right away; You can disable this
behaviour by setting it to never
; You can also use the default (0
), meaning
that Git will only show you what it thinks you intended, but not actually run
the command.
One setting you might be wondering about is core.pager
. The pager
is the
text viewer Git commands are using and is usually less
or whatever your
$GIT_PAGER
or $PAGER
environment variable specifies. I’m using
delta for that and I can highly recommend it. It’s packaged for most
platforms/distros but can also be installed using cargo install git-delta
and
it allows for a wide range of output format and color customizations. Let’s have
a look at the color
and delta
settings specifically.
Colors & output
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ Colors ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[color]
ui = true
[color "branch"]
current = yellow
local = blue
remote = yellow
upstream = magenta
plain = cyan
[color "diff"]
meta = blue
frag = magenta
old = red
new = green
whitespace = magenta
[color "status"]
added = cyan
changed = yellow
untracked = red
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ Delta (https://dandavison.github.io/delta/) ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[delta]
navigate = true
features = style
[delta "style"]
side-by-side = true
commit-decoration-style = "blue" box
dark = true
file-decoration-style = none
file-style = cyan
file-added-label = [+]
file-copied-label = [=]
file-modified-label = [~]
file-removed-label = [-]
file-renamed-label = [>]
hunk-header-decoration-style = "#022b45" box ul
hunk-header-file-style = "blue"
hunk-header-line-number-style = "yellow"
hunk-header-style = file line-number syntax
line-numbers = true
line-numbers-left-style = "#022b45"
line-numbers-minus-style = "#80002a"
line-numbers-plus-style = "#003300"
line-numbers-right-style = "#022b45"
line-numbers-zero-style = "#999999"
minus-emph-style = normal "#80002a"
minus-style = normal "#330011"
plus-emph-style = syntax "#003300"
plus-style = syntax "#001a00"
whitespace-error-style = reverse red
syntax-theme = Nord
Git colors can be customized using the color
setting and its sub-settings.
While the official documentation contains the full set of options, I
found the Atlassian write-up easier to comprehend.
The settings for delta below the color definitions specify how the output for
e.g. git whatchanged -p --abbrev-commit --pretty=medium
looks like. One of the
many benefits delta offers is for example the side-by-side
view, for which I’d
normally have to use git difftool
– nvimdiff
in my case.
Merging & diffing
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ Merging & Diffing ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[merge]
tool = nvimdiff
[mergetool]
prompt = true
[mergetool "nvimdiff"]
cmd = nvim -d "$LOCAL" "$REMOTE" "$MERGED" -c 'wincmd w' -c 'wincmd J'
[interactive]
diffFilter = delta --color-only
[difftool]
prompt = false
[diff]
tool = nvimdiff
With these settings I’m telling Git to use my favorite editor, nvim, as tool for
merging/diffing. For interactive commands like git add --patch
however, I’m
piping the diff through delta --color-only
.
URL rewrites
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ GitHub ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[url "ssh://git@github.com/"]
insteadOf = https://github.com/
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ GitLab ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[url "ssh://git@gitlab.com/"]
insteadOf = https://gitlab.com/
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ Codeberg ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[url "ssh://git@codeberg.org/"]
insteadOf = https://codeberg.org/
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ sr.ht ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[url "ssh://git@git.sr.ht/"]
insteadOf = https://git.sr.ht/
I’m using these rewrites on a global level but also have per-repo rewrites for on-prem Git servers that I better not upload to my dotfiles.
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ sendemail ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[sendemail]
annotate = yes
smtpserver = 127.0.0.1
smtpuser = marius@xn--gckvb8fzb.com
smtpencryption = tls
smtpserverport = 1025
smtpsslcertpath =
[credential "smtp://marius%40xn--gckvb8fzb.com@127.0.0.1%3a1025"]
helper = !pass smtp/marius@xn--gckvb8fzb.com
IRL I’d probably be looking into empty faces of many millennials, Gen Zs and As,
but indeed Git can send emails and requires the sendemail
configuration for the mail based workflow. It’s what allows you to collaborate by
sending patches as files via email, without having a centralized service like
GitHub that offers you a web UI for your pull request workflow. Although to be
fair, even open source projects implement a UI based PR workflow
these days, and it’s mostly the bearded software fundamentalists that still
heavily rely on this feature. I’m obviously being facetious here, but you’ll
nevertheless rarely find a reference to sendemail
in even the
most popular dotfiles for this reason.
A noteworthy tool built by those bearded software fundamentalists at
sourcehut is git-send-email.io. It’s a neat introduction
to the whole sendemail
topic and you should give it a try in case you haven’t
yet configured sendemail
.
Extensions
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ rerere ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[rerere]
enabled = true
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ Git Flow ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[gitflow "branch"]
master = master
develop = develop
[gitflow "prefix"]
feature = feature/
release = release/
hotfix = hotfix/
bugfix = bugfix/
support = support/
versiontag = v
By now everyone probably knows what Git flow is and while I, as well
as probably many others rarely use it these days, I nevertheless keep the
configuration in place. What’s more interesting in this snippet, however, is
rerere
, which is a somewhat lesser known feature.
rerere
helps resolve repetitive conflicts easily by recording the manual
resolution of conflicted automerges and applying them to successive conflicts.
Filters
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ go.mod replace ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[filter "gomodreplace"]
clean = rg -U -v 'replace ((?s)\\(.*\\)|.*)'
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ LFS ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[filter "lfs"]
clean = git-lfs clean -- %f
smudge = git-lfs smudge -- %f
process = git-lfs filter-process
required = true
As with the git-flow
extension, the lfs
filter should be fairly common.
What’s rather uncommon is this custom gomodreplace
filter. If you happen to
develop with Go, you might have had to deal with the
replace
directive from time to time. Often replace
is
used for local development, to tell one module to use a local version of another
module, which might contain changes that aren’t available upstream yet:
replace github.com/cloudbuild-sh/api => ../api
In order to not check in these replace
ments by mistake, I use this filter, in
combination with the following line in my global attributes file
(core.attributesFile
):
go.mod filter=gomodreplace
The filter makes Git basically ignore replace
directives in the go.mod
file, hence resulting in them not being committed to the branch. This filter
already saved me a lot of back and forth and I believe that
many people can relate.
It can also be adjusted (on a per-repo basis) to e.g. only act on specific
replace
ments, by adjusting the rg
pattern. It’s also possible to
make it work for other languages, like Rust, for example by removing path
from every line in the Cargo.toml
.
Filters: Bonus round!
One Git extension that I heavily rely on is git-crypt. It provides
transparent file encryption using GPG and it’s using .gitattribute
filters,
e.g.:
secretfile filter=git-crypt diff=git-crypt
*.key filter=git-crypt diff=git-crypt
secretdir/** filter=git-crypt diff=git-crypt
In this example, the file secretfile
, as well as all files ending with .key
and all files inside the folder secretdir/
will be encrypted using a GPG key.
Thanks to these filters, all files will remain unencrypted locally, yet will
end up being encrypted when committed and pushed upstream. You can
always check what is going to be encrypted and what not by using the
git crypt status
command.
A somewhat similar yet different add-on is git-secret by the way.
It is basically to secrets what git-crypt
is to files. Obviously git-crypt
could do both (by defining one encrypted file as secret storage), yet if
you’re only looking to encrypt secrets (e.g. environment variables required for
local development), git-secrets
is easier to use.
Aliases
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ Aliases ║
# ╚════════════════════════════════════════════════════════════════════════════╝
[alias]
a = "add"
aa = "add --all"
ce = "git commit --allow-empty"
am = "am"
ama = "am --abort"
amc = "am --continue"
ams = "am --skip"
amscp = "am --show-current-patch"
ap = "apply"
apa = "add --patch"
apt = "apply --3way"
au = "add --update"
av = "add --verbose"
b = "branch"
bD = "branch -D"
ba = "branch -a"
bd = "branch -d"
bl = "blame -b -w"
bnm = "branch --no-merged"
br = "branch --remote"
bs = "bisect"
bsb = "bisect bad"
bsg = "bisect good"
bsr = "bisect reset"
bss = "bisect start"
c = "commit -v"
ca = "commit -v -a"
cam = "commit -a -m"
cas = "commit -a -s"
casm = "commit -a -s -m"
cb = "checkout -b"
cf = "config --list"
cl = "clone --recurse-submodules"
clnid = "clean -id"
cmsg = "commit -m"
cn = "commit -v --no-edit"
co = "checkout"
cor = "checkout --recurse-submodules"
count = "shortlog -sn"
cp = "cherry-pick"
cpa = "cherry-pick --abort"
cpc = "cherry-pick --continue"
cs = "commit -S"
csm = "commit -s -m"
css = "commit -S -s"
cssm = "commit -S -s -m"
d = "diff"
dca = "diff --cached"
dcw = "diff --cached --word-diff"
ds = "diff --staged"
dt = "diff-tree --no-commit-id --name-only -r"
dup = "diff @{upstream}"
dw = "diff --word-diff"
f = "fetch"
fa = "fetch --all --prune --jobs=10"
fg = "ls-files | grep"
fl = "flow"
flf = "flow feature"
flff = "flow feature finish"
flfp = "flow feature publish"
flfpll = "flow feature pull"
flfs = "flow feature start"
flh = "flow hotfix"
flhf = "flow hotfix finish"
flhp = "flow hotfix publish"
flhs = "flow hotfix start"
fli = "flow init"
flr = "flow release"
flrf = "flow release finish"
flrp = "flow release publish"
flrs = "flow release start"
fo = "fetch origin"
hh = "help"
ign = "update-index --assume-unchanged"
ignd = "ls-files -v | grep "^[[:lower:]]""
l = "pull"
lg = "log --stat"
lgg = "log --graph"
lgga = "log --graph --decorate --all"
lgm = "log --graph --max-count=10"
lgp = "log --stat -p"
lo = "log --oneline --decorate"
logg = "log --oneline --decorate --graph"
loga = "log --oneline --decorate --graph --all"
m = "merge"
ma = "merge --abort"
mtl = "mergetool --no-prompt"
mtlvim = "mergetool --no-prompt --tool=nvimdiff"
p = "push"
pd = "push --dry-run"
pf = "push --force-with-lease"
poat = "push origin --all && git push origin --tags"
pr = "pull --rebase"
pristine = "reset --hard && git clean -dffx"
pu = "push upstream"
pv = "push -v"
r = "remote"
ra = "remote add"
rb = "rebase"
rba = "rebase --abort"
rbc = "rebase --continue"
rbi = "rebase -i"
rbo = "rebase --onto"
rbs = "rebase --skip"
rep = "grep --color=auto --exclude-dir={.bzr,CVS,.git,.hg,.svn,.idea,.tox}"
rev = "revert"
rh = "reset"
rhh = "reset --hard"
rmc = "rm --cached"
rmv = "remote rename"
rrm = "remote remove"
rs = "restore"
rset = "remote set-url"
rss = "restore --source"
rst = "restore --staged"
ru = "reset --"
rup = "remote update"
rv = "remote -v"
sb = "status -sb"
sd = "svn dcommit"
sh = "show"
si = "submodule init"
sps = "show --pretty=short --show-signature"
sr = "svn rebase"
ss = "status -s"
st = "status"
sta = "stash push"
staa = "stash apply"
stall = "stash --all"
stc = "stash clear"
std = "stash drop"
stl = "stash list"
stp = "stash pop"
sts = "stash show --text"
su = "submodule update"
sw = "switch"
swc = "switch -c"
ts = "tag -s"
tv = "tag | sort -V"
unign = "update-index --no-assume-unchanged"
up = "pull --rebase"
upa = "pull --rebase --autostash"
upav = "pull --rebase --autostash -v"
upv = "pull --rebase -v"
wch = "whatchanged -p --abbrev-commit --pretty=medium"
wt = "worktree"
wta = "worktree add"
wtls = "worktree list"
wtmv = "worktree move"
wtrm = "worktree remove"
Aliases are more of a personal preference. I’m using a similar set of aliases as
specified by the oh-my-zsh Git plugin, simply because I got used to
the commands and if I ever end up on a system that won’t have the ZSH aliases
in place, I could still use them directly on the git
command, e.g. ga
becoming git a
, grb
becoming git rb
, or with a simple alias g=git
in
place even g rb
.
Hooks
While there is no shortage of resources dedicated to Git hooks, I’m not using any global hooks whatsoever. Most of the things that these hooks would accomplish – e.g. reformatting or linting – are already sufficiently done by my editor, nvim. Everything else are hooks that are usually specific to individual project repositories and hence live on the server-side.
Add-ons
Besides the ones mentioned previously (git-flow
, lfs
, git-crypt
,
git-secrets
), there are a handful of add-ons that can greatly enhance your Git
workflow.
git-extras
git-extras are little extras for Git, that give you commands
like git summary
, for retrieving a short summary about a repo, git effort
,
for an overview of efforts that went into individual files, git changelog
for
generating a changelog between versions, and much much more.
git-stats
git-stats shows interesting contribution stats for individual contributors as well as whole repositories.
git-quick-stats
git-quick-stats is similar to git-stats
, yet a lot more
powerful in regard of the individual statistics it can gather. In case you’d
ever need to due diligence a repository, git-quick-stats
is what you’ll
probably want to be using.
git-user / git-profile-manager
git-user and git-profile-manager both basically do the same thing: Allowing you to have and switch between different user profiles.
git-standup
Everyone who’s still stuck in nonsense Scrum daily meetings, that require them to reiterate on what they’ve been doing on a daily basis will appreciate git-standup. This add-on can basically look back in time and list all commits that were performed, in an easily digestible overview. That is, of course, only as long as commit messages are descriptive enough and make sense.
git-jump
git-jump is a helper that implements fuzzy search on top of git switch
, to help jump branches even when you’re too drunk to hit the right
keys. Assuming a branch named hello-world
exists, git jump hlw
would switch
to it.
The full .gitconfig
I keep the full version of this .gitconfig
in
my dotfiles repository and I frequently update it. Depending
on when you might be reading this post it might already look differently to
what was shown here. You might nevertheless find something valuable that might
improve your own workflow.
Stay curious and happy committing!
Enjoyed this? Support me via Monero, Bitcoin, Lightning, or Ethereum! More info.