clubmate.fi

A good[ish] website

Web development blog, loads of UI and JavaScript topics

Git: dealing with branches, merging and rebasing

Filed under: Source code management— Tagged with: git

Here's how to make, delete, list, and use branches. Plus solutions to some hairy branching scenarios you might find yourself in.

Branching models

There's two prevailing branching schools:

  1. git-flow
  2. github-flow

The diagram for Git flow is so complex that I'm not even going to try to draw any pseudo graphics for it. See the flowchart here.

GitHub flow goes like this:

Master  ●────●────●────●────●────●────●
                \      /
Feature  ┈┈┈┈┈┈┈●───●┈┈┈┈┈┈┈`</pre>

Github Flow, is so simple and intuitive that it can be explained in few bullet points. Actually in six:

  • Anything in the master branch is deployable
  • To work on something new, create a descriptively named branch off of master (ie: new-oauth2-scopes)
  • Commit to that branch locally and regularly push your work to the same named branch on the server
  • When you need feedback or help, or you think the branch is ready for merging, open a pull request
  • After someone else has reviewed and signed off on the feature, you can merge it into master
  • Once it is merged and pushed to master, you can and should deploy immediately

I'll quote Scott Chacon's article once more:

[In git flow] one of the bigger issues for me is that it’s more complicated than I think most developers and development teams actually require. It’s complicated enough that a big helper script was developed to help enforce the flow.

I prefer the GitHub Flow exactly because of it's simplicity. This kind of simplicity might not necessarily be a requirement for you, both models serve a different purpose.

GitFlow is good for projects that do formal releases rarely (rarely being anything from weeks to months). Where as, it's overly complex when there are no release cycles, like my little blog here, or the GitHub, there are new features and bug fixes, but no version numbers.

Branch commands in Git

# List branches
$ git branch

# Make a branch
$ git branch my-new-branch

# Change into a branch
$ git checkout my-new-branch

# To create and checkout a branch in one command, use the `-b` flag
$ git checkout -b my-new-branch

# Delete local branch
$ git branch -d my-new-branch

Examples

Here's a little bash script to generate a test repo to play with:

mkdir merge-test \
&& cd merge-test \
&& echo Banana > banana.txt \
&& echo Apple > apple.txt \
&& echo Kiwi > kiwi.txt \
&& git init \
&& git add -A \
&& git commit -m "Initial commit"

Put your beer down:

# See what branches there are, only master for now
$ git branch
* master

# Make a new branch
$ git branch new-fruit

# Then check again, the one with the asterisk
# is the active branch
$ git branch
* master
    new-fruit

# Make changes to the new-fruit branch,
# start by switching to it by checking it out
$ git checkout new-fruit

# Make some new fruits, add, and commit them.
# Just paste it to your terminal.
$ echo Orange > orange.txt && git add -A && git commit -m "Added Orange"
$ echo Plum > plum.txt && git add -A && git commit -m "Added Plum"
$ echo Pear > pear.txt && git add -A && git commit -m "Added Pear"

Now there's more "meat" in the tree: two branches, and the other one has three commit more.

The tree can be printed out with the following command:

$ git log --graph --full-history --all --pretty=format:"%h%x09%d%x20%s"
* 622f05d        (new-fruit) Add pear
* cd74786        Add Plum
* c6a23e2        Add Orange
* 4ff629a        (HEAD, master) Initial commit

It's very straight, the tree is linear. More on that in the merging section of the article.

Presented with pseudo graphics it looks like this:

            ┌───┌───┌─ The new fruits
            ▼   ▼   ▼
new-fruit   ●───●───●
            /
master    ●
          ▲
          └── Initial commit

Merging

In Git, there are two main ways to integrate changes from one branch into another: the merge and the rebase.

When talking about branching, one must also talk about merging. It is the opposite of branching, you pull the branches back into the trunk. We'll talk about rebase later on in the article.

$ git merge new-branch

That will merge the new-branch to the branch you are at. Git will decide the merging algorithm for you, which come in two types:

  1. Fast forward merge
  2. and a 3-way merge (Ménage à trois hehe)

Fast forward merge

Fast forward merge can be used when there is linear path to the new branch. I.e. there has been no commits in the master after the new-branch diverged from it.

If your git tree looks something like this:

                     ┌─ New feature
                     ▼
newbranch┈┈┈┈●───●───●
                /
master    ━━●━━●
                ▲
                └── Master

What the fast forward merge does, is quite simple, it just copies the new feature to the master:

                        ┌─ New feature and Master
                        ▼
                ●───●───●
                /
master ●───●───●

Nothing really changes, the linear structure remains, all commits are the same.

3-way merge

Take the below, very common, git tree:

                       ┌─ Your New feature branch
                       ▼
newbranch  ┈┈┈●───●───●
                /
master━━●━━●━━●
              ▲
              └── MASTER. Someone else merged
                  in their changes after you
                  branched out with your feature.

There's now two different histories that Git needs to smash together somehow. It does this by using data from 3 different commits (hence the name). The commits are: 1) master tip, 2) feature tip, 3) their ancestor.

                     ┌── 2
                     ▼
newbranch┈┈┈┈●───●───●
                /
master━━●━━●━━●
           ▲   ▲
       3 ──┘   └── 1

The three way merge also creates a merge commit.

What merge algorithm to use then? And how?

Git just does this for you, it uses the fast forward merge if it can. Then there's also rebasing, it's related strongly to merging, that's the next chapter in the article.

Merge examples

Lets pick up from the test tree where we left of in the first example. It looked like this:

* 622f05d        (new-fruit) Add pear
* cd74786        Add Plum
* c6a23e2        Add Orange
* 4ff629a        (HEAD, master) Initial commit

Let's break the linearity by making a commit to the Master:

$ git checkout master
$ echo Pineapple > pineaple.txt
$ git add -A
$ git commit -m "Add Pineapple"

Now check it with the extended log command:

git log --graph --full-history --all --pretty=format:"%h%x09%d%x20%s"
* d41e17c        (HEAD, master) Add Pineapple
| * 622f05d      (new-fruit) Add pear
| * cd74786      Add Plum
| * c6a23e2      Add Orange
|/
* 4ff629a        Initial commit

The new-fruit branch is clearly visible now. Merge it into master:

# Switch to master if not active
$ git checkout master
$ git merge new-fruit
Merge made by the 'recursive' strategy.
    orange.txt | 1 +
    pear.txt   | 1 +
    plum.txt   | 1 +
    3 files changed, 3 insertions(+)
    create mode 100644 orange.txt
    create mode 100644 pear.txt
    create mode 100644 plum.txt

Git asks you to enter a commit message, since this is a three-way merge.

Screenshot 2015-02-25 09.44.59

Now the log shows the following:

$ git log --graph --full-history --all --pretty=format:"%h%x09%d%x20%s"
*   ef27c6c      (HEAD, master) Merge branch 'new-fruit'
|\
| * 622f05d      (new-fruit) Add pear
| * cd74786      Add Plum
| * c6a23e2      Add Orange
* | d41e17c      Add Pineapple
|/
* 4ff629a        Initial commit

The new-fruit has merged to the trunk. Now, there's the "Merge branch 'new-fruit'" commit and the history is not linear. Rebasing can help in that.

Rebase

Rebasing is similar to merging, it also combines branches, but completely different command from merging. Rebase rewrites history and reduces merge conflicts in doing that. What all this really means? Let's look how rebasing works to understand when to use it.

An example tree:

                        ┌─ FEATURE
                        ▼
newbranch          ●───●
                  /
master───●───●───●─────●───●
                           ▲
                           └── MASTER

After a rebase, the feature branch kind of 'migrates' to the tip of the tree:

the "old" FEATURE ─┐         ┌─ FEATURE
                   ▼         ▼
newbranch          ●───●     ●───●
                  /         /
master───●───●───●─────●───●
                           ▲
                           └── MASTER

Essentially, rebase moves a commit from branch to another. But, those commits are not the same commits, they contain the same stuff, but they're new commits, they have different commit ID etc. So, rebase rewrites history.

Without rebase you wouldn't be able to retain linear history, rebasing tidies up the repo.

Rebasing wont replace merging, they're used side by side usually, first rebase then merge.

Rebase examples

This is where we left off in our example tree:

$ git log --graph --full-history --all --pretty=format:"%h%x09%d%x20%s"
*   ef27c6c      (HEAD, master) Merge branch 'new-fruit'
|\
| * 622f05d      (new-fruit) Add pear
| * cd74786      Add Plum
| * c6a23e2      Add Orange
* | d41e17c      Add Pineapple
|/
* 4ff629a        Initial commit

First let's rollback that merge commit, so we can test the rebasing better:

# Roll back 1 commit
$ git reset --hard HEAD~1
HEAD is now at d41e17c Add Pineapple

Now we want to:

  1. Rebase the master to new-fruit and then
  2. merge new-fruit to master.
$ git checkout new-fruit
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Add Orange
Applying: Add Plum
Applying: Add pear

# Then merge
$ git checkout master
$ git merge new-fruit

Note that it didn't ask us to write a commit message, because there's no need to do that, because Git only did a fast forward merge. Now the log shows a linear history:

$ git log --graph --full-history --all --pretty=format:"%h%x09%d%x20%s"
* f5777c2        (HEAD, new-fruit, master) Add pear
* 31a525b        Add Plum
* 72c2e92        Add Orange
* d41e17c        Add Pineapple
* 4ff629a        Initial commit

There's one commit less and it's much tidier.

For instance, the Plum commit from before the rebase: * cd74786 Add Plum, and after: * 31a525b Add Plum. Different commit IDs, cause they're completely different commits now and the old commits are destroyed. History rewriting in action.

When to rebase

Atlassian has great article about the little nuance differences of merging and rebasing. Git's docs are amazing on this subject also.

Common branching scenarios and problems

The "forgot to make a feature branch" problem

You've started to work on a new feature, and forgot to make a new branch for it. This is what you have:

master ●───●───●───●───●───●
                ^
                └── start a new feature

And this is what you want:

newbranch   ┈┈┈┈┈●───●───●
                /
master ●───●───●

This is very easy to solve with some branch juggling. Start by making a new branch:

$ git branch newbranch

Now, all your new stuff is safe in the new branch, and you can freely roll back on the master:

$ git reset --hard HEAD~3 # This goes back three commits

Or rollback to a commit id

$ git reset --hard <commit-id>
# Commit IDs look like this 5c00039c86ca321e9ac41557e6515da20cfeee44
# You can see your commit ID by issuing: git log

There you have it, newbranch has the new stuff and master has the old. Watch for not going back too far.

Then check out the newbranch to continue work on it:

$ git checkout newbranch

The "tip of your current branch is behind its remote counterpart" problem

You branch off from the trunk and hack on your feature, at this time someone else finishes their feature and pushes it to the master. When you try to push you get an error:

$ git push origin master
error: failed to push some refs to '/path/to/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Now, you need to pull the changes from the remote master to your branch, using the --rebase flag:

$ git pull --rebase origin master

It grabs the new commits from the Master and puts them at the tip of your feature branch. Now you can push to it.

Conclusions

Branching and rebasing are totally confusing at first, but they're an essential part of Git. Learn to love them :) Please drop a comment if you got something to add.

Comments would go here, but the commenting system isn’t ready yet, sorry.

  • © 2022 Antti Hiljá
  • About
  • All rights reserved yadda yadda.
  • I can put just about anything here, no one reads the footer anyways.
  • I love u!