gps/verify/lockdiff.go

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package verify

import (
    "bytes"
    "sort"
    "strings"

    "github.com/golang/dep/gps"
)

// DeltaDimension defines a bitset enumerating all of the different dimensions
// along which a Lock, and its constitutent components, can change.
type DeltaDimension uint32

// Each flag represents an ortohgonal dimension along which Locks can vary with
// respect to each other.
const (
    InputImportsChanged DeltaDimension = 1 << iota
    ProjectAdded
    ProjectRemoved
    SourceChanged
    VersionChanged
    RevisionChanged
    PackagesChanged
    PruneOptsChanged
    HashVersionChanged
    HashChanged
    AnyChanged = (1 << iota) - 1
)

// LockDelta represents all possible differences between two Locks.
type LockDelta struct {
    AddedImportInputs   []string
    RemovedImportInputs []string
    ProjectDeltas       map[gps.ProjectRoot]LockedProjectDelta
}

// LockedProjectDelta represents all possible state changes of a LockedProject
// within a Lock. It encapsulates the property-level differences represented by
// a LockedProjectPropertiesDelta, but can also represent existence deltas - a
// given name came to exist, or cease to exist, across two Locks.
type LockedProjectDelta struct {
    Name                         gps.ProjectRoot
    ProjectRemoved, ProjectAdded bool
    LockedProjectPropertiesDelta
}

// LockedProjectPropertiesDelta represents all possible differences between the
// properties of two LockedProjects. It can represent deltas for
// VerifiableProject properties, as well.
type LockedProjectPropertiesDelta struct {
    PackagesAdded, PackagesRemoved      []string
    VersionBefore, VersionAfter         gps.UnpairedVersion
    RevisionBefore, RevisionAfter       gps.Revision
    SourceBefore, SourceAfter           string
    PruneOptsBefore, PruneOptsAfter     gps.PruneOptions
    HashVersionBefore, HashVersionAfter int
    HashChanged                         bool
}

// DiffLocks compares two locks and computes a semantically rich delta between
// them.
func DiffLocks(l1, l2 gps.Lock) LockDelta {
    // Default nil locks to empty locks, so that we can still generate a diff.
    if l1 == nil {
        if l2 == nil {
            // But both locks being nil results in an empty delta.
            return LockDelta{}
        }
        l1 = gps.SimpleLock{}
    }
    if l2 == nil {
        l2 = gps.SimpleLock{}
    }

    p1, p2 := l1.Projects(), l2.Projects()

    p1 = sortLockedProjects(p1)
    p2 = sortLockedProjects(p2)

    diff := LockDelta{
        ProjectDeltas: make(map[gps.ProjectRoot]LockedProjectDelta),
    }

    var i2next int
    for i1 := 0; i1 < len(p1); i1++ {
        lp1 := p1[i1]
        pr1 := lp1.Ident().ProjectRoot

        lpd := LockedProjectDelta{
            Name: pr1,
            // Default to assuming a project was removed, as it will handle both
            // the obvious removal case (where there's a visible hole in p2),
            // and the non-obvious case, where p2 is shorter than p1.
            ProjectRemoved: true,
        }

        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
                lpd = LockedProjectDelta{
                    Name:                         pr1,
                    LockedProjectPropertiesDelta: DiffLockedProjectProperties(lp1, lp2),
                }
                i2next = i2 + 1 // Don't visit this project again
            case +1: // Found a new project
                diff.ProjectDeltas[pr2] = LockedProjectDelta{
                    Name:         pr2,
                    ProjectAdded: true,
                }
                i2next = i2 + 1 // Don't visit this project again
                continue        // Keep looking for a matching project
            }

            break // Done evaluating this project, move onto the next
        }

        diff.ProjectDeltas[pr1] = lpd
    }

    // Anything that still hasn't been evaluated are adds
    for i2 := i2next; i2 < len(p2); i2++ {
        lp2 := p2[i2]
        pr2 := lp2.Ident().ProjectRoot
        diff.ProjectDeltas[pr2] = LockedProjectDelta{
            Name:         pr2,
            ProjectAdded: true,
        }
    }

    diff.AddedImportInputs, diff.RemovedImportInputs = findAddedAndRemoved(l1.InputImports(), l2.InputImports())

    return diff
}

func findAddedAndRemoved(l1, l2 []string) (add, remove []string) {
    // Computing package add/removes might be optimizable to O(n) (?), but it's
    // not critical path for any known case, so not worth the effort right now.
    p1, p2 := make(map[string]bool, len(l1)), make(map[string]bool, len(l2))

    for _, pkg := range l1 {
        p1[pkg] = true
    }
    for _, pkg := range l2 {
        p2[pkg] = true
    }

    for pkg := range p1 {
        if !p2[pkg] {
            remove = append(remove, pkg)
        }
    }
    for pkg := range p2 {
        if !p1[pkg] {
            add = append(add, pkg)
        }
    }

    return add, remove
}

// DiffLockedProjectProperties takes two gps.LockedProject and computes a delta
// for each of their component properties.
//
// This function is focused exclusively on the properties of a LockedProject. As
// such, it does not compare the ProjectRoot part of the LockedProject's
// ProjectIdentifier, as those are names, and the concern here is a difference
// in properties, not intrinsic identity.
func DiffLockedProjectProperties(lp1, lp2 gps.LockedProject) LockedProjectPropertiesDelta {
    ld := LockedProjectPropertiesDelta{
        SourceBefore: lp1.Ident().Source,
        SourceAfter:  lp2.Ident().Source,
    }

    ld.PackagesAdded, ld.PackagesRemoved = findAddedAndRemoved(lp1.Packages(), lp2.Packages())

    switch v := lp1.Version().(type) {
    case gps.PairedVersion:
        ld.VersionBefore, ld.RevisionBefore = v.Unpair(), v.Revision()
    case gps.Revision:
        ld.RevisionBefore = v
    case gps.UnpairedVersion:
        // This should ideally never happen
        ld.VersionBefore = v
    }

    switch v := lp2.Version().(type) {
    case gps.PairedVersion:
        ld.VersionAfter, ld.RevisionAfter = v.Unpair(), v.Revision()
    case gps.Revision:
        ld.RevisionAfter = v
    case gps.UnpairedVersion:
        // This should ideally never happen
        ld.VersionAfter = v
    }

    vp1, ok1 := lp1.(VerifiableProject)
    vp2, ok2 := lp2.(VerifiableProject)

    if ok1 && ok2 {
        ld.PruneOptsBefore, ld.PruneOptsAfter = vp1.PruneOpts, vp2.PruneOpts
        ld.HashVersionBefore, ld.HashVersionAfter = vp1.Digest.HashVersion, vp2.Digest.HashVersion

        if !bytes.Equal(vp1.Digest.Digest, vp2.Digest.Digest) {
            ld.HashChanged = true
        }
    } else if ok1 {
        ld.PruneOptsBefore = vp1.PruneOpts
        ld.HashVersionBefore = vp1.Digest.HashVersion
        ld.HashChanged = true
    } else if ok2 {
        ld.PruneOptsAfter = vp2.PruneOpts
        ld.HashVersionAfter = vp2.Digest.HashVersion
        ld.HashChanged = true
    }

    return ld
}

// Changed indicates whether the delta contains a change along the dimensions
// with their corresponding bits set.
//
// This implementation checks the topmost-level Lock properties
func (ld LockDelta) Changed(dims DeltaDimension) bool {
    if dims&InputImportsChanged != 0 && (len(ld.AddedImportInputs) > 0 || len(ld.RemovedImportInputs) > 0) {
        return true
    }

    for _, ld := range ld.ProjectDeltas {
        if ld.Changed(dims & ^InputImportsChanged) {
            return true
        }
    }

    return false
}

// Changes returns a bitset indicating the dimensions along which deltas exist across
// all contents of the LockDelta.
//
// This recurses down into the individual LockedProjectDeltas contained within
// the LockDelta. A single delta along a particular dimension from a single
// project is sufficient to flip the bit on for that dimension.
func (ld LockDelta) Changes() DeltaDimension {
    var dd DeltaDimension
    if len(ld.AddedImportInputs) > 0 || len(ld.RemovedImportInputs) > 0 {
        dd |= InputImportsChanged
    }

    for _, ld := range ld.ProjectDeltas {
        dd |= ld.Changes()
    }

    return dd
}

// Changed indicates whether the delta contains a change along the dimensions
// with their corresponding bits set.
//
// For example, if only the Revision changed, and this method is called with
// SourceChanged | VersionChanged, it will return false; if it is called with
// VersionChanged | RevisionChanged, it will return true.
func (ld LockedProjectDelta) Changed(dims DeltaDimension) bool {
    if dims&ProjectAdded != 0 && ld.WasAdded() {
        return true
    }

    if dims&ProjectRemoved != 0 && ld.WasRemoved() {
        return true
    }

    return ld.LockedProjectPropertiesDelta.Changed(dims & ^ProjectAdded & ^ProjectRemoved)
}

// Changes returns a bitset indicating the dimensions along which there were
// changes between the compared LockedProjects. This includes both
// existence-level deltas (add/remove) and property-level deltas.
func (ld LockedProjectDelta) Changes() DeltaDimension {
    var dd DeltaDimension
    if ld.WasAdded() {
        dd |= ProjectAdded
    }

    if ld.WasRemoved() {
        dd |= ProjectRemoved
    }

    return dd | ld.LockedProjectPropertiesDelta.Changes()
}

// WasRemoved returns true if the named project existed in the first lock, but
// did not exist in the second lock.
func (ld LockedProjectDelta) WasRemoved() bool {
    return ld.ProjectRemoved
}

// WasAdded returns true if the named project did not exist in the first lock,
// but did exist in the second lock.
func (ld LockedProjectDelta) WasAdded() bool {
    return ld.ProjectAdded
}

// Changed indicates whether the delta contains a change along the dimensions
// with their corresponding bits set.
//
// For example, if only the Revision changed, and this method is called with
// SourceChanged | VersionChanged, it will return false; if it is called with
// VersionChanged | RevisionChanged, it will return true.
func (ld LockedProjectPropertiesDelta) Changed(dims DeltaDimension) bool {
    if dims&SourceChanged != 0 && ld.SourceChanged() {
        return true
    }
    if dims&RevisionChanged != 0 && ld.RevisionChanged() {
        return true
    }
    if dims&PruneOptsChanged != 0 && ld.PruneOptsChanged() {
        return true
    }
    if dims&HashChanged != 0 && ld.HashChanged {
        return true
    }
    if dims&HashVersionChanged != 0 && ld.HashVersionChanged() {
        return true
    }
    if dims&VersionChanged != 0 && ld.VersionChanged() {
        return true
    }
    if dims&PackagesChanged != 0 && ld.PackagesChanged() {
        return true
    }

    return false
}

// Changes returns a bitset indicating the dimensions along which there were
// changes between the compared LockedProjects.
func (ld LockedProjectPropertiesDelta) Changes() DeltaDimension {
    var dd DeltaDimension
    if ld.SourceChanged() {
        dd |= SourceChanged
    }
    if ld.RevisionChanged() {
        dd |= RevisionChanged
    }
    if ld.PruneOptsChanged() {
        dd |= PruneOptsChanged
    }
    if ld.HashChanged {
        dd |= HashChanged
    }
    if ld.HashVersionChanged() {
        dd |= HashVersionChanged
    }
    if ld.VersionChanged() {
        dd |= VersionChanged
    }
    if ld.PackagesChanged() {
        dd |= PackagesChanged
    }

    return dd
}

// SourceChanged returns true if the source field differed between the first and
// second locks.
func (ld LockedProjectPropertiesDelta) SourceChanged() bool {
    return ld.SourceBefore != ld.SourceAfter
}

// VersionChanged returns true if the version property differed between the
// first and second locks. In addition to simple changes (e.g. 1.0.1 -> 1.0.2),
// this also includes all possible version type changes either going from a
// paired version to a plain revision, or the reverse direction, or the type of
// unpaired version changing (e.g. branch -> semver).
func (ld LockedProjectPropertiesDelta) VersionChanged() bool {
    if ld.VersionBefore == nil && ld.VersionAfter == nil {
        return false
    } else if (ld.VersionBefore == nil || ld.VersionAfter == nil) || (ld.VersionBefore.Type() != ld.VersionAfter.Type()) {
        return true
    } else if !ld.VersionBefore.Matches(ld.VersionAfter) {
        return true
    }

    return false
}

// RevisionChanged returns true if the revision property differed between the
// first and second locks.
func (ld LockedProjectPropertiesDelta) RevisionChanged() bool {
    return ld.RevisionBefore != ld.RevisionAfter
}

// PackagesChanged returns true if the package set gained or lost members (or
// both) between the first and second locks.
func (ld LockedProjectPropertiesDelta) PackagesChanged() bool {
    return len(ld.PackagesAdded) > 0 || len(ld.PackagesRemoved) > 0
}

// PruneOptsChanged returns true if the pruning flags for the project changed
// between the first and second locks.
func (ld LockedProjectPropertiesDelta) PruneOptsChanged() bool {
    return ld.PruneOptsBefore != ld.PruneOptsAfter
}

// HashVersionChanged returns true if the version of the hashing algorithm
// changed between the first and second locks.
func (ld LockedProjectPropertiesDelta) HashVersionChanged() bool {
    return ld.HashVersionBefore != ld.HashVersionAfter
}

// HashVersionWasZero returns true if the first lock had a zero hash version,
// which can only mean it was uninitialized.
func (ld LockedProjectPropertiesDelta) HashVersionWasZero() bool {
    return ld.HashVersionBefore == 0
}

// sortLockedProjects returns a sorted copy of lps, or itself if already sorted.
func sortLockedProjects(lps []gps.LockedProject) []gps.LockedProject {
    if len(lps) <= 1 || sort.SliceIsSorted(lps, func(i, j int) bool {
        return lps[i].Ident().Less(lps[j].Ident())
    }) {
        return lps
    }

    cp := make([]gps.LockedProject, len(lps))
    copy(cp, lps)

    sort.Slice(cp, func(i, j int) bool {
        return cp[i].Ident().Less(cp[j].Ident())
    })
    return cp
}