Commit 8eb3623130

8eb3623130a664a78cded04c9bd3ec2796b0b9ec

parent: 2dd892270a

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-24 00:45 UTC

release list: --limit and --cursor; no title column when it repeats the tag

Ref #254
cmd/gitbay/main.go +1 −1
@@ -561,7 +561,7 @@ func releaseCmd() *cobra.Command {
561561 passOpts{server: []string{"release", "create"}, needsRepo: true, stdinOK: true, editor: "release"}),
562562 pass("edit", "update title and notes: <tag> [--title <t>] [--notes|--file -]",
563563 passOpts{server: []string{"release", "edit"}, needsRepo: true, stdinOK: true}),
564 pass("list", "list releases", passOpts{server: []string{"release", "list"}, needsRepo: true}),
564 pass("list", "releases: <owner/name> [--limit <n>] [--cursor <c>]", passOpts{server: []string{"release", "list"}, needsRepo: true}),
565565 pass("show", "show a release with assets: <tag>", passOpts{server: []string{"release", "show"}, needsRepo: true}),
566566 pass("delete", "delete a release and its assets: <tag> --yes", passOpts{server: []string{"release", "delete"}, needsRepo: true}),
567567 group("asset", "binary assets on a release",
internal/control/release.go +15 −6
@@ -28,7 +28,7 @@ func init() {
2828 ReadsStdin: true, Run: runReleaseEdit})
2929 register(Command{Path: []string{"release", "list"},
3030 Summary: "list releases",
31 Usage: "release list <owner/name>", ReadOnly: true, Run: runReleaseList})
31 Usage: "release list <owner/name> [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runReleaseList})
3232 register(Command{Path: []string{"release", "show"},
3333 Summary: "show a release with assets",
3434 Usage: "release show <owner/name> <tag>", ReadOnly: true, Run: runReleaseShow})
@@ -197,24 +197,33 @@ func runReleaseEdit(c *Ctx, args []string) int {
197197}
198198
199199func runReleaseList(c *Ctx, args []string) int {
200 if len(args) != 1 {
200 rest, p, code := parsePageFlags(c, args, "release", true)
201 if code >= 0 {
202 return code
203 }
204 if len(rest) != 1 {
201205 return c.usage()
202206 }
203 repo, code := resolveRepo(c, args[0], policy.CanRead)
207 repo, code := resolveRepo(c, rest[0], policy.CanRead)
204208 if code >= 0 {
205209 return code
206210 }
207 rels, err := c.Store.ListReleases(repo.ID)
211 rels, err := c.Store.ListReleasesPage(repo.ID, p.queryLimit(), p.keyInt())
208212 if err != nil {
209213 return c.fail(protocol.ExitFailure, "%v", err)
210214 }
215 rels, next := trimPage(p, rels, "release", func(r store.Release) string { return strconv.FormatInt(r.ID, 10) })
211216 var ds []releaseOut
212217 for _, r := range rels {
213218 ds = append(ds, releaseToOut(r, false))
214219 }
215 return c.emit(ds, func(w io.Writer) {
220 return c.emitPage(p, ds, next, func(w io.Writer) {
216221 for _, d := range ds {
217 fmt.Fprintf(w, "%s\t%s\t%d asset(s)\n", d.Tag, d.Title, len(d.Assets))
222 title := d.Title
223 if title == d.Tag {
224 title = ""
225 }
226 fmt.Fprintf(w, "%s\t%s\t%d asset(s)\n", d.Tag, title, len(d.Assets))
218227 }
219228 })
220229}
internal/control/release_test.go added +40
@@ -0,0 +1,40 @@
1package control
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7
8 "gitbay.org/gitbay/internal/protocol"
9 "gitbay.org/gitbay/internal/store"
10)
11
12func TestReleaseListPagesAndHidesTagTitle(t *testing.T) {
13 st, repo, uid := newQueueTestRepo(t)
14 for _, tag := range []string{"v1", "v2", "v3"} {
15 title := tag
16 if tag == "v2" {
17 title = "Second"
18 }
19 if _, err := st.CreateRelease(repo.ID, tag, title, "", uid, "md"); err != nil {
20 t.Fatal(err)
21 }
22 }
23 c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid})
24 if code := Dispatch(c, []string{"release", "list", repo.Path(), "--limit", "2"}); code != protocol.ExitOK {
25 t.Fatalf("exit %d: %s", code, errOut)
26 }
27 lines := strings.Split(strings.TrimSpace(c.Stdout.(*bytes.Buffer).String()), "\n")
28 if len(lines) != 3 || lines[0] != "v3\t\t0 asset(s)" || lines[1] != "v2\tSecond\t0 asset(s)" || !strings.HasPrefix(lines[2], "next\t") {
29 t.Fatalf("page 1:\n%s", strings.Join(lines, "\n"))
30 }
31 cursor := strings.TrimPrefix(lines[2], "next\t")
32
33 c, errOut = pruneCtx(st, t.TempDir(), store.User{ID: uid})
34 if code := Dispatch(c, []string{"release", "list", repo.Path(), "--cursor", cursor}); code != protocol.ExitOK {
35 t.Fatalf("exit %d: %s", code, errOut)
36 }
37 if got := c.Stdout.(*bytes.Buffer).String(); got != "v1\t\t0 asset(s)\n" {
38 t.Fatalf("page 2: %q", got)
39 }
40}
internal/store/releases.go +18 −1
@@ -90,7 +90,24 @@ func (s *Store) ReleaseByTag(repoID int64, tag string) (Release, error) {
9090
9191// ListReleases returns releases newest-first, assets included.
9292func (s *Store) ListReleases(repoID int64) ([]Release, error) {
93 rows, err := s.DB.Query(releaseSelect+" WHERE r.repo_id = ? ORDER BY r.created_at DESC", repoID)
93 return s.ListReleasesPage(repoID, 0, 0)
94}
95
96// ListReleasesPage lists newest first. limit 0 is every row; afterID is
97// the last release of the previous page, 0 for the first.
98func (s *Store) ListReleasesPage(repoID int64, limit int, afterID int64) ([]Release, error) {
99 q := releaseSelect + " WHERE r.repo_id = ?"
100 args := []any{repoID}
101 if afterID > 0 {
102 q += " AND (r.created_at, r.id) < (SELECT created_at, id FROM releases WHERE id = ?)"
103 args = append(args, afterID)
104 }
105 q += " ORDER BY r.created_at DESC, r.id DESC"
106 if limit > 0 {
107 q += " LIMIT ?"
108 args = append(args, limit)
109 }
110 rows, err := s.DB.Query(q, args...)
94111 if err != nil {
95112 return nil, err
96113 }