diff --git a/CHANGELOG.md b/CHANGELOG.md index f4c14d8..bb053a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Most recent version is listed first. +# v0.0.18 +- Add ability to diff items: https://github.com/komuw/kama/pull/67 + # v0.0.17 - Add ability to dump items with circular references: https://github.com/komuw/kama/pull/66 diff --git a/README.md b/README.md index 74f61c0..a822767 100644 --- a/README.md +++ b/README.md @@ -216,3 +216,4 @@ go test -race ./... -count=1 5. https://github.com/alecthomas/repr 6. https://github.com/k0kubun/pp 7. https://github.com/jba/printsrc +8. https://github.com/kylelemons/godebug diff --git a/diff.go b/diff.go new file mode 100644 index 0000000..0ac5f95 --- /dev/null +++ b/diff.go @@ -0,0 +1,82 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package diff provides basic text comparison (like Unix's diff(1)). +package kama + +import ( + "fmt" + "strings" +) + +// Most of the code here is insipired by(or taken from): +// (a) https://github.com/rsc/diff whose license(BSD 3-Clause "New" or "Revised" License) can be found here: https://github.com/rsc/diff/blob/master/LICENSE + +// diff returns a formatted diff of the two texts, +// showing the entire text and the minimum line-level +// additions and removals to turn text1 into text2. +// (That is, lines only in text1 appear with a leading -, +// and lines only in text2 appear with a leading +.) +func diff(text1, text2 string) string { + if text1 != "" && !strings.HasSuffix(text1, "\n") { + text1 += "(missing final newline)" + } + lines1 := strings.Split(text1, "\n") + lines1 = lines1[:len(lines1)-1] // remove empty string after final line + if text2 != "" && !strings.HasSuffix(text2, "\n") { + text2 += "(missing final newline)" + } + lines2 := strings.Split(text2, "\n") + lines2 = lines2[:len(lines2)-1] // remove empty string after final line + + // Naive dynamic programming algorithm for edit distance. + // https://en.wikipedia.org/wiki/Wagner–Fischer_algorithm + // dist[i][j] = edit distance between lines1[:len(lines1)-i] and lines2[:len(lines2)-j] + // (The reversed indices make following the minimum cost path + // visit lines in the same order as in the text.) + dist := make([][]int, len(lines1)+1) + for i := range dist { + dist[i] = make([]int, len(lines2)+1) + if i == 0 { + for j := range dist[0] { + dist[0][j] = j + } + continue + } + for j := range dist[i] { + if j == 0 { + dist[i][0] = i + continue + } + cost := dist[i][j-1] + 1 + if cost > dist[i-1][j]+1 { + cost = dist[i-1][j] + 1 + } + if lines1[len(lines1)-i] == lines2[len(lines2)-j] { + if cost > dist[i-1][j-1] { + cost = dist[i-1][j-1] + } + } + dist[i][j] = cost + } + } + + var buf strings.Builder + i, j := len(lines1), len(lines2) + for i > 0 || j > 0 { + cost := dist[i][j] + if i > 0 && j > 0 && cost == dist[i-1][j-1] && lines1[len(lines1)-i] == lines2[len(lines2)-j] { + fmt.Fprintf(&buf, " %s\n", lines1[len(lines1)-i]) + i-- + j-- + } else if i > 0 && cost == dist[i-1][j]+1 { + fmt.Fprintf(&buf, "-%s\n", lines1[len(lines1)-i]) + i-- + } else { + fmt.Fprintf(&buf, "+%s\n", lines2[len(lines2)-j]) + j-- + } + } + return buf.String() +} diff --git a/diff_test.go b/diff_test.go new file mode 100644 index 0000000..06d4c5c --- /dev/null +++ b/diff_test.go @@ -0,0 +1,45 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package kama + +import ( + "strings" + "testing" +) + +func TestSmallDiff(t *testing.T) { + t.Parallel() + + tests := []struct { + text1 string + text2 string + diff string + }{ + {"a b c", "a b d e f", "a b -c +d +e +f"}, + {"", "a b c", "+a +b +c"}, + {"a b c", "", "-a -b -c"}, + {"a b c", "d e f", "-a -b -c +d +e +f"}, + {"a b c d e f", "a b d e f", "a b -c d e f"}, + {"a b c e f", "a b c d e f", "a b c +d e f"}, + } + + for _, tt := range tests { + // Turn spaces into \n. + text1 := strings.ReplaceAll(tt.text1, " ", "\n") + if text1 != "" { + text1 += "\n" + } + text2 := strings.ReplaceAll(tt.text2, " ", "\n") + if text2 != "" { + text2 += "\n" + } + out := diff(text1, text2) + // Cut final \n, cut spaces, turn remaining \n into spaces. + out = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSuffix(out, "\n"), " ", ""), "\n", " ") + if out != tt.diff { + t.Errorf("diff(%q, %q) = %q, want %q", text1, text2, out, tt.diff) + } + } +} diff --git a/kama.go b/kama.go index 37e52de..0b4effa 100644 --- a/kama.go +++ b/kama.go @@ -135,3 +135,18 @@ func Dir(i interface{}, c ...Config) string { func Stackp() { stackp() } + +// Diffp prints a formatted diff showing the minimum line-level additions and removals that would turn old into new. +func Diffp(old, new interface{}, c ...Config) { + fmt.Println( + Diff(old, new, c...), + ) +} + +// Diff returns a formatted diff showing the minimum line-level additions and removals that would turn old into new. +func Diff(old, new interface{}, c ...Config) string { + return diff( + Dir(old, c...), + Dir(new, c...), + ) +} diff --git a/kama_test.go b/kama_test.go index f8f2506..75129f3 100644 --- a/kama_test.go +++ b/kama_test.go @@ -248,3 +248,43 @@ func TestReadmeExamples(t *testing.T) { }) } } + +func TestDiff(t *testing.T) { + t.Parallel() + + tt := []struct { + tName string + old interface{} + new interface{} + }{ + { + tName: "package compress/flate", + old: "compress/flate", + new: "compress/flate", + }, + { + tName: "errors", + old: "errors", + new: "github.com/pkg/errors", + }, + { + tName: "http Request", + old: http.Request{Method: "GET"}, + new: http.Request{Method: "POST"}, + }, + } + + for _, v := range tt { + v := v + tName := fmt.Sprintf("TestDiff-%s", v.tName) + + t.Run(tName, func(t *testing.T) { + t.Parallel() + + res := Diff(v.old, v.new) + + path := getDataPath(t, "kama_test.go", tName) + dealWithTestData(t, path, res) + }) + } +} diff --git a/testdata/kama_test/TestDiff-errors.txt b/testdata/kama_test/TestDiff-errors.txt new file mode 100644 index 0000000..bb14abf --- /dev/null +++ b/testdata/kama_test/TestDiff-errors.txt @@ -0,0 +1,29 @@ + + [ +-NAME: errors ++NAME: github.com/pkg/errors + CONSTANTS: [] + VARIABLES: [] + FUNCTIONS: [ +- As(err error, target any) bool ++ As(err error, target interface{}) bool ++ Cause(err error) error ++ Errorf(format string, args ...interface{}) error + Is(err error, target error) bool +- Join(errs ...error) error +- New(text string) error ++ New(message string) error + Unwrap(err error) error ++ WithMessage(err error, message string) error ++ WithMessagef(err error, format string, args ...interface{}) error ++ WithStack(err error) error ++ Wrap(err error, message string) error ++ Wrapf(err error, format string, args ...interface{}) error + ] +-TYPES: [] ++TYPES: [ ++ Frame uintptr ++ (Frame) Format(s fmt.State, verb rune) ++ (Frame) MarshalText() ([]byte, error) ++ StackTrace []Frame ++ (StackTrace) Format(s fmt.State, verb rune)] diff --git a/testdata/kama_test/TestDiff-http_Request.txt b/testdata/kama_test/TestDiff-http_Request.txt new file mode 100644 index 0000000..7b1695d --- /dev/null +++ b/testdata/kama_test/TestDiff-http_Request.txt @@ -0,0 +1,76 @@ + + [ + NAME: net/http.Request + KIND: struct + SIGNATURE: [http.Request *http.Request] + FIELDS: [ + Method string + URL *url.URL + Proto string + ProtoMajor int + ProtoMinor int + Header http.Header + Body io.ReadCloser + GetBody func() (io.ReadCloser, error) + ContentLength int64 + TransferEncoding []string + Close bool + Host string + Form url.Values + PostForm url.Values + MultipartForm *multipart.Form + Trailer http.Header + RemoteAddr string + RequestURI string + TLS *tls.ConnectionState + Cancel <-chan struct {} + Response *http.Response + ] + METHODS: [ + AddCookie func(*http.Request, *http.Cookie) + BasicAuth func(*http.Request) (string, string, bool) + Clone func(*http.Request, context.Context) *http.Request + Context func(*http.Request) context.Context + Cookie func(*http.Request, string) (*http.Cookie, error) + Cookies func(*http.Request) []*http.Cookie + FormFile func(*http.Request, string) (multipart.File, *multipart.FileHeader, error) + FormValue func(*http.Request, string) string + MultipartReader func(*http.Request) (*multipart.Reader, error) + ParseForm func(*http.Request) error + ParseMultipartForm func(*http.Request, int64) error + PathValue func(*http.Request, string) string + PostFormValue func(*http.Request, string) string + ProtoAtLeast func(*http.Request, int, int) bool + Referer func(*http.Request) string + SetBasicAuth func(*http.Request, string, string) + SetPathValue func(*http.Request, string, string) + UserAgent func(*http.Request) string + WithContext func(*http.Request, context.Context) *http.Request + Write func(*http.Request, io.Writer) error + WriteProxy func(*http.Request, io.Writer) error + ] + SNIPPET: Request{ +- Method: "GET", ++ Method: "POST", + URL: *url.URL(nil), + Proto: "", + ProtoMajor: int(0), + ProtoMinor: int(0), + Header: http.Header{(nil)}, + Body: io.ReadCloser nil, + GetBody: func() (io.ReadCloser, error), + ContentLength: int64(0), + TransferEncoding: []string{(nil)}, + Close: false, + Host: "", + Form: url.Values{(nil)}, + PostForm: url.Values{(nil)}, + MultipartForm: *multipart.Form(nil), + Trailer: http.Header{(nil)}, + RemoteAddr: "", + RequestURI: "", + TLS: *tls.ConnectionState(nil), + Cancel: <-chan struct {} (len=0, cap=0), + Response: *http.Response(nil), + } + ] diff --git a/testdata/kama_test/TestDiff-package_compress_flate.txt b/testdata/kama_test/TestDiff-package_compress_flate.txt new file mode 100644 index 0000000..fe48bbd --- /dev/null +++ b/testdata/kama_test/TestDiff-package_compress_flate.txt @@ -0,0 +1,36 @@ + + [ + NAME: compress/flate + CONSTANTS: [ + BestCompression untyped int + BestSpeed untyped int + DefaultCompression untyped int + HuffmanOnly untyped int + NoCompression untyped int + ] + VARIABLES: [] + FUNCTIONS: [ + NewReader(r io.Reader) io.ReadCloser + NewReaderDict(r io.Reader, dict []byte) io.ReadCloser + NewWriter(w io.Writer, level int) (*Writer, error) + NewWriterDict(w io.Writer, level int, dict []byte) (*Writer, error) + ] + TYPES: [ + CorruptInputError int64 + (CorruptInputError) Error() string + InternalError string + (InternalError) Error() string + ReadError struct + (*ReadError) Error() string + Reader interface + (Reader) Read(p []byte) (n int, err error) + (Reader) ReadByte() (byte, error) + Resetter interface + (Resetter) Reset(r io.Reader, dict []byte) error + WriteError struct + (*WriteError) Error() string + Writer struct + (*Writer) Close() error + (*Writer) Flush() error + (*Writer) Reset(dst io.Writer) + (*Writer) Write(data []byte) (n int, err error)]