From ec554822b81d3f26095ef6b4eff10ce7cc2f8174 Mon Sep 17 00:00:00 2001 From: Parth <76231594+pxrth9@users.noreply.github.com> Date: Tue, 12 Oct 2021 06:48:40 -0400 Subject: [PATCH] Add repo archive command (#4410) Co-authored-by: meiji163 --- api/queries_repo.go | 19 +++ pkg/cmd/repo/archive/archive.go | 99 ++++++++++++++++ pkg/cmd/repo/archive/archive_test.go | 165 +++++++++++++++++++++++++++ pkg/cmd/repo/archive/http.go | 32 ++++++ pkg/cmd/repo/repo.go | 2 + pkg/cmd/repo/view/http.go | 19 --- pkg/cmd/repo/view/view.go | 2 +- 7 files changed, 318 insertions(+), 20 deletions(-) create mode 100644 pkg/cmd/repo/archive/archive.go create mode 100644 pkg/cmd/repo/archive/archive_test.go create mode 100644 pkg/cmd/repo/archive/http.go diff --git a/api/queries_repo.go b/api/queries_repo.go index b1e41370d..c9d8b46f9 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -230,6 +230,25 @@ func (r Repository) ViewerCanTriage() bool { } } +func FetchRepository(client *Client, repo ghrepo.Interface, fields []string) (*Repository, error) { + query := fmt.Sprintf(`query RepositoryInfo($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) {%s} + }`, RepositoryGraphQL(fields)) + + variables := map[string]interface{}{ + "owner": repo.RepoOwner(), + "name": repo.RepoName(), + } + + var result struct { + Repository Repository + } + if err := client.GraphQL(repo.RepoHost(), query, variables, &result); err != nil { + return nil, err + } + return InitRepoHostname(&result.Repository, repo.RepoHost()), nil +} + func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) { query := ` fragment repo on Repository { diff --git a/pkg/cmd/repo/archive/archive.go b/pkg/cmd/repo/archive/archive.go new file mode 100644 index 000000000..c4d6be2bb --- /dev/null +++ b/pkg/cmd/repo/archive/archive.go @@ -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 ", + 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 +} diff --git a/pkg/cmd/repo/archive/archive_test.go b/pkg/cmd/repo/archive/archive_test.go new file mode 100644 index 000000000..6ed5ecb8f --- /dev/null +++ b/pkg/cmd/repo/archive/archive_test.go @@ -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()) + }) + } +} diff --git a/pkg/cmd/repo/archive/http.go b/pkg/cmd/repo/archive/http.go new file mode 100644 index 000000000..814fbcd33 --- /dev/null +++ b/pkg/cmd/repo/archive/http.go @@ -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 +} diff --git a/pkg/cmd/repo/repo.go b/pkg/cmd/repo/repo.go index 252c322e9..11c36b012 100644 --- a/pkg/cmd/repo/repo.go +++ b/pkg/cmd/repo/repo.go @@ -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" @@ -42,6 +43,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(repoSyncCmd.NewCmdSync(f, nil)) cmd.AddCommand(creditsCmd.NewCmdRepoCredits(f, nil)) cmd.AddCommand(gardenCmd.NewCmdGarden(f, nil)) + cmd.AddCommand(repoArchiveCmd.NewCmdArchive(f, nil)) return cmd } diff --git a/pkg/cmd/repo/view/http.go b/pkg/cmd/repo/view/http.go index dee1df677..fe2584a90 100644 --- a/pkg/cmd/repo/view/http.go +++ b/pkg/cmd/repo/view/http.go @@ -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 diff --git a/pkg/cmd/repo/view/view.go b/pkg/cmd/repo/view/view.go index 38524f1a2..ef8a7dfa2 100644 --- a/pkg/cmd/repo/view/view.go +++ b/pkg/cmd/repo/view/view.go @@ -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 }