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