Merge pull request #2953 from cristiand391/add-repo-list

Add `repo list` command
This commit is contained in:
Mislav Marohnić 2021-02-27 17:34:32 +01:00 committed by GitHub
commit 00cb921cd5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1004 additions and 0 deletions

View file

@ -0,0 +1,40 @@
{
"data": {
"repositoryOwner": {
"login": "octocat",
"repositories": {
"totalCount": 3,
"nodes": [
{
"nameWithOwner": "octocat/hello-world",
"description": "My first repository",
"isFork": false,
"isPrivate": false,
"isArchived": false,
"pushedAt": "2021-02-19T06:34:58Z"
},
{
"nameWithOwner": "octocat/cli",
"description": "GitHub CLI",
"isFork": true,
"isPrivate": false,
"isArchived": false,
"pushedAt": "2021-02-19T06:06:06Z"
},
{
"nameWithOwner": "octocat/testing",
"description": null,
"isFork": false,
"isPrivate": true,
"isArchived": false,
"pushedAt": "2021-02-11T22:32:05Z"
}
],
"pageInfo": {
"hasNextPage": false,
"endCursor": ""
}
}
}
}
}

View file

@ -0,0 +1,37 @@
{
"data": {
"search": {
"repositoryCount": 3,
"nodes": [
{
"nameWithOwner": "octocat/hello-world",
"description": "My first repository",
"isFork": false,
"isPrivate": false,
"isArchived": false,
"pushedAt": "2021-02-19T06:34:58Z"
},
{
"nameWithOwner": "octocat/cli",
"description": "GitHub CLI",
"isFork": true,
"isPrivate": false,
"isArchived": false,
"pushedAt": "2021-02-19T06:06:06Z"
},
{
"nameWithOwner": "octocat/testing",
"description": null,
"isFork": false,
"isPrivate": true,
"isArchived": false,
"pushedAt": "2021-02-11T22:32:05Z"
}
],
"pageInfo": {
"hasNextPage": false,
"endCursor": ""
}
}
}
}

235
pkg/cmd/repo/list/http.go Normal file
View file

@ -0,0 +1,235 @@
package list
import (
"context"
"fmt"
"net/http"
"reflect"
"strings"
"time"
"github.com/cli/cli/internal/ghinstance"
"github.com/shurcooL/githubv4"
"github.com/shurcooL/graphql"
)
type Repository struct {
NameWithOwner string
Description string
IsFork bool
IsPrivate bool
IsArchived bool
PushedAt time.Time
}
func (r Repository) Info() string {
var tags []string
if r.IsPrivate {
tags = append(tags, "private")
} else {
tags = append(tags, "public")
}
if r.IsFork {
tags = append(tags, "fork")
}
if r.IsArchived {
tags = append(tags, "archived")
}
return strings.Join(tags, ", ")
}
type RepositoryList struct {
Owner string
Repositories []Repository
TotalCount int
FromSearch bool
}
type FilterOptions struct {
Visibility string // private, public
Fork bool
Source bool
Language string
Archived bool
NonArchived bool
}
func listRepos(client *http.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
if filter.Language != "" || filter.Archived || filter.NonArchived {
return searchRepos(client, hostname, limit, owner, filter)
}
perPage := limit
if perPage > 100 {
perPage = 100
}
variables := map[string]interface{}{
"perPage": githubv4.Int(perPage),
"endCursor": (*githubv4.String)(nil),
}
if filter.Visibility != "" {
variables["privacy"] = githubv4.RepositoryPrivacy(strings.ToUpper(filter.Visibility))
} else {
variables["privacy"] = (*githubv4.RepositoryPrivacy)(nil)
}
if filter.Fork {
variables["fork"] = githubv4.Boolean(true)
} else if filter.Source {
variables["fork"] = githubv4.Boolean(false)
} else {
variables["fork"] = (*githubv4.Boolean)(nil)
}
var ownerConnection string
if owner == "" {
ownerConnection = `graphql:"repositoryOwner: viewer"`
} else {
ownerConnection = `graphql:"repositoryOwner(login: $owner)"`
variables["owner"] = githubv4.String(owner)
}
type repositoryOwner struct {
Login string
Repositories struct {
Nodes []Repository
TotalCount int
PageInfo struct {
HasNextPage bool
EndCursor string
}
} `graphql:"repositories(first: $perPage, after: $endCursor, privacy: $privacy, isFork: $fork, ownerAffiliations: OWNER, orderBy: { field: PUSHED_AT, direction: DESC })"`
}
query := reflect.StructOf([]reflect.StructField{
{
Name: "RepositoryOwner",
Type: reflect.TypeOf(repositoryOwner{}),
Tag: reflect.StructTag(ownerConnection),
},
})
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
listResult := RepositoryList{}
pagination:
for {
result := reflect.New(query)
err := gql.QueryNamed(context.Background(), "RepositoryList", result.Interface(), variables)
if err != nil {
return nil, err
}
owner := result.Elem().FieldByName("RepositoryOwner").Interface().(repositoryOwner)
listResult.TotalCount = owner.Repositories.TotalCount
listResult.Owner = owner.Login
for _, repo := range owner.Repositories.Nodes {
listResult.Repositories = append(listResult.Repositories, repo)
if len(listResult.Repositories) >= limit {
break pagination
}
}
if !owner.Repositories.PageInfo.HasNextPage {
break
}
variables["endCursor"] = githubv4.String(owner.Repositories.PageInfo.EndCursor)
}
return &listResult, nil
}
func searchRepos(client *http.Client, hostname string, limit int, owner string, filter FilterOptions) (*RepositoryList, error) {
type query struct {
Search struct {
RepositoryCount int
Nodes []struct {
Repository Repository `graphql:"...on Repository"`
}
PageInfo struct {
HasNextPage bool
EndCursor string
}
} `graphql:"search(type: REPOSITORY, query: $query, first: $perPage, after: $endCursor)"`
}
perPage := limit
if perPage > 100 {
perPage = 100
}
variables := map[string]interface{}{
"query": githubv4.String(searchQuery(owner, filter)),
"perPage": githubv4.Int(perPage),
"endCursor": (*githubv4.String)(nil),
}
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), client)
listResult := RepositoryList{FromSearch: true}
pagination:
for {
var result query
err := gql.QueryNamed(context.Background(), "RepositoryListSearch", &result, variables)
if err != nil {
return nil, err
}
listResult.TotalCount = result.Search.RepositoryCount
for _, node := range result.Search.Nodes {
if listResult.Owner == "" {
idx := strings.IndexRune(node.Repository.NameWithOwner, '/')
listResult.Owner = node.Repository.NameWithOwner[:idx]
}
listResult.Repositories = append(listResult.Repositories, node.Repository)
if len(listResult.Repositories) >= limit {
break pagination
}
}
if !result.Search.PageInfo.HasNextPage {
break
}
variables["endCursor"] = githubv4.String(result.Search.PageInfo.EndCursor)
}
return &listResult, nil
}
func searchQuery(owner string, filter FilterOptions) string {
queryParts := []string{"sort:updated-desc"}
if owner == "" {
queryParts = append(queryParts, "user:@me")
} else {
queryParts = append(queryParts, "user:"+owner)
}
if filter.Fork {
queryParts = append(queryParts, "fork:only")
} else if filter.Source {
queryParts = append(queryParts, "fork:false")
} else {
queryParts = append(queryParts, "fork:true")
}
if filter.Language != "" {
queryParts = append(queryParts, fmt.Sprintf("language:%q", filter.Language))
}
switch filter.Visibility {
case "public":
queryParts = append(queryParts, "is:public")
case "private":
queryParts = append(queryParts, "is:private")
}
if filter.Archived {
queryParts = append(queryParts, "archived:true")
} else if filter.NonArchived {
queryParts = append(queryParts, "archived:false")
}
return strings.Join(queryParts, " ")
}

View file

@ -0,0 +1,162 @@
package list
import (
"encoding/json"
"io/ioutil"
"net/http"
"os"
"testing"
"github.com/cli/cli/pkg/httpmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_listReposWithLanguage(t *testing.T) {
reg := httpmock.Registry{}
defer reg.Verify(t)
var searchData struct {
Query string
Variables map[string]interface{}
}
reg.Register(
httpmock.GraphQL(`query RepositoryListSearch\b`),
func(req *http.Request) (*http.Response, error) {
jsonData, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(jsonData, &searchData)
if err != nil {
return nil, err
}
respBody, err := os.Open("./fixtures/repoSearch.json")
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: 200,
Request: req,
Body: respBody,
}, nil
},
)
client := http.Client{Transport: &reg}
res, err := listRepos(&client, "github.com", 10, "", FilterOptions{
Language: "go",
})
require.NoError(t, err)
assert.Equal(t, 3, res.TotalCount)
assert.Equal(t, true, res.FromSearch)
assert.Equal(t, "octocat", res.Owner)
assert.Equal(t, "octocat/hello-world", res.Repositories[0].NameWithOwner)
assert.Equal(t, float64(10), searchData.Variables["perPage"])
assert.Equal(t, `sort:updated-desc user:@me fork:true language:"go"`, searchData.Variables["query"])
}
func Test_searchQuery(t *testing.T) {
type args struct {
owner string
filter FilterOptions
}
tests := []struct {
name string
args args
want string
}{
{
name: "blank",
want: "sort:updated-desc user:@me fork:true",
},
{
name: "in org",
args: args{
owner: "cli",
},
want: "sort:updated-desc user:cli fork:true",
},
{
name: "only public",
args: args{
owner: "",
filter: FilterOptions{
Visibility: "public",
},
},
want: "sort:updated-desc user:@me fork:true is:public",
},
{
name: "only private",
args: args{
owner: "",
filter: FilterOptions{
Visibility: "private",
},
},
want: "sort:updated-desc user:@me fork:true is:private",
},
{
name: "only forks",
args: args{
owner: "",
filter: FilterOptions{
Fork: true,
},
},
want: "sort:updated-desc user:@me fork:only",
},
{
name: "no forks",
args: args{
owner: "",
filter: FilterOptions{
Source: true,
},
},
want: "sort:updated-desc user:@me fork:false",
},
{
name: "with language",
args: args{
owner: "",
filter: FilterOptions{
Language: "ruby",
},
},
want: "sort:updated-desc user:@me fork:true language:\"ruby\"",
},
{
name: "only archived",
args: args{
owner: "",
filter: FilterOptions{
Archived: true,
},
},
want: "sort:updated-desc user:@me fork:true archived:true",
},
{
name: "only non-archived",
args: args{
owner: "",
filter: FilterOptions{
NonArchived: true,
},
},
want: "sort:updated-desc user:@me fork:true archived:false",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := searchQuery(tt.args.owner, tt.args.filter); got != tt.want {
t.Errorf("searchQuery() = %q, want %q", got, tt.want)
}
})
}
}

169
pkg/cmd/repo/list/list.go Normal file
View file

@ -0,0 +1,169 @@
package list
import (
"fmt"
"net/http"
"time"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/pkg/text"
"github.com/cli/cli/utils"
"github.com/spf13/cobra"
)
type ListOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
Limit int
Owner string
Visibility string
Fork bool
Source bool
Language string
Archived bool
NonArchived bool
Now func() time.Time
}
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
opts := ListOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
Now: time.Now,
}
var (
flagPublic bool
flagPrivate bool
)
cmd := &cobra.Command{
Use: "list [<owner>]",
Args: cobra.MaximumNArgs(1),
Short: "List repositories owned by user or organization",
RunE: func(c *cobra.Command, args []string) error {
if opts.Limit < 1 {
return &cmdutil.FlagError{Err: fmt.Errorf("invalid limit: %v", opts.Limit)}
}
if flagPrivate && flagPublic {
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--public` or `--private`")}
}
if opts.Source && opts.Fork {
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--source` or `--fork`")}
}
if opts.Archived && opts.NonArchived {
return &cmdutil.FlagError{Err: fmt.Errorf("specify only one of `--archived` or `--no-archived`")}
}
if flagPrivate {
opts.Visibility = "private"
} else if flagPublic {
opts.Visibility = "public"
}
if len(args) > 0 {
opts.Owner = args[0]
}
if runF != nil {
return runF(&opts)
}
return listRun(&opts)
},
}
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum number of repositories to list")
cmd.Flags().BoolVar(&flagPrivate, "private", false, "Show only private repositories")
cmd.Flags().BoolVar(&flagPublic, "public", false, "Show only public repositories")
cmd.Flags().BoolVar(&opts.Source, "source", false, "Show only non-forks")
cmd.Flags().BoolVar(&opts.Fork, "fork", false, "Show only forks")
cmd.Flags().StringVarP(&opts.Language, "language", "l", "", "Filter by primary coding language")
cmd.Flags().BoolVar(&opts.Archived, "archived", false, "Show only archived repositories")
cmd.Flags().BoolVar(&opts.NonArchived, "no-archived", false, "Omit archived repositories")
return cmd
}
func listRun(opts *ListOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
filter := FilterOptions{
Visibility: opts.Visibility,
Fork: opts.Fork,
Source: opts.Source,
Language: opts.Language,
Archived: opts.Archived,
NonArchived: opts.NonArchived,
}
listResult, err := listRepos(httpClient, ghinstance.OverridableDefault(), opts.Limit, opts.Owner, filter)
if err != nil {
return err
}
if err := opts.IO.StartPager(); err != nil {
fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err)
}
defer opts.IO.StopPager()
cs := opts.IO.ColorScheme()
tp := utils.NewTablePrinter(opts.IO)
now := opts.Now()
for _, repo := range listResult.Repositories {
info := repo.Info()
infoColor := cs.Gray
if repo.IsPrivate {
infoColor = cs.Yellow
}
t := repo.PushedAt
// if listResult.FromSearch {
// t = repo.UpdatedAt
// }
tp.AddField(repo.NameWithOwner, nil, cs.Bold)
tp.AddField(text.ReplaceExcessiveWhitespace(repo.Description), nil, nil)
tp.AddField(info, nil, infoColor)
if tp.IsTTY() {
tp.AddField(utils.FuzzyAgoAbbr(now, t), nil, cs.Gray)
} else {
tp.AddField(t.Format(time.RFC3339), nil, nil)
}
tp.EndRow()
}
if opts.IO.IsStdoutTTY() {
hasFilters := filter.Visibility != "" || filter.Fork || filter.Source || filter.Language != ""
title := listHeader(listResult.Owner, len(listResult.Repositories), listResult.TotalCount, hasFilters)
fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title)
}
return tp.Render()
}
func listHeader(owner string, matchCount, totalMatchCount int, hasFilters bool) string {
if totalMatchCount == 0 {
if hasFilters {
return "No results match your search"
} else if owner != "" {
return "There are no repositories in @" + owner
}
return "No results"
}
var matchStr string
if hasFilters {
matchStr = " that match your search"
}
return fmt.Sprintf("Showing %d of %d repositories in @%s%s", matchCount, totalMatchCount, owner, matchStr)
}

View file

@ -0,0 +1,359 @@
package list
import (
"bytes"
"io/ioutil"
"net/http"
"testing"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/test"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCmdList(t *testing.T) {
tests := []struct {
name string
cli string
wants ListOptions
wantsErr string
}{
{
name: "no arguments",
cli: "",
wants: ListOptions{
Limit: 30,
Owner: "",
Visibility: "",
Fork: false,
Source: false,
Language: "",
Archived: false,
NonArchived: false,
},
},
{
name: "with owner",
cli: "monalisa",
wants: ListOptions{
Limit: 30,
Owner: "monalisa",
Visibility: "",
Fork: false,
Source: false,
Language: "",
Archived: false,
NonArchived: false,
},
},
{
name: "with limit",
cli: "-L 101",
wants: ListOptions{
Limit: 101,
Owner: "",
Visibility: "",
Fork: false,
Source: false,
Language: "",
Archived: false,
NonArchived: false,
},
},
{
name: "only public",
cli: "--public",
wants: ListOptions{
Limit: 30,
Owner: "",
Visibility: "public",
Fork: false,
Source: false,
Language: "",
Archived: false,
NonArchived: false,
},
},
{
name: "only private",
cli: "--private",
wants: ListOptions{
Limit: 30,
Owner: "",
Visibility: "private",
Fork: false,
Source: false,
Language: "",
Archived: false,
NonArchived: false,
},
},
{
name: "only forks",
cli: "--fork",
wants: ListOptions{
Limit: 30,
Owner: "",
Visibility: "",
Fork: true,
Source: false,
Language: "",
Archived: false,
NonArchived: false,
},
},
{
name: "only sources",
cli: "--source",
wants: ListOptions{
Limit: 30,
Owner: "",
Visibility: "",
Fork: false,
Source: true,
Language: "",
Archived: false,
NonArchived: false,
},
},
{
name: "with language",
cli: "-l go",
wants: ListOptions{
Limit: 30,
Owner: "",
Visibility: "",
Fork: false,
Source: false,
Language: "go",
Archived: false,
NonArchived: false,
},
},
{
name: "only archived",
cli: "--archived",
wants: ListOptions{
Limit: 30,
Owner: "",
Visibility: "",
Fork: false,
Source: false,
Language: "",
Archived: true,
NonArchived: false,
},
},
{
name: "only non-archived",
cli: "--no-archived",
wants: ListOptions{
Limit: 30,
Owner: "",
Visibility: "",
Fork: false,
Source: false,
Language: "",
Archived: false,
NonArchived: true,
},
},
{
name: "no public and private",
cli: "--public --private",
wantsErr: "specify only one of `--public` or `--private`",
},
{
name: "no forks with sources",
cli: "--fork --source",
wantsErr: "specify only one of `--source` or `--fork`",
},
{
name: "conflicting archived",
cli: "--archived --no-archived",
wantsErr: "specify only one of `--archived` or `--no-archived`",
},
{
name: "too many arguments",
cli: "monalisa hubot",
wantsErr: "accepts at most 1 arg(s), received 2",
},
{
name: "invalid limit",
cli: "-L 0",
wantsErr: "invalid limit: 0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &cmdutil.Factory{}
argv, err := shlex.Split(tt.cli)
assert.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.wantsErr != "" {
assert.EqualError(t, err, tt.wantsErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wants.Limit, gotOpts.Limit)
assert.Equal(t, tt.wants.Owner, gotOpts.Owner)
assert.Equal(t, tt.wants.Visibility, gotOpts.Visibility)
assert.Equal(t, tt.wants.Fork, gotOpts.Fork)
assert.Equal(t, tt.wants.Source, gotOpts.Source)
assert.Equal(t, tt.wants.Archived, gotOpts.Archived)
assert.Equal(t, tt.wants.NonArchived, gotOpts.NonArchived)
})
}
}
func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
io, _, stdout, stderr := iostreams.Test()
io.SetStdoutTTY(isTTY)
io.SetStdinTTY(isTTY)
io.SetStderrTTY(isTTY)
factory := &cmdutil.Factory{
IOStreams: io,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: rt}, nil
},
}
cmd := NewCmdList(factory, nil)
argv, err := shlex.Split(cli)
if err != nil {
return nil, err
}
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(ioutil.Discard)
cmd.SetErr(ioutil.Discard)
_, err = cmd.ExecuteC()
return &test.CmdOut{
OutBuf: stdout,
ErrBuf: stderr,
}, err
}
func TestRepoList_nontty(t *testing.T) {
io, _, stdout, stderr := iostreams.Test()
io.SetStdoutTTY(false)
io.SetStdinTTY(false)
io.SetStderrTTY(false)
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)
httpReg.Register(
httpmock.GraphQL(`query RepositoryList\b`),
httpmock.FileResponse("./fixtures/repoList.json"),
)
opts := ListOptions{
IO: io,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: httpReg}, nil
},
Now: func() time.Time {
t, _ := time.Parse(time.RFC822, "19 Feb 21 15:00 UTC")
return t
},
Limit: 30,
}
err := listRun(&opts)
assert.NoError(t, err)
assert.Equal(t, "", stderr.String())
assert.Equal(t, heredoc.Doc(`
octocat/hello-world My first repository public 2021-02-19T06:34:58Z
octocat/cli GitHub CLI public, fork 2021-02-19T06:06:06Z
octocat/testing private 2021-02-11T22:32:05Z
`), stdout.String())
}
func TestRepoList_tty(t *testing.T) {
io, _, stdout, stderr := iostreams.Test()
io.SetStdoutTTY(true)
io.SetStdinTTY(true)
io.SetStderrTTY(true)
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)
httpReg.Register(
httpmock.GraphQL(`query RepositoryList\b`),
httpmock.FileResponse("./fixtures/repoList.json"),
)
opts := ListOptions{
IO: io,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: httpReg}, nil
},
Now: func() time.Time {
t, _ := time.Parse(time.RFC822, "19 Feb 21 15:00 UTC")
return t
},
Limit: 30,
}
err := listRun(&opts)
assert.NoError(t, err)
assert.Equal(t, "", stderr.String())
assert.Equal(t, heredoc.Doc(`
Showing 3 of 3 repositories in @octocat
octocat/hello-world My first repository public 8h
octocat/cli GitHub CLI public, fork 8h
octocat/testing private 7d
`), stdout.String())
}
func TestRepoList_filtering(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
http.Register(
httpmock.GraphQL(`query RepositoryList\b`),
httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) {
assert.Equal(t, "PRIVATE", params["privacy"])
assert.Equal(t, float64(2), params["perPage"])
}),
)
output, err := runCommand(http, true, `--private --limit 2 `)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "", output.Stderr())
assert.Equal(t, "\nNo results match your search\n\n", output.String())
}

View file

@ -7,6 +7,7 @@ import (
creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits"
repoForkCmd "github.com/cli/cli/pkg/cmd/repo/fork"
gardenCmd "github.com/cli/cli/pkg/cmd/repo/garden"
repoListCmd "github.com/cli/cli/pkg/cmd/repo/list"
repoViewCmd "github.com/cli/cli/pkg/cmd/repo/view"
"github.com/cli/cli/pkg/cmdutil"
"github.com/spf13/cobra"
@ -36,6 +37,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(repoListCmd.NewCmdList(f, nil))
cmd.AddCommand(creditsCmd.NewCmdRepoCredits(f, nil))
cmd.AddCommand(gardenCmd.NewCmdGarden(f, nil))