This introduces people familiar with Git to trunk-based development, and vice-versa. I wrote it for work in reference to Github, but it applies to any Git web UI that supports pull requests. I’ve been told it’s a useful reference, so I’m posting a lightly-edited version publicly.
tl;dr: One idea is one commit. Implement trunk-based development using the standard Github branch and PR-based development process, defaulting to squash commits. Rebase onto
mainto resolve merge conflicts.
🚨 Do not merge
main into your branch! 🚨
Branch PR Workflow
Github documents a common workflow in this guide. We adapt the approach slightly:
- Please prefix your branches with your username, e.g.
- Unless you are a Git Pro (TM) and have specifically cleaned your branch’s history for clarity, merge using a squash commit. This should be set as the default merge strategy.
- Never merge
maininto your branch. Instead, rebase your branch onto
- Repositories should be set up to delete branches after merging, so that we don’t pollute the global namespace
- If you are opening a PR before it is ready for review, prefix the PR name in
- You are responsible for getting your own PRs merged. If review is blocked, it is on you to hunt down reviewers. If your PR has been approved but not yet merged, it is on you to actually click the merge button.
This depends significantly on your organizations engineering culture, and will likely need to be tweaked. For example, ZeroMQ uses optimistic merging.
Branches should be reviewed before merging. Unless explicitly noted in the PR description, approval from a single reviewer is sufficient to merge. If the code has an OWNERS file, you must request review from the owner. Approval is indicated using the “approve” button on Github.
LGTM % Approvals
Sometimes minor changes are suggested, but implementing these changes shouldn’t
require another round of reviews. We use “sticky” approvals, which means that
the approval stays even if the code changes. If you have minor comments about
style, spelling, etc. that don’t need further review, leave the comments and
approve the PR, except note in the acceptance message that everything looks good
aside from your comments, e.g. by writing
LGTM % comments (Looks good to me
mod comments). This indicates to the developer you expect that to make the minor
changes before merging.
Straight-To-Main (STM) Commits
Sometimes, it’s not worth the time to get a review. You shouldn’t do this all
the time, but trivial fixes can be pushed straight-to-main (STM). Please use
STM: in your commit message. If this breaks something, it is on
you to fix it. With great power comes great responsibility.
Why Squash Commits?
Squash merges condense all commits on a branch into a single-commit, and then “applies” that commit to the target branch being merged into. This causes a PR to look like a single commit after merge. As a result, there is no “merge commit” with two parents in the Git history.
In general, we want all commits to be reversible if they break anything, whether
it’s post-merge CI, staging, or production. One commit = one idea.
Realisitically, not all commits are revertable for correctness reasons (e.g. a
commit that does a destructive data migration). However, using a squash-merge
technique means that ops can revert any change by only specifying a single
commit hash. The lack of multi-parent merge commits keeps the
history linear, which is easier to trace historically, both in the Github UI and
using tools such as
git-bisect. This strategy in general aligns well with
The main downside to squash merging is the same as rebasing: the commit hashes change on merge. The lack of merge commits also means that your local Git client “can’t tell” that the branch has been merged, so you may receive additional warnings when attempting to delete the branch locally.
Like any guidelines, there are exceptions to this. Merge commits may be useful when:
- a branch is long-lived with a clean history
- an external repository is being imported into an existing repository
Remember, long-lived branches should be avoided. For large refactors and long-term work, prefer to branch by abstraction.
Just give me some commands!
Checkout an existing branch:
git checkout dadrian/my-existing-branch
Create a new branch relative to the current branch:
git branch dadrain/my-new-branch
Create a new branch relative to another branch:
git branch dadrian/rabbit-hole dadrian/my-existing-branch
Create a new branch relative to the current branch, and switch to it:
git checkout -b dadrian/my-new-branch
Create a new branch relative to specific branch, and switch to it:
git checkout -b dadrian/my-new-branch dadrian/my-existing-branch
Push a branch:
git push origin dadrian/my-existing-branch
Push a new branch for the first time:
git push -u origin dadrian/my-existing-branch # The -u prevents you from having to run git branch --set-upstream-to later
Switch branch to be based on latest main
Rebase a branch onto latest main (useful to resolve merge conflicts)
git checkout main git pull --rebase git checkout dadrian/out-of-date-branch git rebase main git push --force-with-lease origin dadrian/out-of-date-branch
Squash merge causes conflicts for dependent branch
Did you branch off main to create
pr-one, then branch off
pr-one to create
pr-two, then squash merge
pr-one and now you have a bunch of conflicts?
Usually the easiest fix is to rebase
pr-two onto main and “drop” each commit
that got squashed by merging
git checkout main git pull --rebase git checkout pr-two git rebase -i main # mark each commit merged as part of pr-one as "drop" git push --force-with-lease origin pr-two