github.com/posener/gitfs@v1.2.2-0.20200410105819-ea4e48d73ab9/fsutil/diff.go (about) 1 package fsutil 2 3 import ( 4 "bytes" 5 "io/ioutil" 6 "net/http" 7 "sort" 8 "strings" 9 "text/template" 10 11 "github.com/pkg/errors" 12 "github.com/posener/diff" 13 ) 14 15 const ( 16 msgOnlyInA = "only in {{.A}}" 17 msgOnlyInB = "only in {{.B}}" 18 msgAFileBDir = "on {{.A}} is file, on {{.B}} directory" 19 msgADirBFile = "on {{.A}} is directory, on {{.B}} file" 20 msgContentDiff = "content diff (-{{.A}}, +{{.B}}):" 21 ) 22 23 // FileSystemDiff lists all differences between two filesystems. 24 type FileSystemDiff struct { 25 Diffs []PathDiff 26 // FileSystem names. 27 A, B string 28 } 29 30 // PathDiff is a diff between two filesystems at a single path. 31 type PathDiff struct { 32 Path string 33 Diff string 34 DiffInfo string 35 } 36 37 func (d *FileSystemDiff) template(tmpl string) string { 38 out := bytes.NewBuffer(nil) 39 err := template.Must(template.New("title").Parse(tmpl)).Execute(out, d) 40 if err != nil { 41 panic(err) 42 } 43 return out.String() 44 } 45 46 // String returns pretty representation of a filesystem diff. 47 func (d *FileSystemDiff) String() string { 48 if len(d.Diffs) == 0 { 49 return "" 50 } 51 // Concatenate all differences. 52 out := strings.Builder{} 53 out.WriteString(d.template("Diff between {{.A}} and {{.B}}:\n")) 54 for _, diff := range d.Diffs { 55 out.WriteString("[" + diff.Path + "]: " + d.template(diff.Diff) + "\n") 56 if diff.DiffInfo != "" { 57 out.WriteString(diff.DiffInfo + "\n") 58 } 59 } 60 return out.String() 61 } 62 63 // Diff returns the difference in filesystem structure and file content 64 // between two filesystems. If the implementation of the filesystem is 65 // different but the structure and content are equal, the function will 66 // consider the object as equal. 67 // For equal filesystems, an empty slice will be returned. 68 // The returned differences are ordered by file path. 69 func Diff(a, b http.FileSystem) (*FileSystemDiff, error) { 70 aFiles, err := lsR(a) 71 if err != nil { 72 return nil, errors.Errorf("walking filesystem a: %s", err) 73 } 74 bFiles, err := lsR(b) 75 if err != nil { 76 return nil, errors.Errorf("walking filesystem b: %s", err) 77 } 78 79 d := &FileSystemDiff{A: "a", B: "b"} 80 // Compare two slices of ordered file names. Always compare first element 81 // in each slice and pop the elements from the slice accordingly. 82 for len(aFiles) > 0 || len(bFiles) > 0 { 83 switch { 84 case len(bFiles) == 0 || (len(aFiles) > 0 && aFiles[0] < bFiles[0]): 85 // File exists only in a. 86 path := aFiles[0] 87 d.Diffs = append(d.Diffs, PathDiff{Path: path, Diff: msgOnlyInA}) 88 aFiles = aFiles[1:] 89 case len(aFiles) == 0 || (len(bFiles) > 0 && bFiles[0] < aFiles[0]): 90 // File exists only in b. 91 path := bFiles[0] 92 d.Diffs = append(d.Diffs, PathDiff{Path: path, Diff: msgOnlyInB}) 93 bFiles = bFiles[1:] 94 default: 95 // File exists both in a and in b. 96 path := aFiles[0] 97 diff, err := contentDiff(a, b, path) 98 if err != nil { 99 return nil, err 100 } 101 if diff != nil { 102 d.Diffs = append(d.Diffs, *diff) 103 } 104 aFiles = aFiles[1:] 105 bFiles = bFiles[1:] 106 } 107 } 108 return d, nil 109 } 110 111 // lsR is ls -r. Sorted by name. 112 func lsR(fs http.FileSystem) ([]string, error) { 113 w := Walk(fs, "") 114 var paths []string 115 for w.Step() { 116 paths = append(paths, w.Path()) 117 } 118 if err := w.Err(); err != nil { 119 return nil, err 120 } 121 sort.Strings(paths) 122 return paths, nil 123 } 124 125 func contentDiff(a, b http.FileSystem, path string) (*PathDiff, error) { 126 aF, err := a.Open(path) 127 if err != nil { 128 return nil, errors.Wrapf(err, "open %s in filesystem a", path) 129 } 130 defer aF.Close() 131 132 bF, err := b.Open(path) 133 if err != nil { 134 return nil, errors.Wrapf(err, "open %s in filesystem b", path) 135 } 136 defer bF.Close() 137 138 aSt, err := aF.Stat() 139 if err != nil { 140 return nil, errors.Wrapf(err, "stat %s in filesystem a", path) 141 } 142 143 bSt, err := bF.Stat() 144 if err != nil { 145 return nil, errors.Wrapf(err, "stat %s in filesystem b", path) 146 } 147 148 if aSt.IsDir() || bSt.IsDir() { 149 if !aSt.IsDir() { 150 return &PathDiff{Path: path, Diff: msgAFileBDir}, nil 151 } 152 if !bSt.IsDir() { 153 return &PathDiff{Path: path, Diff: msgADirBFile}, nil 154 } 155 return nil, nil 156 } 157 158 aData, err := ioutil.ReadAll(aF) 159 if err != nil { 160 return nil, errors.Wrapf(err, "reading %s from filesystem a", path) 161 } 162 163 bData, err := ioutil.ReadAll(bF) 164 if err != nil { 165 return nil, errors.Wrapf(err, "reading %s from filesystem b", path) 166 } 167 168 if string(aData) == string(bData) { 169 return nil, nil 170 } 171 d := diff.Format(string(aData), string(bData), diff.OptSuppressCommon()) 172 if d != "" { 173 return &PathDiff{ 174 Path: path, 175 Diff: msgContentDiff, 176 DiffInfo: strings.TrimRight(d, "\n"), 177 }, nil 178 } 179 return nil, nil 180 }