Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
A Go program to iterate through a specified owner's repositories on BitBucket and display the status of open Pull Requests
//
// 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 {
     ...
  }
  ...
Owner

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.

Owner

Thanks for clearing that up. I just reverted my last commit.

Owner

Becoming beyond the scope of a Gist, moved to a proper Repository: https://github.com/morrisonbrett/pullrequests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment