github.com/sdboyer/gps@v0.16.3/lockdiff.go (about)

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