Git Hacks to Replace a GUI

Posted by Stephen Cook

Up until recently, I’ve been a big user of SourceTree. Right off the bat, I want to be clear – I’m a big fan of Atlassian, and think they make great software – SourceTree included.

The mental-model used in designing git is very unintuitive, resulting in an interface that is needlessly complex, and with weird defaults. Generally this makes life for developers (especially junior developers) much harder than it needs to be.

To anyone starting out in software development using version control – I would still wholeheartedly recommend that they look at using SourceTree. SourceTree does a great job of abstracting away a lot of git’s complexity, and lets the developer just get on with their life.

This is why I am quite upset to come to the decision to move away from SourceTree, and start using git directly in the CLI.

In this post I want to quickly go over why I made the decision, and then focus on how I’ve set up my workspace, to ensure I don’t lose any of SourceTree’s powerful benefits.


Why stop using SourceTree? #

I don’t want to spend too long on the reasoning behind this decision here, as I don’t think it’s that interesting (and I’m hardly the only person in the world who’s decided that the CLI is the best fit for them).

So very briefly:

  • Performance issues: often the performance noticeably lags in SourceTree, especially when multiple windows are open. I assume this is due to a memory-leak or some such issue, as often I can resolve the issue by just quitting SourceTree and reopening it
  • Repo opening: there are little to no options for grouping, searching, or quickly opening + closing sets of windows – making the whole process a drag. This normally results in me having 10+ windows open after a while, and as previously mentioned — performance starts to suffer
  • Lack of Linux support (something that doesn’t look to be changing any time soon). My development OS of choice at home is a Linux, meaning I would be SourceTree-less whenever working on personal projects, or when working from home
  • Dependence: SourceTree is good enough that my actual knowledge of git during my time in using it has massively suffered. This mostly shows when using other people’s machines; I hate having to respond to a request for help with “I dunno man, I just use SourceTree”

But now that the why is out of the way – let’s get on to the fun stuff.

Using git without crying #

The reasons listed above are the negatives of using SourceTree, but ignore all of the negatives of using the CLI. Obviously if using the CLI is ultimately worse, and slows me down – then I don’t think any of the arguments above will ultimately overrule the fact that SourceTree is just easier and nicer.

Fortunately, I’ve been slowly optimising my workspace for the last few weeks and believe I’ve got to a point where I’m working as smoothly (if not more) as I was with SourceTree.

I’ve hosted my workspace configs on GitHub if you want to see my entire workspace, but I’ll go into depth with some of the nicer parts in this post.

As an aside, my aims when customising my workspace have been to add to the tools, rather than to modify or abstract away the default git commands. This way, if on a foreign machine – I’ll just be slightly slower, rather than entirely stuck.

General niceness #

Some workspace additions I’ve made that aren’t specific to git, but have come about because of my swap to the CLI, are the following:

  • tmux – I use tmux to manage shell. I’m a pretty light user, and just use it to segregate my terminal shells into sessions and windows. I have a git session, where I’ll have a different window for each git directory I’m working on
  • .gitconfig for aliases – I leverage my gitconfig file for shorthand aliases. Some of these I’ll go into later on in this post, but largely these are just shorthands like git s for git status, and git co for git checkout
  • .bashrc for aliases + autocompletion – I’ve enabled tab-autocompletion for git, and also aliased git to g. So to git status I actually just have to g s

Diffing #

One of the most important features for me is diffing. The default git diff command in terms of api is generally fine, but the output is super unreadable. I can read it, but I can’t easily glance through the output to get an idea of what’s changed.

I would often find myself sort of reading through the diff before raising a PR… Only to get to the (much more readable) diff-view in GitHub/BitBucket, and instantly see that I had missed unit tests for a file, or forgotten to refactor a method I hacked out during development.

To address this issue, I’ve employed the use of 2 scripts:

  • git diff – this is now set up to run through diff-so-fancy, and just makes the default terminal diff a lot more readable
  • git diffhtml – is aliased to run diff2html, which generates HTML that gets piped to the browser, with a GitHub-esque display

These scripts are so good, here are some print screens comparing the 3 outputs:

This is the normal git diff

This is git diff running through diff-so-fancy

This is the output of git diffhtml

Managing HEAD, workspace, and stage #

Outside of branches and specific commits, we have the git HEAD, workspace, and stage. Getting files between, and viewing the diff for these 3 states is something that should be so much simpler than it is. Best thing to do here is just to knuckle down and memorise the commands:

Adding #

  • HEAD → workspace: just write a change
  • workspace → stage: git add

All easy so far!

Removing #

  • stage → workspace: git reset HEAD <file>, this one always confused me because the file doesn’t get reset to the HEAD – it gets reset to the workspace. But the way to think about this is that it doesn’t change the file at all, it just changes how git looks at the file. It stops thinking about how it was staged, and goes back to thinking of it as it was back in the HEAD (unstaged), and then we just have our diff on top of that
  • workspace → HEAD: if the file is new, then rm <file>, since it’s nothing to do with git. If it’s a tracked file, git checkout -- <file>, which is effectively telling git to just checkout the file again (overwriting any work you’ve done)

Diffing #

  • stage vs. workspace: git diff --staged, a nice easy one
  • workspace vs. HEAD: this is where things get messy. If there is an untracked file (one that isn’t staged, and has never before been committed) then git fights you from seeing it a bit. If you do want to ignore untracked files, then just git diff is sufficient. To include untracked files, I’ve aliased git diffws to the following:

(which looks more complicated than it is, it just gets a list of untracked files, and then forces git to diff it to nothing)

Chunks + Lines #

In SourceTree it is super easy to stage, unstage, and revert particular lines, or hunks of a diff. The git CLI offers git add -p to enter patch mode. The UX here is understandably a lot less nice than SourceTree’s GUI, and I haven’t found any great solutions to improving this.

It sounds like diff-so-fancy could integrate itself into the patch mode diffs, which would be a lot nicer – but still involves a quite fiddly way of actually choosing the lines and patches to stage.

Log history #

Stealing from this StackOverflow, I have the following command aliased to git lg:

And although the command itself isn’t winning any prizes in terms of conciseness, its output is a pretty nice representation of the log history.

I really want to eventually add a similar html output alternative (similar to git diff and git diffhtml), possibly using GitGraph.js, but unfortunately there’s no solution currently to generate a GitGraph.js output, piping in input from git log.

Local branches #

Obviously just git branch to view your local branches. But I very often fail to keep on top of my local branches (forgetting to delete my branches after merging back to development). I always start off with the best intentions, but end up at the end of the sprint with 30 local branches checked out.

Which is why I have aliased git recent to the following git command:

This command produces an output like this:

So I can quickly see at a glance what branches I’ve been working with, and the name of the one I’m actually trying to check out.

Rebasing + Merging #

Taking inspiration from Buddy’s awesome article on some git aliases, I have the following commands set up to merge or rebase with a branch (but updating the target branch first)

This is an improvement upon the same problem in SourceTree, of having to first check out the target branch – pulling it, swapping back to the original branch, and then running your command to actually do the rebase/merge.

The commands are git pullbase and git pullmerge, which are both aliased to this script

(a different script for git pullmerge obviously, but with very similar structure)

A beast of a command, made worse with some if statements I had to put in to sidestep some weird bash behaviour when accessing variables of count 0, 1, or n+… But put simply, this command is just used as you would normally use merge or rebase, but it gets the name of the branch and runs a git checkout myBranch ; git pull ; git checkout - before finally running the merge/rebase command.

Wrapping Up #

As I say, I’ve been using these commands for just a couple of months, and have been getting on fine with them so far.

I certainly still miss some of SourceTree’s shininess, but less than I thought I might.

Have you recently stopped using a git GUI in favour of the CLI? Or do you have any cool workspace hacks? Share your story below in (the new, shiny) comments section!