basic ruleset view works

This commit is contained in:
vaindil 2023-04-25 16:42:36 -04:00
parent 5155844d7f
commit 7f81645c78
6 changed files with 319 additions and 46 deletions

View file

@ -1,17 +1,19 @@
package list
import (
"fmt"
"net/http"
"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 []Ruleset
Nodes []shared.Ruleset
PageInfo struct {
HasNextPage bool
EndCursor string
@ -20,26 +22,9 @@ type RulesetResponse struct {
}
}
type Ruleset struct {
DatabaseId int
Name string
Target string
Enforcement string
Conditions struct {
RefName struct {
Include []string
Exclude []string
}
RepositoryName struct {
Include []string
Exclude []string
}
}
}
type RulesetList struct {
TotalCount int
Rulesets []Ruleset
Rulesets []shared.Ruleset
}
func listRepoRulesets(httpClient *http.Client, repo ghrepo.Interface, limit int) (*RulesetList, error) {
@ -48,7 +33,7 @@ func listRepoRulesets(httpClient *http.Client, repo ghrepo.Interface, limit int)
"repo": repo.RepoName(),
}
return listRulesets(httpClient, rulesetQuery(false), variables, limit, repo.RepoHost())
return listRulesets(httpClient, rulesetsQuery(false), variables, limit, repo.RepoHost())
}
func listOrgRulesets(httpClient *http.Client, orgLogin string, limit int, host string) (*RulesetList, error) {
@ -56,14 +41,14 @@ func listOrgRulesets(httpClient *http.Client, orgLogin string, limit int, host s
"login": orgLogin,
}
return listRulesets(httpClient, rulesetQuery(true), variables, limit, host)
return listRulesets(httpClient, rulesetsQuery(true), variables, limit, host)
}
func listRulesets(httpClient *http.Client, query string, variables map[string]interface{}, limit int, host string) (*RulesetList, error) {
pageLimit := min(limit, 100)
res := RulesetList{
Rulesets: []Ruleset{},
Rulesets: []shared.Ruleset{},
}
client := api.NewClientFromHTTP(httpClient)
@ -93,7 +78,7 @@ func listRulesets(httpClient *http.Client, query string, variables map[string]in
return &res, nil
}
func rulesetQuery(org bool) string {
func rulesetsQuery(org bool) string {
var args string
var level string
@ -105,28 +90,28 @@ func rulesetQuery(org bool) string {
level = "repository(owner: $owner, name: $repo)"
}
str := "query RulesetList($limit: Int!, $endCursor: String, " + args + ") { level: " + level + " {"
str := fmt.Sprintf("query RulesetList($limit: Int!, $endCursor: String, %s) { level: %s {", args, level)
str += `
return str + `
rulesets(first: $limit, after: $endCursor) {
totalCount
nodes {
databaseId
id: databaseId
name
target
enforcement
conditions {
refName {
include
exclude
}
repositoryName {
include
exclude
protected
}
}
rules {
# conditions {
# refName {
# include
# exclude
# }
# repositoryName {
# include
# exclude
# protected
# }
# }
rulesGql: rules {
totalCount
}
}
@ -134,9 +119,8 @@ func rulesetQuery(org bool) string {
hasNextPage
endCursor
}
}`
return str + "}}"
}
}}`
}
func min(a, b int) int {

View file

@ -61,9 +61,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
},
}
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum number of rules to list")
cmd.Flags().StringVarP(&opts.Organization, "org", "o", "", "List organization-wide rules")
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List rules in the web browser")
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")
return cmd
}
@ -125,6 +125,7 @@ func listRun(opts *ListOptions) error {
return cmdutil.NewNoResultsError(msg)
}
opts.IO.DetectTerminalTheme()
if err := opts.IO.StartPager(); err == nil {
defer opts.IO.StopPager()
} else {
@ -141,7 +142,7 @@ func listRun(opts *ListOptions) error {
tp.HeaderRow("ID", "NAME", "STATUS", "TARGET")
for _, rs := range result.Rulesets {
tp.AddField(strconv.Itoa(rs.DatabaseId))
tp.AddField(strconv.Itoa(rs.Id))
tp.AddField(rs.Name, tableprinter.WithColor(cs.Bold))
tp.AddField(strings.ToLower(rs.Enforcement))
tp.AddField(strings.ToLower(rs.Target))

View file

@ -4,6 +4,7 @@ import (
"github.com/MakeNowJust/heredoc"
cmdCheck "github.com/cli/cli/v2/pkg/cmd/ruleset/check"
cmdList "github.com/cli/cli/v2/pkg/cmd/ruleset/list"
cmdView "github.com/cli/cli/v2/pkg/cmd/ruleset/view"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
)
@ -21,7 +22,7 @@ func NewCmdRuleset(f *cmdutil.Factory) *cobra.Command {
cmdutil.EnableRepoOverride(cmd, f)
cmd.AddCommand(cmdList.NewCmdList(f, nil))
// cmd.AddCommand(cmdList.NewCmdView(f, nil))
cmd.AddCommand(cmdView.NewCmdView(f, nil))
cmd.AddCommand(cmdCheck.NewCmdCheck(f, nil))
return cmd

View file

@ -0,0 +1,28 @@
package shared
type Ruleset struct {
Id int
Name string
Target string
Enforcement string
BypassMode string `json:"bypass_mode"`
BypassActors []struct {
ActorId int `json:"actor_id"`
ActorType string `json:"actor_type"`
} `json:"bypass_actors"`
Conditions map[string]map[string]interface {
// RefName struct {
// Include []string
// Exclude []string
// } `json:"ref_name"`
// RepositoryName struct {
// Include []string
// Exclude []string
// Protected bool
// } `json:"repository_name"`
}
RulesGql struct {
TotalCount int
}
Rules []interface{}
}

View file

@ -0,0 +1,32 @@
package view
import (
"fmt"
"net/http"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/ruleset/shared"
)
func viewRepoRuleset(httpClient *http.Client, repo ghrepo.Interface, databaseId string) (*shared.Ruleset, error) {
path := fmt.Sprintf("repos/%s/%s/rulesets/%s", repo.RepoOwner(), repo.RepoName(), databaseId)
return viewRuleset(httpClient, repo.RepoHost(), path)
}
func viewOrgRuleset(httpClient *http.Client, orgLogin string, databaseId string, host string) (*shared.Ruleset, error) {
path := fmt.Sprintf("orgs/%s/rulesets/%s", orgLogin, databaseId)
return viewRuleset(httpClient, host, path)
}
func viewRuleset(httpClient *http.Client, hostname string, path string) (*shared.Ruleset, error) {
apiClient := api.NewClientFromHTTP(httpClient)
result := shared.Ruleset{}
err := apiClient.REST(hostname, "GET", path, nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}

View file

@ -0,0 +1,227 @@
package view
import (
"fmt"
"net/http"
"reflect"
"sort"
"strconv"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghinstance"
"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"
)
type ViewOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
Config func() (config.Config, error)
BaseRepo func() (ghrepo.Interface, error)
Browser browser.Browser
ID string
WebMode bool
Organization string
}
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
opts := &ViewOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
Browser: f.Browser,
Config: f.Config,
}
cmd := &cobra.Command{
Use: "view [<ruleset-id>]",
Short: "View information about a ruleset",
Long: heredoc.Doc(`
View information about a GitHub ruleset.
If no ID is provided, an interactive prompt will be used to choose
the ruleset to view.
`),
Example: heredoc.Doc(`
# View a ruleset in the current repository
$ gh ruleset view 43
# View a ruleset in a different repository
$ 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
if len(args) > 0 {
// a string is actually needed later on, so verify that it's numeric
// but use the string anyway
_, err := strconv.Atoi(args[0])
if err != nil {
return cmdutil.FlagErrorf("invalid value for ruleset ID: %v is not an integer", args[0])
}
opts.ID = args[0]
}
if runF != nil {
return runF(opts)
}
return viewRun(opts)
},
}
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 ")
return cmd
}
func viewRun(opts *ViewOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
repoI, err := opts.BaseRepo()
if err != nil {
return err
}
cfg, err := opts.Config()
if err != nil {
return err
}
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)
} else {
rs, err = viewRepoRuleset(httpClient, repoI, opts.ID)
}
if err != nil {
return err
}
cs := opts.IO.ColorScheme()
w := opts.IO.Out
fmt.Fprintf(w, "\n%s\n", cs.Bold(rs.Name))
fmt.Fprintf(w, "ID: %d\n", rs.Id)
switch rs.Enforcement {
case "disabled":
fmt.Fprintf(w, "%s\n", cs.Red("Disabled"))
case "evaluate":
fmt.Fprintf(w, "%s\n", cs.Yellow("Evaluate Mode (not being enforced)"))
case "active":
fmt.Fprintf(w, "%s\n", cs.Green("Active"))
default:
fmt.Fprintf(w, "Enforcement: %s\n", rs.Enforcement)
}
fmt.Fprintf(w, "\n%s\n", cs.Bold("Bypassing"))
fmt.Fprintf(w, "Mode: %s\n", rs.BypassMode)
if len(rs.BypassActors) == 0 {
fmt.Fprintf(w, "No actors configured for bypass\n")
} else {
types := make(map[string]int)
for _, t := range rs.BypassActors {
val, exists := types[t.ActorType]
if exists {
types[t.ActorType] = val + 1
} else {
types[t.ActorType] = 1
}
}
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, "\n%s\n", cs.Bold("Conditions"))
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
keys := make([]string, 0, len(rs.Conditions))
for key := range rs.Conditions {
keys = append(keys, key)
}
sort.Strings(keys)
for _, name := range keys {
condition := rs.Conditions[name]
fmt.Fprintf(w, "- %s: ", name)
// sort these keys too for consistency
subkeys := make([]string, 0, len(condition))
for subkey := range condition {
subkeys = append(subkeys, subkey)
}
sort.Strings(subkeys)
for _, n := range subkeys {
rawVal := condition[n]
k := reflect.TypeOf(rawVal).Kind()
if rawVal == nil ||
((k == reflect.Slice || k == reflect.Map) && len(rawVal.([]interface{})) == 0) {
continue
}
printVal := fmt.Sprint(rawVal)
// fmt.Fprintf(w, "n: %s, type: %s\n", n, reflect.TypeOf(rawVal).String())
// switch val := rawVal.(type) {
// case []interface{}:
// // currently only string arrays are returned by the API at this level
// printVal = fmt.Sprint(val)
// default:
// printVal = fmt.Sprint(val)
// }
fmt.Fprintf(w, "[%s: %s] ", n, printVal)
}
fmt.Fprint(w, "\n")
}
}
fmt.Fprintf(w, "\n%s\n", cs.Bold("Rules"))
fmt.Fprintf(w, "%d configured\n", reflect.ValueOf(rs.Rules).Len())
return nil
}