I've always had a couple of issues with Git:
- 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.
- 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.
- 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:
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:
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.
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.
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'
Step-by-step explanation:
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)git log --oneline
- Gets its IDgit commit --fixup=
- Creates a fixup targetting this ID--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