-
-
Save morrisonbrett/a10b2b40993b8eb1b29f to your computer and use it in GitHub Desktop.
// | |
// Brett Morrison, June 2015 | |
// | |
// A simple program to display a list of open pull requests from BitBucket ✔ | |
// | |
package main | |
import ( | |
"encoding/json" | |
"flag" | |
"fmt" | |
"io" | |
"net/http" | |
"os" | |
) | |
// Setup the Authentication info | |
const bbBaseURL = "https://bitbucket.org/api/2.0" | |
var bitbucketOwnerName string | |
var bitbucketUserName string | |
var bitbucketPassword string | |
// | |
// Given a BB API, return JSON as a map interface | |
// | |
func getJSON(URL string) (map[string]interface{}, error) { | |
client := &http.Client{} | |
req, err := http.NewRequest("GET", URL, nil) | |
if err != nil { | |
return nil, fmt.Errorf("request error: %s", err) | |
} | |
req.SetBasicAuth(bitbucketUserName, bitbucketPassword) | |
res, err := client.Do(req) | |
if err != nil { | |
return nil, fmt.Errorf("request error: %s", err) | |
} | |
defer res.Body.Close() | |
if res.StatusCode != 200 { | |
return nil, fmt.Errorf("response code: %d", res.StatusCode) | |
} | |
var dat interface{} | |
decoder := json.NewDecoder(res.Body) | |
if err := decoder.Decode(&dat); err == io.EOF { | |
return nil, fmt.Errorf("decode error: %s", err) | |
} | |
jsonResponse := dat.(map[string]interface{}) | |
return jsonResponse, nil | |
} | |
func displayParticipantInfo(ID int, selfHrefLink string) error { | |
// Get more details about the PR | |
jsonResponseDet, err := getJSON(selfHrefLink) | |
if err != nil { | |
return err | |
} | |
// Get details about the participants | |
prsDet := jsonResponseDet["participants"] | |
prsDetI := prsDet.([]interface{}) | |
// For determining if the PR is ready to merge | |
numApprovedReviewers := 0 | |
numReviewers := 0 | |
// For each participant in the PR, display state depending on role | |
for _, value := range prsDetI { | |
valueMap := value.(map[string]interface{}) | |
role := valueMap["role"] | |
approved := valueMap["approved"] == true | |
displayName := valueMap["user"].(map[string]interface{})["display_name"] | |
// TODO Rewrite with one line RegEx? | |
var approvedS = " " | |
if approved { | |
approvedS = "X" | |
} | |
switch role { | |
case "REVIEWER": | |
fmt.Printf(" %s %s\n", approvedS, displayName) | |
numReviewers++ | |
if approved { | |
numApprovedReviewers++ | |
} | |
case "PARTICIPANT": | |
fmt.Printf(" %s (%s)\n", approvedS, displayName) | |
default: | |
fmt.Printf(" %s %s (%s)\n", approvedS, displayName, role) | |
} | |
} | |
var isOrNot = "IS NOT" | |
if numReviewers > 0 && numReviewers == numApprovedReviewers { | |
isOrNot = "IS" | |
} | |
fmt.Printf(" #%d %s READY TO MERGE, %d of %d REVIEWERS APPROVED\n\n", ID, isOrNot, numApprovedReviewers, numReviewers) | |
return nil | |
} | |
// | |
// Given a PR URL, iterate through state and print info | |
// | |
func listPR(pullRequestsLink string) error { | |
var prAPI = pullRequestsLink | |
// PR API has pagination, code for > 1 page | |
for len(prAPI) > 0 { | |
jsonResponse, err := getJSON(prAPI) | |
if err != nil { | |
return err | |
} | |
prs := jsonResponse["values"] | |
prsI := prs.([]interface{}) | |
// For each PR in the repo | |
for _, value := range prsI { | |
valueMap := value.(map[string]interface{}) | |
ID := int(valueMap["id"].(float64)) | |
// Display base info about the PR | |
fmt.Printf(" #%d %s (%s -> %s) by %s\n", | |
ID, | |
valueMap["title"], | |
valueMap["source"].(map[string]interface{})["branch"].(map[string]interface{})["name"], | |
valueMap["destination"].(map[string]interface{})["branch"].(map[string]interface{})["name"], | |
valueMap["author"].(map[string]interface{})["display_name"]) | |
// Prep the URL for more details about the PR | |
links := valueMap["links"] | |
self := links.(map[string]interface{})["self"] | |
selfHref := self.(map[string]interface{})["href"] | |
selfHrefLink := fmt.Sprint(selfHref) | |
// Display participant details about the PR | |
err := displayParticipantInfo(ID, selfHrefLink) | |
if err != nil { | |
return err | |
} | |
} | |
// Determine if there's more results - if so, loop control back | |
next := jsonResponse["next"] | |
if next != nil { | |
prAPI = fmt.Sprint(next) | |
} else { | |
prAPI = "" | |
} | |
} | |
return nil | |
} | |
func init() { | |
flag.StringVar(&bitbucketOwnerName, "ownername", "", "Bitbucket repository owner account") | |
flag.StringVar(&bitbucketUserName, "username", "", "Bitbucket account username") | |
flag.StringVar(&bitbucketPassword, "password", "", "Bitbucket account password") | |
} | |
func main() { | |
// Command line args | |
flag.Parse() | |
if len(os.Args) != 4 { | |
flag.Usage() | |
os.Exit(1) | |
} | |
var reposAPI = bbBaseURL + "/repositories/" + bitbucketOwnerName | |
// Repo API has pagination, code for > 1 page | |
for len(reposAPI) > 0 { | |
// Get the list of repos for this user / group | |
jsonResponse, err := getJSON(reposAPI) | |
if err != nil { | |
fmt.Println(err) | |
os.Exit(1) | |
} | |
repos := jsonResponse["values"] | |
reposI := repos.([]interface{}) | |
// For each repo, get the PR URL and process | |
for _, value := range reposI { | |
links := value.(map[string]interface{})["links"] | |
pullRequests := links.(map[string]interface{})["pullrequests"] | |
pullRequestsHref := pullRequests.(map[string]interface{})["href"] | |
pullRequestsLink := fmt.Sprint(pullRequestsHref) | |
fmt.Println("Repo:", pullRequestsLink) | |
err := listPR(pullRequestsLink) | |
if err != nil { | |
fmt.Println(err) // OK to continue here if error, no need to exit the program | |
} | |
} | |
// Determine if there's more results - if so, loop control back | |
next := jsonResponse["next"] | |
if next != nil { | |
reposAPI = fmt.Sprint(next) | |
} else { | |
reposAPI = "" | |
} | |
} | |
} |
In case this helps; I too had the same experience (and frustration) with working with JSON in Go. I was trying to use JSON like I might do in JavaScript - my JS code was my schema.
I've since switched to defining a schema (using structs) and then marshalling the JSON to/from Go. This makes it much nicer to use - and, as there was always a schema anyway (i.e. my code), it makes sense to do it this way anyway and you get all the benefits of type checking/validation/....
The beauty of the marshalling is that you can define as much (or as little) of the schema as you want/need.
Not tested, but, hopefully this'll give you a starting point...
type Response struct {
Repos []Repo `json:"values"`
Next string `json:"next"`
}
type Repo struct {
Links Links `json:"links"`
}
type Links struct {
PullRequests PullRequests `json:"pullrequests"`
}
type PullRequests struct {
Href string `json:"href"`
}
func jsonToGo(json string) {
response := Response{}
if err = json.Unmarshal(data, &jsonResposne); nil != err {
...
}
...
That's great feedback, thank you. I'm going to write some tests, and then refactor per your suggestion. Thanks!
You don't need to pass strings and ints like this, by pointer, if they aren't in or in/out parameters.
Strings in Go are immutable, hence cheap to pass by value as no copy of the string is made, basically
just a pair (len, pointer-to-immutable) is passed.
Thanks for clearing that up. I just reverted my last commit.
Becoming beyond the scope of a Gist, moved to a proper Repository: https://github.com/morrisonbrett/pullrequests
Blogged: http://www.brettmorrison.com/a-golang-programming-exercise-bitbucket-pull-requests