Branches (version control) – The ultimate guide

For everything you need to know about version control, check out Version control – Everything you need to know, on Programming Duck.

Knowledge prerequisites: This article assumes that you can already use the basics of git and know the basics of version control. If you don’t, then I recommend that you start at the beginner section of the article Version control – Everything you need to know.

A branch is a separate version of your code. It’s essentially a sandbox where you can code whatever you want without affecting any other code. Then, if you don’t like the result, you can delete the branch and switch to a different one to start over.

Things to consider when working with branches are:

  • Branching strategy and workflow
  • Merging strategy (normal merges vs fast-forward merges)
  • Solving conflicts with merging vs back merging
  • What to name branches
  • And many more things

Branching strategies and workflows

Have a stable main branch

Note: In this article the "stable main branch" refers to the "master" branch. You can name branches whatever you want, but "master" is the common name for the default and stable branch, so that’s what this article uses.

Ideally, you should always have a stable main branch. This branch should be as close to being release-ready as possible.

The primary reason for this, is because it’s relatively easy to maintain an already-stable branch. However, the longer a branch remains unstable, the harder it will be to make it stable again. This is because broken code may keep pilling up. As a result, things may break in more complicated ways over time.

Additionally, an unstable branch may block or slow down the development of new features. For example, if a section of the codebase is not working properly, developers may need to fix it before they can complete a new feature. Alternatively, if some tests aren’t passing, developers may be confused or distracted by them when they’re writing new tests. And so on…

Further, Agile advocates frequent, small releases. Keeping a main branch continuously stable and close to release-ready is very helpful in making this possible.

Use feature branches

Since the master branch should always be stable and as close to release-ready as possible, new development should be done on "feature branches".

That’s because, as you develop new features, the code may break. You may write prototype code that’s not fit for production, hardcode values, write failing tests, etc. That code shouldn’t be committed to master until it’s finished and working.

Further, using a feature branch ensures that even if you do accidentally push the code to the remote, it doesn’t affect master and accidentally get released.

In addition, branches give you more benefits such as the ability to do pull requests and code reviews. You can also have multiple developers collaborate on them. All of these features require you to push the code to the remote at some point. However, since you shouldn’t push code to master until it’s complete and stable, you should use feature branches to gain these benefits.

In summary: create branches off master (or another stable branch), do all of your development there, then merge to your stable branch when you’re done.

For more information on this, see feature branch workflow.

Gitflow

Gitflow is an established branching strategy / workflow. It’s quite strict and has a lot of ceremony regarding branches and releases.

In my opinion, one of its greatest benefits is that it’s very safe. All of the ceremony gives the team plenty of time to ensure that the master branch is as stable as possible before releasing. Other workflows can also provide the same level of safety, but only if you’re able to implement them properly and with that purpose in mind.

A neutral point of Gitflow is that it’s well-defined and not modifiable, or at least, it doesn’t claim to be modifiable. You may consider this an advantage, since it means that you don’t have to worry about designing your own workflow, or you may consider it a disadvantage, since you technically shouldn’t modify it even if it would suit your project better. In either case, perhaps it’s a moot point. It’s your project, which means that you can technically do whatever you want, including modifying your use of Gitflow.

A small disadvantage of Gitflow is its complexity. It does things in a way that’s not necessary intuitive, particularly with its release and hotfix branches. Each of those branches has particular rules about where it must branch off from and be merged into. For example, release branches branch off develop and are merged into master and also into develop. Hotfix branches branch off master and are merged into master and develop.

In comparison, other workflows like GitLab flow are simpler in this aspect. Everything branches off master and merges into master. Bugfixes should additionally be merged or cherry picked into the appropriate release branches, but that’s to be expected.

Another minor disadvantage, is that Gitflow is not as suitable for continuous delivery. You can set it up for continuous delivery, but the additional branches and ceremony tend to make it more difficult to do so than other workflows.

For actual information on how Gitflow works, please see the Gitflow Workflow page by Atlassian or the Gitflow post by Vincent Driessen.

Consider using Gitflow if:

  • You want maximum safety regarding releases.
  • You’re not practicing continuous delivery.
  • You have a personal preference for Gitflow.

GitLab flow

GitLab flow is a fairly simple and flexible branching strategy.

Here is how it works:

  • You create feature branches off the master branch and merge them back into the master branch.
  • Options for release branches are flexible:
    • You can release directly from master.
    • You can have a separate branch for "production".
    • You can have multiple branches between master and "production", such as "pre-production", "staging", etc.
    • You can have multiple release branches for different numbered releases.
  • Hotfixes branch off master and are merged into master. You also need to add them to the production branch and / or numbered release branches. You can do this by merging master into them, or merging the hotfix branch, or cherry-picking some of the hotfix commits. The option you choose depends on how much additional commit history you want to merge into the branches. If you only want the hotfix commits, just cherry pick them.

That’s pretty much it.

It’s very flexible, which means that you can make it as safe as you want and / or keep it as simple as you want.

One thing that I also like about it, is that it follows an intuitive branch "stability hierarchy". There is a clear order in terms of the stability of branches. Merges / cherry-picks follow that order. For example:

  • feature branch (least stable branch) -> merged into master -> merged into pre-production (if you have this branch) -> merged into production (if you have this branch) (most stable branch).
  • hotfix (similar to a feature branch in terms of stability) -> merged into master -> cherry-picked into pre-production or release-candidate branches -> cherry picked into production or numbered release branches.

For full information on GitLab flow, see Introduction to GitLab Flow.

Consider using GitLab flow if:

  • You want a simple, fast workflow, potentially with continuous delivery.
  • You want to design your own workflow to suit your project’s needs.
  • You have a preference for GitLab flow.

More branching strategies

There are many more branching strategies to choose from. It might be worth exploring some of them, as you may like them better than the ones presented in this article.

Many of them, such as GitLab flow, are flexible, with only a few prescribed concepts such as working off master and using feature branches. As a result, many of them are similar.

Here are some more branching workflows that I’ve encountered:

If you’re interested, feel free to do your own search for more.

Conclusion

  • Keep a stable branch
  • Use feature branches
  • Consider picking a workflow like Gitflow, GitLab flow, or an alternative, and stick to it.

Merging strategy (fast-forwards vs normal merges)

For an explanation of normal merges vs fast-forward merges, please see the Git Merge tutorial by Atlassian.

Long-story-short, a normal merge creates a "merge commit" on the target branch. A commit with a message such as "Merge branch ‘X’". This commit will contain the commit history of both branches.

A fast-forward merge makes the commit history look as though you made each commit directly on the target branch, instead of on a feature branch which you then merged. There won’t be a merge commit.

Each strategy has pros and cons.

Feature Normal merge Fast-forward merge
Commit history graph Tends to create very messy commit history graphs. Creates neat, linear commit history graphs.
Filtering commit history Allows you to select whether you want to see only merge commits, only commits which aren’t merge commits, or both. Can only see normal commits (as only those exist in the commit history).
Reverting / resetting Can revert individual commits or entire branches easily. Can only revert individual commits, meaning if you want to revert an entire branch, you’ll have to revert every commit from that branch.
Rebase vs merge command The merge command is easier for beginners and less error-prone than rebase. The rebase command can be more difficult for beginners. It’s also easier to make mistakes due to having to resolve similar conflicts repeatedly.
Commit history preservation Preserves project history perfectly. All of the original commits are available, and merge conflicts can be reproduced and examined. Doesn’t preserve the full project history. Changes made with the rebase command are permanently lost.

There is also a third option. You can use normal merges after rebasing. In other words, rebase to get your branch into a state where a fast-forward merge is possible, but do a normal merge. This creates a very neat, linear history and also includes the merge commit.

However, this strategy is more difficult to enforce. It would probably require more configuration and custom scripts.

Which strategy should you choose?

Both strategies (normal merges and fast-forward merges) have been used successfully in all sorts of projects.

For private (not open-source) projects, I don’t think the choice matters very much. You can use whichever strategy you (and your team) prefer.

The main consideration for most people is: Do you care about having a neat, linear, commit history? If so, then use the fast-forward merging strategy. Otherwise, go for normal merges for the additional benefits with filtering and reverting commits and branches.

For open-source software, the fast-forward merging strategy seems to be much more common (based on GitHub’s top 20 repositories in terms of stars as of the time of writing).

This may be because the vast majority of "issues" (similar to "tickets" in task management software) in open-source software seem to result in just a single, small commit. In this case, having a merge commit would add unnecessary noise to the commit history. Also, since most contributors don’t work on the codebase full-time and therefore aren’t as familiar with it, a clean commit history may be more helpful for them in case they want to examine some of the commits to understand the coding standards of the codebase. Finally, if not managed well, normal merges can create very messy commit histories. Especially in an open-source project setting, where strong coding standards can be more difficult to enforce, this may make fast-forward merges more desirable.

Solving conflicts with merging vs back merging (or rebasing)

(This section only applies if you’re not enforcing fast-forward merges.)

Sometimes, your feature branch and master may have code conflicts between them. These need to be resolved before git finishes merging one branch into the other.

There are a few options for resolving them:

  1. You can merge your feature branch into master (and resolve the code conflicts during the merge).
  2. You can "back merge" your feature branch onto master (meaning that you merge master into your feature branch) (and resolve the code conflicts during the merge). Afterwards, you can merge your feature branch into master.
  3. You can rebase your feature branch onto master (and resolve the code conflicts during the rebase). Afterwards, you can merge your feature branch into master.

Merging directly into master is the most unsafe option. Back merging is very safe. Rebasing is relatively safe and results in the cleanest commit history (but also bear in mind that rewriting the history of remote branches is dangerous, which may happen if you rebase. This is covered in more detail in another section.)

The issue is that resolving code conflicts means that you’re making new code changes. If you directly merge into master, you’ll have untested code in master. As already explained, it’s best to avoid that.

Additionally, code conflicts are especially dangerous. They are conflicts due to code changing differently in different branches. Those changes are much more likely to be incompatible than other code changes.

Therefore, it’s recommended to back merge or rebase instead. That way, if there are any problems, only your feature branch will break, which is much better than master breaking. You can then fix the issues in your feature branch, test them and merge to master when you’re satisfied.

Other branch tips

Don’t rewrite the commit history of remote branches

Rewriting the history of remote branches is dangerous because:

  • Force pushing is dangerous.
  • It creates conflicts for everyone else.
  • It would be a nightmare if everyone did it often.

Force pushing is dangerous

Rewriting the commit history of a remote branch requires you to force push.

Force pushing is very dangerous. It completely overwrites the remote branch with whatever you push. If you force push at the wrong time, you may overwrite any new commits your teammates have made. This means that you’ll delete their work.

The best way to avoid this is to never force push.

But, if you’ve decided that you will force push, at the very least use the git push --force-with-lease command instead of git push --force. If used correctly, this command will prevent a push if any new commits exist on the remote branch, meaning that you won’t accidentally overwrite any new commits your teammates have made.

However, note that it’s very easy to misuse this command, making it no different from force pushing. For an explanation of how to use it properly, please see –force considered harmful; understanding git’s –force-with-lease.

Overwriting remote history creates conflicts for everyone else

Changing the commit history of a remote branch will create code conflicts for everyone else using that branch. Every other developer will then have to merge or rebase the new branch changes and also resolve the code conflicts.

This is inconvenient and potentially error-prone. Further, if the team uses merges instead of rebases this will create messier commit histories.

In addition, conflicts can also happen if the changed branch is an ancestor of a branch that a developer is using or if the changed branch is the target branch they want to merge into.

If everyone rewrote history, working would be very difficult

Consider if everyone rewrote the history of remote branches as often as they wanted to. You would have to interrupt your work to fix your local branch fairly often. As a result, working could become very difficult.

Conclusion

The best way to avoid all of these problems is to not modify the history of remote branches.

Exceptions

There are times when it’s probably safe to rewrite the history of remote branches.

  • The safest time to rewrite the history of your feature branch is probably soon before merging into master and then deleting the feature branch. That’s because, after that point, no one will have access to the feature branch, so it won’t affect anyone negatively. Even so, ideally, this should be a process that the entire team is aware of, so that no one is surprised when it happens.
  • If you’re absolutely certain that no one else has used that branch (or created a new branch off it) and that no one will be negatively affected by rewriting its history. However, to truly be sure, you may have to confirm with many other developers.

Branch naming

If you’re using normal merges, then the branch name will show up in the commit message of the merge commit when you merge the branch. Therefore, it’s useful to have a good branch name that describes the work done in the branch.

Anything sufficiently descriptive will do. For example:

"fix-issue-with-service-worker"

A format I’ve seen in many places where I’ve worked (which is also my personally preferred format) is:

"feat/ticketId-title-of-ticket-or-description-of-work"

In more detail:

  • The "feat" at the start is the type of the story. E.g. feature, hotfix, etc. This is a convention used by Gitflow.
  • The ticket ID or issue number comes next. This is here so that a user reading the merge commit can immediately open the ticket for more information. The only time I wouldn’t include this is if the version control provider automatically linked to the relevant ticket / issue anyway.
  • There are many delimiters you can use between the words, such as dash (-) and underscore (_). My personal preference is the dash (-).
  • The rest describes what the branch is working on. If you’re using task management software, this part could be the title of the ticket.

Use git pull with rebase

Your local branch and the corresponding remote branch can diverge. This means that they can end up with different commit histories.

This can happen when:

  • Someone adds more commits to the remote branch.
  • Someone rewrites the history of the remote branch.
  • You rewrite the history of your local branch, so it no longer matches the history of the remote branch.

If your local branch and the remote branch have diverged, then, when you fetch those changes, you can either merge them or rebase them into your local branch.

You can merge them with git pull, or:

git fetch
git merge

You can rebase them with git pull --rebase, or:

git fetch
git rebase

You can also make git pull have the default behaviour of rebasing by changing your global git configuration file or by executing the command git config --global pull.rebase true in your terminal.

The difference between merging and rebasing is that, if you merge the changes, if the merge isn’t be a fast-forward merge, you’ll end up with a merge commit in your local branch and therefore a messier commit history. Further, the merge commit will contain the history of the remote branch and the history of your local branch. If the developer rewrote the history of the remote branch to clean it up, this somewhat defeats the point, as the original history will be reintroduced when you use git pull.

However, if you rebase the changes with git pull --rebase, you’ll end up with a linear commit history.

My personal recommendation is to always rebase these changes, rather than merge them.

This is because after the feature branch has been merged into master, a merge commit in the middle of the commits of the feature branch is unlikely to provide any benefit to a future user. Rather, having to navigate through merge commits will probably be an inconvenience. I imagine that a future user will be much better served with an easy-to-follow commit history featuring only the unique commits of the feature branch.

Another reason is because it’s "safe" to rebase at this point, as far as the remote branch is concerned. If you use git pull --rebase and then you git push, you won’t overwrite the history of the remote branch.

For the details of how all of this works, please see the git pull tutorial by Atlassian and the git branch rebasing page in Pro Git.

Have short-lived feature branches

It’s important for feature branches to be as short-lived as possible.

This is also recommended by Agile, as seen in the Agile 12 principles.

One problem with long-lived branches is that the parent and feature branches diverge over time. This means that code conflicts build up over time, making merging more difficult.

Another issue is that bigger features tend to be harder to test. They are also more dangerous, meaning that many things can go wrong with them. Therefore, it’s generally safer to develop features in small increments. You can do this by developing only one part of a feature (in a short-lived feature branch), then merging it and testing it. Then repeat with the next part and so on until the whole feature has been developed.

You can release partially-complete features by:

  • Using vertically sliced stories that can be published even though the feature as a whole is incomplete.
  • Hiding the incomplete feature from the user in some way, for example by not displaying a link to that page in the header menu.
  • Disabling the incomplete feature with feature toggles.

In summary, use short-lived feature branches when possible. If a particular feature will take a long time to complete, see if you can use one of the methods outlined above and use short-lived feature branches regardless.

Finally, if you need to have long-lived feature branches, at the very minimum you should "back merge", or rebase master onto them regularly. That way they won’t diverge too much. You should also be testing as you develop, to minimise the danger of merging large untested features into master.

Don’t squash branches into a single commit, unless you have a good reason for doing so

This refers to having a branch with multiple commits and then rewriting history to make them appear as a single commit before merging.

Well-structured commits provide many benefits. Additionally, good commit messages make it much easier to understand the commit history of the codebase. Don’t "delete" this useful information if you don’t have to.

However, there may be good reasons for squashing all the commits in a branch. For example, if many commits are not well-thought out, or the commit messages are not very helpful, then it may be better to squash those commits into a single commit with a good commit message.

Another reason may be if you are using the fast-forward merge strategy, but want your commits to resemble "merge commits" for easy reverting.

In the end, it’s up to you to decide whether to squash commits or not. Just make sure you have a sufficient reason for it.

Final notes

That’s it for this article.

If you have any feedback, or even counter-arguments, please let me know in the comments.

Next, if you want to know more about version control, please see the article Version control – Everything you need to know.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments