Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
nasdf committed Dec 24, 2020
1 parent 5ee2e25 commit 527b852
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 0 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Diff3

A diff3 text merge implementation in Go based on the awesome paper below.

["A Formal Investigation of Diff3" by Sanjeev Khanna, Keshav Kunal, and Benjamin C. Pierce](https://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf)

## Usage

```go
import "github.com/nasdf/diff3"
textO := "original"
textA := "changesA"
textB := "changesB"
merge := diff3.Merge(textO, textA, textB)
```

### Customize seperators

```go
diff3.Sep1 = "$$$$$$$"
diff3.Sep2 = "@@@@@@@"
diff3.Sep3 = "*******"
```

### Customize DiffMatchPatch settings

```go
diff3.DiffMatchPatch.DiffTimeout = time.Second
diff3.DiffMatchPatch.DiffEditCost = 4
diff3.DiffMatchPatch.MatchThreshold = 0.5
diff3.DiffMatchPatch.MatchDistance = 1000
diff3.DiffMatchPatch.PatchDeleteThreshold = 0.5
diff3.DiffMatchPatch.PatchMargin = 4
diff3.DiffMatchPatch.MatchMaxBits = 32
```

## License

MIT
133 changes: 133 additions & 0 deletions diff3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package diff3

import (
"fmt"
"strings"

"github.com/sergi/go-diff/diffmatchpatch"
)

const (
// Sep1 signifies the start of a conflict.
Sep1 = "<<<<<<<"
// Sep2 signifies the middle of a conflict.
Sep2 = "======="
// Sep3 signifies the end of a conflict.
Sep3 = ">>>>>>>"
)

// DiffMatchPatch contains the diff algorithm settings.
var DiffMatchPatch = diffmatchpatch.New()

// Merge implements the diff3 algorithm to merge two texts into a common base.
func Merge(textO, textA, textB string) string {
runesO, runesA, linesA := DiffMatchPatch.DiffLinesToRunes(textO, textA)
_, runesB, linesB := DiffMatchPatch.DiffLinesToRunes(textO, textB)

diffsA := DiffMatchPatch.DiffMainRunes(runesO, runesA, false)
diffsB := DiffMatchPatch.DiffMainRunes(runesO, runesB, false)

matchesA := matches(diffsA, runesA)
matchesB := matches(diffsB, runesB)

var result strings.Builder
indexO, indexA, indexB := 0, 0, 0
for {
i := nextMismatch(indexO, indexA, indexB, runesA, runesB, matchesA, matchesB)

o, a, b := 0, 0, 0
if i == 1 {
o, a, b = nextMatch(indexO, runesO, matchesA, matchesB)
} else if i > 1 {
o, a, b = indexO+i, indexA+i, indexB+i
}

if o == 0 || a == 0 || b == 0 {
break
}

chunk(indexO, indexA, indexB, o-1, a-1, b-1, runesO, runesA, runesB, linesA, linesB, &result)
indexO, indexA, indexB = o-1, a-1, b-1
}

chunk(indexO, indexA, indexB, len(runesO), len(runesA), len(runesB), runesO, runesA, runesB, linesA, linesB, &result)
return result.String()
}

// matches returns a map of the non-crossing matches.
func matches(diffs []diffmatchpatch.Diff, runes []rune) map[int]int {
matches := make(map[int]int)
for _, d := range diffs {
if d.Type != diffmatchpatch.DiffEqual {
continue
}

for _, r := range d.Text {
matches[int(r)] = indexOf(runes, r) + 1
}
}
return matches
}

// nextMismatch searches for the next index where a or b is not equal to o.
func nextMismatch(indexO, indexA, indexB int, runesA, runesB []rune, matchesA, matchesB map[int]int) int {
for i := 1; i <= len(runesA) && i <= len(runesB); i++ {
a, okA := matchesA[indexO+i]
b, okB := matchesB[indexO+i]

if !okA || a != indexA+i || !okB || b != indexB+i {
return i
}
}
return 0
}

// nextMatch searches for the next index where a and b are equal to o.
func nextMatch(indexO int, runesO []rune, matchesA, matchesB map[int]int) (int, int, int) {
for o := indexO + 1; o <= len(runesO); o++ {
a, okA := matchesA[o]
b, okB := matchesB[o]

if okA && okB {
return o, a, b
}
}
return 0, 0, 0
}

// chunk merges the lines from o, a, and b into a single text.
func chunk(indexO, indexA, indexB, o, a, b int, runesO, runesA, runesB []rune, linesA, linesB []string, result *strings.Builder) {
chunkO := buildChunk(linesA, runesO[indexO:o])
chunkA := buildChunk(linesA, runesA[indexA:a])
chunkB := buildChunk(linesB, runesB[indexB:b])

switch {
case chunkA == chunkB:
fmt.Fprint(result, chunkO)
case chunkO == chunkA:
fmt.Fprint(result, chunkB)
case chunkO == chunkB:
fmt.Fprint(result, chunkA)
default:
fmt.Fprintf(result, "%s\n%s%s\n%s%s\n", Sep1, chunkA, Sep2, chunkB, Sep3)
}
}

// indexOf returns the index of the first occurance of the given value.
func indexOf(runes []rune, value rune) int {
for i, r := range runes {
if r == value {
return i
}
}
return -1
}

// buildChunk assembles the lines of the chunk into a string.
func buildChunk(lines []string, runes []rune) string {
var chunk strings.Builder
for _, r := range runes {
fmt.Fprint(&chunk, lines[int(r)])
}
return chunk.String()
}
49 changes: 49 additions & 0 deletions diff3_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package diff3

import (
"testing"
)

const textO = `celery
garlic
onions
salmon
tomatoes
wine
`

const textA = `celery
salmon
tomatoes
garlic
onions
wine
`

const textB = `celery
garlic
salmon
tomatoes
onions
wine
`

const expect = `celery
salmon
tomatoes
garlic
<<<<<<<
onions
=======
salmon
tomatoes
onions
>>>>>>>
wine
`

func TestMerge(t *testing.T) {
if Merge(textO, textA, textB) != expect {
t.Errorf("unexpected merge result")
}
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/nasdf/diff3

go 1.14

require github.com/sergi/go-diff v1.1.0
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

0 comments on commit 527b852

Please sign in to comment.