Back to all posts

Git for Trunk-based development

By Eli SchleiferNovember 30, 2022
General

Eli is co-founder and Co-CEO of trunk.io. He’s held engineering leadership positions at Microsoft, Uber, and Google (which acquired the startup he co-founded). His main focus is to help teams build better software, faster.

Developing in a collaborative environment is a complicated operation that involves tracking and coordinating work from multiple developers. Many of us use Git for version control to keep a handle on all these changes. But in Linus’s own words, Git is “the information manager from hell.” It offers so many options and weird states, you can easily find yourself in a bad place to the point where you need to re-clone from scratch — even if you did nothing wrong.

Many of us know that one developer who’s studied the Git refspec and the underlying Merkle tree it’s based on, but being a Git savant is probably not in your job description. I can drive a car perfectly fine without understanding how the transmission actually works, and at the end of the day, Git is just a means to an end.

To get the most out of Git, you must use it the least amount possible when it comes to trunk-based development. Limit the commands you use, keep your feature branch up to date properly, and standardize usage as a team. Individually, you may enjoy the freedom to do whatever you want. But as a team, the price for freedom is paid for in friction.

Limit your Git actions

The easiest practice to implement for peak Git efficiency is to stick to a subset of commands. Git can do many things, but the vast majority of its power only exists to help you get out of terrible situations. The number of commands that Git allows versus the number of commands you should actually ever invoke are quite different.

I was once asked by a CTO to give a class in advanced git to his engineering team. My response was simple:

by the time you need advanced git — the wheels have already come off car. lets focus instead on using core git correctly.

Here are the 10 git commands I use to get my work done while minimizing the number of times I land in Git hell:

Branch Management

git checkout -t -b {branch-name}
Make a branch, set the upstream to the current branch, and switch to it. You can also do this via the `git branch` command, but it takes multiple commands. Since I always want to switch to a branch when I create it, this command is more succinct. Create and checkout the branch in one fell swoop. I usually name all my branches eli/{work-being-done}.

git checkout {branch-name}
Switch to a branch.

git branch -vv
View your current branches and their upstreams.

git branch -D {branch-name}
Delete a local branch, usually to clean up stale/merged-into-main branches.

Pushing & Pulling

git fetch origin main:main
Pull the remote main into your local main, even when you’re not currently on the main branch! That’s the 🔑; very useful when you’re trying to merge the latest maininto a PR branch.

git push origin
Push my code to the remote; likely spinning up lots of machines in CI to check my work.

git pull
Update my local branch with the remote of the same name.

Committing

git add -A .
Add everything I’m working on (new and edited files).

git commit -am "work"
See here for a deep dive into my thoughts on local commit messages

git merge main
Lots more on this below. I am pragmatic and not a rebaser and happy to muddy my local branch with the goings-on from main.

Keep your feature branch up to date properly

In my last piece, I discussed how to manage landing code onto main. But often during development, and certainly before merging, you need to be up to date with the latest changes on main because:

  • You need some feature that landed in main to do your feature work.

  • Something in main changed and if you don’t pick it up, you’ll break it when you try to land your code.

  • You just want to stay fresh because you’re an engineer and love shiny new things.

So what’s the best way to keep your feature branch up to date with main?

😇 Door 1: git merge

This is my go-to merge strategy. It is simple, reliable, and no-frills. A basic git mergetakes all the changes that happened in main and merges them together as commits alongside your changes. You can read ad nauseam about the downsides of this no-frills solution, but I honestly just shrug and say “I don’t care about that.” If you squash-merge your work branches onto main then all that nonsense about git-log pollution becomes just that.

It honestly doesn’t really matter how/why/what came into my work branch. What matters is — does it build, does it work, and does the code do what I want it to do? The rest is academic.

In practice, and I am a very practical engineer, git mergeallows you to handle merge conflicts cleanly and get back to building. You resolve any merge conflicts once — and you’re good to go. This is the best option for beginners, busy advanced users, and those of you who don’t daydream about becoming Git gurus.

👿 Door 2: git rebase

This is the worst option by far. Okay, I know; there’s practically a whole generation who view rebase vs merge something like tabs vs spaces. But in “real” projects, where one or many teams are merging to a repo daily, rebase requires resolving n merge conflicts instead of one with a merge or squash rebase (more on that below).

Additionally, any rebasing rewrites Git history, and that means if your branch has a remote counterpart, you need to git push --force to stomp its history with your branch’s new rebased history. This is fine in theory but has the potential to accidentally delete your previous work in a way that’s extremely difficult to recover from. It’s simply much more dangerous than merging and honestly gives me a headache just thinking about it.

For the purpose of this article, I tried rebasing just to remind myself how much I dislike this workflow. Look at what a simple rebase command spews into my terminal:

There is so much wrong with that terminal. I get notes about commit hashes — don’t want to see those. I get four lines of yellow hints to remind me how this whole thing works. And I am now in some workflow just to get the code from main into my branch. The decision tree is silly complicated; git rebase --skip — what is that?! Why would it be okay to skip over a commit? You know what? Don’t tell me because I have work to do.

🧙 Door 3: git squash rebase

You definitely don’t want what’s behind door #2, but some people are diehard rebasers. My co-founder, co-CEO David Apirian, has shown me his secret backstage entrance to rebase and it’s kind of awesome. We’ll call this the squash rebase.

I’ve reiterated the importance of Git simplicity, but with this approach, you can have your rebase and eat it too. You get the magical layering of a rebase without the insanity of a standard rebase flow. With a squash rebase, you can:

  • See a diff of your local commits before pushing to GitHub.

  • Easily undo your last commit.

  • Avoid polluting GitHub’s view of your pull request with a ton of little local commits.

The secret sauce here is to squash all your local commits on your work branch before the rebase. This means you won’t have to resolve merge conflicts across all your local commits as a normal rebase would require. You get to rebase without all the hassle. Now this approach does take a bit of git wizardry, but maybe it’s just the right amount of magic

Step 1 — squash all your local commits:

1git reset --soft $(git merge-base HEAD main) &&
2git commit -am "" --allow-empty-message

Step 2 — rebase

git rebase main

You can put this all together into a single uber-command to pull the latest main, squash all your commits, and rebase on the latest main:

1git reset --soft $(git merge-base HEAD main) &&
2git commit -am "" --allow-empty-message &&
3git fetch origin main:main &&
4git rebase main

You can super geek out by turning this command into a set of aliases to supercharge your git flow. Add this block to your ~/.gitconfig file:

1[alias]
2 smartcommit = !git add -A . && git commit -am "" --allow-empty-message
3 squash = !git reset --soft $(git merge-base HEAD main) && git commit -am "" --allow-empty-message
4 squashrebase = !git squash && git fetch origin main:main && git rebase main
5 smart = !git smartcommit && git squashrebase

Now when you run git smart you’ll add all new/edited files, commit those changes, squash those commits alongside your other work to date, and ensure you’ve rebased the latest code from main onto your branch.

One of the cooler aspects of always squashing your local commits is that at the top of your git log is all the work you’ve done in the branch. You’re local work now more closely resembles what the squash merged PR will look like when it lands in main.

You can at any point pop the last commit off of HEAD with your local branch and see all the work you’ve done off of main. This is similar to the detailed view that GitHub gives you of what changed, but allows you to stay in your terminal and avoid confusingly toggling between two differing UIs.

I have to admit that the first time David showed me this workflow I was impressed. He had mostly slain the rebasedragon and the ability to locally view all the files you’ve changed in your branch is super cool.

While squash rebasing helps you maintain a better flow state, the overwhelming majority of developers are better off just merging. Using rebase or squash rebase incorrectly results in friction that kills productivity.

Pitfalls of GitHub branch help

GitHub offers this setting ☝️ to suggest merging main automatically into your branch if it’s out of date. This setting is often counter-productive. I want my local branch to be the only source of truth for my PR, and no one else (including GitHub) should be pushing to it. Working with GitHub on my personal branch should be a one-way street. I code locally and push.

If you configure GitHub to start pushing updates to your work branch you are asking for traffic to start heading in the wrong direction. And that’s a recipe for pain.

This problem arises as well when code reviewers leave you little commit suggestions on your pull request and you blithely apply them. If you then do any work on your local branch before pulling those changes…you’ve opened a little git bag of hurt.

I often discover I’ve accidentally done this to myself when I try to push my updates back to the remote and git tells me I’m out of sync. At that point, I have two options:

  1. git push --force and blow away anything on the remote not on your local branch. Generally anything with the word force is destructive and discouraged.

  2. git merge origin/{my-work-branch} to merge the remote changes into the local view of the branch.

In the spirit of git simplicity — I try very hard never to get into this mess, to begin with. Keep the one-way street always running in just one direction and you never will feel like you’re swimming upstream against the git current.

This GitHub setting ️☝️ requires PRs to be up to date with main before merging into it. This generally “solves” the potential to have a broken main; however, it only works with very low-velocity repos. Once a handful of developers try to merge multiple PRs per day, they wind up in a race to merge to main — or otherwise find themselves constantly performing a merge/rebase main into their branch to be “up to date” again, and basically race their coworkers to be the first to merge. Painful. Typically if you want this feature but you aren’t a solo developer, you probably want a merge queue instead — which is the scalable way to achieve no-broken-main. This is something we felt was important enough to productize with Trunk Merge.

Minimize Git for maximum value

By keeping your Git usage as simple as possible, you give developers less rope to hang themselves with and more time to do the development work that really matters. Make Git play a supporting role, and not become the protagonist. To maintain a sane and collaborative development environment, remember to:

  • Pick the smallest surface area of Git to use and stick with it.

  • Always manage your upstream changes in the same way.

  • Standardize practices across your team so your peers don’t run off the rails either.

Try it yourself or
request a demo

Get started for free

Try it yourself or
Request a Demo

Free for first 5 users