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