loading...
Cover image for Git tips for trunk-based development

Git tips for trunk-based development

alediaferia profile image Alessandro Diaferia Updated on ・5 min read

Trunk-based development is a source code branching model aimed at mitigating code integration and delivery risk. If implemented successfully, it helps reducing lead time in delivering value to production.

In this post, I'm not going to focus on the benefits of trunk-based development but rather, I'll try to outline a few examples of how git can be leveraged to achieve better results when implementing this kind of branching model.

git pull -r

With trunk-based development the master (or trunk) branch will likely update pretty often while you're off implementing your little feature in your own branch. In order to keep in sync, make sure your change-set always gets applied on top of the latest version of master:

1. Make sure you're on your feature branch:

$ git branch
master
* my-awesome-feature

2a. Now let's replay our changes on top of master on your local branch:

$ git pull -r origin master

This command makes sure the latest changes from origin are fetched before rebasing on top of them.

2b. Another option, if you want more control over your commits is to rebase interactively:

$ git pull --rebase=interactive

This will list all the commits that are going to be applied on top of master on your local branch:

pick 1ff6000 Brilliant commit
pick f144bad Even better commit
pick dc69aa1 Average commit

You can follow the instructions included in the editor to fine-tune your list of commits and maybe change something like squashing the last two commits.

pick 1ff6000 Brilliant commit
pick f144bad Even better commit
fixup dc69aa1 Average commit

NOTE: fixup will discard the commit message of the commit being fixed up

Temporarily parking your changes

You might have started developing something new locally when somebody from the team needs you to review their pull request. In order not to lose your progress you have to make sure you store it somewhere before checking out the changes to review.

git stash

git-stash helps you with this by stashing your changes locally.

$ git stash -u -m"Exploring a possible AI-based blockchain compiler 🤯"

The -u switch ensures that also untracked files get stashed, which is pretty neat.
Stashing does not require a message but I find it pretty useful when resuming my parked work.

Now you can make sure your changes are stashed by doing:

$ git stash list
stash@{0}: On ai-spike: Exploring a possible AI-based blockchain compiler 🤯

You're now ready to check out someone else's code. When you're ready to get back to what you were working on you can:

$ git stash pop

which will automatically apply the latest stashed change and remove it from the stashes collection (if no conflicts occur, otherwise you will have to manually git stash drop it once you resolve the conflicts).

Or, you can use

$ git stash apply

which will do the same as pop but without automatically dropping the changes from the stashes collection.

Both commands accept a stash reference (in the form of stash@{<index>}) which you can use to unstash specific changes.

git commit -m"WIP"

Another option, especially if you need to push your temporary changes to origin (maybe because you want to finish work from a different machine) might be temporarily committing everything and coming back later to clean up the branch history.

What you can do is:

$ git branch
* my-temp-branch
master
$ git add --all
$ git commit -m"[WIP] Current progress on AI-based blockchain compiler 🤯"

I like to prefix my commits with a common label like WIP (work in progress) when this is the case.

When you're ready to resume your work you can do:

$ git checkout my-temp-branch
$ git reset HEAD~1 # assuming you've stored everything in the last commit

reset will restore your current work directory state to how it was just before the commit, keeping the modifications of your WIP commit (unless you specify --hard).

git rebase

Even though I have mentioned rebase as part of the pull section, I'll use the pure git rebase command here to cover a slightly different case.

If you think the commit history of your current feature branch could use a little clean up git rebase might come in handy.

Some times you might have a couple of commits that could be squashed into one as well as maybe reordering a few other commits to give more clarity to the overall branch history. All of this can be achieved pretty easily using git rebase --interactive.

Let's say you want to review all the 7 commits you've got on your branch since master. (PRO TIP: If you don't remember how many commits you have diverging from master you can do: git log master... which will visualize only the relevant ones)

$ git branch
* lovely-branch
master
$ git rebase -i HEAD~7

Your configured editor will open up with your 7 commits waiting to be taken care of:

pick 30d730d It begins...
pick abd7151 Made some progress...
pick ba722fd Tests pass!
pick 06566b7 Added one more test case
pick 6bc75a2 Refactored class names
pick cb81607 Added docs
pick 2f9b368 Fixed typo in docs

We could rewrite history as follows:

pick 30d730d It begins...
sqaush abd7151 Made some progress...
fixup ba722fd Tests pass!
fixup 06566b7 Added one more test case
reword 6bc75a2 Refactored class names
reword cb81607 Added docs
fixup 2f9b368 Fixed typo in docs

Upon the first squash we will be requested to amend the commit message: we can specify something more descriptive like "New compilation target: WebAssembly". The subsequent fixups will lose their commit message.
Then, the first reword item will require us to specify a new commit message: "Renamed WASM* classes to WA* classes". Finally, the last rewordwill become: "Added missing documentation section for the WebAssembly target".

b981780 New compilation target: WebAssembly
16cde4c Renamed WASM* classes to WA* classes
451eaf2 Added missing documentation section for the WebAssembly target

A much cleaner branch history.

Use the --force (with lease)

If your branch had already been pushed to your remote before rewriting its history you will have to be explicit about overwriting it.

$ git push --force-with-lease lovely-branch:lovely-branch

This will overwrite the remote history of the current branch if it is as expected (e.g. it matches the local ref of origin).

Here's a more detailed explanation:

"What --force-with-lease does is refuse to update a branch unless it is the state that we expect; i.e. nobody has updated the branch upstream. In practice this works by checking that the upstream ref is what we expect, because refs are hashes, and implicitly encode the chain of parents into their value."

Unfortunately, --force-with-lease will not be able to protect you if you (or your IDE) does fetch in the background because git won't be able to detect any differences with the expected remote ref.

In general, with trunk-based development, the chances somebody else works on the same feature branch in parallel to you are quite low, if not non-existent. This is because with this branching model you are supposed to work on small changes at a time, enabling all the developers to work on different things (hence different branches) at the same time.

Conclusion

The commands I have presented are part of my usual development workflow. I hope this overview has been helpful to you and please, leave feedback and let me know how you use git to achieve trunk-based development.


If you want to hear more from me about software engineering practices follow me on Twitter.

Photo by Dries Augustyns on Unsplash

Originally published on Clubhouse.io

Discussion

pic
Editor guide
Collapse
tonivdv profile image
Toni Van de Voorde

Feature branching is when you finish a full feature and then only you merge it back to trunk/master. When doing this on a very big feature, this will lead to long-living branching, which is a real pain ... especially when working with multiple developers on a project.

However, when you e.g. doing agile and split a feature/epic into multiple (dev) stories/tasks , the idea is that every time a story is finished it is merged in trunk/master. Obviously if this merged story cannot be seen by the end user, you'll have to come up with solutions like e.g. the use of feature flags/toggles.

So yes, those tips of Alessandro are very valid. I use all of them on my project where we are doing trunk based development with 10 developers ;)

tonivdv profile image
Toni Van de Voorde

We could but we have some pre-requisites before doing that:

1) reviews through PR => ensures knowledge transfer and code quality
2) ci => ensures tests are passing, code quality/styling, and many other stuff
3) pushing to remote branch while not finish, prevents potentially losing code in case of theft of portable during home<->work trip, portable breaks, etc ..

And imagine 10 developers pushing without any checks on master ... yes it's called "trunk based development", but you should not take that literally ;)

Also, our master is protected to any modification involving history re-write. Going through a branch allows more flexibility ...

To give you an idea, our branches live approximatively 1 to 3 days tops ... In the past when we did not have feature flags in place and had to wait a feature was releasable that branch could stay for weeks and in some cases months ... And that's the big difference.

Collapse
tsalikispk profile image
Kostas Tsalikis

Great post, thank you! There is also a way to configure pull commands to rebase and auto-stash by default:

git config --global pull.rebase true
git config --global rebase.autoStash true

(credits to cscheng.info)

Collapse
mpern profile image
Markus Perndorfer

I'm all for trunk-based development, but here is an very intersting take why one must not use rebase and other history-altering commands:

fossil-scm.org/xfer/doc/trunk/www/...

Collapse
fly profile image
joon

Never really tried to study git in-depth because sourcetree did everything I needed pretty much all the time. Might need to reconsider after reading this. Thank you for the post, a very informational read! :)

Collapse
sivaraam profile image
Kaartic Sivaraam

Just wanted to post a little note about the following point:

Both commands accept a stash reference (in the form of stash@{}) which you can use to unstash specific changes.

You don't have to explicitly specify git stash apply stash@{1} (or) git stash pop stash@{0}. Just passing the number would work fine too i.e., you can use them as follows:

# Pops (apply and drop) the last stashed change
git stash pop 0

# Applies the last stashed change
git stash apply 0
Collapse
alediaferia profile image
Alessandro Diaferia Author

Love it, thank you!

Collapse
alexisfinn profile image
AlexisFinn

I kindof use this at work:

  • branch
  • commit + push
  • commit --amend + push --force (to try and keep one commit)
  • rebase --interactive HEAD~x|sha1 to squash commits into one (this shouldn't happen because of the commit --amend, but well it does)
  • rebase master

---> go on to Merge Request.

The main problem with this approach is that while it does keep everything pretty clean, it messes up the history, crippling commands such as:

  • git blame: author can't be trusted because of squashing commits
  • git bisect: still works but since commits tend to become huge, it doesn't help much
  • git revert: same problem, commits tend to become huge so reverting a whole commit is usually out of the question

It's also pretty dangerous because when doing a push --force, if you don't pay attention at some point... well you're pretty much F'd (actually you can go explore the reflog, but that shouldn't happen)

So whereas it does make things more streamlined and easier to manage cherry-picking features into a production branch, it seriously diminishes the capabilities and reliability of the versioning system.

So i'm not saying its a bad process, I actually kind of like it, but boy does it get stressfull when the new junior dev/intern starts working because there's the potential to mess things up real bad real quick.

Collapse
tonivdv profile image
Toni Van de Voorde

If you are using a cloud solution like github/bitbucket you can protect your branches to avoid messing it up.

About the history being messed up, well this usually happens when working on long living branches. Here the idea is that you don't have that, so there shouldn't be different authors working on a branch. And even if that is the case, it's not mandatory to squash to merge back to master once it is done. You can even keep the full history if that is what you prefer.

We tend to not squash doing PR reviews ... we only squash when putting everything back in master. And that is only in the case of small short lived branches. In very rare cases we didn't squash.

Collapse
alexisfinn profile image
AlexisFinn

I get that the method I'm currently using (abiding by?) is not exactly the same thing, and I actually really like rebasing and commit --amending or squashing. I think it makes for a clean, easier to manage repository.

That being said I just thought I'd point out some of the potential drawbacks to going this route which has a lot to do with User error.

I've met a lot of devs who had a hard time visualizing what was happening with Git, either because they're used to SVN or just because it's not an easy tool to apprehend.

I've also met a good number of project managers / client demands that really don't lend very well to small iterative branches and commits, I have often ended up working several weeks on a single feature.

So whereas I truly like this approach, it should be carefully thought over if your team is not very proficient with git or your manager prefers long all-encompasing features and branches over small ones.

But of course you're right that there are many ways in which you can safegard against dramatic problems or event circumvent them entirely through a well thought out branching tree, and that is one of the beauties of Git: It can pretty much do it all.

Collapse
itayronen profile image
Itay Ronen

The author did not recommend to squash all your commits into one.
I recommend you to do use commits to split your work and make PR easier and history cleaner.
Use rebase to edit your commit history and apply it on top of master.
Then when you go to PR, the reviewer will see your (edited) commits, on top of master.

Collapse
itayronen profile image
Itay Ronen

I agree the article is not really about Trunk-based development, but it does not negate it.
You can work on a feature branch and merge to the "Trunk" (master).
trunkbaseddevelopment.com/short-li...

Collapse
dricomdragon profile image
Jovian Hersemeule

Thanks for this article ! A comment about your PRO tip for interactive git rebase. Instead of counting the number of commits since you diverged from master (like git rebase -i HEAD~7), you can directly give the branch ref :

git rebase -i master

Then it works the same. No need to use git log master.. 😉

Collapse
alediaferia profile image
Alessandro Diaferia Author

That's a great tip, thank you!

Collapse
alediaferia profile image
Alessandro Diaferia Author

Trunk-based development doesn't negate the use of branches. As long as they are short-lived they ease reviews and help keep master (trunk) in a deplorable state all the times: trunkbaseddevelopment.com/short-li...

Collapse
lukaszsarzynski profile image
Łukasz Sarzyński

Great article, congratulate!

Collapse
hasii2011 profile image
Collapse
alexandrusimandi profile image
Alexandru Simandi

In trunk based development you control code with feature toggles and branch by abstraction. So technically there is source code branching, just not at VCS level

Collapse
geewiz profile image
Jochen Lillich

Very good overview of a clean merging workflow! I’ll recommend it to our team.

Greetings from Bray!

alediaferia profile image
Alessandro Diaferia Author

I agree, pure trunk-based development would work off a single trunk, always. But you still will need to rebase before pushing to master :)

Thread Thread
alediaferia profile image
Alessandro Diaferia Author

I would love to hear about your workflow. How many developers do you work with managing to keep a pure trunk-based development? How long does your full testing pipeline take to execute? Do you allow multiple people to push to master even before a full pipeline has finished executing? How do you ensure all your modifications are feature-flagged or how do you coordinate the release of new functionality? Do you have a testing pyramid or how are your testing efforts balanced? Is it a B2B or B2C software? Do you have automated roll-backs if a change introduces an outage? Do you halt pushing to master if the changes introduced a high severity issue that has not been caught by your automated tests?

Thread Thread
shirishp profile image
Shirish Padalkar

We have a team of 15+ developers and all of our development is always on trunk / master.
Our testing pipeline takes less than 10 minutes to execute end to end.
We do not restrict any pushes to master, so multiple people can push to master even before the build is completed.
We trust people to make sure their code is always ready to release to production, or when it's not, put it behind a feature toggle. If something still goes wrong, we feel our test suite is good enough to catch such problems.

I think to make this happen, you do need a higher level of trust on people to do the right thing. And for context sharing, we do pair programming, which helps a lot.

Thread Thread
alediaferia profile image
Alessandro Diaferia Author

That is just awesome. Is it a user-facing product? What's your testing allocation like between unit, integration, e2e? Is it a single repo between front end and back end code?

Thread Thread
shirishp profile image
Shirish Padalkar

Thanks. Yes, it is a user facing product. We have React based frontend, which is unit tested and 7-8 microservices which are also unit tested. We do have API level integration tests to cover interactions between services. And finally, we are in process of writing UI based functional tests to cover user flows.

So far, it's been working out pretty great. But I do want to call out that the team is extremely disciplined about the workflow and hence we haven't seen any problems. I do understand this might not be the case with all teams and each team might need a different variation of version control branching model.

Collapse
rbukovansky profile image
Richard Bukovansky

So in basic you are bringing CVS/SVN development style Git is trying to avoid... 🤔

Collapse
alexandrusimandi profile image
Alexandru Simandi

This is a common misunderstanding about trunk based development. I use it at work daily, our pipeline is pushing the same code on both dev/staging and production. The only difference is in feature toggles. This forces you to think about the code in a different way. If the client complains about a certain build having an issue, you can just toggle back the old version of that feature, hell even the client can do that if it's an emergency using something like rollout.io/