Monday, July 04, 2011

Git: Branching and Merging

Git allows us to maintain multiple branches of development.  This is common and consider a must to develop with git.  You may switch between branches to enjoy isolate development workflow.

To ease the explanation, we create a bare repository as origin and 2 cloned repository from origin.

$ mkdir project.git 
$ cd project.git/ 
$ git init --bare 
Initialized empty Git repository in /tmp/test.branch/project.git/ 
$ cd .. 
$ git clone project.git clone1 
Initialized empty Git repository in /tmp/test.branch/clone1/.git/ 
warning: You appear to have cloned an empty repository. 
$ cd clone1 
$ echo "This is readm" > readme 
$ git add readme 
$ git commit -m "first commit" 
[master (root-commit) 8b60032] first commit 
1 files changed, 1 insertions(+), 0 deletions(-) 
create mode 100644 readme 
$ git push 
No refs in common and none specified; doing nothing. 
Perhaps you should specify a branch such as 'master'. 
fatal: The remote end hung up unexpectedly 
error: failed to push some refs to '/tmp/test.branch/project.git' 
$ git push origin master 
Counting objects: 3, done. 
Writing objects: 100% (3/3), 215 bytes, done. 
Total 3 (delta 0), reused 0 (delta 0) 
Unpacking objects: 100% (3/3), done. 
To /tmp/test.branch/project.git 
* [new branch]      master –> master 
$ cd .. 
$ git clone project.git clone2 
Initialized empty Git repository in /tmp/test.branch/clone2/.git/ 
$ ls -a clone2 
.  ..  .git  readme

Create git branch

Let’s create a new branch “newfeature1” in clone1:

$ cd clone1 
$ git branch 
* master 
$ git branch newfeature1
$ git branch 
* master 
  newfeature1 
$ git checkout newfeature1 
Switched to branch 'newfeature1' 
$ git branch 
  master 
* newfeature1

The repository is now point to branch newfeature1.  Add a new file to the repository and commit changes:

$ echo "This is new feature" > feature1 
$ git add feature1 
$ git commit -m "Commit new feature" 
[newfeature 399cffe] Commit new feature 
1 files changed, 1 insertions(+), 0 deletions(-) 
create mode 100644 feature1

Switch branch

Continue from above example.  Let’s switch between branches and see what happen to the working directory:

$ git branch 
  master 
* newfeature1 
$ ls -a 
.  ..  .git  feature1  readme 
$ git checkout master 
Switched to branch 'master' 
$ ls -a 
.  ..  .git  readme

When switch branch from “newfeature1” to “master”, file “feature1” that was committed in “newfeature1” won’t appear in branch “master”.

List branches in repository

To list existing local branches:

$ git branch 
* master 
  newfeature1

Active branch is indicated by * in front.

To list local and remote tracked branch

$ git branch -a 
* master 
  newfeature1 
  remotes/origin/master

To list remote tracked branch only:

$ git branch -r 
  origin/master

Merge with Fast Forward

Assume we perform few commits into branch “newfeature1”:

$ echo "changes #1" >> feature1 
$ git commit -a -m "commit changes #1" 
[newfeature1 fc6a399] commit changes #1 
1 files changed, 1 insertions(+), 0 deletions(-) 
$ echo "changes #2" >> feature1 
$ git commit -a -m "commit changes #2" 
[newfeature1 1ea0fda] commit changes #2 
1 files changed, 1 insertions(+), 0 deletions(-) 
$ git log --oneline --graph 
* 1ea0fda commit changes #2 
* fc6a399 commit changes #1 
* 048ce34 Commit new feature 1 
* 8b60032 first commit

Perform a fast forward merge into master:

$ git checkout master 
Switched to branch 'master' 
$ git merge newfeature1 
Updating 8b60032..1ea0fda 
Fast-forward 
feature1 |    3 +++ 
1 files changed, 3 insertions(+), 0 deletions(-) 
create mode 100644 feature1 
$ git log --oneline --graph 
* 1ea0fda commit changes #2 
* fc6a399 commit changes #1 
* 048ce34 Commit new feature 1 
* 8b60032 first commit

All commit changes from newfeature1 is now merge into master.

Merge without Fast Forward

An advantage with no fast forward merge preserve the branch commit history:

$ git log --oneline --graph 
* 8b60032 first commit 
$ git merge --no-ff newfeature1 
Merge made by recursive. 
feature1 |    3 +++ 
1 files changed, 3 insertions(+), 0 deletions(-) 
create mode 100644 feature1 
$ git log --oneline --graph 
*   4725f13 Merge branch 'newfeature1' 
|\ 
| * 1ea0fda commit changes #2 
| * fc6a399 commit changes #1 
| * 048ce34 Commit new feature 1 
|/ 
* 8b60032 first commit

Push Branch to remote

So far all branches are stored in local repository only.  You may share the branch with others by pushing it to origin:

$ git branch -a 
* master 
  newfeature1 
  remotes/origin/master 
$ git checkout newfeature1 
Switched to branch 'newfeature1' 
$ git push origin HEAD 
Total 0 (delta 0), reused 0 (delta 0) 
To /tmp/test.branch/project.git 
* [new branch]      HEAD -> newfeature1 
$ git branch -r 
  origin/master 
  origin/newfeature1

Now we create more branch and push to origin:

$ git branch newfeature2 master 
$ git checkout newfeature2 
Switched to branch 'newfeature2' 
$ echo "Feature 2" > feature2 
$ git branch 
  feature3 
  master 
  newfeature1 
* newfeature2 
$ git add feature2 
$ git commit -m "commit feature 2" 
[newfeature2 ef39e45] commit feature 2 
1 files changed, 1 insertions(+), 0 deletions(-) 
create mode 100644 feature2 
$ git push origin HEAD 
Total 0 (delta 0), reused 0 (delta 0) 
To /tmp/test.branch/project.git 
* [new branch]      HEAD –> newfeature2

$ git branch newfeature3 master 
$ git checkout newfeature3 
Switched to branch 'newfeat 
$ echo "Feature 3" > feature3 
$ git add feature3 
$ git commit -m "commit feature 3" 
[newfeature3 ec1da8d] commit feature 3 
1 files changed, 1 insertions(+), 0 deletions(-) 
create mode 100644 feature3 
$ git push origin HEAD 
Counting objects: 7, done. 
Compressing objects: 100% (4/4), done. 
Writing objects: 100% (6/6), 512 bytes, done. 
Total 6 (delta 1), reused 0 (delta 0) 
Unpacking objects: 100% (6/6), done. 
To /tmp/test.branch/project.git 
* [new branch]      HEAD –> newfeature3

We have these branches now both in local and origin repositories:

$ git branch -a 
  master 
  newfeature1 
* newfeature2 
  newfeature3 
  remotes/origin/master 
  remotes/origin/newfeature1 
  remotes/origin/newfeature2 
  remotes/origin/newfeature3

Fetch Remote Branch

Let’s switch to clone2:

$ cd clone2 
$ git branch -a 
* master 
  remotes/origin/HEAD -> origin/master 
  remotes/origin/master 
$ git remote show origin 
* remote origin 
  Fetch URL: /tmp/test.branch/project.git 
  Push  URL: /tmp/test.branch/project.git 
  HEAD branch: master 
  Remote branches: 
    master      tracked 
    newfeature1 new (next fetch will store in remotes/origin) 
    newfeature2 new (next fetch will store in remotes/origin) 
    newfeature3 new (next fetch will store in remotes/origin) 
  Local branch configured for 'git pull': 
    master merges with remote master 
  Local ref configured for 'git push': 
    master pushes to master (up to date)

Notice that there are 3 new branches: newfeature1, newfeature2 and newfeature3 in origin.

Now fetch from origin,

$ git fetch 
From /tmp/test.branch/project 
* [new branch]      newfeature1 -> origin/newfeature1 
* [new branch]      newfeature2 -> origin/newfeature2 
* [new branch]      newfeature3 -> origin/newfeature3 
$ git branch -a 
* master 
  remotes/origin/HEAD -> origin/master 
  remotes/origin/master 
  remotes/origin/newfeature1 
  remotes/origin/newfeature2 
  remotes/origin/newfeature3 
$ git remote show origin 
* remote origin 
  Fetch URL: /tmp/test.branch/project.git 
  Push  URL: /tmp/test.branch/project.git 
  HEAD branch: master 
  Remote branches: 
    master      tracked 
    newfeature1 tracked 
    newfeature2 tracked 
    newfeature3 tracked 
  Local branch configured for 'git pull': 
    master merges with remote master 
  Local ref configured for 'git push': 
    master pushes to master (up to date)

To have a look on the remote branch newfeature2, use git checkout:

$ git checkout origin/newfeature2 
Note: checking out 'origin/newfeature2'.

You are in 'detached HEAD' state. You can look around, make experimental 
changes and commit them, and you can discard any commits you make in this 
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may 
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD is now at c47ec36... This is new feature 2 
$ ls -a 
.  ..  .git  feature2  readme

The checkout origin/newfeature2 will switch local repository to “detached HEAD”.  A detached HEAD doesn’t belongs to any branch:

$ git branch -a 
* (no branch) 
  master 
  remotes/origin/master 
  remotes/origin/newfeature1 
  remotes/origin/newfeature2 
  remotes/origin/newfeature3

You shouldn’t commit any changes that doesn’t belong to a named branch or else others will have problems merging with the changes.  Instead you should name a branch or checkout local branch before commit any changes:

$ git checkout newfeature2
Branch newfeature2 set up to track remote branch newfeature2 from origin.
Switched to a new branch 'newfeature2'
$ git branch -a
  master
* newfeature2
  remotes/origin/master
  remotes/origin/newfeature1
  remotes/origin/newfeature2
  remotes/origin/newfeature3

Delete local branch

For branch that you never push to origin, you may delete it as follow:

$ git branch
  master
* newfeature1
$ git branch -d newfeature1
error: Cannot delete the branch 'newfeature1' which you are currently on.

Active branch is not allow to delete.  You may switch to other branch first

$ git checkout master
Switched to branch 'master'
$ git branch -d newfeature1
error: The branch 'newfeature1' is not fully merged.
If you are sure you want to delete it, run 'git branch -D newfeature1'.
$ git branch -D newfeature1
Deleted branch newfeature1 (was 201bb22).
$ git branch
* master

You can’t delete branch using “-d” if it is not merged with others.  To force delete, use “-D” option.

Delete remote tracked branch

Assume the repository has the following branches:

$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/newfeature1
  remotes/origin/newfeature2
  remotes/origin/newfeature3
$ git remote show origin
* remote origin
  Fetch URL: /tmp/test.branch/project.git
  Push  URL: /tmp/test.branch/project.git
  HEAD branch: master
  Remote branches:
    master      tracked
    newfeature1 tracked
    newfeature2 tracked
    newfeature3 tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

Delete a remote tracked branch:

$ git branch -d -r origin/newfeature3
Deleted remote branch origin/newfeature3 (was ec1da8d).
$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/newfeature1
  remotes/origin/newfeature2
$ git remote show origin
* remote origin
  Fetch URL: /tmp/test.branch/project.git
  Push  URL: /tmp/test.branch/project.git
  HEAD branch: master
  Remote branches:
    master      tracked
    newfeature1 tracked
    newfeature2 tracked
    newfeature3 new (next fetch will store in remotes/origin)
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

No comments: