Git Branching#

Git supports “branches”, a feature of tracking different versions of your project code. These branches can be used to model different versions, or to safely test out a new idea or fix a bug. In this lecture, we’ll cover what Git branches are used for, and how to best use them.

To download the demo for this lecture:

$ dmget wb-git-branching --demo

Introduction#

Goals#

  • Understand data structures of Git

  • Learn to use Git branches

Data Structures of Git#

Commits are linked nodes#

digraph { rankdir=LR node [shape=rectangle] first [label=< <table cellborder="0" cellspacing="0" cellpadding="3" border="0"> <tr> <td bgcolor="#cccccc" colspan="2"><font color="black"><b>93ca9</b></font></td> </tr> <tr> <td bgcolor="#cccccc" align="left"><font color="black">author</font></td> <td bgcolor="#cccccc"><font color="black">Jane Hacks</font></td> </tr> <tr> <td colspan="2"></td> </tr> <tr> <td colspan="2" align="left"><i>Initial commit</i></td> </tr> </table> >] second [label=< <table cellborder="0" cellspacing="0" cellpadding="3" border="0"> <tr> <td bgcolor="#cccccc" colspan="2"><font color="black"><b>f30ab</b></font></td> </tr> <tr> <td bgcolor="#cccccc" align="left"><font color="black">author</font></td> <td bgcolor="#cccccc"><font color="black">Jane Hacks</font></td> </tr> <tr> <td colspan="2"></td> </tr> <tr> <td colspan="2" align="left"><i>Add a feature</i></td> </tr> </table> >] third [label=< <table cellborder="0" cellspacing="0" cellpadding="3" border="0"> <tr> <td bgcolor="#cccccc" colspan="2"><font color="black"><b>34ac2</b></font></td> </tr> <tr> <td bgcolor="#cccccc" align="left"><font color="black">author</font></td> <td bgcolor="#cccccc"><font color="black">Jane Hacks</font></td> </tr> <tr> <td colspan="2"></td> </tr> <tr> <td colspan="2" align="left"><i>Finish MVP</i></td> </tr> </table> >] first -> second -> third [dir=back] }

Each commit node contains:

  • Information about all changes made to files

  • Author name and email

  • Commit message

  • Pointer(s) to commit(s) that came directly before this commit

  • Zero parents for the initial commit

  • One parent for a normal commit

  • Multiple parents for a commit merging two or more branches.

Git Branching#

What are branches good for?#

  • Testing an idea safely

    • So you can still work on the “main branch” separately

  • Maintaining separate releases

    • Example branches: dev and prod

Starting a project#

Let’s start a fresh Git repo in the demo directory.

$ git init
$ git add README.md
$ git commit -am "Initial commit."

Now let’s add hello.js and commit it.

hello.js#
console.log('Hello, world!');
$ git add hello.js
$ git commit -am "Add hello.js"
$ git log
commit f5956ee (HEAD -> main)
Author: Jane Hacks <jhacks@developer.dev>
Date:   Tue Aug 15 13:05:15 2050 -0000

    Add hello.js

commit 4e7a1dd
Author: Jane Hacks <jhacks@developer.dev>
Date:   Tue Aug 15 13:04:51 2050 -0000

    Initial commit.

digraph { rankdir=TB ranksep=0.3 node [shape=rect fillcolor="#dddddd" color="#ffffff" style=filled width=1.5] HEAD [color="transparent" fillcolor="#cc9f23" fontcolor="white" shape=Mrecord] main [color="transparent" fillcolor="#2288cc" fontcolor="white" shape=Mrecord] { rank=same "4e7a" -> "f595" [dir=back] } HEAD -> main [color="#cc9f23"] main -> "f595" [color="#2288cc"] }

main

The default branch Git creates for you.

HEAD

A pointer to the commit at which your working directory is currently set.

Creating a branch#

Our project exists but it needs something more…

Let’s add a new feature.

$ git branch output-more-stuff

digraph { rankdir=TB ranksep=0.3 node [shape=rect fillcolor="#dddddd" color="#ffffff" style=filled width=1.5] HEAD [color="transparent" fillcolor="#cc9f23" fontcolor="white" shape=Mrecord] main [color="transparent" fillcolor="#2288cc" fontcolor="white" shape=Mrecord] "more-stuff" [color="red" fillcolor="#2288cc" fontcolor="white" shape=Mrecord penwidth=1.4] { rank=same "4e7a" -> "f595" [dir=back] } HEAD -> main [color="#cc9f23"] main -> "f595" [color="#2288cc"] "f595" -> "more-stuff" [color="red" dir=back penwidth=1.4] }

Notice git branch doesn’t change HEAD.

To move HEAD ➡️ more-stuff, we’ll need to checkout more-stuff.

Checkout a branch#

digraph { rankdir=TB ranksep=0.3 node [shape=rect fillcolor="#dddddd" color="#ffffff" style=filled width=1.5] HEAD [color="red" fillcolor="#cc9f23" fontcolor="white" shape=Mrecord penwidth=1.4] main [color="transparent" fillcolor="#2288cc" fontcolor="white" shape=Mrecord] "more-stuff" [color="transparent" fillcolor="#2288cc" fontcolor="white" shape=Mrecord] { rank=same "4e7a" -> "f595" [dir=back] } main -> "f595" [color="#2288cc"] "f595" -> "more-stuff" [color="#2288cc" dir=back] "more-stuff" -> HEAD [color="red" dir=back penwidth=1.4] }

$ git branch
* main
  more-stuff

$ git checkout more-stuff
Switched to branch 'more-stuff'

$ git branch
  main
* more-stuff

You’ve now switched to the more-stuff branch!

You can shortcut these two steps with:

$ git checkout -b name-of-branch

This creates the branch and moves HEAD in one step.

Work on your branch#

Let’s add more stuff to hello.js and commit our changes.

hello.js#
console.log('Hello, world!');
console.log('more stuff...');
$ git commit -am "Start implementing more stuff"

digraph { rankdir=TB ranksep=0.3 node [shape=rect fillcolor="#dddddd" color="#ffffff" style=filled width=1.5] HEAD [color="red" fillcolor="#cc9f23" fontcolor="white" shape=Mrecord penwidth=1.4] main [color="transparent" fillcolor="#2288cc" fontcolor="white" shape=Mrecord] "more-stuff" [color="red" fillcolor="#2288cc" fontcolor="white" shape=Mrecord penwidth=1.4] "87ab" [color="red" penwidth=1.4] { rank=same "4e7a" -> "f595" [dir=back] "f595" -> "87ab" [dir=back color="red" penwidth=1.4] } main -> "f595" [color="#2288cc"] "87ab" -> "more-stuff" [color="red" dir=back penwidth=1.4] "more-stuff" -> HEAD [color="red" dir=back penwidth=1.4] }

Merging in changes#

You love the new stuff added to the more-stuff branch!

Let’s merge those changes back into main.

$ git checkout main
Switched to branch 'main'

$ git merge more-stuff
Updating 71760b68..0707a4f0
Fast-forward
  hello.js | 1 +
  1 file changed, 1 insertion(+)

$ git branch
* main
  more-stuff

digraph { rankdir=TB ranksep=0.3 node [shape=rect fillcolor="#dddddd" color="#ffffff" style=filled width=1.5] HEAD [color="red" fillcolor="#cc9f23" fontcolor="white" shape=Mrecord penwidth=1.4] main [color="red" fillcolor="#2288cc" fontcolor="white" shape=Mrecord penwidth=1.4] "more-stuff" [color="transparent" fillcolor="#2288cc" fontcolor="white" shape=Mrecord] { rank=same "4e7a" -> "f595" [dir=back] "f595" -> "87ab" [dir=back] } HEAD -> main -> "87ab" [color="red" penwidth=1.4] "87ab" -> "more-stuff" [color="#2288cc" dir=back] }

  • This didn’t delete more-stuff—it’s still there!

  • But we’re back on the main branch, so commits will go onto that

  • We can switch back to more-stuff and work more there

    • And re-merge it back into main periodically

Fast-forward merge#

This merge was a fast-forward merge

  • All of the changes were on one branch

  • So the other branch could just be “fast-forwarded” to that commit

  • This kind of merge is very straightforward for you and for Git

Deleting branches#

We can delete the more-stuff branch

  • Perhaps it was a failed experiment

  • Or perhaps we’ve merged it back to main and don’t need to keep it around

$ git branch -d more-stuff
Deleted branch more-stuff

Complex Merging#

Working across branches#

  • Earlier, we branched more-stuff and did all our work there.

  • Sometimes you’ll have active work in multiple branches

    • A longer-lived feature/bug branch, while work is still happening in main

    • Working on teams (each feature or person might have a branch)

  • This can lead to more significant merges

Merging changes across branches#

Let’s say we’re working on a team.

During sprint planning, we’re assigned a new task: to integrate a hip, new JS library.

We’ll make a new branch for this feature.

$ git checkout -b hip-js

…and make changes in a new file:

hip.js#
// Code using the hip new JS lib!
$ git add hip.js
$ git commit -am "Integrate new library"

digraph { rankdir=TB ranksep=0.3 node [shape=rect fillcolor="#dddddd" color="#ffffff" style=filled width=1.5] HEAD [color="red" fillcolor="#cc9f23" fontcolor="white" shape=Mrecord penwidth=1.4] main [color="transparent" fillcolor="#2288cc" fontcolor="white" shape=Mrecord] "hip-js" [color="red" fillcolor="#2288cc" fontcolor="white" shape=Mrecord penwidth=1.4] "5fc3" [color="red" penwidth=1.4] { rank=same "4e7a" -> "f595" [dir=back] "f595" -> "87ab" [dir=back] "87ab" -> "5fc3" [dir=back color="red" penwidth=1.4] } main -> "87ab" [color="#2288cc" dir=back] "5fc3" -> "hip-js" -> HEAD [color="red" penwidth=1.4 dir=back] }

While you’re working on hip-js, your team makes some changes to main

$ git checkout main
Switched to branch ‘main’
hello.js#
console.log('Hello, world!');
console.log('more stuff...');
console.log('A bug fix');
$ git commit -am "Bugfix"

digraph { rankdir=TB ranksep=0.3 node [shape=rect fillcolor="#dddddd" color="#ffffff" style=filled width=1.5] HEAD [color="red" fillcolor="#cc9f23" fontcolor="white" shape=Mrecord penwidth=1.4] main [color="red" fillcolor="#2288cc" fontcolor="white" shape=Mrecord penwidth=1.4] "hip-js" [color="transparent" fillcolor="#2288cc" fontcolor="white" shape=Mrecord] "cc90" [color="red" penwidth=1.4] { rank=same "4e7a" -> "f595" -> "87ab" [dir=back] "87ab" -> "cc90" [dir=back color="red" penwidth=1.4] } { rank=same "5fc3" } "87ab":se -> "5fc3":w [dir=back] "cc90" -> "5fc3" [weight=10 style=invis] // this is just to line up cc90 and 5fc3 HEAD -> main -> "cc90" [color="red" penwidth=1.4] "5fc3" -> "hip-js" [color="#2288cc" dir=back] }

Now they’re ready to merge in your changes:

$ git status # check that we're still on main
On branch main
nothing to commit, working directory clean

$ git merge hip-js -m "Merge in hip-js."
Merge made by the 'recursive' strategy.
  hip.js | 1 +
  1 file changed, 1 insertion(+)
create mode 100644 hip.js

digraph { rankdir=TB ranksep=0.3 node [shape=rect fillcolor="#dddddd" color="#ffffff" style=filled width=1.5] HEAD [color="red" fillcolor="#cc9f23" fontcolor="white" shape=Mrecord penwidth=1.4] main [color="red" fillcolor="#2288cc" fontcolor="white" shape=Mrecord penwidth=1.4] "hip-js" [color="transparent" fillcolor="#2288cc" fontcolor="white" shape=Mrecord] "abc4" [color="red" penwidth=1.4] { rank=same "4e7a" -> "f595" -> "87ab" -> "cc90" [dir=back] "cc90" -> "abc4" [dir=back color="red" penwidth=1.4] } { rank=same "5fc3" } "87ab":se -> "5fc3":w [dir=back] "cc90" -> "5fc3" [weight=10 style=invis] // this is just to line up cc90 and 5fc3 HEAD -> main -> "abc4" [color="red" penwidth=1.4] "5fc3" -> "hip-js" [color="#2288cc" dir=back] "5fc3":e -> "abc4":sw [dir=back color="red" penwidth=1.4] }

git log --graph#

Adding --graph to git log will show a visualization of merge history:

$ git log --graph
On branch main
*   commit abc4
|\  Merge: cc90 5fc3
| | Author: Jane Hacks <jhacks@developer.dev>
| |
| |     Merge in 'hip-js'.
| |
| * commit 5fc3
| | Author: Jane Hacks <jhacks@developer.dev>
| |
| |     Integrate new library.
| |
* | commit cc90
|/  Author: Jane Hacks <jhacks@developer.dev>
|
|       Bugfix.
|
* commit 87ab
| Author: Jane Hacks <jhacks@developer.dev>
|
|     Start implementing more stuff.
|
...

Syncing hip-js with main#

To continue working in hip-js, we should sync it with changes in main:

$ git checkout hip-js
Switched to branch hip-js

$ git merge main
Updating ...
Fast-forward
  hello.js | 1 +
  1 file changed, 1 insertion(+)

digraph { rankdir=TB ranksep=0.3 node [shape=rect fillcolor="#dddddd" color="#ffffff" style=filled width=1.5] HEAD [color="red" fillcolor="#cc9f23" fontcolor="white" shape=Mrecord penwidth=1.4] main [color="transparent" fillcolor="#2288cc" fontcolor="white" shape=Mrecord] "hip-js" [color="red" fillcolor="#2288cc" fontcolor="white" shape=Mrecord penwidth=1.4] { rank=same "4e7a" -> "f595" -> "87ab" -> "cc90" [dir=back] "cc90" -> "abc4" [dir=back] } { rank=same "5fc3" } "87ab":se -> "5fc3":w [dir=back] "cc90" -> "5fc3" [weight=10 style=invis] // this is just to line up cc90 and 5fc3 main -> "abc4" [color="#2288cc"] "abc4" -> "hip-js" -> HEAD [color="red" penwidth=1.4 dir=back] "5fc3":e -> "abc4":sw [dir=back] }

Merge Conflicts#

What’s a merge conflict?#

Most of the time, Git can figure out how to merge your work…

…unless there are changes to the same parts of the same file(s).

In this case, you have to resolve a merge conflict.

Making a conflict#

Let’s add two lines to hello.js on main and commit:

$ git checkout main
Switched to branch ‘main’
hello.js#
console.log('Hello, world!');
console.log('more stuff...');
console.log('A bug fix');
// Change from main
// End
$ git commit -am "Add comments."
[main abe15a32] Add comments.
  1 file changed, 2 insertions(+)

Now we’ll do the same on hip-js:

$ git checkout hip-js
Switched to branch ‘hip-js’
hello.js#
console.log('Hello, world!');
console.log('more stuff...');
console.log('A bug fix');
// Change from hip-js
// End
$ git commit -am "Add comments."
[main abe15a32] Add comments.
  1 file changed, 2 insertions(+)

Merging with conflicts#

When we go to merge this, we’ll get a conflict:

hello.js in main#
console.log('Hello, world!');
console.log('more stuff...');
console.log('A bug fix');
// Change from main
// End
hello.js in hip-js#
console.log('Hello, world!');
console.log('more stuff...');
console.log('A bug fix');
// Change from hip-js
// End
$ git checkout main
$ git merge hip-js -m "Merge hip-js"
Auto-merging hello.js
CONFLICT (content): Merge conflict in hello.js
Automatic merge failed; fix conflicts and then commit the result.

Resolving conflicts#

Start by following the directions from git status:

$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add [file]..." to mark resolution)

    both modified:      hello.js

Open the file with the conflicts:

hello.js#
console.log('Hello, world!');
console.log('more stuff...');
console.log('A bug fix');
<<<<<< HEAD
// Change from main
======
// Change from hip-js
>>>>>> hip-js
// End

VS Code comes with tools to help resolve merge conflicts.

Screenshot of VS Code's merge conflict tools

Changes in green come from HEAD (main), changes in blue come from hip-js.#

You can resolve conflicts without VS Code’s tools too.

Just remove the merge markers (like <<<<<<< HEAD)

hello.js#
console.log('Hello, world!');
console.log('more stuff...');
console.log('A bug fix');
// Change from main and hip-js
// End
$ git commit -am "Merge comments from hip-js"

Best Practices#

Branching styles#

These are tools

  • Different companies use them differently!

  • Some branch for every bug, some rarely branch

  • Expect your team lead to explain how it works

  • You can practice with throwaway repos

Don’t panic#

  • You probably won’t be doing much merging yourself

  • Typically, a team lead does the merging

  • It’s good for you to understand how to help them

Good habits to practice#

  • Make sure you’re working on different areas

    • Communicate before you code!

  • Make small commits and push often

  • Avoid using GitHub’s “edit in browser” feature

    • Making changes in the browser and locally can lead to merge conflicts!

Branches and GitHub#

Pushing branches to GitHub#

To push branches to GitHub:

$ git push -u origin hip-js

This works even if you’re in a different branch.

Pulling from GitHub#

To pull a branch from GitHub:

$ git checkout name-of-branch
$ git pull origin name-of-branch

Branches and Pull Requests#

In GitHub, pull requests let you notify others about changes you’ve made to a branch.

Pull requests enable collaborators to discuss and review potential changes before they’re merged into the base branch.

You’ll learn how to make a pull request during lab!

Resources#

Resources#

Other things to learn about#

Looking ahead#

Learn how to collaborate using GitHub (forking, code reviews, etc.)!