Dotfiles digest: git
There has been a slight buzz of git related content lately. From Scott Chacon’s So you think you know Git to Julia Evans’ article on Popular git config options. Both are very much worth your time if you use git somewhat often. I use git almost every day and I think it’s really great (Survivorship bias? Never heard of it), so this feels like great timing to make a post annotating my own configuration, which I have checked in to my dotfiles repository.
The point of this “series” (if I ever do another one!) is to document some of the choices I have made in configuring tools I use regularly. Both for my own benefit (some of these options were set a long time ago) and for beginners who are looking for a good starting point in their config. I like to keep my custom configuration pretty small.
It’s also a pretty good motivation to keep my dotfiles clean and checked in. 😬
Global configuration
This is $HOME/.config/git/config
. It’s common to use $HOME/.gitconfig
too,
but I think this is a bit neater because I use multiple configuration files.
More on that below.
[user]
name = Adrian Göransson
Well, that’s just me!
[alias]
ch = checkout
ci = commit
df = diff
l = log
ph = push
pl = pull
sh = show
st = status
rb = rebase
rbi = rebase -i --keep-base
f = fetch
fa = fetch --all
r = reset
rh = reset --hard
dfst = diff --stat
Okay, something more substantial! These are some of my most commonly used
commands, which is why I’ve configured aliases for easier use. I prefer
configuring these aliases in git as opposed to having shell aliases for every
subcommand like gl
or gph
. Using g
as an alias for git is plenty enough
for me.
Most of these aliases are pretty basic. fetch --all
fetches from all remotes
if you have multiple configured. Rebase with --keep-base
may not be very
common though, so let me try to explain what it does!
Rebasing with --keep-base
Some familiarity with rebasing is assumed here, otherwise check out the git book chapter on rebasing. With that out of the way, let’s consult the manual.
”[
--keep-base
] is useful in the case where one is developing a feature on top of an upstream branch. While the feature is being worked on, the upstream branch may advance and it may not be the best idea to keep rebasing on top of the upstream but to keep the base commit as-is.”
I think this is a pretty good explanation actually! For a bit more in-depth with visualizations, expand the section below.
More on git rebase --keep-base
You have a feature branch creatively named feature
that continues from main
on commit E
. You create a couple of commits and push your changes for review
by your team. The simplified commit graph looks like this.
A---B---C feature
/
D---E main
While you wait for your code to get reviewed, you start working on something
else. While you were developing feature
, commits F
and G
were merged to main
, resulting in the following graph.
A---B---C feature
/
D---E---F---G main
After some time, your feature
branch has been reviewed and you want to make
changes in response. You might find yourself in a place where you want to
rewrite the commit history (in your feature branch, don’t do this on a shared
branch like main
!), in which case you would use rebase
. In this case, we
are editing commit B
, creating B₂
in its place.
However, as your main
has advanced, rebase
will try to create this graph.
A---B₂---C feature
/
D---E---F---G main
If commits F
and G
happen to conflict with your work, you will get to
handle those as well as the potential conflicts that you may have created going
from B₂
→ C
. That can get really messy. If you have to go several rounds
of review and main
keeps advancing, this can get frustrating quickly. What
you probably would want to do instead, is use --keep-base
, yielding this
graph.
A---B₂---C feature
/
D---E---F---G main
In my experience, the conflicts towards the merge target are best handled when
the code has passed review and you are ready to merge. Only then do you merge
to main
, handling the potential conflicts during the merge, instead of during
rebases.
A---B₂---C feature
/ \
D---E-----F------G main
In my team, we have moved away from editing commit history and force pushing
during review, and are instead using fixup!
commits to mark where history
is going to get rewritten once the branch passes review. To do so with less
effort, I have used git-fixup and git-absorb.
Returning to dotfiles
[commit]
verbose = true
Commit with verbose mode is really nice. Your commit message editor gets populated with the about-to-be-committed diff as a comment, giving you a last look at what is being committed.
[gpg]
format = ssh
[commit]
gpgSign = true
[tag]
gpgSign = true
Always sign commits and tags. The few times I have used GPG it has always been a hassle, but no more with SSH keys!
[init]
defaultBranch = main
When creating a new git repository, use main
as the default branch name.
[log]
date = local
Show datetimes in my local time zone, instead of the timezone of the committer/author.
[tag]
sort = version:refname
Change the sort order of git tag
to sort tags numerically and respect version
numbers. This way, v10
comes after v9
instead of v1
.
A short example from one of my repositories.
default | version:refname
---------+-----------------
v0.1.0 | v0.1.0
v0.1.1 | v0.1.1
v0.1.10 | v0.1.2
v0.1.11 | v0.1.3
v0.1.2 | v0.1.7
[branch]
sort = -committerdate
Sort the output of git branch
to show most recently committed to branches
first.
[rerere]
enabled = true
I had heard tales of rerere
(REuse REcorded REsolution
) for a long time as
an invaluable tool to manage conflicts, but never bothered to read more about
it and discarded it, thinking it was something I had to learn and/or remember
to do manually. Turns out, after watching Scott Chacon’s presentation, it was none of those things! Basically, with rerere
,
git remembers how you have handled a conflict earlier and tries to apply that
same resolution again if it can. This is a life saver if you, like me, often
use rebase to clean/rewrite your local history. In those cases, one small
conflict can stack up and become something that you have to solve when
reapplying every commit.
[include]
path = config.local
Last, but not least, the [include]
directive! Having your dotfiles checked in
to version control can pretty quickly get messy, since you may want to set
options that should only apply on a certain device. For me, I have different SSH
keys on my work laptop, and repository-specific configuration as well, which
doesn’t make much sense on any of my other devices, or in version control. Sure,
sophisticated tools to manage your dotfiles (or even just git branches) may
solve this for you, but I appreciate the simplicity of just chucking things into
a plain git repository.
I do this with device-local files. With git, that means using [include]
.
Basically, I have a separate git config file called config.local
that git
will try to load. If the file doesn’t exist, git won’t complain, so it’s safe
to keep the directive even on devices where you don’t have a local
configuration file.
Local configuration
So how does it look? Here’s an excerpt:
[user]
signingKey = ssh-ed25519 [...]
[gpg "ssh"]
# $HOME does not work, so this is OS-specific :(
allowedSignersFile = /Users/adrian/.ssh/authorized_signatures
[pager]
diff = riff
show = riff
log = riff
[includeIf "hasconfig:remote.*.url:git@github.com:<organisation>/**"]
path = work.local
Here we see the aforementioned SSH key configuration. I also use the excellent riff
as a pager to highlight diffs. The reason that I have it
configured in config.local
is that git doesn’t play very nice if the
configured pager is not installed, so I prefer to set this option when I know
the command is available.
And finally, the powerful includeIf
directive, which lets me conditionally
load another config file based on various conditions. In this case a remote URL!
Basically, when I git clone <work repo>
, the repository is already configured
to use the configuration in work.local
as defaults. Pretty useful if you have
a bunch of microservice repositories.
Right now, mine is pretty boring:
[user]
email = <work email>
Because it is of great annoyance to me when I try to commit something in a new repository and git hits me with this:
Author identity unknown
*** Please tell me who you are.
Run
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
to set your account's default identity.
Omit --global to set the identity only in this repository.
Maybe I’m just a very easily annoyed person, I don’t know!
Wrapping up
I hope that this post can be of use to someone, either a beginner or someone who just hasn’t thought about their git configuration for some time. With an actively maintained project like git, cool new features are introduced quite frequently. It’s easy to miss out.
Until next time!