github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/util/diff/diff.go (about) 1 // Copyright 2019 Google LLC 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 diff contains libraries for diffing packages. 16 package diff 17 18 import ( 19 "context" 20 "fmt" 21 "io" 22 "os" 23 "os/exec" 24 "path/filepath" 25 "strings" 26 27 "github.com/GoogleContainerTools/kpt/internal/gitutil" 28 "github.com/GoogleContainerTools/kpt/internal/pkg" 29 "github.com/GoogleContainerTools/kpt/internal/util/addmergecomment" 30 "github.com/GoogleContainerTools/kpt/internal/util/fetch" 31 "github.com/GoogleContainerTools/kpt/internal/util/pkgutil" 32 kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 33 "github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" 34 "sigs.k8s.io/kustomize/kyaml/errors" 35 "sigs.k8s.io/kustomize/kyaml/filesys" 36 ) 37 38 // Type represents type of diff comparison to be performed. 39 type Type string 40 41 const ( 42 // TypeLocal shows the changes in local pkg relative to upstream source pkg at original version 43 TypeLocal Type = "local" 44 // TypeRemote shows changes in the upstream source pkg between original and target version 45 TypeRemote Type = "remote" 46 // TypeCombined shows changes in local pkg relative to upstream source pkg at target version 47 TypeCombined Type = "combined" 48 // 3way shows changes in local and remote changes side-by-side 49 Type3Way Type = "3way" 50 ) 51 52 // A collection of user-readable "source" definitions for diffed packages. 53 const ( 54 // localPackageSource represents the local package 55 LocalPackageSource string = "local" 56 // remotePackageSource represents the remote version of the package 57 RemotePackageSource string = "remote" 58 // targetRemotePackageSource represents the targeted remote version of a package 59 TargetRemotePackageSource string = "target" 60 ) 61 62 const ( 63 exitCodeDiffWarning string = "\nThe selected diff tool (%s) exited with an " + 64 "error. It may not support the chosen diff type (%s). To use a different " + 65 "diff tool please provide the tool using the --diff-tool flag. \n\nFor " + 66 "more information about using kpt's diff command please see the commands " + 67 "--help.\n" 68 ) 69 70 // String implements Stringer. 71 func (dt Type) String() string { 72 return string(dt) 73 } 74 75 var SupportedDiffTypes = []Type{TypeLocal, TypeRemote, TypeCombined, Type3Way} 76 77 func SupportedDiffTypesLabel() string { 78 var labels []string 79 for _, dt := range SupportedDiffTypes { 80 labels = append(labels, dt.String()) 81 } 82 return strings.Join(labels, ", ") 83 } 84 85 // Command shows changes in local package relative to upstream source pkg, changes in 86 // upstream source package between original and target version etc. 87 type Command struct { 88 // Path to the local package directory 89 Path string 90 91 // Ref is the target Ref in the upstream source package to compare against 92 Ref string 93 94 // DiffType specifies the type of changes to show 95 DiffType Type 96 97 // Difftool refers to diffing commandline tool for showing changes. 98 DiffTool string 99 100 // DiffToolOpts refers to the commandline options to for the diffing tool. 101 DiffToolOpts string 102 103 // When Debug is true, command will run with verbose logging and will not 104 // cleanup the staged packages to assist with debugging. 105 Debug bool 106 107 // Output is an io.Writer where command will write the output of the 108 // command. 109 Output io.Writer 110 111 // PkgDiffer specifies package differ 112 PkgDiffer PkgDiffer 113 114 // PkgGetter specifies packaging sourcing adapter 115 PkgGetter PkgGetter 116 } 117 118 func (c *Command) Run(ctx context.Context) error { 119 c.DefaultValues() 120 121 kptFile, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, c.Path) 122 if err != nil { 123 return errors.Errorf("package missing Kptfile at '%s': %v", c.Path, err) 124 } 125 126 // Return early if upstream is not set 127 if kptFile.Upstream == nil || kptFile.Upstream.Git == nil { 128 return errors.Errorf("package missing upstream in Kptfile at '%s'", c.Path) 129 } 130 131 // Create a staging directory to store all compared packages 132 stagingDirectory, err := os.MkdirTemp("", "kpt-") 133 if err != nil { 134 return errors.Errorf("failed to create stage dir: %v", err) 135 } 136 defer func() { 137 // Cleanup staged content after diff. Ignore cleanup if debugging. 138 if !c.Debug { 139 defer os.RemoveAll(stagingDirectory) 140 } 141 }() 142 143 // Stage current package 144 // This prevents prepareForDiff from modifying the local package 145 localPkgName := NameStagingDirectory(LocalPackageSource, 146 kptFile.Upstream.Git.Ref) 147 currPkg, err := stageDirectory(stagingDirectory, localPkgName) 148 if err != nil { 149 return errors.Errorf("failed to create stage dir for current package: %v", err) 150 } 151 152 err = pkgutil.CopyPackage(c.Path, currPkg, true, pkg.Local) 153 if err != nil { 154 return errors.Errorf("failed to stage current package: %v", err) 155 } 156 157 // get the upstreamPkg at current version 158 upstreamPkgName := NameStagingDirectory(RemotePackageSource, 159 kptFile.Upstream.Git.Ref) 160 upstreamPkg, err := c.PkgGetter.GetPkg(ctx, 161 stagingDirectory, 162 upstreamPkgName, 163 kptFile.Upstream.Git.Repo, 164 kptFile.Upstream.Git.Directory, 165 kptFile.Upstream.Git.Ref) 166 if err != nil { 167 return err 168 } 169 170 var upstreamTargetPkg string 171 172 if c.Ref == "" { 173 gur, err := gitutil.NewGitUpstreamRepo(ctx, kptFile.UpstreamLock.Git.Repo) 174 if err != nil { 175 return err 176 } 177 c.Ref, err = gur.GetDefaultBranch(ctx) 178 if err != nil { 179 return err 180 } 181 } 182 183 if c.DiffType == TypeRemote || 184 c.DiffType == TypeCombined || 185 c.DiffType == Type3Way { 186 // get the upstream pkg at the target version 187 upstreamTargetPkgName := NameStagingDirectory(TargetRemotePackageSource, 188 c.Ref) 189 upstreamTargetPkg, err = c.PkgGetter.GetPkg(ctx, stagingDirectory, 190 upstreamTargetPkgName, 191 kptFile.Upstream.Git.Repo, 192 kptFile.Upstream.Git.Directory, 193 c.Ref) 194 if err != nil { 195 return err 196 } 197 } 198 199 if c.Debug { 200 fmt.Fprintf(c.Output, "diffing currPkg: %v, upstreamPkg: %v, upstreamTargetPkg: %v \n", 201 currPkg, upstreamPkg, upstreamTargetPkg) 202 } 203 204 switch c.DiffType { 205 case TypeLocal: 206 return c.PkgDiffer.Diff(currPkg, upstreamPkg) 207 case TypeRemote: 208 return c.PkgDiffer.Diff(upstreamPkg, upstreamTargetPkg) 209 case TypeCombined: 210 return c.PkgDiffer.Diff(currPkg, upstreamTargetPkg) 211 case Type3Way: 212 return c.PkgDiffer.Diff(currPkg, upstreamPkg, upstreamTargetPkg) 213 default: 214 return errors.Errorf("unsupported diff type '%s'", c.DiffType) 215 } 216 } 217 218 func (c *Command) Validate() error { 219 switch c.DiffType { 220 case TypeLocal, TypeCombined, TypeRemote, Type3Way: 221 default: 222 return errors.Errorf("invalid diff-type '%s': supported diff-types are: %s", 223 c.DiffType, SupportedDiffTypesLabel()) 224 } 225 226 path, err := exec.LookPath(c.DiffTool) 227 if err != nil { 228 return errors.Errorf("diff-tool '%s' not found in the PATH", c.DiffTool) 229 } 230 c.DiffTool = path 231 return nil 232 } 233 234 // DefaultValues sets up the default values for the command. 235 func (c *Command) DefaultValues() { 236 if c.Output == nil { 237 c.Output = os.Stdout 238 } 239 if c.PkgGetter == nil { 240 c.PkgGetter = defaultPkgGetter{} 241 } 242 if c.PkgDiffer == nil { 243 c.PkgDiffer = &defaultPkgDiffer{ 244 DiffType: c.DiffType, 245 DiffTool: c.DiffTool, 246 DiffToolOpts: c.DiffToolOpts, 247 Debug: c.Debug, 248 Output: c.Output, 249 } 250 } 251 } 252 253 // PkgDiffer knows how to compare given packages. 254 type PkgDiffer interface { 255 Diff(pkgs ...string) error 256 } 257 258 type defaultPkgDiffer struct { 259 // DiffType specifies the type of changes to show 260 DiffType Type 261 262 // Difftool refers to diffing commandline tool for showing changes. 263 DiffTool string 264 265 // DiffToolOpts refers to the commandline options to for the diffing tool. 266 DiffToolOpts string 267 268 // When Debug is true, command will run with verbose logging and will not 269 // cleanup the staged packages to assist with debugging. 270 Debug bool 271 272 // Output is an io.Writer where command will write the output of the 273 // command. 274 Output io.Writer 275 } 276 277 func (d *defaultPkgDiffer) Diff(pkgs ...string) error { 278 // add merge comments before comparing so that there are no unwanted diffs 279 if err := addmergecomment.Process(pkgs...); err != nil { 280 return err 281 } 282 for _, pkg := range pkgs { 283 if err := d.prepareForDiff(pkg); err != nil { 284 return err 285 } 286 } 287 var args []string 288 if d.DiffToolOpts != "" { 289 args = strings.Split(d.DiffToolOpts, " ") 290 args = append(args, pkgs...) 291 } else { 292 args = pkgs 293 } 294 cmd := exec.Command(d.DiffTool, args...) 295 cmd.Stdout = d.Output 296 cmd.Stderr = d.Output 297 298 if d.Debug { 299 fmt.Fprintf(d.Output, "%s\n", strings.Join(cmd.Args, " ")) 300 } 301 err := cmd.Run() 302 if err != nil { 303 exitErr, ok := err.(*exec.ExitError) 304 if ok && exitErr.ExitCode() == 1 { 305 // diff tool will exit with return code 1 if there are differences 306 // between two dirs. This suppresses those errors. 307 err = nil 308 } else if ok { 309 // An error occurred but was not one of the excluded ones 310 // Attempt to display help information to assist with resolving 311 fmt.Printf(exitCodeDiffWarning, d.DiffTool, d.DiffType) 312 } 313 } 314 return err 315 } 316 317 // prepareForDiff removes metadata such as .git and Kptfile from a staged package 318 // to exclude them from diffing. 319 func (d *defaultPkgDiffer) prepareForDiff(dir string) error { 320 excludePaths := []string{".git", kptfilev1.KptFileName} 321 for _, path := range excludePaths { 322 path = filepath.Join(dir, path) 323 if err := os.RemoveAll(path); err != nil { 324 return err 325 } 326 } 327 return nil 328 } 329 330 // PkgGetter knows how to fetch a package given a git repo, path and ref. 331 type PkgGetter interface { 332 GetPkg(ctx context.Context, stagingDir, targetDir, repo, path, ref string) (dir string, err error) 333 } 334 335 // defaultPkgGetter uses fetch.Command abstraction to implement PkgGetter. 336 type defaultPkgGetter struct{} 337 338 // GetPkg checks out a repository into a temporary directory for diffing 339 // and returns the directory containing the checked out package or an error. 340 // repo is the git repository the package was cloned from. e.g. https:// 341 // path is the sub directory of the git repository that the package was cloned from 342 // ref is the git ref the package was cloned from 343 func (pg defaultPkgGetter) GetPkg(ctx context.Context, stagingDir, targetDir, repo, path, ref string) (string, error) { 344 dir, err := stageDirectory(stagingDir, targetDir) 345 if err != nil { 346 return dir, err 347 } 348 349 name := filepath.Base(dir) 350 kf := kptfileutil.DefaultKptfile(name) 351 kf.Upstream = &kptfilev1.Upstream{ 352 Type: kptfilev1.GitOrigin, 353 Git: &kptfilev1.Git{ 354 Repo: repo, 355 Directory: path, 356 Ref: ref, 357 }, 358 } 359 err = kptfileutil.WriteFile(dir, kf) 360 if err != nil { 361 return dir, err 362 } 363 364 p, err := pkg.New(filesys.FileSystemOrOnDisk{}, dir) 365 if err != nil { 366 return dir, err 367 } 368 369 cmdGet := &fetch.Command{ 370 Pkg: p, 371 } 372 err = cmdGet.Run(ctx) 373 return dir, err 374 } 375 376 // stageDirectory creates a subdirectory of the provided path for temporary operations 377 // path is the parent staged directory and should already exist 378 // subpath is the subdirectory that should be created inside path 379 func stageDirectory(path, subpath string) (string, error) { 380 targetPath := filepath.Join(path, subpath) 381 err := os.Mkdir(targetPath, os.ModePerm) 382 return targetPath, err 383 } 384 385 // NameStagingDirectory assigns a name that matches the package source information 386 func NameStagingDirectory(source, ref string) string { 387 // Using tags may result in references like /refs/tags/version 388 // To avoid creating additional directory's use only the last name after a / 389 splitRef := strings.Split(ref, "/") 390 reducedRef := splitRef[len(splitRef)-1] 391 392 return fmt.Sprintf("%s-%s", 393 source, 394 reducedRef) 395 }