Integrating Topic Branches in Git
Originally posted on Carbon Five’s Blog.
A key feature of Git is how easy it is to create topic branches to separate and organize work. This power leads to a codebase with many more branches than you would typically see in other SCMs, like SVN. However, without an appropriate and consistent branch-and-merge strategy, your team will wind up with a confusing and unhelpful history.
How do we avoid this mess? And what do we actually want our history to look like?
A Tale of Two Timelines
No one sets out to create a messy history. Most of us want our main branches to be a straight line of commits.
This clear, linear history absent of any merge commits is highly readable. Git’s default behavior when merging a branch that has not diverged from the mergee is to perform a fast-forward, resulting in this type of history.
There is a major shortcoming; it doesn’t reflect the use of topic branches! You can’t see the workflow that was used and you can’t rollback the work from a single topic branch without a bit of investigation.
So lately we’ve been aiming to have our histories look like the following:
The main branch (master in this case) consists of nothing (besides the initial commit) but merge commits from topic branches. This is just as clear as the above linear timeline, but now:
- the history reflects the fact that we used topic branches for our work
- a naming convention for topic branches helps identify the work done
- it’s easy to revert the work of a branch; just revert the single merge commit!
So how do we achieve this model?
The Workflow
We follow a few key steps around branching and merging in order to create this style of history.
Branch Around Stories
As part of the agile process, we write stories to describe one feature, bug fix, or chore to be delivered. When we begin work on a story we create a topic branch named after it.
We usually use Pivotal Tracker to manage our stories, but no matter what system we can easily apply our naming convention:
[feature|bug|chore]-[id]-[abbreviated_story_title_separated_by_underscores]
Here are some examples:
Story | Branch |
---|---|
Feature #12345: Threaded post comments | feature-12345-threaded_post_comments |
Bug #23456: Can create 2 groups with the same name | bug-23456-prevent_duplicate_groups |
Chore #34567: Setup CI environment | chore-34567-setup_ci_environment |
Assuming you are on the master branch, creating a new branch would look like this:
git checkout -b feature-12345-threaded_post_comments
It also makes sense to push this topic branch to the remote repository for backup or remote access by you and others.
git push origin feature-12345-threaded_post_comments
Rebase When Ready to Deliver
When a feature is complete, we rebase our work on the latest version of our main branch (master in this case):
git rebase master
This step is what gives our history model the appearance of each topic branch being created sequentially.
You may be concerned about rebasing when working in a team environment as your next push would have to be forced, rewriting the history. But remember, we only do this step when we’ve finished the story i.e. we no longer plan any further changes to it.
Merge Without Fast Forwarding
As discussed, Git’s default merge behavior (when the 2 branches have not diverged or after a rebase of one on another) is to perform a fast forward. Instead of accepting the default behavior we use merge’s --no-ff
flag:
git checkout master
git merge --no-ff feature-12345-threaded_post_comments
This prevents the default behavior and generates a merge commit, achieving the goal of our model!
A Note on Squashing
Some developers prefer to squash all their work in a topic branch into a single commit before they merge. If you’re doing this, then we don’t see much advantage to not fast-fowarding because every topic branch in your history would be a single commit! Instead accept the default behavior and fast foward but at least make sure the story id is in the commit message for reference.
Be Good, Cleanup after Yourself!
Always remember to delete local topic branches after integrating them into another branch.
git branch -d feature-12345-threaded_post_comments
If you’ve pushed the topic branch to a remote, delete it there as well to avoid confusing other developers about its status.
git push origin :feature-12345-threaded_post_comments
Conclusion
Implementing this non-fast-forward workflow requires a bit of discipline from all of us after using the default behavior for some time. But we do enjoy the results, particularly a history that preserves the existence of topic branches.
We would love to hear your opinions and how you manage branching in your own work.