16c8e08b7f1bb83ac267be438251fe523aa37dc6

Author
Kieran Klukas <l41cge3m@duck.com>
Committer
GitHub <noreply@github.com>
Date

Message

Merge pull request #885 from charmbracelet/mirror-command

feat(commands): add mirror command

Diff

  1diff --git a/README.md b/README.md
  2index 0c5414b1f8660931245e71a66ff78492ab51ffde..cfe4043523bc139b5c5f0a1f22356c6d6d10689c 100644
  3--- a/README.md
  4+++ b/README.md
  5@@ -501,8 +501,8 @@ Available Commands:
  6   hide         Hide or unhide a repository
  7   import       Import a new repository from remote
  8   info         Get information about a repository
  9-  is-mirror    Whether a repository is a mirror
 10   list         List repositories
 11+  mirror       Set or get a repository mirror property
 12   private      Set or get a repository private property
 13   project-name Set or get the project name for a repository
 14   rename       Rename an existing repository
 15diff --git a/pkg/backend/repo.go b/pkg/backend/repo.go
 16index f9b70bbc3a8f07be4a0e440bd103042fa162b550..d92ac4d094114bcd54f5e84d87b63b483f5426d7 100644
 17--- a/pkg/backend/repo.go
 18+++ b/pkg/backend/repo.go
 19@@ -553,6 +553,69 @@ func (d *Backend) SetHidden(ctx context.Context, name string, hidden bool) error
 20 	}))
 21 }
 22 
 23+// SetMirror sets the mirror flag of a repository.
 24+// Note: enabling mirror mode requires the repository to have been imported
 25+// with a remote URL. Use ImportRepository to create a new mirror.
 26+func (d *Backend) SetMirror(ctx context.Context, name string, mirror bool) error {
 27+	name = utils.SanitizeRepo(name)
 28+	rp := filepath.Join(d.repoPath(name))
 29+
 30+	// Delete cache
 31+	d.cache.Delete(name)
 32+
 33+	return db.WrapError(d.db.TransactionContext(ctx, func(tx *db.Tx) error {
 34+		// Update git config
 35+		r, err := git.Open(rp)
 36+		if err != nil {
 37+			return err
 38+		}
 39+
 40+		rcfg, err := r.Config()
 41+		if err != nil {
 42+			return err
 43+		}
 44+
 45+		// Update mirror option for all remotes
 46+		remoteSection := rcfg.Section("remote")
 47+		hasRemote := false
 48+		for _, sub := range remoteSection.Subsections {
 49+			// Check if this remote has a URL
 50+			for _, opt := range sub.Options {
 51+				if opt.Key == "url" && opt.Value != "" {
 52+					hasRemote = true
 53+					found := false
 54+					for i, opt := range sub.Options {
 55+						if opt.Key == "mirror" {
 56+							found = true
 57+							if mirror {
 58+								sub.Options[i].Value = "true"
 59+							} else {
 60+								sub.Options = append(sub.Options[:i], sub.Options[i+1:]...)
 61+							}
 62+							break
 63+						}
 64+					}
 65+					if !found && mirror {
 66+						sub.SetOption("mirror", "true")
 67+					}
 68+					break
 69+				}
 70+			}
 71+		}
 72+
 73+		if mirror && !hasRemote {
 74+			return errors.New("cannot enable mirror mode: repository has no remote URL configured")
 75+		}
 76+
 77+		if err := r.SetConfig(rcfg); err != nil {
 78+			d.logger.Error("failed to set repository config", "err", err, "path", rp)
 79+			return err
 80+		}
 81+
 82+		return d.store.SetRepoIsMirrorByName(ctx, tx, name, mirror)
 83+	}))
 84+}
 85+
 86 // SetDescription sets the description of a repository.
 87 //
 88 // It implements backend.Backend.
 89diff --git a/pkg/ssh/cmd/mirror.go b/pkg/ssh/cmd/mirror.go
 90index 4ef05a904fd45813a961c999402ba9dcfa6618db..c3e7131262a0aa3589b41dbc5f723782c8df7f3c 100644
 91--- a/pkg/ssh/cmd/mirror.go
 92+++ b/pkg/ssh/cmd/mirror.go
 93@@ -7,21 +7,34 @@ import (
 94 
 95 func mirrorCommand() *cobra.Command {
 96 	cmd := &cobra.Command{
 97-		Use:               "is-mirror REPOSITORY",
 98-		Short:             "Whether a repository is a mirror",
 99-		Args:              cobra.ExactArgs(1),
100+		Use:               "mirror REPOSITORY [true|false]",
101+		Short:             "Set or get a repository mirror property",
102+		Args:              cobra.RangeArgs(1, 2),
103 		PersistentPreRunE: checkIfReadable,
104 		RunE: func(cmd *cobra.Command, args []string) error {
105 			ctx := cmd.Context()
106 			be := backend.FromContext(ctx)
107 			rn := args[0]
108-			rr, err := be.Repository(ctx, rn)
109-			if err != nil {
110-				return err
111+
112+			switch len(args) {
113+			case 1:
114+				isMirror, err := be.IsMirror(ctx, rn)
115+				if err != nil {
116+					return err
117+				}
118+
119+				cmd.Println(isMirror)
120+			case 2:
121+				if err := checkIfCollab(cmd, args); err != nil {
122+					return err
123+				}
124+
125+				isMirror := args[1] == "true"
126+				if err := be.SetMirror(ctx, rn, isMirror); err != nil {
127+					return err
128+				}
129 			}
130 
131-			isMirror := rr.IsMirror()
132-			cmd.Println(isMirror)
133 			return nil
134 		},
135 	}
136diff --git a/pkg/store/database/repo.go b/pkg/store/database/repo.go
137index 32b91291e93da308d767f529872186ef481bbec4..7307b6c454f50226ee814f8057cf23b8cf1bcc5a 100644
138--- a/pkg/store/database/repo.go
139+++ b/pkg/store/database/repo.go
140@@ -92,6 +92,14 @@ func (*repoStore) GetRepoIsMirrorByName(ctx context.Context, tx db.Handler, name
141 	return isMirror, db.WrapError(err)
142 }
143 
144+// SetRepoIsMirrorByName implements store.RepositoryStore.
145+func (*repoStore) SetRepoIsMirrorByName(ctx context.Context, tx db.Handler, name string, isMirror bool) error {
146+	name = utils.SanitizeRepo(name)
147+	query := tx.Rebind("UPDATE repos SET mirror = ? WHERE name = ?;")
148+	_, err := tx.ExecContext(ctx, query, isMirror, name)
149+	return db.WrapError(err)
150+}
151+
152 // GetRepoIsPrivateByName implements store.RepositoryStore.
153 func (*repoStore) GetRepoIsPrivateByName(ctx context.Context, tx db.Handler, name string) (bool, error) {
154 	var isPrivate bool
155diff --git a/pkg/store/repo.go b/pkg/store/repo.go
156index 768679251ac790bae008a97259257c9444c78b01..3d229058d10dbb1b43170c6c08b25091ba25a14a 100644
157--- a/pkg/store/repo.go
158+++ b/pkg/store/repo.go
159@@ -25,4 +25,5 @@ type RepositoryStore interface {
160 	GetRepoIsHiddenByName(ctx context.Context, h db.Handler, name string) (bool, error)
161 	SetRepoIsHiddenByName(ctx context.Context, h db.Handler, name string, isHidden bool) error
162 	GetRepoIsMirrorByName(ctx context.Context, h db.Handler, name string) (bool, error)
163+	SetRepoIsMirrorByName(ctx context.Context, h db.Handler, name string, isMirror bool) error
164 }
165diff --git a/testscript/testdata/mirror.txtar b/testscript/testdata/mirror.txtar
166index fded61ff9d0488d4d4de820a0fc92120e7764591..5c1bf6141d94df5e00b0b94ee993fe5a2792372b 100644
167--- a/testscript/testdata/mirror.txtar
168+++ b/testscript/testdata/mirror.txtar
169@@ -22,9 +22,28 @@ cmp stdout info1.txt
170 soft repo list
171 stdout charmbracelet/wizard-tutorial
172 
173-# is-mirror?
174-soft repo is-mirror charmbracelet/wizard-tutorial
175+# create a non-mirror repo
176+soft repo create test-normal
177+soft repo mirror test-normal
178+stdout false
179+# try to enable mirror on repo without remote - should fail
180+! soft repo mirror test-normal true
181+
182+# disable mirror
183+soft repo mirror charmbracelet/wizard-tutorial false
184+soft repo mirror charmbracelet/wizard-tutorial
185+stdout false
186+# verify git config no longer has mirror
187+readfile $DATA_PATH/repos/charmbracelet/wizard-tutorial.git/config
188+! stdout 'mirror = true'
189+
190+# re-enable mirror
191+soft repo mirror charmbracelet/wizard-tutorial true
192+soft repo mirror charmbracelet/wizard-tutorial
193 stdout true
194+# verify git config has mirror again
195+readfile $DATA_PATH/repos/charmbracelet/wizard-tutorial.git/config
196+stdout 'mirror = true'
197 
198 # set project name
199 soft repo project-name charmbracelet/wizard-tutorial wizard-tutorial