1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
|
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// The copyutil package contains libraries for copying directories of configuration.
package copyutil
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/sergi/go-diff/diffmatchpatch"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/filesys"
"sigs.k8s.io/kustomize/kyaml/sets"
)
// CopyDir copies a src directory to a dst directory. CopyDir skips copying the .git directory from the src.
func CopyDir(fSys filesys.FileSystem, src string, dst string) error {
return errors.Wrap(fSys.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// don't copy the .git dir
if path != src {
rel := strings.TrimPrefix(path, src)
if IsDotGitFolder(rel) {
return nil
}
}
// path is an absolute path, rather than a path relative to src.
// e.g. if src is /path/to/package, then path might be /path/to/package/and/sub/dir
// we need the path relative to src `and/sub/dir` when we are copying the files to dest.
copyTo := strings.TrimPrefix(path, src)
// make directories that don't exist
if info.IsDir() {
return errors.Wrap(fSys.MkdirAll(filepath.Join(dst, copyTo)))
}
// copy file by reading and writing it
b, err := fSys.ReadFile(filepath.Join(src, copyTo))
if err != nil {
return errors.Wrap(err)
}
err = fSys.WriteFile(filepath.Join(dst, copyTo), b)
if err != nil {
return errors.Wrap(err)
}
return nil
}))
}
// Diff returns a list of files that differ between the source and destination.
//
// Diff is guaranteed to return a non-empty set if any files differ, but
// this set is not guaranteed to contain all differing files.
func Diff(sourceDir, destDir string) (sets.String, error) {
// get set of filenames in the package source
upstreamFiles := sets.String{}
err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// skip git repo if it exists
if IsDotGitFolder(path) {
return nil
}
upstreamFiles.Insert(strings.TrimPrefix(strings.TrimPrefix(path, sourceDir), string(filepath.Separator)))
return nil
})
if err != nil {
return sets.String{}, err
}
// get set of filenames in the cloned package
localFiles := sets.String{}
err = filepath.Walk(destDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// skip git repo if it exists
if IsDotGitFolder(path) {
return nil
}
localFiles.Insert(strings.TrimPrefix(strings.TrimPrefix(path, destDir), string(filepath.Separator)))
return nil
})
if err != nil {
return sets.String{}, err
}
// verify the source and cloned packages have the same set of filenames
diff := upstreamFiles.SymmetricDifference(localFiles)
// verify file contents match
for _, f := range upstreamFiles.Intersection(localFiles).List() {
fi, err := os.Stat(filepath.Join(destDir, f))
if err != nil {
return diff, err
}
if fi.Mode().IsDir() {
// already checked that this directory exists in the local files
continue
}
// compare upstreamFiles
b1, err := os.ReadFile(filepath.Join(destDir, f))
if err != nil {
return diff, err
}
b2, err := os.ReadFile(filepath.Join(sourceDir, f))
if err != nil {
return diff, err
}
if !bytes.Equal(b1, b2) {
fmt.Println(PrettyFileDiff(string(b1), string(b2)))
diff.Insert(f)
}
}
// return the differing files
return diff, nil
}
// IsDotGitFolder checks if the provided path is either the .git folder or
// a file underneath the .git folder.
func IsDotGitFolder(path string) bool {
cleanPath := filepath.ToSlash(filepath.Clean(path))
for _, c := range strings.Split(cleanPath, "/") {
if c == ".git" {
return true
}
}
return false
}
// PrettyFileDiff takes the content of two files and returns the pretty diff
func PrettyFileDiff(s1, s2 string) string {
dmp := diffmatchpatch.New()
wSrc, wDst, warray := dmp.DiffLinesToRunes(s1, s2)
diffs := dmp.DiffMainRunes(wSrc, wDst, false)
diffs = dmp.DiffCharsToLines(diffs, warray)
return dmp.DiffPrettyText(diffs)
}
// SyncFile copies file from src file path to a dst file path by replacement
// deletes dst file if src file doesn't exist
func SyncFile(src, dst string) error {
srcFileInfo, err := os.Stat(src)
if err != nil {
// delete dst if source doesn't exist
if err = deleteFile(dst); err != nil {
return err
}
return nil
}
input, err := os.ReadFile(src)
if err != nil {
return err
}
var filePerm os.FileMode
// get the destination file perm if file exists
dstFileInfo, err := os.Stat(dst)
if err != nil {
// get source file perm if destination file doesn't exist
filePerm = srcFileInfo.Mode().Perm()
} else {
filePerm = dstFileInfo.Mode().Perm()
}
err = os.WriteFile(dst, input, filePerm)
if err != nil {
return err
}
return nil
}
// deleteFile deletes file from path, returns no error if file doesn't exist
func deleteFile(path string) error {
if _, err := os.Stat(path); err != nil {
// return nil if file doesn't exist
return nil
}
return os.Remove(path)
}
|