Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions internal/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@ func (o Options) Active() bool {
return o.JSONSet || o.JQ != "" || o.Template != ""
}

// MarkWebMutuallyExclusive declares the standard output flags
// (--json, --jq, --template) incompatible with --web on cmd. Call
// from any builder that has both — without this, cobra silently lets
// both groups coexist and runtime picks --web, discarding the
// machine-readable request. The C-audit (C5, C15) flagged exactly
// this footgun.
//
// Three independent pair-wise calls (rather than one quadruple call)
// so cobra still allows the existing legal combinations among
// --json / --jq / --template.
func MarkWebMutuallyExclusive(cmd *cobra.Command) {
cmd.MarkFlagsMutuallyExclusive("web", FlagJSON)
cmd.MarkFlagsMutuallyExclusive("web", FlagJQ)
cmd.MarkFlagsMutuallyExclusive("web", FlagTemplate)
}

// validate enforces mutual exclusion across --jq / --template against
// each other. --json may combine with --jq or --template because it
// drives the *projection*; jq/template format the projection.
Expand Down
54 changes: 54 additions & 0 deletions internal/output/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"encoding/json"
"strings"
"testing"

"github.com/spf13/cobra"
)

// fakeExporter projects the supplied data through json.Marshal-friendly
Expand Down Expand Up @@ -204,3 +206,55 @@ func TestDefaultTemplateFuncs(t *testing.T) {
t.Errorf("color stub: got %q", got)
}
}

// TestMarkWebMutuallyExclusive: C-audit C5/C15. When a command has
// both --web and the standard output flags, supplying both must be
// rejected at parse time. Three pair-wise groups means --json + --jq
// stays legal (existing legal combination).
func TestMarkWebMutuallyExclusive(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
args []string
wantErr bool
}{
{"web alone", []string{"--web"}, false},
{"json alone", []string{"--json", "id"}, false},
{"jq alone", []string{"--jq", ".id"}, false},
{"template alone", []string{"--template", "{{.id}}"}, false},
{"json+jq still legal", []string{"--json", "id", "--jq", ".id"}, false},
{"web + json rejected", []string{"--web", "--json", "id"}, true},
{"web + jq rejected", []string{"--web", "--jq", ".id"}, true},
{"web + template rejected", []string{"--web", "--template", "{{.id}}"}, true},
} {
t.Run(tc.name, func(t *testing.T) {
cmd := newTestCmdWithWebAndOutput()
cmd.SetArgs(tc.args)
err := cmd.Execute()
gotErr := err != nil
if gotErr != tc.wantErr {
t.Errorf("err: got %v, wantErr=%v", err, tc.wantErr)
}
// cobra's group-message form: "if any flags in the group
// [a b] are set none of the others can be; [...] were all set"
if tc.wantErr && err != nil && !strings.Contains(err.Error(), "none of the others can be") {
t.Errorf("error should be cobra's mutex form; got: %v", err)
}
})
}
}

// newTestCmdWithWebAndOutput builds a no-op cobra command that has
// both `--web` and the output flag set, with the mutex applied.
func newTestCmdWithWebAndOutput() *cobra.Command {
var web bool
opts := &Options{}
cmd := &cobra.Command{
Use: "x",
RunE: func(*cobra.Command, []string) error { return nil },
}
cmd.Flags().BoolVar(&web, "web", false, "")
AddFlags(cmd, opts)
MarkWebMutuallyExclusive(cmd)
return cmd
}
1 change: 1 addition & 0 deletions pkg/cmd/issue/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func NewCmd(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", DefaultLimit, "maximum number of issues to fetch")
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "open the issues list in a browser")
output.AddFlags(cmd, &opts.Exporter)
output.MarkWebMutuallyExclusive(cmd)
return cmd
}

Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/issue/view/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func NewCmd(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "open the issue in a browser")
cmd.Flags().BoolVarP(&opts.Comments, "comments", "c", false, "include the comment thread inline")
output.AddFlags(cmd, &opts.Exporter)
output.MarkWebMutuallyExclusive(cmd)
return cmd
}

Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/pr/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func NewCmd(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", DefaultLimit, "maximum number of PRs to fetch")
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "open the PR list in a browser")
output.AddFlags(cmd, &opts.Exporter)
output.MarkWebMutuallyExclusive(cmd)
return cmd
}

Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/pr/view/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func NewCmd(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "open the PR in a browser")
cmd.Flags().BoolVarP(&opts.Comments, "comments", "c", false, "include the conversation thread inline")
output.AddFlags(cmd, &opts.Exporter)
output.MarkWebMutuallyExclusive(cmd)
return cmd
}

Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/repo/view/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func NewCmd(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "open the repository in the browser")
cmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "branch to view the README from (default: default branch)")
output.AddFlags(cmd, &opts.Exporter)
output.MarkWebMutuallyExclusive(cmd)
return cmd
}

Expand Down
Loading