├── LICENSE ├── bench_test.go ├── cmd └── pkg-diff-example │ └── main.go ├── ctxt ├── size.go └── todo.go ├── diff.go ├── edit ├── edit.go └── op_string.go ├── example_test.go ├── fuzz.go ├── go.mod ├── go.sum ├── intern └── intern.go ├── myers ├── myers.go └── myers_test.go ├── readme.md ├── testdata ├── rewriteAMD64.go.a ├── rewriteAMD64.go.b └── rewriteAMD64.go.out ├── todo.go └── write ├── option.go ├── todo.go ├── unified.go └── unified_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Joshua Bleecher Snyder 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package diff_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/pkg/diff" 11 | ) 12 | 13 | const regenerate = false // set to true to overwrite .out files 14 | 15 | func BenchmarkGolden(b *testing.B) { 16 | aa, err := filepath.Glob("testdata/*.a") 17 | if err != nil { 18 | b.Fatal(err) 19 | } 20 | for _, aPath := range aa { 21 | base := strings.TrimSuffix(aPath, ".a") 22 | bPath := base + ".b" 23 | outPath := base + ".out" 24 | out, err := ioutil.ReadFile(outPath) 25 | if err != nil && !regenerate { 26 | b.Fatal(err) 27 | } 28 | buf := new(bytes.Buffer) 29 | buf.Grow(len(out)) 30 | b.Run(base, func(b *testing.B) { 31 | for i := 0; i < b.N; i++ { 32 | buf.Reset() 33 | err := diff.Text(aPath, bPath, nil, nil, buf) 34 | if err != nil { 35 | b.Fatal(err) 36 | } 37 | if regenerate { 38 | err := ioutil.WriteFile(outPath, buf.Bytes(), 0644) 39 | if err != nil { 40 | b.Fatal(err) 41 | } 42 | return 43 | } 44 | if !bytes.Equal(buf.Bytes(), out) { 45 | b.Fatal("wrong output") 46 | } 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cmd/pkg-diff-example/main.go: -------------------------------------------------------------------------------- 1 | // Command pkg-diff-example implements a subset of the diff command using 2 | // github.com/pkg/diff. 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | 12 | "github.com/pkg/diff" 13 | "github.com/pkg/diff/write" 14 | ) 15 | 16 | var color = flag.Bool("color", false, "colorize the output") 17 | 18 | // check logs a fatal error and exits if err is not nil. 19 | func check(err error) { 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | } 24 | 25 | func usage() { 26 | fmt.Fprintf(os.Stderr, "pkg-diff-example [flags] file1 file2\n") 27 | flag.PrintDefaults() 28 | os.Exit(2) 29 | } 30 | 31 | func main() { 32 | log.SetPrefix("pkg-diff-example: ") 33 | log.SetFlags(0) 34 | 35 | flag.Usage = usage 36 | flag.Parse() 37 | 38 | var aName, bName string 39 | var a, b interface{} 40 | switch flag.NArg() { 41 | case 2: 42 | // A human has invoked us. 43 | aName, bName = flag.Arg(0), flag.Arg(1) 44 | case 7, 9: 45 | // We are a git external diff tool. 46 | // We have been passed the following arguments: 47 | // path old-file old-hex old-mode new-file new-hex new-mode [new-path similarity-metrics] 48 | aName = "a/" + flag.Arg(0) 49 | if flag.NArg() == 7 { 50 | bName = "b/" + flag.Arg(0) 51 | } else { 52 | bName = "b/" + flag.Arg(7) 53 | } 54 | var err error 55 | a, err = ioutil.ReadFile(flag.Arg(1)) 56 | check(err) 57 | b, err = ioutil.ReadFile(flag.Arg(4)) 58 | check(err) 59 | default: 60 | flag.Usage() 61 | } 62 | 63 | var opts []write.Option 64 | if *color { 65 | opts = append(opts, write.TerminalColor()) 66 | } 67 | 68 | err := diff.Text(aName, bName, a, b, os.Stdout, opts...) 69 | check(err) 70 | } 71 | -------------------------------------------------------------------------------- /ctxt/size.go: -------------------------------------------------------------------------------- 1 | // Package ctxt provides routines to reduce the amount of context in an edit script. 2 | package ctxt 3 | 4 | import ( 5 | "github.com/pkg/diff/edit" 6 | ) 7 | 8 | // Size returns an edit script preserving only n common elements of context for changes. 9 | // The returned edit script may alias the input. 10 | // If n is negative, Size panics. 11 | func Size(e edit.Script, n int) edit.Script { 12 | if n < 0 { 13 | panic("ctxt.Size called with negative n") 14 | } 15 | 16 | // Handle small scripts. 17 | switch len(e.Ranges) { 18 | case 0: 19 | return edit.Script{} 20 | case 1: 21 | if e.Ranges[0].IsEqual() { 22 | // Entirely identical contents. 23 | // Unclear what to do here. For now, just bail. 24 | // TODO: something else? what does command line diff do? 25 | return edit.Script{} 26 | } 27 | return edit.NewScript(e.Ranges[0]) 28 | } 29 | 30 | out := make([]edit.Range, 0, len(e.Ranges)) 31 | for i, seg := range e.Ranges { 32 | if !seg.IsEqual() { 33 | out = append(out, seg) 34 | continue 35 | } 36 | if i == 0 { 37 | // Leading Range. Keep only the final n entries. 38 | if seg.Len() > n { 39 | seg = rangeLastN(seg, n) 40 | } 41 | out = append(out, seg) 42 | continue 43 | } 44 | if i == len(e.Ranges)-1 { 45 | // Trailing Range. Keep only the first n entries. 46 | if seg.Len() > n { 47 | seg = rangeFirstN(seg, n) 48 | } 49 | out = append(out, seg) 50 | continue 51 | } 52 | if seg.Len() <= n*2 { 53 | // Small middle Range. Keep unchanged. 54 | out = append(out, seg) 55 | continue 56 | } 57 | // Large middle Range. Break into two disjoint parts. 58 | out = append(out, rangeFirstN(seg, n), rangeLastN(seg, n)) 59 | } 60 | 61 | // TODO: Stock macOS diff also trims common blank lines 62 | // from the beginning/end of eq IndexRangess. 63 | // Perhaps we should do that here too. 64 | // Or perhaps that should be a separate, composable function? 65 | return edit.Script{Ranges: out} 66 | } 67 | 68 | func rangeFirstN(seg edit.Range, n int) edit.Range { 69 | if !seg.IsEqual() { 70 | panic("rangeFirstN bad op") 71 | } 72 | if seg.Len() < n { 73 | panic("rangeFirstN bad Len") 74 | } 75 | return edit.Range{ 76 | LowA: seg.LowA, HighA: seg.LowA + n, 77 | LowB: seg.LowB, HighB: seg.LowB + n, 78 | } 79 | } 80 | 81 | func rangeLastN(seg edit.Range, n int) edit.Range { 82 | if !seg.IsEqual() { 83 | panic("rangeLastN bad op") 84 | } 85 | if seg.Len() < n { 86 | panic("rangeLastN bad Len") 87 | } 88 | return edit.Range{ 89 | LowA: seg.HighA - n, HighA: seg.HighA, 90 | LowB: seg.HighB - n, HighB: seg.HighB, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ctxt/todo.go: -------------------------------------------------------------------------------- 1 | package ctxt 2 | 3 | // TODO: the standard way to reduce context is to a fixed number of lines around changes. 4 | // But it would be better to be more flexible, to try to match human needs. 5 | // For example, if I deleted the first line of a function, I don't need three full lines of "before" context; 6 | // it should truncate at the function declaration. 7 | -------------------------------------------------------------------------------- /diff.go: -------------------------------------------------------------------------------- 1 | // Package diff contains high level routines that generate a textual diff. 2 | // 3 | // It is implemented in terms of the other packages in this module. 4 | // If you want fine-grained control, 5 | // want to inspect a diff programmatically, 6 | // want to provide a context for cancellation, 7 | // need to diff gigantic files that don't fit in memory, 8 | // or want to diff unusual things, 9 | // use the lower level packages. 10 | package diff 11 | 12 | import ( 13 | "bufio" 14 | "bytes" 15 | "context" 16 | "fmt" 17 | "io" 18 | "os" 19 | "reflect" 20 | "strings" 21 | 22 | "github.com/pkg/diff/ctxt" 23 | "github.com/pkg/diff/intern" 24 | "github.com/pkg/diff/myers" 25 | "github.com/pkg/diff/write" 26 | ) 27 | 28 | // lines returns the lines contained in text/filename. 29 | // text and filename are interpreted as described in the docs for Text. 30 | func lines(m intern.Strings, filename string, text interface{}) ([]*string, error) { 31 | var r io.Reader 32 | switch text := text.(type) { 33 | case nil: 34 | f, err := os.Open(filename) 35 | if err != nil { 36 | return nil, err 37 | } 38 | defer f.Close() 39 | r = f 40 | case string: 41 | r = strings.NewReader(text) 42 | case []byte: 43 | r = bytes.NewReader(text) 44 | case io.Reader: 45 | r = text 46 | default: 47 | return nil, fmt.Errorf("unexpected type %T, want string, []byte, io.Reader, or nil", text) 48 | } 49 | var x []*string 50 | scan := bufio.NewScanner(r) 51 | for scan.Scan() { 52 | x = append(x, m.FromBytes(scan.Bytes())) 53 | } 54 | return x, scan.Err() 55 | } 56 | 57 | // addNames adds a Names write.Option using aName and bName, 58 | // taking care to put it at the end, 59 | // so as not to overwrite any competing option. 60 | func addNames(aName, bName string, options []write.Option) []write.Option { 61 | opts := make([]write.Option, len(options)+1) 62 | opts[0] = write.Names(aName, bName) 63 | copy(opts[1:], options) 64 | return opts 65 | } 66 | 67 | // Text diffs a and b and writes the result to w. 68 | // It treats a and b as text, and splits their contents 69 | // into lines using bufio.ScanLines. 70 | // aFile and bFile are filenames to use in the output. 71 | // 72 | // a and b each may be nil or may have type string, []byte, or io.Reader. 73 | // If nil, the text is read from the filename. 74 | func Text(aFile, bFile string, a, b interface{}, w io.Writer, options ...write.Option) error { 75 | m := make(intern.Strings) 76 | aLines, err := lines(m, aFile, a) 77 | if err != nil { 78 | return err 79 | } 80 | bLines, err := lines(m, bFile, b) 81 | if err != nil { 82 | return err 83 | } 84 | ab := &diffStrings{a: aLines, b: bLines} 85 | s := myers.Diff(context.Background(), ab) 86 | s = ctxt.Size(s, 3) 87 | opts := addNames(aFile, bFile, options) 88 | err = write.Unified(s, w, ab, opts...) 89 | return err 90 | } 91 | 92 | type diffStrings struct { 93 | a, b []*string 94 | } 95 | 96 | func (ab *diffStrings) LenA() int { return len(ab.a) } 97 | func (ab *diffStrings) LenB() int { return len(ab.b) } 98 | func (ab *diffStrings) Equal(ai, bi int) bool { return ab.a[ai] == ab.b[bi] } 99 | func (ab *diffStrings) WriteATo(w io.Writer, i int) (int, error) { return io.WriteString(w, *ab.a[i]) } 100 | func (ab *diffStrings) WriteBTo(w io.Writer, i int) (int, error) { return io.WriteString(w, *ab.b[i]) } 101 | 102 | // Slices diffs slices a and b and writes the result to w. 103 | // It uses fmt.Print to print the elements of a and b. 104 | // It uses reflect.DeepEqual to compare elements of a and b. 105 | // It uses aName and bName as the names of a and b in the output. 106 | func Slices(aName, bName string, a, b interface{}, w io.Writer, options ...write.Option) error { 107 | ab := &diffSlices{a: reflect.ValueOf(a), b: reflect.ValueOf(b)} 108 | if err := ab.validateTypes(); err != nil { 109 | return err 110 | } 111 | s := myers.Diff(context.Background(), ab) 112 | s = ctxt.Size(s, 3) 113 | opts := addNames(aName, bName, options) 114 | err := write.Unified(s, w, ab, opts...) 115 | return err 116 | } 117 | 118 | type diffSlices struct { 119 | a, b reflect.Value 120 | } 121 | 122 | func (ab *diffSlices) LenA() int { return ab.a.Len() } 123 | func (ab *diffSlices) LenB() int { return ab.b.Len() } 124 | func (ab *diffSlices) atA(i int) interface{} { return ab.a.Index(i).Interface() } 125 | func (ab *diffSlices) atB(i int) interface{} { return ab.b.Index(i).Interface() } 126 | func (ab *diffSlices) Equal(ai, bi int) bool { return reflect.DeepEqual(ab.atA(ai), ab.atB(bi)) } 127 | func (ab *diffSlices) WriteATo(w io.Writer, i int) (int, error) { return fmt.Fprint(w, ab.atA(i)) } 128 | func (ab *diffSlices) WriteBTo(w io.Writer, i int) (int, error) { return fmt.Fprint(w, ab.atB(i)) } 129 | 130 | func (ab *diffSlices) validateTypes() error { 131 | if t := ab.a.Type(); t.Kind() != reflect.Slice { 132 | return fmt.Errorf("a has type %v, must be a slice", t) 133 | } 134 | if t := ab.b.Type(); t.Kind() != reflect.Slice { 135 | return fmt.Errorf("b has type %v, must be a slice", t) 136 | } 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /edit/edit.go: -------------------------------------------------------------------------------- 1 | // Package edit provides edit scripts. 2 | // Edit scripts are a core notion for diffs. 3 | // They represent a way to go from A to B by a sequence 4 | // of insertions, deletions, and equal elements. 5 | package edit 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // A Script is an edit script to alter A into B. 13 | type Script struct { 14 | Ranges []Range 15 | } 16 | 17 | // NewScript returns a Script containing the ranges r. 18 | // It is only a convenience wrapper used to reduce line noise. 19 | func NewScript(r ...Range) Script { 20 | return Script{Ranges: r} 21 | } 22 | 23 | // IsIdentity reports whether s is the identity edit script, 24 | // that is, whether A and B are identical. 25 | func (s *Script) IsIdentity() bool { 26 | for _, r := range s.Ranges { 27 | if !r.IsEqual() { 28 | return false 29 | } 30 | } 31 | return true 32 | } 33 | 34 | // Stat reports the total number of insertions and deletions in s. 35 | func (s *Script) Stat() (ins, del int) { 36 | for _, r := range s.Ranges { 37 | switch { 38 | case r.IsDelete(): 39 | del += r.HighA - r.LowA 40 | case r.IsInsert(): 41 | ins += r.HighB - r.LowB 42 | } 43 | } 44 | return ins, del 45 | } 46 | 47 | // dump formats s for debugging. 48 | func (s *Script) dump() string { 49 | buf := new(strings.Builder) 50 | for _, r := range s.Ranges { 51 | fmt.Fprintln(buf, r) 52 | } 53 | return buf.String() 54 | } 55 | 56 | // A Range is a pair of clopen index ranges. 57 | // It represents the elements A[LowA:HighA] and B[LowB:HighB]. 58 | type Range struct { 59 | LowA, HighA int 60 | LowB, HighB int 61 | } 62 | 63 | // IsInsert reports whether r represents an insertion in a Script. 64 | // If so, the inserted elements are B[LowB:HighB]. 65 | func (r *Range) IsInsert() bool { 66 | return r.LowA == r.HighA 67 | } 68 | 69 | // IsDelete reports whether r represents a deletion in a Script. 70 | // If so, the deleted elements are A[LowA:HighA]. 71 | func (r *Range) IsDelete() bool { 72 | return r.LowB == r.HighB 73 | } 74 | 75 | // IsEqual reports whether r represents a series of equal elements in a Script. 76 | // If so, the elements A[LowA:HighA] are equal to the elements B[LowB:HighB]. 77 | func (r *Range) IsEqual() bool { 78 | return r.HighB-r.LowB == r.HighA-r.LowA 79 | } 80 | 81 | // An Op is a edit operation in a Script. 82 | type Op int8 83 | 84 | //go:generate stringer -type Op 85 | 86 | const ( 87 | Del Op = -1 // delete 88 | Eq Op = 0 // equal 89 | Ins Op = 1 // insert 90 | ) 91 | 92 | // Op reports what kind of operation r represents. 93 | // This can also be determined by calling r.IsInsert, 94 | // r.IsDelete, and r.IsEqual, 95 | // but this form is sometimes more convenient to use. 96 | func (r *Range) Op() Op { 97 | if r.IsInsert() { 98 | return Ins 99 | } 100 | if r.IsDelete() { 101 | return Del 102 | } 103 | if r.IsEqual() { 104 | return Eq 105 | } 106 | panic("malformed Range") 107 | } 108 | 109 | // Len reports the number of elements in r. 110 | // In a deletion, it is the number of deleted elements. 111 | // In an insertion, it is the number of inserted elements. 112 | // For equal elements, it is the number of equal elements. 113 | func (r *Range) Len() int { 114 | if r.LowA == r.HighA { 115 | return r.HighB - r.LowB 116 | } 117 | return r.HighA - r.LowA 118 | } 119 | -------------------------------------------------------------------------------- /edit/op_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type Op"; DO NOT EDIT. 2 | 3 | package edit 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[Del - -1] 12 | _ = x[Eq-0] 13 | _ = x[Ins-1] 14 | } 15 | 16 | const _Op_name = "DelEqIns" 17 | 18 | var _Op_index = [...]uint8{0, 3, 5, 8} 19 | 20 | func (i Op) String() string { 21 | i -= -1 22 | if i < 0 || i >= Op(len(_Op_index)-1) { 23 | return "Op(" + strconv.FormatInt(int64(i+-1), 10) + ")" 24 | } 25 | return _Op_name[_Op_index[i]:_Op_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package diff_test 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/pkg/diff" 7 | ) 8 | 9 | func Example_Slices() { 10 | want := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} 11 | got := []int{1, 2, 3, 4, 6, 7, 8, 9} 12 | err := diff.Slices("want", "got", want, got, os.Stdout) 13 | if err != nil { 14 | panic(err) 15 | } 16 | // Output: 17 | // --- want 18 | // +++ got 19 | // @@ -2,7 +2,6 @@ 20 | // 2 21 | // 3 22 | // 4 23 | // -5 24 | // 6 25 | // 7 26 | // 8 27 | } 28 | 29 | func Example_Text() { 30 | a := ` 31 | a 32 | b 33 | c 34 | `[1:] 35 | b := ` 36 | a 37 | c 38 | d 39 | `[1:] 40 | err := diff.Text("a", "b", a, b, os.Stdout) 41 | if err != nil { 42 | panic(err) 43 | } 44 | // Output: 45 | // --- a 46 | // +++ b 47 | // @@ -1,3 +1,3 @@ 48 | // a 49 | // -b 50 | // c 51 | // +d 52 | } 53 | -------------------------------------------------------------------------------- /fuzz.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | 3 | package diff 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "io" 9 | "io/ioutil" 10 | 11 | "github.com/pkg/diff/ctxt" 12 | "github.com/pkg/diff/myers" 13 | "github.com/pkg/diff/write" 14 | ) 15 | 16 | func Fuzz(data []byte) int { 17 | if len(data) < 2 { 18 | return -1 19 | } 20 | sz := int(data[0]) 21 | data = data[1:] 22 | 23 | nul := bytes.IndexByte(data, 0) 24 | if nul == -1 { 25 | nul = len(data) - 1 26 | } 27 | a := data[:nul] 28 | b := data[nul:] 29 | ab := &IndividualBytes{a: a, b: b} 30 | s := myers.Diff(context.Background(), ab) 31 | s = ctxt.Size(s, sz) 32 | err := write.Unified(s, ioutil.Discard, ab) 33 | if err != nil { 34 | panic(err) 35 | } 36 | return 0 37 | } 38 | 39 | type IndividualBytes struct { 40 | a, b []byte 41 | } 42 | 43 | func (ab *IndividualBytes) LenA() int { return len(ab.a) } 44 | func (ab *IndividualBytes) LenB() int { return len(ab.b) } 45 | func (ab *IndividualBytes) Equal(ai, bi int) bool { return ab.a[ai] == ab.b[bi] } 46 | func (ab *IndividualBytes) WriteATo(w io.Writer, i int) (int, error) { return w.Write([]byte{ab.a[i]}) } 47 | func (ab *IndividualBytes) WriteBTo(w io.Writer, i int) (int, error) { return w.Write([]byte{ab.b[i]}) } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pkg/diff 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkg/diff/4e6772a4315c211e75d179250699dede5881c54c/go.sum -------------------------------------------------------------------------------- /intern/intern.go: -------------------------------------------------------------------------------- 1 | // Package intern provides string interning. 2 | // 3 | // Unlike much string interning, the routines in this package 4 | // return *string instead of string. This enables extremely 5 | // cheap (compare only a pointer) comparisons of any strings 6 | // interned by this package. Since diff algorithms involve 7 | // many string comparisons, this often ends up paying for the 8 | // cost of the interning. Also, in the typical case, 9 | // diffs involve lots of repeated lines (most of the file 10 | // contents are typically unchanged, so any give line 11 | // appears at least twice), so string interning saves memory. 12 | package intern 13 | 14 | type Strings map[string]*string 15 | 16 | func (m Strings) FromBytes(b []byte) *string { 17 | p, ok := m[string(b)] 18 | if ok { 19 | return p 20 | } 21 | s := string(b) 22 | p = &s 23 | m[s] = p 24 | return p 25 | } 26 | -------------------------------------------------------------------------------- /myers/myers.go: -------------------------------------------------------------------------------- 1 | // Package myers implements the Myers diff algorithm. 2 | package myers 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | 8 | "github.com/pkg/diff/edit" 9 | ) 10 | 11 | // A Pair is two things that can be diffed using the Myers diff algorithm. 12 | // A is the initial state; B is the final state. 13 | type Pair interface { 14 | // LenA returns the number of initial elements. 15 | LenA() int 16 | // LenB returns the number of final elements. 17 | LenB() int 18 | // Equal reports whether the aᵢ'th element of A is equal to the bᵢ'th element of B. 19 | Equal(ai, bi int) bool 20 | } 21 | 22 | // Diff calculates an edit.Script for ab using the Myers diff algorithm. 23 | // This implementation uses the algorithm described in the first half 24 | // of Myers' paper, which requires quadratric space. 25 | // (An implementation of the linear space version is forthcoming.) 26 | // 27 | // Because diff calculation can be expensive, Myers supports cancellation via ctx. 28 | func Diff(ctx context.Context, ab Pair) edit.Script { 29 | aLen := ab.LenA() 30 | bLen := ab.LenB() 31 | if aLen == 0 && bLen == 0 { 32 | return edit.NewScript() 33 | } 34 | if aLen == 0 { 35 | return edit.NewScript(edit.Range{HighB: bLen}) 36 | } 37 | if bLen == 0 { 38 | return edit.NewScript(edit.Range{HighA: aLen}) 39 | } 40 | 41 | max := aLen + bLen 42 | if max < 0 { 43 | panic("overflow in myers.Diff") 44 | } 45 | // v has indices -max .. 0 .. max 46 | // access to elements of v have the form max + actual offset 47 | v := make([]int, 2*max+1) 48 | 49 | var trace [][]int 50 | search: 51 | for d := 0; d < max; d++ { 52 | // Only check context every 16th iteration to reduce overhead. 53 | if ctx != nil && uint(d)%16 == 0 && ctx.Err() != nil { 54 | return edit.Script{} 55 | } 56 | 57 | // append the middle (populated) elements of v to trace 58 | middle := v[max-d : max+d+1] 59 | vcopy := make([]int, len(middle)) 60 | copy(vcopy, middle) 61 | trace = append(trace, vcopy) 62 | 63 | for k := -d; k <= d; k += 2 { 64 | var x int 65 | if k == -d || (k != d && v[max+k-1] < v[max+k+1]) { 66 | x = v[max+k+1] 67 | } else { 68 | x = v[max+k-1] + 1 69 | } 70 | 71 | y := x - k 72 | for x < aLen && y < bLen && ab.Equal(x, y) { 73 | x++ 74 | y++ 75 | } 76 | v[max+k] = x 77 | 78 | if x == aLen && y == bLen { 79 | break search 80 | } 81 | } 82 | } 83 | 84 | if len(trace) == max { 85 | // No commonality at all, delete everything and then insert everything. 86 | // This is handled as a special case to avoid complicating the logic below. 87 | return edit.NewScript(edit.Range{HighA: aLen}, edit.Range{HighB: bLen}) 88 | } 89 | 90 | // Create reversed edit script. 91 | x := aLen 92 | y := bLen 93 | var e edit.Script 94 | for d := len(trace) - 1; d >= 0; d-- { 95 | // v has indices -d .. 0 .. d 96 | // access to elements of v have the form d + actual offset 97 | v := trace[d] 98 | k := x - y 99 | var prevk int 100 | if k == -d || (k != d && v[d+k-1] < v[d+k+1]) { 101 | prevk = k + 1 102 | } else { 103 | prevk = k - 1 104 | } 105 | var prevx int 106 | if idx := d + prevk; 0 <= idx && idx < len(v) { 107 | prevx = v[idx] 108 | } 109 | prevy := prevx - prevk 110 | for x > prevx && y > prevy { 111 | appendToReversed(&e, edit.Range{LowA: x - 1, LowB: y - 1, HighA: x, HighB: y}) 112 | x-- 113 | y-- 114 | } 115 | if d > 0 { 116 | appendToReversed(&e, edit.Range{LowA: prevx, LowB: prevy, HighA: x, HighB: y}) 117 | } 118 | x, y = prevx, prevy 119 | } 120 | 121 | // Reverse reversed edit script, to return to natural order. 122 | reverse(e) 123 | 124 | // Sanity check 125 | for i := 1; i < len(e.Ranges); i++ { 126 | prevop := e.Ranges[i-1].Op() 127 | currop := e.Ranges[i].Op() 128 | if (prevop == currop) || (prevop == edit.Ins && currop != edit.Eq) || (currop == edit.Del && prevop != edit.Eq) { 129 | panic(fmt.Errorf("bad script: %v -> %v", prevop, currop)) 130 | } 131 | } 132 | 133 | return e 134 | } 135 | 136 | func reverse(e edit.Script) { 137 | for i := 0; i < len(e.Ranges)/2; i++ { 138 | j := len(e.Ranges) - i - 1 139 | e.Ranges[i], e.Ranges[j] = e.Ranges[j], e.Ranges[i] 140 | } 141 | } 142 | 143 | func appendToReversed(e *edit.Script, seg edit.Range) { 144 | if len(e.Ranges) == 0 { 145 | e.Ranges = append(e.Ranges, seg) 146 | return 147 | } 148 | u, ok := combineRanges(seg, e.Ranges[len(e.Ranges)-1]) 149 | if !ok { 150 | e.Ranges = append(e.Ranges, seg) 151 | return 152 | } 153 | e.Ranges[len(e.Ranges)-1] = u 154 | return 155 | } 156 | 157 | // combineRanges combines s and t into a single edit.Range if possible 158 | // and reports whether it succeeded. 159 | func combineRanges(s, t edit.Range) (u edit.Range, ok bool) { 160 | if t.Len() == 0 { 161 | return s, true 162 | } 163 | if s.Len() == 0 { 164 | return t, true 165 | } 166 | if s.Op() != t.Op() { 167 | return edit.Range{LowA: -1, HighA: -1, LowB: -1, HighB: -1}, false 168 | } 169 | switch s.Op() { 170 | case edit.Ins: 171 | s.HighB = t.HighB 172 | case edit.Del: 173 | s.HighA = t.HighA 174 | case edit.Eq: 175 | s.HighA = t.HighA 176 | s.HighB = t.HighB 177 | default: 178 | panic("bad op") 179 | } 180 | return s, true 181 | } 182 | 183 | func rangeString(r edit.Range) string { 184 | // This output is helpful when hacking on a Myers diff. 185 | // In other contexts it is usually more natural to group LowA, HighA and LowB, HighB. 186 | return fmt.Sprintf("(%d, %d) -- %s %d --> (%d, %d)", r.LowA, r.LowB, r.Op(), r.Len(), r.HighA, r.HighB) 187 | } 188 | -------------------------------------------------------------------------------- /myers/myers_test.go: -------------------------------------------------------------------------------- 1 | package myers_test 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/pkg/diff/edit" 9 | "github.com/pkg/diff/myers" 10 | ) 11 | 12 | func TestMyers(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | a, b string 16 | want []edit.Range 17 | wantStatIns int 18 | wantStatDel int 19 | }{ 20 | { 21 | name: "BasicExample", 22 | a: "ABCABBA", 23 | b: "CBABAC", 24 | want: []edit.Range{ 25 | {LowA: 0, HighA: 2, LowB: 0, HighB: 0}, 26 | {LowA: 2, HighA: 3, LowB: 0, HighB: 1}, 27 | {LowA: 3, HighA: 3, LowB: 1, HighB: 2}, 28 | {LowA: 3, HighA: 5, LowB: 2, HighB: 4}, 29 | {LowA: 5, HighA: 6, LowB: 4, HighB: 4}, 30 | {LowA: 6, HighA: 7, LowB: 4, HighB: 5}, 31 | {LowA: 7, HighA: 7, LowB: 5, HighB: 6}, 32 | }, 33 | wantStatIns: 2, 34 | wantStatDel: 3, 35 | }, 36 | { 37 | name: "AllDifferent", 38 | a: "ABCDE", 39 | b: "xyz", 40 | want: []edit.Range{ 41 | {LowA: 0, HighA: 5, LowB: 0, HighB: 0}, 42 | {LowA: 0, HighA: 0, LowB: 0, HighB: 3}, 43 | }, 44 | wantStatIns: 3, 45 | wantStatDel: 5, 46 | }, 47 | { 48 | name: "AllEmpty", 49 | a: "", 50 | b: "", 51 | want: nil, 52 | wantStatIns: 0, 53 | wantStatDel: 0, 54 | }, 55 | // TODO: add more tests 56 | } 57 | 58 | for _, test := range tests { 59 | t.Run(test.name, func(t *testing.T) { 60 | ab := &diffByByte{a: test.a, b: test.b} 61 | got := myers.Diff(context.Background(), ab) 62 | want := edit.Script{Ranges: test.want} 63 | 64 | if !reflect.DeepEqual(got, want) { 65 | // Ironically, it'd be nice to provide a diff between got and want here... 66 | // but our diff algorithm is busted. 67 | t.Errorf("got:\n%v\n\nwant:\n%v\n\n", got, want) 68 | } 69 | ins, del := got.Stat() 70 | if ins != test.wantStatIns { 71 | t.Errorf("got %d insertions, want %d", ins, test.wantStatIns) 72 | } 73 | if del != test.wantStatDel { 74 | t.Errorf("got %d deletions, want %d", del, test.wantStatDel) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | type diffByByte struct { 81 | a, b string 82 | } 83 | 84 | func (ab *diffByByte) LenA() int { return len(ab.a) } 85 | func (ab *diffByByte) LenB() int { return len(ab.b) } 86 | func (ab *diffByByte) Equal(ai, bi int) bool { return ab.a[ai] == ab.b[bi] } 87 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # diff [![GoDoc](https://godoc.org/github.com/pkg/diff?status.svg)](http://godoc.org/github.com/pkg/diff) 2 | 3 | Module github.com/pkg/diff can be used to create, modify, and print diffs. 4 | 5 | The top level package, `diff`, contains convenience functions for the most common uses. 6 | 7 | The subpackages provide very fine-grained control over every aspect: 8 | 9 | * `myers` creates diffs using the Myers diff algorithm. 10 | * `edit` contains the core diff data types. 11 | * `ctxt` provides tools to reduce the amount of context in a diff. 12 | * `write` provides routines to write diffs in standard formats. 13 | 14 | License: BSD 3-Clause. 15 | 16 | ### Contributing 17 | 18 | Contributions are welcome. However, I am not always fast to respond. 19 | I apologize for any sadness or frustration that that causes. 20 | 21 | Useful background reading about diffs: 22 | 23 | * [Neil Fraser's website](https://neil.fraser.name/writing/diff) 24 | * [Myers diff paper](http://www.xmailserver.org/diff2.pdf) 25 | * [Guido Van Rossum's reverse engineering of the unified diff format](https://www.artima.com/weblogs/viewpost.jsp?thread=164293) 26 | * [The If Works](https://blog.jcoglan.com/) blog entries about diff algorithms and implementations 27 | 28 | This module has not yet reached v1.0; 29 | the API is not yet settled (issue #18). 30 | -------------------------------------------------------------------------------- /todo.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | // TODO: add a package for diffing gigantic files. 4 | // Instead of reading the entire thing into memory, we could 5 | // scan through the file once, storing the location of all newlines in each file. 6 | // Then Seek/ReadAt to read each line lazily as needed, 7 | // relying on the OS page cache for performance. 8 | // This will allow diffing giant files with low memory use, 9 | // albeit at a some time cost. 10 | // An alternative is to mmap the files, 11 | // although this is OS-specific and can be fiddly. 12 | 13 | // TODO: add a package providing a StringIntern type, something like: 14 | // 15 | // type StringIntern struct { 16 | // s map[string]*string 17 | // } 18 | // 19 | // func (i *StringIntern) Bytes(b []byte) *string 20 | // func (i *StringIntern) String(s string) *string 21 | // 22 | // And document what it is and why to use it. 23 | // And consider adding helper functions to Strings and Bytes to use it. 24 | // The reason to use it is that a lot of the execution time in diffing 25 | // (which is an expensive operation) is taken up doing string comparisons. 26 | // If you have paid the O(n) cost to intern all strings involved in both A and B, 27 | // then string comparisons are reduced to cheap pointer comparisons. 28 | 29 | // TODO: consider adding an "it just works" test helper that accepts two slices (via interface{}), 30 | // diffs them using Strings or Bytes or Slices (using reflect.DeepEqual) as appropriate, 31 | // and calls t.Errorf with a generated diff if they're not equal. 32 | 33 | // TODO: add support for hunk/section/function headers. 34 | // This will probably take the form of a write option 35 | // providing access to the necessary data, 36 | // and a package that helps calculate the necessary data. 37 | // There are several ways to do that calculation... 38 | 39 | // TODO: add copyright headers at top of all files 40 | 41 | // TODO: hook up some CI 42 | 43 | // TODO: add more badges? see github.com/pkg/errors for some likely candidates. 44 | -------------------------------------------------------------------------------- /write/option.go: -------------------------------------------------------------------------------- 1 | // Package write provides routines for writing diffs. 2 | package write 3 | 4 | // An Option modifies behavior when writing a diff. 5 | type Option interface { 6 | isOption() 7 | } 8 | 9 | // Names provides the before/after names for writing a diff. 10 | // They are traditionally filenames. 11 | func Names(a, b string) Option { 12 | return names{a, b} 13 | } 14 | 15 | type names struct { 16 | a, b string 17 | } 18 | 19 | func (names) isOption() {} 20 | 21 | // TerminalColor specifies that a diff intended 22 | // for a terminal should be written using colors. 23 | // 24 | // Do not use TerminalColor if TERM=dumb is set in the environment. 25 | func TerminalColor() Option { 26 | return colorOpt(true) 27 | } 28 | 29 | type colorOpt bool 30 | 31 | func (colorOpt) isOption() {} 32 | 33 | const ( 34 | ansiBold = "\u001b[1m" 35 | ansiFgRed = "\u001b[31m" 36 | ansiFgGreen = "\u001b[32m" 37 | ansiFgBlue = "\u001b[36m" 38 | ansiReset = "\u001b[0m" 39 | ) 40 | -------------------------------------------------------------------------------- /write/todo.go: -------------------------------------------------------------------------------- 1 | package write 2 | 3 | // TODO: add diff writing that uses < and > (don't know what that is called) 4 | // TODO: add side by side diffs 5 | // TODO: add html diffs (?) 6 | // TODO: add intraline highlighting? 7 | // TODO: a way to specify alternative colors, like a ColorScheme write option 8 | -------------------------------------------------------------------------------- /write/unified.go: -------------------------------------------------------------------------------- 1 | package write 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/pkg/diff/edit" 9 | ) 10 | 11 | // A Pair supports writing a unified diff, element by element. 12 | // A is the initial state; B is the final state. 13 | type Pair interface { 14 | // WriteATo writes the element a[aᵢ] to w. 15 | WriteATo(w io.Writer, ai int) (int, error) 16 | // WriteBTo writes the element b[bᵢ] to w. 17 | WriteBTo(w io.Writer, bi int) (int, error) 18 | } 19 | 20 | // Unified writes e to w using unified diff format. 21 | // ab writes the individual elements. Opts are optional write arguments. 22 | // Unified returns the number of bytes written and the first error (if any) encountered. 23 | // Before writing, edit scripts usually have their context reduced, 24 | // such as by a call to ctxt.Size. 25 | func Unified(e edit.Script, w io.Writer, ab Pair, opts ...Option) error { 26 | // read opts 27 | nameA := "a" 28 | nameB := "b" 29 | color := false 30 | for _, opt := range opts { 31 | switch opt := opt.(type) { 32 | case names: 33 | nameA = opt.a 34 | nameB = opt.b 35 | case colorOpt: 36 | color = true 37 | // TODO: add date/time/timezone WriteOpts 38 | default: 39 | panic(fmt.Sprintf("unrecognized WriteOpt type %T", opt)) 40 | } 41 | } 42 | 43 | bw := bufio.NewWriter(w) 44 | 45 | needsColorReset := false 46 | 47 | // per-file header 48 | if color { 49 | bw.WriteString(ansiBold) 50 | needsColorReset = true 51 | } 52 | fmt.Fprintf(bw, "--- %s\n", nameA) 53 | fmt.Fprintf(bw, "+++ %s\n", nameB) 54 | 55 | for i := 0; i < len(e.Ranges); { 56 | // Peek into the future to learn the line ranges for this chunk of output. 57 | // A chunk of output ends when there's a discontiguity in the edit script. 58 | var ar, br lineRange 59 | var started [2]bool 60 | var j int 61 | for j = i; j < len(e.Ranges); j++ { 62 | curr := e.Ranges[j] 63 | if !curr.IsInsert() { 64 | if !started[0] { 65 | ar.first = curr.LowA 66 | started[0] = true 67 | } 68 | ar.last = curr.HighA 69 | } 70 | if !curr.IsDelete() { 71 | if !started[1] { 72 | br.first = curr.LowB 73 | started[1] = true 74 | } 75 | br.last = curr.HighB 76 | } 77 | if j+1 >= len(e.Ranges) { 78 | // end of script 79 | break 80 | } 81 | if next := e.Ranges[j+1]; curr.HighA != next.LowA || curr.HighB != next.LowB { 82 | // discontiguous edit script 83 | break 84 | } 85 | } 86 | 87 | // Print chunk header. 88 | // TODO: add per-chunk context, like what function we're in 89 | // But how do we get this? need to add PairWriter methods? 90 | // Maybe it should be stored in the EditScript, 91 | // and we can have EditScript methods to populate it somehow? 92 | if color { 93 | if needsColorReset { 94 | bw.WriteString(ansiReset) 95 | } 96 | bw.WriteString(ansiFgBlue) 97 | needsColorReset = true 98 | } 99 | fmt.Fprintf(bw, "@@ -%s +%s @@\n", ar, br) 100 | 101 | // Print prefixed lines. 102 | for k := i; k <= j; k++ { 103 | seg := e.Ranges[k] 104 | switch seg.Op() { 105 | case edit.Eq: 106 | if needsColorReset { 107 | bw.WriteString(ansiReset) 108 | } 109 | for m := seg.LowA; m < seg.HighA; m++ { 110 | // " a[m]\n" 111 | bw.WriteByte(' ') 112 | ab.WriteATo(bw, m) 113 | bw.WriteByte('\n') 114 | } 115 | case edit.Del: 116 | if color { 117 | bw.WriteString(ansiFgRed) 118 | needsColorReset = true 119 | } 120 | for m := seg.LowA; m < seg.HighA; m++ { 121 | // "-a[m]\n" 122 | bw.WriteByte('-') 123 | ab.WriteATo(bw, m) 124 | bw.WriteByte('\n') 125 | } 126 | case edit.Ins: 127 | if color { 128 | bw.WriteString(ansiFgGreen) 129 | needsColorReset = true 130 | } 131 | for m := seg.LowB; m < seg.HighB; m++ { 132 | // "+b[m]\n" 133 | bw.WriteByte('+') 134 | ab.WriteBTo(bw, m) 135 | bw.WriteByte('\n') 136 | } 137 | } 138 | } 139 | 140 | // Advance to next chunk. 141 | i = j + 1 142 | 143 | // TODO: break if error detected? 144 | } 145 | 146 | // Always finish the output with no color, to prevent "leaking" the 147 | // color into any output that follows a diff. 148 | if needsColorReset { 149 | bw.WriteString(ansiReset) 150 | } 151 | 152 | // TODO: 153 | // If the last line of a file doesn't end in a newline character, 154 | // it is displayed with a newline character, 155 | // and the following line in the chunk has the literal text (starting in the first column): 156 | // '\ No newline at end of file' 157 | 158 | return bw.Flush() 159 | } 160 | 161 | type lineRange struct { 162 | first, last int 163 | } 164 | 165 | func (r lineRange) String() string { 166 | len := r.last - r.first 167 | r.first++ // 1-based index, safe to modify r directly because it is a value 168 | if len <= 0 { 169 | r.first-- // for no obvious reason, empty ranges are "before" the range 170 | } 171 | return fmt.Sprintf("%d,%d", r.first, len) 172 | } 173 | 174 | func (r lineRange) GoString() string { 175 | return fmt.Sprintf("(%d, %d)", r.first, r.last) 176 | } 177 | -------------------------------------------------------------------------------- /write/unified_test.go: -------------------------------------------------------------------------------- 1 | package write_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/pkg/diff/ctxt" 11 | "github.com/pkg/diff/myers" 12 | "github.com/pkg/diff/write" 13 | ) 14 | 15 | var goldenTests = []struct { 16 | name string 17 | a, b string 18 | opts []write.Option 19 | want string // usually from running diff --unified and cleaning up the output 20 | }{ 21 | { 22 | name: "AddedLinesEnd", 23 | a: "A\nB\nC\nD\nE\nF\n", 24 | b: "A\nB\nC\nD\nE\nF\n1\n2\n3\n", 25 | // TODO: stock macOS diff omits the trailing common blank line in this diff, 26 | // which also changes the @@ line ranges to be 4,3 and 4,6. 27 | want: ` 28 | --- a 29 | +++ b 30 | @@ -4,4 +4,7 @@ 31 | D 32 | E 33 | F 34 | +1 35 | +2 36 | +3 37 | 38 | `[1:], 39 | }, 40 | 41 | { 42 | name: "AddedLinesStart", 43 | a: "A\nB\nC\nD\nE\nF\n", 44 | b: "1\n2\n3\nA\nB\nC\nD\nE\nF\n", 45 | want: ` 46 | --- a 47 | +++ b 48 | @@ -1,3 +1,6 @@ 49 | +1 50 | +2 51 | +3 52 | A 53 | B 54 | C 55 | `[1:], 56 | }, 57 | 58 | { 59 | name: "WithTerminalColor", 60 | a: "1\n2\n2", 61 | b: "1\n3\n3", 62 | opts: []write.Option{write.TerminalColor()}, 63 | want: ` 64 | `[1:] + "\u001b[1m" + `--- a 65 | +++ b 66 | ` + "\u001b[0m" + "\u001b[36m" + `@@ -1,3 +1,3 @@ 67 | ` + "\u001b[0m" + ` 1 68 | ` + "\u001b[31m" + `-2 69 | -2 70 | ` + "\u001b[32m" + `+3 71 | +3 72 | ` + "\u001b[0m", 73 | }, 74 | } 75 | 76 | func TestGolden(t *testing.T) { 77 | for _, test := range goldenTests { 78 | t.Run(test.name, func(t *testing.T) { 79 | as := strings.Split(test.a, "\n") 80 | bs := strings.Split(test.b, "\n") 81 | ab := &diffStrings{a: as, b: bs} 82 | // TODO: supply an edit.Script to the tests instead doing a Myers diff here. 83 | // Doing it as I have done, the lazy way, mixes concerns: diff algorithm vs unification algorithm 84 | // vs unified diff formatting. 85 | e := myers.Diff(context.Background(), ab) 86 | e = ctxt.Size(e, 3) 87 | buf := new(bytes.Buffer) 88 | err := write.Unified(e, buf, ab, test.opts...) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | got := buf.String() 93 | if test.want != got { 94 | t.Logf("%q\n", test.want) 95 | t.Logf("%q\n", got) 96 | t.Errorf("bad diff: a=%q b=%q\n\ngot:\n%s\nwant:\n%s", 97 | test.a, test.b, 98 | got, test.want, 99 | ) 100 | } 101 | }) 102 | } 103 | } 104 | 105 | type diffStrings struct { 106 | a, b []string 107 | } 108 | 109 | func (ab *diffStrings) LenA() int { return len(ab.a) } 110 | func (ab *diffStrings) LenB() int { return len(ab.b) } 111 | func (ab *diffStrings) Equal(ai, bi int) bool { return ab.a[ai] == ab.b[bi] } 112 | func (ab *diffStrings) WriteATo(w io.Writer, i int) (int, error) { return io.WriteString(w, ab.a[i]) } 113 | func (ab *diffStrings) WriteBTo(w io.Writer, i int) (int, error) { return io.WriteString(w, ab.b[i]) } 114 | --------------------------------------------------------------------------------