Files
dolt/cmd/noms-diff/diff/diff.go
2016-06-09 13:45:12 -07:00

222 lines
5.8 KiB
Go

// Copyright 2016 The Noms Authors. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package diff
import (
"bytes"
"io"
"github.com/attic-labs/noms/go/d"
"github.com/attic-labs/noms/go/types"
)
const (
addPrefix = "+ "
subPrefix = "- "
)
func isPrimitiveOrRef(v1 types.Value) bool {
kind := v1.Type().Kind()
return types.IsPrimitiveKind(kind) || kind == types.RefKind
}
func canCompare(v1, v2 types.Value) bool {
return !isPrimitiveOrRef(v1) && v1.Type().Kind() == v2.Type().Kind()
}
func Diff(w io.Writer, v1, v2 types.Value) (err error) {
dq := NewDiffQueue()
di := diffInfo{path: types.NewPath().AddField("/"), v1: v1, v2: v2}
dq.PushBack(di)
err = d.Try(func() {
for di, ok := dq.PopFront(); ok; di, ok = dq.PopFront() {
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(dq, w, p, v1.(types.List), v2.(types.List))
case types.MapKind:
diffMaps(dq, w, p, v1.(types.Map), v2.(types.Map))
case types.SetKind:
diffSets(dq, w, p, v1.(types.Set), v2.(types.Set))
case types.StructKind:
diffStructs(dq, w, p, v1.(types.Struct), v2.(types.Struct))
default:
panic("Unrecognized type in diff function")
}
}
}
}
})
return
}
func diffLists(dq *diffQueue, 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)
dq.PushBack(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(dq *diffQueue, 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)
d.Exp.NoError(types.WriteEncodedValueWithTags(buf, k))
p1 := p.AddField(buf.String())
dq.PushBack(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(dq *diffQueue, 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)
dq.PushBack(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(dq *diffQueue, 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())
dq.PushBack(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)
return
}
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) {
var err error
pw := prefixWriter{w: w, prefix: []byte(start)}
_, err = w.Write([]byte(start))
d.Exp.NoError(err)
if key != nil {
d.Exp.NoError(types.WriteEncodedValueWithTags(pw, key))
_, err = w.Write([]byte(": "))
d.Exp.NoError(err)
}
d.Exp.NoError(types.WriteEncodedValueWithTags(pw, v2))
_, err = w.Write([]byte("\n"))
d.Exp.NoError(err)
}
func writeHeader(w io.Writer, wroteHeader bool, p types.Path) bool {
var err error
if !wroteHeader {
_, err = w.Write([]byte(p.String()))
d.Exp.NoError(err)
_, err = w.Write([]byte(" {\n"))
d.Exp.NoError(err)
wroteHeader = true
}
return wroteHeader
}
func writeFooter(w io.Writer, wroteHeader bool) {
if wroteHeader {
_, err := w.Write([]byte(" }\n"))
d.Exp.NoError(err)
}
}