Skip to content

Commit ee7391a

Browse files
authored
fix(librarian): prevent panic on release command without subcommand (#1901)
Fixes #1723 When running without a subcommand, the program panicked due to a nil pointer dereference. This occurred because the command has no action and no configuration. This change adds a check to see if a command has a function. If it does not, it prints the command's help text and exits, preventing the panic. With the fix: ``` jinseop@jinseop:/workspace/librarian$ go run ./cmd/librarian release Manages releases of libraries. Usage: librarian release <command> [arguments] Commands: init initiates a release by creating a release pull request. tag-and-release tags and creates a GitHub release for a merged pull request. 2025/09/02 16:55:30 command "release" requires a subcommand exit status 1 ```
1 parent e9f2ea3 commit ee7391a

File tree

2 files changed

+48
-0
lines changed

2 files changed

+48
-0
lines changed

internal/librarian/librarian.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ func Run(ctx context.Context, arg ...string) error {
5959
if err != nil {
6060
return err
6161
}
62+
63+
// If a command is just a container for subcommands, it won't have a
64+
// Run function. In that case, display its usage instructions.
65+
if cmd.Run == nil {
66+
cmd.Flags.Usage()
67+
return fmt.Errorf("command %q requires a subcommand", cmd.Name())
68+
}
69+
6270
if err := cmd.Parse(arg); err != nil {
6371
// We expect that if cmd.Parse fails, it will already
6472
// have printed out a command-specific usage error,

internal/librarian/librarian_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
package librarian
1616

1717
import (
18+
"context"
1819
"fmt"
1920
"log"
2021
"math/rand"
2122
"os"
2223
"os/exec"
2324
"path/filepath"
25+
"strings"
2426
"testing"
2527

2628
"github.com/go-git/go-git/v5"
@@ -42,6 +44,44 @@ func TestRun(t *testing.T) {
4244
}
4345
}
4446

47+
func TestParentCommands(t *testing.T) {
48+
ctx := context.Background()
49+
50+
tests := []struct {
51+
name string
52+
command string
53+
wantErr bool
54+
wantErrMsg string // Expected substring in the error
55+
}{
56+
{
57+
name: "release no subcommand",
58+
command: "release",
59+
wantErr: true,
60+
wantErrMsg: `command "release" requires a subcommand`,
61+
},
62+
}
63+
64+
for _, test := range tests {
65+
t.Run(test.name, func(t *testing.T) {
66+
err := Run(ctx, test.command)
67+
68+
if test.wantErr {
69+
if err == nil {
70+
t.Fatalf("Run(ctx, %q) got nil, want error containing %q", test.command, test.wantErrMsg)
71+
}
72+
if !strings.Contains(err.Error(), test.wantErrMsg) {
73+
t.Errorf("Run(ctx, %q) got error %q, want error containing %q", test.command, err.Error(), test.wantErrMsg)
74+
}
75+
return
76+
}
77+
78+
if err != nil {
79+
t.Fatalf("Run(ctx, %q) got error %v, want nil", test.command, err)
80+
}
81+
})
82+
}
83+
}
84+
4585
func TestIsURL(t *testing.T) {
4686
for _, test := range []struct {
4787
name string

0 commit comments

Comments
 (0)