Display diff of lock file during dry-run

This commit is contained in:
Carolyn Van Slyck 2017-03-01 14:52:09 -06:00
Родитель d02eb6961e
Коммит 02635cc8b0
9 изменённых файлов: 432 добавлений и 35 удалений

Просмотреть файл

@ -161,7 +161,8 @@ func (cmd *ensureCommand) Run(ctx *dep.Ctx, args []string) error {
manifest = p.Manifest
}
sw.Prepare(manifest, p.Lock, solution, writeV)
newLock := dep.LockFromInterface(solution)
sw.Prepare(manifest, p.Lock, newLock, writeV)
if cmd.dryRun {
return sw.PrintPreparedActions()
}

Просмотреть файл

@ -180,7 +180,8 @@ func (cmd *removeCommand) Run(ctx *dep.Ctx, args []string) error {
}
var sw dep.SafeWriter
sw.Prepare(p.Manifest, p.Lock, soln, false)
newLock := dep.LockFromInterface(soln)
sw.Prepare(p.Manifest, p.Lock, newLock, false)
if err := sw.Write(p.AbsRoot, sm); err != nil {
return errors.Wrap(err, "grouped write of manifest, lock and vendor")
}

11
lock.go
Просмотреть файл

@ -184,14 +184,3 @@ func (s SortedLockedProjects) Less(i, j int) bool {
return l.Source < r.Source
}
// locksAreEquivalent compares two locks to see if they differ. If EITHER lock
// is nil, or their memos do not match, or any projects differ, then false is
// returned.
func locksAreEquivalent(l, r *Lock) bool {
if l == nil || r == nil {
return false
}
return gps.LocksAreEq(l, r, true)
}

Просмотреть файл

@ -96,8 +96,7 @@ func (pc *TestProjectContext) getVendorPath() string {
// Updates the golden file when -UpdateGolden flag is present.
func (pc *TestProjectContext) LockShouldMatchGolden(goldenLockPath string) error {
got := pc.h.ReadLock()
want := pc.h.GetTestFileString(goldenLockPath)
return pc.shouldMatchGolden(goldenLockPath, want, got)
return pc.ShouldMatchGolden(goldenLockPath, got)
}
// LockShouldNotExist returns an error when the lock exists.
@ -110,8 +109,7 @@ func (pc *TestProjectContext) LockShouldNotExist() error {
// Updates the golden file when -UpdateGolden flag is present
func (pc *TestProjectContext) ManifestShouldMatchGolden(goldenManifestPath string) error {
got := pc.h.ReadManifest()
want := pc.h.GetTestFileString(goldenManifestPath)
return pc.shouldMatchGolden(goldenManifestPath, want, got)
return pc.ShouldMatchGolden(goldenManifestPath, got)
}
// ManifestShouldNotExist returns an error when the lock exists.
@ -122,7 +120,8 @@ func (pc *TestProjectContext) ManifestShouldNotExist() error {
// ShouldMatchGolden returns an error when a file does not match the golden file.
// goldenFile is the path to the golden file, relative to the testdata directory
// Updates the golden file when -UpdateGolden flag is present
func (pc *TestProjectContext) shouldMatchGolden(goldenFile string, want string, got string) error {
func (pc *TestProjectContext) ShouldMatchGolden(goldenFile string, got string) error {
want := pc.h.GetTestFileString(goldenFile)
if want != got {
if *test.UpdateGolden {
if err := pc.h.WriteTestFile(goldenFile, got); err != nil {

26
testdata/txn_writer/expected_diff_output.txt поставляемый Normal file
Просмотреть файл

@ -0,0 +1,26 @@
Add: [
{
"name": "github.com/stuff/realthing",
"version": "2.0.0",
"revision": "1f02e52d6bac308da54ab84a234c58a98ca82347",
"packages": [
"."
]
}
]
Remove: [
"github.com/stuff/placeholder"
]
Modify: [
{
"name": "github.com/foo/bar",
"repo": "+ http://github.example.com/foo/bar",
"version": "+ 1.2.0",
"branch": "- master",
"revision": "f24338400f072ef18125ae0fbe6b06fe6d1783e7 -> 2a3a211e171803acb82d1d5d42ceb53228f51751",
"packages": [
"- placeholder",
"+ thing"
]
}
]

22
testdata/txn_writer/original_lock.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,22 @@
{
"memo": "595716d270828e763c811ef79c9c41f85b1d1bfbdfe85280036405c03772206c",
"projects": [
{
"name": "github.com/foo/bar",
"branch": "master",
"revision": "f24338400f072ef18125ae0fbe6b06fe6d1783e7",
"packages": [
"placeholder",
"util"
]
},
{
"name": "github.com/stuff/placeholder",
"version": "2.0.0",
"revision": "6694017eeb4e20fd277b049bf29dba4895c97234",
"packages": [
"."
]
}
]
}

23
testdata/txn_writer/updated_lock.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,23 @@
{
"memo": "2252a285ab27944a4d7adcba8dbd03980f59ba652f12db39fa93b927c345593e",
"projects": [
{
"name": "github.com/foo/bar",
"repo": "http://github.example.com/foo/bar",
"version": "1.2.0",
"revision": "2a3a211e171803acb82d1d5d42ceb53228f51751",
"packages": [
"thing",
"util"
]
},
{
"name": "github.com/stuff/realthing",
"version": "2.0.0",
"revision": "1f02e52d6bac308da54ab84a234c58a98ca82347",
"packages": [
"."
]
}
]
}

Просмотреть файл

@ -5,10 +5,14 @@
package dep
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/sdboyer/gps"
@ -48,18 +52,97 @@ func (payload *SafeWriterPayload) HasVendor() bool {
}
// LockDiff is the set of differences between an existing lock file and an updated lock file.
// Fields are only populated when there is a difference, otherwise they are empty.
// TODO(carolynvs) this should be moved to gps
type LockDiff struct {
Add []gps.LockedProject
Remove []gps.LockedProject
Modify []LockedProjectDiff
HashDiff *StringDiff
Add []LockedProjectDiff
Remove []gps.ProjectRoot
Modify []LockedProjectDiff
}
func (diff *LockDiff) Format() (string, error) {
if diff == nil {
return "", nil
}
var buf bytes.Buffer
if len(diff.Add) > 0 {
buf.WriteString("Add: ")
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
err := enc.Encode(diff.Add)
if err != nil {
return "", errors.Wrap(err, "Unable to format LockDiff.Add")
}
}
if len(diff.Remove) > 0 {
buf.WriteString("Remove: ")
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
err := enc.Encode(diff.Remove)
if err != nil {
return "", errors.Wrap(err, "Unable to format LockDiff.Remove")
}
}
if len(diff.Modify) > 0 {
buf.WriteString("Modify: ")
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
err := enc.Encode(diff.Modify)
if err != nil {
return "", errors.Wrap(err, "Unable to format LockDiff.Modify")
}
}
return buf.String(), nil
}
// LockedProjectDiff contains the before and after snapshot of a project reference.
// Fields are only populated when there is a difference, otherwise they are empty.
// TODO(carolynvs) this should be moved to gps
type LockedProjectDiff struct {
Current gps.LockedProject // Current represents the project reference as defined in the existing lock file.
Updated gps.LockedProject // Updated represents the desired project reference.
Name gps.ProjectRoot `json:"name"`
Repository *StringDiff `json:"repo,omitempty"`
Version *StringDiff `json:"version,omitempty"`
Branch *StringDiff `json:"branch,omitempty"`
Revision *StringDiff `json:"revision,omitempty"`
Packages []StringDiff `json:"packages,omitempty"`
}
type StringDiff struct {
Previous string
Current string
}
func (diff StringDiff) MarshalJSON() ([]byte, error) {
var value string
if diff.Previous == "" && diff.Current != "" {
value = fmt.Sprintf("+ %s", diff.Current)
} else if diff.Previous != "" && diff.Current == "" {
value = fmt.Sprintf("- %s", diff.Previous)
} else if diff.Previous != diff.Current {
value = fmt.Sprintf("%s -> %s", diff.Previous, diff.Current)
} else {
value = diff.Current
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
err := enc.Encode(value)
return buf.Bytes(), err
}
// Prepare to write a set of config yaml, lock and vendor tree.
@ -77,20 +160,21 @@ type LockedProjectDiff struct {
// and the vendor directory in the same way
// - If the forceVendor param is true, then vendor/ will be unconditionally
// written out based on newLock if present, else lock, else error.
func (sw *SafeWriter) Prepare(manifest *Manifest, lock *Lock, newLock gps.Lock, forceVendor bool) {
func (sw *SafeWriter) Prepare(manifest *Manifest, lock *Lock, newLock *Lock, forceVendor bool) {
sw.Payload = &SafeWriterPayload{
Manifest: manifest,
ForceWriteVendor: forceVendor,
}
if newLock != nil {
rlf := LockFromInterface(newLock)
if lock == nil {
sw.Payload.Lock = rlf
sw.Payload.Lock = newLock
sw.Payload.ForceWriteVendor = true
} else {
if !locksAreEquivalent(rlf, lock) {
sw.Payload.Lock = rlf
diff := diffLocks(lock, newLock)
if diff != nil {
sw.Payload.Lock = newLock
sw.Payload.LockDiff = diff
sw.Payload.ForceWriteVendor = true
}
}
@ -268,25 +352,25 @@ func (sw *SafeWriter) PrintPreparedActions() error {
fmt.Println("Would have written the following manifest.json:")
m, err := sw.Payload.Manifest.MarshalJSON()
if err != nil {
return errors.Wrap(err, "ensure DryRun cannot read manifest")
return errors.Wrap(err, "ensure DryRun cannot serialize manifest")
}
fmt.Println(string(m))
}
if sw.Payload.HasLock() {
fmt.Println("Would have written the following lock.json:")
m, err := sw.Payload.Lock.MarshalJSON()
fmt.Println("Would have written the following changes to lock.json:")
diff, err := sw.Payload.LockDiff.Format()
if err != nil {
return errors.Wrap(err, "ensure DryRun cannot read lock")
return errors.Wrap(err, "ensure DryRun cannot serialize the lock diff")
}
fmt.Println(string(m))
fmt.Println(diff)
}
if sw.Payload.HasVendor() {
fmt.Println("Would have written the following projects to the vendor directory:")
for _, project := range sw.Payload.Lock.Projects() {
prj := project.Ident()
rev := GetRevisionFromVersion(project.Version())
rev, _, _ := getVersionInfo(project.Version())
if prj.Source == "" {
fmt.Printf("%s@%s\n", prj.ProjectRoot, rev)
} else {
@ -297,3 +381,191 @@ func (sw *SafeWriter) PrintPreparedActions() error {
return nil
}
// diffLocks compares two locks and identifies the differences between them.
// Returns nil if there are no differences.
// TODO(carolynvs) this should be moved to gps
func diffLocks(l1 gps.Lock, l2 gps.Lock) *LockDiff {
// Default nil locks to empty locks, so that we can still generate a diff
if l1 == nil {
l1 = &gps.SimpleLock{}
}
if l2 == nil {
l2 = &gps.SimpleLock{}
}
p1, p2 := l1.Projects(), l2.Projects()
// Check if the slices are sorted already. If they are, we can compare
// without copying. Otherwise, we have to copy to avoid altering the
// original input.
sp1, sp2 := SortedLockedProjects(p1), SortedLockedProjects(p2)
if len(p1) > 1 && !sort.IsSorted(sp1) {
p1 = make([]gps.LockedProject, len(p1))
copy(p1, l1.Projects())
sort.Sort(SortedLockedProjects(p1))
}
if len(p2) > 1 && !sort.IsSorted(sp2) {
p2 = make([]gps.LockedProject, len(p2))
copy(p2, l2.Projects())
sort.Sort(SortedLockedProjects(p2))
}
diff := LockDiff{}
h1 := l1.InputHash()
h2 := l2.InputHash()
if !bytes.Equal(h1, h2) {
diff.HashDiff = &StringDiff{Previous: string(h1), Current: string(h2)}
}
var i2next int
for i1 := 0; i1 < len(p1); i1++ {
lp1 := p1[i1]
pr1 := lp1.Ident().ProjectRoot
var matched bool
for i2 := i2next; i2 < len(p2); i2++ {
lp2 := p2[i2]
pr2 := lp2.Ident().ProjectRoot
switch strings.Compare(string(pr1), string(pr2)) {
case 0: // Found a matching project
matched = true
pdiff := diffProjects(lp1, lp2)
if pdiff != nil {
diff.Modify = append(diff.Modify, *pdiff)
}
i2next = i2 + 1 // Don't evaluate to this again
case -1: // Found a new project
add := buildAddProject(lp2)
diff.Add = append(diff.Add, add)
i2next = i2 + 1 // Don't evaluate to this again
continue // Keep looking for a matching project
case +1: // Project has been removed, handled below
break
}
break // Done evaluating this project, move onto the next
}
if !matched {
diff.Remove = append(diff.Remove, pr1)
}
}
// Anything that still hasn't been evaluated are adds
for i2 := i2next; i2 < len(p2); i2++ {
lp2 := p2[i2]
add := buildAddProject(lp2)
diff.Add = append(diff.Add, add)
}
if diff.HashDiff == nil && len(diff.Add) == 0 && len(diff.Remove) == 0 && len(diff.Modify) == 0 {
return nil // The locks are the equivalent
}
return &diff
}
func buildAddProject(lp gps.LockedProject) LockedProjectDiff {
r2, b2, v2 := getVersionInfo(lp.Version())
var rev, version, branch *StringDiff
if r2 != "" {
rev = &StringDiff{Previous: r2, Current: r2}
}
if b2 != "" {
branch = &StringDiff{Previous: b2, Current: b2}
}
if v2 != "" {
version = &StringDiff{Previous: v2, Current: v2}
}
add := LockedProjectDiff{
Name: lp.Ident().ProjectRoot,
Revision: rev,
Version: version,
Branch: branch,
Packages: make([]StringDiff, len(lp.Packages())),
}
for i, pkg := range lp.Packages() {
add.Packages[i] = StringDiff{Previous: pkg, Current: pkg}
}
return add
}
// diffProjects compares two projects and identifies the differences between them.
// Returns nil if there are no differences
// TODO(carolynvs) this should be moved to gps and updated once the gps unexported fields are available to use.
func diffProjects(lp1 gps.LockedProject, lp2 gps.LockedProject) *LockedProjectDiff {
diff := LockedProjectDiff{Name: lp1.Ident().ProjectRoot}
s1 := lp1.Ident().Source
s2 := lp2.Ident().Source
if s1 != s2 {
diff.Repository = &StringDiff{Previous: s1, Current: s2}
}
r1, b1, v1 := getVersionInfo(lp1.Version())
r2, b2, v2 := getVersionInfo(lp2.Version())
if r1 != r2 {
diff.Revision = &StringDiff{Previous: r1, Current: r2}
}
if b1 != b2 {
diff.Branch = &StringDiff{Previous: b1, Current: b2}
}
if v1 != v2 {
diff.Version = &StringDiff{Previous: v1, Current: v2}
}
p1 := lp1.Packages()
p2 := lp2.Packages()
if !sort.StringsAreSorted(p1) {
p1 = make([]string, len(p1))
copy(p1, lp1.Packages())
sort.Strings(p1)
}
if !sort.StringsAreSorted(p2) {
p2 = make([]string, len(p2))
copy(p2, lp2.Packages())
sort.Strings(p2)
}
var i2next int
for i1 := 0; i1 < len(p1); i1++ {
pkg1 := p1[i1]
var matched bool
for i2 := i2next; i2 < len(p2); i2++ {
pkg2 := p2[i2]
switch strings.Compare(pkg1, pkg2) {
case 0: // Found matching package
matched = true
i2next = i2 + 1 // Don't evaluate to this again
case +1: // Found a new package
add := StringDiff{Current: pkg2}
diff.Packages = append(diff.Packages, add)
i2next = i2 + 1 // Don't evaluate to this again
continue // Keep looking for a match
case -1: // Package has been removed (handled below)
}
break // Done evaluating this package, move onto the next
}
if !matched {
diff.Packages = append(diff.Packages, StringDiff{Previous: pkg1})
}
}
// Anything that still hasn't been evaluated are adds
for i2 := i2next; i2 < len(p2); i2++ {
pkg2 := p2[i2]
add := StringDiff{Current: pkg2}
diff.Packages = append(diff.Packages, add)
}
if diff.Repository == nil && diff.Version == nil && diff.Revision == nil && len(diff.Packages) == 0 {
return nil // The projects are equivalent
}
return &diff
}

Просмотреть файл

@ -257,7 +257,7 @@ func TestSafeWriter_UnmodifiedLock(t *testing.T) {
t.Fatal("Did not expect the payload to contain the manifest")
}
if sw.Payload.HasLock() {
t.Fatal("Did not expect the payload to contain the lock")
t.Fatal("Did not expect the payload to contain the lock.")
}
if sw.Payload.HasVendor() {
t.Fatal("Did not expect the payload to contain the vendor directory")
@ -372,3 +372,67 @@ func TestSafeWriter_ForceVendorWhenVendorAlreadyExists(t *testing.T) {
t.Fatal(err)
}
}
func TestSafeWriter_DiffLocks(t *testing.T) {
test.NeedsExternalNetwork(t)
test.NeedsGit(t)
h := test.NewHelper(t)
defer h.Cleanup()
pc := NewTestProjectContext(h, safeWriterProject)
defer pc.Release()
pc.CopyFile(LockName, "txn_writer/original_lock.json")
pc.Load()
ulf := h.GetTestFile("txn_writer/updated_lock.json")
defer ulf.Close()
updatedLock, err := readLock(ulf)
h.Must(err)
var sw SafeWriter
sw.Prepare(nil, pc.Project.Lock, updatedLock, true)
// Verify lock diff
diff := sw.Payload.LockDiff
if diff == nil {
t.Fatal("Expected the payload to contain a diff of the lock files")
}
if diff.HashDiff == nil {
t.Fatalf("Expected the lock diff to contain the updated hash: expected %s, got %s", pc.Project.Lock.Memo, updatedLock.Memo)
}
if len(diff.Add) != 1 {
t.Fatalf("Expected the lock diff to contain 1 added project, got %d", len(diff.Add))
} else {
add := diff.Add[0]
if add.Name != "github.com/stuff/realthing" {
t.Errorf("expected new project github.com/stuff/realthing, got %s", add.Name)
}
}
if len(diff.Remove) != 1 {
t.Fatalf("Expected the lock diff to contain 1 removed project, got %d", len(diff.Remove))
} else {
remove := diff.Remove[0]
if remove != "github.com/stuff/placeholder" {
t.Fatalf("expected new project github.com/stuff/placeholder, got %s", remove)
}
}
if len(diff.Modify) != 1 {
t.Fatalf("Expected the lock diff to contain 1 modified project, got %d", len(diff.Modify))
} else {
modify := diff.Modify[0]
if modify.Name != "github.com/foo/bar" {
t.Fatalf("expected new project github.com/foo/bar, got %s", modify.Name)
}
}
output, err := diff.Format()
h.Must(err)
goldenOutput := "txn_writer/expected_diff_output.txt"
if err = pc.ShouldMatchGolden(goldenOutput, output); err != nil {
t.Fatal(err)
}
}