Skip to content

Commit

Permalink
feat(diff): update to new deepdiff output, show context in diffs
Browse files Browse the repository at this point in the history
Merge pull request #1149 from qri-io/feat_diff_output
  • Loading branch information
b5 authored Mar 5, 2020
2 parents 129d681 + 140a7b4 commit b6288dd
Show file tree
Hide file tree
Showing 16 changed files with 282 additions and 199 deletions.
Binary file modified api/testdata/api.snapshot
Binary file not shown.
7 changes: 4 additions & 3 deletions base/dsfs/dataset.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,8 @@ func generateCommitDescriptions(prev, ds *dataset.Dataset, force bool) (short, l
return "created dataset", "created dataset", nil
}

ctx := context.TODO()

// TODO(dlong): Inline body if it is a reasonable size, in order to get information about
// how the body has changed.
// TODO(dlong): Also should ignore derived fields, like structure.{checksum,entries,length}.
Expand All @@ -491,13 +493,12 @@ func generateCommitDescriptions(prev, ds *dataset.Dataset, force bool) (short, l
return "", "", err
}

stat := deepdiff.Stats{}
diff, err := deepdiff.Diff(prevData, nextData, deepdiff.OptionSetStats(&stat))
diff, stat, err := deepdiff.New().StatDiff(ctx, prevData, nextData)
if err != nil {
return "", "", err
}

shortTitle, longMessage := friendly.DiffDescriptions(diff, &stat)
shortTitle, longMessage := friendly.DiffDescriptions(diff, stat)
if shortTitle == "" {
if force {
return "forced update", "forced update", nil
Expand Down
34 changes: 10 additions & 24 deletions base/dsfs/dataset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,37 +479,23 @@ func TestGenerateCommitMessage(t *testing.T) {
&dataset.Dataset{
Structure: &dataset.Structure{Format: "json"},
Body: toqtype.MustParseJSONAsArray(`[
{
"fruit": "apple", "color": "red"
},
{
"fruit": "banana", "color": "yellow"
},
{
"fruit": "cherry", "color": "red"
}
{ "fruit": "apple", "color": "red" },
{ "fruit": "banana", "color": "yellow" },
{ "fruit": "cherry", "color": "red" }
]`),
},
&dataset.Dataset{
Structure: &dataset.Structure{Format: "json"},
Body: toqtype.MustParseJSONAsArray(`[
{
"fruit": "apple", "color": "red"
},
{
"fruit": "blueberry", "color": "blue"
},
{
"fruit": "cherry", "color": "red"
},
{
"fruit": "durian", "color": "green"
}
{ "fruit": "apple", "color": "red" },
{ "fruit": "blueberry", "color": "blue" },
{ "fruit": "cherry", "color": "red" },
{ "fruit": "durian", "color": "green" }
]`),
},
false,
"body replaced row 1 and added row 3",
"body:\n\treplaced row 1\n\tadded row 3",
"body updated row 1 and added row 3",
"body:\n\tupdated row 1\n\tadded row 3",
},
{
"body with lots of changes",
Expand Down Expand Up @@ -665,7 +651,7 @@ hen,twenty-nine,30`),
}

for _, c := range goodCases {
t.Run(fmt.Sprintf("%s", c.description), func(t *testing.T) {
t.Run(c.description, func(t *testing.T) {
shortTitle, longMessage, err := generateCommitDescriptions(c.prev, c.ds, c.force)
if err != nil {
t.Errorf("error: %s", err.Error())
Expand Down
114 changes: 77 additions & 37 deletions base/friendly/friendly.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/qri-io/deepdiff"
)

var log = logger.Logger("dsfs")
var log = logger.Logger("friendly")

const smallNumberOfChangesToBody = 3

Expand All @@ -25,7 +25,7 @@ func DiffDescriptions(deltas []*deepdiff.Delta, stats *deepdiff.Stats) (string,
return "", ""
}

deltas = preprocess(deltas)
deltas = preprocess(deltas, "")
perComponentChanges := buildComponentChanges(deltas)

// Data accumulated while iterating over the components.
Expand Down Expand Up @@ -68,6 +68,14 @@ func DiffDescriptions(deltas []*deepdiff.Delta, stats *deepdiff.Stats) (string,
// it for both the long message and short title.
msg = fmt.Sprintf("%s:\n\t%s", compName, changes.Rows[0])
shortTitle = fmt.Sprintf("%s %s", compName, changes.Rows[0])
} else if len(changes.Rows) == 0 && compName == "transform" {
// TODO (b5) - this is a hack to make TestSaveTransformWithoutChanges
// in the cmd package pass. We're getting to this stage with 0 rows of
// changes, which is making msg & title not-empty, which is in turn allowing
// a commit b/c it looks like a change. ideally we don't make it here at all,
// but we DEFINITELY need a better hueristic for dsfs.CreateDataset's
// change detection check. Maybe use diffstat?
continue
} else {
// If there were multiple changes, describe them all for the long message
// but just show the number of changes for the short title.
Expand Down Expand Up @@ -107,38 +115,46 @@ func DiffDescriptions(deltas []*deepdiff.Delta, stats *deepdiff.Stats) (string,
return shortTitle, longMessage
}

// preprocess makes delta lists easier to work with, by combining operations when possible
func preprocess(deltas []*deepdiff.Delta) []*deepdiff.Delta {
const dtReplace = deepdiff.Operation("replace")

// preprocess makes delta lists easier to work with, by combining operations
// when possible & removing unwanted paths
func preprocess(deltas deepdiff.Deltas, path string) deepdiff.Deltas {
build := make([]*deepdiff.Delta, 0, len(deltas))
for i, d := range deltas {
if i > 0 {
last := build[len(build)-1]
if last.Path == d.Path {
if last.Path.String() == d.Path.String() {
if last.Type == deepdiff.DTDelete && d.Type == deepdiff.DTInsert {
last.Type = "replace"
last.Type = dtReplace
continue
}
}
}

p := joinPath(path, d.Path.String())
// TODO (b5) - We need this because it's possible to write a transform script
// that changes nothing other than the script itself, and we currently reject
// that change. Should we? I think I'm missing something.
if p == "transform.scriptPath" {
continue
}

build = append(build, d)
if len(d.Deltas) > 0 {
d.Deltas = preprocess(d.Deltas, joinPath(path, d.Path.String()))
}
}
return build
}

func buildComponentChanges(deltas []*deepdiff.Delta) map[string]*ComponentChanges {
func buildComponentChanges(deltas deepdiff.Deltas) map[string]*ComponentChanges {
perComponentChanges := make(map[string]*ComponentChanges)
for _, d := range deltas {
if d.Path == "/transform/scriptPath" {
continue
}
parts := strings.Split(d.Path, "/")
if len(parts) < 2 {
log.Debugf("path %q cannot map to dataset delta", d.Path)
continue
} else if len(parts) == 2 {
compName := d.Path.String()
if d.Type != deepdiff.DTContext {
// Entire component changed
compName := parts[1]
if d.Type == deepdiff.DTInsert || d.Type == deepdiff.DTDelete {
if d.Type == deepdiff.DTInsert || d.Type == deepdiff.DTDelete || d.Type == dtReplace {
if _, ok := perComponentChanges[compName]; !ok {
perComponentChanges[compName] = &ComponentChanges{}
}
Expand All @@ -149,43 +165,67 @@ func buildComponentChanges(deltas []*deepdiff.Delta) map[string]*ComponentChange
log.Debugf("unknown delta type %q for path %q", d.Type, d.Path)
continue
}
} else {
} else if len(d.Deltas) > 0 {
// Part of the component changed, record some state to build into a message later
compName := parts[1]
if _, ok := perComponentChanges[compName]; !ok {
perComponentChanges[compName] = &ComponentChanges{}
}
changes, _ := perComponentChanges[compName]
changes := &ComponentChanges{}

if compName == "body" {
changes.Num++
if changes.Num <= smallNumberOfChangesToBody {
rowNum := parts[2]
rowModify := fmt.Sprintf("%s row %s", pastTense(string(d.Type)), rowNum)
changes.Rows = append(changes.Rows, rowModify)
} else {
changes.Rows = nil
}
buildBodyChanges(changes, "", d.Deltas)
} else {
rowModify := fmt.Sprintf("%s %s", pastTense(string(d.Type)),
strings.Join(parts[2:], "."))
changes.Rows = append(changes.Rows, rowModify)
buildChanges(changes, "", d.Deltas)
}

perComponentChanges[compName] = changes
}
}
return perComponentChanges
}

func buildChanges(changes *ComponentChanges, parentPath string, deltas deepdiff.Deltas) {
for _, d := range deltas {
if d.Type != deepdiff.DTContext {
rowModify := fmt.Sprintf("%s %s", pastTense(string(d.Type)), joinPath(parentPath, d.Path.String()))
changes.Rows = append(changes.Rows, rowModify)
}

if len(d.Deltas) > 0 {
buildChanges(changes, joinPath(parentPath, d.Path.String()), d.Deltas)
}
}
}

func buildBodyChanges(changes *ComponentChanges, parentPath string, deltas deepdiff.Deltas) {
for _, d := range deltas {
if d.Type == deepdiff.DTDelete || d.Type == deepdiff.DTInsert || d.Type == deepdiff.DTUpdate || d.Type == dtReplace {
changes.Num++
if changes.Num <= smallNumberOfChangesToBody {
rowModify := fmt.Sprintf("%s row %s", pastTense(string(d.Type)), joinPath(parentPath, d.Path.String()))
changes.Rows = append(changes.Rows, rowModify)
} else {
changes.Rows = nil
}
} else if len(d.Deltas) > 0 {
buildBodyChanges(changes, joinPath(parentPath, d.Path.String()), d.Deltas)
}
}
}

func joinPath(parent, element string) string {
if parent == "" {
return element
}
return fmt.Sprintf("%s.%s", parent, element)
}

func pastTense(text string) string {
if text == string(deepdiff.DTDelete) {
return "removed"
} else if text == string(deepdiff.DTInsert) {
return "added"
} else if text == string(deepdiff.DTMove) {
return "moved"
} else if text == string(deepdiff.DTUpdate) {
return "updated"
} else if text == "replace" {
return "replaced"
return "updated"
}
return text
}
74 changes: 31 additions & 43 deletions base/friendly/friendly_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,17 @@ import (

func TestFriendlyDiffDescriptions(t *testing.T) {
// Change both the meta and structure
deltas := []*deepdiff.Delta{
&deepdiff.Delta{
Type: deepdiff.DTUpdate,
Path: "/meta/title",
Value: "def",
SourcePath: "/meta/title",
SourceValue: "abc",
},
&deepdiff.Delta{
Type: deepdiff.DTUpdate,
Path: "/structure/formatConfig/headerRow",
Value: true,
SourcePath: "/structure/formatConfig/headerRow",
SourceValue: false,
},
deltas := deepdiff.Deltas{
{Type: deepdiff.DTContext, Path: deepdiff.StringAddr("meta"), Deltas: deepdiff.Deltas{
{Type: deepdiff.DTDelete, Path: deepdiff.StringAddr("title"), Value: "abc"},
{Type: deepdiff.DTInsert, Path: deepdiff.StringAddr("title"), Value: "def"},
}},
{Type: deepdiff.DTContext, Path: deepdiff.StringAddr("structure"), Deltas: deepdiff.Deltas{
{Type: deepdiff.DTContext, Path: deepdiff.StringAddr("formatConfig"), Deltas: deepdiff.Deltas{
{Type: deepdiff.DTDelete, Path: deepdiff.StringAddr("headerRow"), Value: false},
{Type: deepdiff.DTInsert, Path: deepdiff.StringAddr("headerRow"), Value: true},
}},
}},
}
stats := deepdiff.Stats{
Left: 46,
Expand All @@ -48,11 +44,11 @@ func TestBuildComponentChanges(t *testing.T) {
// Change the meta.title
deltas := []*deepdiff.Delta{
&deepdiff.Delta{
Type: deepdiff.DTUpdate,
Path: "/meta/title",
Value: "def",
SourcePath: "/meta/title",
SourceValue: "abc",
Type: deepdiff.DTContext,
Path: deepdiff.StringAddr("meta"),
Deltas: deepdiff.Deltas{
{Type: deepdiff.DTUpdate, Path: deepdiff.StringAddr("title"), Value: "def", SourceValue: "abc"},
},
},
}
m := buildComponentChanges(deltas)
Expand All @@ -69,14 +65,12 @@ func TestBuildComponentChanges(t *testing.T) {
}

// Change the structure
deltas = []*deepdiff.Delta{
&deepdiff.Delta{
Type: deepdiff.DTUpdate,
Path: "/structure/formatConfig/headerRow",
Value: true,
SourcePath: "/structure/formatConfig/headerRow",
SourceValue: false,
},
deltas = deepdiff.Deltas{
{Type: deepdiff.DTContext, Path: deepdiff.StringAddr("structure"), Deltas: deepdiff.Deltas{
{Type: deepdiff.DTContext, Path: deepdiff.StringAddr("formatConfig"), Deltas: deepdiff.Deltas{
{Type: deepdiff.DTUpdate, Path: deepdiff.StringAddr("headerRow"), Value: true, SourceValue: false},
}},
}},
}
m = buildComponentChanges(deltas)
keys = getKeys(m)
Expand All @@ -92,21 +86,15 @@ func TestBuildComponentChanges(t *testing.T) {
}

// Change both the meta and structure
deltas = []*deepdiff.Delta{
&deepdiff.Delta{
Type: deepdiff.DTUpdate,
Path: "/meta/title",
Value: "def",
SourcePath: "/meta/title",
SourceValue: "abc",
},
&deepdiff.Delta{
Type: deepdiff.DTUpdate,
Path: "/structure/formatConfig/headerRow",
Value: true,
SourcePath: "/structure/formatConfig/headerRow",
SourceValue: false,
},
deltas = deepdiff.Deltas{
{Type: deepdiff.DTContext, Path: deepdiff.StringAddr("meta"), Deltas: deepdiff.Deltas{
{Type: deepdiff.DTUpdate, Path: deepdiff.StringAddr("title"), Value: "def", SourceValue: "abc"},
}},
{Type: deepdiff.DTContext, Path: deepdiff.StringAddr("structure"), Deltas: deepdiff.Deltas{
{Type: deepdiff.DTContext, Path: deepdiff.StringAddr("formatConfig"), Deltas: deepdiff.Deltas{
{Type: deepdiff.DTUpdate, Path: deepdiff.StringAddr("headerRow"), Value: true, SourceValue: false},
}},
}},
}
m = buildComponentChanges(deltas)
keys = getKeys(m)
Expand Down
Loading

0 comments on commit b6288dd

Please sign in to comment.