go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/proto/srcman/diff.go (about) 1 // Copyright 2017 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package srcman 16 17 import ( 18 "reflect" 19 "strings" 20 ) 21 22 // zeroCmpTwo compares two objects of the same type where the objects could be 23 // pointers, slices or strings. 24 // 25 // It generates a Stat value of: 26 // - EQUAL if they're both nil/empty/zero 27 // - ADDED if a is nil and b is not 28 // - REMOVED if b is nil and a is not 29 // - For strings: 30 // - EQUAL/MODIFIED depending on string equality 31 // - For slices: 32 // - MODIFIED if they have differing lengths 33 // - Whatever the callback method returns 34 // - For others 35 // - Whatever the callback method returns 36 // 37 // The callback method is required for non-string types. 38 func zeroCmpTwo(a, b any, modCb func() ManifestDiff_Stat) ManifestDiff_Stat { 39 av, bv := reflect.ValueOf(a), reflect.ValueOf(b) 40 t := av.Type() 41 if t != bv.Type() { 42 panic("inconsistent types") 43 } 44 var az, bz bool 45 if t.Kind() == reflect.Slice || t.Kind() == reflect.String { 46 az, bz = av.Len() == 0, bv.Len() == 0 47 } else { 48 z := reflect.Zero(t) 49 az, bz = av == z, bv == z 50 } 51 52 switch { 53 case az && bz: 54 return ManifestDiff_EQUAL 55 56 case az && !bz: 57 return ManifestDiff_ADDED 58 59 case !az && bz: 60 return ManifestDiff_REMOVED 61 62 default: 63 switch t.Kind() { 64 case reflect.String: 65 if av.String() == bv.String() { 66 return ManifestDiff_EQUAL 67 } 68 return ManifestDiff_MODIFIED 69 70 case reflect.Slice: 71 if av.Len() != bv.Len() { 72 return ManifestDiff_MODIFIED 73 } 74 } 75 return modCb() 76 } 77 } 78 79 // modifiedTracker is a simplistic structure. It: 80 // - tracks the status of anything you feed to add(). If you feed a status 81 // other than EQUAL, it flips to true. 82 // - returns MODIFIED from status() if its bool value is true, otherwise EQUAL. 83 // 84 // It's used for semi-transparently computing `Overall` Stat values for diff 85 // entries containing many interesting fields. 86 type modifiedTracker bool 87 88 // add incorporates `st` into this modifiedTracker's state. 89 func (c *modifiedTracker) add(st ManifestDiff_Stat) ManifestDiff_Stat { 90 *c = *c || st != ManifestDiff_EQUAL 91 return st 92 } 93 94 // status returns MODIFIED or EQUAL, depending on if the tracked state is true 95 // or false. 96 func (c modifiedTracker) status() ManifestDiff_Stat { 97 if c { 98 return ManifestDiff_MODIFIED 99 } 100 return ManifestDiff_EQUAL 101 } 102 103 func parseChangeRef(ref string) (cl string) { 104 if !strings.HasPrefix(ref, "refs/changes/") { 105 return "" 106 } 107 parts := strings.Split(ref, "/")[2:] // remove refs/changes 108 // should be NN/YYYYYNN/ZZ 109 if len(parts) != 3 { 110 return "" 111 } 112 return parts[1] 113 } 114 115 // Diff generates a Stat reflecting the difference between the `old` 116 // GitCheckout and the `new` one. 117 // 118 // This will generate a Stat of `DIFF` if the two GitCheckout's are non-nil and 119 // share the same RepoUrl. 120 // 121 // This only calculates the pure-data differences. Notably, this will not reach 122 // out to any remote services to populate the git_history field. 123 func (old *Manifest_GitCheckout) Diff(new *Manifest_GitCheckout) *ManifestDiff_GitCheckout { 124 if old == nil && new == nil { 125 return nil 126 } 127 128 ret := &ManifestDiff_GitCheckout{} 129 130 ret.Overall = zeroCmpTwo(old, new, func() ManifestDiff_Stat { 131 if old.RepoUrl == new.RepoUrl { 132 // For now, the canonical 'diff' URL is always the new URL. If we add 133 // support for source-of-truth migrations, this could change. 134 ret.RepoUrl = new.RepoUrl 135 136 // FetchRef doesn't matter for comparison purposes for now. 137 ret.Revision = zeroCmpTwo(old.Revision, new.Revision, nil) 138 if ret.Revision == ManifestDiff_MODIFIED { 139 ret.Revision = ManifestDiff_DIFF 140 } 141 142 // We calculate DIFF for PatchRevision iff the two checkouts both include 143 // patches from the same CL. 144 ret.PatchRevision = zeroCmpTwo(old.PatchRevision, new.PatchRevision, nil) 145 if ret.PatchRevision == ManifestDiff_MODIFIED { 146 oldCL, newCL := parseChangeRef(old.PatchFetchRef), parseChangeRef(new.PatchFetchRef) 147 if oldCL != "" && newCL != "" && oldCL == newCL { 148 ret.PatchRevision = ManifestDiff_DIFF 149 } 150 } 151 152 // If all the revisions are the same and the repo url is the same, that's 153 // good enough for the whole checkout to be equal. 154 if ret.Revision == ManifestDiff_EQUAL && ret.PatchRevision == ManifestDiff_EQUAL { 155 return ManifestDiff_EQUAL 156 } 157 } 158 159 return ManifestDiff_MODIFIED 160 }) 161 162 return ret 163 } 164 165 // Diff generates a ManifestDiff_Stat reflecting the difference between the `old` 166 // CIPDPackage and the `new` one. 167 func (old *Manifest_CIPDPackage) Diff(new *Manifest_CIPDPackage) ManifestDiff_Stat { 168 return zeroCmpTwo(old, new, func() ManifestDiff_Stat { 169 // Version and PackagePattern don't matter for diff purposes. 170 return zeroCmpTwo(old.InstanceId, new.InstanceId, nil) 171 }) 172 } 173 174 // Diff generates a ManifestDiff_Directory object which shows what changed 175 // between the `old` manifest directory and the `new` manifest directory. 176 func (old *Manifest_Directory) Diff(new *Manifest_Directory) *ManifestDiff_Directory { 177 ret := &ManifestDiff_Directory{} 178 179 ret.Overall = zeroCmpTwo(old, new, func() ManifestDiff_Stat { 180 var dirChanged modifiedTracker 181 182 if ret.GitCheckout = old.GitCheckout.Diff(new.GitCheckout); ret.GitCheckout != nil { 183 dirChanged.add(ret.GitCheckout.Overall) 184 } 185 186 ret.CipdServerHost = dirChanged.add(zeroCmpTwo(old.CipdServerHost, new.CipdServerHost, nil)) 187 if ret.CipdServerHost == ManifestDiff_EQUAL && new.CipdServerHost != "" { 188 cipdPackages := map[string]ManifestDiff_Stat{} 189 190 for name, pkg := range old.CipdPackage { 191 cipdPackages[name] = dirChanged.add(pkg.Diff(new.CipdPackage[name])) 192 } 193 for name, pkg := range new.CipdPackage { 194 if _, ok := cipdPackages[name]; !ok { 195 cipdPackages[name] = dirChanged.add(old.CipdPackage[name].Diff(pkg)) 196 } 197 } 198 199 if len(cipdPackages) > 0 { 200 ret.CipdPackage = cipdPackages 201 } 202 } 203 204 return dirChanged.status() 205 }) 206 207 return ret 208 } 209 210 // Diff generates a ManifestDiff object which shows what changed between the 211 // `old` manifest and the `new` manifest. 212 // 213 // This only calculates the pure-data differences. Notably, this will not reach 214 // out to any remote services to populate the git_history field. 215 func (old *Manifest) Diff(new *Manifest) *ManifestDiff { 216 ret := &ManifestDiff{ 217 Old: old, 218 New: new, 219 Directories: map[string]*ManifestDiff_Directory{}, 220 } 221 ret.Overall = zeroCmpTwo(old, new, func() ManifestDiff_Stat { 222 var anyChanged modifiedTracker 223 224 for path, olddir := range old.Directories { 225 dir := olddir.Diff(new.Directories[path]) 226 ret.Directories[path] = dir 227 anyChanged.add(dir.Overall) 228 } 229 for path, newdir := range new.Directories { 230 if _, ok := ret.Directories[path]; !ok { 231 dir := old.Directories[path].Diff(newdir) 232 ret.Directories[path] = dir 233 anyChanged.add(dir.Overall) 234 } 235 } 236 237 return anyChanged.status() 238 }) 239 return ret 240 }