Thursday, August 15, 2013

Working With Git Branches

At a previous job, we switched from using CVS to using Git and I was lucky enough to work with a bunch of guys who were big on Git best practices. My biggest take-away was branching. Branching is great because it gives you a “sandbox” to develop in. It gives you the freedom to commit your work early and often, essentially creating backups as you develop. You can revert to a previous commit if you ever need to. And you can merge in other people’s changes without the risk of losing your work if things don’t go smoothly (it’s bound to happen with multiple developers, no matter which source control system you use.) Think of committing to a branch like saving your code as you type, only at a much higher level.

So how do you branch? Well, let’s start by updating the branch that you will be branching off of. When you clone a repository, you are creating a local copy of the server’s branches, along with those branches’ complete histories. Most of the work you do with Git is done offline, without connecting to the server that you cloned the repo from. That means that your local repo will not be aware of new commits (i.e., commits made by other developers) until you connect to the server and ask for them. That is done with Git’s fetch command.

git fetch

Now your local repo knows about everything that happened remotely since your repo was last updated. But fetching doesn’t automatically update your local branches (e.g., master) to look like they do remotely (e.g., origin/master.) After all, wouldn’t want a source control tool to meddle with your project unless you told it to. So how do you get your local master branch updated to the latest changes you just pulled? Assuming you haven’t made any changes you haven’t committed, get a fresh start by doing a “hard reset” on your local master branch.

git reset --hard origin/master

Now that your master branch is the same as origin/master, create the branch that you will be doing your work on.

git branch new-branch-name
git checkout new-branch-name

You can also do that in one step using

git checkout -b new-branch-name
. A common way to use branching is to create a branch for every feature you work on, which would likely mean that you would have one branch for every ticket that gets assigned to you in a system like JIRA or Version One. Doing so allows you to work on multiple features independently, without intertwining their code.

Now that you are on your newly created feature branch, work on your feature and commit your changes any time you want a “checkpoint” your progress.

Once you’re ready to check in your work, you’ll want to make sure that your changes will play well with any changes people have committed to origin/master in the mean time.

  1. Make sure that all of your changes have been committed to your branch. If not, commit them or forever hold your peace.
  2. git fetch
    Just like before, this retrieves all of the changes that have been made remotely but doesn’t affect your local branches or workspace.
  3. git merge origin/master
    merges those changes into the current branch. If Git is able to merge without any conflicts it can’t resolve on its own, it will commit the result of the merge. If there are conflicts it can’t resolve, your branch will be left in a “merging” state until you resolve the conflicts manually, “add” those conflicting files and commit. Before moving on, make sure that the changes you brought in from origin/master don’t cause any problems that keep the project from compiling or running as expected. That may mean updating your project’s dependencies or even updating your code to address API changes that may have happened elsewhere in the project. If you end up making changes, be sure to commit those new changes before continuing.
  4. Now that your branch is up to date, it’s time to merge your changes back into master and push them to origin. In a sense, we really just use our local master branch as a staging area for pushing changes to origin. Switch to your master branch using
    git checkout master
    . Since we haven’t been developing on the master branch (i.e., it doesn’t have changes that haven’t been pushed to origin) and haven’t been bothering to keep it up to date, rather than merging in the latest changes from origin, reset it using
    git reset --hard origin/master
    .
  5. Merge your feature changes into your freshly reset master branch using
    git merge --squash feature-branch-name
    . Since your feature branch was just updated with the latest changes from origin/master (the same commit that you just reset your local master branch to,) Git shouldn't have any problem performing the merge/squash and you should see that it was performed as a “fast forward” squash.
    On a side note, notice that we are using the squash flag. Without it, every commit that was made on the feature branch would be “brought over” to master and would show up in its history. That’s bad for two reasons. First, from a best practice standpoint, the master branch’s history should read like a list of features that were committed atomically, not cobbled together with a dozen ten line commits. To put it another way, master is not your sandbox – branches are. And second, from a more practical standpoint for people using code review tools like Gerrit, without squashing, every one of those commits pushed to origin will need to be code reviewed separately.
  6. When you perform a merge using the squash flag, it does not commit the changes for you. That’s good because it gives you an opportunity to write a nice little commit message, summarizing all the hard work you did on your feature branch (with a Gerrit change-id if needed) before pushing your changes to origin. Commit your changes with
    git commit
    .
  7. Now your local master branch has all of your feature branch changes in a single commit that’s ready to be pushed to master. Do so with
    git push
    . If the remote Git server rejects your push, look at the error message. It’s possible that someone beat you to the punch. That sucks but it’s simple to fix. Checkout your feature branch again (
    git checkout branch-name
    ) and repeat this process starting with step 2.