internal/diff: first stab add diffing functionality
for supporting various commands:
- cue test
- cue diff/snapshot
Change-Id: I9ddccb0622530db9759cd25768656b712268d3c5
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3981
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/internal/diff/print.go b/internal/diff/print.go
new file mode 100644
index 0000000..ebe2d10
--- /dev/null
+++ b/internal/diff/print.go
@@ -0,0 +1,271 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package diff
+
+import (
+ "fmt"
+ "io"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/format"
+)
+
+// Print the differences between two structs represented by an edit script.
+func Print(w io.Writer, es *EditScript) error {
+ p := printer{
+ w: w,
+ margin: 2,
+ context: 2,
+ }
+ p.script(es)
+ return p.errs
+}
+
+type printer struct {
+ w io.Writer
+ context int
+ margin int
+ indent int
+ prefix string
+ hasPrefix bool
+ hasPrint bool
+ errs errors.Error
+}
+
+func (p *printer) writeRaw(b []byte) {
+ if len(b) == 0 {
+ return
+ }
+ if !p.hasPrefix {
+ io.WriteString(p.w, p.prefix)
+ p.hasPrefix = true
+ }
+ if !p.hasPrint {
+ fmt.Fprintf(p.w, "% [1]*s", p.indent+p.margin-len(p.prefix), "")
+ p.hasPrint = true
+ }
+ p.w.Write(b)
+}
+
+func (p *printer) Write(b []byte) (n int, err error) {
+ i, last := 0, 0
+ for ; i < len(b); i++ {
+ if b[i] != '\n' {
+ continue
+ }
+ p.writeRaw(b[last:i])
+ last = i + 1
+ io.WriteString(p.w, "\n")
+ p.hasPrefix = false
+ p.hasPrint = false
+ }
+ p.writeRaw(b[last:])
+ return len(b), nil
+}
+
+func (p *printer) write(b []byte) {
+ _, _ = p.Write(b)
+}
+
+func (p *printer) printLen(align int, str string) {
+ fmt.Fprintf(p, "% -[1]*s", align, str)
+}
+
+func (p *printer) println(s string) {
+ fmt.Fprintln(p, s)
+}
+
+func (p *printer) printf(format string, args ...interface{}) {
+ fmt.Fprintf(p, format, args...)
+}
+
+func (p *printer) script(e *EditScript) {
+ switch e.x.Kind() {
+ case cue.StructKind:
+ p.printStruct(e)
+ case cue.ListKind:
+ p.printList(e)
+ default:
+ p.printf("BadExpr")
+ }
+}
+
+func (p *printer) findRun(es *EditScript, i int) (start, end int) {
+ lastEnd := i
+
+ for ; i < es.Len() && es.edits[i].kind == Identity; i++ {
+ }
+ start = i
+
+ // Find end of run
+ include := p.context
+ for ; i < es.Len(); i++ {
+ e := es.edits[i]
+ if e.kind != Identity {
+ include = p.context + 1
+ continue
+ }
+ if include--; include == 0 {
+ break
+ }
+ }
+
+ if i-start > 0 {
+ // Adjust start of run
+ if s := start - p.context; s > lastEnd {
+ start = s
+ } else {
+ start = lastEnd
+ }
+ }
+ return start, i
+}
+
+func (p *printer) printStruct(es *EditScript) {
+ // TODO: consider not printing outer curlies, or make it an option.
+ // if p.indent > 0 {
+ p.println("{")
+ defer p.println("}")
+ // }
+ p.indent += 4
+ defer func() {
+ p.indent -= 4
+ }()
+
+ var start, i int
+ for i < es.Len() {
+ lastEnd := i
+ // Find provisional start of run.
+ start, i = p.findRun(es, i)
+
+ p.printSkipped(start - lastEnd)
+ p.printFieldRun(es, start, i)
+ }
+ p.printSkipped(es.Len() - i)
+}
+
+func (p *printer) printList(es *EditScript) {
+ p.println("[")
+ p.indent += 4
+ defer func() {
+ p.indent -= 4
+ p.println("]")
+ }()
+
+ x := getElems(es.x)
+ y := getElems(es.y)
+
+ var start, i int
+ for i < es.Len() {
+ lastEnd := i
+ // Find provisional start of run.
+ start, i = p.findRun(es, i)
+
+ p.printSkipped(start - lastEnd)
+ p.printElemRun(es, x, y, start, i)
+ }
+ p.printSkipped(es.Len() - i)
+}
+
+func getElems(x cue.Value) (a []cue.Value) {
+ for i, _ := x.List(); i.Next(); {
+ a = append(a, i.Value())
+ }
+ return a
+}
+
+func (p *printer) printSkipped(n int) {
+ if n > 0 {
+ p.printf("... // %d identical elements\n", n)
+ }
+}
+
+func (p *printer) printValue(v cue.Value) {
+ // TODO: have indent option.
+ b, _ := format.Node(v.Syntax())
+ p.write(b)
+}
+
+func (p *printer) printFieldRun(es *EditScript, start, end int) {
+ // Determine max field len.
+ for i := start; i < end; i++ {
+ e := es.edits[i]
+
+ switch e.kind {
+ case UniqueX:
+ p.printField("-", es, es.LabelX(i), es.ValueX(i))
+
+ case UniqueY:
+ p.printField("+", es, es.LabelY(i), es.ValueY(i))
+
+ case Modified:
+ if e.sub != nil {
+ io.WriteString(p, es.LabelX(i))
+ io.WriteString(p, " ")
+ p.script(e.sub)
+ break
+ }
+ // TODO: show per-line differences for multiline strings.
+ p.printField("-", es, es.LabelX(i), es.ValueX(i))
+ p.printField("+", es, es.LabelY(i), es.ValueY(i))
+
+ case Identity:
+ // TODO: write on one line
+ p.printField("", es, es.LabelX(i), es.ValueX(i))
+ }
+ }
+}
+
+func (p *printer) printField(prefix string, es *EditScript, label string, v cue.Value) {
+ p.prefix = prefix
+ io.WriteString(p, label)
+ io.WriteString(p, " ")
+ p.printValue(v)
+ io.WriteString(p, "\n")
+ p.prefix = ""
+}
+
+func (p *printer) printElemRun(es *EditScript, x, y []cue.Value, start, end int) {
+ for _, e := range es.edits[start:end] {
+ switch e.kind {
+ case UniqueX:
+ p.printElem("-", x[e.XPos()])
+
+ case UniqueY:
+ p.printElem("+", y[e.YPos()])
+
+ case Modified:
+ if e.sub != nil {
+ p.script(e.sub)
+ break
+ }
+ // TODO: show per-line differences for multiline strings.
+ p.printElem("-", x[e.XPos()])
+ p.printElem("+", y[e.YPos()])
+
+ case Identity:
+ // TODO: write on one line
+ p.printElem("", x[e.XPos()])
+ }
+ }
+}
+
+func (p *printer) printElem(prefix string, v cue.Value) {
+ p.prefix = prefix
+ p.printValue(v)
+ io.WriteString(p, ",\n")
+ p.prefix = ""
+}