From af56e7ba8cefa8eed8628833fe73261a423894e6 Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Fri, 26 Sep 2025 16:16:50 +0200 Subject: [PATCH 01/10] Add a 'status' command for git&hg directories For git & mercurial fully cloned directories, the status command returns a summary of local changes. Signed-off-by: Christophe de Vienne --- pkg/vendir/cmd/status.go | 108 +++++++++++++++++++++++++++++++++ pkg/vendir/cmd/vendir.go | 1 + pkg/vendir/directory/status.go | 44 ++++++++++++++ pkg/vendir/fetch/git/status.go | 74 ++++++++++++++++++++++ pkg/vendir/fetch/hg/hg.go | 8 ++- pkg/vendir/fetch/hg/status.go | 84 +++++++++++++++++++++++++ pkg/vendir/status/status.go | 71 ++++++++++++++++++++++ 7 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 pkg/vendir/cmd/status.go create mode 100644 pkg/vendir/directory/status.go create mode 100644 pkg/vendir/fetch/git/status.go create mode 100644 pkg/vendir/fetch/hg/status.go create mode 100644 pkg/vendir/status/status.go diff --git a/pkg/vendir/cmd/status.go b/pkg/vendir/cmd/status.go new file mode 100644 index 00000000..b04d1c65 --- /dev/null +++ b/pkg/vendir/cmd/status.go @@ -0,0 +1,108 @@ +package cmd + +import ( + "fmt" + "maps" + "os" + + "github.com/cppforlife/go-cli-ui/ui" + "github.com/spf13/cobra" + + ctlconf "carvel.dev/vendir/pkg/vendir/config" + ctldir "carvel.dev/vendir/pkg/vendir/directory" + ctlcache "carvel.dev/vendir/pkg/vendir/fetch/cache" + ctlstatus "carvel.dev/vendir/pkg/vendir/status" +) + +type StatusOptions struct { + ui ui.UI + + Files []string + LockFile string + + Chdir string + ExitCode bool +} + +func NewStatusOptions(ui ui.UI) *StatusOptions { + return &StatusOptions{ui: ui} +} + +func NewStatusCmd(o *StatusOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "status", + Short: "Check local repositories status", + RunE: func(_ *cobra.Command, _ []string) error { return o.Run() }, + } + + cmd.Flags().StringSliceVarP(&o.Files, "file", "f", []string{defaultConfigName}, "Set configuration file") + cmd.Flags().StringVar(&o.LockFile, "lock-file", defaultLockName, "Set lock file") + cmd.Flags().StringVar(&o.Chdir, "chdir", "", "Set current directory for process") + cmd.Flags().BoolVar(&o.ExitCode, "exit-code", false, "Set to 'true', it exits with a non-0 code if any subproject is not clean") + + return cmd +} + +func (o *StatusOptions) Run() error { + if len(o.Chdir) > 0 { + err := os.Chdir(o.Chdir) + if err != nil { + return fmt.Errorf("Running chdir: %s", err) + } + } + + conf, secrets, configMaps, err := ctlconf.NewConfigFromFiles(o.Files) + if err != nil { + return (*SyncOptions)(nil).configReadHintErrMsg(err, o.Files) + } + + existingLockConfig, err := ctlconf.NewLockConfigFromFile(o.LockFile) + if err != nil { + return err + } + + cache, err := ctlcache.NewCache(os.Getenv("VENDIR_CACHE_DIR"), "0Mi") + if err != nil { + return fmt.Errorf("Unable to create cache: %s", err) + } + syncOpts := ctldir.SyncOpts{ + RefFetcher: ctldir.NewNamedRefFetcher(secrets, configMaps), + GithubAPIToken: os.Getenv("VENDIR_GITHUB_API_TOKEN"), + HelmBinary: os.Getenv("VENDIR_HELM_BINARY"), + Cache: cache, + Lazy: false, + Partial: false, + } + + status, err := fullStatus(conf, syncOpts, existingLockConfig, o.ui) + if err != nil { + return err + } + + o.ui.PrintBlock([]byte("---------------\n\n")) + o.ui.PrintBlock([]byte(status.String())) + + return nil +} + +func fullStatus( + conf ctlconf.Config, + syncOpts ctldir.SyncOpts, + existingLockConfig ctlconf.LockConfig, + ui ui.UI, +) (ctlstatus.StatusMap, error) { + status := ctlstatus.StatusMap{} + for _, dirConf := range conf.Directories { + dirExistingLockConf, _ := existingLockConfig.FindDirectory(dirConf.Path) + directory := ctldir.NewDirectory(dirConf, dirExistingLockConf, ui) + + dirStatus, err := directory.Status(syncOpts) + if err != nil { + return nil, fmt.Errorf("Reading directory '%s': %s", dirConf.Path, err) + } + + maps.Copy(status, dirStatus) + } + + return status, nil +} diff --git a/pkg/vendir/cmd/vendir.go b/pkg/vendir/cmd/vendir.go index a16722e5..c2bb162e 100644 --- a/pkg/vendir/cmd/vendir.go +++ b/pkg/vendir/cmd/vendir.go @@ -42,6 +42,7 @@ func NewVendirCmd(o *VendirOptions) *cobra.Command { o.UIFlags.Set(cmd) cmd.AddCommand(NewSyncCmd(NewSyncOptions(o.ui))) + cmd.AddCommand(NewStatusCmd(NewStatusOptions(o.ui))) cmd.AddCommand(NewVersionCmd(NewVersionOptions(o.ui))) toolsCmd := NewToolsCmd() diff --git a/pkg/vendir/directory/status.go b/pkg/vendir/directory/status.go new file mode 100644 index 00000000..601c8295 --- /dev/null +++ b/pkg/vendir/directory/status.go @@ -0,0 +1,44 @@ +package directory + +import ( + "path" + + ctlgit "carvel.dev/vendir/pkg/vendir/fetch/git" + ctlhg "carvel.dev/vendir/pkg/vendir/fetch/hg" + ctlstatus "carvel.dev/vendir/pkg/vendir/status" +) + +func (d *Directory) Status(syncOpts SyncOpts) (map[string]*ctlstatus.Status, error) { + res := map[string]*ctlstatus.Status{} + + for _, contents := range d.opts.Contents { + path := path.Join(d.opts.Path, contents.Path) + switch { + case contents.Git != nil: + gitSync := ctlgit.NewSync(*contents.Git, NewInfoLog(d.ui), syncOpts.RefFetcher, syncOpts.Cache) + + gitStatus, err := gitSync.Status(path) + if err != nil { + return nil, err + } + + if gitStatus != nil { + res[path] = gitStatus + } + case contents.Hg != nil: + hgSync := ctlhg.NewSync( + *contents.Hg, NewInfoLog(d.ui), syncOpts.RefFetcher, syncOpts.Cache) + + hgStatus, err := hgSync.Status(path) + if err != nil { + return nil, err + } + + if hgStatus != nil { + res[path] = hgStatus + } + } + } + + return res, nil +} diff --git a/pkg/vendir/fetch/git/status.go b/pkg/vendir/fetch/git/status.go new file mode 100644 index 00000000..ac1e52dd --- /dev/null +++ b/pkg/vendir/fetch/git/status.go @@ -0,0 +1,74 @@ +package git + +import ( + "os" + "path" + "strings" + + ctlstatus "carvel.dev/vendir/pkg/vendir/status" +) + +func (d Sync) Status(target string) (*ctlstatus.Status, error) { + _, err := os.Stat(path.Join(target, ".git")) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + + return nil, err + } + + git := NewGit(d.opts, d.log, d.refFetcher) + + status := ctlstatus.Status{} + + out, _, err := git.cmdRunner.Run([]string{"rev-parse", "HEAD"}, []string{}, target) + if err != nil { + return nil, err + } + status.Ref.SHA = strings.TrimSpace(out) + + out, _, err = git.cmdRunner.Run([]string{"tag", "--contains"}, []string{}, target) + if err != nil { + return nil, err + } + if out != "" { + status.Ref.Tags = strings.Split(strings.TrimSpace(out), "\n") + } + + out, _, err = git.cmdRunner.Run([]string{"branch", "--contains"}, []string{}, target) + if err != nil { + return nil, err + } + if out != "" { + status.Ref.Others = strings.Split(out, "\n") + } + + out, _, err = git.cmdRunner.Run( + []string{"log", "--branches", "--not", "--remotes", "--oneline"}, + []string{}, + target, + ) + if err != nil { + return nil, err + } + + if out != "" { + status.LocalCsets = strings.Split(strings.TrimSpace(out), "\n") + } + + out, _, err = git.cmdRunner.Run( + []string{"status", "--short"}, + []string{}, + target, + ) + if err != nil { + return nil, err + } + + if out != "" { + status.UncommitedChanges = strings.Split(strings.TrimSpace(out), "\n") + } + + return &status, nil +} diff --git a/pkg/vendir/fetch/hg/hg.go b/pkg/vendir/fetch/hg/hg.go index aa24ff40..665517b6 100644 --- a/pkg/vendir/fetch/hg/hg.go +++ b/pkg/vendir/fetch/hg/hg.go @@ -33,8 +33,10 @@ func NewHg(opts ctlconf.DirectoryContentsHg, tempArea ctlfetch.TempArea, ) (*Hg, error) { t := Hg{opts, infoLog, refFetcher, "", nil, ""} - if err := t.setup(tempArea); err != nil { - return nil, err + if tempArea != nil { + if err := t.setup(tempArea); err != nil { + return nil, err + } } return &t, nil } @@ -236,7 +238,7 @@ func (t *Hg) run(args []string, dstPath string) (string, string, error) { err := cmd.Run() if err != nil { - return "", "", fmt.Errorf("Hg %s: %s (stderr: %s)", args, err, stderrBs.String()) + return "", "", fmt.Errorf("Hg %s: %w (stderr: %s)", args, err, stderrBs.String()) } return stdoutBs.String(), stderrBs.String(), nil diff --git a/pkg/vendir/fetch/hg/status.go b/pkg/vendir/fetch/hg/status.go new file mode 100644 index 00000000..24c95881 --- /dev/null +++ b/pkg/vendir/fetch/hg/status.go @@ -0,0 +1,84 @@ +package hg + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path" + "strings" + + ctlstatus "carvel.dev/vendir/pkg/vendir/status" +) + +func (d Sync) Status(target string) (*ctlstatus.Status, error) { + _, err := os.Stat(path.Join(target, ".hg")) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + + return nil, err + } + + hg, err := NewHg(d.opts, d.log, d.refFetcher, nil) + if err != nil { + return nil, fmt.Errorf("Setting up hg: %w", err) + } + defer hg.Close() + + out, _, err := hg.run([]string{ + "log", "-r", ".", + "-T", "{node}\n{tags}\n{branch}\n{topic}\n{bookmarks}\n", + }, target) + if err != nil { + return nil, err + } + + splitted := strings.Split(out, "\n") + sha := splitted[0] + tags := splitted[1] + branch := splitted[2] + topic := splitted[3] + bookmarks := splitted[4] + + status := ctlstatus.Status{ + Ref: ctlstatus.CompleteReference{ + SHA: sha, + }, + } + if tags != "" { + status.Ref.Tags = strings.Split(tags, " ") + } + if branch != "" { + status.Ref.Others = append(status.Ref.Others, branch) + } + if topic != "" { + status.Ref.Others = append(status.Ref.Others, topic) + } + if bookmarks != "" { + status.Ref.Others = append(status.Ref.Others, bookmarks) + } + + out, _, err = hg.run([]string{"status"}, target) + if err != nil { + return nil, err + } + if out != "" { + status.UncommitedChanges = strings.Split(strings.TrimSpace(out), "\n") + } + + out, _, err = hg.run([]string{"out", "-q", "-T", "{node}\n"}, target) + if err != nil { + var exitErr *exec.ExitError + if !errors.As(err, &exitErr) || exitErr.ExitCode() != 1 { + return nil, err + } + } + + if out != "" { + status.LocalCsets = strings.Split(strings.TrimSpace(out), "\n") + } + + return &status, nil +} diff --git a/pkg/vendir/status/status.go b/pkg/vendir/status/status.go new file mode 100644 index 00000000..748a92c1 --- /dev/null +++ b/pkg/vendir/status/status.go @@ -0,0 +1,71 @@ +// Copyright 2025 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "fmt" + "strings" +) + +type CompleteReference struct { + SHA string + Tags []string + Others []string +} + +type Status struct { + Ref CompleteReference + UncommitedChanges []string + LocalCsets []string +} + +func (s Status) IsSafe() bool { + return len(s.UncommitedChanges) == 0 && len(s.LocalCsets) == 0 +} + +func (s Status) String() string { + if s.IsSafe() { + return "clean" + } + + messages := make([]string, 0, 2) + + if len(s.UncommitedChanges) != 0 { + messages = append(messages, fmt.Sprintf("%d uncommited changes", len(s.UncommitedChanges))) + } + if len(s.LocalCsets) != 0 { + messages = append(messages, fmt.Sprintf("%d unpushed commits", len(s.LocalCsets))) + } + + return strings.Join(messages, ", ") +} + +type StatusMap map[string]*Status + +func (sm StatusMap) String() string { + var s string + + s += "Detailled status:\n" + for dir, status := range sm { + s += "- " + dir + ": " + status.String() + "\n" + } + + if !sm.IsSafe() { + s += "\n /!\\ At least one directory is not clean /!\\\n" + } + + return s +} + +func (sm StatusMap) IsSafe() bool { + isSafe := true + for _, status := range sm { + if !status.IsSafe() { + isSafe = false + break + } + } + + return isSafe +} From b5b8b73e60c3d1fb1b308b3f7e70678f70e74674 Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Tue, 23 Dec 2025 17:48:13 +0100 Subject: [PATCH 02/10] Add '--safe' mode the the 'sync' command Signed-off-by: Christophe de Vienne --- pkg/vendir/cmd/sync.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/vendir/cmd/sync.go b/pkg/vendir/cmd/sync.go index 4e2cc816..354a6df0 100644 --- a/pkg/vendir/cmd/sync.go +++ b/pkg/vendir/cmd/sync.go @@ -34,6 +34,8 @@ type SyncOptions struct { Chdir string AllowAllSymlinkDestinations bool + + Safe bool } func NewSyncOptions(ui ui.UI) *SyncOptions { @@ -56,6 +58,8 @@ func NewSyncCmd(o *SyncOptions) *cobra.Command { cmd.Flags().StringVar(&o.Chdir, "chdir", "", "Set current directory for process") cmd.Flags().BoolVar(&o.AllowAllSymlinkDestinations, "dangerous-allow-all-symlink-destinations", false, "Symlinks to all destinations are allowed") + cmd.Flags().BoolVar(&o.Safe, "safe", false, "sync only if local DVCS clones are clean") + return cmd } @@ -137,6 +141,17 @@ func (o *SyncOptions) Run() error { } newLockConfig := ctlconf.NewLockConfig() + if o.Safe { + status, err := fullStatus(conf, syncOpts, existingLockConfig, o.ui) + if err != nil { + return err + } + + if !status.IsSafe() { + return fmt.Errorf("--safe mode forbids a sync: %s", status.String()) + } + } + for _, dirConf := range conf.Directories { // error safe to ignore, since lock file might not exist dirExistingLockConf, _ := existingLockConfig.FindDirectory(dirConf.Path) From 485152435e5b53f5627902d7b9b175534a97387c Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Mon, 29 Dec 2025 14:11:10 +0100 Subject: [PATCH 03/10] Status: add a 'TargetRef' field Signed-off-by: Christophe de Vienne --- pkg/vendir/fetch/git/status.go | 4 +++- pkg/vendir/fetch/hg/status.go | 1 + pkg/vendir/status/status.go | 14 +++++++++++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/vendir/fetch/git/status.go b/pkg/vendir/fetch/git/status.go index ac1e52dd..d477a1b0 100644 --- a/pkg/vendir/fetch/git/status.go +++ b/pkg/vendir/fetch/git/status.go @@ -20,7 +20,9 @@ func (d Sync) Status(target string) (*ctlstatus.Status, error) { git := NewGit(d.opts, d.log, d.refFetcher) - status := ctlstatus.Status{} + status := ctlstatus.Status{ + TargetRef: d.opts.Ref, + } out, _, err := git.cmdRunner.Run([]string{"rev-parse", "HEAD"}, []string{}, target) if err != nil { diff --git a/pkg/vendir/fetch/hg/status.go b/pkg/vendir/fetch/hg/status.go index 24c95881..9317ea0e 100644 --- a/pkg/vendir/fetch/hg/status.go +++ b/pkg/vendir/fetch/hg/status.go @@ -43,6 +43,7 @@ func (d Sync) Status(target string) (*ctlstatus.Status, error) { bookmarks := splitted[4] status := ctlstatus.Status{ + TargetRef: d.opts.Ref, Ref: ctlstatus.CompleteReference{ SHA: sha, }, diff --git a/pkg/vendir/status/status.go b/pkg/vendir/status/status.go index 748a92c1..d21bf452 100644 --- a/pkg/vendir/status/status.go +++ b/pkg/vendir/status/status.go @@ -5,6 +5,7 @@ package status import ( "fmt" + "slices" "strings" ) @@ -15,6 +16,7 @@ type CompleteReference struct { } type Status struct { + TargetRef string Ref CompleteReference UncommitedChanges []string LocalCsets []string @@ -25,12 +27,12 @@ func (s Status) IsSafe() bool { } func (s Status) String() string { + messages := make([]string, 0, 3) + if s.IsSafe() { - return "clean" + messages = append(messages, "clean") } - messages := make([]string, 0, 2) - if len(s.UncommitedChanges) != 0 { messages = append(messages, fmt.Sprintf("%d uncommited changes", len(s.UncommitedChanges))) } @@ -38,6 +40,12 @@ func (s Status) String() string { messages = append(messages, fmt.Sprintf("%d unpushed commits", len(s.LocalCsets))) } + if !strings.HasPrefix(s.Ref.SHA, s.TargetRef) && + !slices.Contains(s.Ref.Tags, s.TargetRef) && + !slices.Contains(s.Ref.Others, s.TargetRef) { + messages = append(messages, "ref mismatch") + } + return strings.Join(messages, ", ") } From 8348b75d9f719350a1b2a76152ffe2c1bf91ed1c Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Mon, 29 Dec 2025 15:50:17 +0100 Subject: [PATCH 04/10] Status: add DirectoryPath & ContentPath Signed-off-by: Christophe de Vienne --- pkg/vendir/cmd/status.go | 7 +++---- pkg/vendir/directory/status.go | 12 ++++++++---- pkg/vendir/status/status.go | 12 +++++++----- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/pkg/vendir/cmd/status.go b/pkg/vendir/cmd/status.go index b04d1c65..c6f913f9 100644 --- a/pkg/vendir/cmd/status.go +++ b/pkg/vendir/cmd/status.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "maps" "os" "github.com/cppforlife/go-cli-ui/ui" @@ -90,8 +89,8 @@ func fullStatus( syncOpts ctldir.SyncOpts, existingLockConfig ctlconf.LockConfig, ui ui.UI, -) (ctlstatus.StatusMap, error) { - status := ctlstatus.StatusMap{} +) (ctlstatus.StatusList, error) { + status := ctlstatus.StatusList{} for _, dirConf := range conf.Directories { dirExistingLockConf, _ := existingLockConfig.FindDirectory(dirConf.Path) directory := ctldir.NewDirectory(dirConf, dirExistingLockConf, ui) @@ -101,7 +100,7 @@ func fullStatus( return nil, fmt.Errorf("Reading directory '%s': %s", dirConf.Path, err) } - maps.Copy(status, dirStatus) + status = append(status, dirStatus...) } return status, nil diff --git a/pkg/vendir/directory/status.go b/pkg/vendir/directory/status.go index 601c8295..124f9d1d 100644 --- a/pkg/vendir/directory/status.go +++ b/pkg/vendir/directory/status.go @@ -8,8 +8,8 @@ import ( ctlstatus "carvel.dev/vendir/pkg/vendir/status" ) -func (d *Directory) Status(syncOpts SyncOpts) (map[string]*ctlstatus.Status, error) { - res := map[string]*ctlstatus.Status{} +func (d *Directory) Status(syncOpts SyncOpts) (ctlstatus.StatusList, error) { + var res ctlstatus.StatusList for _, contents := range d.opts.Contents { path := path.Join(d.opts.Path, contents.Path) @@ -23,7 +23,9 @@ func (d *Directory) Status(syncOpts SyncOpts) (map[string]*ctlstatus.Status, err } if gitStatus != nil { - res[path] = gitStatus + gitStatus.DirectoryPath = d.opts.Path + gitStatus.ContentPath = contents.Path + res = append(res, gitStatus) } case contents.Hg != nil: hgSync := ctlhg.NewSync( @@ -35,7 +37,9 @@ func (d *Directory) Status(syncOpts SyncOpts) (map[string]*ctlstatus.Status, err } if hgStatus != nil { - res[path] = hgStatus + hgStatus.DirectoryPath = d.opts.Path + hgStatus.ContentPath = contents.Path + res = append(res, hgStatus) } } } diff --git a/pkg/vendir/status/status.go b/pkg/vendir/status/status.go index d21bf452..1b0af9b0 100644 --- a/pkg/vendir/status/status.go +++ b/pkg/vendir/status/status.go @@ -16,6 +16,8 @@ type CompleteReference struct { } type Status struct { + DirectoryPath string + ContentPath string TargetRef string Ref CompleteReference UncommitedChanges []string @@ -49,14 +51,14 @@ func (s Status) String() string { return strings.Join(messages, ", ") } -type StatusMap map[string]*Status +type StatusList []*Status -func (sm StatusMap) String() string { +func (sm StatusList) String() string { var s string s += "Detailled status:\n" - for dir, status := range sm { - s += "- " + dir + ": " + status.String() + "\n" + for _, status := range sm { + s += "- " + status.DirectoryPath + "/" + status.ContentPath + ": " + status.String() + "\n" } if !sm.IsSafe() { @@ -66,7 +68,7 @@ func (sm StatusMap) String() string { return s } -func (sm StatusMap) IsSafe() bool { +func (sm StatusList) IsSafe() bool { isSafe := true for _, status := range sm { if !status.IsSafe() { From 4c166f184f6adad72f798fa86d1e5e021d107059 Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Mon, 29 Dec 2025 16:38:10 +0100 Subject: [PATCH 05/10] New command: 'vendir baseline' This command updates the git & hg 'ref' field based on the actual checked-out commit of the corresponding content paths Signed-off-by: Christophe de Vienne --- go.mod | 2 +- pkg/vendir/cmd/baseline.go | 222 ++++++++++++++++++++++++++++++++++ pkg/vendir/cmd/vendir.go | 1 + pkg/vendir/fetch/hg/status.go | 3 + pkg/vendir/status/status.go | 19 ++- 5 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 pkg/vendir/cmd/baseline.go diff --git a/go.mod b/go.mod index 429f0957..b3071996 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( golang.org/x/oauth2 v0.30.0 golang.org/x/tools v0.34.0 gopkg.in/inf.v0 v0.9.1 + gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.24.3 k8s.io/code-generator v0.17.2 sigs.k8s.io/yaml v1.4.0 @@ -103,7 +104,6 @@ require ( golang.org/x/text v0.26.0 // indirect gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c // indirect k8s.io/klog v1.0.0 // indirect k8s.io/klog/v2 v2.70.1 // indirect diff --git a/pkg/vendir/cmd/baseline.go b/pkg/vendir/cmd/baseline.go new file mode 100644 index 00000000..72a1f20e --- /dev/null +++ b/pkg/vendir/cmd/baseline.go @@ -0,0 +1,222 @@ +package cmd + +import ( + "fmt" + "os" + "path" + + "github.com/cppforlife/go-cli-ui/ui" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + ctlconf "carvel.dev/vendir/pkg/vendir/config" + ctldir "carvel.dev/vendir/pkg/vendir/directory" + ctlcache "carvel.dev/vendir/pkg/vendir/fetch/cache" +) + +func NewBaselineOptions(ui ui.UI) *BaselineOptions { + return &BaselineOptions{ui: ui} +} + +func NewBaselineCmd(o *BaselineOptions) *cobra.Command { + cmd := cobra.Command{ + Use: "baseline", + Short: "Update git/hg repositories 'ref' to match the current commits", + RunE: func(_ *cobra.Command, _ []string) error { return o.Run() }, + } + + cmd.Flags().BoolVarP( + &o.Yes, "yes", "y", false, + "If true, automatically answer 'yes' to all the questions") + + cmd.Flags().StringSliceVarP(&o.Files, "file", "f", []string{defaultConfigName}, "Set configuration file") + cmd.Flags().StringVar(&o.LockFile, "lock-file", defaultLockName, "Set lock file") + cmd.Flags().StringVar(&o.Chdir, "chdir", "", "Set current directory for process") + cmd.Flags().BoolVar(&o.PreferSHA, "prefer-sha", false, "Prefer sha instead of tags") + cmd.Flags().BoolVar(&o.DryRun, "dry-run", false, "List what would be done") + + return &cmd +} + +type BaselineOptions struct { + ui ui.UI + + Files []string + LockFile string + + Chdir string + ExitCode bool + + PreferSHA bool + Yes bool + DryRun bool +} + +func (o *BaselineOptions) Run() error { + if len(o.Chdir) > 0 { + err := os.Chdir(o.Chdir) + if err != nil { + return fmt.Errorf("Running chdir: %s", err) + } + } + + conf, secrets, configMaps, err := ctlconf.NewConfigFromFiles(o.Files) + if err != nil { + return (*SyncOptions)(nil).configReadHintErrMsg(err, o.Files) + } + + existingLockConfig, err := ctlconf.NewLockConfigFromFile(o.LockFile) + if err != nil { + return err + } + + cache, err := ctlcache.NewCache(os.Getenv("VENDIR_CACHE_DIR"), "0Mi") + if err != nil { + return fmt.Errorf("Unable to create cache: %s", err) + } + syncOpts := ctldir.SyncOpts{ + RefFetcher: ctldir.NewNamedRefFetcher(secrets, configMaps), + GithubAPIToken: os.Getenv("VENDIR_GITHUB_API_TOKEN"), + HelmBinary: os.Getenv("VENDIR_HELM_BINARY"), + Cache: cache, + Lazy: false, + Partial: false, + } + + statusMap, err := fullStatus(conf, syncOpts, existingLockConfig, o.ui) + if err != nil { + return err + } + + newRefs := make(map[string]string) + + for _, status := range statusMap { + if !status.MatchTarget() || !o.PreferSHA && !status.MatchTargetTag() && len(status.Ref.Tags) != 0 { + var newRef string + if o.PreferSHA || len(status.Ref.Tags) == 0 { + newRef = status.Ref.SHA + } else { + newRef = status.Ref.Tags[0] + } + newRefs[status.Path()] = newRef + } + } + + if len(newRefs) == 0 { + o.ui.PrintLinef("All references already match current state, no update needed") + + return nil + } + + o.ui.PrintLinef("New baseline:") + for _, status := range statusMap { + newRef := newRefs[status.Path()] + if newRef != "" { + newRef = " -> " + newRef + } + o.ui.PrintLinef("%s/%s: %s%s", status.DirectoryPath, status.ContentPath, status.TargetRef, newRef) + } + + if !o.DryRun { + for _, fname := range o.Files { + if err := updateRefs(fname, newRefs); err != nil { + return err + } + } + } + + return nil +} + +func loadFile(fname string) (*yaml.Node, error) { + f, err := os.Open(fname) + if err != nil { + return nil, err + } + + defer f.Close() + + var node yaml.Node + + if err := yaml.NewDecoder(f).Decode(&node); err != nil { + return nil, err + } + + return &node, err +} + +func saveFile(fname string, doc *yaml.Node) error { + f, err := os.Create(fname) + if err != nil { + return err + } + + enc := yaml.NewEncoder(f) + enc.SetIndent(2) + if err := enc.Encode(doc); err != nil { + _ = f.Close() + + return err + } + + return f.Close() +} + +func updateRefs(fname string, newRefs map[string]string) error { + doc, err := loadFile(fname) + if err != nil { + return err + } + + if doc.Kind != yaml.DocumentNode { + panic("expects the root node") + } + + top := doc.Content[0] + if top.Kind != yaml.MappingNode { + panic("top content must be a mapping") + } + + directories := getMappingNodeChild(top, "directories") + + for _, d := range directories.Content { + dirPath := getMappingNodeChild(d, "path").Value + + contents := getMappingNodeChild(d, "contents") + + for _, content := range contents.Content { + contentPath := getMappingNodeChild(content, "path").Value + + fullPath := path.Join(dirPath, contentPath) + + if newRef, ok := newRefs[fullPath]; ok { + if hg := getMappingNodeChild(content, "hg"); hg != nil { + ref := getMappingNodeChild(hg, "ref") + if ref == nil { + return fmt.Errorf("could not find 'ref' for '%s'", fullPath) + } + ref.Value = newRef + } + if git := getMappingNodeChild(content, "git"); git != nil { + ref := getMappingNodeChild(git, "ref") + if ref == nil { + return fmt.Errorf("could not find 'ref' for '%s'", fullPath) + } + ref.Value = newRef + } + } + } + } + + return saveFile(fname, doc) +} + +func getMappingNodeChild(node *yaml.Node, name string) *yaml.Node { + for i := 0; i < len(node.Content); i += 2 { + if node.Content[i].Value == name { + return node.Content[i+1] + } + } + + return nil +} diff --git a/pkg/vendir/cmd/vendir.go b/pkg/vendir/cmd/vendir.go index c2bb162e..cccb98ac 100644 --- a/pkg/vendir/cmd/vendir.go +++ b/pkg/vendir/cmd/vendir.go @@ -44,6 +44,7 @@ func NewVendirCmd(o *VendirOptions) *cobra.Command { cmd.AddCommand(NewSyncCmd(NewSyncOptions(o.ui))) cmd.AddCommand(NewStatusCmd(NewStatusOptions(o.ui))) cmd.AddCommand(NewVersionCmd(NewVersionOptions(o.ui))) + cmd.AddCommand(NewBaselineCmd(NewBaselineOptions(o.ui))) toolsCmd := NewToolsCmd() toolsCmd.AddCommand(NewSortSemverCmd(NewSortSemverOptions(o.ui))) diff --git a/pkg/vendir/fetch/hg/status.go b/pkg/vendir/fetch/hg/status.go index 9317ea0e..876dec38 100644 --- a/pkg/vendir/fetch/hg/status.go +++ b/pkg/vendir/fetch/hg/status.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path" + "slices" "strings" ctlstatus "carvel.dev/vendir/pkg/vendir/status" @@ -50,6 +51,8 @@ func (d Sync) Status(target string) (*ctlstatus.Status, error) { } if tags != "" { status.Ref.Tags = strings.Split(tags, " ") + status.Ref.Tags = slices.DeleteFunc( + status.Ref.Tags, func(t string) bool { return t == "tip" }) } if branch != "" { status.Ref.Others = append(status.Ref.Others, branch) diff --git a/pkg/vendir/status/status.go b/pkg/vendir/status/status.go index 1b0af9b0..fac32e88 100644 --- a/pkg/vendir/status/status.go +++ b/pkg/vendir/status/status.go @@ -5,6 +5,7 @@ package status import ( "fmt" + "path" "slices" "strings" ) @@ -24,10 +25,24 @@ type Status struct { LocalCsets []string } +func (s Status) Path() string { + return path.Join(s.DirectoryPath, s.ContentPath) +} + func (s Status) IsSafe() bool { return len(s.UncommitedChanges) == 0 && len(s.LocalCsets) == 0 } +func (s Status) MatchTarget() bool { + return strings.HasPrefix(s.Ref.SHA, s.TargetRef) || + slices.Contains(s.Ref.Tags, s.TargetRef) || + slices.Contains(s.Ref.Others, s.TargetRef) +} + +func (s Status) MatchTargetTag() bool { + return slices.Contains(s.Ref.Tags, s.TargetRef) +} + func (s Status) String() string { messages := make([]string, 0, 3) @@ -42,9 +57,7 @@ func (s Status) String() string { messages = append(messages, fmt.Sprintf("%d unpushed commits", len(s.LocalCsets))) } - if !strings.HasPrefix(s.Ref.SHA, s.TargetRef) && - !slices.Contains(s.Ref.Tags, s.TargetRef) && - !slices.Contains(s.Ref.Others, s.TargetRef) { + if !s.MatchTarget() { messages = append(messages, "ref mismatch") } From 2e81a148176ed68ffa0f59044d6f9d7b263c3d18 Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Tue, 30 Dec 2025 10:34:12 +0100 Subject: [PATCH 06/10] Code cleaning Apply some of the advice of 'revive'. Signed-off-by: Christophe de Vienne --- pkg/vendir/cmd/baseline.go | 52 ++++++++++++++++++++++------------ pkg/vendir/cmd/status.go | 27 ++++++++++++------ pkg/vendir/cmd/sync.go | 3 +- pkg/vendir/directory/status.go | 16 +++++++---- pkg/vendir/fetch/git/status.go | 12 ++++++-- pkg/vendir/fetch/hg/status.go | 27 +++++++++++------- 6 files changed, 91 insertions(+), 46 deletions(-) diff --git a/pkg/vendir/cmd/baseline.go b/pkg/vendir/cmd/baseline.go index 72a1f20e..2f9d7259 100644 --- a/pkg/vendir/cmd/baseline.go +++ b/pkg/vendir/cmd/baseline.go @@ -1,3 +1,6 @@ +// Copyright 2024 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + package cmd import ( @@ -14,7 +17,7 @@ import ( ctlcache "carvel.dev/vendir/pkg/vendir/fetch/cache" ) -func NewBaselineOptions(ui ui.UI) *BaselineOptions { +func NewBaselineOptions(ui ui.UI) *BaselineOptions { //nolint:revive return &BaselineOptions{ui: ui} } @@ -29,10 +32,15 @@ func NewBaselineCmd(o *BaselineOptions) *cobra.Command { &o.Yes, "yes", "y", false, "If true, automatically answer 'yes' to all the questions") - cmd.Flags().StringSliceVarP(&o.Files, "file", "f", []string{defaultConfigName}, "Set configuration file") - cmd.Flags().StringVar(&o.LockFile, "lock-file", defaultLockName, "Set lock file") - cmd.Flags().StringVar(&o.Chdir, "chdir", "", "Set current directory for process") - cmd.Flags().BoolVar(&o.PreferSHA, "prefer-sha", false, "Prefer sha instead of tags") + cmd.Flags().StringSliceVarP( + &o.Files, "file", "f", []string{defaultConfigName}, + "Set configuration file") + cmd.Flags().StringVar( + &o.LockFile, "lock-file", defaultLockName, "Set lock file") + cmd.Flags().StringVar( + &o.Chdir, "chdir", "", "Set current directory for process") + cmd.Flags().BoolVar( + &o.PreferSHA, "prefer-sha", false, "Prefer sha instead of tags") cmd.Flags().BoolVar(&o.DryRun, "dry-run", false, "List what would be done") return &cmd @@ -53,7 +61,7 @@ type BaselineOptions struct { } func (o *BaselineOptions) Run() error { - if len(o.Chdir) > 0 { + if len(o.Chdir) > 0 { //nolint:revive err := os.Chdir(o.Chdir) if err != nil { return fmt.Errorf("Running chdir: %s", err) @@ -91,19 +99,23 @@ func (o *BaselineOptions) Run() error { newRefs := make(map[string]string) for _, status := range statusMap { - if !status.MatchTarget() || !o.PreferSHA && !status.MatchTargetTag() && len(status.Ref.Tags) != 0 { + if !status.MatchTarget() || + !o.PreferSHA && + !status.MatchTargetTag() && + len(status.Ref.Tags) != 0 { //nolint:revive var newRef string - if o.PreferSHA || len(status.Ref.Tags) == 0 { + if o.PreferSHA || len(status.Ref.Tags) == 0 { //nolint:revive newRef = status.Ref.SHA } else { - newRef = status.Ref.Tags[0] + newRef = status.Ref.Tags[0] //nolint:revive } newRefs[status.Path()] = newRef } } - if len(newRefs) == 0 { - o.ui.PrintLinef("All references already match current state, no update needed") + if len(newRefs) == 0 { //nolint:revive + o.ui.PrintLinef( + "All references already match current state, no update needed") return nil } @@ -114,7 +126,9 @@ func (o *BaselineOptions) Run() error { if newRef != "" { newRef = " -> " + newRef } - o.ui.PrintLinef("%s/%s: %s%s", status.DirectoryPath, status.ContentPath, status.TargetRef, newRef) + o.ui.PrintLinef( + "%s/%s: %s%s", + status.DirectoryPath, status.ContentPath, status.TargetRef, newRef) } if !o.DryRun { @@ -152,7 +166,7 @@ func saveFile(fname string, doc *yaml.Node) error { } enc := yaml.NewEncoder(f) - enc.SetIndent(2) + enc.SetIndent(2) //nolint:revive if err := enc.Encode(doc); err != nil { _ = f.Close() @@ -172,7 +186,7 @@ func updateRefs(fname string, newRefs map[string]string) error { panic("expects the root node") } - top := doc.Content[0] + top := doc.Content[0] //nolint:revive if top.Kind != yaml.MappingNode { panic("top content must be a mapping") } @@ -193,14 +207,16 @@ func updateRefs(fname string, newRefs map[string]string) error { if hg := getMappingNodeChild(content, "hg"); hg != nil { ref := getMappingNodeChild(hg, "ref") if ref == nil { - return fmt.Errorf("could not find 'ref' for '%s'", fullPath) + return fmt.Errorf( + "could not find 'ref' for '%s'", fullPath) } ref.Value = newRef } if git := getMappingNodeChild(content, "git"); git != nil { ref := getMappingNodeChild(git, "ref") if ref == nil { - return fmt.Errorf("could not find 'ref' for '%s'", fullPath) + return fmt.Errorf( + "could not find 'ref' for '%s'", fullPath) } ref.Value = newRef } @@ -212,9 +228,9 @@ func updateRefs(fname string, newRefs map[string]string) error { } func getMappingNodeChild(node *yaml.Node, name string) *yaml.Node { - for i := 0; i < len(node.Content); i += 2 { + for i := 0; i < len(node.Content); i += 2 { //nolint:revive if node.Content[i].Value == name { - return node.Content[i+1] + return node.Content[i+1] //nolint:revive } } diff --git a/pkg/vendir/cmd/status.go b/pkg/vendir/cmd/status.go index c6f913f9..8f0e27ce 100644 --- a/pkg/vendir/cmd/status.go +++ b/pkg/vendir/cmd/status.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + package cmd import ( @@ -23,7 +26,7 @@ type StatusOptions struct { ExitCode bool } -func NewStatusOptions(ui ui.UI) *StatusOptions { +func NewStatusOptions(ui ui.UI) *StatusOptions { //nolint:revive return &StatusOptions{ui: ui} } @@ -34,16 +37,23 @@ func NewStatusCmd(o *StatusOptions) *cobra.Command { RunE: func(_ *cobra.Command, _ []string) error { return o.Run() }, } - cmd.Flags().StringSliceVarP(&o.Files, "file", "f", []string{defaultConfigName}, "Set configuration file") - cmd.Flags().StringVar(&o.LockFile, "lock-file", defaultLockName, "Set lock file") - cmd.Flags().StringVar(&o.Chdir, "chdir", "", "Set current directory for process") - cmd.Flags().BoolVar(&o.ExitCode, "exit-code", false, "Set to 'true', it exits with a non-0 code if any subproject is not clean") + cmd.Flags().StringSliceVarP( + &o.Files, "file", "f", + []string{defaultConfigName}, "Set configuration file") + cmd.Flags().StringVar( + &o.LockFile, "lock-file", defaultLockName, "Set lock file") + cmd.Flags().StringVar( + &o.Chdir, "chdir", "", "Set current directory for process") + cmd.Flags().BoolVar( + &o.ExitCode, "exit-code", false, + "Set to 'true', it exits with a non-0 code if any "+ + "subproject is not clean") return cmd } func (o *StatusOptions) Run() error { - if len(o.Chdir) > 0 { + if len(o.Chdir) > 0 { //nolint:revive err := os.Chdir(o.Chdir) if err != nil { return fmt.Errorf("Running chdir: %s", err) @@ -88,7 +98,7 @@ func fullStatus( conf ctlconf.Config, syncOpts ctldir.SyncOpts, existingLockConfig ctlconf.LockConfig, - ui ui.UI, + ui ui.UI, //nolint:revive ) (ctlstatus.StatusList, error) { status := ctlstatus.StatusList{} for _, dirConf := range conf.Directories { @@ -97,7 +107,8 @@ func fullStatus( dirStatus, err := directory.Status(syncOpts) if err != nil { - return nil, fmt.Errorf("Reading directory '%s': %s", dirConf.Path, err) + return nil, fmt.Errorf( + "Reading directory '%s': %s", dirConf.Path, err) } status = append(status, dirStatus...) diff --git a/pkg/vendir/cmd/sync.go b/pkg/vendir/cmd/sync.go index 354a6df0..646cac0b 100644 --- a/pkg/vendir/cmd/sync.go +++ b/pkg/vendir/cmd/sync.go @@ -58,7 +58,8 @@ func NewSyncCmd(o *SyncOptions) *cobra.Command { cmd.Flags().StringVar(&o.Chdir, "chdir", "", "Set current directory for process") cmd.Flags().BoolVar(&o.AllowAllSymlinkDestinations, "dangerous-allow-all-symlink-destinations", false, "Symlinks to all destinations are allowed") - cmd.Flags().BoolVar(&o.Safe, "safe", false, "sync only if local DVCS clones are clean") + cmd.Flags().BoolVar( + &o.Safe, "safe", false, "sync only if local DVCS clones are clean") return cmd } diff --git a/pkg/vendir/directory/status.go b/pkg/vendir/directory/status.go index 124f9d1d..f2535ac0 100644 --- a/pkg/vendir/directory/status.go +++ b/pkg/vendir/directory/status.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + package directory import ( @@ -12,12 +15,14 @@ func (d *Directory) Status(syncOpts SyncOpts) (ctlstatus.StatusList, error) { var res ctlstatus.StatusList for _, contents := range d.opts.Contents { - path := path.Join(d.opts.Path, contents.Path) + contentPath := path.Join(d.opts.Path, contents.Path) switch { case contents.Git != nil: - gitSync := ctlgit.NewSync(*contents.Git, NewInfoLog(d.ui), syncOpts.RefFetcher, syncOpts.Cache) + gitSync := ctlgit.NewSync( + *contents.Git, NewInfoLog(d.ui), + syncOpts.RefFetcher, syncOpts.Cache) - gitStatus, err := gitSync.Status(path) + gitStatus, err := gitSync.Status(contentPath) if err != nil { return nil, err } @@ -29,9 +34,10 @@ func (d *Directory) Status(syncOpts SyncOpts) (ctlstatus.StatusList, error) { } case contents.Hg != nil: hgSync := ctlhg.NewSync( - *contents.Hg, NewInfoLog(d.ui), syncOpts.RefFetcher, syncOpts.Cache) + *contents.Hg, NewInfoLog(d.ui), + syncOpts.RefFetcher, syncOpts.Cache) - hgStatus, err := hgSync.Status(path) + hgStatus, err := hgSync.Status(contentPath) if err != nil { return nil, err } diff --git a/pkg/vendir/fetch/git/status.go b/pkg/vendir/fetch/git/status.go index d477a1b0..7999d9bd 100644 --- a/pkg/vendir/fetch/git/status.go +++ b/pkg/vendir/fetch/git/status.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + package git import ( @@ -24,13 +27,15 @@ func (d Sync) Status(target string) (*ctlstatus.Status, error) { TargetRef: d.opts.Ref, } - out, _, err := git.cmdRunner.Run([]string{"rev-parse", "HEAD"}, []string{}, target) + out, _, err := git.cmdRunner.Run( + []string{"rev-parse", "HEAD"}, []string{}, target) if err != nil { return nil, err } status.Ref.SHA = strings.TrimSpace(out) - out, _, err = git.cmdRunner.Run([]string{"tag", "--contains"}, []string{}, target) + out, _, err = git.cmdRunner.Run( + []string{"tag", "--contains"}, []string{}, target) if err != nil { return nil, err } @@ -38,7 +43,8 @@ func (d Sync) Status(target string) (*ctlstatus.Status, error) { status.Ref.Tags = strings.Split(strings.TrimSpace(out), "\n") } - out, _, err = git.cmdRunner.Run([]string{"branch", "--contains"}, []string{}, target) + out, _, err = git.cmdRunner.Run( + []string{"branch", "--contains"}, []string{}, target) if err != nil { return nil, err } diff --git a/pkg/vendir/fetch/hg/status.go b/pkg/vendir/fetch/hg/status.go index 876dec38..1a71857c 100644 --- a/pkg/vendir/fetch/hg/status.go +++ b/pkg/vendir/fetch/hg/status.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Carvel Authors. +// SPDX-License-Identifier: Apache-2.0 + package hg import ( @@ -12,6 +15,8 @@ import ( ctlstatus "carvel.dev/vendir/pkg/vendir/status" ) +const emptyString = "" + func (d Sync) Status(target string) (*ctlstatus.Status, error) { _, err := os.Stat(path.Join(target, ".hg")) if err != nil { @@ -37,11 +42,11 @@ func (d Sync) Status(target string) (*ctlstatus.Status, error) { } splitted := strings.Split(out, "\n") - sha := splitted[0] - tags := splitted[1] - branch := splitted[2] - topic := splitted[3] - bookmarks := splitted[4] + sha := splitted[0] //nolint:revive + tags := splitted[1] //nolint:revive + branch := splitted[2] //nolint:revive + topic := splitted[3] //nolint:revive + bookmarks := splitted[4] //nolint:revive status := ctlstatus.Status{ TargetRef: d.opts.Ref, @@ -49,18 +54,18 @@ func (d Sync) Status(target string) (*ctlstatus.Status, error) { SHA: sha, }, } - if tags != "" { + if tags != emptyString { status.Ref.Tags = strings.Split(tags, " ") status.Ref.Tags = slices.DeleteFunc( status.Ref.Tags, func(t string) bool { return t == "tip" }) } - if branch != "" { + if branch != emptyString { status.Ref.Others = append(status.Ref.Others, branch) } - if topic != "" { + if topic != emptyString { status.Ref.Others = append(status.Ref.Others, topic) } - if bookmarks != "" { + if bookmarks != emptyString { status.Ref.Others = append(status.Ref.Others, bookmarks) } @@ -68,7 +73,7 @@ func (d Sync) Status(target string) (*ctlstatus.Status, error) { if err != nil { return nil, err } - if out != "" { + if out != emptyString { status.UncommitedChanges = strings.Split(strings.TrimSpace(out), "\n") } @@ -80,7 +85,7 @@ func (d Sync) Status(target string) (*ctlstatus.Status, error) { } } - if out != "" { + if out != emptyString { status.LocalCsets = strings.Split(strings.TrimSpace(out), "\n") } From b7aab272c105da737f2787b13957720e55e0abe1 Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Tue, 30 Dec 2025 13:48:07 +0100 Subject: [PATCH 07/10] Improve ui of 'baseline' & 'status' Signed-off-by: Christophe de Vienne --- pkg/vendir/cmd/baseline.go | 16 ++++++---- pkg/vendir/cmd/status.go | 3 +- pkg/vendir/cmd/sync.go | 3 +- pkg/vendir/status/status.go | 58 +++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/pkg/vendir/cmd/baseline.go b/pkg/vendir/cmd/baseline.go index 2f9d7259..be7081f2 100644 --- a/pkg/vendir/cmd/baseline.go +++ b/pkg/vendir/cmd/baseline.go @@ -120,21 +120,25 @@ func (o *BaselineOptions) Run() error { return nil } - o.ui.PrintLinef("New baseline:") + block := "New baseline:\n" for _, status := range statusMap { newRef := newRefs[status.Path()] if newRef != "" { newRef = " -> " + newRef } - o.ui.PrintLinef( - "%s/%s: %s%s", + block += fmt.Sprintf( + "- %s/%s: %s%s\n", status.DirectoryPath, status.ContentPath, status.TargetRef, newRef) } + o.ui.PrintBlock([]byte(block)) if !o.DryRun { - for _, fname := range o.Files { - if err := updateRefs(fname, newRefs); err != nil { - return err + if o.Yes || o.ui.AskForConfirmation() == nil { + for _, fname := range o.Files { + if err := updateRefs(fname, newRefs); err != nil { + return err + } + o.ui.PrintLinef("Updated '%s'", fname) } } } diff --git a/pkg/vendir/cmd/status.go b/pkg/vendir/cmd/status.go index 8f0e27ce..a7af0b3a 100644 --- a/pkg/vendir/cmd/status.go +++ b/pkg/vendir/cmd/status.go @@ -89,7 +89,8 @@ func (o *StatusOptions) Run() error { } o.ui.PrintBlock([]byte("---------------\n\n")) - o.ui.PrintBlock([]byte(status.String())) + + o.ui.PrintTable(status.Table()) return nil } diff --git a/pkg/vendir/cmd/sync.go b/pkg/vendir/cmd/sync.go index 646cac0b..69b157df 100644 --- a/pkg/vendir/cmd/sync.go +++ b/pkg/vendir/cmd/sync.go @@ -149,7 +149,8 @@ func (o *SyncOptions) Run() error { } if !status.IsSafe() { - return fmt.Errorf("--safe mode forbids a sync: %s", status.String()) + o.ui.PrintTable(status.Table()) + return fmt.Errorf("--safe mode forbids a sync") } } diff --git a/pkg/vendir/status/status.go b/pkg/vendir/status/status.go index fac32e88..2be8f44a 100644 --- a/pkg/vendir/status/status.go +++ b/pkg/vendir/status/status.go @@ -8,6 +8,8 @@ import ( "path" "slices" "strings" + + "github.com/cppforlife/go-cli-ui/ui/table" ) type CompleteReference struct { @@ -64,6 +66,34 @@ func (s Status) String() string { return strings.Join(messages, ", ") } +func (s Status) toRow() []table.Value { + row := make([]table.Value, 5) + + row[0] = table.NewValueString(s.DirectoryPath + "/" + s.ContentPath) + + if len(s.UncommitedChanges) != 0 { + row[1] = table.NewValueInt(len(s.UncommitedChanges)) + } + + if len(s.LocalCsets) != 0 { + row[2] = table.NewValueInt(len(s.LocalCsets)) + } + + if s.MatchTarget() { + row[3] = table.NewValueFmt(table.NewValueString("up-to-date"), false) + } else { + row[3] = table.NewValueFmt(table.NewValueString("mismatch"), true) + } + + if s.IsSafe() { + row[4] = table.NewValueFmt(table.NewValueString("safe"), false) + } else { + row[4] = table.NewValueFmt(table.NewValueString("not safe"), true) + } + + return row +} + type StatusList []*Status func (sm StatusList) String() string { @@ -81,6 +111,34 @@ func (sm StatusList) String() string { return s } +func (sm StatusList) Table() table.Table { + t := table.Table{ + Title: "Detailled status", + Header: []table.Header{{ + Key: "path", + Title: "path", + }, { + Key: "changes", + Title: "changes", + }, { + Key: "commits", + Title: "commits", + }, { + Key: "match", + Title: "match", + }, { + Key: "safe", + Title: "safe", + }}, + } + + for _, status := range sm { + t.Rows = append(t.Rows, status.toRow()) + } + + return t +} + func (sm StatusList) IsSafe() bool { isSafe := true for _, status := range sm { From f33d523f7f7ea9f5e3e00ca97d4a011b17aedc2b Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Tue, 30 Dec 2025 16:23:01 +0100 Subject: [PATCH 08/10] --safe default value can now be changed with the VENDIR_SYNC_SAFE environment variable Signed-off-by: Christophe de Vienne --- pkg/vendir/cmd/sync.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/vendir/cmd/sync.go b/pkg/vendir/cmd/sync.go index 69b157df..8dc4d1c1 100644 --- a/pkg/vendir/cmd/sync.go +++ b/pkg/vendir/cmd/sync.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strings" ctlconf "carvel.dev/vendir/pkg/vendir/config" @@ -22,6 +23,12 @@ const ( defaultLockName = "vendir.lock.yml" ) +var ( + defaultSafeFlagValue = slices.Contains([]string{ + "y", "yes", "Y", "YES", "1", "t", "True", "true", "TRUE", + }, os.Getenv("VENDIR_SYNC_SAFE")) +) + type SyncOptions struct { ui ui.UI @@ -59,7 +66,7 @@ func NewSyncCmd(o *SyncOptions) *cobra.Command { cmd.Flags().BoolVar(&o.AllowAllSymlinkDestinations, "dangerous-allow-all-symlink-destinations", false, "Symlinks to all destinations are allowed") cmd.Flags().BoolVar( - &o.Safe, "safe", false, "sync only if local DVCS clones are clean") + &o.Safe, "safe", defaultSafeFlagValue, "sync only if local DVCS clones are clean") return cmd } From fcea598194e82fbd8d9e0ed6a723d9d77c3b2891 Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Tue, 30 Dec 2025 16:33:53 +0100 Subject: [PATCH 09/10] typo Signed-off-by: Christophe de Vienne --- pkg/vendir/status/status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/vendir/status/status.go b/pkg/vendir/status/status.go index 2be8f44a..cc438407 100644 --- a/pkg/vendir/status/status.go +++ b/pkg/vendir/status/status.go @@ -99,7 +99,7 @@ type StatusList []*Status func (sm StatusList) String() string { var s string - s += "Detailled status:\n" + s += "Detailed status:\n" for _, status := range sm { s += "- " + status.DirectoryPath + "/" + status.ContentPath + ": " + status.String() + "\n" } From 22f54f5be3f6eba446134c1d25e8c4351c4ec693 Mon Sep 17 00:00:00 2001 From: Christophe de Vienne Date: Mon, 5 Jan 2026 09:42:27 +0100 Subject: [PATCH 10/10] baseline: avoid unwanted ref updates When the ref is a branch and still matches, it should not be replaced with sha. Signed-off-by: Christophe de Vienne --- pkg/vendir/cmd/baseline.go | 5 ++--- pkg/vendir/status/status.go | 8 ++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/vendir/cmd/baseline.go b/pkg/vendir/cmd/baseline.go index be7081f2..8af6150d 100644 --- a/pkg/vendir/cmd/baseline.go +++ b/pkg/vendir/cmd/baseline.go @@ -100,9 +100,8 @@ func (o *BaselineOptions) Run() error { for _, status := range statusMap { if !status.MatchTarget() || - !o.PreferSHA && - !status.MatchTargetTag() && - len(status.Ref.Tags) != 0 { //nolint:revive + o.PreferSHA && !status.MatchTargetSHA() || + !o.PreferSHA && !status.MatchTargetTag() && len(status.Ref.Tags) != 0 { //nolint:revive var newRef string if o.PreferSHA || len(status.Ref.Tags) == 0 { //nolint:revive newRef = status.Ref.SHA diff --git a/pkg/vendir/status/status.go b/pkg/vendir/status/status.go index cc438407..08320bbc 100644 --- a/pkg/vendir/status/status.go +++ b/pkg/vendir/status/status.go @@ -36,8 +36,8 @@ func (s Status) IsSafe() bool { } func (s Status) MatchTarget() bool { - return strings.HasPrefix(s.Ref.SHA, s.TargetRef) || - slices.Contains(s.Ref.Tags, s.TargetRef) || + return s.MatchTargetSHA() || + s.MatchTargetTag() || slices.Contains(s.Ref.Others, s.TargetRef) } @@ -45,6 +45,10 @@ func (s Status) MatchTargetTag() bool { return slices.Contains(s.Ref.Tags, s.TargetRef) } +func (s Status) MatchTargetSHA() bool { + return strings.HasPrefix(s.Ref.SHA, s.TargetRef) +} + func (s Status) String() string { messages := make([]string, 0, 3)