cott Cheng

Playing With Git Branches

Though it has been almost half a year since the first time I “gitted” (and I love it!), all git commands I’ve ever used are clone, add, commit, pull and push. But when I created my first GitHub page, I had to start dealing with branches. It’s like a rewarding “stepping out of the comfort zone” experience. I very much enjoyed this brief exploration, and would like to write down what I learned about branching in the process.

Branching 101

First, the very basics of branch management:

  • Use git branch <branchname> or git checkout -b <branchname> to create a new branch.
    • If you want the new branch to exist on the remote, push it with git push origin <branchname>.
  • Use git branch to view all branches, and find out what branch you are at.
  • Use git checkout <branchname> to switch between branches.
  • Use git branch -d <branchname> to delete a branch locally.
  • Use git push origin :<branchname> (with the colon) to delete it on the remote.

Merging and Rebasing

Branches often need to interact by merging their changes. There are at least two ways to “merge” changes in different branches: git merge and git rebase. By git merge <branchname>, e.g. git merge dev on the master branch, the following happens:

  1. Commits in dev branch are merged into master.
    • Handle conflicts if necessary.
  2. A new merge commit is created on top of the two branches.

By git rebase <branchname>, e.g. git rebase dev on the master branch, the following will happen:

  1. Commits between the last merge and present on the master branch will be erased (and kept somewhere temporarily).
  2. Commits on dev branch will be appended to the original HEAD on master (the last merge point).
  3. Your changes are put back on top of the dev commits in new commits.
    • Handle conflicts if necessary.

If you are interested in the detailed mechanism, git-scm.com has more on branching / merging and rebasing. Jarrod Spillers elaborated in a post the difference between git merge and git rebase as well as when to use which, thought slightly flawed.

I also found it interesting to learn that git pull is actually git fetch then git merge, whereas git pull --rebase is git fetch then git rebase, as is documented on the man page.

One thing that kinda surprised me is that different branches actually coexist in the same directory! As you switch branches using git checkout <branchname>, the files in that folder would actually change with it. It’s like the Room of Requirement in Harry Potter – it’s the same room, but you can go in and see different things. Amazing. The interesting part is that when I create a file in the repo, it is visible in all branches; but when I add and commit it in a branch, it becomes “private” and vanishes in other branches. Similarly, when a file is modified in a branch, the change is visible everywhere. Sometimes, however, Git does not allow me to switch branches when there are modified files, and I have not figured out why:

1
2
3
4
5
6
7
8
  # On dev branch and modified devonly.md
$ git checkout master
  error: Your local changes to the following files would be
  overwritten by checkout:
    devonly.md
  Please, commit your changes or stash them before you can
  switch branches.
  Aborting

Branching gh-pages

Now I’ll put the branching theory to practice and build my GitHub page. The GitHub pages documentation is perfect if you want to create a page from scratch. I believe this is useful for most projects, but not for mine, where I want the project itself to be the GitHub page.

Create gh-pages branch
1
2
$ git checkout -b gh-pages
$ git push origin gh-pages

Now gh-pages and master branch are on the same “page”. From this point on, If you want to keep the gh-pages in sync with master branch, you should only commit changes to the master branch, and merge them to gh-pages when the page is ready to go public, using git merge master or git rebase master (as Lea Verou does it) on the gh-pages branch.

However, my need is somewhat different: I don’t want the src directory in gh-pages branch; the HTML, CSS and JavaScirpt are sufficient for my static page. So I did git rm -r src on gh-pages branch. The problem is, future merges will most probably pose conflicts, since I will constantly update src on the master branch which I just deleted on the gh-pages branch. No worries, here is a solution. As Nicolas Gallagher points out, instead of merging the entire branch, I can choose to only merge changes in specific files:

1
2
3
4
5
6
$ git checkout gh-pages
$ git checkout master -- index.html style.css
  # Only merge index.html and style.css
$ git commit -m 'commit message'
$ git push
  # Add `origin gh-pages` if you don't want to push to master

Side note: if the project is all about the web page, Oli Studholme offered another solution to simply delete the master branch and only keep gh-pages. This could also work for me.

This is pretty much all I need to know to handle my page, but there’s more. I’ve learned from this snippet that a git hook can be used to automate this syncing process, and I have tailored it to my use case:

post-commit
1
2
3
4
5
#!/bin/sh
git checkout gh-pages
git checkout master -- index.html style.css
git commit -m 'updated gh page'  # Commit loop!
git checkout master

Name it as post-commit, put it under repo/.git/hooks, and the script will run after each commit (as the name “post-commit” suggests). This way, gh-pages branch will automagically sync with master! Just don’t forget to make sure post-commit is executable.

The caveat here is the git commit in the hook script (line 4) – it will cause an unwanted commit loop. There seems no way to bypass the post-commit hook (--no-verify or -n only bypasses pre-commit and commit_msg). Fortunately, git is smart enough to kill the loop soon enough before it goes bad, so basically this hook works alright.

Comments