How I stopped Git from getting in the way

How I stopped Git from getting in the way

Git is a powerful tool but can be hard and cumbersome to use. Here is the ultimate workflow that will make you work faster and more confidently with Git.

πŸ“… November 13, 2023 (7 months ago) - πŸ“– 7 min read - πŸ‘€ 244 views

I've always had a couple of issues with Git:

  1. The average speed and brain power required to commit each commit and merge PRs is too high. In a perfect world, we wouldn't have to interrupt ourselves to think of a commit message every time we want to add changes to our WIP branch.
  2. When rebasing, conflicts are painful and get exponentially harder depending on how many commits you have on your branch. The more commits, the more conflicts you'll have to deal with to successfully rebase, and the more error-prone it becomes.
  3. We often fear diving deeper than the basics (push/pull/rebase) because it can be a scary tool, as messing up your Git state might make you lose your work and/or time. (I have been for years dreading this moment when a new Git error shows up, hinting that I messed up somewhere and I'll probably suffer for some time to get back to normal.)

The methodology I'll show you has allowed me to move faster and work more confidently with Git for years.

The basics

Merging strategies

As ByteByteGo explained so well in their video about git strategies, there are various ways to get your changes into main. This article assumes that, like most companies I've worked at, you use squash commits to keep a clean tree on your main branch.

Shortcuts

  • I useΒ Oh-My-Zsh shortcutsΒ for most of the Git commands because life is short, and so should your time spent writing Git commands.
  • Visit Zsh wiki to learn how to install it.

Let's start

Assuming you're working with squash commits in your repo, your tree should look like something like this:

Tree example

Initial commit

When we merge our PR, all of the commits on the branch we worked on will be squashed together into one. Whether we have 1 or 20 commits on our branch doesn't matter. The result will anyway be the same once merged.

Let's push a nice initial commit message that will describe what the PR will be about:

ga .
gcmsg "feat: 🎸 Build new chat components"
gp

Or without the nice shortcuts:

git add .
git commit -m "feat: 🎸 Create new chat components"
git push

(Also, to write commit messages even faster, I always have a list of commitizen-formatted commit messages in my clipboard manager)

Now we have one single commit on our branch:

Single commit example

A neat lil' trick: Fixups

As it turns out, you can, in Git, define new commits that target another parent commit using fixup. Simple explanation here. (Props to Janis for telling me about fixups!)

TL;DR: When you do that, Git can easily merge the target commit and the fixup in one commit when rebasing.

See where this is going? πŸ‘€

git commit --fixup=$TARGET_COMMIT_ID
gp # git push

What's really cool about this is that you don't think of a name every time you commit! This new commit will automatically take the target commit's name and prefix it with fixup!. Believe me, you'll already save quite some time here.

Commits during interactive rebasing

As you can see, by constantly targeting the initial commit in your branch and consistently applying fixups to it, you end up with quite the unusual tree.

Now, let's squash all of these commits together:

gfa # git fetch --all
git rebase -i --autosquash origin/main
gpf # git push --force

Using -i will trigger an interactive rebase, allowing you to see what will happen to each commit and --autosquash will automatically set all fixup commits to be merged with the first commit when you rebase.

Commits during interactive rebasing

Press CMD + enter in VSCode, and the rebase will get you a final clean tree with one single commit ready to be pushed on your branch and merged into main.

Imagine you want to rebase your branch onto main, but you run into conflicts. You will only have to resolve the conflicts on your single commit and not every commit on your branch!

Where the magic happens πŸͺ„

Now, fixups are cool, but how can we be even faster? Well, here is a pretty neat alias for you:

alias gcfixup='git commit --fixup="$(git log --oneline | grep -v '\''fixup!'\'' | head -n 1 | awk '\''{print $1}'\'')" --no-verify'
Add this to your favorite terminal config (~/.zshrc in my case), reload, and you can use it.

Step-by-step explanation:

  1. grep -v '\''fixup!'\'' | head -n 1 | awk '\''{print $1}'\'') - Looks up the last commit in your tree, which isn't a fixup (guess what, that should be your first commit)
  2. git log --oneline - Gets its ID
  3. git commit --fixup= - Creates a fixup targetting this ID
  4. --no-verify - Override git hooks. In general, we should aim at preventing harmful code from ever reaching main, and that's what CI is for, but nothing should disturb us during our creation process and force us to lose focus when pushing WIP code.

With this alias, here is how I usually work on a branch:

# Do some work...
gcmsg "feat: 🎸 Create new chat components" --no-verify # First commit
gp
# Do some more work...
gcfixup # Second commit, instantaneous
gp
# Do some more work...
gcfixup # Third commit, instantaneous
gp
# Done working!
git rebase -i --autosquash origin/main # Squash all commits, instantaneous
gpf
# Raise a PR πŸŽ‰

πŸ’₯Β BOOM. Simple and fast. ヽ(ο½€Π”Β΄)βŠƒβ”β˜†* MAGIC

Bonus: other Oh-my-Zsh shortcuts I use

glog # See tree
grba # Abort rebase
grbc # Continue rebase
gco $BRANCH_NAME # Git checkout to branch
picture of me
Written by Nathan Brachotte

I'm a Product Engineer at heart, I've helped many companies build great team culture and craft high-performance, customer-centric, well-architected apps.
✨ Always aiming for that UI & UX extra touch ✨