Merge branch 'trunk' into repo-rename

This commit is contained in:
Parth Patel 2021-10-12 18:47:52 -04:00
commit 3a936f65e2
24 changed files with 564 additions and 186 deletions

View file

@ -35,7 +35,7 @@ func NewApp(logger *output.Logger, apiClient apiClient) *App {
type apiClient interface {
GetUser(ctx context.Context) (*api.User, error)
GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
ListCodespaces(ctx context.Context) ([]*api.Codespace, error)
ListCodespaces(ctx context.Context, limit int) ([]*api.Codespace, error)
DeleteCodespace(ctx context.Context, name string) error
StartCodespace(ctx context.Context, name string) error
CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error)
@ -49,7 +49,7 @@ type apiClient interface {
var errNoCodespaces = errors.New("you have no codespaces")
func chooseCodespace(ctx context.Context, apiClient apiClient) (*api.Codespace, error) {
codespaces, err := apiClient.ListCodespaces(ctx)
codespaces, err := apiClient.ListCodespaces(ctx, -1)
if err != nil {
return nil, fmt.Errorf("error getting codespaces: %w", err)
}

View file

@ -62,7 +62,7 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
var codespaces []*api.Codespace
nameFilter := opts.codespaceName
if nameFilter == "" {
codespaces, err = a.apiClient.ListCodespaces(ctx)
codespaces, err = a.apiClient.ListCodespaces(ctx, -1)
if err != nil {
return fmt.Errorf("error getting codespaces: %w", err)
}

View file

@ -164,7 +164,7 @@ func TestDelete(t *testing.T) {
},
}
if tt.opts.codespaceName == "" {
apiMock.ListCodespacesFunc = func(_ context.Context) ([]*api.Codespace, error) {
apiMock.ListCodespacesFunc = func(_ context.Context, num int) ([]*api.Codespace, error) {
return tt.codespaces, nil
}
} else {

View file

@ -6,28 +6,35 @@ import (
"os"
"github.com/cli/cli/v2/pkg/cmd/codespace/output"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
)
func newListCmd(app *App) *cobra.Command {
var asJSON bool
var limit int
listCmd := &cobra.Command{
Use: "list",
Short: "List your codespaces",
Args: noArgsConstraint,
RunE: func(cmd *cobra.Command, args []string) error {
return app.List(cmd.Context(), asJSON)
if limit < 1 {
return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", limit)}
}
return app.List(cmd.Context(), asJSON, limit)
},
}
listCmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
listCmd.Flags().IntVarP(&limit, "limit", "L", 30, "Maximum number of codespaces to list")
return listCmd
}
func (a *App) List(ctx context.Context, asJSON bool) error {
codespaces, err := a.apiClient.ListCodespaces(ctx)
func (a *App) List(ctx context.Context, asJSON bool, limit int) error {
codespaces, err := a.apiClient.ListCodespaces(ctx, limit)
if err != nil {
return fmt.Errorf("error getting codespaces: %w", err)
}

View file

@ -43,7 +43,7 @@ import (
// GetUserFunc: func(ctx context.Context) (*api.User, error) {
// panic("mock out the GetUser method")
// },
// ListCodespacesFunc: func(ctx context.Context) ([]*api.Codespace, error) {
// ListCodespacesFunc: func(ctx context.Context, limit int) ([]*api.Codespace, error) {
// panic("mock out the ListCodespaces method")
// },
// StartCodespaceFunc: func(ctx context.Context, name string) error {
@ -84,7 +84,7 @@ type apiClientMock struct {
GetUserFunc func(ctx context.Context) (*api.User, error)
// ListCodespacesFunc mocks the ListCodespaces method.
ListCodespacesFunc func(ctx context.Context) ([]*api.Codespace, error)
ListCodespacesFunc func(ctx context.Context, limit int) ([]*api.Codespace, error)
// StartCodespaceFunc mocks the StartCodespace method.
StartCodespaceFunc func(ctx context.Context, name string) error
@ -162,6 +162,8 @@ type apiClientMock struct {
ListCodespaces []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Limit is the limit argument value.
Limit int
}
// StartCodespace holds details about calls to the StartCodespace method.
StartCodespace []struct {
@ -508,29 +510,33 @@ func (mock *apiClientMock) GetUserCalls() []struct {
}
// ListCodespaces calls ListCodespacesFunc.
func (mock *apiClientMock) ListCodespaces(ctx context.Context) ([]*api.Codespace, error) {
func (mock *apiClientMock) ListCodespaces(ctx context.Context, limit int) ([]*api.Codespace, error) {
if mock.ListCodespacesFunc == nil {
panic("apiClientMock.ListCodespacesFunc: method is nil but apiClient.ListCodespaces was just called")
}
callInfo := struct {
Ctx context.Context
Ctx context.Context
Limit int
}{
Ctx: ctx,
Ctx: ctx,
Limit: limit,
}
mock.lockListCodespaces.Lock()
mock.calls.ListCodespaces = append(mock.calls.ListCodespaces, callInfo)
mock.lockListCodespaces.Unlock()
return mock.ListCodespacesFunc(ctx)
return mock.ListCodespacesFunc(ctx, limit)
}
// ListCodespacesCalls gets all the calls that were made to ListCodespaces.
// Check the length with:
// len(mockedapiClient.ListCodespacesCalls())
func (mock *apiClientMock) ListCodespacesCalls() []struct {
Ctx context.Context
Ctx context.Context
Limit int
} {
var calls []struct {
Ctx context.Context
Ctx context.Context
Limit int
}
mock.lockListCodespaces.RLock()
calls = mock.calls.ListCodespaces

View file

@ -46,6 +46,7 @@ func NewManager(io *iostreams.IOStreams) *Manager {
platform: func() string {
return fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH)
},
io: io,
}
}

View file

@ -11,6 +11,8 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -25,6 +27,7 @@ type DiffOptions struct {
SelectorArg string
UseColor string
Patch bool
}
func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Command {
@ -70,6 +73,7 @@ func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Comman
}
cmd.Flags().StringVar(&opts.UseColor, "color", "auto", "Use color in diff output: {always|never|auto}")
cmd.Flags().BoolVar(&opts.Patch, "patch", false, "Display diff in patch format")
return cmd
}
@ -88,9 +92,8 @@ func diffRun(opts *DiffOptions) error {
if err != nil {
return err
}
apiClient := api.NewClientFromHTTP(httpClient)
diff, err := apiClient.PullRequestDiff(baseRepo, pr.Number)
diff, err := fetchDiff(httpClient, baseRepo, pr.Number, opts.Patch)
if err != nil {
return fmt.Errorf("could not find pull request diff: %w", err)
}
@ -132,6 +135,36 @@ func diffRun(opts *DiffOptions) error {
return nil
}
func fetchDiff(httpClient *http.Client, baseRepo ghrepo.Interface, prNumber int, asPatch bool) (io.ReadCloser, error) {
url := fmt.Sprintf(
"%srepos/%s/pulls/%d",
ghinstance.RESTPrefix(baseRepo.RepoHost()),
ghrepo.FullName(baseRepo),
prNumber,
)
acceptType := "application/vnd.github.v3.diff"
if asPatch {
acceptType = "application/vnd.github.v3.patch"
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", acceptType)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, api.HandleHTTPError(resp)
}
return resp.Body, nil
}
var diffHeaderPrefixes = []string{"+++", "---", "diff", "index"}
func isHeaderLine(dl string) bool {

View file

@ -4,6 +4,7 @@ import (
"bytes"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/cli/cli/v2/api"
@ -140,26 +141,67 @@ func runCommand(rt http.RoundTripper, remotes context.Remotes, isTTY bool, cli s
}, err
}
func TestPRDiff_notty(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
func TestPRDiff_notty_diff(t *testing.T) {
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)
shared.RunCommandFinder("", &api.PullRequest{Number: 123}, ghrepo.New("OWNER", "REPO"))
http.Register(
var gotAccept string
httpReg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/pulls/123"),
httpmock.StringResponse(testDiff))
func(req *http.Request) (*http.Response, error) {
gotAccept = req.Header.Get("Accept")
return &http.Response{
StatusCode: 200,
Request: req,
Body: ioutil.NopCloser(strings.NewReader(testDiff)),
}, nil
})
output, err := runCommand(http, nil, false, "")
output, err := runCommand(httpReg, nil, false, "")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if diff := cmp.Diff(testDiff, output.String()); diff != "" {
t.Errorf("command output did not match:\n%s", diff)
}
if gotAccept != "application/vnd.github.v3.diff" {
t.Errorf("unexpected Accept header: %s", gotAccept)
}
}
func TestPRDiff_tty(t *testing.T) {
func TestPRDiff_notty_patch(t *testing.T) {
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)
shared.RunCommandFinder("", &api.PullRequest{Number: 123}, ghrepo.New("OWNER", "REPO"))
var gotAccept string
httpReg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/pulls/123"),
func(req *http.Request) (*http.Response, error) {
gotAccept = req.Header.Get("Accept")
return &http.Response{
StatusCode: 200,
Request: req,
Body: ioutil.NopCloser(strings.NewReader(testDiff)),
}, nil
})
output, err := runCommand(httpReg, nil, false, "--patch")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if diff := cmp.Diff(testDiff, output.String()); diff != "" {
t.Errorf("command output did not match:\n%s", diff)
}
if gotAccept != "application/vnd.github.v3.patch" {
t.Errorf("unexpected Accept header: %s", gotAccept)
}
}
func TestPRDiff_tty_diff(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)

View file

@ -0,0 +1,99 @@
package archive
import (
"fmt"
"net/http"
"strings"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
type ArchiveOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
RepoArg string
}
func NewCmdArchive(f *cmdutil.Factory, runF func(*ArchiveOptions) error) *cobra.Command {
opts := &ArchiveOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
}
cmd := &cobra.Command{
DisableFlagsInUseLine: true,
Use: "archive <repository>",
Short: "Archive a repository",
Long: "Archive a GitHub repository.",
Args: cmdutil.ExactArgs(1, "cannot archive: repository argument required"),
RunE: func(cmd *cobra.Command, args []string) error {
opts.RepoArg = args[0]
if runF != nil {
return runF(opts)
}
return archiveRun(opts)
},
}
return cmd
}
func archiveRun(opts *ArchiveOptions) error {
cs := opts.IO.ColorScheme()
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
apiClient := api.NewClientFromHTTP(httpClient)
var toArchive ghrepo.Interface
archiveURL := opts.RepoArg
if !strings.Contains(archiveURL, "/") {
currentUser, err := api.CurrentLoginName(apiClient, ghinstance.Default())
if err != nil {
return err
}
archiveURL = currentUser + "/" + archiveURL
}
toArchive, err = ghrepo.FromFullName(archiveURL)
if err != nil {
return fmt.Errorf("argument error: %w", err)
}
fields := []string{"name", "owner", "isArchived", "id"}
repo, err := api.FetchRepository(apiClient, toArchive, fields)
if err != nil {
return err
}
fullName := ghrepo.FullName(toArchive)
if repo.IsArchived {
fmt.Fprintf(opts.IO.ErrOut,
"%s Repository %s is already archived\n",
cs.WarningIcon(),
fullName)
return nil
}
err = archiveRepo(httpClient, repo)
if err != nil {
return fmt.Errorf("API called failed: %w", err)
}
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.Out,
"%s Archived repository %s\n",
cs.SuccessIcon(),
fullName)
}
return nil
}

View file

@ -0,0 +1,165 @@
package archive
import (
"bytes"
"net/http"
"testing"
"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"
)
// probably redundant
func TestNewCmdArchive(t *testing.T) {
tests := []struct {
name string
input string
tty bool
output ArchiveOptions
wantErr bool
errMsg string
}{
{
name: "valid repo",
input: "cli/cli",
tty: true,
output: ArchiveOptions{
RepoArg: "cli/cli",
},
},
{
name: "no argument",
input: "",
wantErr: true,
tty: true,
output: ArchiveOptions{
RepoArg: "",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
io.SetStdinTTY(tt.tty)
io.SetStdoutTTY(tt.tty)
f := &cmdutil.Factory{
IOStreams: io,
}
argv, err := shlex.Split(tt.input)
assert.NoError(t, err)
var gotOpts *ArchiveOptions
cmd := NewCmdArchive(f, func(opts *ArchiveOptions) 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 {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.output.RepoArg, gotOpts.RepoArg)
})
}
}
func Test_ArchiveRun(t *testing.T) {
tests := []struct {
name string
opts ArchiveOptions
httpStubs func(*httpmock.Registry)
isTTY bool
wantStdout string
wantStderr string
}{
{
name: "unarchived repo tty",
opts: ArchiveOptions{RepoArg: "OWNER/REPO"},
wantStdout: "✓ Archived repository OWNER/REPO\n",
isTTY: true,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`{ "data": { "repository": {
"id": "THE-ID",
"isArchived": false} } }`))
reg.Register(
httpmock.GraphQL(`mutation ArchiveRepository\b`),
httpmock.StringResponse(`{}`))
},
},
{
name: "unarchived repo notty",
opts: ArchiveOptions{RepoArg: "OWNER/REPO"},
isTTY: false,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`{ "data": { "repository": {
"id": "THE-ID",
"isArchived": false} } }`))
reg.Register(
httpmock.GraphQL(`mutation ArchiveRepository\b`),
httpmock.StringResponse(`{}`))
},
},
{
name: "archived repo tty",
opts: ArchiveOptions{RepoArg: "OWNER/REPO"},
wantStderr: "! Repository OWNER/REPO is already archived\n",
isTTY: true,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`{ "data": { "repository": {
"id": "THE-ID",
"isArchived": true } } }`))
},
},
{
name: "archived repo notty",
opts: ArchiveOptions{RepoArg: "OWNER/REPO"},
isTTY: false,
wantStderr: "! Repository OWNER/REPO is already archived\n",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`{ "data": { "repository": {
"id": "THE-ID",
"isArchived": true } } }`))
},
},
}
for _, tt := range tests {
reg := &httpmock.Registry{}
if tt.httpStubs != nil {
tt.httpStubs(reg)
}
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
io, _, stdout, stderr := iostreams.Test()
tt.opts.IO = io
t.Run(tt.name, func(t *testing.T) {
defer reg.Verify(t)
io.SetStdoutTTY(tt.isTTY)
io.SetStderrTTY(tt.isTTY)
err := archiveRun(&tt.opts)
assert.NoError(t, err)
assert.Equal(t, tt.wantStdout, stdout.String())
assert.Equal(t, tt.wantStderr, stderr.String())
})
}
}

View file

@ -0,0 +1,32 @@
package archive
import (
"context"
"net/http"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/shurcooL/githubv4"
"github.com/shurcooL/graphql"
)
func archiveRepo(client *http.Client, repo *api.Repository) error {
var mutation struct {
ArchiveRepository struct {
Repository struct {
ID string
}
} `graphql:"archiveRepository(input: $input)"`
}
variables := map[string]interface{}{
"input": githubv4.ArchiveRepositoryInput{
RepositoryID: repo.ID,
},
}
host := repo.RepoHost()
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(host), client)
err := gql.MutateNamed(context.Background(), "ArchiveRepository", &mutation, variables)
return err
}

View file

@ -2,6 +2,7 @@ package repo
import (
"github.com/MakeNowJust/heredoc"
repoArchiveCmd "github.com/cli/cli/v2/pkg/cmd/repo/archive"
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"
@ -44,6 +45,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(creditsCmd.NewCmdRepoCredits(f, nil))
cmd.AddCommand(gardenCmd.NewCmdGarden(f, nil))
cmd.AddCommand(repoRenameCmd.NewCmdRename(f, nil))
cmd.AddCommand(repoArchiveCmd.NewCmdArchive(f, nil))
return cmd
}

View file

@ -12,25 +12,6 @@ import (
var NotFoundError = errors.New("not found")
func fetchRepository(apiClient *api.Client, repo ghrepo.Interface, fields []string) (*api.Repository, error) {
query := fmt.Sprintf(`query RepositoryInfo($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {%s}
}`, api.RepositoryGraphQL(fields))
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"name": repo.RepoName(),
}
var result struct {
Repository api.Repository
}
if err := apiClient.GraphQL(repo.RepoHost(), query, variables, &result); err != nil {
return nil, err
}
return api.InitRepoHostname(&result.Repository, repo.RepoHost()), nil
}
type RepoReadme struct {
Filename string
Content string

View file

@ -111,7 +111,7 @@ func viewRun(opts *ViewOptions) error {
fields = opts.Exporter.Fields()
}
repo, err := fetchRepository(apiClient, toView, fields)
repo, err := api.FetchRepository(apiClient, toView, fields)
if err != nil {
return err
}