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  }