From a78b2397ab6b2815d9e61ad1019a8ed1736daf51 Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Mon, 30 Dec 2019 14:27:23 -0800 Subject: [PATCH] write: create package Part of #18. I am very torn about having a single write package with a routine per format: write.Pair write.Option write.Names write.Unified write.SideBySide vs having a package per format: write.Option write.Names unified.Pair unified.Write sidebyside.Pair sidebyside.Write One motivation for separate packages is that the ideal interface for different formats is different. For unified, WriteTo is high performance, and we don't need more. For side by side, we need to be able to inspect and even modify the line before writing, so it makes more sense to have an interface specified in terms of []byte. However, we can use a bytes.Buffer internally to bridge from WriteTo to []byte, and most write options are shared between the formats. So for the moment, try out a single write package. Maybe Daniel (or someone else) will have a compelling reason to go one way or another. The adaptor API is getting ugly. It'll get fixed (mostly eliminated) soon. --- adapter.go | 22 ++++-- cmd/pkg-diff-example/main.go | 9 +-- diff.go | 21 ------ example_test.go | 7 +- go.mod | 5 +- go.sum | 10 +++ write/errwriter.go | 45 ++++++++++++ write/option.go | 39 ++++++++++ write/todo.go | 7 ++ print.go => write/unified.go | 92 +++--------------------- unified_test.go => write/unified_test.go | 9 +-- 11 files changed, 146 insertions(+), 120 deletions(-) create mode 100644 write/errwriter.go create mode 100644 write/option.go create mode 100644 write/todo.go rename print.go => write/unified.go (66%) rename unified_test.go => write/unified_test.go (92%) diff --git a/adapter.go b/adapter.go index 118f162..64a5c95 100644 --- a/adapter.go +++ b/adapter.go @@ -5,10 +5,20 @@ import ( "fmt" "io" "reflect" + + "github.com/pkg/diff/myers" + "github.com/pkg/diff/write" ) -// Strings returns a PairWriterTo that can diff and write a and b. -func Strings(a, b []string) PairWriterTo { +// DiffWrite is the union of myers.Pair and write.Pair: +// It can be diffed using myers diff, and written in unified diff format. +type DiffWrite interface { + myers.Pair + write.Pair +} + +// Strings returns a DiffWrite that can diff and write a and b. +func Strings(a, b []string) DiffWrite { return &diffStrings{a: a, b: b} } @@ -22,8 +32,8 @@ func (ab *diffStrings) Equal(ai, bi int) bool { return ab.a[a func (ab *diffStrings) WriteATo(w io.Writer, i int) (int, error) { return io.WriteString(w, ab.a[i]) } func (ab *diffStrings) WriteBTo(w io.Writer, i int) (int, error) { return io.WriteString(w, ab.b[i]) } -// Bytes returns a PairWriterTo that can diff and write a and b. -func Bytes(a, b [][]byte) PairWriterTo { +// Bytes returns a DiffWrite that can diff and write a and b. +func Bytes(a, b [][]byte) DiffWrite { return &diffBytes{a: a, b: b} } @@ -37,11 +47,11 @@ func (ab *diffBytes) Equal(ai, bi int) bool { return bytes.Eq func (ab *diffBytes) WriteATo(w io.Writer, i int) (int, error) { return w.Write(ab.a[i]) } func (ab *diffBytes) WriteBTo(w io.Writer, i int) (int, error) { return w.Write(ab.b[i]) } -// Slices returns a PairWriterTo that diffs a and b. +// Slices returns a DiffWrite that diffs a and b. // It uses fmt.Print to print the elements of a and b. // It uses equal to compare elements of a and b; // if equal is nil, Slices uses reflect.DeepEqual. -func Slices(a, b interface{}, equal func(x, y interface{}) bool) PairWriterTo { +func Slices(a, b interface{}, equal func(x, y interface{}) bool) DiffWrite { if equal == nil { equal = reflect.DeepEqual } diff --git a/cmd/pkg-diff-example/main.go b/cmd/pkg-diff-example/main.go index 56d0f83..14d4ffb 100644 --- a/cmd/pkg-diff-example/main.go +++ b/cmd/pkg-diff-example/main.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/diff" "github.com/pkg/diff/ctxt" "github.com/pkg/diff/myers" + "github.com/pkg/diff/write" ) var ( @@ -77,12 +78,12 @@ func main() { } e := myers.Diff(ctx, ab) e = ctxt.Size(e, *unified) // limit amount of output context - opts := []diff.WriteOpt{ - diff.Names(aName, bName), + opts := []write.Option{ + write.Names(aName, bName), } if *color { - opts = append(opts, diff.TerminalColor()) + opts = append(opts, write.TerminalColor()) } - _, err = diff.WriteUnified(e, os.Stdout, ab, opts...) + _, err = write.Unified(e, os.Stdout, ab, opts...) check(err) } diff --git a/diff.go b/diff.go index e956beb..f8689a2 100644 --- a/diff.go +++ b/diff.go @@ -1,22 +1 @@ package diff - -import ( - "io" - - "github.com/pkg/diff/myers" -) - -// A WriterTo type supports writing a diff, element by element. -// A is the initial state; B is the final state. -type WriterTo interface { - // WriteATo writes the element a[ai] to w. - WriteATo(w io.Writer, ai int) (int, error) - // WriteBTo writes the element b[bi] to w. - WriteBTo(w io.Writer, bi int) (int, error) -} - -// PairWriterTo is the union of Pair and WriterTo. -type PairWriterTo interface { - myers.Pair - WriterTo -} diff --git a/example_test.go b/example_test.go index 152f5e7..2197b92 100644 --- a/example_test.go +++ b/example_test.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/diff" "github.com/pkg/diff/ctxt" "github.com/pkg/diff/myers" + "github.com/pkg/diff/write" ) // TODO: use a less heavyweight output format for Example_testHelper @@ -20,7 +21,7 @@ func Example_testHelper() { return } e = ctxt.Size(e, 1) - diff.WriteUnified(e, os.Stdout, ab) + write.Unified(e, os.Stdout, ab) // Output: // --- a // +++ b @@ -35,7 +36,7 @@ func Example_strings() { b := []string{"a", "c", "d"} ab := diff.Strings(a, b) e := myers.Diff(context.Background(), ab) - diff.WriteUnified(e, os.Stdout, ab) + write.Unified(e, os.Stdout, ab) // Output: // --- a // +++ b @@ -51,7 +52,7 @@ func Example_Names() { b := []string{"a", "c", "d"} ab := diff.Strings(a, b) e := myers.Diff(context.Background(), ab) - diff.WriteUnified(e, os.Stdout, ab, diff.Names("before", "after")) + write.Unified(e, os.Stdout, ab, write.Names("before", "after")) // Output: // --- before // +++ after diff --git a/go.mod b/go.mod index a582bb9..1904a9a 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/pkg/diff go 1.13 -require github.com/sergi/go-diff v1.0.0 +require ( + github.com/sergi/go-diff v1.0.0 + github.com/stretchr/testify v1.4.0 // indirect +) diff --git a/go.sum b/go.sum index 099e74e..e130bc2 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +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/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/write/errwriter.go b/write/errwriter.go new file mode 100644 index 0000000..cb8bcad --- /dev/null +++ b/write/errwriter.go @@ -0,0 +1,45 @@ +package write + +import "io" + +func newErrWriter(w io.Writer) *errwriter { + return &errwriter{w: w} +} + +// An errwriter wraps a writer. +// As soon as one write fails, it consumes all subsequent writes. +// This reduces the amount of error-checking required +// in write-heavy code. +type errwriter struct { + w io.Writer + err error + wrote int + attempted int +} + +func (w *errwriter) Write(b []byte) (int, error) { + w.attempted += len(b) + if w.err != nil { + return 0, w.err // TODO: use something like errors.Wrap(w.err)? + } + n, err := w.w.Write(b) + if err != nil { + w.err = err + } + w.wrote += n + return n, err +} + +func (w *errwriter) WriteString(s string) { + // TODO: use w.w's WriteString method, if it exists + w.Write([]byte(s)) +} + +func (w *errwriter) WriteByte(b byte) { + // TODO: use w.w's WriteByte method, if it exists + w.Write([]byte{b}) +} + +func (w *errwriter) Error() error { + return w.err +} diff --git a/write/option.go b/write/option.go new file mode 100644 index 0000000..9e22b1a --- /dev/null +++ b/write/option.go @@ -0,0 +1,39 @@ +// Package write provides routines for writing diffs. +package write + +// An Option modifies behavior when writing a diff. +type Option interface { + isOption() +} + +// Names provides the before/after names for writing a diff. +// They are traditionally filenames. +func Names(a, b string) Option { + return names{a, b} +} + +type names struct { + a, b string +} + +func (names) isOption() {} + +// TerminalColor specifies that a diff intended +// for a terminal should be written using colors. +// +// Do not use TerminalColor if TERM=dumb is set in the environment. +func TerminalColor() Option { + return colorOpt(true) +} + +type colorOpt bool + +func (colorOpt) isOption() {} + +const ( + ansiBold = "\u001b[1m" + ansiFgRed = "\u001b[31m" + ansiFgGreen = "\u001b[32m" + ansiFgBlue = "\u001b[36m" + ansiReset = "\u001b[0m" +) diff --git a/write/todo.go b/write/todo.go new file mode 100644 index 0000000..eb6c42b --- /dev/null +++ b/write/todo.go @@ -0,0 +1,7 @@ +package write + +// TODO: add diff writing that uses < and > (don't know what that is called) +// TODO: add side by side diffs +// TODO: add html diffs (?) +// TODO: add intraline highlighting? +// TODO: a way to specify alternative colors, like a ColorScheme write option diff --git a/print.go b/write/unified.go similarity index 66% rename from print.go rename to write/unified.go index 554a56b..25340e6 100644 --- a/print.go +++ b/write/unified.go @@ -1,4 +1,4 @@ -package diff +package write import ( "fmt" @@ -7,55 +7,21 @@ import ( "github.com/pkg/diff/edit" ) -// TODO: add diff writing that uses < and > (don't know what that is called) -// TODO: add side by side diffs -// TODO: add html diffs (?) -// TODO: add intraline highlighting? -// TODO: a way to specify alternative colors, like a ColorScheme write option - -// A WriteOpt is used to provide options when writing a diff. -type WriteOpt interface { - isWriteOpt() -} - -// Names provides the before/after names for writing a diff. -// They are traditionally filenames. -func Names(a, b string) WriteOpt { - return names{a, b} +// A Pair type supports writing a unified diff, element by element. +// A is the initial state; B is the final state. +type Pair interface { + // WriteATo writes the element a[aᵢ] to w. + WriteATo(w io.Writer, ai int) (int, error) + // WriteBTo writes the element b[bᵢ] to w. + WriteBTo(w io.Writer, bi int) (int, error) } -type names struct { - a, b string -} - -func (names) isWriteOpt() {} - -// TerminalColor specifies that a diff intended for a terminal should be written -// using red and green colors. -// -// Do not use TerminalColor if TERM=dumb is set in the environment. -func TerminalColor() WriteOpt { - return colorOpt(true) -} - -type colorOpt bool - -func (colorOpt) isWriteOpt() {} - -const ( - ansiBold = "\u001b[1m" - ansiFgRed = "\u001b[31m" - ansiFgGreen = "\u001b[32m" - ansiFgBlue = "\u001b[36m" - ansiReset = "\u001b[0m" -) - -// WriteUnified writes e to w using unified diff format. +// Unified writes e to w using unified diff format. // ab writes the individual elements. Opts are optional write arguments. -// WriteUnified returns the number of bytes written and the first error (if any) encountered. +// Unified returns the number of bytes written and the first error (if any) encountered. // Before writing, edit scripts usually have their context reduced, // such as by a call to ctxt.Size. -func WriteUnified(e edit.Script, w io.Writer, ab WriterTo, opts ...WriteOpt) (int, error) { +func Unified(e edit.Script, w io.Writer, ab Pair, opts ...Option) (int, error) { // read opts nameA := "a" nameB := "b" @@ -209,39 +175,3 @@ func (r lineRange) String() string { func (r lineRange) GoString() string { return fmt.Sprintf("(%d, %d)", r.first, r.last) } - -func newErrWriter(w io.Writer) *errwriter { - return &errwriter{w: w} -} - -type errwriter struct { - w io.Writer - err error - wrote int - attempted int -} - -func (w *errwriter) Write(b []byte) (int, error) { - w.attempted += len(b) - if w.err != nil { - return 0, w.err // TODO: use something like errors.Wrap(w.err)? - } - n, err := w.w.Write(b) - if err != nil { - w.err = err - } - w.wrote += n - return n, err -} - -func (w *errwriter) WriteString(s string) { - // TODO: use w.w's WriteString method, if it exists - w.Write([]byte(s)) -} - -func (w *errwriter) WriteByte(b byte) { - // TODO: use w.w's WriteByte method, if it exists - w.Write([]byte{b}) -} - -func (w *errwriter) Error() error { return w.err } diff --git a/unified_test.go b/write/unified_test.go similarity index 92% rename from unified_test.go rename to write/unified_test.go index d693a78..b765864 100644 --- a/unified_test.go +++ b/write/unified_test.go @@ -1,4 +1,4 @@ -package diff_test +package write_test import ( "bytes" @@ -9,13 +9,14 @@ import ( "github.com/pkg/diff" "github.com/pkg/diff/ctxt" "github.com/pkg/diff/myers" + "github.com/pkg/diff/write" "github.com/sergi/go-diff/diffmatchpatch" ) var goldenTests = []struct { name string a, b string - opts []diff.WriteOpt + opts []write.Option want string // usually from running diff --unified and cleaning up the output }{ { @@ -59,7 +60,7 @@ var goldenTests = []struct { name: "WithTerminalColor", a: "1\n2\n2", b: "1\n3\n3", - opts: []diff.WriteOpt{diff.TerminalColor()}, + opts: []write.Option{write.TerminalColor()}, want: ` `[1:] + "\u001b[1m" + `--- a +++ b @@ -85,7 +86,7 @@ func TestGolden(t *testing.T) { e := myers.Diff(context.Background(), ab) e = ctxt.Size(e, 3) buf := new(bytes.Buffer) - diff.WriteUnified(e, buf, ab, test.opts...) + write.Unified(e, buf, ab, test.opts...) got := buf.String() if test.want != got { t.Logf("%q\n", test.want)