Skip to content

Commit 40895dc

Browse files
enhance bitbucket pr attestation to capture more details (#582)
* enhance bitbucket pr attestation to capture more details * only include PR commits for v2 attestations
1 parent 83238ce commit 40895dc

File tree

3 files changed

+140
-64
lines changed

3 files changed

+140
-64
lines changed

cmd/kosli/pullrequest.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ func (o *attestPROptions) run(args []string) error {
125125
o.payload.GitProvider, label = getGitProviderAndLabel(o.retriever)
126126

127127
var pullRequestsEvidence []*types.PREvidence
128-
if o.payload.GitProvider == "github" || o.payload.GitProvider == "gitlab" {
129-
pullRequestsEvidence, err = o.getRetriever().PREvidenceForCommitV2(o.payload.Commit.Sha1)
130-
} else {
128+
if o.payload.GitProvider == "azure" {
131129
pullRequestsEvidence, err = o.getRetriever().PREvidenceForCommitV1(o.payload.Commit.Sha1)
130+
} else {
131+
pullRequestsEvidence, err = o.getRetriever().PREvidenceForCommitV2(o.payload.Commit.Sha1)
132132
}
133133
if err != nil {
134134
return err

internal/bitbucket/bitbucket.go

Lines changed: 136 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import (
44
"encoding/json"
55
"fmt"
66
"net/http"
7+
"time"
78

89
"github.com/kosli-dev/cli/internal/logger"
910
"github.com/kosli-dev/cli/internal/requests"
1011
"github.com/kosli-dev/cli/internal/types"
11-
"github.com/kosli-dev/cli/internal/utils"
1212
)
1313

1414
type Config struct {
@@ -22,17 +22,27 @@ type Config struct {
2222
Assert bool
2323
}
2424

25+
// parseRFC3339NanoTimestamp parses a timestamp string in RFC3339Nano format and returns its Unix timestamp.
26+
// The fieldName parameter is used for error messages to identify which field failed to parse.
27+
func parseRFC3339NanoTimestamp(timestampStr, fieldName string) (int64, error) {
28+
parsedTime, err := time.Parse(time.RFC3339Nano, timestampStr)
29+
if err != nil {
30+
return 0, fmt.Errorf("failed to parse %s timestamp: %w", fieldName, err)
31+
}
32+
return parsedTime.Unix(), nil
33+
}
34+
2535
// This is the old implementation, it will be removed after the PR payload is enhanced for Bitbucket
2636
func (c *Config) PREvidenceForCommitV1(commit string) ([]*types.PREvidence, error) {
27-
return c.getPullRequestsFromBitbucketApi(commit)
37+
return c.getPullRequestsFromBitbucketApi(commit, 1)
2838
}
2939

3040
// This is the new implementation, it will be used for Bitbucket
3141
func (c *Config) PREvidenceForCommitV2(commit string) ([]*types.PREvidence, error) {
32-
return []*types.PREvidence{}, nil
42+
return c.getPullRequestsFromBitbucketApi(commit, 2)
3343
}
3444

35-
func (c *Config) getPullRequestsFromBitbucketApi(commit string) ([]*types.PREvidence, error) {
45+
func (c *Config) getPullRequestsFromBitbucketApi(commit string, version int) ([]*types.PREvidence, error) {
3646
pullRequestsEvidence := []*types.PREvidence{}
3747

3848
url := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s/pullrequests", c.Workspace, c.Repository, commit)
@@ -49,39 +59,41 @@ func (c *Config) getPullRequestsFromBitbucketApi(commit string) ([]*types.PREvid
4959
if err != nil {
5060
return pullRequestsEvidence, err
5161
}
52-
if response.Resp.StatusCode == 200 {
53-
pullRequestsEvidence, err = c.parseBitbucketResponse(commit, response)
62+
switch response.Resp.StatusCode {
63+
case 200:
64+
pullRequestsEvidence, err = c.parseBitbucketResponse(commit, response, version)
5465
if err != nil {
5566
return pullRequestsEvidence, err
5667
}
57-
} else if response.Resp.StatusCode == 202 {
68+
case 202:
5869
return pullRequestsEvidence, fmt.Errorf("repository pull requests are still being indexed, please retry")
59-
} else if response.Resp.StatusCode == 404 {
70+
case 404:
6071
return pullRequestsEvidence, fmt.Errorf("repository does not exist or pull requests are not indexed." +
6172
"Please make sure Pull Request Commit Links app is installed")
62-
} else {
73+
default:
6374
return pullRequestsEvidence, fmt.Errorf("failed to get pull requests from Bitbucket: %v", response.Body)
6475
}
6576
return pullRequestsEvidence, nil
6677
}
6778

68-
func (c *Config) parseBitbucketResponse(commit string, response *requests.HTTPResponse) ([]*types.PREvidence, error) {
79+
// parseBitbucketResponse parses the response from the Bitbucket API and returns the pull requests evidence
80+
func (c *Config) parseBitbucketResponse(commit string, response *requests.HTTPResponse, version int) ([]*types.PREvidence, error) {
6981
pullRequestsEvidence := []*types.PREvidence{}
70-
var responseData map[string]interface{}
82+
var responseData map[string]any
7183
err := json.Unmarshal([]byte(response.Body), &responseData)
7284
if err != nil {
7385
return pullRequestsEvidence, err
7486
}
75-
pullRequests, ok := responseData["values"].([]interface{})
87+
pullRequests, ok := responseData["values"].([]any)
7688
if !ok {
7789
return pullRequestsEvidence, nil
7890
}
7991
for _, prInterface := range pullRequests {
80-
pr := prInterface.(map[string]interface{})
81-
linksInterface := pr["links"].(map[string]interface{})
82-
apiLinkMap := linksInterface["self"].(map[string]interface{})
83-
htmlLinkMap := linksInterface["html"].(map[string]interface{})
84-
evidence, err := c.getPullRequestDetailsFromBitbucket(apiLinkMap["href"].(string), htmlLinkMap["href"].(string), commit)
92+
pr := prInterface.(map[string]any)
93+
linksInterface := pr["links"].(map[string]any)
94+
apiLinkMap := linksInterface["self"].(map[string]any)
95+
htmlLinkMap := linksInterface["html"].(map[string]any)
96+
evidence, err := c.getPullRequestDetailsFromBitbucket(apiLinkMap["href"].(string), htmlLinkMap["href"].(string), commit, version)
8597
if err != nil {
8698
return pullRequestsEvidence, err
8799
}
@@ -91,7 +103,8 @@ func (c *Config) parseBitbucketResponse(commit string, response *requests.HTTPRe
91103
return pullRequestsEvidence, nil
92104
}
93105

94-
func (c *Config) getPullRequestDetailsFromBitbucket(prApiUrl, prHtmlLink, commit string) (*types.PREvidence, error) {
106+
// getPullRequestDetailsFromBitbucket gets the details of a pull request from the Bitbucket API
107+
func (c *Config) getPullRequestDetailsFromBitbucket(prApiUrl, prHtmlLink, commit string, version int) (*types.PREvidence, error) {
95108
c.Logger.Debug("getting pull request details for " + prApiUrl)
96109
evidence := &types.PREvidence{}
97110

@@ -107,7 +120,7 @@ func (c *Config) getPullRequestDetailsFromBitbucket(prApiUrl, prHtmlLink, commit
107120
return evidence, err
108121
}
109122
if response.Resp.StatusCode == 200 {
110-
var responseData map[string]interface{}
123+
var responseData map[string]any
111124
err := json.Unmarshal([]byte(response.Body), &responseData)
112125
if err != nil {
113126
return evidence, err
@@ -116,59 +129,122 @@ func (c *Config) getPullRequestDetailsFromBitbucket(prApiUrl, prHtmlLink, commit
116129
evidence.URL = prHtmlLink
117130
evidence.MergeCommit = commit
118131
evidence.State = responseData["state"].(string)
119-
participants := responseData["participants"].([]interface{})
120-
approvers := []string{}
132+
participants := responseData["participants"].([]any)
133+
approvers := []any{}
121134

122135
if len(participants) > 0 {
123136
for _, participantInterface := range participants {
124-
p := participantInterface.(map[string]interface{})
137+
p := participantInterface.(map[string]any)
125138
if p["approved"].(bool) {
126-
user := p["user"].(map[string]interface{})
127-
approvers = append(approvers, user["display_name"].(string))
139+
user := p["user"].(map[string]any)
140+
if version == 1 {
141+
approvers = append(approvers, user["display_name"].(string))
142+
} else {
143+
approvalTimestamp, err := parseRFC3339NanoTimestamp(p["participated_on"].(string), "participated_on")
144+
if err != nil {
145+
return evidence, err
146+
}
147+
approvers = append(approvers, types.PRApprovals{
148+
Username: user["display_name"].(string),
149+
State: p["state"].(string),
150+
Timestamp: approvalTimestamp,
151+
})
152+
}
128153
}
129154
}
155+
evidence.Approvers = approvers
130156
} else {
131157
c.Logger.Debug("no approvers found")
132158
}
133-
evidence.Approvers = utils.ConvertStringListToInterfaceList(approvers)
134-
// prID := int(responseData["id"].(float64))
135-
// evidence.LastCommit, evidence.LastCommitter, err = getBitbucketPRLastCommit(workspace, repository, username, password, prID)
136-
// if err != nil {
137-
// return evidence, err
138-
// }
139-
// if utils.Contains(approvers, evidence.LastCommitter) {
140-
// evidence.SelfApproved = true
141-
// }
159+
if version == 2 {
160+
evidence.Author = responseData["author"].(map[string]any)["display_name"].(string)
161+
createdAt, err := parseRFC3339NanoTimestamp(responseData["created_on"].(string), "created_on")
162+
if err != nil {
163+
return evidence, err
164+
}
165+
evidence.CreatedAt = createdAt
166+
mergedAt, err := parseRFC3339NanoTimestamp(responseData["updated_on"].(string), "updated_on")
167+
if err != nil {
168+
return evidence, err
169+
}
170+
evidence.MergedAt = mergedAt
171+
evidence.Title = responseData["title"].(string)
172+
evidence.HeadRef = responseData["source"].(map[string]any)["branch"].(map[string]any)["name"].(string)
173+
174+
prCommits, err := c.getPullRequestCommitsFromBitbucket(int(responseData["id"].(float64)))
175+
if err != nil {
176+
return evidence, err
177+
}
178+
evidence.Commits = prCommits
179+
}
142180
} else {
143181
return evidence, fmt.Errorf("failed to get PR details, got HTTP status %d. Please review repository permissions", response.Resp.StatusCode)
144182
}
145183
return evidence, nil
146184
}
147185

148-
// func getBitbucketPRLastCommit(workspace, repository, username, password string, prID int) (string, string, error) {
149-
// url := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/pullrequests/%d/commits", workspace, repository, prID)
150-
// log.Debug("Getting pull requests commits from" + url)
151-
// response, err := requests.SendPayload([]byte{}, url, username, password,
152-
// global.MaxAPIRetries, false, http.MethodGet, log)
153-
// if err != nil {
154-
// return "", "", err
155-
// }
156-
157-
// if response.Resp.StatusCode == 200 {
158-
// var responseData map[string]interface{}
159-
// err := json.Unmarshal([]byte(response.Body), &responseData)
160-
// if err != nil {
161-
// return "", "", err
162-
// }
163-
// prCommits := responseData["values"].([]interface{})
164-
165-
// // the first commit is the merge commit
166-
// // TODO: is it safe to always to get the second commit?
167-
// lastCommit := prCommits[1].(map[string]interface{})
168-
// lastAuthor := lastCommit["author"].(map[string]interface{})
169-
// return lastCommit["hash"].(string), lastAuthor["user"].(map[string]interface{})["display_name"].(string), nil
170-
171-
// } else {
172-
// return "", "", fmt.Errorf("failed to get PR commits, got HTTP status %d", response.Resp.StatusCode)
173-
// }
174-
// }
186+
// getPullRequestCommitsFromBitbucket gets the commits of a pull request from the Bitbucket API
187+
func (c *Config) getPullRequestCommitsFromBitbucket(prID int) ([]types.Commit, error) {
188+
url := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/pullrequests/%d/commits", c.Workspace, c.Repository, prID)
189+
c.Logger.Debug("getting pull request commits from " + url)
190+
191+
allCommits := []types.Commit{}
192+
currentURL := url
193+
194+
for currentURL != "" {
195+
reqParams := &requests.RequestParams{
196+
Method: http.MethodGet,
197+
URL: currentURL,
198+
Username: c.Username,
199+
Password: c.Password,
200+
Token: c.AccessToken,
201+
}
202+
response, err := c.KosliClient.Do(reqParams)
203+
if err != nil {
204+
return nil, err
205+
}
206+
if response.Resp.StatusCode != 200 {
207+
return nil, fmt.Errorf("failed to get PR commits, got HTTP status %d", response.Resp.StatusCode)
208+
}
209+
210+
var responseData map[string]any
211+
err = json.Unmarshal([]byte(response.Body), &responseData)
212+
if err != nil {
213+
return nil, err
214+
}
215+
216+
commits, ok := responseData["values"].([]any)
217+
if !ok {
218+
break
219+
}
220+
221+
for _, commitInterface := range commits {
222+
commit := commitInterface.(map[string]any)
223+
timestamp, err := parseRFC3339NanoTimestamp(commit["date"].(string), "date")
224+
if err != nil {
225+
return nil, err
226+
}
227+
allCommits = append(allCommits, types.Commit{
228+
SHA: commit["hash"].(string),
229+
Message: commit["message"].(string),
230+
Committer: commit["author"].(map[string]any)["raw"].(string),
231+
Timestamp: timestamp,
232+
URL: commit["links"].(map[string]any)["html"].(map[string]any)["href"].(string),
233+
})
234+
}
235+
236+
// Check for next page
237+
nextInterface, hasNext := responseData["next"]
238+
if !hasNext {
239+
break
240+
}
241+
nextURL, ok := nextInterface.(string)
242+
if !ok || nextURL == "" {
243+
break
244+
}
245+
currentURL = nextURL
246+
c.Logger.Debug("fetching next page of commits from " + currentURL)
247+
}
248+
249+
return allCommits, nil
250+
}

internal/github/github.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ func (c *GithubConfig) PREvidenceForCommitV2(commit string) ([]*types.PREvidence
186186
MergedAt: mergedAt,
187187
Title: string(pr.Title),
188188
HeadRef: string(pr.HeadRefName),
189-
Approvers: []interface{}{},
189+
Approvers: []any{},
190190
Commits: []types.Commit{},
191191
}
192192

0 commit comments

Comments
 (0)