github.com/sdboyer/gps@v0.16.3/version_unifier.go (about) 1 package gps 2 3 // versionUnifier facilitates cross-type version comparison and set operations. 4 type versionUnifier struct { 5 b sourceBridge 6 mtr *metrics 7 } 8 9 // pairVersion takes an UnpairedVersion and attempts to pair it with an 10 // underlying Revision in the context of the provided ProjectIdentifier by 11 // consulting the canonical version list. 12 func (vu versionUnifier) pairVersion(id ProjectIdentifier, v UnpairedVersion) PairedVersion { 13 vl, err := vu.b.listVersions(id) 14 if err != nil { 15 return nil 16 } 17 18 vu.mtr.push("b-pair-version") 19 // doing it like this is a bit sloppy 20 for _, v2 := range vl { 21 if p, ok := v2.(PairedVersion); ok { 22 if p.Matches(v) { 23 vu.mtr.pop() 24 return p 25 } 26 } 27 } 28 29 vu.mtr.pop() 30 return nil 31 } 32 33 // pairRevision takes a Revision and attempts to pair it with all possible 34 // versionsby consulting the canonical version list of the provided 35 // ProjectIdentifier. 36 func (vu versionUnifier) pairRevision(id ProjectIdentifier, r Revision) []Version { 37 vl, err := vu.b.listVersions(id) 38 if err != nil { 39 return nil 40 } 41 42 vu.mtr.push("b-pair-rev") 43 p := []Version{r} 44 // doing it like this is a bit sloppy 45 for _, v2 := range vl { 46 if pv, ok := v2.(PairedVersion); ok { 47 if pv.Matches(r) { 48 p = append(p, pv) 49 } 50 } 51 } 52 53 vu.mtr.pop() 54 return p 55 } 56 57 // matches performs a typical match check between the provided version and 58 // constraint. If that basic check fails and the provided version is incomplete 59 // (e.g. an unpaired version or bare revision), it will attempt to gather more 60 // information on one or the other and re-perform the comparison. 61 func (vu versionUnifier) matches(id ProjectIdentifier, c Constraint, v Version) bool { 62 if c.Matches(v) { 63 return true 64 } 65 66 vu.mtr.push("b-matches") 67 // This approach is slightly wasteful, but just SO much less verbose, and 68 // more easily understood. 69 vtu := vu.createTypeUnion(id, v) 70 71 var uc Constraint 72 if cv, ok := c.(Version); ok { 73 uc = vu.createTypeUnion(id, cv) 74 } else { 75 uc = c 76 } 77 78 vu.mtr.pop() 79 return uc.Matches(vtu) 80 } 81 82 // matchesAny is the authoritative version of Constraint.MatchesAny. 83 func (vu versionUnifier) matchesAny(id ProjectIdentifier, c1, c2 Constraint) bool { 84 if c1.MatchesAny(c2) { 85 return true 86 } 87 88 vu.mtr.push("b-matches-any") 89 // This approach is slightly wasteful, but just SO much less verbose, and 90 // more easily understood. 91 var uc1, uc2 Constraint 92 if v1, ok := c1.(Version); ok { 93 uc1 = vu.createTypeUnion(id, v1) 94 } else { 95 uc1 = c1 96 } 97 98 if v2, ok := c2.(Version); ok { 99 uc2 = vu.createTypeUnion(id, v2) 100 } else { 101 uc2 = c2 102 } 103 104 vu.mtr.pop() 105 return uc1.MatchesAny(uc2) 106 } 107 108 // intersect is the authoritative version of Constraint.Intersect. 109 func (vu versionUnifier) intersect(id ProjectIdentifier, c1, c2 Constraint) Constraint { 110 rc := c1.Intersect(c2) 111 if rc != none { 112 return rc 113 } 114 115 vu.mtr.push("b-intersect") 116 // This approach is slightly wasteful, but just SO much less verbose, and 117 // more easily understood. 118 var uc1, uc2 Constraint 119 if v1, ok := c1.(Version); ok { 120 uc1 = vu.createTypeUnion(id, v1) 121 } else { 122 uc1 = c1 123 } 124 125 if v2, ok := c2.(Version); ok { 126 uc2 = vu.createTypeUnion(id, v2) 127 } else { 128 uc2 = c2 129 } 130 131 vu.mtr.pop() 132 return uc1.Intersect(uc2) 133 } 134 135 // createTypeUnion creates a versionTypeUnion for the provided version. 136 // 137 // This union may (and typically will) end up being nothing more than the single 138 // input version, but creating a versionTypeUnion guarantees that 'local' 139 // constraint checks (direct method calls) are authoritative. 140 func (vu versionUnifier) createTypeUnion(id ProjectIdentifier, v Version) versionTypeUnion { 141 switch tv := v.(type) { 142 case Revision: 143 return versionTypeUnion(vu.pairRevision(id, tv)) 144 case PairedVersion: 145 return versionTypeUnion(vu.pairRevision(id, tv.Underlying())) 146 case UnpairedVersion: 147 pv := vu.pairVersion(id, tv) 148 if pv == nil { 149 return versionTypeUnion{tv} 150 } 151 152 return versionTypeUnion(vu.pairRevision(id, pv.Underlying())) 153 } 154 155 return nil 156 } 157 158 // versionTypeUnion represents a set of versions that are, within the scope of 159 // this solver run, equivalent. 160 // 161 // The simple case here is just a pair - a normal version plus its underlying 162 // revision - but if a tag or branch point at the same rev, then we consider 163 // them equivalent. Again, however, this equivalency is short-lived; it must be 164 // re-assessed during every solver run. 165 // 166 // The union members are treated as being OR'd together: all constraint 167 // operations attempt each member, and will take the most open/optimistic 168 // answer. 169 // 170 // This technically does allow tags to match branches - something we otherwise 171 // try hard to avoid - but because the original input constraint never actually 172 // changes (and is never written out in the Solution), there's no harmful case 173 // of a user suddenly riding a branch when they expected a fixed tag. 174 type versionTypeUnion []Version 175 176 // This should generally not be called, but is required for the interface. If it 177 // is called, we have a bigger problem (the type has escaped the solver); thus, 178 // panic. 179 func (vtu versionTypeUnion) String() string { 180 panic("versionTypeUnion should never be turned into a string; it is solver internal-only") 181 } 182 183 func (vtu versionTypeUnion) typedString() string { 184 panic("versionTypeUnion should never be turned into a string; it is solver internal-only") 185 } 186 187 // This should generally not be called, but is required for the interface. If it 188 // is called, we have a bigger problem (the type has escaped the solver); thus, 189 // panic. 190 func (vtu versionTypeUnion) Type() VersionType { 191 panic("versionTypeUnion should never need to answer a Type() call; it is solver internal-only") 192 } 193 194 // Matches takes a version, and returns true if that version matches any version 195 // contained in the union. 196 // 197 // This DOES allow tags to match branches, albeit indirectly through a revision. 198 func (vtu versionTypeUnion) Matches(v Version) bool { 199 vtu2, otherIs := v.(versionTypeUnion) 200 201 for _, v1 := range vtu { 202 if otherIs { 203 for _, v2 := range vtu2 { 204 if v1.Matches(v2) { 205 return true 206 } 207 } 208 } else if v1.Matches(v) { 209 return true 210 } 211 } 212 213 return false 214 } 215 216 // MatchesAny returns true if any of the contained versions (which are also 217 // constraints) in the union successfully MatchAny with the provided 218 // constraint. 219 func (vtu versionTypeUnion) MatchesAny(c Constraint) bool { 220 vtu2, otherIs := c.(versionTypeUnion) 221 222 for _, v1 := range vtu { 223 if otherIs { 224 for _, v2 := range vtu2 { 225 if v1.MatchesAny(v2) { 226 return true 227 } 228 } 229 } else if v1.MatchesAny(c) { 230 return true 231 } 232 } 233 234 return false 235 } 236 237 // Intersect takes a constraint, and attempts to intersect it with all the 238 // versions contained in the union until one returns non-none. If that never 239 // happens, then none is returned. 240 // 241 // In order to avoid weird version floating elsewhere in the solver, the union 242 // always returns the input constraint. (This is probably obviously correct, but 243 // is still worth noting.) 244 func (vtu versionTypeUnion) Intersect(c Constraint) Constraint { 245 vtu2, otherIs := c.(versionTypeUnion) 246 247 for _, v1 := range vtu { 248 if otherIs { 249 for _, v2 := range vtu2 { 250 if rc := v1.Intersect(v2); rc != none { 251 return rc 252 } 253 } 254 } else if rc := v1.Intersect(c); rc != none { 255 return rc 256 } 257 } 258 259 return none 260 }