mirror of
https://github.com/dolthub/dolt.git
synced 2026-02-11 18:49:14 -06:00
noms-diff command
known issues: human readable encoding is too verbose in some cases hard/impossible to correlate changed objects in lists and sets context is never displayed not sure what to print in "path" when descending into diffs with sets and lists probably need to special case printing of long strings and blobs (and maybe diff strings and blobs)
This commit is contained in:
262
cmd/noms-diff/noms_diff.go
Normal file
262
cmd/noms-diff/noms_diff.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/attic-labs/noms/clients/go/flags"
|
||||
"github.com/attic-labs/noms/clients/go/util"
|
||||
"github.com/attic-labs/noms/types"
|
||||
"github.com/attic-labs/noms/util/outputpager"
|
||||
)
|
||||
|
||||
const (
|
||||
addPrefix = "+ "
|
||||
subPrefix = "- "
|
||||
)
|
||||
|
||||
var (
|
||||
showHelp = flag.Bool("help", false, "show help text")
|
||||
diffQ = diffQueue{}
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintln(os.Stderr, "Shows the difference between two objects\n")
|
||||
fmt.Fprintln(os.Stderr, "Usage: noms diff <object1> <object2>\n")
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, "\nSee \"Spelling Objects\" at https://github.com/attic-labs/noms/blob/master/doc/spelling.md for details on the object argument.\n\n")
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
if *showHelp {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
if len(flag.Args()) != 2 {
|
||||
util.CheckError(errors.New("expected exactly two arguments"))
|
||||
}
|
||||
|
||||
spec1, err := flags.ParsePathSpec(flag.Arg(0))
|
||||
util.CheckError(err)
|
||||
spec2, err := flags.ParsePathSpec(flag.Arg(1))
|
||||
util.CheckError(err)
|
||||
|
||||
db1, value1, err := spec1.Value()
|
||||
util.CheckError(err)
|
||||
defer db1.Close()
|
||||
|
||||
db2, value2, err := spec2.Value()
|
||||
util.CheckError(err)
|
||||
defer db2.Close()
|
||||
|
||||
di := diffInfo{
|
||||
path: types.NewPath().AddField("/"),
|
||||
key: nil,
|
||||
v1: value1,
|
||||
v2: value2,
|
||||
}
|
||||
diffQ.Push(di)
|
||||
|
||||
waitChan := outputpager.PageOutput(!*outputpager.NoPager)
|
||||
|
||||
diff(os.Stdout)
|
||||
fmt.Fprintf(os.Stdout, "\n")
|
||||
|
||||
if waitChan != nil {
|
||||
os.Stdout.Close()
|
||||
<-waitChan
|
||||
}
|
||||
}
|
||||
|
||||
func isPrimitive(v1 types.Value) bool {
|
||||
kind := v1.Type().Kind()
|
||||
return types.IsPrimitiveKind(kind) || kind == types.RefKind
|
||||
}
|
||||
|
||||
func canCompare(v1, v2 types.Value) bool {
|
||||
return !isPrimitive(v1) && v1.Type().Kind() == v1.Type().Kind()
|
||||
}
|
||||
|
||||
func diff(w io.Writer) {
|
||||
for di, ok := diffQ.Pop(); ok; di, ok = diffQ.Pop() {
|
||||
p, key, v1, v2 := di.path, di.key, di.v1, di.v2
|
||||
|
||||
v1.Type().Kind()
|
||||
if v1 == nil && v2 != nil {
|
||||
line(w, addPrefix, key, v2)
|
||||
}
|
||||
if v1 != nil && v2 == nil {
|
||||
line(w, subPrefix, key, v1)
|
||||
}
|
||||
if !v1.Equals(v2) {
|
||||
if !canCompare(v1, v2) {
|
||||
line(w, subPrefix, key, v1)
|
||||
line(w, addPrefix, key, v2)
|
||||
} else {
|
||||
switch v1.Type().Kind() {
|
||||
case types.ListKind:
|
||||
diffLists(w, p, v1.(types.List), v2.(types.List))
|
||||
case types.MapKind:
|
||||
diffMaps(w, p, v1.(types.Map), v2.(types.Map))
|
||||
case types.SetKind:
|
||||
diffSets(w, p, v1.(types.Set), v2.(types.Set))
|
||||
case types.StructKind:
|
||||
diffStructs(w, p, v1.(types.Struct), v2.(types.Struct))
|
||||
default:
|
||||
panic("Unrecognized type in diff function")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func diffLists(w io.Writer, p types.Path, v1, v2 types.List) {
|
||||
wroteHeader := false
|
||||
splices, _ := v2.Diff(v1)
|
||||
for _, splice := range splices {
|
||||
if splice.SpRemoved == splice.SpAdded {
|
||||
for i := uint64(0); i < splice.SpRemoved; i++ {
|
||||
lastEl := v1.Get(splice.SpAt + i)
|
||||
newEl := v2.Get(splice.SpFrom + i)
|
||||
if canCompare(lastEl, newEl) {
|
||||
idx := types.Number(splice.SpAt + i)
|
||||
p1 := p.AddIndex(idx)
|
||||
diffQ.Push(diffInfo{p1, idx, lastEl, newEl})
|
||||
} else {
|
||||
wroteHeader = writeHeader(w, wroteHeader, p)
|
||||
line(w, subPrefix, nil, v1.Get(splice.SpAt+i))
|
||||
line(w, addPrefix, nil, v2.Get(splice.SpFrom+i))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := uint64(0); i < splice.SpRemoved; i++ {
|
||||
wroteHeader = writeHeader(w, wroteHeader, p)
|
||||
line(w, subPrefix, nil, v1.Get(splice.SpAt+i))
|
||||
}
|
||||
for i := uint64(0); i < splice.SpAdded; i++ {
|
||||
wroteHeader = writeHeader(w, wroteHeader, p)
|
||||
line(w, addPrefix, nil, v2.Get(splice.SpFrom+i))
|
||||
}
|
||||
}
|
||||
}
|
||||
writeFooter(w, wroteHeader)
|
||||
}
|
||||
|
||||
func diffMaps(w io.Writer, p types.Path, v1, v2 types.Map) {
|
||||
wroteHeader := false
|
||||
|
||||
added, removed, modified := v2.Diff(v1)
|
||||
for _, k := range added {
|
||||
wroteHeader = writeHeader(w, wroteHeader, p)
|
||||
line(w, addPrefix, k, v2.Get(k))
|
||||
}
|
||||
for _, k := range removed {
|
||||
wroteHeader = writeHeader(w, wroteHeader, p)
|
||||
line(w, subPrefix, k, v1.Get(k))
|
||||
}
|
||||
for _, k := range modified {
|
||||
c1, c2 := v1.Get(k), v2.Get(k)
|
||||
if canCompare(c1, c2) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
types.WriteEncodedValueWithTags(buf, k)
|
||||
p1 := p.AddField(buf.String())
|
||||
diffQ.Push(diffInfo{path: p1, key: k, v1: c1, v2: c2})
|
||||
} else {
|
||||
wroteHeader = writeHeader(w, wroteHeader, p)
|
||||
line(w, subPrefix, k, v1.Get(k))
|
||||
line(w, addPrefix, k, v2.Get(k))
|
||||
}
|
||||
}
|
||||
writeFooter(w, wroteHeader)
|
||||
}
|
||||
|
||||
func diffStructs(w io.Writer, p types.Path, v1, v2 types.Struct) {
|
||||
changed := types.StructDiff(v1, v2)
|
||||
wroteHeader := false
|
||||
for _, field := range changed {
|
||||
f1 := v1.Get(field)
|
||||
f2 := v2.Get(field)
|
||||
if canCompare(f1, f2) {
|
||||
p1 := p.AddField(field)
|
||||
diffQ.Push(diffInfo{path: p1, key: types.NewString(field), v1: f1, v2: f2})
|
||||
} else {
|
||||
wroteHeader = writeHeader(w, wroteHeader, p)
|
||||
line(w, subPrefix, types.NewString(field), f1)
|
||||
line(w, addPrefix, types.NewString(field), f2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func diffSets(w io.Writer, p types.Path, v1, v2 types.Set) {
|
||||
wroteHeader := false
|
||||
added, removed := v2.Diff(v1)
|
||||
if len(added) == 1 && len(removed) == 1 && canCompare(added[0], removed[0]) {
|
||||
p1 := p.AddField(added[0].Hash().String())
|
||||
diffQ.Push(diffInfo{path: p1, key: types.NewString(""), v1: removed[0], v2: added[0]})
|
||||
} else {
|
||||
for _, value := range removed {
|
||||
wroteHeader = writeHeader(w, wroteHeader, p)
|
||||
line(w, subPrefix, nil, value)
|
||||
}
|
||||
for _, value := range added {
|
||||
wroteHeader = writeHeader(w, wroteHeader, p)
|
||||
line(w, addPrefix, nil, value)
|
||||
}
|
||||
}
|
||||
writeFooter(w, wroteHeader)
|
||||
}
|
||||
|
||||
type prefixWriter struct {
|
||||
w io.Writer
|
||||
prefix []byte
|
||||
}
|
||||
|
||||
// todo: Not sure if we want to use a writer to do this for the longterm but, if so, we can
|
||||
// probably do better than writing byte by byte
|
||||
func (pw prefixWriter) Write(bytes []byte) (n int, err error) {
|
||||
for i, b := range bytes {
|
||||
_, err = pw.w.Write([]byte{b})
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
if b == '\n' {
|
||||
_, err := pw.w.Write(pw.prefix)
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(bytes), nil
|
||||
}
|
||||
|
||||
func line(w io.Writer, start string, key, v2 types.Value) {
|
||||
pw := prefixWriter{w: w, prefix: []byte(start)}
|
||||
w.Write([]byte(start))
|
||||
if key != nil {
|
||||
types.WriteEncodedValueWithTags(pw, key)
|
||||
w.Write([]byte(": "))
|
||||
}
|
||||
types.WriteEncodedValueWithTags(pw, v2)
|
||||
w.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
func writeHeader(w io.Writer, wroteHeader bool, p types.Path) bool {
|
||||
if !wroteHeader {
|
||||
w.Write([]byte(p.String()))
|
||||
w.Write([]byte(" {\n"))
|
||||
wroteHeader = true
|
||||
}
|
||||
return wroteHeader
|
||||
}
|
||||
|
||||
func writeFooter(w io.Writer, wroteHeader bool) {
|
||||
if wroteHeader {
|
||||
w.Write([]byte(" }\n"))
|
||||
}
|
||||
}
|
||||
141
cmd/noms-diff/noms_diff_test.go
Normal file
141
cmd/noms-diff/noms_diff_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/attic-labs/noms/types"
|
||||
"github.com/attic-labs/testify/assert"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
var (
|
||||
aa1 = createMap("a1", "a-one", "a2", "a-two", "a3", "a-three", "a4", "a-four")
|
||||
aa1x = createMap("a1", "a-one-diff", "a2", "a-two", "a3", "a-three", "a4", "a-four")
|
||||
|
||||
mm1 = createMap("k1", "k-one", "k2", "k-two", "k3", "k-three", "k4", aa1)
|
||||
mm2 = createMap("l1", "l-one", "l2", "l-two", "l3", "l-three", "l4", aa1)
|
||||
mm3 = createMap("m1", "m-one", "v2", "m-two", "m3", "m-three", "m4", aa1)
|
||||
mm3x = createMap("m1", "m-one", "v2", "m-two", "m3", "m-three-diff", "m4", aa1x)
|
||||
mm4 = createMap("n1", "n-one", "n2", "n-two", "n3", "n-three", "n4", aa1)
|
||||
|
||||
startPath = types.NewPath().AddField("/")
|
||||
)
|
||||
|
||||
func valToTypesValue(v interface{}) types.Value {
|
||||
var v1 types.Value
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
v1 = types.NewString(t)
|
||||
case int:
|
||||
v1 = types.Number(t)
|
||||
case types.Value:
|
||||
v1 = t
|
||||
}
|
||||
return v1
|
||||
}
|
||||
|
||||
func valsToTypesValues(kv ...interface{}) []types.Value {
|
||||
keyValues := []types.Value{}
|
||||
for _, e := range kv {
|
||||
v := valToTypesValue(e)
|
||||
keyValues = append(keyValues, v)
|
||||
}
|
||||
return keyValues
|
||||
}
|
||||
|
||||
func createMap(kv ...interface{}) types.Map {
|
||||
keyValues := valsToTypesValues(kv...)
|
||||
return types.NewMap(keyValues...)
|
||||
}
|
||||
|
||||
func createSet(kv ...interface{}) types.Set {
|
||||
keyValues := valsToTypesValues(kv...)
|
||||
return types.NewSet(keyValues...)
|
||||
}
|
||||
|
||||
func createList(kv ...interface{}) types.List {
|
||||
keyValues := valsToTypesValues(kv...)
|
||||
return types.NewList(keyValues...)
|
||||
}
|
||||
|
||||
func createStruct(name string, kv ...interface{}) types.Struct {
|
||||
fields := map[string]types.Value{}
|
||||
for i := 0; i < len(kv); i += 2 {
|
||||
fields[kv[i].(string)] = valToTypesValue(kv[i+1])
|
||||
}
|
||||
return types.NewStruct(name, fields)
|
||||
}
|
||||
|
||||
func TestNomsMapdiff(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
expected := "./.\"map-3\" {\n- \"m3\": \"m-three\"\n+ \"m3\": \"m-three-diff\"\n }\n./.\"map-3\".\"m4\" {\n- \"a1\": \"a-one\"\n+ \"a1\": \"a-one-diff\"\n }\n"
|
||||
|
||||
m1 := createMap("map-1", mm1, "map-2", mm2, "map-3", mm3, "map-4", mm4)
|
||||
m2 := createMap("map-1", mm1, "map-2", mm2, "map-3", mm3x, "map-4", mm4)
|
||||
diffQ.Push(diffInfo{path: startPath, key: nil, v1: m1, v2: m2})
|
||||
buf := util.NewBuffer(nil)
|
||||
diff(buf)
|
||||
assert.Equal(expected, buf.String())
|
||||
}
|
||||
|
||||
func TestNomsSetDiff(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
expected := "./.sha1-c26be7ea6e815f747c1552fe402a773ad466be88 {\n- \"m3\": \"m-three\"\n+ \"m3\": \"m-three-diff\"\n }\n./.sha1-c26be7ea6e815f747c1552fe402a773ad466be88.\"m4\" {\n- \"a1\": \"a-one\"\n+ \"a1\": \"a-one-diff\"\n }\n"
|
||||
|
||||
s1 := createSet("one", "three", "five", "seven", "nine")
|
||||
s2 := createSet("one", "three", "five-diff", "seven", "nine")
|
||||
diffQ.Push(diffInfo{path: startPath, key: nil, v1: s1, v2: s2})
|
||||
diff(os.Stdout)
|
||||
|
||||
s1 = createSet(mm1, mm2, mm3, mm4)
|
||||
s2 = createSet(mm1, mm2, mm3x, mm4)
|
||||
diffQ.Push(diffInfo{path: startPath, key: nil, v1: s1, v2: s2})
|
||||
buf := util.NewBuffer(nil)
|
||||
diff(buf)
|
||||
assert.Equal(expected, buf.String())
|
||||
}
|
||||
|
||||
func TestNomsStructDiff(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
expected := "./ {\n- \"four\": \"four\"\n+ \"four\": \"four-diff\"\n }\n./.\"three\" {\n- \"field3\": \"field3-data\"\n+ \"field3\": \"field3-data-diff\"\n"
|
||||
|
||||
fieldData := []interface{}{
|
||||
"field1", "field1-data",
|
||||
"field2", "field2-data",
|
||||
"field3", "field3-data",
|
||||
"field4", "field4-data",
|
||||
}
|
||||
s1 := createStruct("TestData", fieldData...)
|
||||
s2 := s1.Set("field3", types.NewString("field3-data-diff"))
|
||||
|
||||
m1 := createMap("one", 1, "two", 2, "three", s1, "four", "four")
|
||||
m2 := createMap("one", 1, "two", 2, "three", s2, "four", "four-diff")
|
||||
|
||||
diffQ.Push(diffInfo{path: startPath, key: nil, v1: m1, v2: m2})
|
||||
buf := util.NewBuffer(nil)
|
||||
diff(buf)
|
||||
assert.Equal(expected, buf.String())
|
||||
}
|
||||
|
||||
func TestNomsListDiff(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
expected := "./[2] {\n- \"m3\": \"m-three\"\n+ \"m3\": \"m-three-diff\"\n }\n./[2].\"m4\" {\n- \"a1\": \"a-one\"\n+ \"a1\": \"a-one-diff\"\n }\n"
|
||||
|
||||
l1 := createList(1, 2, 3, 4, 44, 5, 6)
|
||||
l2 := createList(1, 22, 3, 4, 5, 6)
|
||||
diffQ.Push(diffInfo{path: startPath, key: nil, v1: l1, v2: l2})
|
||||
diff(os.Stdout)
|
||||
|
||||
l1 = createList("one", "two", "three", "four", "five", "six")
|
||||
l2 = createList("one", "two", "three", "four", "five", "six", "seven")
|
||||
diffQ.Push(diffInfo{path: startPath, key: nil, v1: l1, v2: l2})
|
||||
diff(os.Stdout)
|
||||
|
||||
l1 = createList(mm1, mm2, mm3, mm4)
|
||||
l2 = createList(mm1, mm2, mm3x, mm4)
|
||||
diffQ.Push(diffInfo{path: startPath, key: nil, v1: l1, v2: l2})
|
||||
buf := util.NewBuffer(nil)
|
||||
diff(buf)
|
||||
assert.Equal(expected, buf.String())
|
||||
}
|
||||
50
cmd/noms-diff/queue.go
Normal file
50
cmd/noms-diff/queue.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/attic-labs/noms/types"
|
||||
)
|
||||
|
||||
type diffInfo struct {
|
||||
path types.Path
|
||||
key types.Value
|
||||
v1 types.Value
|
||||
v2 types.Value
|
||||
}
|
||||
|
||||
type queueNode struct {
|
||||
value diffInfo
|
||||
next *queueNode
|
||||
}
|
||||
|
||||
type diffQueue struct {
|
||||
head *queueNode
|
||||
tail *queueNode
|
||||
len int
|
||||
}
|
||||
|
||||
func (q *diffQueue) Push(node diffInfo) {
|
||||
qn := queueNode{value: node}
|
||||
q.len += 1
|
||||
if q.head == nil {
|
||||
q.head = &qn
|
||||
q.tail = &qn
|
||||
return
|
||||
}
|
||||
q.tail.next = &qn
|
||||
q.tail = &qn
|
||||
}
|
||||
|
||||
func (q *diffQueue) Pop() (diffInfo, bool) {
|
||||
if q.head == nil {
|
||||
return diffInfo{}, false
|
||||
}
|
||||
q.len -= 1
|
||||
qn := q.head
|
||||
if q.head == q.tail {
|
||||
q.head = nil
|
||||
q.tail = nil
|
||||
}
|
||||
|
||||
q.head = qn.next
|
||||
return qn.value, true
|
||||
}
|
||||
30
cmd/noms-diff/queue_test.go
Normal file
30
cmd/noms-diff/queue_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/attic-labs/noms/types"
|
||||
"github.com/attic-labs/testify/assert"
|
||||
)
|
||||
|
||||
func TestQueue(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
const testSize = 4
|
||||
dq := diffQueue{}
|
||||
|
||||
for i := 1; i <= testSize; i++ {
|
||||
dq.Push(diffInfo{key: types.Number(i)})
|
||||
assert.Equal(i, dq.len)
|
||||
}
|
||||
|
||||
for i := 1; i <= testSize; i++ {
|
||||
di, ok := dq.Pop()
|
||||
assert.True(ok)
|
||||
assert.Equal(di.key.(types.Number).ToPrimitive().(float64), float64(i))
|
||||
assert.Equal(testSize-i, dq.len)
|
||||
}
|
||||
|
||||
_, ok := dq.Pop()
|
||||
assert.False(ok)
|
||||
assert.Equal(diffQueue{}, dq)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func TestPathMap(t *testing.T) {
|
||||
assertPathResolvesTo(assert, nil, v, NewPath().AddIndex(Number(4)))
|
||||
}
|
||||
|
||||
func TestPathMutli(t *testing.T) {
|
||||
func TestPathMulti(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m1 := NewMap(
|
||||
|
||||
Reference in New Issue
Block a user