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 5package verify 6 7import ( 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. 17type DeltaDimension uint32 18 19// Each flag represents an ortohgonal dimension along which Locks can vary with 20// respect to each other. 21const ( 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. 36type 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. 46type 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. 55type 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. 67func 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 143func 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. 176func 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 231func (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. 251func (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. 270func (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. 285func (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. 300func (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. 306func (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. 316func (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. 344func (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. 373func (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). 382func (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. 396func (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. 402func (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. 408func (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. 414func (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. 420func (ld LockedProjectPropertiesDelta) HashVersionWasZero() bool { 421 return ld.HashVersionBefore == 0 422} 423 424// sortLockedProjects returns a sorted copy of lps, or itself if already sorted. 425func 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} 440