@@ -2,10 +2,12 @@ package repositories
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
67 "fmt"
78 "net/http"
89 "net/url"
10+ "sort"
911 "strings"
1012
1113 "github.com/OrcaCD/orca-cd/internal/hub/models"
@@ -14,7 +16,22 @@ import (
1416
1517type giteaProvider struct {}
1618
17- const httpsScheme = "https"
19+ type parsedGiteaRepositoryURL struct {
20+ baseURL string
21+ owner string
22+ repo string
23+ }
24+
25+ type giteaBranch struct {
26+ Name string `json:"name"`
27+ }
28+
29+ type giteaTreeResponse struct {
30+ Tree []struct {
31+ Path string `json:"path"`
32+ Type string `json:"type"`
33+ } `json:"tree"`
34+ }
1835
1936func init () {
2037 Register (models .Gitea , giteaProvider {})
@@ -23,15 +40,24 @@ func init() {
2340// parseGiteaURL validates a Gitea repository URL (including self-hosted instances)
2441// and returns the owner and repository name.
2542func parseGiteaURL (rawURL string ) (owner , repo string , err error ) {
43+ parsedRepoURL , err := parseGiteaRepositoryURL (rawURL )
44+ if err != nil {
45+ return "" , "" , err
46+ }
47+
48+ return parsedRepoURL .owner , parsedRepoURL .repo , nil
49+ }
50+
51+ func parseGiteaRepositoryURL (rawURL string ) (parsedGiteaRepositoryURL , error ) {
2652 u , err := url .Parse (rawURL )
2753 if err != nil {
28- return "" , "" , errors .New ("invalid URL" )
54+ return parsedGiteaRepositoryURL {} , errors .New ("invalid URL" )
2955 }
3056 if u .Scheme != httpsScheme {
31- return "" , "" , fmt .Errorf ("URL must use %s" , httpsScheme )
57+ return parsedGiteaRepositoryURL {} , fmt .Errorf ("URL must use %s" , httpsScheme )
3258 }
3359 if u .Host == "" {
34- return "" , "" , errors .New ("URL must have a valid host" )
60+ return parsedGiteaRepositoryURL {} , errors .New ("URL must have a valid host" )
3561 }
3662
3763 // Allow URLs ending with .git
@@ -40,18 +66,22 @@ func parseGiteaURL(rawURL string) (owner, repo string, err error) {
4066
4167 parts := strings .SplitN (path , "/" , 3 )
4268 if len (parts ) != 2 || parts [0 ] == "" || parts [1 ] == "" {
43- return "" , "" , fmt .Errorf ("URL must be in the format %s://{host}/{owner}/{repo}" , httpsScheme )
69+ return parsedGiteaRepositoryURL {} , fmt .Errorf ("URL must be in the format %s://{host}/{owner}/{repo}" , httpsScheme )
4470 }
4571
46- owner , repo = parts [0 ], parts [1 ]
72+ owner , repo : = parts [0 ], parts [1 ]
4773 if ! ownerRe .MatchString (owner ) {
48- return "" , "" , errors .New ("invalid Gitea owner name" )
74+ return parsedGiteaRepositoryURL {} , errors .New ("invalid Gitea owner name" )
4975 }
5076 if ! repoRe .MatchString (repo ) {
51- return "" , "" , errors .New ("invalid Gitea repository name" )
77+ return parsedGiteaRepositoryURL {} , errors .New ("invalid Gitea repository name" )
5278 }
5379
54- return owner , repo , nil
80+ return parsedGiteaRepositoryURL {
81+ baseURL : fmt .Sprintf ("%s://%s" , u .Scheme , u .Host ),
82+ owner : owner ,
83+ repo : repo ,
84+ }, nil
5585}
5686
5787func (giteaProvider ) ParseURL (rawURL string ) (string , string , error ) {
@@ -70,28 +100,22 @@ func (giteaProvider) TestConnection(ctx context.Context, repo *models.Repository
70100 return errors .New ("repository is required" )
71101 }
72102
73- owner , repoName , err := parseGiteaURL (repo .Url )
103+ parsedRepoURL , err := parseGiteaRepositoryURL (repo .Url )
74104 if err != nil {
75105 return fmt .Errorf ("invalid repository URL: %w" , err )
76106 }
77107
78- u , _ := url .Parse (repo .Url )
79- baseURL := fmt .Sprintf ("%s://%s" , u .Scheme , u .Host )
80-
81- apiURL := fmt .Sprintf ("%s/api/v1/repos/%s/%s" , baseURL , url .PathEscape (owner ), url .PathEscape (repoName ))
108+ apiURL := fmt .Sprintf (
109+ "%s/api/v1/repos/%s/%s" ,
110+ parsedRepoURL .baseURL ,
111+ url .PathEscape (parsedRepoURL .owner ),
112+ url .PathEscape (parsedRepoURL .repo ),
113+ )
82114 req , err := httpclient .NewRequest (ctx , http .MethodGet , apiURL , nil )
83115 if err != nil {
84116 return fmt .Errorf ("failed to build Gitea request: %w" , err )
85117 }
86-
87- req .Header .Set ("Accept" , "application/json" )
88-
89- if repo .AuthMethod == models .AuthMethodToken && repo .AuthToken != nil {
90- token := strings .TrimSpace (repo .AuthToken .String ())
91- if token != "" {
92- req .Header .Set ("Authorization" , "token " + token )
93- }
94- }
118+ addGiteaHeaders (req , repo )
95119
96120 resp , err := httpclient .Default .Do (req )
97121 if err != nil {
@@ -115,3 +139,170 @@ func (giteaProvider) TestConnection(ctx context.Context, repo *models.Repository
115139 return fmt .Errorf ("gitea API returned unexpected status: %d" , resp .StatusCode )
116140 }
117141}
142+
143+ func (giteaProvider ) ListBranches (ctx context.Context , repo * models.Repository ) ([]string , error ) {
144+ if repo == nil {
145+ return nil , errors .New ("repository is required" )
146+ }
147+
148+ parsedRepoURL , err := parseGiteaRepositoryURL (repo .Url )
149+ if err != nil {
150+ return nil , fmt .Errorf ("invalid repository URL: %w" , err )
151+ }
152+
153+ branches := make ([]string , 0 )
154+
155+ for page := 1 ; ; page ++ {
156+ apiURL := fmt .Sprintf (
157+ "%s/api/v1/repos/%s/%s/branches?page=%d&limit=%d" ,
158+ parsedRepoURL .baseURL ,
159+ url .PathEscape (parsedRepoURL .owner ),
160+ url .PathEscape (parsedRepoURL .repo ),
161+ page ,
162+ providerPageSize ,
163+ )
164+
165+ req , err := httpclient .NewRequest (ctx , http .MethodGet , apiURL , nil )
166+ if err != nil {
167+ return nil , fmt .Errorf ("failed to build Gitea request: %w" , err )
168+ }
169+ addGiteaHeaders (req , repo )
170+
171+ resp , err := httpclient .Default .Do (req )
172+ if err != nil {
173+ return nil , fmt .Errorf ("failed to fetch Gitea branches: %w" , err )
174+ }
175+
176+ switch resp .StatusCode {
177+ case http .StatusOK :
178+ var parsed []giteaBranch
179+ if err := json .NewDecoder (resp .Body ).Decode (& parsed ); err != nil {
180+ closeErr := resp .Body .Close ()
181+ if closeErr != nil {
182+ fmt .Printf ("warning: failed to close Gitea response body: %v\n " , closeErr )
183+ }
184+ return nil , fmt .Errorf ("failed to decode Gitea branches response: %w" , err )
185+ }
186+
187+ closeErr := resp .Body .Close ()
188+ if closeErr != nil {
189+ fmt .Printf ("warning: failed to close Gitea response body: %v\n " , closeErr )
190+ }
191+
192+ for _ , branch := range parsed {
193+ if branch .Name != "" {
194+ branches = append (branches , branch .Name )
195+ }
196+ }
197+
198+ if len (parsed ) < providerPageSize {
199+ sortBranches (branches )
200+ return branches , nil
201+ }
202+ case http .StatusUnauthorized , http .StatusForbidden :
203+ closeErr := resp .Body .Close ()
204+ if closeErr != nil {
205+ fmt .Printf ("warning: failed to close Gitea response body: %v\n " , closeErr )
206+ }
207+ return nil , errors .New ("authentication failed or access denied" )
208+ case http .StatusNotFound :
209+ closeErr := resp .Body .Close ()
210+ if closeErr != nil {
211+ fmt .Printf ("warning: failed to close Gitea response body: %v\n " , closeErr )
212+ }
213+ return nil , errors .New ("repository not found or access denied" )
214+ default :
215+ statusCode := resp .StatusCode
216+ closeErr := resp .Body .Close ()
217+ if closeErr != nil {
218+ fmt .Printf ("warning: failed to close Gitea response body: %v\n " , closeErr )
219+ }
220+ return nil , fmt .Errorf ("gitea API returned unexpected status: %d" , statusCode )
221+ }
222+ }
223+ }
224+
225+ func (giteaProvider ) ListTree (ctx context.Context , repo * models.Repository , branch string ) ([]TreeEntry , error ) {
226+ if repo == nil {
227+ return nil , errors .New ("repository is required" )
228+ }
229+
230+ if strings .TrimSpace (branch ) == "" {
231+ return nil , errors .New ("branch is required" )
232+ }
233+
234+ parsedRepoURL , err := parseGiteaRepositoryURL (repo .Url )
235+ if err != nil {
236+ return nil , fmt .Errorf ("invalid repository URL: %w" , err )
237+ }
238+
239+ apiURL := fmt .Sprintf (
240+ "%s/api/v1/repos/%s/%s/git/trees/%s?recursive=true" ,
241+ parsedRepoURL .baseURL ,
242+ url .PathEscape (parsedRepoURL .owner ),
243+ url .PathEscape (parsedRepoURL .repo ),
244+ url .PathEscape (branch ),
245+ )
246+
247+ req , err := httpclient .NewRequest (ctx , http .MethodGet , apiURL , nil )
248+ if err != nil {
249+ return nil , fmt .Errorf ("failed to build Gitea request: %w" , err )
250+ }
251+ addGiteaHeaders (req , repo )
252+
253+ resp , err := httpclient .Default .Do (req )
254+ if err != nil {
255+ return nil , fmt .Errorf ("failed to fetch Gitea repository tree: %w" , err )
256+ }
257+ defer func () {
258+ err := resp .Body .Close ()
259+ if err != nil {
260+ fmt .Printf ("warning: failed to close Gitea response body: %v\n " , err )
261+ }
262+ }()
263+
264+ switch resp .StatusCode {
265+ case http .StatusOK :
266+ var parsed giteaTreeResponse
267+ if err := json .NewDecoder (resp .Body ).Decode (& parsed ); err != nil {
268+ return nil , fmt .Errorf ("failed to decode Gitea tree response: %w" , err )
269+ }
270+
271+ entries := make ([]TreeEntry , 0 , len (parsed .Tree ))
272+ for _ , entry := range parsed .Tree {
273+ if entry .Path == "" {
274+ continue
275+ }
276+
277+ switch entry .Type {
278+ case "blob" :
279+ entries = append (entries , TreeEntry {Path : entry .Path , Type : TreeEntryTypeFile })
280+ case "tree" :
281+ entries = append (entries , TreeEntry {Path : entry .Path , Type : TreeEntryTypeDir })
282+ }
283+ }
284+
285+ sort .Slice (entries , func (i , j int ) bool {
286+ return entries [i ].Path < entries [j ].Path
287+ })
288+
289+ return entries , nil
290+ case http .StatusUnauthorized , http .StatusForbidden :
291+ return nil , errors .New ("authentication failed or access denied" )
292+ case http .StatusNotFound :
293+ return nil , errors .New ("repository not found or access denied" )
294+ default :
295+ return nil , fmt .Errorf ("gitea API returned unexpected status: %d" , resp .StatusCode )
296+ }
297+ }
298+
299+ func addGiteaHeaders (req * http.Request , repo * models.Repository ) {
300+ req .Header .Set ("Accept" , "application/json" )
301+
302+ if repo .AuthMethod == models .AuthMethodToken && repo .AuthToken != nil {
303+ token := strings .TrimSpace (repo .AuthToken .String ())
304+ if token != "" {
305+ req .Header .Set ("Authorization" , "token " + token )
306+ }
307+ }
308+ }
0 commit comments