Skip to content

Instantly share code, notes, and snippets.

@x-yuri
Last active June 27, 2023 18:46
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save x-yuri/8ad01701db51ec2891ca431b78c58c72 to your computer and use it in GitHub Desktop.
Save x-yuri/8ad01701db51ec2891ca431b78c58c72 to your computer and use it in GitHub Desktop.

Subtree merges

This is an alternative to submodules:

This gives us a way to have a workflow somewhat similar to the submodule workflow without using submodules (which we will cover in Submodules). We can keep branches with other related projects in our repository and subtree merge them into our project occasionally. It is nice in some ways, for example all the code is committed to a single place. However, it has other drawbacks in that it’s a bit more complex and easier to make mistakes in reintegrating changes or accidentally pushing a branch into an unrelated repository.

Say you want to merge repository a into b. This can be accomplished with one command:

git subtree add --prefix=a ../a master

This produces the following history (commit names reflect names of the files that were added):

$ git log --oneline --graph --decorate --all
*   97d548a Add 'a/' from commit 'b1630a6'
|\  
| * b1630a6 ad1/a2
| * 0e8e0d3 a1
* 3b75042 bd1/b2
* 4b2b43b b1

And the following tree:

r
├── b1
├── bd1
│   └── b2
└── a
    ├── a1
    └── ad1
        └── a2

But in the merged commits the files are still at the root, so:

$ git log -p --name-status --oneline a/a1
97d548a Add 'a/' from commit 'b1630a6'

$ git log -p --name-status --oneline --follow -- a1
0e8e0d3 a1
A	a1

If you later change say a/a1, then git log a/a1 will show you the change + the merge commit. And git log --follow -- a1 will show you only the change from the merged history. None of them will show you all the changes.

Later, to pull in changes from repository a you need:

git pull -s subtree a master

As a result you get:

$ git log --oneline --graph --decorate --all
*   f4469a5 (HEAD -> master) Merge branch 'master' of ../a
|\  
| * 367e02f (a/master) a3
* | 97d548a Add 'a/' from commit 'b1630a6'
|\| 
| * b1630a6 ad1/a2
| * 0e8e0d3 a1
* 3b75042 bd1/b2
* 4b2b43b b1

A more wordy way of doing git subtree is as follows.

First you add a remote:

git remote add -f a ../a

-f makes it autofetch the remote.

Then attach the a's history, but don't make a commit:

git merge -s ours --no-commit --allow-unrelated-histories a/master

-s ours makes it not apply the changes, as that would apply them to the root of the repository (not to a subdirectory). --allow-unrelated-histories is needed since git >= 2.9.

Then apply changes to a subdirectory:

git read-tree --prefix=a -u a/master

-u makes it apply them not only to index, but also to the working directory.

And commit the changes:

git commit -m "Subtree merged in a"

About Git subtree merges
How to use the subtree merge strategy
Subtree Merging
Merging git repositories, putting one of them into a subdirectory

#!/bin/sh
set -eux
mkcommit() {
local p=$1
touch "$p"
git add .
git commit -m "$p"
}
rm -rf sr r
(mkdir sr
cd sr
git init
mkcommit sr1
mkdir srd1
mkcommit srd1/sr2
git log --oneline --graph --decorate --all
git ls-files -s
)
(mkdir r
cd r
git init
mkcommit r1
mkdir rd1
mkcommit rd1/r2
git log --oneline --graph --decorate --all
git ls-files -s
git remote add -f sr ../sr
git subtree add --prefix=sr ../sr master
git log --oneline --graph --decorate --all
git ls-files -s
git log -p --name-status --oneline
git log -p --name-status --oneline sr/sr1
git log -p --name-status --oneline --follow -- sr1
)
(cd sr
mkcommit sr3
)
(cd r
git config pull.rebase false
git pull -s subtree sr master
git log --oneline --graph --decorate --all
git ls-files -s
git log -p --name-status --oneline
git log -p --name-status --oneline sr/sr1
git log -p --name-status --oneline --follow -- sr1
)
$ ./1.sh
Initialized empty Git repository in /home/yuri/_/git-filter-branch/sr/.git/
[master (root-commit) 01348ce] sr1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 sr1
[master 8f65392] srd1/sr2
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 srd1/sr2
* 8f65392 (HEAD -> master) srd1/sr2
* 01348ce sr1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 srd1/sr2
Initialized empty Git repository in /home/yuri/_/git-filter-branch/r/.git/
[master (root-commit) 37747c3] r1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 r1
[master d49088b] rd1/r2
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 rd1/r2
* d49088b (HEAD -> master) rd1/r2
* 37747c3 r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 rd1/r2
Updating sr
git fetch ../sr master
* e603231 (HEAD -> master) Add 'sr/' from commit '8f653929c86f144695fa984e68f4a57b995ba442'
|\
| * 8f65392 (sr/master) srd1/sr2
| * 01348ce sr1
* d49088b rd1/r2
* 37747c3 r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 rd1/r2
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr/sr1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr/srd1/sr2
e603231 Add 'sr/' from commit '8f653929c86f144695fa984e68f4a57b995ba442'
d49088b rd1/r2
A rd1/r2
8f65392 srd1/sr2
A srd1/sr2
37747c3 r1
A r1
01348ce sr1
A sr1
e603231 Add 'sr/' from commit '8f653929c86f144695fa984e68f4a57b995ba442'
01348ce sr1
A sr1
[master a623f9c] sr3
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 sr3
Merge made by the 'subtree' strategy.
sr/sr3 | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 sr/sr3
* 57f24f4 (HEAD -> master) Merge branch 'master' of ../sr
|\
| * a623f9c (sr/master) sr3
* | e603231 Add 'sr/' from commit '8f653929c86f144695fa984e68f4a57b995ba442'
|\|
| * 8f65392 srd1/sr2
| * 01348ce sr1
* d49088b rd1/r2
* 37747c3 r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 rd1/r2
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr/sr1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr/sr3
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr/srd1/sr2
57f24f4 Merge branch 'master' of ../sr
e603231 Add 'sr/' from commit '8f653929c86f144695fa984e68f4a57b995ba442'
a623f9c sr3
A sr3
d49088b rd1/r2
A rd1/r2
8f65392 srd1/sr2
A srd1/sr2
37747c3 r1
A r1
01348ce sr1
A sr1
e603231 Add 'sr/' from commit '8f653929c86f144695fa984e68f4a57b995ba442'
01348ce sr1
A sr1
#!/bin/sh
set -eux
mkcommit() {
local p=$1
touch "$p"
git add .
git commit -m "$p"
}
rm -rf sr r
(mkdir sr
cd sr
git init
mkcommit sr1
mkdir srd1
mkcommit srd1/sr2
git log --oneline --graph --decorate --all
git ls-files -s
)
(mkdir r
cd r
git init
mkcommit r1
mkdir rd1
mkcommit rd1/r2
git log --oneline --graph --decorate --all
git ls-files -s
git remote add -f sr ../sr
git merge -s ours --no-commit --allow-unrelated-histories sr/master
git read-tree --prefix=sr -u sr/master
git commit -m "Subtree merged in sr"
git log --oneline --graph --decorate --all
git ls-files -s
git log -p --name-status --oneline
git log -p --name-status --oneline sr/sr1
git log -p --name-status --oneline --follow -- sr1
)
(cd sr
mkcommit sr3
)
(cd r
git config pull.rebase false
git pull -s subtree sr master
git log --oneline --graph --decorate --all
git ls-files -s
git log -p --name-status --oneline
git log -p --name-status --oneline sr/sr1
git log -p --name-status --oneline --follow -- sr1
)
$ ./2.sh
Initialized empty Git repository in /home/yuri/_/git-filter-branch/sr/.git/
[master (root-commit) 82c466c] sr1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 sr1
[master 5a1627f] srd1/sr2
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 srd1/sr2
* 5a1627f (HEAD -> master) srd1/sr2
* 82c466c sr1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 srd1/sr2
Initialized empty Git repository in /home/yuri/_/git-filter-branch/r/.git/
[master (root-commit) 4f6fedf] r1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 r1
[master c9f1e2b] rd1/r2
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 rd1/r2
* c9f1e2b (HEAD -> master) rd1/r2
* 4f6fedf r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 rd1/r2
Updating sr
[master 76d9528] Subtree merged in sr
* 76d9528 (HEAD -> master) Subtree merged in sr
|\
| * 5a1627f (sr/master) srd1/sr2
| * 82c466c sr1
* c9f1e2b rd1/r2
* 4f6fedf r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 rd1/r2
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr/sr1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr/srd1/sr2
76d9528 Subtree merged in sr
c9f1e2b rd1/r2
A rd1/r2
5a1627f srd1/sr2
A srd1/sr2
4f6fedf r1
A r1
82c466c sr1
A sr1
76d9528 Subtree merged in sr
82c466c sr1
A sr1
[master bf3a8db] sr3
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 sr3
Merge made by the 'subtree' strategy.
sr/sr3 | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 sr/sr3
* 8850800 (HEAD -> master) Merge branch 'master' of ../sr
|\
| * bf3a8db (sr/master) sr3
* | 76d9528 Subtree merged in sr
|\|
| * 5a1627f srd1/sr2
| * 82c466c sr1
* c9f1e2b rd1/r2
* 4f6fedf r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 r1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 rd1/r2
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr/sr1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr/sr3
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 sr/srd1/sr2
8850800 Merge branch 'master' of ../sr
76d9528 Subtree merged in sr
bf3a8db sr3
A sr3
c9f1e2b rd1/r2
A rd1/r2
5a1627f srd1/sr2
A srd1/sr2
4f6fedf r1
A r1
82c466c sr1
A sr1
76d9528 Subtree merged in sr
82c466c sr1
A sr1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment