From 20ef72b04caa1e7b034ffacab3fa03a7a6b13223 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 30 Jan 2025 12:38:20 -0500 Subject: [PATCH 01/13] diff command --- cmd/diff/main.go | 284 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 cmd/diff/main.go diff --git a/cmd/diff/main.go b/cmd/diff/main.go new file mode 100644 index 00000000..a48c2a75 --- /dev/null +++ b/cmd/diff/main.go @@ -0,0 +1,284 @@ +// Copyright 2021-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "encoding/hex" + "errors" + "flag" + "fmt" + "io/fs" + "os" + "strings" + + "github.com/bufbuild/buf/private/bufpkg/bufcas" + "github.com/bufbuild/buf/private/pkg/slicesext" + "github.com/bufbuild/buf/private/pkg/storage" + "github.com/bufbuild/buf/private/pkg/storage/storageos" + "github.com/bufbuild/modules/private/bufpkg/bufstate" + "github.com/pmezard/go-difflib/difflib" +) + +type command struct { + from string + to string +} + +func main() { + flag.Parse() + if len(flag.Args()) != 2 { + _, _ = fmt.Fprintln(flag.CommandLine.Output(), "usage: diff ") + flag.PrintDefaults() + os.Exit(2) + } + if flag.Args()[0] == flag.Args()[1] { + _, _ = fmt.Fprintf(os.Stderr, " and cannot be the same") + flag.PrintDefaults() + os.Exit(2) + } + cmd := &command{ + from: flag.Args()[0], + to: flag.Args()[1], + } + if err := cmd.run(context.Background()); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "diff: %v\n", err) + os.Exit(1) + } +} + +func (c *command) run(ctx context.Context) error { + // first, attempt to match from/to as module references in a state file in the same directory + // where the command is run + bucket, err := storageos.NewProvider().NewReadWriteBucket(".") + if err != nil { + return fmt.Errorf("new rw bucket: %w", err) + } + modStateReader, err := bucket.Get(ctx, bufstate.ModStateFileName) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("read module state file: %w", err) + } + // if the state file does not exist, we assume we are in the cas directory, and that from/to are + // the manifest paths + return diffFromCASDirectory(ctx, bucket, c.from, c.to) + } + // state file was found, attempt to parse it and use its digests + rw, err := bufstate.NewReadWriter() + if err != nil { + return fmt.Errorf("new state rw: %w", err) + } + modState, err := rw.ReadModStateFile(modStateReader) + if err != nil { + return fmt.Errorf("read module state: %w", err) + } + var ( + fromManifestPath string + toManifestPath string + ) + for _, ref := range modState.GetReferences() { + if ref.GetName() == c.from { + fromManifestPath = ref.GetDigest() + if toManifestPath != "" { + break + } + } else if ref.GetName() == c.to { + toManifestPath = ref.GetDigest() + if fromManifestPath != "" { + break + } + } + } + if fromManifestPath == "" { + return fmt.Errorf("from reference %s not found in the module state file", c.from) + } + if toManifestPath == "" { + return fmt.Errorf("to reference %s not found in the module state file", c.to) + } + casBucket, err := storageos.NewProvider().NewReadWriteBucket("cas") + if err != nil { + return fmt.Errorf("new rw cas bucket: %w", err) + } + return diffFromCASDirectory(ctx, casBucket, fromManifestPath, toManifestPath) +} + +func diffFromCASDirectory( + ctx context.Context, + bucket storage.ReadBucket, + fromManifestPath string, + toManifestPath string, +) error { + fromManifest, err := readManifest(ctx, bucket, fromManifestPath) + if err != nil { + return fmt.Errorf("read manifest from: %w", err) + } + toManifest, err := readManifest(ctx, bucket, toManifestPath) + if err != nil { + return fmt.Errorf("read manifest to: %w", err) + } + diff := newDiff() + // removed and changed + for _, fromNode := range fromManifest.FileNodes() { + path := fromNode.Path() + toNode := toManifest.GetFileNode(path) + if toNode == nil { + diff.removed(path, fromNode) + continue + } + if bufcas.DigestEqual(fromNode.Digest(), toNode.Digest()) { + continue // no changes + } + if err := diff.changedDigest(ctx, bucket, path, fromNode, toNode); err != nil { + return fmt.Errorf( + "changed digest from %s to %s: %w", + fromNode.String(), + toNode.String(), + err, + ) + } + } + // added + for _, toNode := range toManifest.FileNodes() { + if fromManifest.GetFileNode(toNode.Path()) == nil { + diff.added(toNode.Path(), toNode) + } + } + fmt.Println(diff.String()) + return nil +} + +func readManifest(ctx context.Context, bucket storage.ReadBucket, manifestPath string) (bufcas.Manifest, error) { + data, err := storage.ReadPath(ctx, bucket, manifestPath) + if err != nil { + return nil, fmt.Errorf("read path: %w", err) + } + m, err := bufcas.ParseManifest(string(data)) + if err != nil { + return nil, fmt.Errorf("parse manifest: %w", err) + } + return m, nil +} + +type pathDiff struct { + from bufcas.FileNode + to bufcas.FileNode + diff string +} + +type diff struct { + addedPaths map[string]bufcas.FileNode + removedPaths map[string]bufcas.FileNode + changedDigestPaths map[string]pathDiff +} + +func newDiff() *diff { + return &diff{ + addedPaths: make(map[string]bufcas.FileNode), + removedPaths: make(map[string]bufcas.FileNode), + changedDigestPaths: make(map[string]pathDiff), + } +} + +func (d *diff) added(path string, node bufcas.FileNode) { + d.addedPaths[path] = node +} + +func (d *diff) removed(path string, node bufcas.FileNode) { + d.removedPaths[path] = node +} + +func (d *diff) changedDigest( + ctx context.Context, + bucket storage.ReadBucket, + path string, + from bufcas.FileNode, + to bufcas.FileNode, +) error { + fromData, err := storage.ReadPath(ctx, bucket, fileNodePath(from)) + if err != nil { + return fmt.Errorf("read from changed path: %w", err) + } + toData, err := storage.ReadPath(ctx, bucket, fileNodePath(to)) + if err != nil { + return fmt.Errorf("read to changed path: %w", err) + } + udiff := difflib.UnifiedDiff{ + A: difflib.SplitLines(string(fromData)), + B: difflib.SplitLines(string(toData)), + FromFile: from.Digest().String(), + ToFile: to.Digest().String(), + Context: 2, + } + diff, err := difflib.GetUnifiedDiffString(udiff) + if err != nil { + return fmt.Errorf("get unified diff: %w", err) + } + d.changedDigestPaths[path] = pathDiff{ + from: from, + to: to, + diff: diff, + } + return nil +} + +func (d *diff) String() string { + var sb strings.Builder + if len(d.removedPaths) > 0 { + sb.WriteString("# Removed:\n\n") + sb.WriteString("```diff\n") + sortedPaths := slicesext.MapKeysToSortedSlice(d.removedPaths) + for _, path := range sortedPaths { + sb.WriteString("- " + d.removedPaths[path].String() + "\n") + } + sb.WriteString("```\n") + sb.WriteString("\n") + } + if len(d.addedPaths) > 0 { + sb.WriteString("# Added:\n\n") + sb.WriteString("```diff\n") + sortedPaths := slicesext.MapKeysToSortedSlice(d.addedPaths) + for _, path := range sortedPaths { + sb.WriteString("+ " + d.addedPaths[path].String() + "\n") + } + sb.WriteString("```\n") + sb.WriteString("\n") + } + if len(d.changedDigestPaths) > 0 { + sb.WriteString("# Changed content:\n\n") + sortedPaths := slicesext.MapKeysToSortedSlice(d.changedDigestPaths) + for _, path := range sortedPaths { + pd := d.changedDigestPaths[path] + sb.WriteString("## `" + pd.from.Path() + "`:\n") + sb.WriteString( + "```diff\n" + + pd.diff + "\n" + + "```\n", + ) + } + sb.WriteString("\n") + } + sb.WriteString(fmt.Sprintf( + "Total changes: %d (%d deletions, %d additions, %d changed)\n", + len(d.removedPaths)+len(d.addedPaths)+len(d.changedDigestPaths), + len(d.removedPaths), + len(d.addedPaths), + len(d.changedDigestPaths), + )) + return sb.String() +} + +func fileNodePath(n bufcas.FileNode) string { + return hex.EncodeToString(n.Digest().Value()) +} From a9c1deb9fab55cc615a6a2f6f49c2abb97cfff72 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 30 Jan 2025 12:51:14 -0500 Subject: [PATCH 02/13] lint --- cmd/diff/main.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/diff/main.go b/cmd/diff/main.go index a48c2a75..2b24c42b 100644 --- a/cmd/diff/main.go +++ b/cmd/diff/main.go @@ -155,7 +155,7 @@ func diffFromCASDirectory( diff.added(toNode.Path(), toNode) } } - fmt.Println(diff.String()) + _, _ = fmt.Fprint(os.Stdout, diff.String()) return nil } @@ -203,22 +203,22 @@ func (d *diff) changedDigest( ctx context.Context, bucket storage.ReadBucket, path string, - from bufcas.FileNode, - to bufcas.FileNode, + fromNode bufcas.FileNode, + toNode bufcas.FileNode, ) error { - fromData, err := storage.ReadPath(ctx, bucket, fileNodePath(from)) + fromData, err := storage.ReadPath(ctx, bucket, fileNodePath(fromNode)) if err != nil { return fmt.Errorf("read from changed path: %w", err) } - toData, err := storage.ReadPath(ctx, bucket, fileNodePath(to)) + toData, err := storage.ReadPath(ctx, bucket, fileNodePath(toNode)) if err != nil { return fmt.Errorf("read to changed path: %w", err) } udiff := difflib.UnifiedDiff{ A: difflib.SplitLines(string(fromData)), B: difflib.SplitLines(string(toData)), - FromFile: from.Digest().String(), - ToFile: to.Digest().String(), + FromFile: fromNode.Digest().String(), + ToFile: toNode.Digest().String(), Context: 2, } diff, err := difflib.GetUnifiedDiffString(udiff) @@ -226,15 +226,15 @@ func (d *diff) changedDigest( return fmt.Errorf("get unified diff: %w", err) } d.changedDigestPaths[path] = pathDiff{ - from: from, - to: to, + from: fromNode, + to: toNode, diff: diff, } return nil } func (d *diff) String() string { - var sb strings.Builder + var sb strings.Builder //nolint:varnamelen // sb is a common builder varname if len(d.removedPaths) > 0 { sb.WriteString("# Removed:\n\n") sb.WriteString("```diff\n") From e8fc1fcc382bb0e603feef34fe390698f36d32a9 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 30 Jan 2025 12:58:22 -0500 Subject: [PATCH 03/13] go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 39570606..122583a3 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/go-github/v64 v64.0.0 github.com/hashicorp/go-retryablehttp v0.7.7 + github.com/pmezard/go-difflib v1.0.0 github.com/stretchr/testify v1.10.0 go.uber.org/multierr v1.11.0 golang.org/x/mod v0.22.0 @@ -25,7 +26,6 @@ require ( github.com/google/cel-go v0.22.1 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect From ca559f31a4b29574eded99f12572678d70f58254 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 30 Jan 2025 13:53:33 -0500 Subject: [PATCH 04/13] extra validation --- cmd/diff/main.go | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/cmd/diff/main.go b/cmd/diff/main.go index 2b24c42b..bcfa8a7b 100644 --- a/cmd/diff/main.go +++ b/cmd/diff/main.go @@ -128,7 +128,7 @@ func diffFromCASDirectory( if err != nil { return fmt.Errorf("read manifest to: %w", err) } - diff := newDiff() + diff := newManifestDiff() // removed and changed for _, fromNode := range fromManifest.FileNodes() { path := fromNode.Path() @@ -171,41 +171,45 @@ func readManifest(ctx context.Context, bucket storage.ReadBucket, manifestPath s return m, nil } -type pathDiff struct { - from bufcas.FileNode - to bufcas.FileNode +type fileNodeDiff struct { + path string + from bufcas.Digest + to bufcas.Digest diff string } -type diff struct { +type manifestDiff struct { addedPaths map[string]bufcas.FileNode removedPaths map[string]bufcas.FileNode - changedDigestPaths map[string]pathDiff + changedDigestPaths map[string]fileNodeDiff } -func newDiff() *diff { - return &diff{ +func newManifestDiff() *manifestDiff { + return &manifestDiff{ addedPaths: make(map[string]bufcas.FileNode), removedPaths: make(map[string]bufcas.FileNode), - changedDigestPaths: make(map[string]pathDiff), + changedDigestPaths: make(map[string]fileNodeDiff), } } -func (d *diff) added(path string, node bufcas.FileNode) { +func (d *manifestDiff) added(path string, node bufcas.FileNode) { d.addedPaths[path] = node } -func (d *diff) removed(path string, node bufcas.FileNode) { +func (d *manifestDiff) removed(path string, node bufcas.FileNode) { d.removedPaths[path] = node } -func (d *diff) changedDigest( +func (d *manifestDiff) changedDigest( ctx context.Context, bucket storage.ReadBucket, path string, fromNode bufcas.FileNode, toNode bufcas.FileNode, ) error { + if fromNode.Path() != toNode.Path() { + return errors.New("from path and to path are different") + } fromData, err := storage.ReadPath(ctx, bucket, fileNodePath(fromNode)) if err != nil { return fmt.Errorf("read from changed path: %w", err) @@ -225,15 +229,16 @@ func (d *diff) changedDigest( if err != nil { return fmt.Errorf("get unified diff: %w", err) } - d.changedDigestPaths[path] = pathDiff{ - from: fromNode, - to: toNode, + d.changedDigestPaths[path] = fileNodeDiff{ + path: fromNode.Path(), + from: fromNode.Digest(), + to: toNode.Digest(), diff: diff, } return nil } -func (d *diff) String() string { +func (d *manifestDiff) String() string { var sb strings.Builder //nolint:varnamelen // sb is a common builder varname if len(d.removedPaths) > 0 { sb.WriteString("# Removed:\n\n") @@ -259,11 +264,11 @@ func (d *diff) String() string { sb.WriteString("# Changed content:\n\n") sortedPaths := slicesext.MapKeysToSortedSlice(d.changedDigestPaths) for _, path := range sortedPaths { - pd := d.changedDigestPaths[path] - sb.WriteString("## `" + pd.from.Path() + "`:\n") + fnDiff := d.changedDigestPaths[path] + sb.WriteString("## `" + fnDiff.path + "`:\n") sb.WriteString( "```diff\n" + - pd.diff + "\n" + + fnDiff.diff + "\n" + "```\n", ) } From bc7ca505c3e9e91679c72f838b3d9f8ec548c649 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 30 Jan 2025 16:27:58 -0500 Subject: [PATCH 05/13] Use buf diff pkg --- cmd/diff/main.go | 23 ++++++++++++----------- go.mod | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cmd/diff/main.go b/cmd/diff/main.go index bcfa8a7b..b160b6d4 100644 --- a/cmd/diff/main.go +++ b/cmd/diff/main.go @@ -25,11 +25,11 @@ import ( "strings" "github.com/bufbuild/buf/private/bufpkg/bufcas" + "github.com/bufbuild/buf/private/pkg/diff" "github.com/bufbuild/buf/private/pkg/slicesext" "github.com/bufbuild/buf/private/pkg/storage" "github.com/bufbuild/buf/private/pkg/storage/storageos" "github.com/bufbuild/modules/private/bufpkg/bufstate" - "github.com/pmezard/go-difflib/difflib" ) type command struct { @@ -218,22 +218,23 @@ func (d *manifestDiff) changedDigest( if err != nil { return fmt.Errorf("read to changed path: %w", err) } - udiff := difflib.UnifiedDiff{ - A: difflib.SplitLines(string(fromData)), - B: difflib.SplitLines(string(toData)), - FromFile: fromNode.Digest().String(), - ToFile: toNode.Digest().String(), - Context: 2, - } - diff, err := difflib.GetUnifiedDiffString(udiff) + diffData, err := diff.Diff( + ctx, + fromData, + toData, + fromNode.Digest().String(), + toNode.Digest().String(), + diff.DiffWithSuppressCommands(), + diff.DiffWithSuppressTimestamps(), + ) if err != nil { - return fmt.Errorf("get unified diff: %w", err) + return fmt.Errorf("diff: %w", err) } d.changedDigestPaths[path] = fileNodeDiff{ path: fromNode.Path(), from: fromNode.Digest(), to: toNode.Digest(), - diff: diff, + diff: string(diffData), } return nil } diff --git a/go.mod b/go.mod index 122583a3..39570606 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/go-github/v64 v64.0.0 github.com/hashicorp/go-retryablehttp v0.7.7 - github.com/pmezard/go-difflib v1.0.0 github.com/stretchr/testify v1.10.0 go.uber.org/multierr v1.11.0 golang.org/x/mod v0.22.0 @@ -26,6 +25,7 @@ require ( github.com/google/cel-go v0.22.1 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect From 1ceb1d723f60279faca1ce2609c8bed12160d2f9 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 6 Feb 2025 10:45:20 -0500 Subject: [PATCH 06/13] rename to casdiff --- cmd/{diff => casdiff}/main.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cmd/{diff => casdiff}/main.go (100%) diff --git a/cmd/diff/main.go b/cmd/casdiff/main.go similarity index 100% rename from cmd/diff/main.go rename to cmd/casdiff/main.go From fd472a020f6e7d9519d5fc5160ab4440bd426b36 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 6 Feb 2025 10:57:13 -0500 Subject: [PATCH 07/13] empty diffs --- cmd/casdiff/main.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cmd/casdiff/main.go b/cmd/casdiff/main.go index b160b6d4..bc1a5936 100644 --- a/cmd/casdiff/main.go +++ b/cmd/casdiff/main.go @@ -32,6 +32,10 @@ import ( "github.com/bufbuild/modules/private/bufpkg/bufstate" ) +const ( + emptyDiffSameFromAndTo = "empty diff, from and to are the same" +) + type command struct { from string to string @@ -45,9 +49,8 @@ func main() { os.Exit(2) } if flag.Args()[0] == flag.Args()[1] { - _, _ = fmt.Fprintf(os.Stderr, " and cannot be the same") - flag.PrintDefaults() - os.Exit(2) + _, _ = fmt.Fprint(os.Stdout, newManifestDiff().String()) + return } cmd := &command{ from: flag.Args()[0], @@ -120,6 +123,10 @@ func diffFromCASDirectory( fromManifestPath string, toManifestPath string, ) error { + if fromManifestPath == toManifestPath { + _, _ = fmt.Fprint(os.Stdout, newManifestDiff().String()) + return nil + } fromManifest, err := readManifest(ctx, bucket, fromManifestPath) if err != nil { return fmt.Errorf("read manifest from: %w", err) From a3571986129f31fc7fafe3d9c70c4458b1ffcdeb Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 6 Feb 2025 16:00:28 -0500 Subject: [PATCH 08/13] Use appcmd --- cmd/casdiff/casdiff.go | 228 +++++++++++++++++++++++++++++ cmd/casdiff/main.go | 275 +---------------------------------- cmd/casdiff/manifest_diff.go | 223 ++++++++++++++++++++++++++++ go.mod | 10 ++ go.sum | 47 ++++++ 5 files changed, 512 insertions(+), 271 deletions(-) create mode 100644 cmd/casdiff/casdiff.go create mode 100644 cmd/casdiff/manifest_diff.go diff --git a/cmd/casdiff/casdiff.go b/cmd/casdiff/casdiff.go new file mode 100644 index 00000000..f416f3af --- /dev/null +++ b/cmd/casdiff/casdiff.go @@ -0,0 +1,228 @@ +// Copyright 2021-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "errors" + "fmt" + "io/fs" + "strconv" + + "github.com/bufbuild/buf/private/bufpkg/bufcas" + "github.com/bufbuild/buf/private/pkg/app/appcmd" + "github.com/bufbuild/buf/private/pkg/app/appext" + "github.com/bufbuild/buf/private/pkg/slicesext" + "github.com/bufbuild/buf/private/pkg/slogapp" + "github.com/bufbuild/buf/private/pkg/storage" + "github.com/bufbuild/buf/private/pkg/storage/storageos" + "github.com/bufbuild/modules/private/bufpkg/bufstate" + "github.com/spf13/pflag" +) + +// format is a format to print the casdiff. +type format int + +const ( + formatFlagName = "format" + formatFlagShortName = "f" + + emptyDiffSameFromAndTo = "empty diff, from and to are the same" +) + +const ( + formatText format = iota + 1 + formatMarkdown +) + +var ( + formatsValuesToNames = map[format]string{ + formatText: "text", + formatMarkdown: "markdown", + } + formatsNamesToValues, _ = slicesext.ToUniqueValuesMap( + slicesext.MapKeysToSlice(formatsValuesToNames), + func(f format) string { return formatsValuesToNames[f] }, + ) + allFormatsString = slicesext.MapKeysToSortedSlice(formatsNamesToValues) +) + +func (f format) String() string { + if n, ok := formatsValuesToNames[f]; ok { + return n + } + return strconv.Itoa(int(f)) +} + +func newCommand(name string) *appcmd.Command { + builder := appext.NewBuilder( + name, + appext.BuilderWithLoggerProvider(slogapp.LoggerProvider), + ) + flags := newFlags() + return &appcmd.Command{ + Use: name + " ", + Short: "Run a CAS diff.", + Args: appcmd.ExactArgs(2), + BindFlags: flags.bind, + Run: builder.NewRunFunc( + func(ctx context.Context, container appext.Container) error { + return run(ctx, container, flags) + }, + ), + } +} + +type flags struct { + format string +} + +func newFlags() *flags { + return &flags{} +} + +func (f *flags) bind(flagSet *pflag.FlagSet) { + flagSet.StringVarP( + &f.format, + formatFlagName, + formatFlagShortName, + formatText.String(), + fmt.Sprintf(`The out format to use. Must be one of %s`, allFormatsString), + ) +} + +func run( + ctx context.Context, + container appext.Container, + flags *flags, +) error { + f, ok := formatsNamesToValues[flags.format] + if !ok { + return fmt.Errorf("unsupported format %s", flags.format) + } + from, to := container.Arg(0), container.Arg(1) + if from == to { + return printDiff(newManifestDiff(), f) + } + // first, attempt to match from/to as module references in a state file in the same directory + // where the command is run + bucket, err := storageos.NewProvider().NewReadWriteBucket(".") + if err != nil { + return fmt.Errorf("new rw bucket: %w", err) + } + moduleStateReader, err := bucket.Get(ctx, bufstate.ModStateFileName) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("read module state file: %w", err) + } + // if the state file does not exist, we assume we are in the cas directory, and that from/to are + // the manifest paths + mdiff, err := calculateDiffFromCASDirectory(ctx, bucket, from, to) + if err != nil { + return fmt.Errorf("calculate cas diff: %w", err) + } + return printDiff(mdiff, f) + } + // state file was found, attempt to parse it and match from/to with its references + stateRW, err := bufstate.NewReadWriter() + if err != nil { + return fmt.Errorf("new state rw: %w", err) + } + moduleState, err := stateRW.ReadModStateFile(moduleStateReader) + if err != nil { + return fmt.Errorf("read module state: %w", err) + } + var ( + fromManifestPath string + toManifestPath string + ) + for _, ref := range moduleState.GetReferences() { + if ref.GetName() == from { + fromManifestPath = ref.GetDigest() + if toManifestPath != "" { + break + } + } else if ref.GetName() == to { + toManifestPath = ref.GetDigest() + if fromManifestPath != "" { + break + } + } + } + if fromManifestPath == "" { + return fmt.Errorf("from reference %s not found in the module state file", from) + } + if toManifestPath == "" { + return fmt.Errorf("to reference %s not found in the module state file", to) + } + if fromManifestPath == toManifestPath { + return printDiff(newManifestDiff(), f) + } + casBucket, err := storageos.NewProvider().NewReadWriteBucket("cas") + if err != nil { + return fmt.Errorf("new rw cas bucket: %w", err) + } + mdiff, err := calculateDiffFromCASDirectory(ctx, casBucket, fromManifestPath, toManifestPath) + if err != nil { + return fmt.Errorf("calculate cas diff from state references: %w", err) + } + return printDiff(mdiff, f) +} + +// calculateDiffFromCASDirectory takes the cas bucket, and the from/to manifest paths to calculate a +// diff. +func calculateDiffFromCASDirectory( + ctx context.Context, + casBucket storage.ReadBucket, + fromManifestPath string, + toManifestPath string, +) (*manifestDiff, error) { + if fromManifestPath == toManifestPath { + return newManifestDiff(), nil + } + fromManifest, err := readManifest(ctx, casBucket, fromManifestPath) + if err != nil { + return nil, fmt.Errorf("read manifest from: %w", err) + } + toManifest, err := readManifest(ctx, casBucket, toManifestPath) + if err != nil { + return nil, fmt.Errorf("read manifest to: %w", err) + } + return buildManifestDiff(ctx, fromManifest, toManifest, casBucket) +} + +func readManifest(ctx context.Context, bucket storage.ReadBucket, manifestPath string) (bufcas.Manifest, error) { + data, err := storage.ReadPath(ctx, bucket, manifestPath) + if err != nil { + return nil, fmt.Errorf("read path: %w", err) + } + m, err := bufcas.ParseManifest(string(data)) + if err != nil { + return nil, fmt.Errorf("parse manifest: %w", err) + } + return m, nil +} + +func printDiff(mdiff *manifestDiff, f format) error { + switch f { + case formatText: + mdiff.printText() + case formatMarkdown: + mdiff.printMarkdown() + default: + return fmt.Errorf("format %s not supported", f.String()) + } + return nil +} diff --git a/cmd/casdiff/main.go b/cmd/casdiff/main.go index bc1a5936..bc9abb7f 100644 --- a/cmd/casdiff/main.go +++ b/cmd/casdiff/main.go @@ -16,282 +16,15 @@ package main import ( "context" - "encoding/hex" - "errors" - "flag" - "fmt" - "io/fs" - "os" - "strings" - "github.com/bufbuild/buf/private/bufpkg/bufcas" - "github.com/bufbuild/buf/private/pkg/diff" - "github.com/bufbuild/buf/private/pkg/slicesext" - "github.com/bufbuild/buf/private/pkg/storage" - "github.com/bufbuild/buf/private/pkg/storage/storageos" - "github.com/bufbuild/modules/private/bufpkg/bufstate" + "github.com/bufbuild/buf/private/pkg/app/appcmd" ) const ( - emptyDiffSameFromAndTo = "empty diff, from and to are the same" + rootCmdName = "casdiff" ) -type command struct { - from string - to string -} - func main() { - flag.Parse() - if len(flag.Args()) != 2 { - _, _ = fmt.Fprintln(flag.CommandLine.Output(), "usage: diff ") - flag.PrintDefaults() - os.Exit(2) - } - if flag.Args()[0] == flag.Args()[1] { - _, _ = fmt.Fprint(os.Stdout, newManifestDiff().String()) - return - } - cmd := &command{ - from: flag.Args()[0], - to: flag.Args()[1], - } - if err := cmd.run(context.Background()); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "diff: %v\n", err) - os.Exit(1) - } -} - -func (c *command) run(ctx context.Context) error { - // first, attempt to match from/to as module references in a state file in the same directory - // where the command is run - bucket, err := storageos.NewProvider().NewReadWriteBucket(".") - if err != nil { - return fmt.Errorf("new rw bucket: %w", err) - } - modStateReader, err := bucket.Get(ctx, bufstate.ModStateFileName) - if err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return fmt.Errorf("read module state file: %w", err) - } - // if the state file does not exist, we assume we are in the cas directory, and that from/to are - // the manifest paths - return diffFromCASDirectory(ctx, bucket, c.from, c.to) - } - // state file was found, attempt to parse it and use its digests - rw, err := bufstate.NewReadWriter() - if err != nil { - return fmt.Errorf("new state rw: %w", err) - } - modState, err := rw.ReadModStateFile(modStateReader) - if err != nil { - return fmt.Errorf("read module state: %w", err) - } - var ( - fromManifestPath string - toManifestPath string - ) - for _, ref := range modState.GetReferences() { - if ref.GetName() == c.from { - fromManifestPath = ref.GetDigest() - if toManifestPath != "" { - break - } - } else if ref.GetName() == c.to { - toManifestPath = ref.GetDigest() - if fromManifestPath != "" { - break - } - } - } - if fromManifestPath == "" { - return fmt.Errorf("from reference %s not found in the module state file", c.from) - } - if toManifestPath == "" { - return fmt.Errorf("to reference %s not found in the module state file", c.to) - } - casBucket, err := storageos.NewProvider().NewReadWriteBucket("cas") - if err != nil { - return fmt.Errorf("new rw cas bucket: %w", err) - } - return diffFromCASDirectory(ctx, casBucket, fromManifestPath, toManifestPath) -} - -func diffFromCASDirectory( - ctx context.Context, - bucket storage.ReadBucket, - fromManifestPath string, - toManifestPath string, -) error { - if fromManifestPath == toManifestPath { - _, _ = fmt.Fprint(os.Stdout, newManifestDiff().String()) - return nil - } - fromManifest, err := readManifest(ctx, bucket, fromManifestPath) - if err != nil { - return fmt.Errorf("read manifest from: %w", err) - } - toManifest, err := readManifest(ctx, bucket, toManifestPath) - if err != nil { - return fmt.Errorf("read manifest to: %w", err) - } - diff := newManifestDiff() - // removed and changed - for _, fromNode := range fromManifest.FileNodes() { - path := fromNode.Path() - toNode := toManifest.GetFileNode(path) - if toNode == nil { - diff.removed(path, fromNode) - continue - } - if bufcas.DigestEqual(fromNode.Digest(), toNode.Digest()) { - continue // no changes - } - if err := diff.changedDigest(ctx, bucket, path, fromNode, toNode); err != nil { - return fmt.Errorf( - "changed digest from %s to %s: %w", - fromNode.String(), - toNode.String(), - err, - ) - } - } - // added - for _, toNode := range toManifest.FileNodes() { - if fromManifest.GetFileNode(toNode.Path()) == nil { - diff.added(toNode.Path(), toNode) - } - } - _, _ = fmt.Fprint(os.Stdout, diff.String()) - return nil -} - -func readManifest(ctx context.Context, bucket storage.ReadBucket, manifestPath string) (bufcas.Manifest, error) { - data, err := storage.ReadPath(ctx, bucket, manifestPath) - if err != nil { - return nil, fmt.Errorf("read path: %w", err) - } - m, err := bufcas.ParseManifest(string(data)) - if err != nil { - return nil, fmt.Errorf("parse manifest: %w", err) - } - return m, nil -} - -type fileNodeDiff struct { - path string - from bufcas.Digest - to bufcas.Digest - diff string -} - -type manifestDiff struct { - addedPaths map[string]bufcas.FileNode - removedPaths map[string]bufcas.FileNode - changedDigestPaths map[string]fileNodeDiff -} - -func newManifestDiff() *manifestDiff { - return &manifestDiff{ - addedPaths: make(map[string]bufcas.FileNode), - removedPaths: make(map[string]bufcas.FileNode), - changedDigestPaths: make(map[string]fileNodeDiff), - } -} - -func (d *manifestDiff) added(path string, node bufcas.FileNode) { - d.addedPaths[path] = node -} - -func (d *manifestDiff) removed(path string, node bufcas.FileNode) { - d.removedPaths[path] = node -} - -func (d *manifestDiff) changedDigest( - ctx context.Context, - bucket storage.ReadBucket, - path string, - fromNode bufcas.FileNode, - toNode bufcas.FileNode, -) error { - if fromNode.Path() != toNode.Path() { - return errors.New("from path and to path are different") - } - fromData, err := storage.ReadPath(ctx, bucket, fileNodePath(fromNode)) - if err != nil { - return fmt.Errorf("read from changed path: %w", err) - } - toData, err := storage.ReadPath(ctx, bucket, fileNodePath(toNode)) - if err != nil { - return fmt.Errorf("read to changed path: %w", err) - } - diffData, err := diff.Diff( - ctx, - fromData, - toData, - fromNode.Digest().String(), - toNode.Digest().String(), - diff.DiffWithSuppressCommands(), - diff.DiffWithSuppressTimestamps(), - ) - if err != nil { - return fmt.Errorf("diff: %w", err) - } - d.changedDigestPaths[path] = fileNodeDiff{ - path: fromNode.Path(), - from: fromNode.Digest(), - to: toNode.Digest(), - diff: string(diffData), - } - return nil -} - -func (d *manifestDiff) String() string { - var sb strings.Builder //nolint:varnamelen // sb is a common builder varname - if len(d.removedPaths) > 0 { - sb.WriteString("# Removed:\n\n") - sb.WriteString("```diff\n") - sortedPaths := slicesext.MapKeysToSortedSlice(d.removedPaths) - for _, path := range sortedPaths { - sb.WriteString("- " + d.removedPaths[path].String() + "\n") - } - sb.WriteString("```\n") - sb.WriteString("\n") - } - if len(d.addedPaths) > 0 { - sb.WriteString("# Added:\n\n") - sb.WriteString("```diff\n") - sortedPaths := slicesext.MapKeysToSortedSlice(d.addedPaths) - for _, path := range sortedPaths { - sb.WriteString("+ " + d.addedPaths[path].String() + "\n") - } - sb.WriteString("```\n") - sb.WriteString("\n") - } - if len(d.changedDigestPaths) > 0 { - sb.WriteString("# Changed content:\n\n") - sortedPaths := slicesext.MapKeysToSortedSlice(d.changedDigestPaths) - for _, path := range sortedPaths { - fnDiff := d.changedDigestPaths[path] - sb.WriteString("## `" + fnDiff.path + "`:\n") - sb.WriteString( - "```diff\n" + - fnDiff.diff + "\n" + - "```\n", - ) - } - sb.WriteString("\n") - } - sb.WriteString(fmt.Sprintf( - "Total changes: %d (%d deletions, %d additions, %d changed)\n", - len(d.removedPaths)+len(d.addedPaths)+len(d.changedDigestPaths), - len(d.removedPaths), - len(d.addedPaths), - len(d.changedDigestPaths), - )) - return sb.String() -} - -func fileNodePath(n bufcas.FileNode) string { - return hex.EncodeToString(n.Digest().Value()) + appcmd.Main(context.Background(), newCommand(rootCmdName)) + return } diff --git a/cmd/casdiff/manifest_diff.go b/cmd/casdiff/manifest_diff.go new file mode 100644 index 00000000..3ce0e50e --- /dev/null +++ b/cmd/casdiff/manifest_diff.go @@ -0,0 +1,223 @@ +// Copyright 2021-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "os" + + "github.com/bufbuild/buf/private/bufpkg/bufcas" + "github.com/bufbuild/buf/private/pkg/diff" + "github.com/bufbuild/buf/private/pkg/slicesext" + "github.com/bufbuild/buf/private/pkg/storage" +) + +type manifestDiff struct { + added map[string]bufcas.FileNode + removed map[string]bufcas.FileNode + changed map[string]fileChangedDiff +} + +type fileChangedDiff struct { + path string + from bufcas.Digest + to bufcas.Digest + diff string +} + +func newManifestDiff() *manifestDiff { + return &manifestDiff{ + added: make(map[string]bufcas.FileNode), + removed: make(map[string]bufcas.FileNode), + changed: make(map[string]fileChangedDiff), + } +} + +func buildManifestDiff( + ctx context.Context, + from bufcas.Manifest, + to bufcas.Manifest, + bucket storage.ReadBucket, +) (*manifestDiff, error) { + diff := newManifestDiff() + // removed and changed + for _, fromNode := range from.FileNodes() { + path := fromNode.Path() + toNode := to.GetFileNode(path) + if toNode == nil { + diff.removed[path] = fromNode + continue + } + if bufcas.DigestEqual(fromNode.Digest(), toNode.Digest()) { + continue // no changes + } + if err := diff.newChangedPath(ctx, bucket, path, fromNode, toNode); err != nil { + return nil, fmt.Errorf( + "changed digest from %s to %s: %w", + fromNode.String(), + toNode.String(), + err, + ) + } + } + // added + for _, toNode := range to.FileNodes() { + if from.GetFileNode(toNode.Path()) == nil { + diff.added[toNode.Path()] = toNode + } + } + return diff, nil +} + +func (d *manifestDiff) newChangedPath( + ctx context.Context, + bucket storage.ReadBucket, + path string, + from bufcas.FileNode, + to bufcas.FileNode, +) error { + if from.Path() != to.Path() { + return errors.New("from and to paths are different") + } + diffString, err := calculateFileNodeDiff(ctx, from, to, bucket) + if err != nil { + return fmt.Errorf("calculate digest diff: %w", err) + } + d.changed[path] = fileChangedDiff{ + path: from.Path(), + from: from.Digest(), + to: to.Digest(), + diff: diffString, + } + return nil +} + +func (d *manifestDiff) printText() { + os.Stdout.WriteString(fmt.Sprintf( + "%d files changed: %d removed, %d added, %d changed content\n", + len(d.removed)+len(d.added)+len(d.changed), + len(d.removed), + len(d.added), + len(d.changed), + )) + if len(d.removed) > 0 { + os.Stdout.WriteString("\n") + os.Stdout.WriteString("Files removed:\n\n") + sortedPaths := slicesext.MapKeysToSortedSlice(d.removed) + for _, path := range sortedPaths { + os.Stdout.WriteString("- " + d.removed[path].String() + "\n") + } + } + if len(d.added) > 0 { + os.Stdout.WriteString("\n") + os.Stdout.WriteString("Files added:\n\n") + sortedPaths := slicesext.MapKeysToSortedSlice(d.added) + for _, path := range sortedPaths { + os.Stdout.WriteString("+ " + d.added[path].String() + "\n") + } + } + if len(d.changed) > 0 { + os.Stdout.WriteString("\n") + os.Stdout.WriteString("Files changed content:\n\n") + sortedPaths := slicesext.MapKeysToSortedSlice(d.changed) + for _, path := range sortedPaths { + fnDiff := d.changed[path] + os.Stdout.WriteString(fnDiff.diff + "\n") + } + } +} + +func (d *manifestDiff) printMarkdown() { + os.Stdout.WriteString(fmt.Sprintf( + "> _%d files changed: %d removed, %d added, %d changed content_\n", + len(d.removed)+len(d.added)+len(d.changed), + len(d.removed), + len(d.added), + len(d.changed), + )) + if len(d.removed) > 0 { + os.Stdout.WriteString("\n") + os.Stdout.WriteString("# Files removed:\n\n") + os.Stdout.WriteString("```diff\n") + sortedPaths := slicesext.MapKeysToSortedSlice(d.removed) + for _, path := range sortedPaths { + os.Stdout.WriteString("- " + d.removed[path].String() + "\n") + } + os.Stdout.WriteString("```\n") + } + if len(d.added) > 0 { + os.Stdout.WriteString("\n") + os.Stdout.WriteString("# Files added:\n\n") + os.Stdout.WriteString("```diff\n") + sortedPaths := slicesext.MapKeysToSortedSlice(d.added) + for _, path := range sortedPaths { + os.Stdout.WriteString("+ " + d.added[path].String() + "\n") + } + os.Stdout.WriteString("```\n") + } + if len(d.changed) > 0 { + os.Stdout.WriteString("\n") + os.Stdout.WriteString("# Files changed content:\n\n") + sortedPaths := slicesext.MapKeysToSortedSlice(d.changed) + for _, path := range sortedPaths { + fnDiff := d.changed[path] + os.Stdout.WriteString("## `" + fnDiff.path + "`:\n") + os.Stdout.WriteString( + "```diff\n" + + fnDiff.diff + "\n" + + "```\n", + ) + } + } +} + +func calculateFileNodeDiff( + ctx context.Context, + from bufcas.FileNode, + to bufcas.FileNode, + bucket storage.ReadBucket, +) (string, error) { + if from.Path() == to.Path() && bufcas.DigestEqual(from.Digest(), to.Digest()) { + return "", nil + } + var ( + fromFilePath = hex.EncodeToString(from.Digest().Value()) + toFilePath = hex.EncodeToString(to.Digest().Value()) + ) + fromData, err := storage.ReadPath(ctx, bucket, fromFilePath) + if err != nil { + return "", fmt.Errorf("read from path: %w", err) + } + toData, err := storage.ReadPath(ctx, bucket, toFilePath) + if err != nil { + return "", fmt.Errorf("read to path: %w", err) + } + diffData, err := diff.Diff( + ctx, + fromData, + toData, + from.String(), + to.String(), + diff.DiffWithSuppressCommands(), + diff.DiffWithSuppressTimestamps(), + ) + if err != nil { + return "", fmt.Errorf("diff: %w", err) + } + return string(diffData), nil +} diff --git a/go.mod b/go.mod index 87c758a1..b3fe2eec 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/go-github/v64 v64.0.0 github.com/hashicorp/go-retryablehttp v0.7.7 + github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.10.0 go.uber.org/multierr v1.11.0 golang.org/x/mod v0.22.0 @@ -21,12 +22,21 @@ require ( cel.dev/expr v0.19.2 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/bufbuild/protocompile v0.14.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/fgprof v0.9.5 // indirect github.com/google/cel-go v0.23.2 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pkg/profile v1.7.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap/exp v0.3.0 // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect golang.org/x/sync v0.10.0 // indirect diff --git a/go.sum b/go.sum index 98ad82aa..44c6d678 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,18 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/ github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/bufbuild/protovalidate-go v0.9.1 h1:cdrIA33994yCcJyEIZRL36ZGTe9UDM/WHs5MBHEimiE= github.com/bufbuild/protovalidate-go v0.9.1/go.mod h1:5jptBxfvlY51RhX32zR6875JfPBRXUsQjyZjm/NqkLQ= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -19,6 +31,12 @@ github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfU github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -28,6 +46,10 @@ github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKby github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -36,18 +58,34 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -58,8 +96,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= @@ -70,6 +114,9 @@ golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= From 40c2e4f78b00481aae83c3fd9bdd02e9009792e9 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 6 Feb 2025 17:02:14 -0500 Subject: [PATCH 09/13] Support renames --- cmd/casdiff/manifest_diff.go | 198 ++++++++++++++++++++++------------- 1 file changed, 125 insertions(+), 73 deletions(-) diff --git a/cmd/casdiff/manifest_diff.go b/cmd/casdiff/manifest_diff.go index 3ce0e50e..1d7021b6 100644 --- a/cmd/casdiff/manifest_diff.go +++ b/cmd/casdiff/manifest_diff.go @@ -17,7 +17,6 @@ package main import ( "context" "encoding/hex" - "errors" "fmt" "os" @@ -28,23 +27,24 @@ import ( ) type manifestDiff struct { - added map[string]bufcas.FileNode - removed map[string]bufcas.FileNode - changed map[string]fileChangedDiff + pathsAdded map[string]bufcas.FileNode + pathsRenamed map[string]fileDiff + pathsRemoved map[string]bufcas.FileNode + pathsChangedContent map[string]fileDiff } -type fileChangedDiff struct { - path string - from bufcas.Digest - to bufcas.Digest +type fileDiff struct { + from bufcas.FileNode + to bufcas.FileNode diff string } func newManifestDiff() *manifestDiff { return &manifestDiff{ - added: make(map[string]bufcas.FileNode), - removed: make(map[string]bufcas.FileNode), - changed: make(map[string]fileChangedDiff), + pathsAdded: make(map[string]bufcas.FileNode), + pathsRenamed: make(map[string]fileDiff), + pathsRemoved: make(map[string]bufcas.FileNode), + pathsChangedContent: make(map[string]fileDiff), } } @@ -54,89 +54,122 @@ func buildManifestDiff( to bufcas.Manifest, bucket storage.ReadBucket, ) (*manifestDiff, error) { - diff := newManifestDiff() + var ( + diff = newManifestDiff() + digestToAddedPaths = make(map[string][]string) + digestToRemovedPaths = make(map[string][]string) + ) // removed and changed for _, fromNode := range from.FileNodes() { path := fromNode.Path() toNode := to.GetFileNode(path) if toNode == nil { - diff.removed[path] = fromNode + diff.pathsRemoved[path] = fromNode + digestToRemovedPaths[fromNode.Digest().String()] = append(digestToRemovedPaths[path], path) continue } if bufcas.DigestEqual(fromNode.Digest(), toNode.Digest()) { continue // no changes } - if err := diff.newChangedPath(ctx, bucket, path, fromNode, toNode); err != nil { - return nil, fmt.Errorf( - "changed digest from %s to %s: %w", - fromNode.String(), - toNode.String(), - err, - ) + diffString, err := calculateFileNodeDiff(ctx, fromNode, toNode, bucket) + if err != nil { + return nil, fmt.Errorf("calculate file node diff: %w", err) + } + diff.pathsChangedContent[path] = fileDiff{ + from: fromNode, + to: toNode, + diff: diffString, } } // added for _, toNode := range to.FileNodes() { - if from.GetFileNode(toNode.Path()) == nil { - diff.added[toNode.Path()] = toNode + path := toNode.Path() + if from.GetFileNode(path) == nil { + diff.pathsAdded[path] = toNode + digestToAddedPaths[toNode.Digest().String()] = append(digestToAddedPaths[path], path) } } - return diff, nil -} - -func (d *manifestDiff) newChangedPath( - ctx context.Context, - bucket storage.ReadBucket, - path string, - from bufcas.FileNode, - to bufcas.FileNode, -) error { - if from.Path() != to.Path() { - return errors.New("from and to paths are different") + // renamed: defined as digests present both in pathsRemoved and pathsAdded but under different + // paths + // + // We'll keep track of the paths that we matched as renames to later remove them from the + // added/removed maps + var ( + matchedRemovedPaths []string + matchedAddedPaths []string + ) + for digest, removedPaths := range digestToRemovedPaths { + // removedPaths and addedPaths should be lists with no items in common since they're recorded + // only for added and removed nodes exclusively + addedPaths, digestHasAddedPaths := digestToAddedPaths[digest] + if !digestHasAddedPaths { + continue + } + for _, removedPath := range removedPaths { + if len(addedPaths) == 0 { + continue + } + // both lists are sorted by path, we can always take out the first one to match + var addedPath string + addedPaths, addedPath = removeSliceItem(addedPaths, 0) + matchedRemovedPaths = append(matchedRemovedPaths, removedPath) + matchedAddedPaths = append(matchedAddedPaths, addedPath) + diff.pathsRenamed[removedPath] = fileDiff{ + from: from.GetFileNode(removedPath), + to: to.GetFileNode(addedPath), + } + } } - diffString, err := calculateFileNodeDiff(ctx, from, to, bucket) - if err != nil { - return fmt.Errorf("calculate digest diff: %w", err) + // delete the matches + for _, matchedRemovedPath := range matchedRemovedPaths { + delete(diff.pathsRemoved, matchedRemovedPath) } - d.changed[path] = fileChangedDiff{ - path: from.Path(), - from: from.Digest(), - to: to.Digest(), - diff: diffString, + for _, matchedAddedPath := range matchedAddedPaths { + delete(diff.pathsAdded, matchedAddedPath) } - return nil + return diff, nil } func (d *manifestDiff) printText() { os.Stdout.WriteString(fmt.Sprintf( - "%d files changed: %d removed, %d added, %d changed content\n", - len(d.removed)+len(d.added)+len(d.changed), - len(d.removed), - len(d.added), - len(d.changed), + "%d files changed: %d removed, %d renamed, %d added, %d changed content\n", + len(d.pathsRemoved)+len(d.pathsRenamed)+len(d.pathsAdded)+len(d.pathsChangedContent), + len(d.pathsRemoved), + len(d.pathsRenamed), + len(d.pathsAdded), + len(d.pathsChangedContent), )) - if len(d.removed) > 0 { + if len(d.pathsRemoved) > 0 { os.Stdout.WriteString("\n") os.Stdout.WriteString("Files removed:\n\n") - sortedPaths := slicesext.MapKeysToSortedSlice(d.removed) + sortedPaths := slicesext.MapKeysToSortedSlice(d.pathsRemoved) + for _, path := range sortedPaths { + os.Stdout.WriteString("- " + d.pathsRemoved[path].String() + "\n") + } + } + if len(d.pathsRenamed) > 0 { + os.Stdout.WriteString("\n") + os.Stdout.WriteString("Files renamed:\n\n") + sortedPaths := slicesext.MapKeysToSortedSlice(d.pathsRenamed) for _, path := range sortedPaths { - os.Stdout.WriteString("- " + d.removed[path].String() + "\n") + os.Stdout.WriteString("- " + d.pathsRenamed[path].from.String() + "\n") + os.Stdout.WriteString("+ " + d.pathsRenamed[path].to.String() + "\n") } } - if len(d.added) > 0 { + if len(d.pathsAdded) > 0 { os.Stdout.WriteString("\n") os.Stdout.WriteString("Files added:\n\n") - sortedPaths := slicesext.MapKeysToSortedSlice(d.added) + sortedPaths := slicesext.MapKeysToSortedSlice(d.pathsAdded) for _, path := range sortedPaths { - os.Stdout.WriteString("+ " + d.added[path].String() + "\n") + os.Stdout.WriteString("+ " + d.pathsAdded[path].String() + "\n") } } - if len(d.changed) > 0 { + if len(d.pathsChangedContent) > 0 { os.Stdout.WriteString("\n") os.Stdout.WriteString("Files changed content:\n\n") - sortedPaths := slicesext.MapKeysToSortedSlice(d.changed) + sortedPaths := slicesext.MapKeysToSortedSlice(d.pathsChangedContent) for _, path := range sortedPaths { - fnDiff := d.changed[path] + fnDiff := d.pathsChangedContent[path] os.Stdout.WriteString(fnDiff.diff + "\n") } } @@ -144,42 +177,55 @@ func (d *manifestDiff) printText() { func (d *manifestDiff) printMarkdown() { os.Stdout.WriteString(fmt.Sprintf( - "> _%d files changed: %d removed, %d added, %d changed content_\n", - len(d.removed)+len(d.added)+len(d.changed), - len(d.removed), - len(d.added), - len(d.changed), + "> _%d files changed: %d removed, %d renamed, %d added, %d changed content_\n", + len(d.pathsRemoved)+len(d.pathsRenamed)+len(d.pathsAdded)+len(d.pathsChangedContent), + len(d.pathsRemoved), + len(d.pathsRenamed), + len(d.pathsAdded), + len(d.pathsChangedContent), )) - if len(d.removed) > 0 { + if len(d.pathsRemoved) > 0 { os.Stdout.WriteString("\n") os.Stdout.WriteString("# Files removed:\n\n") os.Stdout.WriteString("```diff\n") - sortedPaths := slicesext.MapKeysToSortedSlice(d.removed) + sortedPaths := slicesext.MapKeysToSortedSlice(d.pathsRemoved) for _, path := range sortedPaths { - os.Stdout.WriteString("- " + d.removed[path].String() + "\n") + os.Stdout.WriteString("- " + d.pathsRemoved[path].String() + "\n") } os.Stdout.WriteString("```\n") } - if len(d.added) > 0 { + if len(d.pathsRenamed) > 0 { + os.Stdout.WriteString("\n") + os.Stdout.WriteString("# Files renamed:\n\n") + os.Stdout.WriteString("```diff\n") + sortedPaths := slicesext.MapKeysToSortedSlice(d.pathsRenamed) + for _, path := range sortedPaths { + os.Stdout.WriteString("- " + d.pathsRenamed[path].from.String() + "\n") + os.Stdout.WriteString("+ " + d.pathsRenamed[path].to.String() + "\n") + } + os.Stdout.WriteString("```\n") + } + if len(d.pathsAdded) > 0 { os.Stdout.WriteString("\n") os.Stdout.WriteString("# Files added:\n\n") os.Stdout.WriteString("```diff\n") - sortedPaths := slicesext.MapKeysToSortedSlice(d.added) + sortedPaths := slicesext.MapKeysToSortedSlice(d.pathsAdded) for _, path := range sortedPaths { - os.Stdout.WriteString("+ " + d.added[path].String() + "\n") + os.Stdout.WriteString("+ " + d.pathsAdded[path].String() + "\n") } os.Stdout.WriteString("```\n") } - if len(d.changed) > 0 { + if len(d.pathsChangedContent) > 0 { os.Stdout.WriteString("\n") os.Stdout.WriteString("# Files changed content:\n\n") - sortedPaths := slicesext.MapKeysToSortedSlice(d.changed) + sortedPaths := slicesext.MapKeysToSortedSlice(d.pathsChangedContent) for _, path := range sortedPaths { - fnDiff := d.changed[path] - os.Stdout.WriteString("## `" + fnDiff.path + "`:\n") + fdiff := d.pathsChangedContent[path] + // the path we use here can be from/to, is the same, what changed was the content. + os.Stdout.WriteString("## `" + fdiff.from.Path() + "`:\n") os.Stdout.WriteString( "```diff\n" + - fnDiff.diff + "\n" + + fdiff.diff + "\n" + "```\n", ) } @@ -221,3 +267,9 @@ func calculateFileNodeDiff( } return string(diffData), nil } + +// removeSliceItem returns the slice with the item in index i removed, and the removed item. +func removeSliceItem[T any](s []T, i int) ([]T, T) { + item := s[i] + return append(s[:i], s[i+1:]...), item +} From dbd7ffd5437c2f81d0d90adfabceeb00191ea5c2 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 6 Feb 2025 17:12:49 -0500 Subject: [PATCH 10/13] lint --- cmd/casdiff/casdiff.go | 21 ++++++++++----------- cmd/casdiff/main.go | 1 - 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/cmd/casdiff/casdiff.go b/cmd/casdiff/casdiff.go index f416f3af..6392b030 100644 --- a/cmd/casdiff/casdiff.go +++ b/cmd/casdiff/casdiff.go @@ -38,8 +38,6 @@ type format int const ( formatFlagName = "format" formatFlagShortName = "f" - - emptyDiffSameFromAndTo = "empty diff, from and to are the same" ) const ( @@ -47,6 +45,7 @@ const ( formatMarkdown ) +//nolint:gochecknoglobals // treated as consts var ( formatsValuesToNames = map[format]string{ formatText: "text", @@ -108,13 +107,13 @@ func run( container appext.Container, flags *flags, ) error { - f, ok := formatsNamesToValues[flags.format] + format, ok := formatsNamesToValues[flags.format] if !ok { return fmt.Errorf("unsupported format %s", flags.format) } - from, to := container.Arg(0), container.Arg(1) + from, to := container.Arg(0), container.Arg(1) //nolint:varnamelen // from/to used for symmetry if from == to { - return printDiff(newManifestDiff(), f) + return printDiff(newManifestDiff(), format) } // first, attempt to match from/to as module references in a state file in the same directory // where the command is run @@ -133,7 +132,7 @@ func run( if err != nil { return fmt.Errorf("calculate cas diff: %w", err) } - return printDiff(mdiff, f) + return printDiff(mdiff, format) } // state file was found, attempt to parse it and match from/to with its references stateRW, err := bufstate.NewReadWriter() @@ -168,7 +167,7 @@ func run( return fmt.Errorf("to reference %s not found in the module state file", to) } if fromManifestPath == toManifestPath { - return printDiff(newManifestDiff(), f) + return printDiff(newManifestDiff(), format) } casBucket, err := storageos.NewProvider().NewReadWriteBucket("cas") if err != nil { @@ -178,7 +177,7 @@ func run( if err != nil { return fmt.Errorf("calculate cas diff from state references: %w", err) } - return printDiff(mdiff, f) + return printDiff(mdiff, format) } // calculateDiffFromCASDirectory takes the cas bucket, and the from/to manifest paths to calculate a @@ -215,14 +214,14 @@ func readManifest(ctx context.Context, bucket storage.ReadBucket, manifestPath s return m, nil } -func printDiff(mdiff *manifestDiff, f format) error { - switch f { +func printDiff(mdiff *manifestDiff, format format) error { + switch format { case formatText: mdiff.printText() case formatMarkdown: mdiff.printMarkdown() default: - return fmt.Errorf("format %s not supported", f.String()) + return fmt.Errorf("format %s not supported", format.String()) } return nil } diff --git a/cmd/casdiff/main.go b/cmd/casdiff/main.go index bc9abb7f..5e75f9b7 100644 --- a/cmd/casdiff/main.go +++ b/cmd/casdiff/main.go @@ -26,5 +26,4 @@ const ( func main() { appcmd.Main(context.Background(), newCommand(rootCmdName)) - return } From cfd4a529ef04d744b6c0d066bdab0f645e0cb49d Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 6 Feb 2025 18:12:28 -0500 Subject: [PATCH 11/13] Test --- cmd/casdiff/manifest_diff.go | 8 +- cmd/casdiff/manifest_diff_test.go | 134 ++++++++++++++++++ cmd/casdiff/testdata/manifest_diff/README.md | 12 ++ .../testdata/manifest_diff/from/changes.txt | 1 + .../testdata/manifest_diff/from/to_remove.txt | 1 + .../manifest_diff/from/to_rename_bar/1.txt | 1 + .../manifest_diff/from/to_rename_bar/2.txt | 1 + .../manifest_diff/from/to_rename_bar/3.txt | 1 + .../manifest_diff/from/to_rename_foo/1.txt | 1 + .../manifest_diff/from/to_rename_foo/2.txt | 1 + .../manifest_diff/from/to_rename_foo/3.txt | 1 + .../testdata/manifest_diff/to/added.txt | 1 + .../testdata/manifest_diff/to/changes.txt | 1 + .../manifest_diff/to/renamed_bar/1.txt | 1 + .../manifest_diff/to/renamed_bar/2.txt | 1 + .../manifest_diff/to/renamed_bar/3.txt | 1 + .../manifest_diff/to/renamed_foo/1.txt | 1 + .../manifest_diff/to/renamed_foo/2.txt | 1 + .../manifest_diff/to/renamed_foo/3.txt | 1 + 19 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 cmd/casdiff/manifest_diff_test.go create mode 100644 cmd/casdiff/testdata/manifest_diff/README.md create mode 100644 cmd/casdiff/testdata/manifest_diff/from/changes.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/from/to_remove.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/1.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/2.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/3.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/1.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/2.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/3.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/to/added.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/to/changes.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/to/renamed_bar/1.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/to/renamed_bar/2.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/to/renamed_bar/3.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/to/renamed_foo/1.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/to/renamed_foo/2.txt create mode 100644 cmd/casdiff/testdata/manifest_diff/to/renamed_foo/3.txt diff --git a/cmd/casdiff/manifest_diff.go b/cmd/casdiff/manifest_diff.go index 1d7021b6..b63d8317 100644 --- a/cmd/casdiff/manifest_diff.go +++ b/cmd/casdiff/manifest_diff.go @@ -65,7 +65,7 @@ func buildManifestDiff( toNode := to.GetFileNode(path) if toNode == nil { diff.pathsRemoved[path] = fromNode - digestToRemovedPaths[fromNode.Digest().String()] = append(digestToRemovedPaths[path], path) + digestToRemovedPaths[fromNode.Digest().String()] = append(digestToRemovedPaths[fromNode.Digest().String()], path) continue } if bufcas.DigestEqual(fromNode.Digest(), toNode.Digest()) { @@ -86,7 +86,7 @@ func buildManifestDiff( path := toNode.Path() if from.GetFileNode(path) == nil { diff.pathsAdded[path] = toNode - digestToAddedPaths[toNode.Digest().String()] = append(digestToAddedPaths[path], path) + digestToAddedPaths[toNode.Digest().String()] = append(digestToAddedPaths[toNode.Digest().String()], path) } } // renamed: defined as digests present both in pathsRemoved and pathsAdded but under different @@ -247,11 +247,11 @@ func calculateFileNodeDiff( ) fromData, err := storage.ReadPath(ctx, bucket, fromFilePath) if err != nil { - return "", fmt.Errorf("read from path: %w", err) + return "", fmt.Errorf("read path from: %w", err) } toData, err := storage.ReadPath(ctx, bucket, toFilePath) if err != nil { - return "", fmt.Errorf("read to path: %w", err) + return "", fmt.Errorf("read path to: %w", err) } diffData, err := diff.Diff( ctx, diff --git a/cmd/casdiff/manifest_diff_test.go b/cmd/casdiff/manifest_diff_test.go new file mode 100644 index 00000000..3cbc63dc --- /dev/null +++ b/cmd/casdiff/manifest_diff_test.go @@ -0,0 +1,134 @@ +// Copyright 2021-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/bufbuild/buf/private/bufpkg/bufcas" + "github.com/bufbuild/buf/private/pkg/storage" + "github.com/bufbuild/buf/private/pkg/storage/storagemem" + "github.com/bufbuild/buf/private/pkg/storage/storageos" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestManifestDiff(t *testing.T) { + ctx := context.Background() + casBucket, mFrom, mTo := prepareDiffCASBucket(ctx, t) + mdiff, err := buildManifestDiff(ctx, mFrom, mTo, casBucket) + require.NoError(t, err) + require.NotNil(t, mdiff) + t.Run("removed", func(t *testing.T) { + t.Parallel() + expectedRemovedPaths := map[string]struct{}{ + "to_remove.txt": {}, + } + assert.Len(t, mdiff.pathsRemoved, len(expectedRemovedPaths)) + for expectedRemovedPath := range expectedRemovedPaths { + expected := mFrom.GetFileNode(expectedRemovedPath) + require.NotNil(t, expected) + actual, present := mdiff.pathsRemoved[expectedRemovedPath] + require.True(t, present) + assert.Equal(t, expectedRemovedPath, actual.Path()) + assert.True(t, bufcas.DigestEqual(expected.Digest(), actual.Digest())) + } + }) + t.Run("renamed", func(t *testing.T) { + t.Parallel() + // two directories in testdata are renamed, and the content in all files of each group is the + // same, so this test actually makes sure that all of them are matched (even with the same + // digest), and that the order is trying to be kept. + expectedRenamedPaths := map[string]string{ + "to_rename_bar/1.txt": "renamed_bar/1.txt", + "to_rename_bar/2.txt": "renamed_bar/2.txt", + "to_rename_bar/3.txt": "renamed_bar/3.txt", + "to_rename_foo/1.txt": "renamed_foo/1.txt", + "to_rename_foo/2.txt": "renamed_foo/2.txt", + "to_rename_foo/3.txt": "renamed_foo/3.txt", + } + assert.Len(t, mdiff.pathsRenamed, len(expectedRenamedPaths)) + for fromPath, toPath := range expectedRenamedPaths { + expected := mFrom.GetFileNode(fromPath) + require.NotNil(t, expected) + actual, present := mdiff.pathsRenamed[fromPath] + require.True(t, present) + assert.Equal(t, fromPath, actual.from.Path()) + assert.Equal(t, toPath, actual.to.Path()) + assert.True(t, bufcas.DigestEqual(expected.Digest(), actual.from.Digest())) + assert.True(t, bufcas.DigestEqual(actual.from.Digest(), actual.to.Digest())) + assert.Empty(t, actual.diff) + } + }) + t.Run("added", func(t *testing.T) { + t.Parallel() + expectedAddedPaths := map[string]struct{}{ + "added.txt": {}, + } + assert.Len(t, mdiff.pathsAdded, len(expectedAddedPaths)) + for expectedAddedPath := range expectedAddedPaths { + expected := mTo.GetFileNode(expectedAddedPath) + require.NotNil(t, expected) + actual, present := mdiff.pathsAdded[expectedAddedPath] + require.True(t, present) + assert.Equal(t, expectedAddedPath, actual.Path()) + assert.True(t, bufcas.DigestEqual(expected.Digest(), actual.Digest())) + } + }) + t.Run("changed_content", func(t *testing.T) { + t.Parallel() + expectedChangedContentPaths := map[string]struct{}{ + "changes.txt": {}, + } + assert.Len(t, mdiff.pathsChangedContent, len(expectedChangedContentPaths)) + for expectedChangedContentPath := range expectedChangedContentPaths { + expected := mTo.GetFileNode(expectedChangedContentPath) + require.NotNil(t, expected) + actual, present := mdiff.pathsChangedContent[expectedChangedContentPath] + require.True(t, present) + assert.Equal(t, actual.from.Path(), actual.to.Path()) + assert.NotEqual(t, actual.from.Digest(), actual.to.Digest()) + assert.NotEmpty(t, actual.diff) + } + }) +} + +func prepareDiffCASBucket(ctx context.Context, t *testing.T) ( + storage.ReadBucket, + bufcas.Manifest, + bufcas.Manifest, +) { + casBucket := storagemem.NewReadWriteBucket() + casWrite := func(dirpath string) bufcas.Manifest { + testFiles, err := storageos.NewProvider().NewReadWriteBucket("testdata/manifest_diff/" + dirpath) + require.NoError(t, err) + fileSet, err := bufcas.NewFileSetForBucket(ctx, testFiles) + require.NoError(t, err) + mBlob, err := bufcas.ManifestToBlob(fileSet.Manifest()) + require.NoError(t, err) + blobsToPut := append(fileSet.BlobSet().Blobs(), mBlob) + for _, blob := range blobsToPut { + require.NoError(t, storage.PutPath(ctx, casBucket, hex.EncodeToString(blob.Digest().Value()), blob.Content())) + } + return fileSet.Manifest() + } + var ( + mFrom = casWrite("from") + mTo = casWrite("to") + ) + return casBucket, mFrom, mTo +} diff --git a/cmd/casdiff/testdata/manifest_diff/README.md b/cmd/casdiff/testdata/manifest_diff/README.md new file mode 100644 index 00000000..015cb607 --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/README.md @@ -0,0 +1,12 @@ +## Manifest Diff test structure + +This directory is parsed to a single CAS bucket everytime the test runs, as long as it follows the +following structure: + +``` +. +├── from +│ └── +└── to + └── +``` diff --git a/cmd/casdiff/testdata/manifest_diff/from/changes.txt b/cmd/casdiff/testdata/manifest_diff/from/changes.txt new file mode 100644 index 00000000..b4374a2d --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/from/changes.txt @@ -0,0 +1 @@ +content to change diff --git a/cmd/casdiff/testdata/manifest_diff/from/to_remove.txt b/cmd/casdiff/testdata/manifest_diff/from/to_remove.txt new file mode 100644 index 00000000..227ed66d --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/from/to_remove.txt @@ -0,0 +1 @@ +content to remove diff --git a/cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/1.txt b/cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/1.txt new file mode 100644 index 00000000..ada65e61 --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/1.txt @@ -0,0 +1 @@ +content bar to rename diff --git a/cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/2.txt b/cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/2.txt new file mode 100644 index 00000000..ada65e61 --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/2.txt @@ -0,0 +1 @@ +content bar to rename diff --git a/cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/3.txt b/cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/3.txt new file mode 100644 index 00000000..ada65e61 --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/from/to_rename_bar/3.txt @@ -0,0 +1 @@ +content bar to rename diff --git a/cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/1.txt b/cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/1.txt new file mode 100644 index 00000000..479d73fd --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/1.txt @@ -0,0 +1 @@ +content foo to rename diff --git a/cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/2.txt b/cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/2.txt new file mode 100644 index 00000000..479d73fd --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/2.txt @@ -0,0 +1 @@ +content foo to rename diff --git a/cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/3.txt b/cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/3.txt new file mode 100644 index 00000000..479d73fd --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/from/to_rename_foo/3.txt @@ -0,0 +1 @@ +content foo to rename diff --git a/cmd/casdiff/testdata/manifest_diff/to/added.txt b/cmd/casdiff/testdata/manifest_diff/to/added.txt new file mode 100644 index 00000000..f50e4e9c --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/to/added.txt @@ -0,0 +1 @@ +content added diff --git a/cmd/casdiff/testdata/manifest_diff/to/changes.txt b/cmd/casdiff/testdata/manifest_diff/to/changes.txt new file mode 100644 index 00000000..f7f6607f --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/to/changes.txt @@ -0,0 +1 @@ +content changed diff --git a/cmd/casdiff/testdata/manifest_diff/to/renamed_bar/1.txt b/cmd/casdiff/testdata/manifest_diff/to/renamed_bar/1.txt new file mode 100644 index 00000000..ada65e61 --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/to/renamed_bar/1.txt @@ -0,0 +1 @@ +content bar to rename diff --git a/cmd/casdiff/testdata/manifest_diff/to/renamed_bar/2.txt b/cmd/casdiff/testdata/manifest_diff/to/renamed_bar/2.txt new file mode 100644 index 00000000..ada65e61 --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/to/renamed_bar/2.txt @@ -0,0 +1 @@ +content bar to rename diff --git a/cmd/casdiff/testdata/manifest_diff/to/renamed_bar/3.txt b/cmd/casdiff/testdata/manifest_diff/to/renamed_bar/3.txt new file mode 100644 index 00000000..ada65e61 --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/to/renamed_bar/3.txt @@ -0,0 +1 @@ +content bar to rename diff --git a/cmd/casdiff/testdata/manifest_diff/to/renamed_foo/1.txt b/cmd/casdiff/testdata/manifest_diff/to/renamed_foo/1.txt new file mode 100644 index 00000000..479d73fd --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/to/renamed_foo/1.txt @@ -0,0 +1 @@ +content foo to rename diff --git a/cmd/casdiff/testdata/manifest_diff/to/renamed_foo/2.txt b/cmd/casdiff/testdata/manifest_diff/to/renamed_foo/2.txt new file mode 100644 index 00000000..479d73fd --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/to/renamed_foo/2.txt @@ -0,0 +1 @@ +content foo to rename diff --git a/cmd/casdiff/testdata/manifest_diff/to/renamed_foo/3.txt b/cmd/casdiff/testdata/manifest_diff/to/renamed_foo/3.txt new file mode 100644 index 00000000..479d73fd --- /dev/null +++ b/cmd/casdiff/testdata/manifest_diff/to/renamed_foo/3.txt @@ -0,0 +1 @@ +content foo to rename From 5b15ff4e48dff9344de1381d9473f9b20d07fe9e Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Thu, 6 Feb 2025 18:16:10 -0500 Subject: [PATCH 12/13] lint --- cmd/casdiff/casdiff.go | 2 +- cmd/casdiff/manifest_diff.go | 4 ++-- cmd/casdiff/manifest_diff_test.go | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/casdiff/casdiff.go b/cmd/casdiff/casdiff.go index 6392b030..60d83a62 100644 --- a/cmd/casdiff/casdiff.go +++ b/cmd/casdiff/casdiff.go @@ -111,7 +111,7 @@ func run( if !ok { return fmt.Errorf("unsupported format %s", flags.format) } - from, to := container.Arg(0), container.Arg(1) //nolint:varnamelen // from/to used for symmetry + from, to := container.Arg(0), container.Arg(1) //nolint:varnamelen // from/to used symmetrically if from == to { return printDiff(newManifestDiff(), format) } diff --git a/cmd/casdiff/manifest_diff.go b/cmd/casdiff/manifest_diff.go index b63d8317..50b44a25 100644 --- a/cmd/casdiff/manifest_diff.go +++ b/cmd/casdiff/manifest_diff.go @@ -51,7 +51,7 @@ func newManifestDiff() *manifestDiff { func buildManifestDiff( ctx context.Context, from bufcas.Manifest, - to bufcas.Manifest, + to bufcas.Manifest, //nolint:varnamelen // from/to used symmetrically bucket storage.ReadBucket, ) (*manifestDiff, error) { var ( @@ -235,7 +235,7 @@ func (d *manifestDiff) printMarkdown() { func calculateFileNodeDiff( ctx context.Context, from bufcas.FileNode, - to bufcas.FileNode, + to bufcas.FileNode, //nolint:varnamelen // from/to used symmetrically bucket storage.ReadBucket, ) (string, error) { if from.Path() == to.Path() && bufcas.DigestEqual(from.Digest(), to.Digest()) { diff --git a/cmd/casdiff/manifest_diff_test.go b/cmd/casdiff/manifest_diff_test.go index 3cbc63dc..22afc750 100644 --- a/cmd/casdiff/manifest_diff_test.go +++ b/cmd/casdiff/manifest_diff_test.go @@ -28,6 +28,7 @@ import ( ) func TestManifestDiff(t *testing.T) { + t.Parallel() ctx := context.Background() casBucket, mFrom, mTo := prepareDiffCASBucket(ctx, t) mdiff, err := buildManifestDiff(ctx, mFrom, mTo, casBucket) @@ -112,6 +113,7 @@ func prepareDiffCASBucket(ctx context.Context, t *testing.T) ( bufcas.Manifest, bufcas.Manifest, ) { + t.Helper() casBucket := storagemem.NewReadWriteBucket() casWrite := func(dirpath string) bufcas.Manifest { testFiles, err := storageos.NewProvider().NewReadWriteBucket("testdata/manifest_diff/" + dirpath) From e60533d635931a0f60370320f2c4f67286be3ba4 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Fri, 7 Feb 2025 09:36:41 -0500 Subject: [PATCH 13/13] nit test --- cmd/casdiff/manifest_diff_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/casdiff/manifest_diff_test.go b/cmd/casdiff/manifest_diff_test.go index 22afc750..73f0b73c 100644 --- a/cmd/casdiff/manifest_diff_test.go +++ b/cmd/casdiff/manifest_diff_test.go @@ -102,7 +102,7 @@ func TestManifestDiff(t *testing.T) { actual, present := mdiff.pathsChangedContent[expectedChangedContentPath] require.True(t, present) assert.Equal(t, actual.from.Path(), actual.to.Path()) - assert.NotEqual(t, actual.from.Digest(), actual.to.Digest()) + assert.False(t, bufcas.DigestEqual(actual.from.Digest(), actual.to.Digest())) assert.NotEmpty(t, actual.diff) } })