control: an argument refusal prints the usage too !390

merged merged by cmc on 2026-09-12 06:38 UTC · krz/gitbay:fix-215-expected-usage into main

5 files changed, +26 −3

Layout: unified · split

internal/control/build.go +1 −1
@@ -85,7 +85,7 @@ func buildToOut(b store.Build) BuildOut {
85 85
86func buildRef(c *Ctx, args []string) (store.Repo, store.Build, int) { 86func buildRef(c *Ctx, args []string) (store.Repo, store.Build, int) {
87 if len(args) != 2 { 87 if len(args) != 2 {
88 return store.Repo{}, store.Build{}, c.fail(protocol.ExitUsage, "expected <owner/name> <number>") 88 return store.Repo{}, store.Build{}, c.usageWith("expected <owner/name> <number>")
89 } 89 }
90 repo, code := resolveRepo(c, args[0], policy.CanRead) 90 repo, code := resolveRepo(c, args[0], policy.CanRead)
91 if code >= 0 { 91 if code >= 0 {
internal/control/control.go +6
@@ -47,6 +47,12 @@ func (c *Ctx) usage() int {
47 return c.fail(protocol.ExitUsage, "usage: %s", c.Cmd.Usage) 47 return c.fail(protocol.ExitUsage, "usage: %s", c.Cmd.Usage)
48} 48}
49 49
50// usageWith reports a specific problem with the arguments, then the
51// registered usage, so a person always sees the shape that was expected.
52func (c *Ctx) usageWith(msg string) int {
53 return c.fail(protocol.ExitUsage, "%s\nusage: %s", msg, c.Cmd.Usage)
54}
55
50type Command struct { 56type Command struct {
51 Path []string // e.g. ["keys", "add"] 57 Path []string // e.g. ["keys", "add"]
52 // Summary is one line of prose: what the command does, no argument 58 // Summary is one line of prose: what the command does, no argument
internal/control/control_test.go +17
@@ -254,3 +254,20 @@ func TestFailErrExitCodes(t *testing.T) {
254 } 254 }
255 } 255 }
256} 256}
257
258// TestArgumentRefusalsNameTheUsage: a missing positional argument is a
259// usage error that prints the registered usage, the shared reference
260// helpers included (#215).
261func TestArgumentRefusalsNameTheUsage(t *testing.T) {
262 for _, argv := range [][]string{{"build", "show"}, {"release", "show"}, {"mr", "resolve"}, {"issue", "show"}} {
263 var out, errOut bytes.Buffer
264 c := &Ctx{Scope: "full", Stdout: &out, Stderr: &errOut}
265 if code := Dispatch(c, argv); code != protocol.ExitUsage {
266 t.Errorf("%v: exit %d, want %d (%s)", argv, code, protocol.ExitUsage, errOut.String())
267 continue
268 }
269 if !strings.Contains(errOut.String(), "usage: "+strings.Join(argv, " ")) {
270 t.Errorf("%v: no usage line: %q", argv, errOut.String())
271 }
272 }
273}
internal/control/release.go +1 −1
@@ -59,7 +59,7 @@ func assetDir(root string, repo store.Repo, releaseID int64) string {
59// releaseRef loads a release for "<owner/name> <tag>" with the permission. 59// releaseRef loads a release for "<owner/name> <tag>" with the permission.
60func releaseRef(c *Ctx, args []string, perm func(store.User, store.Repo, string) bool) (store.Repo, store.Release, int) { 60func releaseRef(c *Ctx, args []string, perm func(store.User, store.Repo, string) bool) (store.Repo, store.Release, int) {
61 if len(args) < 2 { 61 if len(args) < 2 {
62 return store.Repo{}, store.Release{}, c.fail(protocol.ExitUsage, "expected <owner/name> <tag>") 62 return store.Repo{}, store.Release{}, c.usageWith("expected <owner/name> <tag>")
63 } 63 }
64 repo, code := resolveRepo(c, args[0], perm) 64 repo, code := resolveRepo(c, args[0], perm)
65 if code >= 0 { 65 if code >= 0 {
internal/control/thread.go +1 −1
@@ -18,7 +18,7 @@ import (
18// refArgs resolves "<owner/name> <n>" to the repository and the number. 18// refArgs resolves "<owner/name> <n>" to the repository and the number.
19func refArgs(c *Ctx, args []string, perm func(store.User, store.Repo, string) bool, noun string) (store.Repo, int64, int) { 19func refArgs(c *Ctx, args []string, perm func(store.User, store.Repo, string) bool, noun string) (store.Repo, int64, int) {
20 if len(args) < 2 { 20 if len(args) < 2 {
21 return store.Repo{}, 0, c.fail(protocol.ExitUsage, "expected <owner/name> <number>") 21 return store.Repo{}, 0, c.usageWith("expected <owner/name> <number>")
22 } 22 }
23 repo, code := resolveRepo(c, args[0], perm) 23 repo, code := resolveRepo(c, args[0], perm)
24 if code >= 0 { 24 if code >= 0 {