Commit 0f4f9b4c10
Verified · cmc ci/build: success ci/sonar: success ci/test: success ci/vuln: success
Layout: unified · split
.gitbay/wiki/Parity.org +5 −4
| @@ -146,7 +146,7 @@ format column and are always markdown. | |||
| 146 | | build trigger | yes | yes | yes | | 146 | | build trigger | yes | yes | yes | |
| 147 | | build cancel | yes | no | no | | 147 | | build cancel | yes | no | no | |
| 148 | | dependency checks on/off | yes | yes | no | | 148 | | dependency checks on/off | yes | yes | no | |
| 149 | | dependency status | yes | no | no | | 149 | | dependency status | yes | yes | no | |
| 150 | | delete, transfer | yes | no | no | | 150 | | delete, transfer | yes | no | no | |
| 151 | | release delete | yes | yes | no | | 151 | | release delete | yes | yes | no | |
| 152 | 152 | ||
| @@ -163,9 +163,10 @@ on the build page yet. | |||
| 163 | 163 | ||
| 164 | Dependency checks are off until a repository's admin turns them on: the | 164 | Dependency checks are off until a repository's admin turns them on: the |
| 165 | check tells a public registry what the repository depends on. =repo deps | 165 | check tells a public registry what the repository depends on. =repo deps |
| 166 | status= lists what is behind and has no web view because the report | 166 | status= lists what is behind; the repository's settings page renders the |
| 167 | itself is an issue the worker opens, rewrites and closes, which every | 167 | same report under the toggle — last check, last error, the tracking |
| 168 | surface already reads. | 168 | issue, and the rows. The issue the worker opens, rewrites and closes is |
| 169 | still the copy every other surface reads. | ||
| 169 | 170 | ||
| 170 | The wiki row covers reading. A wiki lives at =.gitbay/wiki/= on the | 171 | The wiki row covers reading. A wiki lives at =.gitbay/wiki/= on the |
| 171 | default branch, so editing one is editing a file in the repository — | 172 | default branch, so editing one is editing a file in the repository — |
e2e/deps_test.go +8 −1
| @@ -20,7 +20,7 @@ type depsStatus struct { | |||
| 20 | // can turn it on: the check tells a public registry what the repository | 20 | // can turn it on: the check tells a public registry what the repository |
| 21 | // depends on. | 21 | // depends on. |
| 22 | func TestDepsEnableDisable(t *testing.T) { | 22 | func TestDepsEnableDisable(t *testing.T) { |
| 23 | inst := startInstance(t) | 23 | inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n") |
| 24 | aliceKey := inst.newKey(t, "alice") | 24 | aliceKey := inst.newKey(t, "alice") |
| 25 | inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", | 25 | inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", |
| 26 | "--email", "alice@example.test", "--verified") | 26 | "--email", "alice@example.test", "--verified") |
| @@ -72,6 +72,13 @@ func TestDepsEnableDisable(t *testing.T) { | |||
| 72 | t.Fatalf("second deps enable: %s", errOut) | 72 | t.Fatalf("second deps enable: %s", errOut) |
| 73 | } | 73 | } |
| 74 | 74 | ||
| 75 | // The settings page reports the same state the command does (#164). | ||
| 76 | alice := inst.login(t, aliceKey) | ||
| 77 | _, page := browserGet(t, alice, inst.base()+"/alice/app/settings") | ||
| 78 | if !strings.Contains(page, "Last checked") || !strings.Contains(page, "Nothing behind") { | ||
| 79 | t.Errorf("settings page does not report the check state:\n%s", page) | ||
| 80 | } | ||
| 81 | |||
| 75 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "deps", "disable", "alice/app"); code != 0 { | 82 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "deps", "disable", "alice/app"); code != 0 { |
| 76 | t.Fatalf("deps disable: %s", errOut) | 83 | t.Fatalf("deps disable: %s", errOut) |
| 77 | } | 84 | } |
internal/control/deps.go +22 −15
| @@ -25,6 +25,25 @@ func init() { | |||
| 25 | Usage: "repo deps status <owner/name>", ReadOnly: true, Run: runDepsStatus}) | 25 | Usage: "repo deps status <owner/name>", ReadOnly: true, Run: runDepsStatus}) |
| 26 | } | 26 | } |
| 27 | 27 | ||
| 28 | // DepsOut is what `repo deps status` emits: the check's state and what | ||
| 29 | // the last run found behind. Named so the web decodes the same struct | ||
| 30 | // the command writes. | ||
| 31 | type DepsOut struct { | ||
| 32 | Enabled bool `json:"enabled"` | ||
| 33 | LastCheck string `json:"last_check,omitempty"` | ||
| 34 | LastError string `json:"last_error,omitempty"` | ||
| 35 | IssueNumber int64 `json:"issue_number,omitempty"` | ||
| 36 | Behind []DepBehind `json:"behind"` | ||
| 37 | } | ||
| 38 | |||
| 39 | // DepBehind is one dependency with a newer release than the manifest pins. | ||
| 40 | type DepBehind struct { | ||
| 41 | Ecosystem string `json:"ecosystem"` | ||
| 42 | Name string `json:"name"` | ||
| 43 | Current string `json:"current"` | ||
| 44 | Latest string `json:"latest"` | ||
| 45 | } | ||
| 46 | |||
| 28 | func runDepsEnable(c *Ctx, args []string) int { | 47 | func runDepsEnable(c *Ctx, args []string) int { |
| 29 | if len(args) != 1 { | 48 | if len(args) != 1 { |
| 30 | return c.fail(protocol.ExitUsage, "usage: repo deps enable <owner/name>") | 49 | return c.fail(protocol.ExitUsage, "usage: repo deps enable <owner/name>") |
| @@ -78,22 +97,10 @@ func runDepsStatus(c *Ctx, args []string) int { | |||
| 78 | if err != nil { | 97 | if err != nil { |
| 79 | return c.fail(protocol.ExitFailure, "%v", err) | 98 | return c.fail(protocol.ExitFailure, "%v", err) |
| 80 | } | 99 | } |
| 81 | type behind struct { | 100 | out := DepsOut{Enabled: true, LastCheck: check.LastCheck, LastError: check.LastError, |
| 82 | Ecosystem string `json:"ecosystem"` | 101 | IssueNumber: check.IssueNumber, Behind: []DepBehind{}} |
| 83 | Name string `json:"name"` | ||
| 84 | Current string `json:"current"` | ||
| 85 | Latest string `json:"latest"` | ||
| 86 | } | ||
| 87 | out := struct { | ||
| 88 | Enabled bool `json:"enabled"` | ||
| 89 | LastCheck string `json:"last_check,omitempty"` | ||
| 90 | LastError string `json:"last_error,omitempty"` | ||
| 91 | IssueNumber int64 `json:"issue_number,omitempty"` | ||
| 92 | Behind []behind `json:"behind"` | ||
| 93 | }{Enabled: true, LastCheck: check.LastCheck, LastError: check.LastError, | ||
| 94 | IssueNumber: check.IssueNumber, Behind: []behind{}} | ||
| 95 | for _, r := range reports { | 102 | for _, r := range reports { |
| 96 | out.Behind = append(out.Behind, behind{r.Ecosystem, r.Name, r.Current, r.Latest}) | 103 | out.Behind = append(out.Behind, DepBehind{r.Ecosystem, r.Name, r.Current, r.Latest}) |
| 97 | } | 104 | } |
| 98 | return c.emit(out, func(w io.Writer) { | 105 | return c.emit(out, func(w io.Writer) { |
| 99 | fmt.Fprintf(w, "checks on, last %s\n", orDash(check.LastCheck)) | 106 | fmt.Fprintf(w, "checks on, last %s\n", orDash(check.LastCheck)) |
internal/control/output_test.go +2 −2
| @@ -16,7 +16,7 @@ func TestNamedPayloadsRoundTrip(t *testing.T) { | |||
| 16 | &Created{}, &MRCreated{}, &IssueShow{}, &MRShow{}, | 16 | &Created{}, &MRCreated{}, &IssueShow{}, &MRShow{}, |
| 17 | &BuildOut{}, &JobOut{}, &ProfileOut{}, &ProfileRepo{}, &ProfileMember{}, | 17 | &BuildOut{}, &JobOut{}, &ProfileOut{}, &ProfileRepo{}, &ProfileMember{}, |
| 18 | &DashboardOut{}, &DashboardItem{}, &DashboardBuild{}, &PinnedOut{}, | 18 | &DashboardOut{}, &DashboardItem{}, &DashboardBuild{}, &PinnedOut{}, |
| 19 | &FeedOut{}, &ActivityDay{}, &SearchResult{}, &ReviewOut{}, &CheckOut{}, &CommitOut{}, | 19 | &FeedOut{}, &ActivityDay{}, &DepsOut{}, &DepBehind{}, &SearchResult{}, &ReviewOut{}, &CheckOut{}, &CommitOut{}, |
| 20 | } | 20 | } |
| 21 | for _, p := range payloads { | 21 | for _, p := range payloads { |
| 22 | name := reflect.TypeOf(p).Elem().Name() | 22 | name := reflect.TypeOf(p).Elem().Name() |
| @@ -39,7 +39,7 @@ func TestPayloadFieldsAreTagged(t *testing.T) { | |||
| 39 | types := []any{ | 39 | types := []any{ |
| 40 | Created{}, MRCreated{}, BuildOut{}, JobOut{}, ProfileOut{}, ProfileRepo{}, | 40 | Created{}, MRCreated{}, BuildOut{}, JobOut{}, ProfileOut{}, ProfileRepo{}, |
| 41 | ProfileMember{}, DashboardOut{}, DashboardItem{}, DashboardBuild{}, | 41 | ProfileMember{}, DashboardOut{}, DashboardItem{}, DashboardBuild{}, |
| 42 | PinnedOut{}, FeedOut{}, ActivityDay{}, SearchResult{}, ReviewOut{}, | 42 | PinnedOut{}, FeedOut{}, ActivityDay{}, DepsOut{}, DepBehind{}, SearchResult{}, ReviewOut{}, |
| 43 | CheckOut{}, CommitOut{}, ServerOut{}, | 43 | CheckOut{}, CommitOut{}, ServerOut{}, |
| 44 | } | 44 | } |
| 45 | for _, v := range types { | 45 | for _, v := range types { |
internal/httpd/depspage_test.go added +48
| @@ -0,0 +1,48 @@ | |||
| 1 | package httpd | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "strings" | ||
| 5 | "testing" | ||
| 6 | |||
| 7 | "gitbay.org/gitbay/internal/control" | ||
| 8 | "gitbay.org/gitbay/internal/web" | ||
| 9 | ) | ||
| 10 | |||
| 11 | // The settings page renders what `repo deps status` reports, not just the | ||
| 12 | // toggle (#164). A renamed field would be a blank cell rather than a | ||
| 13 | // compile error, so render the page and look for the values. | ||
| 14 | func TestSettingsPageRendersDepsStatus(t *testing.T) { | ||
| 15 | var sb strings.Builder | ||
| 16 | err := web.Render(&sb, "settings.html", settingsPage{ | ||
| 17 | repoPage: testRepoPage(), | ||
| 18 | DepsEnabled: true, | ||
| 19 | Deps: control.DepsOut{ | ||
| 20 | Enabled: true, LastCheck: "2026-09-03T08:20:25Z", IssueNumber: 140, | ||
| 21 | Behind: []control.DepBehind{{Ecosystem: "go", Name: "golang.org/x/crypto", | ||
| 22 | Current: "v0.31.0", Latest: "v0.42.0"}}, | ||
| 23 | }, | ||
| 24 | }) | ||
| 25 | if err != nil { | ||
| 26 | t.Fatal(err) | ||
| 27 | } | ||
| 28 | page := sb.String() | ||
| 29 | for _, want := range []string{ | ||
| 30 | "2026-09-03T08:20:25Z", "golang.org/x/crypto", "v0.31.0", "v0.42.0", | ||
| 31 | `href="/krz/gitbay/issues/140"`, | ||
| 32 | } { | ||
| 33 | if !strings.Contains(page, want) { | ||
| 34 | t.Errorf("settings page does not show %q", want) | ||
| 35 | } | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | // With checks off, none of the report shows. | ||
| 40 | func TestSettingsPageHidesDepsStatusWhenOff(t *testing.T) { | ||
| 41 | var sb strings.Builder | ||
| 42 | if err := web.Render(&sb, "settings.html", settingsPage{repoPage: testRepoPage()}); err != nil { | ||
| 43 | t.Fatal(err) | ||
| 44 | } | ||
| 45 | if strings.Contains(sb.String(), "Last checked") { | ||
| 46 | t.Error("the report shows with checks off") | ||
| 47 | } | ||
| 48 | } | ||
internal/httpd/settings.go +9 −3
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | "net/http" | 5 | "net/http" |
| 6 | "strings" | 6 | "strings" |
| 7 | 7 | ||
| 8 | "gitbay.org/gitbay/internal/control" | ||
| 8 | "gitbay.org/gitbay/internal/gitutil" | 9 | "gitbay.org/gitbay/internal/gitutil" |
| 9 | "gitbay.org/gitbay/internal/store" | 10 | "gitbay.org/gitbay/internal/store" |
| 10 | ) | 11 | ) |
| @@ -19,6 +20,7 @@ type settingsPage struct { | |||
| 19 | Topics []string | 20 | Topics []string |
| 20 | Branches []gitutil.Ref | 21 | Branches []gitutil.Ref |
| 21 | DepsEnabled bool | 22 | DepsEnabled bool |
| 23 | Deps control.DepsOut | ||
| 22 | Notice string | 24 | Notice string |
| 23 | } | 25 | } |
| 24 | 26 | ||
| @@ -34,11 +36,15 @@ func (s *Server) settingsForm(w http.ResponseWriter, r *http.Request, u store.Us | |||
| 34 | p.Tab = "settings" | 36 | p.Tab = "settings" |
| 35 | topics, _ := s.st.ListTopics(repo.ID) | 37 | topics, _ := s.st.ListTopics(repo.ID) |
| 36 | branches, _ := gitutil.Refs(p.Dir, "heads") | 38 | branches, _ := gitutil.Refs(p.Dir, "heads") |
| 37 | _, depsErr := s.st.DepCheckFor(repo.ID) | 39 | // The toggle's state comes from the store; what the last run found |
| 40 | // comes from the command, so the page shows the same report the CLI | ||
| 41 | // prints (#164). | ||
| 42 | var deps control.DepsOut | ||
| 43 | s.runControlInto(u, []string{"repo", "deps", "status", repo.Path()}, &deps) | ||
| 38 | s.render(w, "settings.html", settingsPage{ | 44 | s.render(w, "settings.html", settingsPage{ |
| 39 | repoPage: p, Topics: topics, Branches: branches, | 45 | repoPage: p, Topics: topics, Branches: branches, |
| 40 | DepsEnabled: depsErr == nil, | 46 | DepsEnabled: deps.Enabled, Deps: deps, |
| 41 | Notice: s.takeFlash(w, r), | 47 | Notice: s.takeFlash(w, r), |
| 42 | }) | 48 | }) |
| 43 | } | 49 | } |
| 44 | 50 | ||
internal/web/templates/settings.html +13
| @@ -109,6 +109,19 @@ | |||
| 109 | proxy.golang.org, npm, crates.io, and PyPI once a day, and tracks what is behind | 109 | proxy.golang.org, npm, crates.io, and PyPI once a day, and tracks what is behind |
| 110 | in an issue. Checking a private repository tells those registries what it | 110 | in an issue. Checking a private repository tells those registries what it |
| 111 | depends on.</p> | 111 | depends on.</p> |
| 112 | {{if .DepsEnabled}} | ||
| 113 | <p class="meta">Last checked {{if .Deps.LastCheck}}{{.Deps.LastCheck}}{{else}}never{{end}}{{if .Deps.IssueNumber}} · tracked in <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/issues/{{.Deps.IssueNumber}}">#{{.Deps.IssueNumber}}</a>{{end}}</p> | ||
| 114 | {{if .Deps.LastError}}<p class="error" role="alert">{{.Deps.LastError}}</p>{{end}} | ||
| 115 | {{if .Deps.Behind}}<div class="tablewrap"><table class="keys"> | ||
| 116 | <tr class="cols"><th scope="col">dependency</th><th scope="col">pinned</th><th scope="col">latest</th></tr> | ||
| 117 | {{range .Deps.Behind}}<tr> | ||
| 118 | <td class="mono">{{.Ecosystem}} {{.Name}}</td> | ||
| 119 | <td class="mono">{{.Current}}</td> | ||
| 120 | <td class="mono">{{.Latest}}</td> | ||
| 121 | </tr> | ||
| 122 | {{end}}</table></div> | ||
| 123 | {{else}}<p class="none">Nothing behind{{if not .Deps.LastCheck}} — the first check has not run yet{{end}}.</p>{{end}} | ||
| 124 | {{end}} | ||
| 112 | 125 | ||
| 113 | <h2>Lifecycle</h2> | 126 | <h2>Lifecycle</h2> |
| 114 | <form method="post" action="{{$base}}" class="setform"> | 127 | <form method="post" action="{{$base}}" class="setform"> |