mirror of
https://github.com/unisonweb/unison.git
synced 2024-09-20 23:07:13 +03:00
1750 lines
28 KiB
Markdown
1750 lines
28 KiB
Markdown
# The `merge` command
|
||
|
||
The `merge` command merges together two branches in the same project: the current branch (unspecificed), and the target
|
||
branch. For example, to merge `topic` into `main`, switch to `main` and run `merge topic`:
|
||
|
||
```ucm
|
||
scratch/main> help merge
|
||
scratch/main> help merge.commit
|
||
```
|
||
|
||
Let's see a simple unconflicted merge in action: Alice (us) and Bob (them) add different terms. The merged result
|
||
contains both additions.
|
||
|
||
## Basic merge: two unconflicted adds
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's adds:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "alices foo"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> add
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's adds:
|
||
```unison:hide
|
||
bar : Text
|
||
bar = "bobs bar"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
Merge result:
|
||
```ucm
|
||
scratch/alice> merge /bob
|
||
scratch/alice> view foo bar
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Basic merge: two identical adds
|
||
|
||
If Alice and Bob also happen to add the same definition, that's not a conflict.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's adds:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "alice and bobs foo"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> add
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's adds:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "alice and bobs foo"
|
||
|
||
bar : Text
|
||
bar = "bobs bar"
|
||
```
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
Merge result:
|
||
```ucm
|
||
scratch/alice> merge /bob
|
||
scratch/alice> view foo bar
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Simple update propagation
|
||
|
||
Updates that occur in one branch are propagated to the other. In this example, Alice updates `foo`, while Bob adds a new dependent `bar` of the original `foo`. When Bob's branch is merged into Alice's, her update to `foo` is propagated to his `bar`.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "old foo"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's updates:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "new foo"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> update
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's adds:
|
||
```unison:hide
|
||
bar : Text
|
||
bar = foo ++ " - " ++ foo
|
||
```
|
||
```ucm
|
||
scratch/bob> display bar
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
Merge result:
|
||
```ucm
|
||
scratch/alice> merge /bob
|
||
scratch/alice> view foo bar
|
||
scratch/alice> display bar
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Update propagation with common dependent
|
||
|
||
We classify something as an update if its "syntactic hash"—not its normal Unison hash—differs from the original definition. This allows us to cleanly merge unconflicted updates that were individually propagated to a common dependent.
|
||
|
||
Let's see an example. We have `foo`, which depends on `bar` and `baz`. Alice updates `bar` (propagating to `foo`), and Bob updates `baz` (propagating to `foo`). When we merge their updates, both updates will be reflected in the final `foo`.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "foo" ++ " - " ++ bar ++ " - " ++ baz
|
||
|
||
bar : Text
|
||
bar = "old bar"
|
||
|
||
baz : Text
|
||
baz = "old baz"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's updates:
|
||
```unison:hide
|
||
bar : Text
|
||
bar = "alices bar"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> update
|
||
```
|
||
```ucm
|
||
scratch/alice> display foo
|
||
```
|
||
```ucm:hide
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's updates:
|
||
```unison:hide
|
||
baz : Text
|
||
baz = "bobs baz"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> update
|
||
```
|
||
```ucm
|
||
scratch/bob> display foo
|
||
```
|
||
Merge result:
|
||
```ucm
|
||
scratch/alice> merge /bob
|
||
scratch/alice> view foo bar baz
|
||
scratch/alice> display foo
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Propagating an update to an update
|
||
|
||
Of course, it's also possible for Alice's update to propagate to one of Bob's updates. In this example, `foo` depends on `bar` which depends on `baz`. Alice updates `baz`, propagating to `bar` and `foo`, while Bob updates `bar` (to something that still depends on `foo`), propagating to `baz`. The merged result will have Alice's update to `foo` incorporated into Bob's updated `bar`, and both updates will propagate to `baz`.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "old foo" ++ " - " ++ bar
|
||
|
||
bar : Text
|
||
bar = "old bar" ++ " - " ++ baz
|
||
|
||
baz : Text
|
||
baz = "old baz"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
```
|
||
```ucm
|
||
scratch/main> display foo
|
||
```
|
||
```ucm:hide
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's updates:
|
||
```unison:hide
|
||
baz : Text
|
||
baz = "alices baz"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> update
|
||
```
|
||
```ucm
|
||
scratch/alice> display foo
|
||
```
|
||
```ucm:hide
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's updates:
|
||
```unison:hide
|
||
bar : Text
|
||
bar = "bobs bar" ++ " - " ++ baz
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> update
|
||
```
|
||
```ucm
|
||
scratch/bob> display foo
|
||
```
|
||
|
||
Merge result:
|
||
```ucm
|
||
scratch/alice> merge /bob
|
||
scratch/alice> view foo bar baz
|
||
scratch/alice> display foo
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Update + delete isn't (currently) a conflict
|
||
|
||
We don't currently consider "update + delete" a conflict like Git does. In this situation, the delete is just ignored, allowing the update to proceed.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "old foo"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's updates:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "alices foo"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> update
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's changes:
|
||
```ucm
|
||
scratch/bob> delete.term foo
|
||
```
|
||
|
||
Merge result:
|
||
```ucm
|
||
scratch/alice> merge /bob
|
||
scratch/alice> view foo
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
In a future version, we'd like to give the user a warning at least.
|
||
|
||
## Library dependencies don't create merge conflicts
|
||
|
||
Library dependencies don't cause merge conflicts, the library dependencies are just unioned together. If two library dependencies have the same name but different namespace hashes, then the merge algorithm makes up two fresh names.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Alice's adds:
|
||
```ucm:hide
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
```unison:hide
|
||
lib.alice.foo : Nat
|
||
lib.alice.foo = 17
|
||
|
||
lib.bothSame.bar : Nat
|
||
lib.bothSame.bar = 18
|
||
|
||
lib.bothDifferent.baz : Nat
|
||
lib.bothDifferent.baz = 19
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> add
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's adds:
|
||
```unison:hide
|
||
lib.bob.foo : Nat
|
||
lib.bob.foo = 20
|
||
|
||
lib.bothSame.bar : Nat
|
||
lib.bothSame.bar = 18
|
||
|
||
lib.bothDifferent.baz : Nat
|
||
lib.bothDifferent.baz = 21
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
Merge result:
|
||
```ucm
|
||
scratch/alice> merge bob
|
||
scratch/alice> view foo bar baz
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## No-op merge (Bob = Alice)
|
||
|
||
If Bob is equals Alice, then merging Bob into Alice looks like this.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```ucm
|
||
scratch/main> branch alice
|
||
scratch/main> branch bob
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## No-op merge (Bob < Alice)
|
||
|
||
If Bob is behind Alice, then merging Bob into Alice looks like this.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```ucm
|
||
scratch/main> branch alice
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Alice's addition:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "foo"
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> add
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Fast-forward merge (Bob > Alice)
|
||
|
||
If Bob is ahead of Alice, then merging Bob into Alice looks like this.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```ucm
|
||
scratch/main> branch alice
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's addition:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "foo"
|
||
```
|
||
|
||
```ucm
|
||
scratch/bob> add
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## No-op merge: merge empty namespace into empty namespace
|
||
|
||
```ucm
|
||
scratch/main> branch topic
|
||
scratch/main> merge /topic
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Merge failure: someone deleted something
|
||
|
||
If either Alice or Bob delete something, so long as the other person didn't update it (in which case we ignore the delete, as explained above), then the delete goes through.
|
||
|
||
This can cause merge failures due to out-of-scope identifiers, and the user may have to do some digging around to find what the deleted name used to refer to. In a future version, we would emit a \[better\] warning at least.
|
||
|
||
In this example, Alice deletes `foo`, while Bob adds a new dependent of `foo`.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "foo"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
Alice's delete:
|
||
```ucm
|
||
scratch/alice> delete.term foo
|
||
```
|
||
```ucm:hide
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's new code that depends on `foo`:
|
||
```unison:hide
|
||
bar : Text
|
||
bar = foo ++ " - " ++ foo
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/bob> add
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Merge failure: type error
|
||
|
||
It may be Alice's and Bob's changes merge together cleanly in the sense that there's no textual conflicts, yet the resulting namespace doesn't typecheck.
|
||
|
||
In this example, Alice updates a `Text` to a `Nat`, while Bob adds a new dependent of the `Text`. Upon merging, propagating Alice's update to Bob's dependent causes a typechecking failure.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "foo"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's update:
|
||
```unison:hide
|
||
foo : Nat
|
||
foo = 100
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> update
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's new definition:
|
||
```unison:hide
|
||
bar : Text
|
||
bar = foo ++ " - " ++ foo
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> update
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Merge failure: simple term conflict
|
||
|
||
Alice and Bob may disagree about the definition of a term. In this case, the conflicted term and all of its dependents
|
||
are presented to the user to resolve.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "old foo"
|
||
|
||
bar : Text
|
||
bar = "old bar"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's changes:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "alices foo"
|
||
|
||
bar : Text
|
||
bar = "alices bar"
|
||
|
||
qux : Text
|
||
qux = "alices qux depends on alices foo" ++ foo
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> update
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's changes:
|
||
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "bobs foo"
|
||
|
||
baz : Text
|
||
baz = "bobs baz"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> update
|
||
```
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm
|
||
scratch/merge-bob-into-alice> view bar baz
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Merge failure: simple type conflict
|
||
|
||
Ditto for types; if the hashes don't match, it's a conflict. In this example, Alice and Bob do different things to the same constructor. However, any explicit changes to the same type will result in a conflict, including changes that could concievably be merged (e.g. Alice and Bob both add a new constructor, or edit different constructors).
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
unique type Foo = MkFoo Nat
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's changes:
|
||
```unison:hide
|
||
unique type Foo = MkFoo Nat Nat
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> update
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's changes:
|
||
```unison:hide
|
||
unique type Foo = MkFoo Nat Text
|
||
```
|
||
```ucm:hide
|
||
scratch/bob> update
|
||
```
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Merge failure: type-update + constructor-rename conflict
|
||
|
||
We model the renaming of a type's constructor as an update, so if Alice updates a type and Bob renames one of its constructors (even without changing its structure), we consider it a conflict.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
unique type Foo = Baz Nat | Qux Text
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's changes `Baz Nat` to `Baz Nat Nat`
|
||
```unison:hide
|
||
unique type Foo = Baz Nat Nat | Qux Text
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> update
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's renames `Qux` to `BobQux`:
|
||
```ucm
|
||
scratch/bob> move.term Foo.Qux Foo.BobQux
|
||
```
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Merge failure: constructor-rename conflict
|
||
|
||
Here is another example demonstrating that constructor renames are modeled as updates.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
unique type Foo = Baz Nat | Qux Text
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's rename:
|
||
```ucm
|
||
scratch/alice> move.term Foo.Baz Foo.Alice
|
||
```
|
||
```ucm:hide
|
||
scratch/main> branch bob
|
||
```
|
||
Bob's rename:
|
||
```ucm
|
||
scratch/bob> move.term Foo.Qux Foo.Bob
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Merge failure: non-constructor/constructor conflict
|
||
|
||
A constructor on one side can conflict with a regular term definition on the other.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's additions:
|
||
```unison:hide
|
||
my.cool.thing : Nat
|
||
my.cool.thing = 17
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> add
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's additions:
|
||
```unison:hide
|
||
unique ability my.cool where
|
||
thing : Nat -> Nat
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Merge failure: type/type conflict with term/constructor conflict
|
||
|
||
Here's a subtle situation where a new type is added on each side of the merge, and an existing term is replaced with a constructor of one of the types.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
Foo.Bar : Nat
|
||
Foo.Bar = 17
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice adds this type `Foo` with constructor `Foo.Alice`:
|
||
```unison:hide
|
||
unique type Foo = Alice Nat
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> add
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob adds the type `Foo` with constructor `Foo.Bar`, replacing the original `Foo.Bar` term:
|
||
```ucm
|
||
scratch/bob> delete.term Foo.Bar
|
||
```
|
||
|
||
```unison:hide
|
||
unique type Foo = Bar Nat Nat
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
|
||
These won't cleanly merge.
|
||
```ucm:error
|
||
scratch/alice> merge bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
Here's a more involved example that demonstrates the same idea.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
In the LCA, we have a type with two constructors, and some term.
|
||
|
||
```unison:hide
|
||
unique type Foo
|
||
= Bar.Baz Nat
|
||
| Bar.Qux Nat Nat
|
||
|
||
Foo.Bar.Hello : Nat
|
||
Foo.Bar.Hello = 17
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice deletes this type entirely, and repurposes its constructor names for other terms. She also updates the term.
|
||
|
||
```ucm:hide
|
||
scratch/alice> delete.type Foo
|
||
scratch/alice> delete.term Foo.Bar.Baz
|
||
scratch/alice> delete.term Foo.Bar.Qux
|
||
```
|
||
|
||
```unison:hide:all
|
||
Foo.Bar.Baz : Nat
|
||
Foo.Bar.Baz = 100
|
||
|
||
Foo.Bar.Qux : Nat
|
||
Foo.Bar.Qux = 200
|
||
|
||
Foo.Bar.Hello : Nat
|
||
Foo.Bar.Hello = 18
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> update
|
||
```
|
||
```ucm
|
||
scratch/alice> view Foo.Bar.Baz Foo.Bar.Qux Foo.Bar.Hello
|
||
```
|
||
|
||
Bob, meanwhile, first deletes the term, then sort of deletes the type and re-adds it under another name, but one constructor's fully qualified names doesn't actually change. The other constructor reuses the name of the deleted term.
|
||
|
||
```ucm:hide
|
||
scratch/main> branch bob
|
||
scratch/bob> delete.term Foo.Bar.Hello
|
||
scratch/bob> move.type Foo Foo.Bar
|
||
scratch/bob> move.term Foo.Bar.Qux Foo.Bar.Hello
|
||
```
|
||
|
||
```ucm
|
||
scratch/bob> view Foo.Bar
|
||
```
|
||
|
||
At this point, Bob and alice have both updated the name `Foo.Bar.Hello` in different ways, so that's a conflict. Therefore, Bob's entire type (`Foo.Bar` with constructors `Foo.Bar.Baz` and `Foo.Bar.Hello`) gets rendered into the scratch file.
|
||
|
||
Notably, Alice's "unconflicted" update on the name "Foo.Bar.Baz" (because she changed its hash and Bob didn't touch it) is nonetheless considered conflicted with Bob's "Foo.Bar.Baz".
|
||
|
||
```ucm:error
|
||
scratch/alice> merge bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Merge algorithm quirk: add/add unique types
|
||
|
||
Currently, two unique types created by Alice and Bob will be considered in conflict, even if they "look the same".
|
||
The result may be confusing to a user – a file containing two identical-looking copies of a unique type is rendered,
|
||
which is a parse error.
|
||
|
||
We will resolve this situation automatically in a future version.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's additions:
|
||
```unison:hide
|
||
unique type Foo = Bar
|
||
|
||
alice : Foo -> Nat
|
||
alice _ = 18
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> add
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's additions:
|
||
```unison:hide
|
||
unique type Foo = Bar
|
||
|
||
bob : Foo -> Nat
|
||
bob _ = 19
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## `merge.commit` example (success)
|
||
|
||
After merge conflicts are resolved, you can use `merge.commit` rather than `switch` + `merge` + `branch.delete` to
|
||
"commit" your changes.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "old foo"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's changes:
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "alices foo"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> update
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's changes:
|
||
|
||
```unison:hide
|
||
foo : Text
|
||
foo = "bobs foo"
|
||
```
|
||
|
||
Attempt to merge:
|
||
|
||
```ucm:hide
|
||
scratch/bob> update
|
||
```
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
Resolve conflicts and commit:
|
||
|
||
```unison
|
||
foo : Text
|
||
foo = "alice and bobs foo"
|
||
```
|
||
|
||
```ucm
|
||
scratch/merge-bob-into-alice> update
|
||
scratch/merge-bob-into-alice> merge.commit
|
||
scratch/alice> view foo
|
||
scratch/alice> branches
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## `merge.commit` example (failure)
|
||
|
||
`merge.commit` can only be run on a "merge branch".
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```ucm
|
||
scratch/main> branch topic
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/topic> merge.commit
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
|
||
## Precondition violations
|
||
|
||
There are a number of conditions under which we can't perform a merge, and the user will have to fix up the namespace(s) manually before attempting to merge again.
|
||
|
||
### Conflicted aliases
|
||
|
||
If `foo` and `bar` are aliases in the nearest common ancestor, but not in Alice's branch, then we don't know whether to update Bob's dependents to Alice's `foo` or Alice's `bar` (and vice-versa).
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Original branch:
|
||
```unison:hide
|
||
foo : Nat
|
||
foo = 100
|
||
|
||
bar : Nat
|
||
bar = 100
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's updates:
|
||
```unison:hide
|
||
foo : Nat
|
||
foo = 200
|
||
|
||
bar : Nat
|
||
bar = 300
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> update
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's addition:
|
||
```unison:hide
|
||
baz : Text
|
||
baz = "baz"
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
### Conflict involving builtin
|
||
|
||
We don't have a way of rendering a builtin in a scratch file, where users resolve merge conflicts. Thus, if there is a
|
||
conflict involving a builtin, we can't perform a merge.
|
||
|
||
One way to fix this in the future would be to introduce a syntax for defining aliases in the scratch file.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's branch:
|
||
```ucm
|
||
scratch/alice> alias.type lib.builtins.Nat MyNat
|
||
```
|
||
|
||
Bob's branch:
|
||
```ucm:hide
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
```unison:hide
|
||
unique type MyNat = MyNat Nat
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
### Constructor alias
|
||
|
||
Each naming of a decl may not have more than one name for each constructor, within the decl's namespace.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
Alice's branch:
|
||
```unison:hide
|
||
unique type Foo = Bar
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> add
|
||
```
|
||
```ucm
|
||
scratch/alice> alias.term Foo.Bar Foo.some.other.Alias
|
||
```
|
||
|
||
Bob's branch:
|
||
```ucm:hide
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
```unison:hide
|
||
bob : Nat
|
||
bob = 100
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
### Missing constructor name
|
||
|
||
Each naming of a decl must have a name for each constructor, within the decl's namespace.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Alice's branch:
|
||
```ucm:hide
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
```unison:hide
|
||
unique type Foo = Bar
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> add
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> delete.term Foo.Bar
|
||
```
|
||
|
||
Bob's branch:
|
||
```ucm:hide
|
||
scratch/main> branch /bob
|
||
```
|
||
|
||
```unison:hide
|
||
bob : Nat
|
||
bob = 100
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
### Nested decl alias
|
||
|
||
A decl cannot be aliased within the namespace of another of its aliased.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Alice's branch:
|
||
```ucm:hide
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
```unison:hide
|
||
structural type A = B Nat | C Nat Nat
|
||
structural type A.inner.X = Y Nat | Z Nat Nat
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> add
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> names A
|
||
```
|
||
|
||
Bob's branch:
|
||
```ucm:hide
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
```unison:hide
|
||
bob : Nat
|
||
bob = 100
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
### Stray constructor alias
|
||
|
||
Constructors may only exist within the corresponding decl's namespace.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Alice's branch:
|
||
```ucm:hide
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
```unison:hide:all
|
||
unique type Foo = Bar
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> add
|
||
scratch/alice> alias.term Foo.Bar AliasOutsideFooNamespace
|
||
```
|
||
|
||
Bob's branch:
|
||
```ucm:hide
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
```unison:hide:all
|
||
bob : Nat
|
||
bob = 101
|
||
```
|
||
|
||
```ucm
|
||
scratch/bob> add
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
### Term or type in `lib`
|
||
|
||
By convention, `lib` can only namespaces; each of these represents a library dependencies. Individual terms and types are not allowed at the top level of `lib`.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
Alice's branch:
|
||
```ucm:hide
|
||
scratch/main> branch alice
|
||
```
|
||
|
||
```unison:hide
|
||
lib.foo : Nat
|
||
lib.foo = 1
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/alice> add
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
Bob's branch:
|
||
```unison:hide
|
||
bob : Nat
|
||
bob = 100
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/bob> add
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## LCA precondition violations
|
||
|
||
The LCA is not subject to most precondition violations, which is good, because the user can't easily manipulate it!
|
||
|
||
Here's an example. We'll delete a constructor name from the LCA and still be able to merge Alice and Bob's stuff
|
||
together.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
LCA:
|
||
|
||
```unison
|
||
structural type Foo = Bar Nat | Baz Nat Nat
|
||
```
|
||
|
||
```ucm
|
||
scratch/main> add
|
||
scratch/main> delete.term Foo.Baz
|
||
```
|
||
|
||
Alice's branch:
|
||
|
||
```ucm
|
||
scratch/main> branch alice
|
||
scratch/alice> delete.type Foo
|
||
scratch/alice> delete.term Foo.Bar
|
||
```
|
||
|
||
```unison
|
||
alice : Nat
|
||
alice = 100
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> add
|
||
```
|
||
|
||
Bob's branch:
|
||
|
||
```ucm
|
||
scratch/main> branch bob
|
||
scratch/bob> delete.type Foo
|
||
scratch/bob> delete.term Foo.Bar
|
||
```
|
||
|
||
```unison
|
||
bob : Nat
|
||
bob = 101
|
||
```
|
||
|
||
```ucm
|
||
scratch/bob> add
|
||
```
|
||
|
||
Now we merge:
|
||
|
||
```ucm
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
## Regression tests
|
||
|
||
### Delete one alias and update the other
|
||
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```unison
|
||
foo = 17
|
||
bar = 17
|
||
```
|
||
|
||
```ucm
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
scratch/alice> delete.term bar
|
||
```
|
||
|
||
```unison
|
||
foo = 18
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> update
|
||
scratch/main> branch bob
|
||
```
|
||
|
||
```unison
|
||
bob = 101
|
||
```
|
||
|
||
```ucm
|
||
scratch/bob> add
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
### Delete a constructor
|
||
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```unison
|
||
type Foo = Bar | Baz
|
||
```
|
||
|
||
```ucm
|
||
scratch/main> add
|
||
scratch/main> branch topic
|
||
```
|
||
|
||
```unison
|
||
boop = "boop"
|
||
```
|
||
|
||
```ucm
|
||
scratch/topic> add
|
||
```
|
||
|
||
```unison
|
||
type Foo = Bar
|
||
```
|
||
|
||
```ucm
|
||
scratch/main> update
|
||
```
|
||
|
||
```ucm
|
||
scratch/main> merge topic
|
||
scratch/main> view Foo
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
### Dependent that doesn't need to be in the file
|
||
|
||
This test demonstrates a bug.
|
||
|
||
|
||
```ucm:hide
|
||
scratch/alice> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
In the LCA, we have `foo` with dependent `bar`, and `baz`.
|
||
|
||
```unison
|
||
foo : Nat
|
||
foo = 17
|
||
|
||
bar : Nat
|
||
bar = foo + foo
|
||
|
||
baz : Text
|
||
baz = "lca"
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> add
|
||
scratch/alice> branch bob
|
||
```
|
||
|
||
On Bob, we update `baz` to "bob".
|
||
|
||
```unison
|
||
baz : Text
|
||
baz = "bob"
|
||
```
|
||
|
||
```ucm
|
||
scratch/bob> update
|
||
```
|
||
|
||
On Alice, we update `baz` to "alice" (conflict), but also update `foo` (unconflicted), which propagates to `bar`.
|
||
|
||
```unison
|
||
foo : Nat
|
||
foo = 18
|
||
|
||
baz : Text
|
||
baz = "alice"
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> update
|
||
```
|
||
|
||
When we try to merge Bob into Alice, we should see both versions of `baz`, with Alice's unconflicted `foo` and `bar` in
|
||
the underlying namespace.
|
||
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
But `bar` was put into the scratch file instead.
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
### Merge loop test
|
||
|
||
This tests for regressions of https://github.com/unisonweb/unison/issues/1276 where trivial merges cause loops in the
|
||
history.
|
||
|
||
Let's make three identical namespaces with different histories:
|
||
|
||
```unison
|
||
a = 1
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> add
|
||
```
|
||
|
||
```unison
|
||
b = 2
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> add
|
||
```
|
||
|
||
```unison
|
||
b = 2
|
||
```
|
||
|
||
```ucm
|
||
scratch/bob> add
|
||
```
|
||
|
||
```unison
|
||
a = 1
|
||
```
|
||
|
||
```ucm
|
||
scratch/bob> add
|
||
```
|
||
|
||
```unison
|
||
a = 1
|
||
b = 2
|
||
```
|
||
|
||
```ucm
|
||
scratch/carol> add
|
||
scratch/bob> merge /alice
|
||
scratch/carol> merge /bob
|
||
scratch/carol> history
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
### Variables named `_`
|
||
|
||
This test demonstrates a change in syntactic hashing that fixed a bug due to auto-generated variable names for ignored
|
||
results.
|
||
|
||
```ucm:hide
|
||
scratch/alice> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```unison
|
||
ignore : a -> ()
|
||
ignore _ = ()
|
||
|
||
foo : Nat
|
||
foo = 18
|
||
|
||
bar : Nat
|
||
bar =
|
||
ignore "hi"
|
||
foo + foo
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> add
|
||
scratch/alice> branch bob
|
||
```
|
||
|
||
```unison
|
||
bar : Nat
|
||
bar =
|
||
ignore "hi"
|
||
foo + foo + foo
|
||
```
|
||
|
||
```ucm
|
||
scratch/bob> update
|
||
```
|
||
|
||
Previously, this update to `foo` would also cause a "real update" on `bar`, its dependent. Now it doesn't, so the merge
|
||
will succeed.
|
||
|
||
```unison
|
||
foo : Nat
|
||
foo = 19
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> update
|
||
```
|
||
|
||
```ucm
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|
||
|
||
### Unique type GUID reuse
|
||
|
||
Previously, a merge branch would not include any dependents in the namespace, but that resulted in dependent unique
|
||
types' GUIDs being regenerated.
|
||
|
||
```ucm:hide
|
||
scratch/main> builtins.mergeio lib.builtins
|
||
```
|
||
|
||
```unison
|
||
type Foo = Lca
|
||
type Bar = MkBar Foo
|
||
```
|
||
|
||
```ucm
|
||
scratch/main> add
|
||
scratch/main> branch alice
|
||
scratch/alice> move.term Foo.Lca Foo.Alice
|
||
scratch/main> branch bob
|
||
scratch/bob> move.term Foo.Lca Foo.Bob
|
||
```
|
||
|
||
```ucm:error
|
||
scratch/alice> merge /bob
|
||
```
|
||
|
||
```ucm
|
||
scratch/merge-bob-into-alice>
|
||
```
|
||
|
||
```unison
|
||
type Foo = Merged
|
||
type Bar = MkBar Foo
|
||
```
|
||
|
||
```ucm
|
||
scratch/merge-bob-into-alice> update
|
||
scratch/merge-bob-into-alice> names Bar
|
||
scratch/alice> names Bar
|
||
```
|
||
|
||
```ucm:hide
|
||
scratch/main> project.delete scratch
|
||
```
|