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  }