After a tweet about how I like Git but find it hard to use, I thought it would be interesting to share how I use it on a daily basis. Including some cool aliases and practices you can borrow right away. It’s a bit long so here is a table of contents:
- Getting information
- Working with branches
- Doing some work
- Undoing some work
- Getting up to date
- Rewriting history
If you’re unaware what Git is, I wrote Git, the practical very basis on my brother’s blog where I explain the baby steps in version control. Check it out.
Getting information
I quickly realised there is no way to be comfortable with command-line Git in the default OS terminal. On macOS, I recommend installing iTerm2 and pimping it to display the branch name as part of the prompt. Also, colors. I mean, look at that beauty:
The command I type the most has to be git status
, and given how annoying that word can be, I have git s
for short. The other thing that’s very important, especially when rebasing is to be able to see what the history looks like.
There is git log
but that’s a very blend display of the past commits, not too mention unbearable to read. Because I like my Git logs to reflect what really happened, I have a git lg
that’s short for git log --pretty=oneline --abbrev-commit --graph --decorate
. I am not typing this by hand.
This creates a nice graph with the commits id, message, branch name, etc. Like this:
Working with branches
To quickly jump between branches, I created a few aliases. At N26, the master
branch is the protected release branch, and develop
is the main one—also protected. Everything goes through pull-requests against the main branch.
I aliased git checkout
as git co
and git branch
as git br
:
$ git br -D feature/my-old-feature
$ git co -b feature/my-new-feature
To make it easier to move to the master
and develop
branches (as it can happen quite a lot, especially develop
), I created the git com
and git cod
aliases respectively.
Doing some work
The basics of Git are adding some files to the index, committing the index in history, then pushing the history diff to the remote. Also known as “add-commit-push”.
I didn’t alias the add
command because it’s short enough that an alias is not necessarily going to bring me any value. I could alias to git a
but at this stage it would be more annoying to deal with muscle memory than typing these two extra characters. I did alias git commit -m
into git cm
though.
$ git add .
$ git cm "Replace a regular expression with a split in the forwarder"
$ git push
When it comes to pushing, I like to avoid having to type the name of the remote (usually origin
) and the name of the branch. Problem is Git 1.* uses matching
as a default configuration for the push
command without arguments. This pushes all branches using the same name locally and on the upstream repository.
Because this is a terrible default value (which has been changed in Git 2.* for safety reasons), I updated the push configuration in my .gitconfig
(and made all my coworkers do the same):
# See: https://git-scm.com/docs/git-config#git-config-pushdefault
[push]
default = current
Undoing some work
Git’s interface to undo things is unbearable in my opinion. git reset --soft HEAD^
, what the hell is that? So let’s see how I make undoing/redoing things easy.
To undo a commit entirely, I created a git undo
alias (short for git reset --soft HEAD^
) which deletes the last commit from history but keeps the changes in the index in case I want to do something with them.
To move things out of the index (the opposite of git add
), I have git wait
(for git reset HEAD
). And to remove things from the index entirely, I aliased git checkout .
into git abort
. I also had it under git nope
for a while. Not sure why I ever changed though, git nope
is gold.
So let’s say I realised my last commit was complete poppycock and I want to undo all of it and never speak of it ever again:
$ git undo # This undoes the last commit
$ git wait # This moves staged files out of the index
$ git abort # This cancels anything in the index
Getting up to date
Updating a branch with the main one is done through fetching and rebasing with the origin (or merging but that’s not my thing). I didn’t alias git fetch
, but I did create git rod
for git rebase origin/develop
—mostly because I never remember if it should be a space or a slash.
$ git fetch && git rod # Boom, up to date
Updating develop
with its remote counterpart is done through git pur
(or git purr
for when I feel particularly kitty) for git pull --rebase
. The --rebase
flag unsprisingly rebases the current branch on top of the upstream branch after fetching. This avoids a merge commit whenever I get up to date with the remote branch.
Rewriting history
When working on a branch, I commit frequently and tend to rewrite my commits many times. The goal is that once the feature is done, the branch history should be clean, helpful and explicit. Someone could start reviewing my PR by checking the list of commits and have a pretty good idea of what’s happening before even looking at the code.
To achieve that, I rebase a lot. I know a lot of people don’t like rebasing, and that’s a shame. Rebase is an outstanding tool to make sure the history of the branch you work on is meaningful. I don’t want to open that can of worms, but if you’d like my take on rebasing vs merging: rebase feature branches until they are clean, merge them into main branches. Been running like this for years including on projects with multiple developers and it’s been great.
Anyway, the point is: I do a lot of interactive rebases. My usual workflow looks like this: do a bit of work, do a commit, realise I forgot something therefore update the history (no “fix” commit with me). Eventually push the history onto the remote.
If the commit I want to update is the very last one in history, that’s rather easy: there is git amend
(short for git commit --amend --no-edit
). This simply adds what’s in my index to the last commit, without even asking me if I want to change the message.
$ git add path/to/file/i/updated.js
$ git amend
If the commit is further away in the history of the branch though, I need something more powerful. Usually, I rebase n
commits in the past. Git opens a Visual Studio Code tab/window (yep) to ask me what to do with all them commits. I edit and save this file to continue the rebase, until I’m done. Let’s unfold this.
The command to rebase n
commits is git rebase -i HEAD~n
but seriously, who has time for that? I created a git rb
alias that accepts a number argument. Here it is:
rb = "!sh -c \"git rebase -i HEAD~$1\" -"
I can use it like this:
$ git rb 2
I’m not a fan a Vim, so I made Visual Studio Code my editor for Git. You can do that by updating your .gitconfig
like so (provided code
is in your PATH):
[core]
editor = code -w
After running the git rb
command, a Visual Studio Code tab gets open with content like this:
pick a22f893d3 Inline outputPath and chunkOutputPath in the client-side configuration
pick 5b861eb7f Add process.env.STATS_MODE to configure stats option
# Rebase 2ec919432..5b861eb7f onto 2ec919432 (2 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit’s log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
I can change the pick
keyword to edit
(or the action of my choice). When I save and close this window (⌘S, ⌘W), the rebase starts and applies commit one by one, stopping on these I tagged for edition. On these, I can perform the changes I want, then add my files and do git rc
(for git rebase --continue
), until the rebase is complete. Note that I also aliases git rebase --abort
to git ra
and git rebase --skip
to git rs
for consistency.
At this stage, I might need to update the remote branch with the new history. To do so, I have a git force
alias which is a shortcut for git push --force-with-lease
. The --force-with-lease
argument is a seriously underrated option which protects all remote refs that are going to be updated by requiring their current value to be the same as the remote-tracking branch we have for them. Basically, it makes sure you’re not overriding someone else’s work.
So to sum up:
$ git rb 2
# Tag commits for edition in VSC, ⌘S, ⌘W
$ git add path/to/file/i/updated.js
$ git rc
$ git force
Wrappings things up
I have quite a few other Git tricks up my sleeve, but that will be for another article. For a complete list of my Git aliases, refer to my dotfiles repo.
Speaking of Git tricks, this is your reminde that my brother knows his shit and wrote on this very blog a 3-parts article on Git tips & tricks:
What about you, what are your Git secrets?