internal/control/deps.go
133 lines · 4056 bytes
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7
8 "gitbay.org/gitbay/internal/policy"
9 "gitbay.org/gitbay/internal/protocol"
10 "gitbay.org/gitbay/internal/store"
11)
12
13func init() {
14 // Dependency update checks. Opt-in per repository: the check tells a
15 // public registry what the repository depends on, which is the owner's
16 // disclosure to make, not the instance's.
17 register(Command{Path: []string{"repo", "deps", "enable"},
18 Summary: "check dependencies for updates",
19 Usage: "repo deps enable <owner/name>",
20 Examples: []string{"repo deps enable krz/gitbay"},
21 Run: runDepsEnable})
22 register(Command{Path: []string{"repo", "deps", "disable"},
23 Summary: "stop checking dependencies",
24 Usage: "repo deps disable <owner/name>",
25 Examples: []string{"repo deps disable krz/gitbay"},
26 Run: runDepsDisable})
27 register(Command{Path: []string{"repo", "deps", "status"},
28 Summary: "show dependency check state",
29 Usage: "repo deps status <owner/name>",
30 Examples: []string{"repo deps status krz/gitbay"},
31 ReadOnly: true, Run: runDepsStatus})
32}
33
34// DepsOut is what `repo deps status` emits: the check's state and what
35// the last run found behind. Named so the web decodes the same struct
36// the command writes.
37type DepsOut struct {
38 Enabled bool `json:"enabled"`
39 LastCheck string `json:"last_check,omitempty"`
40 LastError string `json:"last_error,omitempty"`
41 IssueNumber int64 `json:"issue_number,omitempty"`
42 Behind []DepBehind `json:"behind"`
43}
44
45// DepBehind is one dependency with a newer release than the manifest pins.
46type DepBehind struct {
47 Ecosystem string `json:"ecosystem"`
48 Name string `json:"name"`
49 Current string `json:"current"`
50 Latest string `json:"latest"`
51}
52
53func runDepsEnable(c *Ctx, args []string) int {
54 if len(args) != 1 {
55 return c.usage()
56 }
57 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
58 if code >= 0 {
59 return code
60 }
61 if err := c.Store.EnableDepCheck(repo.ID); err != nil {
62 return c.fail(protocol.ExitFailure, "%v", err)
63 }
64 return c.emit(map[string]any{"enabled": true}, func(w io.Writer) {
65 fmt.Fprintf(w, "dependency checks enabled on %s\n", repo.Path())
66 })
67}
68
69func runDepsDisable(c *Ctx, args []string) int {
70 if len(args) != 1 {
71 return c.usage()
72 }
73 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
74 if code >= 0 {
75 return code
76 }
77 if err := c.Store.DisableDepCheck(repo.ID); err != nil {
78 return c.fail(protocol.ExitFailure, "%v", err)
79 }
80 return c.emit(map[string]any{"enabled": false}, func(w io.Writer) {
81 fmt.Fprintf(w, "dependency checks disabled on %s\n", repo.Path())
82 })
83}
84
85func runDepsStatus(c *Ctx, args []string) int {
86 if len(args) != 1 {
87 return c.usage()
88 }
89 repo, code := resolveRepo(c, args[0], policy.CanRead)
90 if code >= 0 {
91 return code
92 }
93 check, err := c.Store.DepCheckFor(repo.ID)
94 if errors.Is(err, store.ErrNotFound) {
95 return c.emit(map[string]any{"enabled": false}, func(w io.Writer) {
96 v := c.view(w)
97 v.fields("checks", fmt.Sprintf("off (repo deps enable %s)", repo.Path()))
98 })
99 }
100 if err != nil {
101 return c.fail(protocol.ExitFailure, "%v", err)
102 }
103 reports, err := c.Store.ReportedDeps(repo.ID)
104 if err != nil {
105 return c.fail(protocol.ExitFailure, "%v", err)
106 }
107 out := DepsOut{Enabled: true, LastCheck: check.LastCheck, LastError: check.LastError,
108 IssueNumber: check.IssueNumber, Behind: []DepBehind{}}
109 for _, r := range reports {
110 out.Behind = append(out.Behind, DepBehind{r.Ecosystem, r.Name, r.Current, r.Latest})
111 }
112 return c.emit(out, func(w io.Writer) {
113 tracked := ""
114 if check.IssueNumber != 0 {
115 tracked = fmt.Sprintf("#%d", check.IssueNumber)
116 }
117 v := c.view(w)
118 v.fields(
119 "checks", "on",
120 "last check", c.when(check.LastCheck),
121 "last error", check.LastError,
122 "tracked in", tracked,
123 )
124 if len(out.Behind) > 0 {
125 v.section("behind")
126 tb := c.table(w, "ECOSYSTEM", "NAME", "CURRENT", "LATEST")
127 for _, b := range out.Behind {
128 tb.row(cText(b.Ecosystem), cRef(b.Name), cText(b.Current), cText(b.Latest))
129 }
130 tb.flush()
131 }
132 })
133}