Skip to content

Commit 9d60ff7

Browse files
aThorp96soharaa
authored andcommitted
fix(gitlab): ensure GetFiles and CreateComment pages over API results
Gitlab's commit-diff API defaults to return 20 files per page. Without paging through the files, cel funcs like `files.all` and `pathChanged` will only match 20 files on push events. This commits also adds paging logic when listing commits in the gitlab CreateComment function. Previously, only the first 100 notes on a MR were compared when finding the note to update. Paging through the list of merge request notes ensures that if a MR has more than 100 comments we will still update the appropriate comment. This commit also creates default gitlab paging options which are used by all gitlab API calls that paginate through the response. Co-authored-by: Cursor <[email protected]>
1 parent e6870e6 commit 9d60ff7

File tree

4 files changed

+208
-34
lines changed

4 files changed

+208
-34
lines changed

pkg/provider/gitlab/acl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (v *Provider) checkOkToTestCommentFromApprovedMember(ctx context.Context, e
7272
}
7373

7474
var nextPage int
75-
opt := &gitlab.ListMergeRequestDiscussionsOptions{Page: page}
75+
opt := &gitlab.ListMergeRequestDiscussionsOptions{Page: page, PerPage: defaultGitlabListOptions.PerPage}
7676
discussions, resp, err := v.Client().Discussions.ListMergeRequestDiscussions(v.targetProjectID, event.PullRequestNumber, opt)
7777
if err != nil || len(discussions) == 0 {
7878
return false, err

pkg/provider/gitlab/gitlab.go

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ type Provider struct {
6868
cachedChangedFiles *changedfiles.ChangedFiles
6969
}
7070

71+
var defaultGitlabListOptions = gitlab.ListOptions{
72+
PerPage: 100,
73+
}
74+
7175
func (v *Provider) Client() *gitlab.Client {
7276
providerMetrics.RecordAPIUsage(
7377
v.Logger,
@@ -99,32 +103,46 @@ func (v *Provider) CreateComment(_ context.Context, event *info.Event, commit, u
99103

100104
// List comments of the merge request
101105
if updateMarker != "" {
102-
comments, _, err := v.Client().Notes.ListMergeRequestNotes(event.TargetProjectID, event.PullRequestNumber, &gitlab.ListMergeRequestNotesOptions{
103-
ListOptions: gitlab.ListOptions{
104-
Page: 1,
105-
PerPage: 100,
106-
},
107-
})
108-
if err != nil {
109-
return err
110-
}
106+
commentRe := regexp.MustCompile(updateMarker)
107+
options := []gitlab.RequestOptionFunc{}
111108

112-
re := regexp.MustCompile(updateMarker)
113-
for _, comment := range comments {
114-
if re.MatchString(comment.Body) {
115-
_, _, err := v.Client().Notes.UpdateMergeRequestNote(event.TargetProjectID, event.PullRequestNumber, comment.ID, &gitlab.UpdateMergeRequestNoteOptions{
116-
Body: &commit,
117-
})
109+
for {
110+
comments, resp, err := v.Client().Notes.ListMergeRequestNotes(event.TargetProjectID, event.PullRequestNumber, &gitlab.ListMergeRequestNotesOptions{ListOptions: defaultGitlabListOptions}, options...)
111+
if err != nil {
118112
return err
119113
}
114+
115+
for _, comment := range comments {
116+
if commentRe.MatchString(comment.Body) {
117+
_, _, err := v.Client().Notes.UpdateMergeRequestNote(event.TargetProjectID, event.PullRequestNumber, comment.ID, &gitlab.UpdateMergeRequestNoteOptions{
118+
Body: &commit,
119+
})
120+
if err != nil {
121+
return fmt.Errorf("unable to update merge request note: %w", err)
122+
}
123+
return nil
124+
}
125+
}
126+
127+
// Exit the loop when we've seen all pages.
128+
if resp.NextLink == "" {
129+
break
130+
}
131+
132+
// Otherwise, set param to query the next page
133+
options = []gitlab.RequestOptionFunc{
134+
gitlab.WithKeysetPaginationParameters(resp.NextLink),
135+
}
120136
}
121137
}
122138

123139
_, _, err := v.Client().Notes.CreateMergeRequestNote(event.TargetProjectID, event.PullRequestNumber, &gitlab.CreateMergeRequestNoteOptions{
124140
Body: &commit,
125141
})
126-
127-
return err
142+
if err != nil {
143+
return fmt.Errorf("unable to create merge request note: %w", err)
144+
}
145+
return nil
128146
}
129147

130148
// CheckPolicyAllowing TODO: Implement ME.
@@ -386,7 +404,7 @@ func (v *Provider) GetTektonDir(_ context.Context, event *info.Event, path, prov
386404
ListOptions: gitlab.ListOptions{
387405
OrderBy: "id",
388406
Pagination: "keyset",
389-
PerPage: 20,
407+
PerPage: defaultGitlabListOptions.PerPage,
390408
Sort: "asc",
391409
},
392410
}
@@ -524,7 +542,7 @@ func (v *Provider) fetchChangedFiles(_ context.Context, runevent *info.Event) (c
524542
ListOptions: gitlab.ListOptions{
525543
OrderBy: "id",
526544
Pagination: "keyset",
527-
PerPage: 20,
545+
PerPage: defaultGitlabListOptions.PerPage,
528546
Sort: "asc",
529547
},
530548
}
@@ -564,23 +582,38 @@ func (v *Provider) fetchChangedFiles(_ context.Context, runevent *info.Event) (c
564582
}
565583
}
566584
case triggertype.Push:
567-
pushChanges, _, err := v.Client().Commits.GetCommitDiff(v.sourceProjectID, runevent.SHA, &gitlab.GetCommitDiffOptions{})
568-
if err != nil {
569-
return changedfiles.ChangedFiles{}, err
570-
}
571-
for _, change := range pushChanges {
572-
changedFiles.All = append(changedFiles.All, change.NewPath)
573-
if change.NewFile {
574-
changedFiles.Added = append(changedFiles.Added, change.NewPath)
585+
options := gitlab.GetCommitDiffOptions{ListOptions: defaultGitlabListOptions}
586+
pageOpts := []gitlab.RequestOptionFunc{}
587+
588+
for {
589+
pushChanges, resp, err := v.Client().Commits.GetCommitDiff(v.sourceProjectID, runevent.SHA, &options, pageOpts...)
590+
if err != nil {
591+
return changedfiles.ChangedFiles{}, err
575592
}
576-
if change.DeletedFile {
577-
changedFiles.Deleted = append(changedFiles.Deleted, change.NewPath)
593+
594+
for _, change := range pushChanges {
595+
changedFiles.All = append(changedFiles.All, change.NewPath)
596+
if change.NewFile {
597+
changedFiles.Added = append(changedFiles.Added, change.NewPath)
598+
}
599+
if change.DeletedFile {
600+
changedFiles.Deleted = append(changedFiles.Deleted, change.NewPath)
601+
}
602+
if !change.RenamedFile && !change.DeletedFile && !change.NewFile {
603+
changedFiles.Modified = append(changedFiles.Modified, change.NewPath)
604+
}
605+
if change.RenamedFile {
606+
changedFiles.Renamed = append(changedFiles.Renamed, change.NewPath)
607+
}
578608
}
579-
if !change.RenamedFile && !change.DeletedFile && !change.NewFile {
580-
changedFiles.Modified = append(changedFiles.Modified, change.NewPath)
609+
610+
if resp.NextLink == "" {
611+
// Exit the loop when we've seen all pages.
612+
break
581613
}
582-
if change.RenamedFile {
583-
changedFiles.Renamed = append(changedFiles.Renamed, change.NewPath)
614+
// Otherwise, set param to query the next page
615+
pageOpts = []gitlab.RequestOptionFunc{
616+
gitlab.WithKeysetPaginationParameters(resp.NextLink),
584617
}
585618
}
586619
default:

pkg/provider/gitlab/gitlab_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,72 @@ func TestGetFiles(t *testing.T) {
13061306
}
13071307
}
13081308

1309+
func TestGetFilesPaging(t *testing.T) {
1310+
tests := []struct {
1311+
name string
1312+
event *info.Event
1313+
}{
1314+
{
1315+
name: "pull-request",
1316+
event: &info.Event{
1317+
TriggerTarget: "pull_request",
1318+
Organization: "owner",
1319+
Repository: "repository",
1320+
},
1321+
},
1322+
{
1323+
name: "push",
1324+
event: &info.Event{
1325+
TriggerTarget: "push",
1326+
Organization: "owner",
1327+
Repository: "repository",
1328+
SHA: "shacommitinfo",
1329+
},
1330+
},
1331+
}
1332+
for _, tt := range tests {
1333+
t.Run(tt.name, func(t *testing.T) {
1334+
ctx, _ := rtesting.SetupFakeContext(t)
1335+
metrics.ResetMetrics()
1336+
fakeclient, mux, teardown := thelp.Setup(t)
1337+
defer teardown()
1338+
1339+
if tt.event.TriggerTarget == "pull_request" {
1340+
mux.HandleFunc(fmt.Sprintf("/projects/0/merge_requests/%d/diffs",
1341+
tt.event.PullRequestNumber), func(rw http.ResponseWriter, req *http.Request) {
1342+
pageCount := thelp.SetPagingHeader(t, rw, req, 5)
1343+
jeez, err := json.Marshal([]*gitlab.MergeRequestDiff{{NewPath: fmt.Sprintf("change-%d.txt", pageCount)}})
1344+
assert.NilError(t, err)
1345+
_, _ = rw.Write(jeez)
1346+
})
1347+
}
1348+
if tt.event.TriggerTarget == "push" {
1349+
mux.HandleFunc(fmt.Sprintf("/projects/0/repository/commits/%s/diff",
1350+
tt.event.SHA), func(rw http.ResponseWriter, req *http.Request) {
1351+
pageCount := thelp.SetPagingHeader(t, rw, req, 5)
1352+
jeez, err := json.Marshal([]*gitlab.Diff{{NewPath: fmt.Sprintf("change-%d.txt", pageCount)}})
1353+
assert.NilError(t, err)
1354+
_, _ = rw.Write(jeez)
1355+
})
1356+
}
1357+
1358+
metricsTags := map[string]string{"provider": "api.gitlab.com", "event-type": string(tt.event.TriggerTarget)}
1359+
metricstest.CheckStatsNotReported(t, "pipelines_as_code_git_provider_api_request_count")
1360+
1361+
providerInfo := &Provider{gitlabClient: fakeclient, sourceProjectID: 0, targetProjectID: 0, triggerEvent: string(tt.event.TriggerTarget), apiURL: "api.gitlab.com"}
1362+
changedFiles, err := providerInfo.GetFiles(ctx, tt.event)
1363+
assert.NilError(t, err, nil)
1364+
assert.DeepEqual(t, changedFiles.All, []string{"change-1.txt", "change-2.txt", "change-3.txt", "change-4.txt", "change-5.txt"})
1365+
assert.Equal(t, len(changedFiles.Modified), 5)
1366+
1367+
// Check caching
1368+
metricstest.CheckCountData(t, "pipelines_as_code_git_provider_api_request_count", metricsTags, 5)
1369+
_, _ = providerInfo.GetFiles(ctx, tt.event)
1370+
metricstest.CheckCountData(t, "pipelines_as_code_git_provider_api_request_count", metricsTags, 5)
1371+
})
1372+
}
1373+
}
1374+
13091375
func TestIsHeadCommitOfBranch(t *testing.T) {
13101376
tests := []struct {
13111377
name string
@@ -1484,3 +1550,50 @@ func TestGitLabCreateComment(t *testing.T) {
14841550
})
14851551
}
14861552
}
1553+
1554+
func TestGitLabCreateCommentPaging(t *testing.T) {
1555+
updated := false
1556+
event := &info.Event{PullRequestNumber: 123, TargetProjectID: 666}
1557+
commit := "Updated Comment"
1558+
updateMarker := "MARKER"
1559+
mockResponses := map[string]func(rw http.ResponseWriter, _ *http.Request){
1560+
"/projects/666/merge_requests/123/notes": func(rw http.ResponseWriter, r *http.Request) {
1561+
if r.Method == http.MethodGet {
1562+
page := thelp.SetPagingHeader(t, rw, r, 100)
1563+
note := "unrelated"
1564+
if page == 10 {
1565+
note = "MARKER"
1566+
} else if page > 10 {
1567+
t.Error("notes shouldn't be queries past the expected ID")
1568+
}
1569+
fmt.Fprintf(rw, `[{"id": %d, "body": "%s"}]`, page, note)
1570+
}
1571+
},
1572+
"/projects/666/merge_requests/123/notes/{id}": func(rw http.ResponseWriter, r *http.Request) {
1573+
assert.Equal(t, r.Method, "PUT")
1574+
if r.PathValue("id") == "10" {
1575+
rw.WriteHeader(http.StatusOK)
1576+
updated = true
1577+
} else {
1578+
rw.WriteHeader(http.StatusNotFound)
1579+
t.Errorf("note %s is not intended to be updated", r.PathValue("id"))
1580+
}
1581+
fmt.Fprint(rw, `{}`)
1582+
},
1583+
}
1584+
1585+
fakeclient, mux, teardown := thelp.Setup(t)
1586+
defer teardown()
1587+
1588+
for endpoint, handler := range mockResponses {
1589+
mux.HandleFunc(endpoint, handler)
1590+
}
1591+
1592+
p := &Provider{
1593+
sourceProjectID: 666,
1594+
gitlabClient: fakeclient,
1595+
}
1596+
err := p.CreateComment(context.Background(), event, commit, updateMarker)
1597+
assert.NilError(t, err)
1598+
assert.Assert(t, updated == true, "comment update handler has not been called")
1599+
}

pkg/provider/gitlab/test/test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"net/http/httptest"
88
"os"
9+
"strconv"
910
"strings"
1011
"testing"
1112

@@ -176,6 +177,33 @@ func MuxGetFile(mux *http.ServeMux, pid int, fname, content string, wantErr bool
176177
})
177178
}
178179

180+
// SetPagingHeader sets the gitlab paging header "link" to the next page. If the requested page
181+
// is equal to or higher than maxPage, no next page header is set. The requested page number
182+
// is returned.
183+
func SetPagingHeader(t *testing.T, rw http.ResponseWriter, req *http.Request, maxPage int) int {
184+
t.Helper()
185+
url := req.URL
186+
query := url.Query()
187+
lastPageStr := query.Get("page")
188+
if lastPageStr == "" {
189+
lastPageStr = "1"
190+
}
191+
lastPage, err := strconv.Atoi(lastPageStr)
192+
if err != nil {
193+
t.Errorf("unable to parse page number %s: %v", lastPageStr, err)
194+
return 0
195+
}
196+
197+
if lastPage < maxPage {
198+
query.Set("page", strconv.Itoa(lastPage+1))
199+
url.RawQuery = query.Encode()
200+
header := fmt.Sprintf("<%s>; rel=\"next\",", url.String())
201+
rw.Header().Add("link", header)
202+
}
203+
204+
return lastPage
205+
}
206+
179207
type TEvent struct {
180208
Username, DefaultBranch, URL, SHA, SHAurl, SHAtitle, Headbranch, Basebranch, HeadURL, BaseURL string
181209
UserID, MRID, TargetProjectID, SourceProjectID, NoteID int

0 commit comments

Comments
 (0)