internal/control/mirrorcmd.go
205 lines · 6605 bytes
1package control
2
3import (
4 "bufio"
5 "errors"
6 "fmt"
7 "io"
8 "net/url"
9 "strconv"
10 "strings"
11
12 "gitbay.org/gitbay/internal/gitpin"
13 "gitbay.org/gitbay/internal/policy"
14 "gitbay.org/gitbay/internal/protocol"
15 "gitbay.org/gitbay/internal/store"
16 "gitbay.org/gitbay/internal/webhook"
17)
18
19func init() {
20 register(Command{Path: []string{"repo", "mirror", "add"},
21 NeedsRecentSignIn: true,
22 Summary: "mirror to or from a remote",
23 Usage: "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]",
24 Flags: []Flag{
25 {"--direction", "push|pull", "which way the mirror syncs", ""},
26 {"--username", "<u>", "the remote's username", ""},
27 {"--token-stdin", "", "read a credential token from stdin", ""},
28 },
29 Examples: []string{"repo mirror add krz/gitbay https://github.com/krazywarez/gitbay.git --direction push"},
30 ReadsStdin: true, Run: runMirrorAdd})
31 register(Command{Path: []string{"repo", "mirror", "list"},
32 Summary: "list mirrors with sync status",
33 Usage: "repo mirror list <owner/name>",
34 Examples: []string{"repo mirror list krz/gitbay"},
35 ReadOnly: true, Run: runMirrorList})
36 register(Command{Path: []string{"repo", "mirror", "remove"},
37 Summary: "remove a mirror",
38 Usage: "repo mirror remove <owner/name> <id>",
39 Examples: []string{"repo mirror remove krz/gitbay 3"},
40 Run: runMirrorRemove})
41 register(Command{Path: []string{"repo", "mirror", "sync"},
42 Summary: "schedule an immediate sync",
43 Usage: "repo mirror sync <owner/name>",
44 Examples: []string{"repo mirror sync krz/gitbay"},
45 Run: runMirrorSync})
46}
47
48func runMirrorAdd(c *Ctx, args []string) int {
49 f, err := c.parseArgs(args, flagSpec{Values: []string{"--direction", "--username"}, Bools: []string{"--token-stdin"}, MaxPos: 2,
50 Usage: "repo mirror add <owner/name> <url> --direction push|pull [--username <u>] [--token-stdin]"})
51 if err != nil {
52 return c.fail(protocol.ExitUsage, "%v", err)
53 }
54 path, urlArg := f.pos(0), f.pos(1)
55 direction, username, tokenStdin := f.Value("--direction"), f.Value("--username"), f.Has("--token-stdin")
56 if path == "" || urlArg == "" || (direction != "push" && direction != "pull") {
57 return c.usage()
58 }
59 // Sync refuses a numerically written host; say so now, before a
60 // resolver gets to read it.
61 if u, err := url.Parse(urlArg); err == nil {
62 if err := gitpin.CheckHost(u.Hostname()); err != nil {
63 return c.failInput(err)
64 }
65 }
66 // The worker's git process dials this URL from the server: same SSRF
67 // surface as a webhook target, same rules.
68 if err := webhook.ValidateURL(urlArg, c.Cfg.Webhooks.AllowLocal); err != nil {
69 return c.failInput(err)
70 }
71 repo, code := resolveRepo(c, path, policy.CanAdmin)
72 if code >= 0 {
73 return code
74 }
75 token := ""
76 if tokenStdin {
77 line, err := bufio.NewReader(io.LimitReader(c.Stdin, 4096)).ReadString('\n')
78 if err != nil && line == "" {
79 return c.fail(protocol.ExitUsage, "--token-stdin given but stdin held no token")
80 }
81 token = strings.TrimSpace(line)
82 }
83 id, err := c.Store.AddMirror(repo.ID, direction, urlArg, username, token)
84 if err != nil {
85 if errors.Is(err, store.ErrExists) {
86 return c.fail(protocol.ExitUsage, "that mirror already exists")
87 }
88 return c.fail(protocol.ExitFailure, "%v", err)
89 }
90 note := ""
91 if direction == "pull" {
92 note = "; local pushes are now refused — refs come from the upstream"
93 }
94 return c.emit(map[string]any{"id": id, "direction": direction, "url": urlArg}, func(w io.Writer) {
95 fmt.Fprintf(w, "mirror %d added (%s %s)%s\n", id, direction, urlArg, note)
96 })
97}
98
99func runMirrorList(c *Ctx, args []string) int {
100 if len(args) != 1 {
101 return c.usage()
102 }
103 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
104 if code >= 0 {
105 return code
106 }
107 ms, err := c.Store.ListMirrors(repo.ID)
108 if err != nil {
109 return c.fail(protocol.ExitFailure, "%v", err)
110 }
111 type out struct {
112 ID int64 `json:"id"`
113 Direction string `json:"direction"`
114 URL string `json:"url"`
115 Username string `json:"username,omitempty"`
116 Pending bool `json:"pending"`
117 LastSync string `json:"last_sync,omitempty"`
118 LastError string `json:"last_error,omitempty"`
119 }
120 var ds []out
121 for _, m := range ms {
122 // The token never leaves the server, in any encoding.
123 ds = append(ds, out{m.ID, m.Direction, m.URL, m.Username, m.Dirty, m.LastSync, m.LastError})
124 }
125 return c.emitView(ds, func(w io.Writer) {
126 tb := c.table(w, "ID", "DIRECTION", "URL", "LAST", "STATUS")
127 for _, d := range ds {
128 status := "ok"
129 if d.Pending {
130 status = "pending"
131 }
132 if d.LastError != "" {
133 status = "error: " + d.LastError
134 }
135 tb.row(cRef(fmt.Sprintf("%d", d.ID)), cText(d.Direction), cText(d.URL), cText("last "+orDash(d.LastSync)), cState(status))
136 }
137 tb.flush()
138 }, func() screen {
139 rows := make([]row, len(ds))
140 for i, d := range ds {
141 state := "ok"
142 if d.Pending {
143 state = "pending"
144 }
145 if d.LastError != "" {
146 state = "error"
147 }
148 synced := "never synced"
149 if d.LastSync != "" {
150 synced = "synced " + relAge(d.LastSync, termNow())
151 }
152 rows[i] = rowOf(cRef(strconv.FormatInt(d.ID, 10)), cGlyph(state), cFlex(d.URL), cMeta(d.Direction, synced), cMark(d.LastError, sgrRed))
153 }
154 return listScreen("Mirrors", rows,
155 action{"Mirrors", []string{"repo", "mirror", "sync", repo.Path()}},
156 action{"Mirrors", []string{"repo", "mirror", "remove", repo.Path(), "<id>"}},
157 )
158 })
159}
160
161func orDash(s string) string {
162 if s == "" {
163 return "-"
164 }
165 return s
166}
167
168func runMirrorRemove(c *Ctx, args []string) int {
169 if len(args) != 2 {
170 return c.usage()
171 }
172 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
173 if code >= 0 {
174 return code
175 }
176 id, err := strconv.ParseInt(args[1], 10, 64)
177 if err != nil {
178 return c.fail(protocol.ExitUsage, "bad mirror id %q", args[1])
179 }
180 if err := c.Store.RemoveMirror(repo.ID, id); err != nil {
181 if errors.Is(err, store.ErrNotFound) {
182 return c.fail(protocol.ExitNotFound, "no mirror %d on %s", id, repo.Path())
183 }
184 return c.fail(protocol.ExitFailure, "%v", err)
185 }
186 return c.emit(map[string]any{"removed": id}, func(w io.Writer) {
187 fmt.Fprintf(w, "removed mirror %d\n", id)
188 })
189}
190
191func runMirrorSync(c *Ctx, args []string) int {
192 if len(args) != 1 {
193 return c.usage()
194 }
195 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
196 if code >= 0 {
197 return code
198 }
199 if err := c.Store.MarkMirrorsDirty(repo.ID, ""); err != nil {
200 return c.fail(protocol.ExitFailure, "%v", err)
201 }
202 return c.emit(map[string]string{"sync": "scheduled"}, func(w io.Writer) {
203 fmt.Fprintln(w, "sync scheduled; check repo mirror list for the outcome")
204 })
205}