Beginners Guide to Git Branching


In this video we are going to look at Branching in Git. If you aren't familiar with the concept of branching, let's start by imagining that you are working on a project in a time before Git ever existed. You have all the project's files saved on your computer's hard disk. At this given moment, you know the code is working.

You want to make a big to your project, and so take a full copy / paste of the entire project directory before hand. Now, you open the new files in your IDE, and go through the motions to make the change.

At best, you are successful and the changes worked. You can now either delete the old folder and rename your new folder to that of the old. Or you can copy / paste all your code across from the new directory into the old directory... Or some other such system.

Likewise, if the changes failed to behave as expected, you could quite easily just delete the new directory and open the old directory again in your IDE.

This manual process is very similar to branching. Fortunately, Git doesn't do the whole copy / paste shenanigans. Instead, a branch is just a pointer to a commit. Again, you do not need to understand how this all works to start using branches, though reading further will certainly help gain confidence in doing so.

As branching is so lightweight (as in, we do not need a full copy / paste of our project) you will find branches are created frequently. A typical git workflow involves creating a new branch for every single feature, or bug fix, that you will do on your project.

However, there involves a potential to create conflicting changes when working with branches - particularly when working with larger teams - and this can lead to the dreaded "merge conflict". We will look at ways to mitigate, and directly resolve this problem as we go through this video.

As with the previous video there is quite a lot to cover here, so let's get started.

Basic Commands For Working With Git Branches

Whether you realise it or not, you have already been working with Branches.

As soon as we run git init, a master branch is created for us.

To see this for ourselves we can run git branch, which will show us all the locally available branches in our repository, along with an asterisk next to the currently active branch:

$ git branch

* master

Here we have a single branch called master, which is the currently active branch as denoted by the asterisk.

As we had finished the previous video by adding a remote - our Github repository - we could also see what branches are available in that remote repository:

$ git branch -r

github/master

Note here that there is no asterisk next to that remote branch.

This flags up a key point: different repositories may / will have different branches. When you create a branch, this branch only exists on your local repository until you push that branch elsewhere.

Lastly we can see all available branches in both our local repository and any known remotes:

$ git branch -a

* master
  github/master

Most real world projects I have worked on tend to adopt a more formally defined Git workflow, the most prominent being Gitflow.

In Gitflow you would keep the master branch in a known 'production ready' / deployable state. Therefore, only a small number of developers would ever work with, or have permission to change the master branch.

Running in parallel to the master branch would be your develop branch. This would be the known stable code to be deployed in the next release. Typically in terms of changes / commits, this branch is ahead of master for most of the time.

Let's imagine we are adopting Gitflow in our project. In order to do so, we need to create a new branch - develop:

$ git branch develop

And here comes the first gotcha:

We have only created the develop branch. We are not yet on that branch.

$ git branch

  develop
* master

We can switch to this branch by checking it out:

$ git checkout develop

Switched to branch 'develop'

$ git branch

* develop
  master

Understandably this is a source of confusion for most developers. Remember though, Git values the explicit. It did exactly what we asked - it created a branch. We didn't tell Git to also switch / checkout that branch.

Now, this being such a common operation, there is a shortcut. A one liner to both creating, and checking out the given branch:

$ git checkout -b new-branch

Switched to a new branch 'new-branch'

$ git branch

  develop
  master
* new-branch

In order to switch to any given branch, all we need to do is checkout that branch by name:

$ git checkout master

Switched to branch 'master'

$ git branch

  develop
* master
  new-branch

Now, more typically in the real world I tend to do this process of creating and switching branches via my IDE. PHPStorm has a very nicely integrated menu for working with Git. However, be sure to understand how to do this via the command line as this is the way Git was designed to be used.

Housekeeping

A core part of working with Git is keeping your house in order.

As branches are so lightweight and easy to create, you will (read: should!) inevitably create and use lots of them.

Much like as discussed in the previous video regarding using git add -p and git reset -p to ensure your commits remain free from noise, you will also need to prune your repository to remove branches that are no longer wanted.

In order to delete a branch, issue the git branch -d {branch-name-here} command:

$ git branch -d new-branch

Deleted branch new-branch (was 6424a8c).

$ git branch

  develop
* master

Again, you could do this via your IDE.

Inevitably you will end up pushing your local branches to your remote server (e.g. Github). And just as inevitably, you will forget to delete them every once in a while. Thankfully, Github (and others) has a nice GUI for showing all your branches, and deleting is as easy as clicking a button.

You can also delete a remote branch via the command line:

# start by pushing our local branch 'new-branch'
# up to github

$ git push github new-branch

Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 276 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local objects.
To git@github.com:codereviewvideos/git-example.git
 * [new branch]      new-branch -> new-branch

# now to delete the remote branch

$ git push github --delete new-branch

To git@github.com:codereviewvideos/git-example.git
 - [deleted]         new-branch

Remember we renamed our remote to github, change the name - typically origin - to match the name of your remote.

Again, typically this is easy to do via your IDE.

Branches Full Of Changes

The advice I was given when first starting with Git, and the same advice I stick to today, is to create a new branch for each bug fix or feature that I am working on.

The advantages for doing this become more evident as we progress, but for now, let's just focus on covering the basic branching and merging work flow.

Let's imagine we have an existing project under Git, and we have just had a new bug fix ticket added to our queue. We aren't using Gitflow here, just the plain old master branch:

Typically the convention to follow would be to create a new branch with the name of the ticket, and optionally a small summary of what the fix / feature intends to do:

$ git branch

  develop
* master

$ git checkout -b TICKET12-fix-readme

Switched to a new branch 'TICKET12-fix-readme'

$ vim README.md

# changes made here to the README.md file

$ git add -p
diff --git a/README.md b/README.md
index 08df0c4..c6e7068 100644
--- a/README.md
+++ b/README.md
@@ -11,4 +11,8 @@ some amazing code here

 ahoy!
-something different!
\ No newline at end of file
+something different!
+
+
+
+fixed some typo here
Stage this hunk [y,n,q,a,d,/,e,?]? y

$ git commit -m "fixes a typo in readme"
[TICKET12-fix-readme f103cb2] fixes a typo in readme
 1 file changed, 5 insertions(+), 1 deletion(-)

To quickly recap, we created and checked out a new branch for our given ticket: TICKET12-fix-readme

We made the changes to the problematic file(s) using our normal development work flow. I faked this here by adding a line using Vim.

We used git add -p to ensure only the relevant changes are staged for the next commit.

Then we committed the files with a short message as we have done previously.

Aside from the inclusion of using a branch, this is exactly the same work flow as we carried out in the previous video.

The important thing to note here is that our new changes are currently only saved to this particular branch. Should we run a git checkout master, our change would not yet be incorporated into that branch. To do this, we need to merge our changes.

We have a number of unexercised options available to us at this point, and this gives us an incredible amount of power.

Firstly, we could simply switch back to (git checkout master) the master branch, and merge in our new changes:

$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'github/master'.

$ git merge TICKET12-fix-readme 
Updating 6c28923..f103cb2
Fast-forward
 README.md | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

Our master branch now contains our bug fix, and we could go ahead and delete that now-no-longer-needed branch:

$ ✗ git branch -d TICKET12-fix-readme 
Deleted branch TICKET12-fix-readme (was f103cb2).

This is the simplest scenario.

However, we could have done a number of other things.

We could have committed our code to the new branch, then switched to an entirely different branch to - perhaps - work on a higher priority issue. At some point in the future we could have switched back to this TICKET12-fix-readme branch and resumed as if we had never left.

If we continue that scenario, in between us committing our code and switching to another branch, we might have had many further changes be merged into master. In this case, we could have merged master into our TICKET12-fix-readme branch, in order to ensure we are working on the very latest code.

Should the ticket no longer be needed, we could have swithed back to master and simply deleted the new branch.

Should we have made an absolute mess of our change, we could have switched back to master (or other branch), deleted our new branch, and started again by creating a new branch and checking that out instead.

There are likely many other scenarios we could work through. The main thing is that Git gives us these options.

One key thing to point out here is that we got lucky, to a certain extent. This happens less in the real world.

Notice that when we did our git merge, the output tells us that Git did a Fast-forward:

$ git merge TICKET12-fix-readme 
Updating 6c28923..f103cb2
Fast-forward
 README.md | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

In this instance, as we had no changes to the master branch after we had checked out and committed back to our TICKET12-fix-readme branch, Git was able to simply move its internal pointer for master from the previous commit hash up to the same commit hash that TICKET12-fix-readme is pointing too.

In a real project, particularly with multiple team members, the chances are much higher that the master branch will be changed in between you checking out your new branch and wanting to merge your new changes back in. Depending on what has been changed in that time by other team members, you may need to resolve potential conflicts before the merge can be completed.

Real World Example

In my experience, most online tutorials tend to follow the happy path.

And again, in my experience, it's what happens when you venture off the happy path - whether by accident, or not - that you tend to spend the majority of your time as a developer.

I therefore want to share with you a very common scenario that I found extremely frustrating when first getting going with Git:

Merge conflicts.

As I mentioned towards the end of the previous video, there is a guideline to follow that steers you away from merge conflicts, but you will inevitably encounter a merge conflict at some point in your time with Git.

The guideline is to have branches that are very short lived. The shorter the time between checkout and merge, the less likely you are to have other changes to the same code in other branches.

Again, in the real world, this is often easier said than done. Sometimes a critical bug fix needs to be done that will impact on an existing feature you have in progress on a different branch. Such is life.

Ok, so let's see this in action, and cover the way I would deal with this. You may have a different approach / favourite tool, and that's ok too.

We're going to start with our master branch as being our 'single source of truth'. All our branching will be done from the master branch, and all branches will need to be merged back into the master branch when work is complete. Of course, as mentioned earlier, a real world approach to this is to use Gitflow or similar, but to keep this example as simple as possible, I am skipping the develop branch. The same principle applies, just change the branch names.

Let's imagine we have a Symfony codebase and we have a new feature request - to add some new widget to our controller (the specifics are not important at all).

We get started on our important change:

$ git checkout -b FEATURE6-add-new-widget
Switched to a new branch 'FEATURE6-add-new-widget'

$ vim src/AppBundle/Controller/DefaultController.php

And we are deep into our work when a project manager scurries over to our desk and insists we drop everything and work immediately on this some reported bug. It's priority one. The CEO has noticed a typo, or some other such critical incident.

We're using Git so this isn't a huge deal.

We know we need to commit our existing changes to our current branch:

$ git add -p
diff --git a/src/AppBundle/Controller/DefaultController.php b/src/AppBundle/Controller/DefaultController.php
index afd0cb3..e39882c 100644
--- a/src/AppBundle/Controller/DefaultController.php
+++ b/src/AppBundle/Controller/DefaultController.php
@@ -19,8 +19,6 @@ class DefaultController extends Controller
         $widget = array(1,3,5,7);

         // replace this example code with whatever you need
-        return $this->render('default/index.html.twig', [
-               'widget' => $widget
-   ]);
+        return $this->render('default/index.html.twig', ['widget' => $widget]);
     }
 }
Stage this hunk [y,n,q,a,d,/,e,?]? y

$ git commit -m "WIP - adding new widget"
[FEATURE6-add-new-widget fd94f31] WIP - adding new widget
 1 file changed, 2 insertions(+), 3 deletions(-)

We commit our code and are now free to switch back to the master branch, from where we can start making the important bug fix our PM so urgently needs.

Ok, now let's imagine we are in a fairly typical large organisation and 3 days go by between our critical typo fix and that change being deployed to production. I know what you're thinking - 3 days?! That's far too quick :) arf arf

Anyway, for whatever reason we've not been able to work on our original feature between that unexpected interruption and today.

We enter the office ready to get our feature completed, and shipped off to the awaiting and highly eager test crew.

In our haste to get going we forget to switch back to our original branch, starting work instead on the master branch:

$ mkdir src/AppBundle/Service
$ vim src/AppBundle/Service/WidgetService.php

# changes made here

$ git status
On branch master
Your branch is ahead of 'github/master' by 1 commit.
  (use "git push" to publish your local commits)
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    src/AppBundle/Service/

nothing added to commit but untracked files present (use "git add" to track)

Arghh, at this point you notice:

On branch master

Ok, actually this is no big deal. We haven't committed our changes yet. However, we don't want to commit to master, we want to use our existing branch (FEATURE6-add-new-widget) with our pre-existing changes on.

No problem, simply checkout that branch:

$ git checkout FEATURE6-add-new-widget 
Switched to branch 'FEATURE6-add-new-widget'

$ git status
On branch FEATURE6-add-new-widget
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    src/AppBundle/Service/

nothing added to commit but untracked files present (use "git add" to track)

And add in the new directory:

$ git add src/AppBundle/Service/
$ git status
On branch FEATURE6-add-new-widget
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   src/AppBundle/Service/WidgetService.php

Cool! Ok, we can commit that as normal.

$ git commit -m "added widget service"
[FEATURE6-add-new-widget e0f9f90] added widget service
 1 file changed, 1 insertion(+)
 create mode 100644 src/AppBundle/Service/WidgetService.php

Now, we are feature complete, so let's get this merged!

Going back to what we covered earlier about unexercised options, we have a similar set of choices here.

Perhaps the most sensible would be to now merge in master to our current branch, fixing any conflicts before switching to master and merging in this branch.

This might be the most sensible, but the likely more intuitive approach (at least, until you make this mistake the first few times) is to do what you know: git checkout master and attempt to git merge FEATURE6-add-new-widget.

Only, a few days have gone by and your team mates have been busy making all manner of changes to the code base. Along the way they have modified the DefaultController, and as such, we would now have a merge conflict on our hands:

$ git merge FEATURE6-add-new-widget 
Auto-merging src/AppBundle/Controller/DefaultController.php
CONFLICT (content): Merge conflict in src/AppBundle/Controller/DefaultController.php
Automatic merge failed; fix conflicts and then commit the result

Yikes.

Well, Git is being nice to us in one regard - it is telling us the name of the file(s) with conflicts:

Merge conflict in src/AppBundle/Controller/DefaultController.php

Ok, let's open that file up and see what we get:

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(Request $request)
    {
        $someVar = true;

<<<<<<< HEAD
        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', array('some'=>'new_value'));
=======
        // a new line from TICKET 66
        $widget = array(1,3,5,7);

        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', ['widget' => $widget]);
>>>>>>> FEATURE6-add-new-widget
    }
}

Oh my Lord.

Ok, not all bad. The HEAD section (between <<< HEAD, and ====) shows the state of DefaultController in the existing master branch.

The section ======= to >>>>>>> FEATURE6-add-new-widget shows the state of DefaultController in the FEATURE6-add-new-widget branch we are trying to merge in.

If we had approached this from the opposite side (trying to merge master into FEATURE6-add-new-widget) then these two blocks would be reversed. That is HEAD always points to the branch you are on.

Anyway, to resolve this we could manually re-arrange the code to meet our needs. However, even in this small example I personally find this very painful to do. I much, much prefer a visual diff tool.

Thankfully, PHPStorm has a built in diff tool that is fantastic.

To resolve this, therefore, I would suggest aborting our merge attempt, and instead try doing so from PHPStorm:

$ git branch
  FEATURE6-add-new-widget
* master
  new-branch

$ git reset HEAD --hard
HEAD is now at d478c19 some other change

Here we have aborted our merge by reverting the current HEAD, which is the last commit to the master branch. If you were on the FEATURE6-add-new-widget, then the HEAD would point to the last commit on that branch.

Now, I would open up PHPStorm and from the Git menu I would attempt the merge this way:

PHPStorm Git Merge Menu

Which when clicked would bring up another dialog:

PHPStorm git merge dialog

By clicking 'Merge...' you gain access to the fantastic PHPStorm visual diff tool, which allows you to pick pieces from both changes, combining to resolve the conflict however you see fit:

PHPStorm git visual diff tool

My resolved file:

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(Request $request)
    {
        $someVar = true;

        $widget = [1,3,5,7];

        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', [
            'some'   => 'new_value',
            'widget' => $widget
        ]);
    }
}

Now, make sure to commit your changes:

$ git status 
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
  (use "git push" to publish your local commits)
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

    modified:   src/AppBundle/Controller/DefaultController.php
    new file:   src/AppBundle/Service/WidgetService.php

$ git commit -m "merged Feature 6 - add new widget"
[master de7a077] merged Feature 6 - add new widget

Now we could go ahead and delete the FEATURE6-add-new-widget branch. But before we do, let's just take a quick look at the git log output:

*   de7a077 (HEAD -> master) merged Feature 6 - add new widget
|\
| * e0f9f90 (FEATURE6-add-new-widget) added widget service
| * fd94f31 WIP - adding new widget
* | d478c19 some other change
|/
* f103cb2 fixes a typo in readme
* cf65462 added new line to readme
* 2eaab3d added somevar
* b18a5d8 updated .gitignore
* 6578d4f initial commit

Note, the exact command I use is: git log --oneline --decorate --graph

What we see here is a diagram of the path our code has taken.

Up until f103cb2 everything was done on master.

At this point, I made my original branch of FEATURE6-add-new-widget, and did my first change.

Meanwhile, back on the master branch, another developer made a different change (d478c19).

Even though my changes weren't quite in sequence, from the point of view of Git, the next thing I did was to add in the widget service (e0f9f90), at which point I was feature complete.

In merging, our branch drew to a logical conclusion and our divergent paths converged back to the single line / master branch.

If we delete the now unused branch, we don't lose this history, just the pointer reference:

$ git branch -d FEATURE6-add-new-widget 
Deleted branch FEATURE6-add-new-widget (was e0f9f90).

$ git log --oneline --decorate --graph

*   de7a077 (HEAD -> master) merged Feature 6 - add new widget
|\
| * e0f9f90 added widget service
| * fd94f31 WIP - adding new widget
* | d478c19 some other change
|/
* f103cb2 fixes a typo in readme
* cf65462 added new line to readme
* 2eaab3d added somevar
* b18a5d8 updated .gitignore
* 6578d4f initial commit

Now, it would be foolish of me to say that this is everything you will ever need to know about Git to complete every single task. But at this point you do know enough of the basics to get going on both small projects, and large.

As ever, whenever you get stuck - and you will - StackOverflow will very likely have you covered. The chances of you being the very first person to have encountered any particular situation in Git are very small indeed.

Now, to finish up I will say that you may be wondering why I've been preaching keeping your Git repository in a tidy fashion, and then left a messy trail in the git log output from our merged in commit. Whilst this is a more advanced topic, you can fix this by Squashing Your Commits. I don't want to leave you thinking this is absolutely essential, but if you ever work on larger / enterprise projects, or open source projects in particular, squashing commits is a very common practice and one that you should be aware of.

If you have any comments, feedback, suggestions, or similar, do please feel free to leave them in the comments box below.

Happy gitting!

Episodes

# Title Duration
1 Getting Started With Git 07:30
2 Beginners Guide to Git Branching 11:58