-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Utility to help diff changes between 2 manifests in CAS directories.
- Loading branch information
1 parent
2eb1e71
commit 6ec4489
Showing
23 changed files
with
752 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
// 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" | ||
) | ||
|
||
const ( | ||
formatText format = iota + 1 | ||
formatMarkdown | ||
) | ||
|
||
//nolint:gochecknoglobals // treated as consts | ||
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 + " <from> <to>", | ||
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 { | ||
format, ok := formatsNamesToValues[flags.format] | ||
if !ok { | ||
return fmt.Errorf("unsupported format %s", flags.format) | ||
} | ||
from, to := container.Arg(0), container.Arg(1) //nolint:varnamelen // from/to used symmetrically | ||
if from == to { | ||
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 | ||
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, format) | ||
} | ||
// 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(), format) | ||
} | ||
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, format) | ||
} | ||
|
||
// 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, format format) error { | ||
switch format { | ||
case formatText: | ||
mdiff.printText() | ||
case formatMarkdown: | ||
mdiff.printMarkdown() | ||
default: | ||
return fmt.Errorf("format %s not supported", format.String()) | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// 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" | ||
|
||
"github.com/bufbuild/buf/private/pkg/app/appcmd" | ||
) | ||
|
||
const ( | ||
rootCmdName = "casdiff" | ||
) | ||
|
||
func main() { | ||
appcmd.Main(context.Background(), newCommand(rootCmdName)) | ||
} |
Oops, something went wrong.