github.com/golang/dep@v0.5.4/internal/feedback/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 feedback
     6  
     7  import (
     8  	"fmt"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/golang/dep/gps"
    13  )
    14  
    15  // StringDiff represents a modified string value.
    16  // * Added: Previous = nil, Current != nil
    17  // * Deleted: Previous != nil, Current = nil
    18  // * Modified: Previous != nil, Current != nil
    19  // * No Change: Previous = Current, or a nil pointer
    20  type StringDiff struct {
    21  	Previous string
    22  	Current  string
    23  }
    24  
    25  func (diff *StringDiff) String() string {
    26  	if diff == nil {
    27  		return ""
    28  	}
    29  
    30  	if diff.Previous == "" && diff.Current != "" {
    31  		return fmt.Sprintf("+ %s", diff.Current)
    32  	}
    33  
    34  	if diff.Previous != "" && diff.Current == "" {
    35  		return fmt.Sprintf("- %s", diff.Previous)
    36  	}
    37  
    38  	if diff.Previous != diff.Current {
    39  		return fmt.Sprintf("%s -> %s", diff.Previous, diff.Current)
    40  	}
    41  
    42  	return diff.Current
    43  }
    44  
    45  // LockDiff is the set of differences between an existing lock file and an updated lock file.
    46  // Fields are only populated when there is a difference, otherwise they are empty.
    47  type LockDiff struct {
    48  	Add    []LockedProjectDiff
    49  	Remove []LockedProjectDiff
    50  	Modify []LockedProjectDiff
    51  }
    52  
    53  // LockedProjectDiff contains the before and after snapshot of a project reference.
    54  // Fields are only populated when there is a difference, otherwise they are empty.
    55  type LockedProjectDiff struct {
    56  	Name     gps.ProjectRoot
    57  	Source   *StringDiff
    58  	Version  *StringDiff
    59  	Branch   *StringDiff
    60  	Revision *StringDiff
    61  	Packages []StringDiff
    62  }
    63  
    64  // DiffLocks compares two locks and identifies the differences between them.
    65  // Returns nil if there are no differences.
    66  func DiffLocks(l1, l2 gps.Lock) *LockDiff {
    67  	// Default nil locks to empty locks, so that we can still generate a diff
    68  	if l1 == nil {
    69  		l1 = gps.SimpleLock{}
    70  	}
    71  	if l2 == nil {
    72  		l2 = gps.SimpleLock{}
    73  	}
    74  
    75  	p1, p2 := l1.Projects(), l2.Projects()
    76  
    77  	p1 = sortLockedProjects(p1)
    78  	p2 = sortLockedProjects(p2)
    79  
    80  	diff := LockDiff{}
    81  
    82  	var i2next int
    83  	for i1 := 0; i1 < len(p1); i1++ {
    84  		lp1 := p1[i1]
    85  		pr1 := lp1.Ident().ProjectRoot
    86  
    87  		var matched bool
    88  		for i2 := i2next; i2 < len(p2); i2++ {
    89  			lp2 := p2[i2]
    90  			pr2 := lp2.Ident().ProjectRoot
    91  
    92  			switch strings.Compare(string(pr1), string(pr2)) {
    93  			case 0: // Found a matching project
    94  				matched = true
    95  				pdiff := DiffProjects(lp1, lp2)
    96  				if pdiff != nil {
    97  					diff.Modify = append(diff.Modify, *pdiff)
    98  				}
    99  				i2next = i2 + 1 // Don't evaluate to this again
   100  			case +1: // Found a new project
   101  				add := buildLockedProjectDiff(lp2)
   102  				diff.Add = append(diff.Add, add)
   103  				i2next = i2 + 1 // Don't evaluate to this again
   104  				continue        // Keep looking for a matching project
   105  			case -1: // Project has been removed, handled below
   106  				continue
   107  			}
   108  
   109  			break // Done evaluating this project, move onto the next
   110  		}
   111  
   112  		if !matched {
   113  			remove := buildLockedProjectDiff(lp1)
   114  			diff.Remove = append(diff.Remove, remove)
   115  		}
   116  	}
   117  
   118  	// Anything that still hasn't been evaluated are adds
   119  	for i2 := i2next; i2 < len(p2); i2++ {
   120  		lp2 := p2[i2]
   121  		add := buildLockedProjectDiff(lp2)
   122  		diff.Add = append(diff.Add, add)
   123  	}
   124  
   125  	if len(diff.Add) == 0 && len(diff.Remove) == 0 && len(diff.Modify) == 0 {
   126  		return nil // The locks are the equivalent
   127  	}
   128  	return &diff
   129  }
   130  
   131  func buildLockedProjectDiff(lp gps.LockedProject) LockedProjectDiff {
   132  	s2 := lp.Ident().Source
   133  	r2, b2, v2 := gps.VersionComponentStrings(lp.Version())
   134  
   135  	var rev, version, branch, source *StringDiff
   136  	if s2 != "" {
   137  		source = &StringDiff{Previous: s2, Current: s2}
   138  	}
   139  	if r2 != "" {
   140  		rev = &StringDiff{Previous: r2, Current: r2}
   141  	}
   142  	if b2 != "" {
   143  		branch = &StringDiff{Previous: b2, Current: b2}
   144  	}
   145  	if v2 != "" {
   146  		version = &StringDiff{Previous: v2, Current: v2}
   147  	}
   148  
   149  	add := LockedProjectDiff{
   150  		Name:     lp.Ident().ProjectRoot,
   151  		Source:   source,
   152  		Revision: rev,
   153  		Version:  version,
   154  		Branch:   branch,
   155  		Packages: make([]StringDiff, len(lp.Packages())),
   156  	}
   157  	for i, pkg := range lp.Packages() {
   158  		add.Packages[i] = StringDiff{Previous: pkg, Current: pkg}
   159  	}
   160  	return add
   161  }
   162  
   163  // DiffProjects compares two projects and identifies the differences between them.
   164  // Returns nil if there are no differences.
   165  func DiffProjects(lp1, lp2 gps.LockedProject) *LockedProjectDiff {
   166  	diff := LockedProjectDiff{Name: lp1.Ident().ProjectRoot}
   167  
   168  	s1 := lp1.Ident().Source
   169  	s2 := lp2.Ident().Source
   170  	if s1 != s2 {
   171  		diff.Source = &StringDiff{Previous: s1, Current: s2}
   172  	}
   173  
   174  	r1, b1, v1 := gps.VersionComponentStrings(lp1.Version())
   175  	r2, b2, v2 := gps.VersionComponentStrings(lp2.Version())
   176  	if r1 != r2 {
   177  		diff.Revision = &StringDiff{Previous: r1, Current: r2}
   178  	}
   179  	if b1 != b2 {
   180  		diff.Branch = &StringDiff{Previous: b1, Current: b2}
   181  	}
   182  	if v1 != v2 {
   183  		diff.Version = &StringDiff{Previous: v1, Current: v2}
   184  	}
   185  
   186  	p1 := lp1.Packages()
   187  	p2 := lp2.Packages()
   188  	if !sort.StringsAreSorted(p1) {
   189  		p1 = make([]string, len(p1))
   190  		copy(p1, lp1.Packages())
   191  		sort.Strings(p1)
   192  	}
   193  	if !sort.StringsAreSorted(p2) {
   194  		p2 = make([]string, len(p2))
   195  		copy(p2, lp2.Packages())
   196  		sort.Strings(p2)
   197  	}
   198  
   199  	var i2next int
   200  	for i1 := 0; i1 < len(p1); i1++ {
   201  		pkg1 := p1[i1]
   202  
   203  		var matched bool
   204  		for i2 := i2next; i2 < len(p2); i2++ {
   205  			pkg2 := p2[i2]
   206  
   207  			switch strings.Compare(pkg1, pkg2) {
   208  			case 0: // Found matching package
   209  				matched = true
   210  				i2next = i2 + 1 // Don't evaluate to this again
   211  			case +1: // Found a new package
   212  				add := StringDiff{Current: pkg2}
   213  				diff.Packages = append(diff.Packages, add)
   214  				i2next = i2 + 1 // Don't evaluate to this again
   215  				continue        // Keep looking for a match
   216  			case -1: // Package has been removed (handled below)
   217  				continue
   218  			}
   219  
   220  			break // Done evaluating this package, move onto the next
   221  		}
   222  
   223  		if !matched {
   224  			diff.Packages = append(diff.Packages, StringDiff{Previous: pkg1})
   225  		}
   226  	}
   227  
   228  	// Anything that still hasn't been evaluated are adds
   229  	for i2 := i2next; i2 < len(p2); i2++ {
   230  		pkg2 := p2[i2]
   231  		add := StringDiff{Current: pkg2}
   232  		diff.Packages = append(diff.Packages, add)
   233  	}
   234  
   235  	if diff.Source == nil && diff.Version == nil && diff.Revision == nil && len(diff.Packages) == 0 {
   236  		return nil // The projects are equivalent
   237  	}
   238  	return &diff
   239  }
   240  
   241  // sortLockedProjects returns a sorted copy of lps, or itself if already sorted.
   242  func sortLockedProjects(lps []gps.LockedProject) []gps.LockedProject {
   243  	if len(lps) <= 1 || sort.SliceIsSorted(lps, func(i, j int) bool {
   244  		return lps[i].Ident().Less(lps[j].Ident())
   245  	}) {
   246  		return lps
   247  	}
   248  
   249  	cp := make([]gps.LockedProject, len(lps))
   250  	copy(cp, lps)
   251  
   252  	sort.Slice(cp, func(i, j int) bool {
   253  		return cp[i].Ident().Less(cp[j].Ident())
   254  	})
   255  	return cp
   256  }