Git on Roids

An overview of .gitconfig adjustments and Git add-ons that can improve workflows.

Git on Roids

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 difftoolnvimdiff 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.

E-mail

# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ 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 replacements 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 replacements, 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.