unison/unison-src/transcripts/merge.md
2021-07-28 23:06:32 -04:00

3.6 KiB

.> builtins.merge

How merging works

Suppose we have two branches, P1 and P2, and a subnamespace, foo, which we'll refer to with P1.foo , P2.foo. This doc explains how merge(P1,P2) is computed, including the merge(P1,P2).foo subnamespace.

LCA(P1,P2) is the lowest common ancestor of P1 and P2. To compute merge(P1,P2), we:

  1. Compute LCA(P1,P2) and do a three way merge of that level of the tree, using the algorithm below. What about the children of P1 and P2? Let's just consider a child namespace foo. There are a few cases:
    1. P1 and P2 both have foo as a child namespace. Then merge(P1,P2).foo == merge(P1.foo, P2.foo)
    2. P1 has foo as a child namespace, but P2 does not (or vice versa). Then we have two subcases:
      1. LCA(P1,P2) has no foo. This means that foo child namespace was added by P1. The merged result for the foo subnamespace is just P1.foo.
      2. LCA(P1,P2) does have foo. This means that P2 deleted the foo subnamespace. The merged result for the foo subnamespace is then merge(P1.foo, cons empty LCA(P1,P2).foo). This does a history-preserving delete of all the definitions that existed at the LCA point in history.
        1. Example is like if P1 added a new definition foo.bar = 23 after the LCA, then foo.bar will exist in the merged result, but all the definitions that existed in foo at the time of the LCA will be deleted in the result.

Diff-based 3-way merge algorithm

Standard 3 way merge algorithm to merge a and b:

  • Let lca = LCA(a,b)
  • merged result is: apply(diff(lca,a) <> diff(lca,b), lca)

Relies on some diff combining operation <>.

foo.w = 2
foo.x = 1
baz.x = 3
quux.x = 4
.P0> add

Now P0 has 3 sub-namespaces.

  • foo will be modified definition-wise in each branch
  • baz will be deleted in the P2 branch and left alone in P1
  • quux will be deleted in the P2 branch and added to in P1
  • P1 will add a bar sub-namespace
.P0> fork .P0 .P1
.P0> fork .P0 .P2
foo.y = 2483908
bar.y = 383
quux.y = 333
.P1> add
.P1> delete.term foo.w

We added to foo, bar and baz, and deleted foo.w, which should stay deleted in the merge.

foo.z = +28348
.P2> add
.P2> delete.namespace baz
.P2> delete.namespace quux
.P2> find

We added foo.z, deleted whole namespaces baz and quux which should stay deleted in the merge.

Now we'll try merging P1 and P2 back into P0. We should see the union of all their definitions in the merged version of P0.

This should succeed and the resulting P0 namespace should have foo, bar and quux namespaces.

.P0> merge .P1
.P0> merge .P2
.P0> find
.P0> view foo.x foo.y foo.z bar.y quux.y

These test that things we expect to be deleted are still deleted.

.> view P0.foo.w
.> view P0.baz.x
.> view P0.quux.x

Corner cases

We're going to now do two concurrent edits with an update on one side to make sure 3-way merge behaves as expected.

Here's the starting namespace, which will be the LCA.

a = 1

f = (x y -> y) a "woot!"
.c1> add
.> fork c1 c1a
.> fork c1 c1b
oog.b = 230948
oog.c = 339249

In c1a, we add new definitions, b and c.

.c1a> add

In c1b, we update the definition a, which is used by f.

a = "hello world!"
.c1b> update

Now merging c1b into c1a should result in the updated version of a and f, and the new definitions b and c:

.> merge c1b c1a
.c1a> todo .c1b.patch
.c1a> find
.c1a> view 1-4