66 "io"
77 "net/http"
88 neturl "net/url"
9+ "sort"
10+ "strings"
911
1012 "github.com/kosli-dev/cli/internal/output"
1113 "github.com/kosli-dev/cli/internal/requests"
@@ -15,19 +17,27 @@ import (
1517const getRepoShortDesc = `Get a repo for an org.`
1618
1719const 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.`
20+ The repo is identified either by its name, specified as an argument
21+ (e.g. "my-org/my-repo"), or unambiguously by its internal ID via --repo-id.
22+ The output includes the repo's internal ID, which is the identifier used
23+ to tag the repo (see: kosli tag).
24+ Use --provider to disambiguate when multiple repos share the same name
25+ across VCS providers.`
2126
2227const getRepoExample = `
2328# get a repo
2429kosli get repo my-org/my-repo \
2530 --api-token yourAPIToken \
2631 --org KosliOrgName
2732
28- # get a repo filtering by provider
33+ # get a repo whose name exists across multiple VCS providers
2934kosli get repo my-org/my-repo \
3035 --provider github \
36+ --api-token yourAPIToken \
37+ --org KosliOrgName
38+
39+ # get a repo by its internal ID
40+ kosli get repo --repo-id yourRepoID \
3141 --api-token yourAPIToken \
3242 --org KosliOrgName`
3343
@@ -40,16 +50,23 @@ type getRepoOptions struct {
4050func newGetRepoCmd (out io.Writer ) * cobra.Command {
4151 o := new (getRepoOptions )
4252 cmd := & cobra.Command {
43- Use : "repo REPO-NAME" ,
53+ Use : "repo [ REPO-NAME] " ,
4454 Short : getRepoShortDesc ,
4555 Long : getRepoLongDesc ,
4656 Example : getRepoExample ,
47- Args : cobra .ExactArgs (1 ),
57+ Args : cobra .MaximumNArgs (1 ),
4858 PreRunE : func (cmd * cobra.Command , args []string ) error {
4959 err := RequireGlobalFlags (global , []string {"Org" , "ApiToken" })
5060 if err != nil {
5161 return ErrorBeforePrintingUsage (cmd , err .Error ())
5262 }
63+ // error unless exactly one of the REPO-NAME arg or --repo-id is set (XOR)
64+ if (len (args ) == 1 ) == (o .repoID != "" ) {
65+ return ErrorBeforePrintingUsage (cmd , "exactly one of the REPO-NAME argument or --repo-id must be provided" )
66+ }
67+ if o .provider != "" && o .repoID != "" {
68+ return ErrorBeforePrintingUsage (cmd , "--provider cannot be combined with --repo-id" )
69+ }
5370 return nil
5471 },
5572 RunE : func (cmd * cobra.Command , args []string ) error {
@@ -58,26 +75,33 @@ func newGetRepoCmd(out io.Writer) *cobra.Command {
5875 }
5976
6077 cmd .Flags ().StringVarP (& o .output , "output" , "o" , "table" , outputFlag )
61- cmd .Flags ().StringVar (& o .provider , "provider" , "" , "[optional] The VCS provider to filter repos by (e.g. github, gitlab)." )
62- cmd .Flags ().StringVar (& o .repoID , "repo-id" , "" , "[optional] The external repo ID to filter repos by ." )
78+ cmd .Flags ().StringVar (& o .provider , "provider" , "" , "[optional] The VCS provider of the repo (e.g. github, gitlab). Required when multiple repos share the same name across providers ." )
79+ cmd .Flags ().StringVar (& o .repoID , "repo-id" , "" , "[optional] The repo's internal ID (as shown in the repo output). Identifies the repo unambiguously; cannot be combined with the REPO-NAME argument ." )
6380
6481 return cmd
6582}
6683
6784func (o * getRepoOptions ) run (out io.Writer , args []string ) error {
68- params := neturl.Values {}
69- params .Set ("name" , args [0 ])
70- if o .provider != "" {
71- params .Set ("provider" , o .provider )
72- }
73- if o .repoID != "" {
74- params .Set ("repo_id" , o .repoID )
85+ // the endpoint's path is the repo name; when fetching by internal id the
86+ // server keys off the ?id= query param and ignores the path, so the id
87+ // intentionally doubles as the path segment instead of a placeholder
88+ pathName := o .repoID
89+ if len (args ) == 1 {
90+ pathName = args [0 ]
7591 }
76- base , err := neturl .JoinPath (global .Host , "api/v2/repos" , global .Org )
92+ reqURL , err := neturl .JoinPath (global .Host , "api/v2/repos" , global .Org , pathName )
7793 if err != nil {
7894 return err
7995 }
80- reqURL := base + "?" + params .Encode ()
96+ params := neturl.Values {}
97+ if o .repoID != "" {
98+ params .Set ("id" , o .repoID )
99+ } else if o .provider != "" {
100+ params .Set ("provider" , o .provider )
101+ }
102+ if len (params ) > 0 {
103+ reqURL += "?" + params .Encode ()
104+ }
81105
82106 reqParams := & requests.RequestParams {
83107 Method : http .MethodGet ,
@@ -89,46 +113,88 @@ func (o *getRepoOptions) run(out io.Writer, args []string) error {
89113 return err
90114 }
91115
92- var parsed struct {
93- Repos []map [string ]any `json:"repos"`
94- }
95- if err := json .Unmarshal ([]byte (response .Body ), & parsed ); err != nil {
96- return err
97- }
98- if len (parsed .Repos ) > 1 {
99- return fmt .Errorf ("found %d repos matching %q. Use --provider or --repo-id to narrow down the search" , len (parsed .Repos ), args [0 ])
100- }
101-
102116 return output .FormattedPrint (response .Body , o .output , out , 0 ,
103117 map [string ]output.FormatOutputFunc {
104118 "table" : printRepoAsTable ,
105119 "json" : output .PrintJson ,
106120 })
107121}
108122
109- func printRepoAsTable (raw string , out io.Writer , page int ) error {
110- var response struct {
111- Repos []map [string ]any `json:"repos"`
123+ // fetchRepoInnerID resolves a repo name to its internal id via the get repo
124+ // endpoint. An empty provider means no provider disambiguation.
125+ func fetchRepoInnerID (org , repoName , provider string ) (string , error ) {
126+ reqURL , err := neturl .JoinPath (global .Host , "api/v2/repos" , org , repoName )
127+ if err != nil {
128+ return "" , err
129+ }
130+ if provider != "" {
131+ params := neturl.Values {}
132+ params .Set ("provider" , provider )
133+ reqURL += "?" + params .Encode ()
112134 }
113135
114- err := json .Unmarshal ([]byte (raw ), & response )
136+ reqParams := & requests.RequestParams {
137+ Method : http .MethodGet ,
138+ URL : reqURL ,
139+ Token : global .ApiToken ,
140+ }
141+ response , err := kosliClient .Do (reqParams )
115142 if err != nil {
116- return err
143+ return "" , err
144+ }
145+
146+ var repo struct {
147+ ID string `json:"id"`
148+ }
149+ if err := json .Unmarshal ([]byte (response .Body ), & repo ); err != nil {
150+ return "" , err
151+ }
152+ if repo .ID == "" {
153+ return "" , fmt .Errorf ("could not resolve repo %q to an id" , repoName )
117154 }
155+ return repo .ID , nil
156+ }
157+
158+ func printRepoAsTable (raw string , out io.Writer , page int ) error {
159+ var repo map [string ]any
118160
119- repos := response .Repos
120- if len (repos ) == 0 {
121- logger .Info ("Repo was not found." )
122- return nil
161+ err := json .Unmarshal ([]byte (raw ), & repo )
162+ if err != nil {
163+ return err
123164 }
124165
125- repo := repos [0 ]
166+ tagsOutput := formatRepoTags (repo ["tags" ])
167+ if tagsOutput == "" {
168+ tagsOutput = "None"
169+ }
126170 rows := []string {
127- fmt .Sprintf ("Name:\t %s" , repo ["name" ]),
128- fmt .Sprintf ("URL:\t %s" , repo ["url" ]),
129- fmt .Sprintf ("Provider:\t %s" , repo ["provider" ]),
171+ fmt .Sprintf ("Name:\t %v" , repo ["name" ]),
172+ fmt .Sprintf ("ID:\t %v" , repo ["id" ]),
173+ fmt .Sprintf ("URL:\t %v" , repo ["url" ]),
174+ fmt .Sprintf ("Provider:\t %v" , repo ["provider" ]),
175+ fmt .Sprintf ("Tags:\t %s" , tagsOutput ),
130176 }
131177
132178 tabFormattedPrint (out , []string {}, rows )
133179 return nil
134180}
181+
182+ // formatRepoTags renders a repo's tags map as sorted "key=value" pairs, or ""
183+ // when there are no tags. Following the flow/env table conventions, list
184+ // columns show the empty string while the get repo detail view shows "None".
185+ func formatRepoTags (rawTags any ) string {
186+ tags , ok := rawTags .(map [string ]any )
187+ if ! ok || len (tags ) == 0 {
188+ return ""
189+ }
190+ keys := make ([]string , 0 , len (tags ))
191+ for key := range tags {
192+ keys = append (keys , key )
193+ }
194+ sort .Strings (keys )
195+ pairs := make ([]string , 0 , len (tags ))
196+ for _ , key := range keys {
197+ pairs = append (pairs , fmt .Sprintf ("%s=%v" , key , tags [key ]))
198+ }
199+ return strings .Join (pairs , ", " )
200+ }
0 commit comments