Merge branch 'trunk' into trunk

This commit is contained in:
Aryan Bhosale 2025-01-04 10:24:07 +05:30 committed by GitHub
commit 3fe6ba4e8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 792 additions and 17 deletions

View file

@ -68,9 +68,11 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
to writing the token to a plain text file. See %[1]sgh auth status%[1]s for its
stored location.
Alternatively, use %[1]s--with-token%[1]s to pass in a token on standard input.
Alternatively, use %[1]s--with-token%[1]s to pass in a personal access token (classic) on standard input.
The minimum required scopes for the token are: %[1]srepo%[1]s, %[1]sread:org%[1]s, and %[1]sgist%[1]s.
Fine-grained personal access tokens are not supported.
Alternatively, gh will use the authentication token found in environment variables.
This method is most suitable for "headless" use of gh such as in automation. See
%[1]sgh help environment%[1]s for more info.

View file

@ -0,0 +1,29 @@
package autolink
import (
"github.com/MakeNowJust/heredoc"
cmdList "github.com/cli/cli/v2/pkg/cmd/repo/autolink/list"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
)
func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "autolink <command>",
Short: "Manage autolink references",
Long: heredoc.Docf(`
Work with GitHub autolink references.
GitHub autolinks require admin access to configure and can be found at
https://github.com/{owner}/{repo}/settings/key_links.
Use %[1]sgh repo autolink list --web%[1]s to open this page for the current repository.
For more information about GitHub autolinks, see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/configuring-autolinks-to-reference-external-resources
`, "`"),
}
cmdutil.EnableRepoOverride(cmd, f)
cmd.AddCommand(cmdList.NewCmdList(f, nil))
return cmd
}

View file

@ -0,0 +1,43 @@
package list
import (
"encoding/json"
"fmt"
"net/http"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
)
type AutolinkLister struct {
HTTPClient *http.Client
}
func (a *AutolinkLister) List(repo ghrepo.Interface) ([]autolink, error) {
path := fmt.Sprintf("repos/%s/%s/autolinks", repo.RepoOwner(), repo.RepoName())
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
resp, err := a.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("error getting autolinks: HTTP 404: Perhaps you are missing admin rights to the repository? (https://api.github.com/%s)", path)
} else if resp.StatusCode > 299 {
return nil, api.HandleHTTPError(resp)
}
var autolinks []autolink
err = json.NewDecoder(resp.Body).Decode(&autolinks)
if err != nil {
return nil, err
}
return autolinks, nil
}

View file

@ -0,0 +1,75 @@
package list
import (
"fmt"
"net/http"
"testing"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAutoLinkLister_List(t *testing.T) {
tests := []struct {
name string
repo ghrepo.Interface
resp []autolink
status int
}{
{
name: "no autolinks",
repo: ghrepo.New("OWNER", "REPO"),
resp: []autolink{},
status: 200,
},
{
name: "two autolinks",
repo: ghrepo.New("OWNER", "REPO"),
resp: []autolink{
{
ID: 1,
IsAlphanumeric: true,
KeyPrefix: "key",
URLTemplate: "https://example.com",
},
{
ID: 2,
IsAlphanumeric: false,
KeyPrefix: "key2",
URLTemplate: "https://example2.com",
},
},
status: 200,
},
{
name: "http error",
repo: ghrepo.New("OWNER", "REPO"),
status: 404,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
reg.Register(
httpmock.REST("GET", fmt.Sprintf("repos/%s/%s/autolinks", tt.repo.RepoOwner(), tt.repo.RepoName())),
httpmock.StatusJSONResponse(tt.status, tt.resp),
)
defer reg.Verify(t)
autolinkLister := &AutolinkLister{
HTTPClient: &http.Client{Transport: reg},
}
autolinks, err := autolinkLister.List(tt.repo)
if tt.status == 404 {
require.Error(t, err)
assert.Equal(t, "error getting autolinks: HTTP 404: Perhaps you are missing admin rights to the repository? (https://api.github.com/repos/OWNER/REPO/autolinks)", err.Error())
} else {
require.NoError(t, err)
assert.Equal(t, tt.resp, autolinks)
}
})
}
}

View file

@ -0,0 +1,137 @@
package list
import (
"fmt"
"strconv"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"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/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
var autolinkFields = []string{
"id",
"isAlphanumeric",
"keyPrefix",
"urlTemplate",
}
type autolink struct {
ID int `json:"id"`
IsAlphanumeric bool `json:"is_alphanumeric"`
KeyPrefix string `json:"key_prefix"`
URLTemplate string `json:"url_template"`
}
func (s *autolink) ExportData(fields []string) map[string]interface{} {
return cmdutil.StructExportData(s, fields)
}
type listOptions struct {
BaseRepo func() (ghrepo.Interface, error)
Browser browser.Browser
AutolinkClient AutolinkClient
IO *iostreams.IOStreams
Exporter cmdutil.Exporter
WebMode bool
}
type AutolinkClient interface {
List(repo ghrepo.Interface) ([]autolink, error)
}
func NewCmdList(f *cmdutil.Factory, runF func(*listOptions) error) *cobra.Command {
opts := &listOptions{
Browser: f.Browser,
IO: f.IOStreams,
}
cmd := &cobra.Command{
Use: "list",
Short: "List autolink references for a GitHub repository",
Long: heredoc.Doc(`
Gets all autolink references that are configured for a repository.
Information about autolinks is only available to repository administrators.
`),
Aliases: []string{"ls"},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
opts.BaseRepo = f.BaseRepo
httpClient, err := f.HttpClient()
if err != nil {
return err
}
opts.AutolinkClient = &AutolinkLister{HTTPClient: httpClient}
if runF != nil {
return runF(opts)
}
return listRun(opts)
},
}
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List autolink references in the web browser")
cmdutil.AddJSONFlags(cmd, &opts.Exporter, autolinkFields)
return cmd
}
func listRun(opts *listOptions) error {
repo, err := opts.BaseRepo()
if err != nil {
return err
}
if opts.WebMode {
autolinksListURL := ghrepo.GenerateRepoURL(repo, "settings/key_links")
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(autolinksListURL))
}
return opts.Browser.Browse(autolinksListURL)
}
autolinks, err := opts.AutolinkClient.List(repo)
if err != nil {
return err
}
if len(autolinks) == 0 {
return cmdutil.NewNoResultsError(fmt.Sprintf("no autolinks found in %s", ghrepo.FullName(repo)))
}
if opts.Exporter != nil {
return opts.Exporter.Write(opts.IO, autolinks)
}
if opts.IO.IsStdoutTTY() {
title := listHeader(ghrepo.FullName(repo), len(autolinks))
fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title)
}
tp := tableprinter.New(opts.IO, tableprinter.WithHeader("ID", "KEY PREFIX", "URL TEMPLATE", "ALPHANUMERIC"))
for _, autolink := range autolinks {
tp.AddField(fmt.Sprintf("%d", autolink.ID))
tp.AddField(autolink.KeyPrefix)
tp.AddField(autolink.URLTemplate)
tp.AddField(strconv.FormatBool(autolink.IsAlphanumeric))
tp.EndRow()
}
return tp.Render()
}
func listHeader(repoName string, count int) string {
return fmt.Sprintf("Showing %s in %s", text.Pluralize(count, "autolink reference"), repoName)
}

View file

@ -0,0 +1,267 @@
package list
import (
"bytes"
"net/http"
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/jsonfieldstest"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJSONFields(t *testing.T) {
jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdList, []string{
"id",
"isAlphanumeric",
"keyPrefix",
"urlTemplate",
})
}
func TestNewCmdList(t *testing.T) {
tests := []struct {
name string
input string
output listOptions
wantErr bool
wantExporter bool
errMsg string
}{
{
name: "no argument",
input: "",
output: listOptions{},
},
{
name: "web flag",
input: "--web",
output: listOptions{WebMode: true},
},
{
name: "json flag",
input: "--json id",
output: listOptions{},
wantExporter: true,
},
{
name: "invalid json flag",
input: "--json invalid",
output: listOptions{},
wantErr: true,
errMsg: "Unknown JSON field: \"invalid\"\nAvailable fields:\n id\n isAlphanumeric\n keyPrefix\n urlTemplate",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: ios,
}
f.HttpClient = func() (*http.Client, error) {
return &http.Client{}, nil
}
argv, err := shlex.Split(tt.input)
require.NoError(t, err)
var gotOpts *listOptions
cmd := NewCmdList(f, func(opts *listOptions) error {
gotOpts = opts
return nil
})
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
if tt.wantErr {
require.EqualError(t, err, tt.errMsg)
} else {
require.NoError(t, err)
assert.Equal(t, tt.output.WebMode, gotOpts.WebMode)
assert.Equal(t, tt.wantExporter, gotOpts.Exporter != nil)
}
})
}
}
type stubAutoLinkLister struct {
autolinks []autolink
err error
}
func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]autolink, error) {
return g.autolinks, g.err
}
type testAutolinkClientListError struct{}
func (e testAutolinkClientListError) Error() string {
return "autolink client list error"
}
func TestListRun(t *testing.T) {
tests := []struct {
name string
opts *listOptions
isTTY bool
stubLister stubAutoLinkLister
expectedErr error
wantStdout string
wantStderr string
}{
{
name: "list tty",
opts: &listOptions{},
isTTY: true,
stubLister: stubAutoLinkLister{
autolinks: []autolink{
{
ID: 1,
KeyPrefix: "TICKET-",
URLTemplate: "https://example.com/TICKET?query=<num>",
IsAlphanumeric: true,
},
{
ID: 2,
KeyPrefix: "STORY-",
URLTemplate: "https://example.com/STORY?id=<num>",
IsAlphanumeric: false,
},
},
},
wantStdout: heredoc.Doc(`
Showing 2 autolink references in OWNER/REPO
ID KEY PREFIX URL TEMPLATE ALPHANUMERIC
1 TICKET- https://example.com/TICKET?query=<num> true
2 STORY- https://example.com/STORY?id=<num> false
`),
wantStderr: "",
},
{
name: "list json",
opts: &listOptions{
Exporter: func() cmdutil.Exporter {
exporter := cmdutil.NewJSONExporter()
exporter.SetFields([]string{"id"})
return exporter
}(),
},
isTTY: true,
stubLister: stubAutoLinkLister{
autolinks: []autolink{
{
ID: 1,
KeyPrefix: "TICKET-",
URLTemplate: "https://example.com/TICKET?query=<num>",
IsAlphanumeric: true,
},
{
ID: 2,
KeyPrefix: "STORY-",
URLTemplate: "https://example.com/STORY?id=<num>",
IsAlphanumeric: false,
},
},
},
wantStdout: "[{\"id\":1},{\"id\":2}]\n",
wantStderr: "",
},
{
name: "list non-tty",
opts: &listOptions{},
isTTY: false,
stubLister: stubAutoLinkLister{
autolinks: []autolink{
{
ID: 1,
KeyPrefix: "TICKET-",
URLTemplate: "https://example.com/TICKET?query=<num>",
IsAlphanumeric: true,
},
{
ID: 2,
KeyPrefix: "STORY-",
URLTemplate: "https://example.com/STORY?id=<num>",
IsAlphanumeric: false,
},
},
},
wantStdout: heredoc.Doc(`
1 TICKET- https://example.com/TICKET?query=<num> true
2 STORY- https://example.com/STORY?id=<num> false
`),
wantStderr: "",
},
{
name: "no results",
opts: &listOptions{},
isTTY: true,
stubLister: stubAutoLinkLister{
autolinks: []autolink{},
},
expectedErr: cmdutil.NewNoResultsError("no autolinks found in OWNER/REPO"),
wantStderr: "",
},
{
name: "client error",
opts: &listOptions{},
isTTY: true,
stubLister: stubAutoLinkLister{
autolinks: []autolink{},
err: testAutolinkClientListError{},
},
expectedErr: testAutolinkClientListError{},
wantStderr: "",
},
{
name: "web mode",
isTTY: true,
opts: &listOptions{WebMode: true},
wantStderr: "Opening https://github.com/OWNER/REPO/settings/key_links in your browser.\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(tt.isTTY)
ios.SetStdinTTY(tt.isTTY)
ios.SetStderrTTY(tt.isTTY)
opts := tt.opts
opts.IO = ios
opts.Browser = &browser.Stub{}
opts.IO = ios
opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
opts.AutolinkClient = &tt.stubLister
err := listRun(opts)
if tt.expectedErr != nil {
require.Error(t, err)
require.ErrorIs(t, err, tt.expectedErr)
} else {
require.NoError(t, err)
assert.Equal(t, tt.wantStdout, stdout.String())
}
if tt.wantStderr != "" {
assert.Equal(t, tt.wantStderr, stderr.String())
}
})
}
}

View file

@ -99,6 +99,8 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
For language or platform .gitignore templates to use with %[1]s--gitignore%[1]s, <https://github.com/github/gitignore>.
For license keywords to use with %[1]s--license%[1]s, run %[1]sgh repo license list%[1]s or visit <https://choosealicense.com>.
The repo is created with the configured repository default branch, see <https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-user-account-settings/managing-the-default-branch-name-for-your-repositories>.
`, "`"),
Example: heredoc.Doc(`
# create a repository interactively

View file

@ -66,22 +66,27 @@ type EditOptions struct {
}
type EditRepositoryInput struct {
AllowForking *bool `json:"allow_forking,omitempty"`
AllowUpdateBranch *bool `json:"allow_update_branch,omitempty"`
DefaultBranch *string `json:"default_branch,omitempty"`
DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"`
Description *string `json:"description,omitempty"`
EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"`
EnableIssues *bool `json:"has_issues,omitempty"`
EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"`
EnableProjects *bool `json:"has_projects,omitempty"`
EnableDiscussions *bool `json:"has_discussions,omitempty"`
EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"`
EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"`
EnableWiki *bool `json:"has_wiki,omitempty"`
Homepage *string `json:"homepage,omitempty"`
IsTemplate *bool `json:"is_template,omitempty"`
Visibility *string `json:"visibility,omitempty"`
enableAdvancedSecurity *bool
enableSecretScanning *bool
enableSecretScanningPushProtection *bool
AllowForking *bool `json:"allow_forking,omitempty"`
AllowUpdateBranch *bool `json:"allow_update_branch,omitempty"`
DefaultBranch *string `json:"default_branch,omitempty"`
DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"`
Description *string `json:"description,omitempty"`
EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"`
EnableIssues *bool `json:"has_issues,omitempty"`
EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"`
EnableProjects *bool `json:"has_projects,omitempty"`
EnableDiscussions *bool `json:"has_discussions,omitempty"`
EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"`
EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"`
EnableWiki *bool `json:"has_wiki,omitempty"`
Homepage *string `json:"homepage,omitempty"`
IsTemplate *bool `json:"is_template,omitempty"`
SecurityAndAnalysis *SecurityAndAnalysisInput `json:"security_and_analysis,omitempty"`
Visibility *string `json:"visibility,omitempty"`
}
func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobra.Command {
@ -157,6 +162,10 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
return cmdutil.FlagErrorf("use of --visibility flag requires --accept-visibility-change-consequences flag")
}
if hasSecurityEdits(opts.Edits) {
opts.Edits.SecurityAndAnalysis = transformSecurityAndAnalysisOpts(opts)
}
if runF != nil {
return runF(opts)
}
@ -177,6 +186,9 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableSquashMerge, "enable-squash-merge", "", "Enable merging pull requests via squashed commit")
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableRebaseMerge, "enable-rebase-merge", "", "Enable merging pull requests via rebase")
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableAutoMerge, "enable-auto-merge", "", "Enable auto-merge functionality")
cmdutil.NilBoolFlag(cmd, &opts.Edits.enableAdvancedSecurity, "enable-advanced-security", "", "Enable advanced security in the repository")
cmdutil.NilBoolFlag(cmd, &opts.Edits.enableSecretScanning, "enable-secret-scanning", "", "Enable secret scanning in the repository")
cmdutil.NilBoolFlag(cmd, &opts.Edits.enableSecretScanningPushProtection, "enable-secret-scanning-push-protection", "", "Enable secret scanning push protection in the repository. Secret scanning must be enabled first")
cmdutil.NilBoolFlag(cmd, &opts.Edits.DeleteBranchOnMerge, "delete-branch-on-merge", "", "Delete head branch when pull requests are merged")
cmdutil.NilBoolFlag(cmd, &opts.Edits.AllowForking, "allow-forking", "", "Allow forking of an organization repository")
cmdutil.NilBoolFlag(cmd, &opts.Edits.AllowUpdateBranch, "allow-update-branch", "", "Allow a pull request head branch that is behind its base branch to be updated")
@ -240,6 +252,17 @@ func editRun(ctx context.Context, opts *EditOptions) error {
}
}
if opts.Edits.SecurityAndAnalysis != nil {
apiClient := api.NewClientFromHTTP(opts.HTTPClient)
repo, err := api.FetchRepository(apiClient, opts.Repository, []string{"viewerCanAdminister"})
if err != nil {
return err
}
if !repo.ViewerCanAdminister {
return fmt.Errorf("you do not have sufficient permissions to edit repository security and analysis features")
}
}
apiPath := fmt.Sprintf("repos/%s/%s", repo.RepoOwner(), repo.RepoName())
body := &bytes.Buffer{}
@ -560,3 +583,49 @@ func isIncluded(value string, opts []string) bool {
}
return false
}
func boolToStatus(status bool) *string {
var result string
if status {
result = "enabled"
} else {
result = "disabled"
}
return &result
}
func hasSecurityEdits(edits EditRepositoryInput) bool {
return edits.enableAdvancedSecurity != nil || edits.enableSecretScanning != nil || edits.enableSecretScanningPushProtection != nil
}
type SecurityAndAnalysisInput struct {
EnableAdvancedSecurity *SecurityAndAnalysisStatus `json:"advanced_security,omitempty"`
EnableSecretScanning *SecurityAndAnalysisStatus `json:"secret_scanning,omitempty"`
EnableSecretScanningPushProtection *SecurityAndAnalysisStatus `json:"secret_scanning_push_protection,omitempty"`
}
type SecurityAndAnalysisStatus struct {
Status *string `json:"status,omitempty"`
}
// Transform security and analysis parameters to properly serialize EditRepositoryInput
// See API Docs: https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#update-a-repository
func transformSecurityAndAnalysisOpts(opts *EditOptions) *SecurityAndAnalysisInput {
securityOptions := &SecurityAndAnalysisInput{}
if opts.Edits.enableAdvancedSecurity != nil {
securityOptions.EnableAdvancedSecurity = &SecurityAndAnalysisStatus{
Status: boolToStatus(*opts.Edits.enableAdvancedSecurity),
}
}
if opts.Edits.enableSecretScanning != nil {
securityOptions.EnableSecretScanning = &SecurityAndAnalysisStatus{
Status: boolToStatus(*opts.Edits.enableSecretScanning),
}
}
if opts.Edits.enableSecretScanningPushProtection != nil {
securityOptions.EnableSecretScanningPushProtection = &SecurityAndAnalysisStatus{
Status: boolToStatus(*opts.Edits.enableSecretScanningPushProtection),
}
}
return securityOptions
}

View file

@ -201,6 +201,65 @@ func Test_editRun(t *testing.T) {
}))
},
},
{
name: "enable/disable security and analysis settings",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{
SecurityAndAnalysis: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanning: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
},
},
},
httpStubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`{"data": { "repository": { "viewerCanAdminister": true } } }`))
r.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO"),
httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
assert.Equal(t, 1, len(payload))
securityAndAnalysis := payload["security_and_analysis"].(map[string]interface{})
assert.Equal(t, "enabled", securityAndAnalysis["advanced_security"].(map[string]interface{})["status"])
assert.Equal(t, "enabled", securityAndAnalysis["secret_scanning"].(map[string]interface{})["status"])
assert.Equal(t, "disabled", securityAndAnalysis["secret_scanning_push_protection"].(map[string]interface{})["status"])
}))
},
},
{
name: "does not have sufficient permissions for security edits",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{
SecurityAndAnalysis: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanning: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
},
},
},
httpStubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`{"data": { "repository": { "viewerCanAdminister": false } } }`))
},
wantsErr: "you do not have sufficient permissions to edit repository security and analysis features",
},
}
for _, tt := range tests {
@ -670,6 +729,95 @@ func Test_editRun_interactive(t *testing.T) {
}
}
func Test_transformSecurityAndAnalysisOpts(t *testing.T) {
tests := []struct {
name string
opts EditOptions
want *SecurityAndAnalysisInput
}{
{
name: "Enable all security and analysis settings",
opts: EditOptions{
Edits: EditRepositoryInput{
enableAdvancedSecurity: bp(true),
enableSecretScanning: bp(true),
enableSecretScanningPushProtection: bp(true),
},
},
want: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanning: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
},
},
{
name: "Disable all security and analysis settings",
opts: EditOptions{
Edits: EditRepositoryInput{
enableAdvancedSecurity: bp(false),
enableSecretScanning: bp(false),
enableSecretScanningPushProtection: bp(false),
},
},
want: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
EnableSecretScanning: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
EnableSecretScanningPushProtection: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
},
},
{
name: "Enable only advanced security",
opts: EditOptions{
Edits: EditRepositoryInput{
enableAdvancedSecurity: bp(true),
},
},
want: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: &SecurityAndAnalysisStatus{
Status: sp("enabled"),
},
EnableSecretScanning: nil,
EnableSecretScanningPushProtection: nil,
},
},
{
name: "Disable only secret scanning",
opts: EditOptions{
Edits: EditRepositoryInput{
enableSecretScanning: bp(false),
},
},
want: &SecurityAndAnalysisInput{
EnableAdvancedSecurity: nil,
EnableSecretScanning: &SecurityAndAnalysisStatus{
Status: sp("disabled"),
},
EnableSecretScanningPushProtection: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := &tt.opts
transformed := transformSecurityAndAnalysisOpts(opts)
assert.Equal(t, tt.want, transformed)
})
}
}
func sp(v string) *string {
return &v
}

View file

@ -3,6 +3,7 @@ package repo
import (
"github.com/MakeNowJust/heredoc"
repoArchiveCmd "github.com/cli/cli/v2/pkg/cmd/repo/archive"
repoAutolinkCmd "github.com/cli/cli/v2/pkg/cmd/repo/autolink"
repoCloneCmd "github.com/cli/cli/v2/pkg/cmd/repo/clone"
repoCreateCmd "github.com/cli/cli/v2/pkg/cmd/repo/create"
creditsCmd "github.com/cli/cli/v2/pkg/cmd/repo/credits"
@ -19,6 +20,7 @@ import (
repoSyncCmd "github.com/cli/cli/v2/pkg/cmd/repo/sync"
repoUnarchiveCmd "github.com/cli/cli/v2/pkg/cmd/repo/unarchive"
repoViewCmd "github.com/cli/cli/v2/pkg/cmd/repo/view"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
)
@ -64,6 +66,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command {
repoDeleteCmd.NewCmdDelete(f, nil),
creditsCmd.NewCmdRepoCredits(f, nil),
gardenCmd.NewCmdGarden(f, nil),
repoAutolinkCmd.NewCmdAutolink(f),
)
return cmd