gh repo edit

Co-authored-by: Mislav Marohnić <mislav@github.com>
This commit is contained in:
Gowtham Munukutla 2021-06-19 15:39:44 +05:30 committed by Mislav Marohnić
parent 0e3c3bb4a4
commit 98217fc38c
4 changed files with 337 additions and 0 deletions

118
pkg/cmd/repo/edit/edit.go Normal file
View file

@ -0,0 +1,118 @@
package edit
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
)
type EditOptions struct {
HTTPClient *http.Client
Repository ghrepo.Interface
Edits EditRepositoryInput
}
type EditRepositoryInput struct {
Description *string `json:"description,omitempty"`
Homepage *string `json:"homepage,omitempty"`
Visibility *string `json:"visibility,omitempty"`
EnableIssues *bool `json:"has_issues,omitempty"`
EnableProjects *bool `json:"has_projects,omitempty"`
EnableWiki *bool `json:"has_wiki,omitempty"`
IsTemplate *bool `json:"is_template,omitempty"`
DefaultBranch *string `json:"default_branch,omitempty"`
EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"`
EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"`
EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"`
EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"`
DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"`
AllowForking *bool `json:"allow_forking,omitempty"`
}
func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobra.Command {
opts := &EditOptions{}
cmd := &cobra.Command{
Use: "edit [<repository>]",
Short: "Edit repository settings",
Annotations: map[string]string{
"help:arguments": heredoc.Doc(`
A repository can be supplied as an argument in any of the following formats:
- "OWNER/REPO"
- by URL, e.g. "https://github.com/OWNER/REPO"
`),
},
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NFlag() == 0 {
return cmdutil.FlagErrorf("at least one flag is required")
}
if len(args) > 0 {
var err error
opts.Repository, err = ghrepo.FromFullName(args[0])
if err != nil {
return err
}
} else {
var err error
opts.Repository, err = f.BaseRepo()
if err != nil {
return err
}
}
if httpClient, err := f.HttpClient(); err == nil {
opts.HTTPClient = httpClient
} else {
return err
}
if runF != nil {
return runF(opts)
}
return editRun(opts)
},
}
cmdutil.NilStringFlag(cmd, &opts.Edits.Description, "description", "d", "Description of the repository")
cmdutil.NilStringFlag(cmd, &opts.Edits.Homepage, "homepage", "h", "Repository home page `URL`")
cmdutil.NilStringFlag(cmd, &opts.Edits.DefaultBranch, "default-branch", "", "Set the default branch `name` for the repository")
cmdutil.NilStringFlag(cmd, &opts.Edits.Visibility, "visibility", "", "Change the visibility of the repository to {public,private,internal}")
cmdutil.NilBoolFlag(cmd, &opts.Edits.IsTemplate, "template", "", "Make the repository available as a template repository")
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableIssues, "enable-issues", "", "Enable issues in the repository")
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableProjects, "enable-projects", "", "Enable projects in the repository")
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableWiki, "enable-wiki", "", "Enable wiki in the repository")
cmdutil.NilBoolFlag(cmd, &opts.Edits.EnableMergeCommit, "enable-merge-commit", "", "Enable merging pull requests via merge commit")
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.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")
return cmd
}
func editRun(opts *EditOptions) error {
repo := opts.Repository
input := opts.Edits
apiPath := fmt.Sprintf("repos/%s/%s", repo.RepoOwner(), repo.RepoName())
body := &bytes.Buffer{}
enc := json.NewEncoder(body)
if err := enc.Encode(input); err != nil {
return err
}
apiClient := api.NewClientFromHTTP(opts.HTTPClient)
_, err := api.CreateRepoTransformToV4(apiClient, repo.RepoHost(), "PATCH", apiPath, body)
return err
}

View file

@ -0,0 +1,140 @@
package edit
import (
"bytes"
"io/ioutil"
"net/http"
"testing"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCmdEdit(t *testing.T) {
tests := []struct {
name string
args string
wantOpts EditOptions
wantErr string
}{
{
name: "no argument",
args: "",
wantErr: "at least one flag is required",
},
{
name: "change repo description",
args: "--description hello",
wantOpts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{
Description: sp("hello"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
io.SetStdoutTTY(true)
io.SetStdinTTY(true)
io.SetStderrTTY(true)
f := &cmdutil.Factory{
IOStreams: io,
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
HttpClient: func() (*http.Client, error) {
return nil, nil
},
}
argv, err := shlex.Split(tt.args)
assert.NoError(t, err)
var gotOpts *EditOptions
cmd := NewCmdEdit(f, func(opts *EditOptions) error {
gotOpts = opts
return nil
})
cmd.Flags().BoolP("help", "x", false, "")
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(ioutil.Discard)
cmd.SetErr(ioutil.Discard)
_, err = cmd.ExecuteC()
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, ghrepo.FullName(tt.wantOpts.Repository), ghrepo.FullName(gotOpts.Repository))
assert.Equal(t, tt.wantOpts.Edits, gotOpts.Edits)
})
}
}
func Test_editRun(t *testing.T) {
tests := []struct {
name string
opts EditOptions
httpStubs func(*testing.T, *httpmock.Registry)
wantsStderr string
wantsErr string
}{
{
name: "change name and description",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
Edits: EditRepositoryInput{
Homepage: sp("newURL"),
Description: sp("hello world!"),
},
},
httpStubs: func(t *testing.T, r *httpmock.Registry) {
r.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO"),
httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
assert.Equal(t, 2, len(payload))
assert.Equal(t, "newURL", payload["homepage"])
assert.Equal(t, "hello world!", payload["description"])
}))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)
if tt.httpStubs != nil {
tt.httpStubs(t, httpReg)
}
opts := &tt.opts
opts.HTTPClient = &http.Client{Transport: httpReg}
err := editRun(opts)
if tt.wantsErr == "" {
require.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantsErr)
return
}
})
}
}
func sp(v string) *string {
return &v
}

View file

@ -7,6 +7,7 @@ import (
repoCreateCmd "github.com/cli/cli/v2/pkg/cmd/repo/create"
creditsCmd "github.com/cli/cli/v2/pkg/cmd/repo/credits"
repoDeleteCmd "github.com/cli/cli/v2/pkg/cmd/repo/delete"
repoEditCmd "github.com/cli/cli/v2/pkg/cmd/repo/edit"
repoForkCmd "github.com/cli/cli/v2/pkg/cmd/repo/fork"
gardenCmd "github.com/cli/cli/v2/pkg/cmd/repo/garden"
repoListCmd "github.com/cli/cli/v2/pkg/cmd/repo/list"
@ -41,6 +42,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(repoForkCmd.NewCmdFork(f, nil))
cmd.AddCommand(repoCloneCmd.NewCmdClone(f, nil))
cmd.AddCommand(repoCreateCmd.NewCmdCreate(f, nil))
cmd.AddCommand(repoEditCmd.NewCmdEdit(f, nil))
cmd.AddCommand(repoListCmd.NewCmdList(f, nil))
cmd.AddCommand(repoSyncCmd.NewCmdSync(f, nil))
cmd.AddCommand(creditsCmd.NewCmdRepoCredits(f, nil))

77
pkg/cmdutil/flags.go Normal file
View file

@ -0,0 +1,77 @@
package cmdutil
import (
"strconv"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// NilStringFlag defines a new flag with a string pointer receiver. This is useful for differentiating
// between the flag being set to a blank value and the flag not being passed at all.
func NilStringFlag(cmd *cobra.Command, p **string, name string, shorthand string, usage string) *pflag.Flag {
return cmd.Flags().VarPF(newStringValue(p), name, shorthand, usage)
}
// NilBoolFlag defines a new flag with a bool pointer receiver. This is useful for differentiating
// between the flag being explicitly set to a false value and the flag not being passed at all.
func NilBoolFlag(cmd *cobra.Command, p **bool, name string, shorthand string, usage string) *pflag.Flag {
f := cmd.Flags().VarPF(newBoolValue(p), name, shorthand, usage)
f.NoOptDefVal = "true"
return f
}
type stringValue struct {
string **string
}
func newStringValue(p **string) *stringValue {
return &stringValue{p}
}
func (s *stringValue) Set(value string) error {
*s.string = &value
return nil
}
func (s *stringValue) String() string {
if s.string == nil || *s.string == nil {
return ""
}
return **s.string
}
func (s *stringValue) Type() string {
return "string"
}
type boolValue struct {
bool **bool
}
func newBoolValue(p **bool) *boolValue {
return &boolValue{p}
}
func (b *boolValue) Set(value string) error {
v, err := strconv.ParseBool(value)
*b.bool = &v
return err
}
func (b *boolValue) String() string {
if b.bool == nil || *b.bool == nil {
return "false"
} else if **b.bool {
return "true"
}
return "false"
}
func (b *boolValue) Type() string {
return "bool"
}
func (b *boolValue) IsBoolFlag() bool {
return true
}