An alternative to submodules is subtree merging. Subtree merging is a strategy that can be used while performing merges with Git. The subtree merge strategy is useful when merging a branch (or as we'll see in this recipe another project) into a subdirectory of a Git repository instead of the root directory. When using the subtree merge strategy, the history of the subproject is joined with the history of the super project, while the subproject's history can be kept clean except for commits indented to go upstream.
We'll use the same repositories as in the last recipe, and we'll reclone the super project to get rid of the submodule setup:
$ git clone https://github.com/dvaske/super.git $ cd super
We'll add the subproject as a new remote and fetch the history:
$ git remote add lib_a git://github.com/dvaske/lib_a.git $ git fetch lib_a warning: no common commits remote: Reusing existing pack: 18, done. remote: Total 18 (delta 0), reused 0 (delta 0) Unpacking objects: 100% (18/18), done. From git://github.com/dvaske/lib_a * [new branch] develop -> lib_a/develop * [new branch] master -> lib_a/master * [new branch] stable -> lib_a/stable
We can now create a local branch, lib_a_master
, which points to the same commit as the master
branch in lib a
(lib_a/master
):
$ git checkout -b lib_a_master lib_a/master Branch lib_a_master set up to track remote branch master from lib_a by rebasing. Switched to a new branch 'lib_a_master'
We can check the content of our working tree using the following command:
$ ls README.md a.txt
If we switch back to the master
branch, we should see the content of the super repository in our directory:
$ git checkout master Switched to branch 'master' Your branch is up-to-date with 'origin/master'. $ ls README.md super.txt
Git changes branches and populates the working directory as normal even though the branches are originally from two different repositories. Now, we want to merge the history from lib_a
into a subdirectory. First, we prepare a merge commit by merging with the ours
strategy and make sure the commit isn't completed (we need to bring in all the files).
$ git merge -s ours --no-commit lib_a_master Automatic merge went well; stopped before committing as requested
In short what the ours
strategy tells Git to do the following: Merge in this branch, but keep the resulting tree the same as the tree on the tip of this branch. So, the branch is merged, but all the changes it introduced are discarded. In our previous command line, we also passed the --no-commit
option. This option stops Git from completing the merge, but leaves the repository in a merging state. We can now add the content of the lib_a
repository to the lib_a
folder in the repository root. We do this with git read-tree
to make sure the two trees are exactly the same as follows:
$ git read-tree --prefix=lib_a/ -u lib_a_master
Our current directory structure looks as follows:
$ tree . |-- README.md |-- lib_a | |-- README.md | '-- a.txt '-- super.txt
It is time to conclude the merge commit we started using the following command:
$ git commit -m 'Initial add of lib_a project' [master 5066b7b] Initial add of lib_a project
Now the subproject is added. Next, we'll see how we can update the super project with new commits from the subproject and how to copy commits made in the super project to the subproject.
We need to add and commit some changes to the super project using the following command:
$ echo "Lib_a included!" >> super.txt $ git add super.txt $ git commit -m "Update super.txt" [master 83ef9a4] Update super.txt 1 file changed, 1 insertion(+)
Some changes are made to the subproject committed in the super project:
$ echo "The b file in lib_a" >> lib_a/b.txt $ git add lib_a/b.txt $ git commit -m "[LIB_A] Enhance lib_a with b.txt" [master debe836] [LIB_A] Enhance lib_a with b.txt 1 file changed, 1 insertion(+) create mode 100644 lib_a/b.txt
The current history looks like the following screenshot:
The merge can be seen in the previous screenshot and the two root commits of the repository, the original root commit and the root from lib_a
.
Now, we will learn to integrate new commits into the super repository made in the subproject, lib_a
. Normally, we would do this by checking out the lib_a_master
branch and performing pull
on this to get the latest commit from the remote repository. However, as we are working with example repositories in this recipe, no new commits are available on the master
branch. Instead, we'll use the develop
and stable
branches from lib_a
. We'll now integrate commits from the develop
branch on lib_a
. We do this directly using the lib_a/develop
reference in the repository as follows:
$ git merge -m '[LIB_A] Update lib_a project to latest state' -s subtree lib_a/develop Merge made by the 'subtree' strategy. lib_a/a.txt | 2 ++ 1 file changed, 2 insertions(+)
Our master
branch has now been updated with the commits from lib_a/develop
as shown in the following screenshot:
Now, it is time to add the commits we made in the lib_a
directory back to the lib_a
project. First, we'll change the lib_a_master
branch and merge that with lib_a/develop
to be as up to date as possible:
$ git checkout lib_a_master $ git merge lib_a/develop Updating 0d96e7c..ab47aca Fast-forward a.txt | 2 ++ 1 file changed, 2 insertions(+)
Now we are ready to merge changes from the super project to the subproject. In order not to merge the history of the super project to the subproject, we'll use the --squash
option. This option stops Git from completing the merge, and unlike the previous case where we also stopped a merge from recording a commit, it does not leave the repository in a merging state. The state of the working directory and staging area are, however, set as though a real merge has happened.
$ git merge --squash -s subtree --no-commit master Squash commit -- not updating HEAD Automatic merge went well; stopped before committing as requested
Now, we can record a commit with all the changes done in lib_a
from the super project:
$ git commit -m 'Enhance lib_a with b.txt' [lib_a_master 01e45f7] Enhance lib_a with b.txt 1 file changed, 1 insertion(+) create mode 100644 b.txt
The history for the lib_a
repository is seen in the following screenshot:
We can integrate more changes from lib_a/stable
into the super project, but first we'll update the lib_a_master
branch so we can integrate them from here:
$ git merge lib_a/stable Merge made by the 'recursive' strategy. a.txt | 2 ++ 1 file changed, 2 insertions(+)
A new commit was added to the subproject, as shown in the following screenshot:
The last task is to integrate the new commit on lib_a_master
to the master branch in the super repository. This is done as in the previous case with the subtree
strategy option to git
merge:
$ git checkout master $ git merge -s subtree -m '[LIB_A] Update to latest state of lib_a' lib_a_master Merge made by the 'subtree' strategy. lib_a/a.txt | 2 ++ 1 file changed, 2 insertions(+)
The resulting history is shown in the following screenshot:
When using the subtree strategy, Git finds out which subtree in your repository, the branch you are trying to merge fits in. This is why we added the content of the lib_a
repository with the read-tree
command to make sure we got the exact same SHA-1 ID for the lib_a
directory in the super project as the root tree in the lib_a
project.
We can verify this by finding the SHA-1 of the lib_a
tree in the super project in the commit where we merged in the subproject:
$ git ls-tree a3662eb94abf0105a25309653b5d2ce67a4028d2 100644 blob 456a5df638694a699fff7a7ff31a496630b12d01 README.md 040000 tree 7d66ad11cb22c6d101c7ac9c309f7dce25231394 lib_a 100644 blob c552dead26fdba634c91d35708f1cfc2c4b2a100 super.txt
The ID of the root tree at lib_a/master
can be found out by using the following command:
$ git cat-file -p lib_a/master tree 7d66ad11cb22c6d101c7ac9c309f7dce25231394 parent a7d76d9114941b9d35dd58e42f33ed7e32a9c134 author Aske Olsson <[email protected]> 1396553189 +0200 committer Aske Olsson <[email protected]> 1396553189 +0200 Fixes book title in README
Another way of using subtree merging is with the git subtree
command. This is not enabled by default in Git installations, but is distributed with Git since 1.7.11. You can see how to install and use it at the following links: