From fdd9fd22d14218115ea5939a7290047d65bb6e2a Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Fri, 8 May 2026 11:34:58 -0400 Subject: [PATCH] feat(commands): add mirror command --- README.md | 2 +- pkg/backend/repo.go | 63 ++++++++++++++++++++++++++++++++ pkg/ssh/cmd/mirror.go | 29 +++++++++++---- pkg/store/database/repo.go | 8 ++++ pkg/store/repo.go | 1 + testscript/testdata/mirror.txtar | 23 +++++++++++- 6 files changed, 115 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0c5414b1f8660931245e71a66ff78492ab51ffde..cfe4043523bc139b5c5f0a1f22356c6d6d10689c 100644 --- a/README.md +++ b/README.md @@ -501,8 +501,8 @@ Available Commands: hide Hide or unhide a repository import Import a new repository from remote info Get information about a repository - is-mirror Whether a repository is a mirror list List repositories + mirror Set or get a repository mirror property private Set or get a repository private property project-name Set or get the project name for a repository rename Rename an existing repository diff --git a/pkg/backend/repo.go b/pkg/backend/repo.go index f9b70bbc3a8f07be4a0e440bd103042fa162b550..d92ac4d094114bcd54f5e84d87b63b483f5426d7 100644 --- a/pkg/backend/repo.go +++ b/pkg/backend/repo.go @@ -553,6 +553,69 @@ func (d *Backend) SetHidden(ctx context.Context, name string, hidden bool) error })) } +// SetMirror sets the mirror flag of a repository. +// Note: enabling mirror mode requires the repository to have been imported +// with a remote URL. Use ImportRepository to create a new mirror. +func (d *Backend) SetMirror(ctx context.Context, name string, mirror bool) error { + name = utils.SanitizeRepo(name) + rp := filepath.Join(d.repoPath(name)) + + // Delete cache + d.cache.Delete(name) + + return db.WrapError(d.db.TransactionContext(ctx, func(tx *db.Tx) error { + // Update git config + r, err := git.Open(rp) + if err != nil { + return err + } + + rcfg, err := r.Config() + if err != nil { + return err + } + + // Update mirror option for all remotes + remoteSection := rcfg.Section("remote") + hasRemote := false + for _, sub := range remoteSection.Subsections { + // Check if this remote has a URL + for _, opt := range sub.Options { + if opt.Key == "url" && opt.Value != "" { + hasRemote = true + found := false + for i, opt := range sub.Options { + if opt.Key == "mirror" { + found = true + if mirror { + sub.Options[i].Value = "true" + } else { + sub.Options = append(sub.Options[:i], sub.Options[i+1:]...) + } + break + } + } + if !found && mirror { + sub.SetOption("mirror", "true") + } + break + } + } + } + + if mirror && !hasRemote { + return errors.New("cannot enable mirror mode: repository has no remote URL configured") + } + + if err := r.SetConfig(rcfg); err != nil { + d.logger.Error("failed to set repository config", "err", err, "path", rp) + return err + } + + return d.store.SetRepoIsMirrorByName(ctx, tx, name, mirror) + })) +} + // SetDescription sets the description of a repository. // // It implements backend.Backend. diff --git a/pkg/ssh/cmd/mirror.go b/pkg/ssh/cmd/mirror.go index 4ef05a904fd45813a961c999402ba9dcfa6618db..c3e7131262a0aa3589b41dbc5f723782c8df7f3c 100644 --- a/pkg/ssh/cmd/mirror.go +++ b/pkg/ssh/cmd/mirror.go @@ -7,21 +7,34 @@ import ( func mirrorCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "is-mirror REPOSITORY", - Short: "Whether a repository is a mirror", - Args: cobra.ExactArgs(1), + Use: "mirror REPOSITORY [true|false]", + Short: "Set or get a repository mirror property", + Args: cobra.RangeArgs(1, 2), PersistentPreRunE: checkIfReadable, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() be := backend.FromContext(ctx) rn := args[0] - rr, err := be.Repository(ctx, rn) - if err != nil { - return err + + switch len(args) { + case 1: + isMirror, err := be.IsMirror(ctx, rn) + if err != nil { + return err + } + + cmd.Println(isMirror) + case 2: + if err := checkIfCollab(cmd, args); err != nil { + return err + } + + isMirror := args[1] == "true" + if err := be.SetMirror(ctx, rn, isMirror); err != nil { + return err + } } - isMirror := rr.IsMirror() - cmd.Println(isMirror) return nil }, } diff --git a/pkg/store/database/repo.go b/pkg/store/database/repo.go index 32b91291e93da308d767f529872186ef481bbec4..7307b6c454f50226ee814f8057cf23b8cf1bcc5a 100644 --- a/pkg/store/database/repo.go +++ b/pkg/store/database/repo.go @@ -92,6 +92,14 @@ func (*repoStore) GetRepoIsMirrorByName(ctx context.Context, tx db.Handler, name return isMirror, db.WrapError(err) } +// SetRepoIsMirrorByName implements store.RepositoryStore. +func (*repoStore) SetRepoIsMirrorByName(ctx context.Context, tx db.Handler, name string, isMirror bool) error { + name = utils.SanitizeRepo(name) + query := tx.Rebind("UPDATE repos SET mirror = ? WHERE name = ?;") + _, err := tx.ExecContext(ctx, query, isMirror, name) + return db.WrapError(err) +} + // GetRepoIsPrivateByName implements store.RepositoryStore. func (*repoStore) GetRepoIsPrivateByName(ctx context.Context, tx db.Handler, name string) (bool, error) { var isPrivate bool diff --git a/pkg/store/repo.go b/pkg/store/repo.go index 768679251ac790bae008a97259257c9444c78b01..3d229058d10dbb1b43170c6c08b25091ba25a14a 100644 --- a/pkg/store/repo.go +++ b/pkg/store/repo.go @@ -25,4 +25,5 @@ type RepositoryStore interface { GetRepoIsHiddenByName(ctx context.Context, h db.Handler, name string) (bool, error) SetRepoIsHiddenByName(ctx context.Context, h db.Handler, name string, isHidden bool) error GetRepoIsMirrorByName(ctx context.Context, h db.Handler, name string) (bool, error) + SetRepoIsMirrorByName(ctx context.Context, h db.Handler, name string, isMirror bool) error } diff --git a/testscript/testdata/mirror.txtar b/testscript/testdata/mirror.txtar index fded61ff9d0488d4d4de820a0fc92120e7764591..5c1bf6141d94df5e00b0b94ee993fe5a2792372b 100644 --- a/testscript/testdata/mirror.txtar +++ b/testscript/testdata/mirror.txtar @@ -22,9 +22,28 @@ cmp stdout info1.txt soft repo list stdout charmbracelet/wizard-tutorial -# is-mirror? -soft repo is-mirror charmbracelet/wizard-tutorial +# create a non-mirror repo +soft repo create test-normal +soft repo mirror test-normal +stdout false +# try to enable mirror on repo without remote - should fail +! soft repo mirror test-normal true + +# disable mirror +soft repo mirror charmbracelet/wizard-tutorial false +soft repo mirror charmbracelet/wizard-tutorial +stdout false +# verify git config no longer has mirror +readfile $DATA_PATH/repos/charmbracelet/wizard-tutorial.git/config +! stdout 'mirror = true' + +# re-enable mirror +soft repo mirror charmbracelet/wizard-tutorial true +soft repo mirror charmbracelet/wizard-tutorial stdout true +# verify git config has mirror again +readfile $DATA_PATH/repos/charmbracelet/wizard-tutorial.git/config +stdout 'mirror = true' # set project name soft repo project-name charmbracelet/wizard-tutorial wizard-tutorial