Monday, July 04, 2011

Git: Submodule

Introduction

For those who familiar with CVS ampersand modules mechanism, git submodule is some how similar to it.

Prepare a submodule bare repository

Create 3 repositories that may use as submodules:

$ mkdir ~/project
$ cd ~/project
$ mkdir sub1.git sub2.git sub3.git
$ git init --bare sub1.git
Initialized empty Git repository in /tmp/test.sub/sub1.git/
$ git init --bare sub2.git
Initialized empty Git repository in /tmp/test.sub/sub2.git/
$ git init --bare sub3.git
Initialized empty Git repository in /tmp/test.sub/sub3.git/

Three bare repositories now exists in your home folder:

$ dir *.git
sub1.git:
HEAD  branches  config  description  hooks  info  objects  refs

sub2.git:
HEAD  branches  config  description  hooks  info  objects  refs

sub3.git:
HEAD  branches  config  description  hooks  info  objects  refs

Prepare super git repository

The super git repository is an example of a git repository that hold some submodules.

$ mkdir super
$ cd ~/super
$ git init
$ echo “This is super” > readme
$ git add readme
$ git commit –m “Super commit”

Add submodules

Next, we add 3 sub modules into the super git repository:

$ git submodule add /tmp/test.sub/sub1.git
Initialized empty Git repository in /tmp/test.sub/super/sub1/.git/
warning: You appear to have cloned an empty repository.
fatal: You are on a branch yet to be born
Unable to checkout submodule 'sub1'
$ git submodule add /tmp/test.sub/sub2.git
Initialized empty Git repository in /tmp/test.sub/super/sub2/.git/
warning: You appear to have cloned an empty repository.
fatal: You are on a branch yet to be born
Unable to checkout submodule 'sub2'
$ git submodule add /tmp/test.sub/sub3.git
Initialized empty Git repository in /tmp/test.sub/super/sub3/.git/
warning: You appear to have cloned an empty repository.
fatal: You are on a branch yet to be born
Unable to checkout submodule 'sub3'
$ ls -a
.  ..  .git  sub1  sub2  sub3

You may have noticed there are warning and error messages when execute “git submodule add” command.  This is due to the sub modules are empty in the example.  You won’t get the messages if submodule is not empty.

We should further re-execute “git submodule add” again to complete the task:

$ git submodule add /tmp/test.sub/sub1.git
Adding existing repo at 'sub1' to the index
$ git submodule add /tmp/test.sub/sub2.git
Adding existing repo at 'sub2' to the index
$ git submodule add /tmp/test.sub/sub3.git
Adding existing repo at 'sub3' to the index
$ ls -a
.  ..  .git  .gitmodules  sub1  sub2  sub3

A new file .gitmodules was created:

$ cat .gitmodules
[submodule "sub1"]
        path = sub1
        url = /home/user/sub1.git
[submodule "sub2"]
        path = sub2
        url = /home/user/sub2.git
[submodule "sub3"]
        path = sub3
        url = /home/user/sub3.git

We should now add some changes to all empty sub module directory:

$ cd sub1
$ touch README
$ git add README
$ git commit -m "commit #1"
[master (root-commit) 3a8d946] commit #1
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 README
$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 201 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To /tmp/test.sub/sub1.git
* [new branch]      master -> master
$ cd ../sub2
$ touch README
$ git add README
$ git commit -m "commit #1"
[master (root-commit) 63b5d68] commit #1
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 README
$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 200 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To /tmp/test.sub/sub2.git
* [new branch]      master -> master
$ cd ../sub3
$ touch README
$ git add README
$ git commit -m "commit #1"
[master (root-commit) 46b2a53] commit #1
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 README
$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 201 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To /tmp/test.sub/sub3.git
* [new branch]      master –> master
$ cd ..

The git repository status now become

$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#       new file:   .gitmodules
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       sub1/
#       sub2/
#       sub3/

Add and commit submodule folder

$ git add sub1 sub2 sub3
$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#       new file:   .gitmodules
#       new file:   sub1
#       new file:   sub2
#       new file:   sub3
#

Commit changes:

$ git commit -m "super commit"
[master (root-commit) 7029640] super commit
4 files changed, 12 insertions(+), 0 deletions(-)
create mode 100644 .gitmodules
create mode 160000 sub1
create mode 160000 sub2
create mode 160000 sub3
$ git status
# On branch master
nothing to commit (working directory clean)

To further make a super git repository as bare repository, run

$ git clone –bare ~/super ~/super.git
$ git remote add origin ~/super.git
$ git config branch.master.remote origin
$ git config branch.master.merge refs/heads/master

Clone a git repository with submodules

This example shows how to work with clone of a super git repository that contain submodules:

$ cd ~/project
$ git clone super.git super.clone
$ cd ~/super.clone
$ ls –a *

readme

sub1:
.  ..

sub2:
.  ..

sub3:
.  ..

All sub module’s directory is empty at this moment.  Run this to get updated commits

$ git submodule init
$ git submodule update
Initialized empty Git repository in /home/user/super.clone2/sub1/.git/
Submodule path 'sub1': checked out '8acdbdd86119c4a9777bbefe13bd8f80c96a8b7a'
Initialized empty Git repository in /home/user/super.clone2/sub2/.git/
Submodule path 'sub2': checked out 'e390c3e3114fde7643a3544c0a262eff5a52e09c'
Initialized empty Git repository in /home/user/super.clone2/sub3/.git/
Submodule path 'sub3': checked out '001355362c4ed758f17206a3fbbb2f7637983434'
$ ls -a *
readme

sub1:
.  ..  .git  readme

sub2:
.  ..  .git  readme

sub3:
.  ..  .git  readme

An alternate way to clone repository is run everything in a single command:

$ git clone --recursive ~/super.git ~/super.clone
Initialized empty Git repository in /tmp/submodules/super.clone2/.git/
Submodule 'sub1' (/home/user/sub1.git) registered for path 'sub1'
Submodule 'sub2' (/home/user/sub2.git) registered for path 'sub2'
Submodule 'sub3' (/home/user/sub3.git) registered for path 'sub3'
Initialized empty Git repository in /home/user/super.clone2/sub1/.git/
Submodule path 'sub1': checked out '8acdbdd86119c4a9777bbefe13bd8f80c96a8b7a'
Initialized empty Git repository in /home/user/super.clone2/sub2/.git/
Submodule path 'sub2': checked out 'e390c3e3114fde7643a3544c0a262eff5a52e09c'
Initialized empty Git repository in /home/user/super.clone2/sub3/.git/
Submodule path 'sub3': checked out '001355362c4ed758f17206a3fbbb2f7637983434'
$ ls -a *
sub1:
.  ..  .git  README

sub2:
.  ..  .git  README

sub3:
.  ..  .git  README

Add changes to submodule

The following example shows a submodule workflow to add, commit, push new changes:

Check out a branch first

$ cd ~/project/super/sub1
super/sub1$ git branch
* (no branch)
  master
super/sub1$ git checkout master
super/sub1$ git branch
* master

Make a changes

$ cat readme
This is submodule
super/sub1$ echo "New changes" >> README
super/sub1$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   readme
#
no changes added to commit (use "git add" and/or "git commit -a")

Commit and push changes

$ git commit -a -m "New changes"
[master 6a26854] New changes
1 files changed, 1 insertions(+), 0 deletions(-)
$ git push
Counting objects: 5, done.
Writing objects: 100% (3/3), 246 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To /tmp/test.sub/sub1.git
   3a8d946..6a26854  master –> master

Check status of submodule directory

$ cd ..
$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   sub1
#
no changes added to commit (use "git add" and/or "git commit -a")

The submodule “sub1” status is modified now.

Commit and push changes of submodule directory

$ git commit -a -m "commit sub1"
[master 6039eee] commit sub1
1 files changed, 1 insertions(+), 1 deletions(-)
$ git status
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
nothing to commit (working directory clean)
$ git push
Counting objects: 3, done.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 309 bytes, done.
Total 2 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (2/2), done.
To /tmp/test.sub/super.git
   7029640..6039eee  master –> master

Update a submodule

Try this to pull changes from origin repository to update local repository:

Pull changes

$ cd ../super.clone
super.clone$ git pull
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 2 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (2/2), done.
Updating 7029640..6039eee
Fast-forward
sub1 |    2 +-
1 files changed, 1 insertions(+), 1 deletions(-)

$ cat sub1/README
$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   sub1
#
no changes added to commit (use "git add" and/or "git commit -a")

Up to this stage, the file has not updated to latest HEAD version yet.  After pull from origin, the status of sub1 show modified.  Continue to next topic to complete submodule update task.

Update submodule

To pull latest commit snapshot from origin, run “git submodule update” to checkout latest committed snapshot:

$ git submodule update
Submodule path 'sub1': checked out '6a26854c7985d6ba516dcca8fc8cb42908bcb639'
$ git status
# On branch master
nothing to commit (working directory clean)
$ cat sub1/README
New Changes

The repository folder is in clean state now.  sub1 folder also updated to latest committed snapshot.  Let’s continue to check the status of sub1 by continue to next topic.

Detached HEAD in submodule

Let’s check the status of sub1 now:

$ cd sub1
$ cat README
New Changes
$ git branch
* (no branch)
  master

The current position of sub1 point to a commit stage without any label.  This is known as detached HEAD commit or headless commit.  Commit detached HEAD changes is not encounrage.  It may be problems for other collaborators to retrieve the changes.

Instead, we should checkout the master branch, work, commit changes and merge commits to master branch:

$ git checkout master
Previous HEAD position was 6a26854... New changes
Switched to branch 'master'
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
$ cat README
$

The master branch’s README file is in last committed snapshot.  Let’s check the status of super git repository after checkout:

$ cd ..
$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   sub1
#
no changes added to commit (use "git add" and/or "git commit -a")

It’s parent directory has changed to modified.  That simply means sub1 doesn’t match with latest committed snapshot.

$ git merge origin
Updating 3a8d946..6a26854
Fast-forward
README |    1 +
1 files changed, 1 insertions(+), 0 deletions(-)
$ cat README
New Changes

You may also use “git pull” to replace the “git merge origin” .  Let’s check parent status again:

$ cd ..
$ git status
# On branch master
nothing to commit (working directory clean)

It is in clean state now.

No comments: