Sunday, December 14, 2014

Git Merge vs. Rebase

Git is a popular revision control software, allowing you to save and centralize changes and additions to your code, making it easier to collaborate with other developers. You've probably heard of Github, a remotely-operated Git repository hosting service and huge proponent of open source. I strongly recommend you create an account, if you haven't already. We're comparing two popular Git branching strategies, today. We won't be going over other revision control alternatives like Subversion(SVN), mercurial, or TFS, here.

Prerequisites: It helps to have a basic understanding of how to set up a Git project, so go through Git's Set Up Guides after you've created your own Github account. Make sure also to check out A Merge-Based Branch and Release Strategy to get a high level understanding of the process we'll be going over below. This guide was written for Mac OS X users.

Make sure that when you create your github repository, you do not opt to set it up with a README file. We'll be doing that below.

Visualizing Git Branches

Think of git code as this tree like structure. The master is your trunk. That's all of your code in one state. The process of adding and committing is, in essence, saving your changes, along the way. To make it easier to maintain your code, you're going to want to make branches off of the trunk, and save code to those branches. If you're satisfied with your code, you'll merge that branch back into the trunk. If you aren't, you can just leave the branch where it is or delete it. The key here is that branches are isolated from each other. That's git branching in a nutshell. It's easy to see the differences between merging and rebasing in SourceTree.

A Basic Merge Workflow

Let's initialize our git repo, associate it with the remote repo we set up using at Github.com, create a README file and push it to the default master branch. Pushing code simply means uploading to the github repository. Enter the following commands in terminal:

$ mkdir git_merge
$ cd git_merge/
$ touch README.md
$ git init
Initialized empty Git repository in /Users/roblayton/sandboxes/repo-name/.git/
$ git add README.md
$ git commit -m "first commit"
[master (root-commit) ea64565] first commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md
$ git remote add origin https://github.com/user/repo-name.git
$ git push -u origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 212 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/user/repo-name.git
 * [new branch]      master -$gt; master
Branch master set up to track remote branch master from origin.

This next step creates a develop branch off of the master trunk.

$ git checkout -b develop
Switched to a new branch 'develop'

Next, we'll create our first feature branch. The convention to use when making feature branches off of develop is firstinitial_featurename.

$ git checkout -b rl_feature_a
Switched to a new branch 'rl_feature_a'
$ touch text.txt
$ git add text.txt
$ git commit -m "Added text file"
[rl_feature_a a8a5bdb] Added text file
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 text.txt

When we're done commit our changes to the feature branch, and we've run our unit tests, it's safe to merge the branch into develop.

$ git checkout develop
Switched to branch 'develop'
$ git merge rl_feature_a
Updating ea64565..a8a5bdb
Fast-forward
 text.txt | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 text.txt

Here, we're pushing our version of develop to the remote repository so that everyone else on our team has our changes.

$ git push origin develop
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 247 bytes | 0 bytes/s, done.
Total 2 (delta 0), reused 0 (delta 0)
To https://github.com/roblayton/proj-name.git
 * [new branch]      develop > develop

Now, this is where we "freeze" the develop branch were it is and make an rc branch for QA. This gets made into it's own separate 0.1.0-rc.1 branch off of develop. No further development will be made on this branch in case unless QA points out a bug. Any further development should be made on feature branches. If we were to make changes to the release candidate at this stage, without QA knowing, we'd be invalidating all of their testing.

$ git checkout -b 0.1.0-rc.1
Switched to a new branch '0.1.0-rc.1'
$ git push origin 0.1.0-rc.1
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/roblayton/proj-name.git
 * [new branch]      0.1.0-rc.1 > 0.1.0-rc.1

The release candidate passed QA so we merge the branch into master.

$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ git merge 0.1.0-rc.1
Updating ea64565..a8a5bdb
Fast-forward
 text.txt | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 text.txt

Finally, we tag our release, making it show up in the "Releases" section of our remore repository where the archived package can be downloaded.

$ git tag -a 0.1.0 -m '0.1.0 release'
$ git push origin master
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/roblayton/proj-name.git
   ea64565..a8a5bdb  master > master
$ git push --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 161 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To https://github.com/roblayton/proj-name.git
 * [new tag]         0.1.0 > 0.1.0

Rebasing

When rebasing, you are rewriting history. So instead of multiple pathways that intertwine around the trunk, you get a linear representation of the history. While the result is much cleaner than merging, you are essentially flattening all the branches, and fast forward merging. You end up resetting the base commit to the most recent commit of the branch you're merging into.

Let's create a new feature branch.

$ git checkout rl_feature_b
error: pathspec 'rl_feature_b' did not match any file(s) known to git.
$ git checkout -b rl_feature_b
Switched to a new branch 'rl_feature_b'
$ touch textb.txt
$ git add textb.txt
$ git commit -m "Added another text file"
[rl_feature_b 38c4fd7] Added another text file
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 textb.txt

Now rebase the feature into develop.

$ git checkout develop
Switched to branch 'develop'
$ git rebase rl_feature_b
First, rewinding head to replay your work on top of it...
Fast-forwarded develop to rl_feature_b.

Cut a release candidate.

$ git checkout -b 0.2.0-rc.1
Switched to a new branch '0.2.0-rc.1'
$ git push origin 0.2.0-rc.1
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 256 bytes | 0 bytes/s, done.
Total 2 (delta 0), reused 0 (delta 0)
To https://github.com/roblayton/proj-name.git
 * [new branch]      0.2.0-rc.1 -> 0.2.0-rc.1

And rebase the release candidate into master.

$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
$ git rebase 0.2.0-rc.1
First, rewinding head to replay your work on top of it...
Fast-forwarded master to 0.2.0-rc.1.
$ git tag -a 0.2.0 -m '0.2.0 release'
$ git push origin master
Total 0 (delta 0), reused 0 (delta 0)
gTo https://github.com/roblayton/proj-name.git
   a8a5bdb..38c4fd7  master -> master
$ git push --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 162 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To https://github.com/roblayton/proj-name.git
 * [new tag]         0.2.0 -> 0.2.0

No comments:

Post a Comment