From e4aa5ba84c06b7e48288e361d5aadfef63380446 Mon Sep 17 00:00:00 2001 From: vaindil Date: Fri, 30 Jun 2023 19:25:28 -0400 Subject: [PATCH] add ruleset check command --- pkg/cmd/ruleset/check/check.go | 81 ++++++++++++++++++++++++-------- pkg/cmd/ruleset/shared/shared.go | 61 ++++++++++++++++++++++-- pkg/cmd/ruleset/view/view.go | 26 +--------- 3 files changed, 118 insertions(+), 50 deletions(-) diff --git a/pkg/cmd/ruleset/check/check.go b/pkg/cmd/ruleset/check/check.go index 34c708c69..53a73efb1 100644 --- a/pkg/cmd/ruleset/check/check.go +++ b/pkg/cmd/ruleset/check/check.go @@ -9,8 +9,11 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/text" + "github.com/cli/cli/v2/pkg/cmd/ruleset/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" @@ -22,9 +25,11 @@ type CheckOptions struct { Config func() (config.Config, error) BaseRepo func() (ghrepo.Interface, error) Git *git.Client + Browser browser.Browser Branch string Default bool + WebMode bool } func NewCmdCheck(f *cmdutil.Factory, runF func(*CheckOptions) error) *cobra.Command { @@ -32,33 +37,58 @@ func NewCmdCheck(f *cmdutil.Factory, runF func(*CheckOptions) error) *cobra.Comm IO: f.IOStreams, Config: f.Config, HttpClient: f.HttpClient, + Browser: f.Browser, Git: f.GitClient, } cmd := &cobra.Command{ Use: "check []", - Short: "Print rules that would apply to a given branch", + Short: "View rules that would apply to a given branch", Long: heredoc.Doc(` - TODO + View information about GitHub rules that apply to a given branch. + + The provided branch name does not need to exist, rules will be displayed that would apply + to a branch with that name. All rules are returned regardless of where they are configured. + + If no branch name is provided, then the current branch will be used. + + The --default flag can be used to view rules that apply to the default branch of the current + repository. `), - Example: "TODO", - Args: cobra.MaximumNArgs(1), + Example: heredoc.Doc(` + # View all rules that apply to the current branch + $ gh ruleset check + + # View all rules that apply to a branch named "my-branch" in a different repository + $ gh ruleset check my-branch --repo owner/repo + + # View all rules that apply to the default branch in a different repository + $ gh ruleset check --default --repo owner/repo + + # View a ruleset configured in a different repository or any of its parents + $ gh ruleset view 23 --repo owner/repo + + # View an organization-level ruleset + $ gh ruleset view 23 --org my-org + `), + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { // support `-R, --repo` override opts.BaseRepo = f.BaseRepo - // TODO flag to do a push - if len(args) > 0 { opts.Branch = args[0] } - if runF != nil { - return runF(opts) + if err := cmdutil.MutuallyExclusive( + "specify only one of `--default` or a branch name", + opts.Branch != "", + opts.Default, + ); err != nil { + return err } - if opts.Branch != "" && opts.Default { - return cmdutil.FlagErrorf( - "branch argument '%s' and --default mutually exclusive", opts.Branch) + if runF != nil { + return runF(opts) } return checkRun(opts) @@ -71,10 +101,6 @@ func NewCmdCheck(f *cmdutil.Factory, runF func(*CheckOptions) error) *cobra.Comm } func checkRun(opts *CheckOptions) error { - // TODO ask about pushing (if interactive) - // TODO error if not interactive and --push not specified - // TODO parsing for errors on push - httpClient, err := opts.HttpClient() if err != nil { return err @@ -103,18 +129,33 @@ func checkRun(opts *CheckOptions) error { } } - var lol interface{} + rawPath := fmt.Sprintf("rules?ref=%s%s", url.QueryEscape("refs/heads/"), url.QueryEscape(opts.Branch)) + rulesURL := ghrepo.GenerateRepoURL(repoI, rawPath) + + if opts.WebMode { + if opts.IO.IsStdoutTTY() { + fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", text.DisplayURL(rulesURL)) + } + + return opts.Browser.Browse(rulesURL) + } + + var rules []shared.RulesetRule endpoint := fmt.Sprintf("repos/%s/%s/rules/branches/%s", repoI.RepoOwner(), repoI.RepoName(), url.PathEscape(opts.Branch)) - if err = client.REST(repoI.RepoHost(), "GET", endpoint, nil, &lol); err != nil { + if err = client.REST(repoI.RepoHost(), "GET", endpoint, nil, &rules); err != nil { return fmt.Errorf("GET %s failed: %w", endpoint, err) } - // TODO handle 404s gracefully - // TODO actually parse JSON + w := opts.IO.Out - fmt.Printf("DBG %#v\n", lol) + fmt.Fprintf(w, "%d rules apply to branch %s in repo %s/%s\n", len(rules), opts.Branch, repoI.RepoOwner(), repoI.RepoName()) + + if len(rules) > 0 { + fmt.Fprint(w, "\n") + fmt.Fprint(w, shared.ParseRulesForDisplay(rules)) + } return nil } diff --git a/pkg/cmd/ruleset/shared/shared.go b/pkg/cmd/ruleset/shared/shared.go index 5631a24e7..b365a006d 100644 --- a/pkg/cmd/ruleset/shared/shared.go +++ b/pkg/cmd/ruleset/shared/shared.go @@ -2,6 +2,8 @@ package shared import ( "fmt" + "sort" + "strings" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmdutil" @@ -35,17 +37,22 @@ type RulesetREST struct { // TODO is this source field used? SourceType string `json:"source_type"` Source string - Rules []struct { - Type string - Parameters map[string]interface{} - } - Links struct { + Rules []RulesetRule + Links struct { Html struct { Href string } } `json:"_links"` } +type RulesetRule struct { + Type string + Parameters map[string]interface{} + RulesetSourceType string `json:"ruleset_source_type"` + RulesetSource string `json:"ruleset_source"` + RulesetId int `json:"ruleset_id"` +} + // Returns the source of the ruleset in the format "owner/name (repo)" or "owner (org)" func RulesetSource(rs RulesetGraphQL) string { var level string @@ -60,6 +67,50 @@ func RulesetSource(rs RulesetGraphQL) string { return fmt.Sprintf("%s (%s)", rs.Source.Owner, level) } +func ParseRulesForDisplay(rules []RulesetRule) string { + var display strings.Builder + + // sort keys for consistent responses + sort.SliceStable(rules, func(i, j int) bool { + return rules[i].Type < rules[j].Type + }) + + for _, rule := range rules { + display.WriteString(fmt.Sprintf("- %s", rule.Type)) + + if rule.Parameters != nil && len(rule.Parameters) > 0 { + display.WriteString(": ") + + // sort these keys too for consistency + params := make([]string, 0, len(rule.Parameters)) + for p := range rule.Parameters { + params = append(params, p) + } + sort.Strings(params) + + for _, n := range params { + display.WriteString(fmt.Sprintf("[%s: %v] ", n, rule.Parameters[n])) + } + } + + // ruleset source info is only returned from the "get rules for a branch" endpoint + if rule.RulesetSource != "" { + display.WriteString( + fmt.Sprintf( + "\n (configured in ruleset %d from %s %s)\n", + rule.RulesetId, + strings.ToLower(rule.RulesetSourceType), + rule.RulesetSource, + ), + ) + } + + display.WriteString("\n") + } + + return display.String() +} + func NoRulesetsFoundError(orgOption string, repoI ghrepo.Interface, includeParents bool) error { entityName := EntityName(orgOption, repoI) parentsMsg := "" diff --git a/pkg/cmd/ruleset/view/view.go b/pkg/cmd/ruleset/view/view.go index 6b3604fa4..b842e2e69 100644 --- a/pkg/cmd/ruleset/view/view.go +++ b/pkg/cmd/ruleset/view/view.go @@ -248,31 +248,7 @@ func viewRun(opts *ViewOptions) error { if len(rs.Rules) == 0 { fmt.Fprintf(w, "No rules configured\n") } else { - // sort keys for consistent responses - sort.SliceStable(rs.Rules, func(i, j int) bool { - return rs.Rules[i].Type < rs.Rules[j].Type - }) - - for _, rule := range rs.Rules { - fmt.Fprintf(w, "- %s", rule.Type) - - if rule.Parameters != nil && len(rule.Parameters) > 0 { - fmt.Fprintf(w, ": ") - - // sort these keys too for consistency - params := make([]string, 0, len(rule.Parameters)) - for p := range rule.Parameters { - params = append(params, p) - } - sort.Strings(params) - - for _, n := range params { - fmt.Fprintf(w, "[%s: %v] ", n, rule.Parameters[n]) - } - } - - fmt.Fprint(w, "\n") - } + fmt.Fprint(w, shared.ParseRulesForDisplay(rs.Rules)) } return nil