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