@@ -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
1414type 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
2636func (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
3141func (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+ }
0 commit comments