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 }