git-subrepo compared
git-subrepo looks the most
like git-splice. The library's files are ordinary monorepo files, a
committed file in the folder, .gitrepo, names the library, every pull is
one commit, and push sends the folder's changes back.
The difference is what else .gitrepo records: one upstream branch for
every monorepo branch, and the monorepo commit the last push came from.
On a feature branch that ends in a squash merge, both get in the way.
The setup: https://git.example.com/lib.git, a library with 30 commits,
and a monorepo with an app. How these pages run.
$ git subrepo --version
0.4.9
Like git-splice: one commit per pull
clone adds the library as one commit:
$ git subrepo clone https://git.example.com/lib.git vendor/lib
Subrepo 'https://git.example.com/lib.git' (main) cloned into 'vendor/lib'.
$ cat vendor/lib/.gitrepo
; DO NOT EDIT (unless you know what you are doing)
;
; This subdirectory is a git "subrepo", and this file is maintained by the
; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme
;
[subrepo]
remote = https://git.example.com/lib.git
branch = main
commit = bafd496b1f4512a95b69464604f3da428aa9e33b
parent = 7de219808bfd601897cdad67aa5c90b5a2e9bce8
method = merge
cmdver = 0.4.9
You fix the library and use the fix in the app, in one commit, f.
parent in .gitrepo still names a, the commit before the clone c:
$ echo "fix" >>vendor/lib/src/parse.txt &&
> echo "call parse()" >>app/main.txt &&
> git commit -q -a -m "fix parse() and call it"
Then you push it:
$ git subrepo push vendor/lib
Subrepo 'vendor/lib' pushed to 'https://git.example.com/lib.git' (main).
The library gets the commit with only the folder's change:
$ git -C "$COMPARISON/upstream/lib.git" show --stat --format=%s main
fix parse() and call it
src/parse.txt | 1 +
1 file changed, 1 insertion(+)
Problem 1: every push is a commit in the monorepo
The monorepo's history is linear, but push added a commit of its own:
$ git log --graph --format=%s -4
* git subrepo push vendor/lib
* fix parse() and call it
* git subrepo clone https://git.example.com/lib.git vendor/lib
* app commit 40
It's p, which records in .gitrepo the library commit it pushed, and
moves parent to the monorepo commit it pushed from, f:
A squash merge or rebase can leave that commit behind, as problem 3 shows (git-subrepo#464, #600).
Problem 2: a feature branch pushes to main
.gitrepo names one upstream branch, for every monorepo branch. A
second fix, on a feature branch:
$ git switch -q -c feature &&
> echo "fix 2" >>vendor/lib/src/parse.txt &&
> git commit -q -a -m "fix 2"
push sends it to main, the branch in .gitrepo, with no pull request
to review it:
$ git subrepo push vendor/lib
Subrepo 'vendor/lib' pushed to 'https://git.example.com/lib.git' (main).
The library's main has it now:
$ git -C "$COMPARISON/upstream/lib.git" log --format=%s -3 main
fix 2
fix parse() and call it
lib commit 30
To push to another branch, --branch names it and --update writes it
into .gitrepo. After a push, that takes a fetch first, or it fails
with "Local repository does not contain" the commit it pushed:
$ echo "fix 3" >>vendor/lib/src/parse.txt &&
> git commit -q -a -m "fix 3" &&
> git subrepo fetch vendor/lib
Fetched 'vendor/lib' from 'https://git.example.com/lib.git' (main).
$ git subrepo push --branch feature --update vendor/lib
Subrepo 'vendor/lib' pushed to 'https://git.example.com/lib.git' (feature).
Now the feature branch syncs with feature (0.4.9 writes it even without
--update, git-subrepo#313):
$ git config --file vendor/lib/.gitrepo subrepo.branch
feature
Problem 3: after a squash merge, the last sync is lost
The feature branch is squash-merged into main, as a pull request would
be:
$ git switch -q main &&
> git merge -q --squash feature &&
> git commit -q -m "feature (squashed)"
Squash commit -- not updating HEAD
.gitrepo comes along with the merge: main now syncs with feature,
and parent names a commit main doesn't have:
$ git config --file vendor/lib/.gitrepo subrepo.branch
feature
$ parent="$(git config --file vendor/lib/.gitrepo subrepo.parent)" &&
> git merge-base --is-ancestor "$parent" HEAD || echo "not in main"
not in main
g and h are fix 2 and fix 3, q and r their push commits, and
s the squash merge. s has h's changes, but not h:
So the next push from main stops, and asks to set parent to p by
hand:
$ echo "fix 4" >>vendor/lib/src/parse.txt &&
> git commit -q -a -m "fix 4" &&
> git subrepo push vendor/lib
git-subrepo: The last sync point (where upstream and the subrepo were equal) is not an ancestor.
This is usually caused by a rebase affecting that commit.
To recover set the subrepo parent in 'vendor/lib/.gitrepo'
to '5bd8dcf5c365cbc7ca13e4e13e51c68260584d43'
and validate the subrepo by comparing with 'git subrepo branch vendor/lib'
[1]
A rebase does the same, as the message says: the pushed commit gets a new
hash, and parent names the old one.
How git-splice avoids them
The library goes back to its 30 commits, without the pushes above:
$ git -C "$COMPARISON/upstream/lib.git" branch -q -f main bafd496 &&
> git -C "$COMPARISON/upstream/lib.git" branch -q -D feature
The same steps, in a copy of the monorepo from before the library.
.splice names the library and the synced commit, no branch and no
monorepo commit:
$ cd ../splice-monorepo && git splice clone https://git.example.com/lib.git vendor/lib
=== vendor/lib: fetching https://git.example.com/lib.git
ok vendor/lib: cloned bafd496 from main
$ cat vendor/lib/.splice
[splice]
id = f34fc447facba9d0
commit = bafd496b1f4512a95b69464604f3da428aa9e33b
[upstream "origin"]
url = https://git.example.com/lib.git
$ echo "fix" >>vendor/lib/src/parse.txt &&
> echo "call parse()" >>app/main.txt &&
> git commit -q -a -m "fix parse() and call it"
$ git splice push vendor/lib
ok vendor/lib: pushed 5069701 to main
push records nothing in the monorepo. The library gets f', a copy of
f with only the folder's change, and the last sync is still c: B, the
boundary:
On a feature branch, push goes to the upstream branch of the same name,
ready for a pull request:
$ git switch -q -c feature &&
> echo "fix 2" >>vendor/lib/src/parse.txt &&
> git commit -q -a -m "fix 2" &&
> git splice push vendor/lib
?? vendor/lib: upstream has no 'feature' branch yet -- this push creates it
ok vendor/lib: pushed 6b3df25 to feature
$ echo "fix 3" >>vendor/lib/src/parse.txt &&
> git commit -q -a -m "fix 3" &&
> git splice push vendor/lib
ok vendor/lib: pushed 79da5cd to feature
The same squash merge:
$ git switch -q main &&
> git merge -q --squash feature &&
> git commit -q -m "feature (squashed)"
Squash commit -- not updating HEAD
The same history, without push commits, and g' and h' on the
library's feature:
main syncs with main again. B is the newest commit that changed
.splice, here c: it comes from the history, not from a stored
commit, so a squash merge can't strand it. The next push sends the
squashed commit and the new one:
$ echo "fix 4" >>vendor/lib/src/parse.txt &&
> git commit -q -a -m "fix 4" &&
> git splice push vendor/lib
ok vendor/lib: pushed 940e7a2 to main
$ git -C "$COMPARISON/upstream/lib.git" log --format=%s -4 main
fix 4
feature (squashed)
fix parse() and call it
lib commit 30
After a rebase of pushed commits, status shows the splice as diverged,
and git splice push --force replaces the upstream branch, as
git push --force would.
Problem 4: fetched refs are keyed by folder
git-subrepo keeps what it fetched under the folder's path,
refs/subrepo/<path>/; git-splice under the upstream,
refs/splices/<key>/heads/<branch>. That matters when another branch
uses the folder for another library:
$ git init -q -b main ../other &&
> echo "other" >../other/README &&
> git -C ../other add README &&
> git -C ../other commit -q -m "other library" &&
> git init -q --bare "$COMPARISON/upstream/other.git" &&
> git -C ../other push -q https://git.example.com/other.git main
In git-subrepo's monorepo, vendor/lib/ was last fetched from
lib.git:
$ cd ../monorepo && git log -1 --format=%s refs/subrepo/vendor/lib/fetch
fix 3
A branch from before the library clones the other one into the same folder:
$ git switch -q -c other ':/^app commit 40' &&
> git subrepo clone https://git.example.com/other.git vendor/lib &&
> git switch -q main
Subrepo 'https://git.example.com/other.git' (main) cloned into 'vendor/lib'.
Back on main, vendor/lib/ still names lib.git, but its refs hold
the other library, and status shows that one's commit:
$ git subrepo status vendor/lib | grep -E 'Remote URL|Upstream Ref'
Remote URL: https://git.example.com/lib.git
Upstream Ref: 3ea1a83
$ git log -1 --format=%s refs/subrepo/vendor/lib/fetch
other library
pull and push fetch first, so they still reach the right library;
status doesn't. With git-splice:
$ cd ../splice-monorepo &&
> git switch -q -c other ':/^app commit 40' &&
> git splice clone https://git.example.com/other.git vendor/lib &&
> git switch -q main
=== vendor/lib: fetching https://git.example.com/other.git
=== vendor/lib: upstream has no 'other' branch -- using 'main'; your first push creates 'other'
ok vendor/lib: cloned 3ea1a83 from main
Each upstream has its own refs, so main's stay as they were:
$ git for-each-ref --format='%(refname) %(subject)' refs/splices
refs/splices/lib/heads/feature fix 3
refs/splices/lib/heads/main fix 4
refs/splices/other/heads/main other library
$ git splice status
On branch main
up to date vendor/lib
After git mv
In git-subrepo's monorepo, a move changes the key too: the next fetch
starts new refs under the new path, and the old ones stay behind:
$ cd ../monorepo &&
> mkdir libs &&
> git mv vendor/lib libs/lib &&
> git commit -q -m "move the library" &&
> git subrepo fetch libs/lib
Fetched 'libs/lib' from 'https://git.example.com/lib.git' (feature).
$ git for-each-ref --format='%(refname)' refs/subrepo
refs/subrepo/libs/lib/fetch
refs/subrepo/vendor/lib/branch
refs/subrepo/vendor/lib/commit
refs/subrepo/vendor/lib/fetch
refs/subrepo/vendor/lib/push
In git-splice's, the refs stay where they are:
$ cd ../splice-monorepo &&
> mkdir libs &&
> git mv vendor/lib libs/lib &&
> git commit -q -m "move the library" &&
> git splice fetch libs/lib
ok libs/lib fetched
$ git for-each-ref --format='%(refname)' refs/splices
refs/splices/lib/heads/feature
refs/splices/lib/heads/main
refs/splices/other/heads/main
In short
git-subrepo | git-splice | |
|---|---|---|
| Monorepo history | linear | linear |
| Commits per sync | one per pull, one per push | one per pull |
| Upstream branch | the one in .gitrepo, for every monorepo branch | the monorepo branch's name, default-branch in .splice on the default branch, or one kept with --track |
| Sync point, the upstream commit last synced | commit in .gitrepo, written on clone, pull and push | commit in .splice, written on clone and pull |
| Where the last sync is in the monorepo | parent in .gitrepo, a stored commit | derived: the newest commit that changed .splice |
| After a squash merge | push stops until .gitrepo is fixed by hand | push works |
| After rebasing pushed commits | push stops until .gitrepo is fixed by hand | diverged, push --force as with Git |
| Fetched refs keyed by | folder path | upstream URL |