Merge pull request #1770 from willhite/log-diff

Log diff
This commit is contained in:
Dan Willhite
2016-06-10 11:06:29 -07:00
committed by GitHub
9 changed files with 401 additions and 305 deletions

221
cmd/noms-diff/diff/diff.go Normal file
View File

@@ -0,0 +1,221 @@
// 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)
}
}

View File

@@ -1,7 +1,10 @@
package main
// 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 (
"os"
"testing"
"github.com/attic-labs/noms/go/types"
@@ -18,8 +21,6 @@ var (
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 {
@@ -73,26 +74,26 @@ func TestNomsMapdiff(t *testing.T) {
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.PushBack(diffInfo{path: startPath, key: nil, v1: m1, v2: m2})
buf := util.NewBuffer(nil)
diff(buf)
Diff(buf, m1, m2)
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"
expected := "./ {\n- \"five\"\n+ \"five-diff\"\n }\n"
s1 := createSet("one", "three", "five", "seven", "nine")
s2 := createSet("one", "three", "five-diff", "seven", "nine")
diffQ.PushBack(diffInfo{path: startPath, key: nil, v1: s1, v2: s2})
diff(os.Stdout)
buf := util.NewBuffer(nil)
Diff(buf, s1, s2)
assert.Equal(expected, buf.String())
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(mm1, mm2, mm3, mm4)
s2 = createSet(mm1, mm2, mm3x, mm4)
diffQ.PushBack(diffInfo{path: startPath, key: nil, v1: s1, v2: s2})
buf := util.NewBuffer(nil)
diff(buf)
buf = util.NewBuffer(nil)
Diff(buf, s1, s2)
assert.Equal(expected, buf.String())
}
@@ -112,30 +113,32 @@ func TestNomsStructDiff(t *testing.T) {
m1 := createMap("one", 1, "two", 2, "three", s1, "four", "four")
m2 := createMap("one", 1, "two", 2, "three", s2, "four", "four-diff")
diffQ.PushBack(diffInfo{path: startPath, key: nil, v1: m1, v2: m2})
buf := util.NewBuffer(nil)
diff(buf)
Diff(buf, m1, m2)
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"
expected := "./ {\n- 2\n+ 22\n- 44\n }\n"
l1 := createList(1, 2, 3, 4, 44, 5, 6)
l2 := createList(1, 22, 3, 4, 5, 6)
diffQ.PushBack(diffInfo{path: startPath, key: nil, v1: l1, v2: l2})
diff(os.Stdout)
buf := util.NewBuffer(nil)
Diff(buf, l1, l2)
assert.Equal(expected, buf.String())
expected = "./ {\n+ \"seven\"\n }\n"
l1 = createList("one", "two", "three", "four", "five", "six")
l2 = createList("one", "two", "three", "four", "five", "six", "seven")
diffQ.PushBack(diffInfo{path: startPath, key: nil, v1: l1, v2: l2})
diff(os.Stdout)
buf = util.NewBuffer(nil)
Diff(buf, l1, l2)
assert.Equal(expected, buf.String())
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(mm1, mm2, mm3, mm4)
l2 = createList(mm1, mm2, mm3x, mm4)
diffQ.PushBack(diffInfo{path: startPath, key: nil, v1: l1, v2: l2})
buf := util.NewBuffer(nil)
diff(buf)
buf = util.NewBuffer(nil)
Diff(buf, l1, l2)
assert.Equal(expected, buf.String())
}

View File

@@ -1,4 +1,8 @@
package main
// 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 (
"container/list"

View File

@@ -1,4 +1,8 @@
package main
// 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 (
"testing"

View File

@@ -1,14 +1,16 @@
// 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 main
import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"os"
"github.com/attic-labs/noms/go/types"
"github.com/attic-labs/noms/cmd/noms-diff/diff"
"github.com/attic-labs/noms/go/util/outputpager"
"github.com/attic-labs/noms/samples/go/flags"
"github.com/attic-labs/noms/samples/go/util"
@@ -21,7 +23,6 @@ const (
var (
showHelp = flag.Bool("help", false, "show help text")
diffQ = NewDiffQueue()
)
func main() {
@@ -55,17 +56,9 @@ func main() {
util.CheckError(err)
defer db2.Close()
di := diffInfo{
path: types.NewPath().AddField("/"),
key: nil,
v1: value1,
v2: value2,
}
diffQ.PushBack(di)
waitChan := outputpager.PageOutput(!*outputpager.NoPager)
diff(os.Stdout)
diff.Diff(os.Stdout, value1, value2)
fmt.Fprintf(os.Stdout, "\n")
if waitChan != nil {
@@ -73,190 +66,3 @@ func main() {
<-waitChan
}
}
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) {
for di, ok := diffQ.PopFront(); ok; di, ok = diffQ.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(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.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(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.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(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.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(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.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)
}
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"))
}
}

View File

@@ -64,7 +64,7 @@ func (iter *CommitIterator) Next() (LogNode, bool) {
// Now that the branchlist has been adusted, check to see if there are branches with common
// ancestors that will be folded together on this commit's graph.
foldedCols := iter.branches.HighestBranchIndexes()
return LogNode{
node := LogNode{
cr: br.cr,
commit: br.commit,
startingColCount: startingColCount,
@@ -73,7 +73,8 @@ func (iter *CommitIterator) Next() (LogNode, bool) {
newCols: newCols,
foldedCols: foldedCols,
lastCommit: iter.branches.IsEmpty(),
}, true
}
return node, true
}
type LogNode struct {
@@ -88,7 +89,7 @@ type LogNode struct {
}
func (n LogNode) String() string {
return fmt.Sprintf("cr: %s, startingColCount: %d, endingColCount: %d, col: %d, newCols: %v, foldedCols: %v, expanding: %t, shrunk: %t, shrinking: %t", n.cr.TargetHash(), n.startingColCount, n.endingColCount, n.col, n.newCols, n.foldedCols, n.Expanding(), n.Shrunk(), n.Shrinking())
return fmt.Sprintf("cr: %s(%d), startingColCount: %d, endingColCount: %d, col: %d, newCols: %v, foldedCols: %v, expanding: %t, shrunk: %t, shrinking: %t", n.cr.TargetHash().String()[0:9], n.cr.Height(), n.startingColCount, n.endingColCount, n.col, n.newCols, n.foldedCols, n.Expanding(), n.Shrunk(), n.Shrinking())
}
// True if this commit's graph will expand to show an additional branch
@@ -112,7 +113,7 @@ type branch struct {
}
func (b branch) String() string {
return b.cr.TargetHash().String()
return fmt.Sprintf("%s(%d)", b.cr.TargetHash().String()[0:9], b.cr.Height())
}
type branchList []branch

View File

@@ -0,0 +1,63 @@
// 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 main
import (
"io"
)
var (
maxLinesError = MaxLinesError{"Maximum number of lines written"}
)
type MaxLinesError struct {
msg string
}
func (e MaxLinesError) Error() string { return e.msg }
type maxLineWriter struct {
numLines int
maxLines int
node LogNode
dest io.Writer
needsPrefix bool
}
func (w *maxLineWriter) writeMax(data []byte, enforceMax bool) (byteCnt int, err error) {
for _, b := range data {
if w.needsPrefix {
w.needsPrefix = false
w.numLines++
if *showGraph {
s := genGraph(w.node, w.numLines)
_, err = w.dest.Write([]byte(s))
}
if err != nil {
return
}
}
byteCnt++
if enforceMax && w.maxLines >= 0 && w.numLines >= w.maxLines {
err = maxLinesError
return
}
// TODO: This is not technically correct due to utf-8, but ... meh.
w.needsPrefix = b == byte('\n')
_, err = w.dest.Write(data[byteCnt-1 : byteCnt])
if err != nil {
return
}
}
return
}
func (w *maxLineWriter) Write(data []byte) (byteCnt int, err error) {
return w.writeMax(data, true)
}
func (w *maxLineWriter) forceWrite(data []byte) (byteCnt int, err error) {
return w.writeMax(data, false)
}

View File

@@ -13,6 +13,7 @@ import (
"os"
"strings"
"github.com/attic-labs/noms/cmd/noms-diff/diff"
"github.com/attic-labs/noms/go/datas"
"github.com/attic-labs/noms/go/types"
"github.com/attic-labs/noms/go/util/outputpager"
@@ -22,10 +23,9 @@ import (
)
var (
color, maxLines, maxCommits *int
showHelp, showGraph *bool
useColor = false
maxLinesError = errors.New("Maximum number of lines written")
color, maxLines, maxCommits *int
showHelp, showGraph, showValue *bool
useColor = false
)
func main() {
@@ -34,6 +34,7 @@ func main() {
maxCommits = flag.Int("n", 0, "max number of commits to display (0 for all commits)")
showHelp = flag.Bool("help", false, "show help text")
showGraph = flag.Bool("graph", false, "show ascii-based commit hierarcy on left side of output")
showValue = flag.Bool("show-value", false, "show commit value rather than diff information -- this is temporary")
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "Displays the history of a Noms dataset\n")
@@ -76,7 +77,7 @@ func main() {
*maxCommits = math.MaxInt32
}
for ln, ok := iter.Next(); ok && displayed < *maxCommits; ln, ok = iter.Next() {
if printCommit(ln) != nil {
if printCommit(ln, database) != nil {
break
}
displayed++
@@ -90,7 +91,7 @@ func main() {
// Prints the information for one commit in the log, including ascii graph on left side of commits if
// -graph arg is true.
func printCommit(node LogNode) (err error) {
func printCommit(node LogNode, db datas.Database) (err error) {
lineno := 0
doColor := func(s string) string { return s }
if useColor {
@@ -113,12 +114,13 @@ func printCommit(node LogNode) (err error) {
}
if *maxLines != 0 {
var n int
n, err = writeCommitLines(node, *maxLines, lineno, os.Stdout)
if *showValue {
n, err = writeCommitLines(node, *maxLines, lineno, os.Stdout)
} else {
n, err = writeDiffLines(node, db, *maxLines, lineno, os.Stdout)
}
lineno += n
}
if !node.lastCommit {
fmt.Printf("%s\n", genGraph(node, lineno))
}
return
}
@@ -175,65 +177,44 @@ func genGraph(node LogNode, lineno int) string {
return string(buf)
}
type maxLineWriter struct {
numLines int
maxLines int
node LogNode
dest io.Writer
first bool
}
func (w *maxLineWriter) Write(data []byte) (n int, err error) {
doGraph := func() error {
var err error
w.numLines++
if *showGraph {
s := genGraph(w.node, w.numLines)
_, err = w.dest.Write([]byte(s))
}
return err
}
if w.first && len(data) > 0 {
w.first = false
err = doGraph()
if err != nil {
return
}
}
for _, b := range data {
n++
if w.numLines == w.maxLines {
err = maxLinesError
return
}
// TODO: This is not technically correct due to utf-8, but ... meh.
newLine := b == byte('\n')
_, err = w.dest.Write(data[n-1 : n])
if err != nil {
return
}
if newLine {
err = doGraph()
}
if err != nil {
return
}
}
return
}
func writeCommitLines(node LogNode, maxLines, lineno int, w io.Writer) (int, error) {
out := &maxLineWriter{numLines: lineno, maxLines: maxLines, node: node, dest: w, first: true}
err := types.WriteEncodedValueWithTags(out, node.commit.Get(datas.ValueField))
if err == maxLinesError {
fmt.Fprint(w, "...")
out.numLines++
func writeCommitLines(node LogNode, maxLines, lineno int, w io.Writer) (lineCnt int, err error) {
mlw := &maxLineWriter{numLines: lineno, maxLines: maxLines, node: node, dest: w, needsPrefix: true}
err = types.WriteEncodedValueWithTags(mlw, node.commit.Get(datas.ValueField))
if err != nil {
mlw.forceWrite([]byte("..."))
mlw.numLines++
err = nil
}
fmt.Fprintln(w)
return out.numLines, err
mlw.forceWrite([]byte("\n"))
if !node.lastCommit {
mlw.forceWrite([]byte("\n"))
}
return mlw.numLines, err
}
func writeDiffLines(node LogNode, db datas.Database, maxLines, lineno int, w io.Writer) (lineCnt int, err error) {
mlw := &maxLineWriter{numLines: lineno, maxLines: maxLines, node: node, dest: w, needsPrefix: true}
parents := node.commit.Get(datas.ParentsField).(types.Set)
var parent types.Value = nil
if parents.Len() > 0 {
parent = parents.First()
}
if parent == nil {
_, err = fmt.Fprint(mlw, "\n")
return 1, err
}
parentCommit := parent.(types.Ref).TargetValue(db).(types.Struct)
err = diff.Diff(mlw, parentCommit.Get(datas.ValueField), node.commit.Get(datas.ValueField))
if err != nil {
mlw.forceWrite([]byte("...\n"))
mlw.numLines++
err = nil
}
if !node.lastCommit {
mlw.forceWrite([]byte("\n"))
}
return mlw.numLines, err
}
func shouldUseColor() bool {

View File

@@ -47,7 +47,6 @@ func testCommitInResults(s *nomsShowTestSuite, spec string, i int) {
ds, err = ds.Commit(types.Number(i))
s.NoError(err)
commit := ds.Head()
fmt.Printf("commit hash: %s, type: %s\n", commit.Hash(), commit.Type().Name())
ds.Database().Close()
s.Contains(s.Run(main, []string{spec}), commit.Hash().String())
}
@@ -168,7 +167,8 @@ func (s *nomsShowTestSuite) TestNomsGraph1() {
s.NoError(err)
b1.Database().Close()
s.Equal(graphRes1, s.Run(main, []string{"-graph", test_util.CreateValueSpecString("ldb", s.LdbDir, "b1")}))
s.Equal(graphRes1, s.Run(main, []string{"-graph", "-show-value=true", test_util.CreateValueSpecString("ldb", s.LdbDir, "b1")}))
s.Equal(diffRes1, s.Run(main, []string{"-graph", "-show-value=false", test_util.CreateValueSpecString("ldb", s.LdbDir, "b1")}))
}
func (s *nomsShowTestSuite) TestNomsGraph2() {
@@ -198,7 +198,8 @@ func (s *nomsShowTestSuite) TestNomsGraph2() {
s.NoError(err)
db.Close()
s.Equal(graphRes2, s.Run(main, []string{"-graph", test_util.CreateValueSpecString("ldb", s.LdbDir, "ba")}))
s.Equal(graphRes2, s.Run(main, []string{"-graph", "-show-value=true", test_util.CreateValueSpecString("ldb", s.LdbDir, "ba")}))
s.Equal(diffRes2, s.Run(main, []string{"-graph", "-show-value=false", test_util.CreateValueSpecString("ldb", s.LdbDir, "ba")}))
}
func (s *nomsShowTestSuite) TestNomsGraph3() {
@@ -238,7 +239,8 @@ func (s *nomsShowTestSuite) TestNomsGraph3() {
s.NoError(err)
db.Close()
s.Equal(graphRes3, s.Run(main, []string{"-graph", test_util.CreateValueSpecString("ldb", s.LdbDir, "w")}))
s.Equal(graphRes3, s.Run(main, []string{"-graph", "-show-value=true", test_util.CreateValueSpecString("ldb", s.LdbDir, "w")}))
s.Equal(diffRes3, s.Run(main, []string{"-graph", "-show-value=false", test_util.CreateValueSpecString("ldb", s.LdbDir, "w")}))
}
func (s *nomsShowTestSuite) TestTruncation() {
@@ -267,9 +269,14 @@ func (s *nomsShowTestSuite) TestTruncation() {
db.Close()
dsSpec := test_util.CreateValueSpecString("ldb", s.LdbDir, "truncate")
s.Equal(truncRes1, s.Run(main, []string{"-graph", dsSpec}))
s.Equal(truncRes2, s.Run(main, []string{"-graph", "-max-lines=-1", dsSpec}))
s.Equal(truncRes3, s.Run(main, []string{"-graph", "-max-lines=0", dsSpec}))
s.Equal(truncRes1, s.Run(main, []string{"-graph", "-show-value=true", dsSpec}))
s.Equal(diffTrunc1, s.Run(main, []string{"-graph", "-show-value=false", dsSpec}))
s.Equal(truncRes2, s.Run(main, []string{"-graph", "-show-value=true", "-max-lines=-1", dsSpec}))
s.Equal(diffTrunc2, s.Run(main, []string{"-graph", "-show-value=false", "-max-lines=-1", dsSpec}))
s.Equal(truncRes3, s.Run(main, []string{"-graph", "-show-value=true", "-max-lines=0", dsSpec}))
s.Equal(diffTrunc3, s.Run(main, []string{"-graph", "-show-value=false", "-max-lines=0", dsSpec}))
}
func TestBranchlistSplice(t *testing.T) {
@@ -297,14 +304,20 @@ func TestBranchlistSplice(t *testing.T) {
const (
graphRes1 = "* sha1-0b0a92f5515b5778194d07d8011b7c18bf9be178\n| Parent: sha1-14491727789ebd03ca0ddd5e3a1307ca3e2651dc\n| \"7\"\n| \n* sha1-14491727789ebd03ca0ddd5e3a1307ca3e2651dc\n| Parent: sha1-b5a8493d7be003673216bdc0ed275fbbacbb9b08\n| \"6\"\n| \n* sha1-b5a8493d7be003673216bdc0ed275fbbacbb9b08\n| Parent: sha1-2b837c2e2dd0e6ef015e4742bd969f9fa1641f38\n| \"5\"\n| \n* sha1-2b837c2e2dd0e6ef015e4742bd969f9fa1641f38\n|\\ Merge: sha1-1f622d4006278a63cc77e95c6efcaf83f46606c4 sha1-eca65988a4ade585b8502d8f9fc37a5a5da94ab2\n| | \"4\"\n| | \n| * sha1-eca65988a4ade585b8502d8f9fc37a5a5da94ab2\n| | Parent: sha1-890cf3b1d49228ace9d691a627131a2872e42eb4\n| | \"3.7\"\n| | \n| * sha1-890cf3b1d49228ace9d691a627131a2872e42eb4\n| |\\ Merge: sha1-4f72aeacef670bc6884a97de5a363b0674ccd008 sha1-fa78bedf601fc6d49ebf70865944dfccf5b5a133\n| | | \"3.5\"\n| | | \n| | * sha1-fa78bedf601fc6d49ebf70865944dfccf5b5a133\n| | | Parent: sha1-72f530e2ba8ba3168de8da8d15271cd856ac5c2b\n| | | \"3.1.7\"\n| | | \n| | * sha1-72f530e2ba8ba3168de8da8d15271cd856ac5c2b\n| | | Parent: sha1-dda04a898b2048558b4238a4f111364ff5c92ab8\n| | | \"3.1.5\"\n| | | \n* | | sha1-1f622d4006278a63cc77e95c6efcaf83f46606c4\n| | | Parent: sha1-e1583426c4201c7602b4aa1ae835915666d933f0\n| | | \"3.6\"\n| | | \n| | * sha1-dda04a898b2048558b4238a4f111364ff5c92ab8\n| | | Parent: sha1-4f72aeacef670bc6884a97de5a363b0674ccd008\n| | | \"3.1.3\"\n| | | \n* | | sha1-e1583426c4201c7602b4aa1ae835915666d933f0\n| |/ Parent: sha1-27290eda714ce5d47df05e8f77a0986647887e32\n| | \"3.2\"\n| | \n| * sha1-4f72aeacef670bc6884a97de5a363b0674ccd008\n|/ Parent: sha1-27290eda714ce5d47df05e8f77a0986647887e32\n| \"3.1\"\n| \n* sha1-27290eda714ce5d47df05e8f77a0986647887e32\n| Parent: sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| \"3\"\n| \n* sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| Parent: sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5\n| \"2\"\n| \n* sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5\n| Parent: None\n| \"1\"\n"
diffRes1 = "* sha1-0b0a92f5515b5778194d07d8011b7c18bf9be178\n| Parent: sha1-14491727789ebd03ca0ddd5e3a1307ca3e2651dc\n| - \"6\"\n| + \"7\"\n| \n* sha1-14491727789ebd03ca0ddd5e3a1307ca3e2651dc\n| Parent: sha1-b5a8493d7be003673216bdc0ed275fbbacbb9b08\n| - \"5\"\n| + \"6\"\n| \n* sha1-b5a8493d7be003673216bdc0ed275fbbacbb9b08\n| Parent: sha1-2b837c2e2dd0e6ef015e4742bd969f9fa1641f38\n| - \"4\"\n| + \"5\"\n| \n* sha1-2b837c2e2dd0e6ef015e4742bd969f9fa1641f38\n|\\ Merge: sha1-1f622d4006278a63cc77e95c6efcaf83f46606c4 sha1-eca65988a4ade585b8502d8f9fc37a5a5da94ab2\n| | - \"3.6\"\n| | + \"4\"\n| | \n| * sha1-eca65988a4ade585b8502d8f9fc37a5a5da94ab2\n| | Parent: sha1-890cf3b1d49228ace9d691a627131a2872e42eb4\n| | - \"3.5\"\n| | + \"3.7\"\n| | \n| * sha1-890cf3b1d49228ace9d691a627131a2872e42eb4\n| |\\ Merge: sha1-4f72aeacef670bc6884a97de5a363b0674ccd008 sha1-fa78bedf601fc6d49ebf70865944dfccf5b5a133\n| | | - \"3.1\"\n| | | + \"3.5\"\n| | | \n| | * sha1-fa78bedf601fc6d49ebf70865944dfccf5b5a133\n| | | Parent: sha1-72f530e2ba8ba3168de8da8d15271cd856ac5c2b\n| | | - \"3.1.5\"\n| | | + \"3.1.7\"\n| | | \n| | * sha1-72f530e2ba8ba3168de8da8d15271cd856ac5c2b\n| | | Parent: sha1-dda04a898b2048558b4238a4f111364ff5c92ab8\n| | | - \"3.1.3\"\n| | | + \"3.1.5\"\n| | | \n* | | sha1-1f622d4006278a63cc77e95c6efcaf83f46606c4\n| | | Parent: sha1-e1583426c4201c7602b4aa1ae835915666d933f0\n| | | - \"3.2\"\n| | | + \"3.6\"\n| | | \n| | * sha1-dda04a898b2048558b4238a4f111364ff5c92ab8\n| | | Parent: sha1-4f72aeacef670bc6884a97de5a363b0674ccd008\n| | | - \"3.1\"\n| | | + \"3.1.3\"\n| | | \n* | | sha1-e1583426c4201c7602b4aa1ae835915666d933f0\n| |/ Parent: sha1-27290eda714ce5d47df05e8f77a0986647887e32\n| | - \"3\"\n| | + \"3.2\"\n| | \n| * sha1-4f72aeacef670bc6884a97de5a363b0674ccd008\n|/ Parent: sha1-27290eda714ce5d47df05e8f77a0986647887e32\n| - \"3\"\n| + \"3.1\"\n| \n* sha1-27290eda714ce5d47df05e8f77a0986647887e32\n| Parent: sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| - \"2\"\n| + \"3\"\n| \n* sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| Parent: sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5\n| - \"1\"\n| + \"2\"\n| \n* sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5\n| Parent: None\n| \n"
graphRes2 = "* sha1-65c7e849861df97129cbd49352e52eef6dad3b11\n|\\ Merge: sha1-4578a4c35f09b6fe85608a95145a36f4c701d030 sha1-aecf8991bf5af5bb0b725ff2bdeb260426508ac5\n| | \"101\"\n| | \n* | sha1-4578a4c35f09b6fe85608a95145a36f4c701d030\n|\\ \\ Merge: sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5 sha1-d9cf6dcbf03a014c28c80a9baa34525b4f9095c8\n| | | \"11\"\n| | | \n* | sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5\n| | Parent: None\n| | \"1\"\n| | \n* sha1-d9cf6dcbf03a014c28c80a9baa34525b4f9095c8\n| Parent: None\n| \"10\"\n| \n* sha1-aecf8991bf5af5bb0b725ff2bdeb260426508ac5\n| Parent: None\n| \"100\"\n"
diffRes2 = "* sha1-65c7e849861df97129cbd49352e52eef6dad3b11\n|\\ Merge: sha1-4578a4c35f09b6fe85608a95145a36f4c701d030 sha1-aecf8991bf5af5bb0b725ff2bdeb260426508ac5\n| | - \"11\"\n| | + \"101\"\n| | \n* | sha1-4578a4c35f09b6fe85608a95145a36f4c701d030\n|\\ \\ Merge: sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5 sha1-d9cf6dcbf03a014c28c80a9baa34525b4f9095c8\n| | | - \"1\"\n| | | + \"11\"\n| | | \n* | sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5\n| | Parent: None\n| | \n* sha1-d9cf6dcbf03a014c28c80a9baa34525b4f9095c8\n| Parent: None\n| \n* sha1-aecf8991bf5af5bb0b725ff2bdeb260426508ac5\n| Parent: None\n| \n"
graphRes3 = "* sha1-a859b0936cc42f073c03a27eb820f29a57f025f3\n|\\ Merge: sha1-d996bef5350a0316da794ae1f262dfcdcd4b2e8c sha1-65c99ad8e4db74b2cb3bf00bff806a4d46a220eb\n| | \"2222-wz\"\n| | \n* | sha1-d996bef5350a0316da794ae1f262dfcdcd4b2e8c\n|\\ \\ Merge: sha1-da122844f28f7a814fb99a4950ff673ea7e3611c sha1-ba3e907b42eaba15bf01752b012c92f8e5821536\n| | | \"222-wy\"\n| | | \n| * | sha1-ba3e907b42eaba15bf01752b012c92f8e5821536\n| |\\ \\ Merge: sha1-8af6c17c296c36f2b5adf19b02a83918a61088d1 sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| | | | \"22-wx\"\n| | | | \n* | | | sha1-da122844f28f7a814fb99a4950ff673ea7e3611c\n| | | | Parent: sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| | | | \"200-y\"\n| | | | \n| * | | sha1-8af6c17c296c36f2b5adf19b02a83918a61088d1\n| | | | Parent: sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| | | | \"20-x\"\n| | | | \n| | | * sha1-65c99ad8e4db74b2cb3bf00bff806a4d46a220eb\n|/ / / Parent: sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| \"2000-z\"\n| \n* sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| Parent: sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5\n| \"2\"\n| \n* sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5\n| Parent: None\n| \"1\"\n"
diffRes3 = "* sha1-a859b0936cc42f073c03a27eb820f29a57f025f3\n|\\ Merge: sha1-d996bef5350a0316da794ae1f262dfcdcd4b2e8c sha1-65c99ad8e4db74b2cb3bf00bff806a4d46a220eb\n| | - \"222-wy\"\n| | + \"2222-wz\"\n| | \n* | sha1-d996bef5350a0316da794ae1f262dfcdcd4b2e8c\n|\\ \\ Merge: sha1-da122844f28f7a814fb99a4950ff673ea7e3611c sha1-ba3e907b42eaba15bf01752b012c92f8e5821536\n| | | - \"200-y\"\n| | | + \"222-wy\"\n| | | \n| * | sha1-ba3e907b42eaba15bf01752b012c92f8e5821536\n| |\\ \\ Merge: sha1-8af6c17c296c36f2b5adf19b02a83918a61088d1 sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| | | | - \"20-x\"\n| | | | + \"22-wx\"\n| | | | \n* | | | sha1-da122844f28f7a814fb99a4950ff673ea7e3611c\n| | | | Parent: sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| | | | - \"2\"\n| | | | + \"200-y\"\n| | | | \n| * | | sha1-8af6c17c296c36f2b5adf19b02a83918a61088d1\n| | | | Parent: sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| | | | - \"2\"\n| | | | + \"20-x\"\n| | | | \n| | | * sha1-65c99ad8e4db74b2cb3bf00bff806a4d46a220eb\n|/ / / Parent: sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| - \"2\"\n| + \"2000-z\"\n| \n* sha1-624532f5d8a5839344ccfc465957a975e1962b6d\n| Parent: sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5\n| - \"1\"\n| + \"2\"\n| \n* sha1-611d5d868352d4d6ae9b778d6627b81f769cdef5\n| Parent: None\n| \n"
truncRes1 = "* sha1-39d1f600887364b2e4832fe80f6853b0966a9e6c\n| Parent: sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| List<String>([\n| \"one\",\n| \"two\",\n| \"three\",\n| \"four\",\n| \"five\",\n| \"six\",\n| \"seven\",\n| ...\n| \n* sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| Parent: None\n| \"the first line\"\n"
truncRes1 = "* sha1-39d1f600887364b2e4832fe80f6853b0966a9e6c\n| Parent: sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| List<String>([\n| \"one\",\n| \"two\",\n| \"three\",\n| \"four\",\n| \"five\",\n| \"six\",\n| \"seven\",\n| ...\n| \n* sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| Parent: None\n| \"the first line\"\n"
diffTrunc1 = "* sha1-39d1f600887364b2e4832fe80f6853b0966a9e6c\n| Parent: sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| - \"the first line\"\n| + List<String>([\n| + \"one\",\n| + \"two\",\n| + \"three\",\n| + \"four\",\n| + \"five\",\n| + \"six\",\n| ...\n| \n* sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| Parent: None\n| \n"
truncRes2 = "* sha1-39d1f600887364b2e4832fe80f6853b0966a9e6c\n| Parent: sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| List<String>([\n| \"one\",\n| \"two\",\n| \"three\",\n| \"four\",\n| \"five\",\n| \"six\",\n| \"seven\",\n| \"eight\",\n| \"nine\",\n| \"ten\",\n| \"eleven\",\n| ])\n| \n* sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| Parent: None\n| \"the first line\"\n"
truncRes2 = "* sha1-39d1f600887364b2e4832fe80f6853b0966a9e6c\n| Parent: sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| List<String>([\n| \"one\",\n| \"two\",\n| \"three\",\n| \"four\",\n| \"five\",\n| \"six\",\n| \"seven\",\n| \"eight\",\n| \"nine\",\n| \"ten\",\n| \"eleven\",\n| ])\n| \n* sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| Parent: None\n| \"the first line\"\n"
diffTrunc2 = "* sha1-39d1f600887364b2e4832fe80f6853b0966a9e6c\n| Parent: sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| - \"the first line\"\n| + List<String>([\n| + \"one\",\n| + \"two\",\n| + \"three\",\n| + \"four\",\n| + \"five\",\n| + \"six\",\n| + \"seven\",\n| + \"eight\",\n| + \"nine\",\n| + \"ten\",\n| + \"eleven\",\n| + ])\n| \n* sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| Parent: None\n| \n"
truncRes3 = "* sha1-39d1f600887364b2e4832fe80f6853b0966a9e6c\n| Parent: sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| \n* sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| Parent: None\n"
truncRes3 = "* sha1-39d1f600887364b2e4832fe80f6853b0966a9e6c\n| Parent: sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n* sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| Parent: None\n"
diffTrunc3 = "* sha1-39d1f600887364b2e4832fe80f6853b0966a9e6c\n| Parent: sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n* sha1-185ad8e966cba1a70b7f6cf19cd7bc5a7983c3d2\n| Parent: None\n"
)