fix(client): merge concurrent appends at end of file
This commit is contained in:
1 parent
30cd772f08
commit
331c1f5b6f
2 files changed
+38
-10
No files matched your search
@@ -254,13 +254,28 @@ func (c *Client) merge(ctx context.Context, local, base, remote protocol.File) (
|
|||||||
cmd := exec.CommandContext(ctx, "git", append([]string{"merge-file", "-p"}, names...)...)
|
cmd := exec.CommandContext(ctx, "git", append([]string{"merge-file", "-p"}, names...)...)
|
||||||
cmd.Dir = dir
|
cmd.Dir = dir
|
||||||
merged, err := cmd.Output()
|
merged, err := cmd.Output()
|
||||||
if err != nil || len(merged) > protocol.MaxBlobSize {
|
clean := err == nil
|
||||||
|
if !clean {
|
||||||
|
merged, clean = appended(versions[0], versions[1], versions[2])
|
||||||
|
}
|
||||||
|
if !clean || len(merged) > protocol.MaxBlobSize {
|
||||||
return protocol.File{}, false
|
return protocol.File{}, false
|
||||||
}
|
}
|
||||||
hash, err := c.cacheBlob(merged)
|
hash, err := c.cacheBlob(merged)
|
||||||
return protocol.File{Hash: hash, Time: max(local.Time, remote.Time)}, err == nil
|
return protocol.File{Hash: hash, Time: max(local.Time, remote.Time)}, err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appended(local, base, remote []byte) ([]byte, bool) {
|
||||||
|
if !bytes.HasPrefix(local, base) || !bytes.HasPrefix(remote, base) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
tail, glue := local[len(base):], []byte{}
|
||||||
|
if !bytes.HasSuffix(remote, []byte("\n")) && !bytes.HasPrefix(tail, []byte("\n")) {
|
||||||
|
glue = []byte("\n")
|
||||||
|
}
|
||||||
|
return slices.Concat(remote, glue, tail), true
|
||||||
|
}
|
||||||
|
|
||||||
func isText(data []byte) bool {
|
func isText(data []byte) bool {
|
||||||
return len(data) <= protocol.MaxBlobSize && utf8.Valid(data) && bytes.IndexByte(data, 0) == -1
|
return len(data) <= protocol.MaxBlobSize && utf8.Valid(data) && bytes.IndexByte(data, 0) == -1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,28 +203,39 @@ func TestLaterEditWinsAndLoserStaysInHistory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConcurrentEditsOfDifferentLinesMerge(t *testing.T) {
|
func TestConcurrentEditsMerge(t *testing.T) {
|
||||||
|
for name, versions := range map[string][4]string{
|
||||||
|
"different lines": {
|
||||||
|
"# plan\n\n## todo\n- a\n\n## notes\n- x\n",
|
||||||
|
"# plan\n\n## todo\n- a\n- from mac\n\n## notes\n- x\n",
|
||||||
|
"# plan\n\n## todo\n- a\n\n## notes\n- x\n- from dell\n",
|
||||||
|
"# plan\n\n## todo\n- a\n- from mac\n\n## notes\n- x\n- from dell\n",
|
||||||
|
},
|
||||||
|
"appends": {"# log\n- a\n", "# log\n- a\n- from mac\n", "# log\n- a\n- from dell\n", "# log\n- a\n- from dell\n- from mac\n"},
|
||||||
|
"appends without newline": {"# log\n- a", "# log\n- a\n- from mac", "# log\n- a\n- from dell", "# log\n- a\n- from dell\n- from mac"},
|
||||||
|
"appends to unfinished line": {"# log\n- a", "# log\n- a b", "# log\n- a\n- from dell", "# log\n- a\n- from dell\n b"},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
mac, dell := setup(t)
|
mac, dell := setup(t)
|
||||||
path := "docs/agents/plan.md"
|
path := "docs/agents/plan.md"
|
||||||
write(t, mac.root, path, "# plan\n\n## todo\n- a\n\n## notes\n- x\n")
|
write(t, mac.root, path, versions[0])
|
||||||
mac.sync()
|
mac.sync()
|
||||||
dell.sync()
|
dell.sync()
|
||||||
|
|
||||||
write(t, mac.root, path, "# plan\n\n## todo\n- a\n- from mac\n\n## notes\n- x\n")
|
write(t, mac.root, path, versions[1])
|
||||||
write(t, dell.root, path, "# plan\n\n## todo\n- a\n\n## notes\n- x\n- from dell\n")
|
write(t, dell.root, path, versions[2])
|
||||||
dell.sync()
|
dell.sync()
|
||||||
mac.sync()
|
mac.sync()
|
||||||
dell.sync()
|
dell.sync()
|
||||||
|
|
||||||
merged := "# plan\n\n## todo\n- a\n- from mac\n\n## notes\n- x\n- from dell\n"
|
mac.expect(path, versions[3])
|
||||||
mac.expect(path, merged)
|
dell.expect(path, versions[3])
|
||||||
dell.expect(path, merged)
|
|
||||||
state, err := mac.client.State(context.Background(), repoKey(t, mac), "")
|
state, err := mac.client.State(context.Background(), repoKey(t, mac), "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if got, _ := mac.client.Blob(context.Background(), state.Spaces[protocol.SharedSpace][path].Hash); string(got) != merged {
|
if got, _ := mac.client.Blob(context.Background(), state.Spaces[protocol.SharedSpace][path].Hash); string(got) != versions[3] {
|
||||||
t.Fatalf("server has %q, want %q", got, merged)
|
t.Fatalf("server has %q, want %q", got, versions[3])
|
||||||
}
|
}
|
||||||
for _, m := range []*machine{mac, dell, mac} {
|
for _, m := range []*machine{mac, dell, mac} {
|
||||||
if res := m.sync(); res.Pushed != 0 || res.Pulled != 0 {
|
if res := m.sync(); res.Pushed != 0 || res.Pulled != 0 {
|
||||||
@@ -235,6 +246,8 @@ func TestConcurrentEditsOfDifferentLinesMerge(t *testing.T) {
|
|||||||
if err != nil || len(revisions) != 3 {
|
if err != nil || len(revisions) != 3 {
|
||||||
t.Fatalf("want base, dell and one merged revision, got %d (%v)", len(revisions), err)
|
t.Fatalf("want base, dell and one merged revision, got %d (%v)", len(revisions), err)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConflictingOrBinaryEditsFallBackToLaterWins(t *testing.T) {
|
func TestConflictingOrBinaryEditsFallBackToLaterWins(t *testing.T) {
|
||||||
|
|||||||
Reference in new issue
Block a user