github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/bundle/diff.go (about)

     1  /*
     2   * Copyright (c) 2018-2020 vChain, Inc. All Rights Reserved.
     3   * This software is released under GPL3.
     4   * The full license information can be found under:
     5   * https://www.gnu.org/licenses/gpl-3.0.en.html
     6   *
     7   */
     8  
     9  package bundle
    10  
    11  import (
    12  	"fmt"
    13  	"strings"
    14  
    15  	"github.com/dustin/go-humanize"
    16  	"github.com/google/go-cmp/cmp"
    17  	digest "github.com/opencontainers/go-digest"
    18  )
    19  
    20  // Diff returns a human-readable report as string of the raw differences between m and x.
    21  //
    22  // Do not depend on this output being stable.
    23  func (m Manifest) Diff(x Manifest) (report string, equal bool, err error) {
    24  	var r diffReporter
    25  	equal = cmp.Equal(x, m, cmp.Reporter(&r))
    26  	report = r.String()
    27  	return
    28  }
    29  
    30  type diffReporter struct {
    31  	path  cmp.Path
    32  	lines []string
    33  }
    34  
    35  func (r *diffReporter) PushStep(ps cmp.PathStep) {
    36  	r.path = append(r.path, ps)
    37  }
    38  
    39  func (r *diffReporter) Report(rs cmp.Result) {
    40  	if !rs.Equal() {
    41  		vx, vy := r.path.Last().Values()
    42  		var line string
    43  		switch true {
    44  		case !vx.IsValid():
    45  			line = fmt.Sprintf("%#v:\n\t+: %+v\n", r.path, vy)
    46  		case !vy.IsValid():
    47  			line = fmt.Sprintf("%#v:\n\t-: %+v\n", r.path, vx)
    48  		default:
    49  			line = fmt.Sprintf("%#v:\n\t-: %+v\n\t+: %+v\n", r.path, vx, vy)
    50  		}
    51  		r.lines = append(r.lines, line)
    52  	}
    53  }
    54  
    55  func (r *diffReporter) PopStep() {
    56  	r.path = r.path[:len(r.path)-1]
    57  }
    58  
    59  func (r *diffReporter) String() string {
    60  	return strings.Join(r.lines, "\n")
    61  }
    62  
    63  // DiffByPath returns a human-readable report as string containing
    64  // additions, modifications, renamings, deletions of x.Items relative to m.Items
    65  // listed by path.
    66  //
    67  // Do not depend on this output being stable.
    68  func (m Manifest) DiffByPath(x Manifest) (report string, equal bool, err error) {
    69  	type modDiff struct {
    70  		path string
    71  		from Descriptor
    72  		to   Descriptor
    73  	}
    74  
    75  	type pathDiff struct {
    76  		desc Descriptor
    77  		from string
    78  		to   string
    79  	}
    80  
    81  	type itemDiff struct {
    82  		path string
    83  		desc Descriptor
    84  	}
    85  
    86  	adds := make([]itemDiff, 0)
    87  	mods := make([]modDiff, 0)
    88  	rens := make([]pathDiff, 0)
    89  	dels := make([]itemDiff, 0)
    90  
    91  	mByPath := make(map[string]Descriptor)
    92  	xByPath := make(map[string]Descriptor)
    93  	newPaths := make(map[digest.Digest][]string)
    94  
    95  	for _, d := range x.Items {
    96  		for _, path := range d.Paths {
    97  			xByPath[path] = d
    98  		}
    99  	}
   100  	for _, d := range m.Items {
   101  		for _, path := range d.Paths {
   102  			mByPath[path] = d
   103  			if _, ok := xByPath[path]; !ok {
   104  				newPaths[d.Digest] = append(newPaths[d.Digest], path)
   105  			}
   106  		}
   107  	}
   108  
   109  	for path, xd := range xByPath {
   110  
   111  		// try by path
   112  		if md, ok := mByPath[path]; ok {
   113  			if md.Digest != xd.Digest {
   114  				// modified
   115  				mods = append(mods, modDiff{
   116  					path: path,
   117  					from: xd,
   118  					to:   md,
   119  				})
   120  			}
   121  			// else:
   122  			// same content, so no diff
   123  
   124  		} else { // try by digest
   125  			byDig := xd.Digest
   126  			if mPaths, ok := newPaths[byDig]; ok && len(mPaths) > 0 {
   127  				// renamed
   128  				newPath := mPaths[0]
   129  				newPaths[byDig] = mPaths[1:]
   130  				rens = append(rens, pathDiff{
   131  					desc: xd,
   132  					from: path,
   133  					to:   newPath,
   134  				})
   135  				delete(mByPath, newPath)
   136  			} else {
   137  				// deleted
   138  				dels = append(dels, itemDiff{
   139  					path: path,
   140  					desc: xd,
   141  				})
   142  			}
   143  
   144  		}
   145  
   146  		delete(mByPath, path)
   147  	}
   148  
   149  	// finally, arrange new items
   150  	for path, d := range mByPath {
   151  		adds = append(adds, itemDiff{
   152  			path: path,
   153  			desc: d,
   154  		})
   155  	}
   156  
   157  	equal = len(adds) == 0 && len(mods) == 0 && len(rens) == 0 && len(dels) == 0
   158  	if equal {
   159  		return // empty diff, no need to format lines
   160  	}
   161  
   162  	lines := []string{}
   163  
   164  	sprintf := func(format string, a ...interface{}) {
   165  		lines = append(lines, fmt.Sprintf(format, a...))
   166  	}
   167  
   168  	for _, d := range adds {
   169  		sprintf(
   170  			"\tnew item:   %s (%s)\n\t            + %s",
   171  			d.path,
   172  			humanize.Bytes(d.desc.Size),
   173  			d.desc.Digest.String(),
   174  		)
   175  	}
   176  	for _, d := range mods {
   177  		sprintf(
   178  			"\tmodified:   %s (%s -> %s)\n\t            - %s\n\t            + %s",
   179  			d.path,
   180  			humanize.Bytes(d.from.Size),
   181  			humanize.Bytes(d.to.Size),
   182  			d.from.Digest.String(),
   183  			d.to.Digest.String(),
   184  		)
   185  	}
   186  	for _, d := range rens {
   187  		sprintf(
   188  			"\trenamed:    %s -> %s (%s)\n\t            = %s",
   189  			d.from,
   190  			d.to,
   191  			humanize.Bytes(d.desc.Size),
   192  			d.desc.Digest.String(),
   193  		)
   194  	}
   195  	for _, d := range dels {
   196  		sprintf(
   197  			"\tdeleted:    %s (%s)\n\t            - %s",
   198  			d.path,
   199  			humanize.Bytes(d.desc.Size),
   200  			d.desc.Digest.String(),
   201  		)
   202  	}
   203  
   204  	report = strings.Join(lines, "\n\n") + "\n"
   205  	return
   206  }