Skip to content

Commit 5a0f3c0

Browse files
add get repo command (#661)
* add get repo command * add provider and repo-id filters to get and list repo commands * correct comment
1 parent 167ed93 commit 5a0f3c0

9 files changed

Lines changed: 298 additions & 12 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ ARG ALPINE_VERSION="3.21"
55

66

77
### Go Builder ###
8-
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} as builder
8+
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
99

1010
RUN apk add --update --no-cache git bash make ca-certificates
1111

cmd/kosli/get.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func newGetCmd(out io.Writer) *cobra.Command {
2727
newGetPolicyCmd(out),
2828
newGetAttestationTypeCmd(out),
2929
newGetAttestationCmd(out),
30+
newGetRepoCmd(out),
3031
)
3132
return cmd
3233
}

cmd/kosli/getRepo.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
neturl "net/url"
9+
10+
"github.com/kosli-dev/cli/internal/output"
11+
"github.com/kosli-dev/cli/internal/requests"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
const getRepoShortDesc = `Get a repo for an org.`
16+
17+
const getRepoLongDesc = getRepoShortDesc + `
18+
The name of the repo is specified as an argument (e.g. "my-org/my-repo").
19+
Use --provider or --repo-id to narrow down the result when multiple repos
20+
match the given name.`
21+
22+
const getRepoExample = `
23+
# get a repo
24+
kosli get repo my-org/my-repo \
25+
--api-token yourAPIToken \
26+
--org KosliOrgName
27+
28+
# get a repo filtering by provider
29+
kosli get repo my-org/my-repo \
30+
--provider github \
31+
--api-token yourAPIToken \
32+
--org KosliOrgName`
33+
34+
type getRepoOptions struct {
35+
output string
36+
provider string
37+
repoID string
38+
}
39+
40+
func newGetRepoCmd(out io.Writer) *cobra.Command {
41+
o := new(getRepoOptions)
42+
cmd := &cobra.Command{
43+
Use: "repo REPO-NAME",
44+
Hidden: true,
45+
Short: getRepoShortDesc,
46+
Long: getRepoLongDesc,
47+
Example: getRepoExample,
48+
Args: cobra.ExactArgs(1),
49+
PreRunE: func(cmd *cobra.Command, args []string) error {
50+
err := RequireGlobalFlags(global, []string{"Org", "ApiToken"})
51+
if err != nil {
52+
return ErrorBeforePrintingUsage(cmd, err.Error())
53+
}
54+
return nil
55+
},
56+
RunE: func(cmd *cobra.Command, args []string) error {
57+
return o.run(out, args)
58+
},
59+
}
60+
61+
cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)
62+
cmd.Flags().StringVar(&o.provider, "provider", "", "[optional] The VCS provider to filter repos by (e.g. github, gitlab).")
63+
cmd.Flags().StringVar(&o.repoID, "repo-id", "", "[optional] The external repo ID to filter repos by.")
64+
65+
return cmd
66+
}
67+
68+
func (o *getRepoOptions) run(out io.Writer, args []string) error {
69+
params := neturl.Values{}
70+
params.Set("name", args[0])
71+
if o.provider != "" {
72+
params.Set("provider", o.provider)
73+
}
74+
if o.repoID != "" {
75+
params.Set("repo_id", o.repoID)
76+
}
77+
reqURL := fmt.Sprintf("%s/api/v2/repos/%s?%s", global.Host, global.Org, params.Encode())
78+
79+
reqParams := &requests.RequestParams{
80+
Method: http.MethodGet,
81+
URL: reqURL,
82+
Token: global.ApiToken,
83+
}
84+
response, err := kosliClient.Do(reqParams)
85+
if err != nil {
86+
return err
87+
}
88+
89+
var parsed struct {
90+
Embedded struct {
91+
Repos []map[string]any `json:"repos"`
92+
} `json:"_embedded"`
93+
}
94+
if err := json.Unmarshal([]byte(response.Body), &parsed); err != nil {
95+
return err
96+
}
97+
if len(parsed.Embedded.Repos) > 1 {
98+
return fmt.Errorf("found %d repos matching %q. Use --provider or --repo-id to narrow down the search", len(parsed.Embedded.Repos), args[0])
99+
}
100+
101+
return output.FormattedPrint(response.Body, o.output, out, 0,
102+
map[string]output.FormatOutputFunc{
103+
"table": printRepoAsTable,
104+
"json": output.PrintJson,
105+
})
106+
}
107+
108+
func printRepoAsTable(raw string, out io.Writer, page int) error {
109+
var response struct {
110+
Embedded struct {
111+
Repos []map[string]any `json:"repos"`
112+
} `json:"_embedded"`
113+
}
114+
115+
err := json.Unmarshal([]byte(raw), &response)
116+
if err != nil {
117+
return err
118+
}
119+
120+
repos := response.Embedded.Repos
121+
if len(repos) == 0 {
122+
logger.Info("Repo was not found.")
123+
return nil
124+
}
125+
126+
repo := repos[0]
127+
rows := []string{
128+
fmt.Sprintf("Name:\t%s", repo["name"]),
129+
fmt.Sprintf("URL:\t%s", repo["url"]),
130+
fmt.Sprintf("Provider:\t%s", repo["provider"]),
131+
fmt.Sprintf("Latest Activity:\t%s", repo["latest_activity"]),
132+
}
133+
134+
tabFormattedPrint(out, []string{}, rows)
135+
return nil
136+
}

cmd/kosli/getRepo_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/suite"
8+
)
9+
10+
// Define the suite, and absorb the built-in basic suite
11+
// functionality from testify - including a T() method which
12+
// returns the current testing context
13+
type GetRepoCommandTestSuite struct {
14+
suite.Suite
15+
defaultKosliArguments string
16+
acmeOrgKosliArguments string
17+
}
18+
19+
func (suite *GetRepoCommandTestSuite) SetupTest() {
20+
global = &GlobalOpts{
21+
ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY",
22+
Org: "docs-cmd-test-user",
23+
Host: "http://localhost:8001",
24+
}
25+
suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
26+
27+
global.Org = "iu-org-shared"
28+
global.ApiToken = "qM9u2_grv6pJLbACwsMMMT5LIQy82tQj2k1zjZnlXti1smnFaGwCKW4jzk0La7ae9RrSYvEwCXSsXknD6YZqd-onLaaIUUKtEn6-B6yh53vWIe9EC5u85FCbKZjFbaicp_d0Me0Zcqq_KcCgrAZRX9xggl_pBb2oaCsNdllqNjk"
29+
suite.acmeOrgKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
30+
CreateFlowWithTemplate("get-repo", "testdata/valid_template.yml", suite.T())
31+
SetEnvVars(map[string]string{
32+
"GITHUB_RUN_NUMBER": "1234",
33+
"GITHUB_SERVER_URL": "https://github.com",
34+
"GITHUB_REPOSITORY": "kosli-dev/cli",
35+
"GITHUB_REPOSITORY_ID": "1234567890",
36+
}, suite.T())
37+
BeginTrail("trail-name", "get-repo", "", suite.T())
38+
}
39+
40+
func (suite *GetRepoCommandTestSuite) TearDownTest() {
41+
UnSetEnvVars(map[string]string{
42+
"GITHUB_RUN_NUMBER": "",
43+
"GITHUB_SERVER_URL": "",
44+
"GITHUB_REPOSITORY": "",
45+
"GITHUB_REPOSITORY_ID": "",
46+
}, suite.T())
47+
}
48+
49+
func (suite *GetRepoCommandTestSuite) TestGetRepoCmd() {
50+
tests := []cmdTestCase{
51+
{
52+
name: "01-getting a non-existing repo returns not-found message",
53+
cmd: fmt.Sprintf(`get repo non-existing/repo %s`, suite.defaultKosliArguments),
54+
golden: "Repo was not found.\n",
55+
},
56+
{
57+
name: "02-getting an existing repo works",
58+
cmd: fmt.Sprintf(`get repo kosli-dev/cli %s`, suite.acmeOrgKosliArguments),
59+
},
60+
{
61+
name: "03-getting an existing repo with --output json works",
62+
cmd: fmt.Sprintf(`get repo kosli-dev/cli --output json %s`, suite.acmeOrgKosliArguments),
63+
goldenJson: []jsonCheck{{"_embedded.repos", "non-empty"}},
64+
},
65+
{
66+
name: "04-getting an existing repo with matching --provider works",
67+
cmd: fmt.Sprintf(`get repo kosli-dev/cli --provider github %s`, suite.acmeOrgKosliArguments),
68+
},
69+
{
70+
name: "05-getting an existing repo with matching --provider and --output json works",
71+
cmd: fmt.Sprintf(`get repo kosli-dev/cli --provider github --output json %s`, suite.acmeOrgKosliArguments),
72+
goldenJson: []jsonCheck{{"_embedded.repos", "non-empty"}},
73+
},
74+
{
75+
name: "06-getting a repo with a non-matching --provider returns not-found message",
76+
cmd: fmt.Sprintf(`get repo kosli-dev/cli --provider gitlab %s`, suite.acmeOrgKosliArguments),
77+
golden: "Repo was not found.\n",
78+
},
79+
{
80+
name: "07-getting a repo with a non-matching --repo-id returns not-found message",
81+
cmd: fmt.Sprintf(`get repo kosli-dev/cli --repo-id non-existing-id %s`, suite.acmeOrgKosliArguments),
82+
golden: "Repo was not found.\n",
83+
},
84+
{
85+
wantError: true,
86+
name: "08-providing no argument fails",
87+
cmd: fmt.Sprintf(`get repo %s`, suite.defaultKosliArguments),
88+
golden: "Error: accepts 1 arg(s), received 0\n",
89+
},
90+
{
91+
wantError: true,
92+
name: "09-providing more than one argument fails",
93+
cmd: fmt.Sprintf(`get repo foo bar %s`, suite.defaultKosliArguments),
94+
golden: "Error: accepts 1 arg(s), received 2\n",
95+
},
96+
}
97+
98+
runTestCmd(suite.T(), tests)
99+
}
100+
101+
// In order for 'go test' to run this suite, we need to create
102+
// a normal test function and pass our suite to suite.Run
103+
func TestGetRepoCommandTestSuite(t *testing.T) {
104+
suite.Run(t, new(GetRepoCommandTestSuite))
105+
}

cmd/kosli/listRepos.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io"
77
"net/http"
8+
neturl "net/url"
89

910
"github.com/kosli-dev/cli/internal/output"
1011
"github.com/kosli-dev/cli/internal/requests"
@@ -15,6 +16,9 @@ const listReposDesc = `List repos for an org.`
1516

1617
type listReposOptions struct {
1718
listOptions
19+
name string
20+
provider string
21+
repoID string
1822
}
1923

2024
func newListReposCmd(out io.Writer) *cobra.Command {
@@ -38,16 +42,31 @@ func newListReposCmd(out io.Writer) *cobra.Command {
3842
}
3943

4044
addListFlags(cmd, &o.listOptions)
45+
cmd.Flags().StringVar(&o.name, "name", "", "[optional] The repo name to filter by.")
46+
cmd.Flags().StringVar(&o.provider, "provider", "", "[optional] The VCS provider to filter repos by (e.g. github, gitlab).")
47+
cmd.Flags().StringVar(&o.repoID, "repo-id", "", "[optional] The external repo ID to filter repos by.")
4148

4249
return cmd
4350
}
4451

4552
func (o *listReposOptions) run(out io.Writer) error {
46-
url := fmt.Sprintf("%s/api/v2/repos/%s?page=%d&per_page=%d", global.Host, global.Org, o.pageNumber, o.pageLimit)
53+
params := neturl.Values{}
54+
params.Set("page", fmt.Sprintf("%d", o.pageNumber))
55+
params.Set("per_page", fmt.Sprintf("%d", o.pageLimit))
56+
if o.name != "" {
57+
params.Set("name", o.name)
58+
}
59+
if o.provider != "" {
60+
params.Set("provider", o.provider)
61+
}
62+
if o.repoID != "" {
63+
params.Set("repo_id", o.repoID)
64+
}
65+
reqURL := fmt.Sprintf("%s/api/v2/repos/%s?%s", global.Host, global.Org, params.Encode())
4766

4867
reqParams := &requests.RequestParams{
4968
Method: http.MethodGet,
50-
URL: url,
69+
URL: reqURL,
5170
Token: global.ApiToken,
5271
}
5372
response, err := kosliClient.Do(reqParams)
@@ -81,10 +100,10 @@ func printReposListAsTable(raw string, out io.Writer, page int) error {
81100
return nil
82101
}
83102

84-
header := []string{"NAME", "URL", "LAST_ACTIVITY"}
103+
header := []string{"NAME", "URL", "PROVIDER", "LAST_ACTIVITY"}
85104
rows := []string{}
86105
for _, repo := range repos {
87-
row := fmt.Sprintf("%s\t%s\t%s", repo["name"], repo["url"], repo["latest_activity"])
106+
row := fmt.Sprintf("%s\t%s\t%s\t%s", repo["name"], repo["url"], repo["provider"], repo["latest_activity"])
88107
rows = append(rows, row)
89108
}
90109
tabFormattedPrint(out, header, rows)

cmd/kosli/listRepos_test.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func (suite *ListReposCommandTestSuite) SetupTest() {
2424
}
2525
suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
2626

27-
global.Org = "acme-org"
27+
global.Org = "acme-org-shared"
2828
global.ApiToken = "v3OWZiYWu9G2IMQStYg9BcPQUQ88lJNNnTJTNq8jfvmkR1C5wVpHSs7F00JcB5i6OGeUzrKt3CwRq7ndcN4TTfMeo8ASVJ5NdHpZT7DkfRfiFvm8s7GbsIHh2PtiQJYs2UoN13T8DblV5C4oKb6-yWH73h67OhotPlKfVKazR-c"
2929
suite.acmeOrgKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
3030
CreateFlowWithTemplate("list-repos", "testdata/valid_template.yml", suite.T())
@@ -57,7 +57,7 @@ func (suite *ListReposCommandTestSuite) TestListReposCmd() {
5757
{
5858
name: "02-listing repos works when there are no repos",
5959
cmd: fmt.Sprintf(`list repos %s`, suite.acmeOrgKosliArguments),
60-
goldenRegex: ".*\nkosli-dev/cli https://github.com/kosli-dev/cli Trail Started at.*",
60+
goldenRegex: ".*\nkosli-dev/cli.*https://github.com/kosli-dev/cli.*github.*Trail Started at.*",
6161
},
6262
{
6363
name: "03-listing repos with --output json works when there are repos",
@@ -93,6 +93,31 @@ func (suite *ListReposCommandTestSuite) TestListReposCmd() {
9393
cmd: fmt.Sprintf(`list repos --page-limit 15 --page 2 %s`, suite.defaultKosliArguments),
9494
golden: "",
9595
},
96+
{
97+
name: "09-listing repos with --name filter works",
98+
cmd: fmt.Sprintf(`list repos --name kosli-dev/cli %s`, suite.acmeOrgKosliArguments),
99+
goldenRegex: ".*\nkosli-dev/cli.*https://github.com/kosli-dev/cli.*github.*Trail Started at.*",
100+
},
101+
{
102+
name: "10-listing repos with --name filter and --output json works",
103+
cmd: fmt.Sprintf(`list repos --name kosli-dev/cli --output json %s`, suite.acmeOrgKosliArguments),
104+
goldenJson: []jsonCheck{{"_embedded.repos", "non-empty"}},
105+
},
106+
{
107+
name: "11-listing repos with --provider filter works",
108+
cmd: fmt.Sprintf(`list repos --provider github %s`, suite.acmeOrgKosliArguments),
109+
goldenRegex: ".*\nkosli-dev/cli.*https://github.com/kosli-dev/cli.*github.*Trail Started at.*",
110+
},
111+
{
112+
name: "12-listing repos with non-matching --provider returns no repos message",
113+
cmd: fmt.Sprintf(`list repos --provider gitlab %s`, suite.acmeOrgKosliArguments),
114+
golden: "No repos were found.\n",
115+
},
116+
{
117+
name: "13-listing repos with non-matching --repo-id returns no repos message",
118+
cmd: fmt.Sprintf(`list repos --repo-id non-existing-id %s`, suite.acmeOrgKosliArguments),
119+
golden: "No repos were found.\n",
120+
},
96121
}
97122

98123
runTestCmd(suite.T(), tests)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ require (
7878
github.com/blang/semver/v4 v4.0.0 // indirect
7979
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
8080
github.com/cespare/xxhash/v2 v2.3.0 // indirect
81-
github.com/cloudflare/circl v1.6.1 // indirect
81+
github.com/cloudflare/circl v1.6.3 // indirect
8282
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
8383
github.com/containers/ocicrypt v1.2.1 // indirect
8484
github.com/containers/storage v1.57.2 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
143143
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
144144
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
145145
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
146-
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
147-
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
146+
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
147+
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
148148
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
149149
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
150150
github.com/containers/image/v5 v5.34.3 h1:/cMgfyA4Y7ILH7nzWP/kqpkE5Df35Ek4bp5ZPvJOVmI=

0 commit comments

Comments
 (0)