diff --git a/pkg/cmd/ruleset/list/list.go b/pkg/cmd/ruleset/list/list.go index 2c9835124..e49710ba8 100644 --- a/pkg/cmd/ruleset/list/list.go +++ b/pkg/cmd/ruleset/list/list.go @@ -13,6 +13,7 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/tableprinter" "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" @@ -25,9 +26,10 @@ type ListOptions struct { BaseRepo func() (ghrepo.Interface, error) Browser browser.Browser - Limit int - WebMode bool - Organization string + Limit int + IncludeParents bool + WebMode bool + Organization string } func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { @@ -62,8 +64,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman } cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum number of rulesets to list") - cmd.Flags().StringVarP(&opts.Organization, "org", "o", "", "List organization-wide rulesets") - cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List rulesets in the web browser") + cmd.Flags().StringVarP(&opts.Organization, "org", "o", "", "List organization-wide rulesets for the provided organization") + cmd.Flags().BoolVarP(&opts.IncludeParents, "parents", "p", false, "Include rulesets configured at higher levels that also apply") + cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the list of rulesets in the web browser") return cmd } @@ -101,12 +104,12 @@ func listRun(opts *ListOptions) error { return opts.Browser.Browse(rulesetURL) } - var result *RulesetList + var result *shared.RulesetList if opts.Organization != "" { - result, err = listOrgRulesets(httpClient, opts.Organization, opts.Limit, hostname) + result, err = shared.ListOrgRulesets(httpClient, opts.Organization, opts.Limit, hostname, opts.IncludeParents) } else { - result, err = listRepoRulesets(httpClient, repoI, opts.Limit) + result, err = shared.ListRepoRulesets(httpClient, repoI, opts.Limit, opts.IncludeParents) } if err != nil { @@ -121,7 +124,11 @@ func listRun(opts *ListOptions) error { } if result.TotalCount == 0 { - msg := fmt.Sprintf("no rulesets found in %s", entityName) + parentsMsg := "" + if opts.IncludeParents { + parentsMsg = " or its parents" + } + msg := fmt.Sprintf("no rulesets found in %s%s", entityName, parentsMsg) return cmdutil.NewNoResultsError(msg) } @@ -139,17 +146,25 @@ func listRun(opts *ListOptions) error { } tp := tableprinter.New(opts.IO) - tp.HeaderRow("ID", "NAME", "STATUS", "TARGET") + tp.HeaderRow("ID", "NAME", "SOURCE", "STATUS", "TARGET", "RULES") for _, rs := range result.Rulesets { tp.AddField(strconv.Itoa(rs.Id)) tp.AddField(rs.Name, tableprinter.WithColor(cs.Bold)) + var ownerString string + if rs.Source.RepoOwner != "" { + ownerString = fmt.Sprintf("%s (repo)", rs.Source.RepoOwner) + } else if rs.Source.OrgOwner != "" { + ownerString = fmt.Sprintf("%s (org)", rs.Source.OrgOwner) + } else { + ownerString = "(unknown)" + } + tp.AddField(ownerString) tp.AddField(strings.ToLower(rs.Enforcement)) tp.AddField(strings.ToLower(rs.Target)) + tp.AddField(strconv.Itoa(rs.RulesGql.TotalCount)) tp.EndRow() } return tp.Render() } - -// func getRulesets() diff --git a/pkg/cmd/ruleset/list/http.go b/pkg/cmd/ruleset/shared/http.go similarity index 73% rename from pkg/cmd/ruleset/list/http.go rename to pkg/cmd/ruleset/shared/http.go index 592bdeda5..9dae0d7e6 100644 --- a/pkg/cmd/ruleset/list/http.go +++ b/pkg/cmd/ruleset/shared/http.go @@ -1,4 +1,4 @@ -package list +package shared import ( "fmt" @@ -6,14 +6,13 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/ruleset/shared" ) type RulesetResponse struct { Level struct { Rulesets struct { TotalCount int - Nodes []shared.Ruleset + Nodes []Ruleset PageInfo struct { HasNextPage bool EndCursor string @@ -24,21 +23,23 @@ type RulesetResponse struct { type RulesetList struct { TotalCount int - Rulesets []shared.Ruleset + Rulesets []Ruleset } -func listRepoRulesets(httpClient *http.Client, repo ghrepo.Interface, limit int) (*RulesetList, error) { +func ListRepoRulesets(httpClient *http.Client, repo ghrepo.Interface, limit int, includeParents bool) (*RulesetList, error) { variables := map[string]interface{}{ - "owner": repo.RepoOwner(), - "repo": repo.RepoName(), + "owner": repo.RepoOwner(), + "repo": repo.RepoName(), + "includeParents": includeParents, } return listRulesets(httpClient, rulesetsQuery(false), variables, limit, repo.RepoHost()) } -func listOrgRulesets(httpClient *http.Client, orgLogin string, limit int, host string) (*RulesetList, error) { +func ListOrgRulesets(httpClient *http.Client, orgLogin string, limit int, host string, includeParents bool) (*RulesetList, error) { variables := map[string]interface{}{ - "login": orgLogin, + "login": orgLogin, + "includeParents": includeParents, } return listRulesets(httpClient, rulesetsQuery(true), variables, limit, host) @@ -48,7 +49,7 @@ func listRulesets(httpClient *http.Client, query string, variables map[string]in pageLimit := min(limit, 100) res := RulesetList{ - Rulesets: []shared.Ruleset{}, + Rulesets: []Ruleset{}, } client := api.NewClientFromHTTP(httpClient) @@ -90,16 +91,20 @@ func rulesetsQuery(org bool) string { level = "repository(owner: $owner, name: $repo)" } - str := fmt.Sprintf("query RulesetList($limit: Int!, $endCursor: String, %s) { level: %s {", args, level) + str := fmt.Sprintf("query RulesetList($limit: Int!, $endCursor: String, $includeParents: Boolean, %s) { level: %s {", args, level) return str + ` - rulesets(first: $limit, after: $endCursor) { + rulesets(first: $limit, after: $endCursor, includeParents: $includeParents) { totalCount nodes { id: databaseId name target enforcement + source { + ... on Repository { repoOwner: nameWithOwner } + ... on Organization { orgOwner: login } + } # conditions { # refName { # include diff --git a/pkg/cmd/ruleset/shared/shared.go b/pkg/cmd/ruleset/shared/shared.go index d226ad18c..fa5f2e30f 100644 --- a/pkg/cmd/ruleset/shared/shared.go +++ b/pkg/cmd/ruleset/shared/shared.go @@ -21,6 +21,10 @@ type Ruleset struct { // Protected bool // } `json:"repository_name"` } + Source struct { + RepoOwner string + OrgOwner string + } RulesGql struct { TotalCount int } diff --git a/pkg/cmd/ruleset/view/view.go b/pkg/cmd/ruleset/view/view.go index 49c74581c..6d0005909 100644 --- a/pkg/cmd/ruleset/view/view.go +++ b/pkg/cmd/ruleset/view/view.go @@ -81,7 +81,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman } cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the ruleset in the browser") - cmd.Flags().StringVarP(&opts.Organization, "org", "o", "", "Organization name if the ID ") + cmd.Flags().StringVarP(&opts.Organization, "org", "o", "", "Organization name if the provided ID is an organization-level ruleset") return cmd } @@ -104,22 +104,6 @@ func viewRun(opts *ViewOptions) error { hostname, _ := cfg.DefaultHost() - if opts.WebMode { - // TODO need to validate ruleset's existence before opening - var rulesetURL string - if opts.Organization != "" { - rulesetURL = fmt.Sprintf("%sorganizations/%s/settings/rules/%s", ghinstance.HostPrefix(hostname), opts.Organization, opts.ID) - } else { - rulesetURL = ghrepo.GenerateRepoURL(repoI, "settings/rules/%s", opts.ID) - } - - if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", text.DisplayURL(rulesetURL)) - } - - return opts.Browser.Browse(rulesetURL) - } - var rs *shared.Ruleset if opts.Organization != "" { rs, err = viewOrgRuleset(httpClient, opts.Organization, opts.ID, hostname) @@ -134,6 +118,25 @@ func viewRun(opts *ViewOptions) error { cs := opts.IO.ColorScheme() w := opts.IO.Out + if opts.WebMode { + if rs != nil { + var rulesetURL string + if opts.Organization != "" { + rulesetURL = fmt.Sprintf("%sorganizations/%s/settings/rules/%s", ghinstance.HostPrefix(hostname), opts.Organization, opts.ID) + } else { + rulesetURL = ghrepo.GenerateRepoURL(repoI, "settings/rules/%s", opts.ID) + } + + if opts.IO.IsStdoutTTY() { + fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", text.DisplayURL(rulesetURL)) + } + + return opts.Browser.Browse(rulesetURL) + } else { + fmt.Fprintf(w, "ruleset not found\n") + } + } + fmt.Fprintf(w, "\n%s\n", cs.Bold(rs.Name)) fmt.Fprintf(w, "ID: %d\n", rs.Id) @@ -141,7 +144,7 @@ func viewRun(opts *ViewOptions) error { case "disabled": fmt.Fprintf(w, "%s\n", cs.Red("Disabled")) case "evaluate": - fmt.Fprintf(w, "%s\n", cs.Yellow("Evaluate Mode (not being enforced)")) + fmt.Fprintf(w, "%s\n", cs.Yellow("Evaluate Mode (not enforced)")) case "active": fmt.Fprintf(w, "%s\n", cs.Green("Active")) default: @@ -165,7 +168,7 @@ func viewRun(opts *ViewOptions) error { fmt.Fprintf(w, "Actor types allowed to bypass:\n") for name, count := range types { - fmt.Fprintf(w, "- %s: %d actors\n", name, count) + fmt.Fprintf(w, "- %s: %d configured\n", name, count) } } @@ -173,8 +176,8 @@ func viewRun(opts *ViewOptions) error { if len(rs.Conditions) == 0 { fmt.Fprintf(w, "No conditions configured\n") } else { - // sort keys for consistent responses, mismatched types don't allow this to be broken - // into a separate function + // sort keys for consistent responses, can't make a separate function due to + // mismatched types keys := make([]string, 0, len(rs.Conditions)) for key := range rs.Conditions { keys = append(keys, key)