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 }