github.com/golang/dep@v0.5.4/gps/verify/lockdiff.go (about)

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package verify
     6  
     7  import (
     8  	"bytes"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/golang/dep/gps"
    13  )
    14  
    15  // DeltaDimension defines a bitset enumerating all of the different dimensions
    16  // along which a Lock, and its constitutent components, can change.
    17  type DeltaDimension uint32
    18  
    19  // Each flag represents an ortohgonal dimension along which Locks can vary with
    20  // respect to each other.
    21  const (
    22  	InputImportsChanged DeltaDimension = 1 << iota
    23  	ProjectAdded
    24  	ProjectRemoved
    25  	SourceChanged
    26  	VersionChanged
    27  	RevisionChanged
    28  	PackagesChanged
    29  	PruneOptsChanged
    30  	HashVersionChanged
    31  	HashChanged
    32  	AnyChanged = (1 << iota) - 1
    33  )
    34  
    35  // LockDelta represents all possible differences between two Locks.
    36  type LockDelta struct {
    37  	AddedImportInputs   []string
    38  	RemovedImportInputs []string
    39  	ProjectDeltas       map[gps.ProjectRoot]LockedProjectDelta
    40  }
    41  
    42  // LockedProjectDelta represents all possible state changes of a LockedProject
    43  // within a Lock. It encapsulates the property-level differences represented by
    44  // a LockedProjectPropertiesDelta, but can also represent existence deltas - a
    45  // given name came to exist, or cease to exist, across two Locks.
    46  type LockedProjectDelta struct {
    47  	Name                         gps.ProjectRoot
    48  	ProjectRemoved, ProjectAdded bool
    49  	LockedProjectPropertiesDelta
    50  }
    51  
    52  // LockedProjectPropertiesDelta represents all possible differences between the
    53  // properties of two LockedProjects. It can represent deltas for
    54  // VerifiableProject properties, as well.
    55  type LockedProjectPropertiesDelta struct {
    56  	PackagesAdded, PackagesRemoved      []string
    57  	VersionBefore, VersionAfter         gps.UnpairedVersion
    58  	RevisionBefore, RevisionAfter       gps.Revision
    59  	SourceBefore, SourceAfter           string
    60  	PruneOptsBefore, PruneOptsAfter     gps.PruneOptions
    61  	HashVersionBefore, HashVersionAfter int
    62  	HashChanged                         bool
    63  }
    64  
    65  // DiffLocks compares two locks and computes a semantically rich delta between
    66  // them.
    67  func DiffLocks(l1, l2 gps.Lock) LockDelta {
    68  	// Default nil locks to empty locks, so that we can still generate a diff.
    69  	if l1 == nil {
    70  		if l2 == nil {
    71  			// But both locks being nil results in an empty delta.
    72  			return LockDelta{}
    73  		}
    74  		l1 = gps.SimpleLock{}
    75  	}
    76  	if l2 == nil {
    77  		l2 = gps.SimpleLock{}
    78  	}
    79  
    80  	p1, p2 := l1.Projects(), l2.Projects()
    81  
    82  	p1 = sortLockedProjects(p1)
    83  	p2 = sortLockedProjects(p2)
    84  
    85  	diff := LockDelta{
    86  		ProjectDeltas: make(map[gps.ProjectRoot]LockedProjectDelta),
    87  	}
    88  
    89  	var i2next int
    90  	for i1 := 0; i1 < len(p1); i1++ {
    91  		lp1 := p1[i1]
    92  		pr1 := lp1.Ident().ProjectRoot
    93  
    94  		lpd := LockedProjectDelta{
    95  			Name: pr1,
    96  			// Default to assuming a project was removed, as it will handle both
    97  			// the obvious removal case (where there's a visible hole in p2),
    98  			// and the non-obvious case, where p2 is shorter than p1.
    99  			ProjectRemoved: true,
   100  		}
   101  
   102  		for i2 := i2next; i2 < len(p2); i2++ {
   103  			lp2 := p2[i2]
   104  			pr2 := lp2.Ident().ProjectRoot
   105  
   106  			switch strings.Compare(string(pr1), string(pr2)) {
   107  			case 0: // Found a matching project
   108  				lpd = LockedProjectDelta{
   109  					Name:                         pr1,
   110  					LockedProjectPropertiesDelta: DiffLockedProjectProperties(lp1, lp2),
   111  				}
   112  				i2next = i2 + 1 // Don't visit this project again
   113  			case +1: // Found a new project
   114  				diff.ProjectDeltas[pr2] = LockedProjectDelta{
   115  					Name:         pr2,
   116  					ProjectAdded: true,
   117  				}
   118  				i2next = i2 + 1 // Don't visit this project again
   119  				continue        // Keep looking for a matching project
   120  			}
   121  
   122  			break // Done evaluating this project, move onto the next
   123  		}
   124  
   125  		diff.ProjectDeltas[pr1] = lpd
   126  	}
   127  
   128  	// Anything that still hasn't been evaluated are adds
   129  	for i2 := i2next; i2 < len(p2); i2++ {
   130  		lp2 := p2[i2]
   131  		pr2 := lp2.Ident().ProjectRoot
   132  		diff.ProjectDeltas[pr2] = LockedProjectDelta{
   133  			Name:         pr2,
   134  			ProjectAdded: true,
   135  		}
   136  	}
   137  
   138  	diff.AddedImportInputs, diff.RemovedImportInputs = findAddedAndRemoved(l1.InputImports(), l2.InputImports())
   139  
   140  	return diff
   141  }
   142  
   143  func findAddedAndRemoved(l1, l2 []string) (add, remove []string) {
   144  	// Computing package add/removes might be optimizable to O(n) (?), but it's
   145  	// not critical path for any known case, so not worth the effort right now.
   146  	p1, p2 := make(map[string]bool, len(l1)), make(map[string]bool, len(l2))
   147  
   148  	for _, pkg := range l1 {
   149  		p1[pkg] = true
   150  	}
   151  	for _, pkg := range l2 {
   152  		p2[pkg] = true
   153  	}
   154  
   155  	for pkg := range p1 {
   156  		if !p2[pkg] {
   157  			remove = append(remove, pkg)
   158  		}
   159  	}
   160  	for pkg := range p2 {
   161  		if !p1[pkg] {
   162  			add = append(add, pkg)
   163  		}
   164  	}
   165  
   166  	return add, remove
   167  }
   168  
   169  // DiffLockedProjectProperties takes two gps.LockedProject and computes a delta
   170  // for each of their component properties.
   171  //
   172  // This function is focused exclusively on the properties of a LockedProject. As
   173  // such, it does not compare the ProjectRoot part of the LockedProject's
   174  // ProjectIdentifier, as those are names, and the concern here is a difference
   175  // in properties, not intrinsic identity.
   176  func DiffLockedProjectProperties(lp1, lp2 gps.LockedProject) LockedProjectPropertiesDelta {
   177  	ld := LockedProjectPropertiesDelta{
   178  		SourceBefore: lp1.Ident().Source,
   179  		SourceAfter:  lp2.Ident().Source,
   180  	}
   181  
   182  	ld.PackagesAdded, ld.PackagesRemoved = findAddedAndRemoved(lp1.Packages(), lp2.Packages())
   183  
   184  	switch v := lp1.Version().(type) {
   185  	case gps.PairedVersion:
   186  		ld.VersionBefore, ld.RevisionBefore = v.Unpair(), v.Revision()
   187  	case gps.Revision:
   188  		ld.RevisionBefore = v
   189  	case gps.UnpairedVersion:
   190  		// This should ideally never happen
   191  		ld.VersionBefore = v
   192  	}
   193  
   194  	switch v := lp2.Version().(type) {
   195  	case gps.PairedVersion:
   196  		ld.VersionAfter, ld.RevisionAfter = v.Unpair(), v.Revision()
   197  	case gps.Revision:
   198  		ld.RevisionAfter = v
   199  	case gps.UnpairedVersion:
   200  		// This should ideally never happen
   201  		ld.VersionAfter = v
   202  	}
   203  
   204  	vp1, ok1 := lp1.(VerifiableProject)
   205  	vp2, ok2 := lp2.(VerifiableProject)
   206  
   207  	if ok1 && ok2 {
   208  		ld.PruneOptsBefore, ld.PruneOptsAfter = vp1.PruneOpts, vp2.PruneOpts
   209  		ld.HashVersionBefore, ld.HashVersionAfter = vp1.Digest.HashVersion, vp2.Digest.HashVersion
   210  
   211  		if !bytes.Equal(vp1.Digest.Digest, vp2.Digest.Digest) {
   212  			ld.HashChanged = true
   213  		}
   214  	} else if ok1 {
   215  		ld.PruneOptsBefore = vp1.PruneOpts
   216  		ld.HashVersionBefore = vp1.Digest.HashVersion
   217  		ld.HashChanged = true
   218  	} else if ok2 {
   219  		ld.PruneOptsAfter = vp2.PruneOpts
   220  		ld.HashVersionAfter = vp2.Digest.HashVersion
   221  		ld.HashChanged = true
   222  	}
   223  
   224  	return ld
   225  }
   226  
   227  // Changed indicates whether the delta contains a change along the dimensions
   228  // with their corresponding bits set.
   229  //
   230  // This implementation checks the topmost-level Lock properties
   231  func (ld LockDelta) Changed(dims DeltaDimension) bool {
   232  	if dims&InputImportsChanged != 0 && (len(ld.AddedImportInputs) > 0 || len(ld.RemovedImportInputs) > 0) {
   233  		return true
   234  	}
   235  
   236  	for _, ld := range ld.ProjectDeltas {
   237  		if ld.Changed(dims & ^InputImportsChanged) {
   238  			return true
   239  		}
   240  	}
   241  
   242  	return false
   243  }
   244  
   245  // Changes returns a bitset indicating the dimensions along which deltas exist across
   246  // all contents of the LockDelta.
   247  //
   248  // This recurses down into the individual LockedProjectDeltas contained within
   249  // the LockDelta. A single delta along a particular dimension from a single
   250  // project is sufficient to flip the bit on for that dimension.
   251  func (ld LockDelta) Changes() DeltaDimension {
   252  	var dd DeltaDimension
   253  	if len(ld.AddedImportInputs) > 0 || len(ld.RemovedImportInputs) > 0 {
   254  		dd |= InputImportsChanged
   255  	}
   256  
   257  	for _, ld := range ld.ProjectDeltas {
   258  		dd |= ld.Changes()
   259  	}
   260  
   261  	return dd
   262  }
   263  
   264  // Changed indicates whether the delta contains a change along the dimensions
   265  // with their corresponding bits set.
   266  //
   267  // For example, if only the Revision changed, and this method is called with
   268  // SourceChanged | VersionChanged, it will return false; if it is called with
   269  // VersionChanged | RevisionChanged, it will return true.
   270  func (ld LockedProjectDelta) Changed(dims DeltaDimension) bool {
   271  	if dims&ProjectAdded != 0 && ld.WasAdded() {
   272  		return true
   273  	}
   274  
   275  	if dims&ProjectRemoved != 0 && ld.WasRemoved() {
   276  		return true
   277  	}
   278  
   279  	return ld.LockedProjectPropertiesDelta.Changed(dims & ^ProjectAdded & ^ProjectRemoved)
   280  }
   281  
   282  // Changes returns a bitset indicating the dimensions along which there were
   283  // changes between the compared LockedProjects. This includes both
   284  // existence-level deltas (add/remove) and property-level deltas.
   285  func (ld LockedProjectDelta) Changes() DeltaDimension {
   286  	var dd DeltaDimension
   287  	if ld.WasAdded() {
   288  		dd |= ProjectAdded
   289  	}
   290  
   291  	if ld.WasRemoved() {
   292  		dd |= ProjectRemoved
   293  	}
   294  
   295  	return dd | ld.LockedProjectPropertiesDelta.Changes()
   296  }
   297  
   298  // WasRemoved returns true if the named project existed in the first lock, but
   299  // did not exist in the second lock.
   300  func (ld LockedProjectDelta) WasRemoved() bool {
   301  	return ld.ProjectRemoved
   302  }
   303  
   304  // WasAdded returns true if the named project did not exist in the first lock,
   305  // but did exist in the second lock.
   306  func (ld LockedProjectDelta) WasAdded() bool {
   307  	return ld.ProjectAdded
   308  }
   309  
   310  // Changed indicates whether the delta contains a change along the dimensions
   311  // with their corresponding bits set.
   312  //
   313  // For example, if only the Revision changed, and this method is called with
   314  // SourceChanged | VersionChanged, it will return false; if it is called with
   315  // VersionChanged | RevisionChanged, it will return true.
   316  func (ld LockedProjectPropertiesDelta) Changed(dims DeltaDimension) bool {
   317  	if dims&SourceChanged != 0 && ld.SourceChanged() {
   318  		return true
   319  	}
   320  	if dims&RevisionChanged != 0 && ld.RevisionChanged() {
   321  		return true
   322  	}
   323  	if dims&PruneOptsChanged != 0 && ld.PruneOptsChanged() {
   324  		return true
   325  	}
   326  	if dims&HashChanged != 0 && ld.HashChanged {
   327  		return true
   328  	}
   329  	if dims&HashVersionChanged != 0 && ld.HashVersionChanged() {
   330  		return true
   331  	}
   332  	if dims&VersionChanged != 0 && ld.VersionChanged() {
   333  		return true
   334  	}
   335  	if dims&PackagesChanged != 0 && ld.PackagesChanged() {
   336  		return true
   337  	}
   338  
   339  	return false
   340  }
   341  
   342  // Changes returns a bitset indicating the dimensions along which there were
   343  // changes between the compared LockedProjects.
   344  func (ld LockedProjectPropertiesDelta) Changes() DeltaDimension {
   345  	var dd DeltaDimension
   346  	if ld.SourceChanged() {
   347  		dd |= SourceChanged
   348  	}
   349  	if ld.RevisionChanged() {
   350  		dd |= RevisionChanged
   351  	}
   352  	if ld.PruneOptsChanged() {
   353  		dd |= PruneOptsChanged
   354  	}
   355  	if ld.HashChanged {
   356  		dd |= HashChanged
   357  	}
   358  	if ld.HashVersionChanged() {
   359  		dd |= HashVersionChanged
   360  	}
   361  	if ld.VersionChanged() {
   362  		dd |= VersionChanged
   363  	}
   364  	if ld.PackagesChanged() {
   365  		dd |= PackagesChanged
   366  	}
   367  
   368  	return dd
   369  }
   370  
   371  // SourceChanged returns true if the source field differed between the first and
   372  // second locks.
   373  func (ld LockedProjectPropertiesDelta) SourceChanged() bool {
   374  	return ld.SourceBefore != ld.SourceAfter
   375  }
   376  
   377  // VersionChanged returns true if the version property differed between the
   378  // first and second locks. In addition to simple changes (e.g. 1.0.1 -> 1.0.2),
   379  // this also includes all possible version type changes either going from a
   380  // paired version to a plain revision, or the reverse direction, or the type of
   381  // unpaired version changing (e.g. branch -> semver).
   382  func (ld LockedProjectPropertiesDelta) VersionChanged() bool {
   383  	if ld.VersionBefore == nil && ld.VersionAfter == nil {
   384  		return false
   385  	} else if (ld.VersionBefore == nil || ld.VersionAfter == nil) || (ld.VersionBefore.Type() != ld.VersionAfter.Type()) {
   386  		return true
   387  	} else if !ld.VersionBefore.Matches(ld.VersionAfter) {
   388  		return true
   389  	}
   390  
   391  	return false
   392  }
   393  
   394  // RevisionChanged returns true if the revision property differed between the
   395  // first and second locks.
   396  func (ld LockedProjectPropertiesDelta) RevisionChanged() bool {
   397  	return ld.RevisionBefore != ld.RevisionAfter
   398  }
   399  
   400  // PackagesChanged returns true if the package set gained or lost members (or
   401  // both) between the first and second locks.
   402  func (ld LockedProjectPropertiesDelta) PackagesChanged() bool {
   403  	return len(ld.PackagesAdded) > 0 || len(ld.PackagesRemoved) > 0
   404  }
   405  
   406  // PruneOptsChanged returns true if the pruning flags for the project changed
   407  // between the first and second locks.
   408  func (ld LockedProjectPropertiesDelta) PruneOptsChanged() bool {
   409  	return ld.PruneOptsBefore != ld.PruneOptsAfter
   410  }
   411  
   412  // HashVersionChanged returns true if the version of the hashing algorithm
   413  // changed between the first and second locks.
   414  func (ld LockedProjectPropertiesDelta) HashVersionChanged() bool {
   415  	return ld.HashVersionBefore != ld.HashVersionAfter
   416  }
   417  
   418  // HashVersionWasZero returns true if the first lock had a zero hash version,
   419  // which can only mean it was uninitialized.
   420  func (ld LockedProjectPropertiesDelta) HashVersionWasZero() bool {
   421  	return ld.HashVersionBefore == 0
   422  }
   423  
   424  // sortLockedProjects returns a sorted copy of lps, or itself if already sorted.
   425  func sortLockedProjects(lps []gps.LockedProject) []gps.LockedProject {
   426  	if len(lps) <= 1 || sort.SliceIsSorted(lps, func(i, j int) bool {
   427  		return lps[i].Ident().Less(lps[j].Ident())
   428  	}) {
   429  		return lps
   430  	}
   431  
   432  	cp := make([]gps.LockedProject, len(lps))
   433  	copy(cp, lps)
   434  
   435  	sort.Slice(cp, func(i, j int) bool {
   436  		return cp[i].Ident().Less(cp[j].Ident())
   437  	})
   438  	return cp
   439  }