Skip to main content

Diverged history

A splice has diverged when both the monorepo and the upstream changed it since the last sync. This walkthrough, in the sandbox, makes both sides change the same line, then resolves the conflict.

First, bring vendor/pkg-a/ up to date:

$ git splice pull vendor/pkg-a
ok vendor/pkg-a fetched
ok vendor/pkg-a: pulled 703b936

Both sides changeโ€‹

The monorepo and the upstream each append a line to the same file:

$ echo "a local fix" >>vendor/pkg-a/file.txt && git commit -qam "pkg-a: a local fix"
$ git clone -q "$WALKTHROUGH/upstream/pkg-a.git" ../pkg-a && echo "pkg-a: an upstream fix" >>../pkg-a/file.txt && git -C ../pkg-a commit -qam "pkg-a: an upstream fix" && git -C ../pkg-a push -q

Once fetched, status reports the splice as diverged, and log shows both sides:

$ git splice fetch
ok vendor/pkg-a fetched (main moved 703b936..4527a78)
$ git splice status
On branch main

ahead 1, behind 1 vendor/pkg-a
$ git splice log
=== vendor/pkg-a (main)
< 2d02eb8 pkg-a: a local fix (Walkthrough <walkthrough@example.com>)
> 4527a78 pkg-a: an upstream fix (Walkthrough <walkthrough@example.com>)

In terms of the four commits, R and T each have a commit the other lacks, and U(H) is where they part:

monorepo, newest first pkg-a.git, newest first

* pkg-a: a local fix R < | pkg-a: a local fix
T | > pkg-a: an upstream fix
B = splice: pull vendor/pkg-a |/
(folder = U(B)) U(B) * pkg-a: a second commit, after the clone

log --graph draws that right-hand side:

$ git splice log --graph
=== vendor/pkg-a (main)
< 2d02eb8 (R) pkg-a: a local fix (Walkthrough <walkthrough@example.com>)
| > 4527a78 (T) pkg-a: an upstream fix (Walkthrough <walkthrough@example.com>)
|/
o 703b936 pkg-a: a second commit, after the clone (Walkthrough <walkthrough@example.com>)

Pull firstโ€‹

push refuses to drop upstream's commit:

$ git splice push vendor/pkg-a
!! vendor/pkg-a: upstream has commits this branch lacks -- run 'git splice pull vendor/pkg-a' first
!! Failed: vendor/pkg-a
[1]

pull splices upstream's side in. The same line changed on both sides, so it stops with a conflict, like any merge:

$ git splice pull vendor/pkg-a
ok vendor/pkg-a fetched
Auto-merging vendor/pkg-a/file.txt
CONFLICT (content): Merge conflict in vendor/pkg-a/file.txt
!! vendor/pkg-a: conflict -- resolve it, then 'git commit' (or 'git cherry-pick --abort' to give up)
!! vendor/pkg-a: pull failed
!! Failed: vendor/pkg-a
[1]
$ cat vendor/pkg-a/file.txt
pkg-a: seed
pkg-a: a second commit, after the clone
<<<<<<< HEAD
a local fix
=======
pkg-a: an upstream fix
>>>>>>> 6842613 (splice: pull vendor/pkg-a from main at 4527a78)

Resolve and pushโ€‹

Resolve it as for any merge: keep both lines, then commit.

$ printf '%s\n' "pkg-a: seed" "pkg-a: a second commit, after the clone" "a local fix" "pkg-a: an upstream fix" >vendor/pkg-a/file.txt
$ git add vendor/pkg-a/file.txt && git commit -q --no-edit && git log --oneline -1
d27a1cd splice: pull vendor/pkg-a from main at 4527a78

The resolved commit records T as the new U(H). Its folder is U(H) plus the local fix, which was never pushed, so B stays the earlier pull, and R joins the rebuilt local fix with U(H) by a merge, as a plain git pull would have:

monorepo, newest first pkg-a.git, newest first

= splice: pull vendor/pkg-a R < splice: pull vendor/pkg-a from main at 4527a78
(folder = U(H) + local fix) |\
* pkg-a: a local fix < | pkg-a: a local fix
B = splice: pull vendor/pkg-a U(H) = T | * pkg-a: an upstream fix
|/
* pkg-a: a second commit, after the clone

T is an ancestor of R now, so the state is push, ahead by R's two < commits. log --graph draws them down to the commits they build on, T among them:

$ git splice log --graph
=== vendor/pkg-a (main)
< 610288c (R) splice: pull vendor/pkg-a from main at 4527a78 (Walkthrough <walkthrough@example.com>)
|\
< | 2d02eb8 pkg-a: a local fix (Walkthrough <walkthrough@example.com>)
| o 4527a78 (T) pkg-a: an upstream fix (Walkthrough <walkthrough@example.com>)
|/
o 703b936 pkg-a: a second commit, after the clone (Walkthrough <walkthrough@example.com>)
$ git splice status
On branch main

ahead 2 vendor/pkg-a

Upstream gets the local fix as its own commit, joined with upstream's fix by a merge:

$ git splice push vendor/pkg-a
ok vendor/pkg-a: pushed 610288c to main
$ git -C "$WALKTHROUGH/upstream/pkg-a.git" log --graph --format=%s main
* splice: pull vendor/pkg-a from main at 4527a78
|\
| * pkg-a: an upstream fix
* | pkg-a: a local fix
|/
* pkg-a: a second commit, after the clone
* pkg-a: seed
$ git splice status
On branch main

up to date vendor/pkg-a

If the two sides share no history at all, e.g. because the upstream was rebuilt from scratch, merge, pull and push don't try to merge. They print the commands to keep either side instead; see the diverged-unrelated-history scenario.