Skip to content

Commit b81eaf5

Browse files
SergKMykolaMarusenko
authored andcommitted
feat: add pull/merge request listing API across GitHub, GitLab, and Bitbucket
Implement end-to-end pull request retrieval capability exposing a unified /api/v1/pull-requests endpoint that abstracts provider differences: - Add OpenAPI spec defining pull request list endpoint with state filtering (open/closed/merged/all), pagination support (page/perPage), and normalized PullRequest schema across all providers - Implement multi-provider service layer with sturdyc caching (2-min TTL) for efficient repeated queries, supporting independent cache per state/page combo - Add provider implementations for GitHub (post-filter merged status), GitLab (direct state mapping), and Bitbucket (direct HTTP API) - Integrate K8s GitServer CR resolution to dynamically retrieve provider credentials from cluster state Enables portal to surface code repository pull/merge requests as browsable data within codebase detail views, unifying PR discovery across multiple VCS providers through a single REST API. Signed-off-by: Sergiy Kulanov <sergiy_kulanov@epam.com>
1 parent 942697e commit b81eaf5

22 files changed

+3594
-36
lines changed

go.mod

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ require (
77
github.com/epam/edp-common v0.0.0-20230710145648-344bbce4120e
88
github.com/go-chi/chi/v5 v5.2.2
99
github.com/go-chi/httplog/v2 v2.1.1
10+
github.com/go-resty/resty/v2 v2.17.1
1011
github.com/google/go-github/v72 v72.0.0
1112
github.com/ktrysmt/go-bitbucket v0.9.85
1213
github.com/oapi-codegen/runtime v1.1.1
1314
github.com/stretchr/testify v1.10.0
1415
github.com/viccon/sturdyc v1.1.5
1516
gitlab.com/gitlab-org/api/client-go v0.128.0
16-
golang.org/x/sync v0.13.0
17+
golang.org/x/sync v0.16.0
1718
k8s.io/api v0.32.1
1819
k8s.io/apimachinery v0.32.1
1920
k8s.io/client-go v0.32.1
@@ -33,7 +34,6 @@ require (
3334
github.com/go-openapi/jsonpointer v0.21.0 // indirect
3435
github.com/go-openapi/jsonreference v0.20.2 // indirect
3536
github.com/go-openapi/swag v0.23.0 // indirect
36-
github.com/go-resty/resty/v2 v2.6.0 // indirect
3737
github.com/gogo/protobuf v1.3.2 // indirect
3838
github.com/golang/protobuf v1.5.4 // indirect
3939
github.com/google/btree v1.1.3 // indirect
@@ -60,12 +60,12 @@ require (
6060
github.com/spf13/pflag v1.0.5 // indirect
6161
github.com/x448/float16 v0.8.4 // indirect
6262
go.uber.org/multierr v1.11.0 // indirect
63-
golang.org/x/net v0.39.0 // indirect
63+
golang.org/x/net v0.43.0 // indirect
6464
golang.org/x/oauth2 v0.29.0 // indirect
65-
golang.org/x/sys v0.32.0 // indirect
66-
golang.org/x/term v0.31.0 // indirect
67-
golang.org/x/text v0.24.0 // indirect
68-
golang.org/x/time v0.10.0 // indirect
65+
golang.org/x/sys v0.35.0 // indirect
66+
golang.org/x/term v0.34.0 // indirect
67+
golang.org/x/text v0.28.0 // indirect
68+
golang.org/x/time v0.12.0 // indirect
6969
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
7070
google.golang.org/protobuf v1.35.1 // indirect
7171
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect

go.sum

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En
4343
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
4444
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
4545
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
46-
github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4=
47-
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
46+
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
47+
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
4848
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
4949
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
5050
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -165,38 +165,34 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
165165
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
166166
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
167167
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
168-
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
169-
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
170-
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
168+
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
169+
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
171170
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
172171
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
173172
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
174173
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
175174
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
176-
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
177-
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
175+
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
176+
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
178177
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
179178
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
180179
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
181-
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
182-
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
183-
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
184-
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
185-
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
186-
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
187-
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
180+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
181+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
182+
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
183+
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
188184
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
189185
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
190-
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
191-
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
192-
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
193-
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
186+
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
187+
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
188+
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
189+
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
194190
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
195191
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
196192
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
197193
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
198-
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
199-
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
194+
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
195+
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
200196
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
201197
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
202198
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

internal/api/oapi.yaml

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,69 @@ paths:
197197
schema:
198198
$ref: '#/components/schemas/Error'
199199

200+
/api/v1/pull-requests:
201+
get:
202+
summary: List pull/merge requests for a repository
203+
operationId: listPullRequests
204+
tags:
205+
- PullRequests
206+
parameters:
207+
- $ref: '#/components/parameters/gitServerParam'
208+
- $ref: '#/components/parameters/repoOwnerParam'
209+
- $ref: '#/components/parameters/repoNameParam'
210+
- name: state
211+
in: query
212+
required: false
213+
description: Filter by state. Defaults to open.
214+
schema:
215+
type: string
216+
enum: [open, closed, merged, all]
217+
default: open
218+
- name: page
219+
in: query
220+
required: false
221+
schema:
222+
type: integer
223+
default: 1
224+
- name: perPage
225+
in: query
226+
required: false
227+
schema:
228+
type: integer
229+
default: 20
230+
maximum: 100
231+
responses:
232+
'200':
233+
description: A list of pull/merge requests
234+
content:
235+
application/json:
236+
schema:
237+
$ref: '#/components/schemas/PullRequestsResponse'
238+
'400':
239+
description: Bad request due to invalid parameters or missing fields.
240+
content:
241+
application/json:
242+
schema:
243+
$ref: '#/components/schemas/Error'
244+
'401':
245+
description: Unauthorized access due to invalid credentials.
246+
content:
247+
application/json:
248+
schema:
249+
$ref: '#/components/schemas/Error'
250+
'404':
251+
description: Repository or git server not found.
252+
content:
253+
application/json:
254+
schema:
255+
$ref: '#/components/schemas/Error'
256+
'500':
257+
description: Internal server error.
258+
content:
259+
application/json:
260+
schema:
261+
$ref: '#/components/schemas/Error'
262+
200263
/api/v1/cache/invalidate:
201264
delete:
202265
summary: Invalidate cache for a specific endpoint
@@ -207,10 +270,10 @@ paths:
207270
- name: endpoint
208271
in: query
209272
required: true
210-
description: The endpoint name to invalidate cache for (repositories, organizations, branches)
273+
description: The endpoint name to invalidate cache for (repositories, organizations, branches, pullrequests)
211274
schema:
212275
type: string
213-
enum: [repositories, organizations, branches]
276+
enum: [repositories, organizations, branches, pullrequests]
214277
responses:
215278
'200':
216279
description: Cache invalidated successfully
@@ -300,6 +363,10 @@ components:
300363
properties:
301364
total:
302365
type: integer
366+
page:
367+
type: integer
368+
per_page:
369+
type: integer
303370
required:
304371
- total
305372
RepositoryDetails:
@@ -420,6 +487,54 @@ components:
420487
required:
421488
- key
422489
- value
490+
PullRequest:
491+
type: object
492+
properties:
493+
id:
494+
type: string
495+
number:
496+
type: integer
497+
title:
498+
type: string
499+
state:
500+
type: string
501+
enum: [open, closed, merged]
502+
author:
503+
$ref: '#/components/schemas/Owner'
504+
source_branch:
505+
type: string
506+
target_branch:
507+
type: string
508+
url:
509+
type: string
510+
created_at:
511+
type: string
512+
format: date-time
513+
updated_at:
514+
type: string
515+
format: date-time
516+
required:
517+
- id
518+
- number
519+
- title
520+
- state
521+
- source_branch
522+
- target_branch
523+
- url
524+
- created_at
525+
- updated_at
526+
PullRequestsResponse:
527+
type: object
528+
properties:
529+
data:
530+
type: array
531+
items:
532+
$ref: '#/components/schemas/PullRequest'
533+
pagination:
534+
$ref: '#/components/schemas/Pagination'
535+
required:
536+
- data
537+
- pagination
423538
PipelineResponse:
424539
type: object
425540
properties:
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
9+
gferrors "github.com/KubeRocketCI/gitfusion/internal/errors"
10+
"github.com/KubeRocketCI/gitfusion/internal/models"
11+
)
12+
13+
// pullRequestLister abstracts the pull-request listing capability
14+
// so the handler can be tested without a real service.
15+
type pullRequestLister interface {
16+
ListPullRequests(
17+
ctx context.Context,
18+
gitServerName, owner, repoName string,
19+
opts models.PullRequestListOptions,
20+
) (*models.PullRequestsResponse, error)
21+
}
22+
23+
// PullRequestHandler handles requests related to pull/merge requests (all providers).
24+
type PullRequestHandler struct {
25+
pullRequestsService pullRequestLister
26+
}
27+
28+
// NewPullRequestHandler creates a new PullRequestHandler.
29+
func NewPullRequestHandler(pullRequestsService pullRequestLister) *PullRequestHandler {
30+
return &PullRequestHandler{
31+
pullRequestsService: pullRequestsService,
32+
}
33+
}
34+
35+
// ListPullRequests implements api.StrictServerInterface.
36+
func (h *PullRequestHandler) ListPullRequests(
37+
ctx context.Context,
38+
request ListPullRequestsRequestObject,
39+
) (ListPullRequestsResponseObject, error) {
40+
// Apply defaults for optional parameters
41+
state := "open"
42+
if request.Params.State != nil {
43+
state = string(*request.Params.State)
44+
}
45+
46+
page := 1
47+
if request.Params.Page != nil {
48+
page = *request.Params.Page
49+
}
50+
51+
perPage := 20
52+
if request.Params.PerPage != nil {
53+
perPage = *request.Params.PerPage
54+
if perPage > 100 {
55+
perPage = 100
56+
}
57+
}
58+
59+
if page < 1 {
60+
page = 1
61+
}
62+
63+
if perPage < 1 {
64+
perPage = 20
65+
}
66+
67+
resp, err := h.pullRequestsService.ListPullRequests(
68+
ctx,
69+
request.Params.GitServer,
70+
request.Params.Owner,
71+
request.Params.RepoName,
72+
models.PullRequestListOptions{
73+
State: state,
74+
Page: page,
75+
PerPage: perPage,
76+
},
77+
)
78+
if err != nil {
79+
return h.errResponse(err), nil
80+
}
81+
82+
return ListPullRequests200JSONResponse(*resp), nil
83+
}
84+
85+
// errResponse maps errors to appropriate HTTP response objects.
86+
// This method must only be called when err is not nil.
87+
func (h *PullRequestHandler) errResponse(err error) ListPullRequestsResponseObject {
88+
if errors.Is(err, gferrors.ErrUnauthorized) {
89+
return ListPullRequests401JSONResponse{
90+
Code: fmt.Sprintf("%d", http.StatusUnauthorized),
91+
Message: err.Error(),
92+
}
93+
}
94+
95+
if errors.Is(err, gferrors.ErrNotFound) {
96+
return ListPullRequests404JSONResponse{
97+
Code: fmt.Sprintf("%d", http.StatusNotFound),
98+
Message: err.Error(),
99+
}
100+
}
101+
102+
return ListPullRequests500JSONResponse{
103+
Code: fmt.Sprintf("%d", http.StatusInternalServerError),
104+
Message: err.Error(),
105+
}
106+
}

0 commit comments

Comments
 (0)