basic ruleset view works
This commit is contained in:
parent
5155844d7f
commit
7f81645c78
6 changed files with 319 additions and 46 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
28
pkg/cmd/ruleset/shared/shared.go
Normal file
28
pkg/cmd/ruleset/shared/shared.go
Normal 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{}
|
||||
}
|
||||
32
pkg/cmd/ruleset/view/http.go
Normal file
32
pkg/cmd/ruleset/view/http.go
Normal 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
|
||||
}
|
||||
227
pkg/cmd/ruleset/view/view.go
Normal file
227
pkg/cmd/ruleset/view/view.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue