Skip to content

Instantly share code, notes, and snippets.

@morrisonbrett
Last active February 9, 2019 22:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save morrisonbrett/a10b2b40993b8eb1b29f to your computer and use it in GitHub Desktop.
Save morrisonbrett/a10b2b40993b8eb1b29f to your computer and use it in GitHub Desktop.
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 = ""
}
}
}
@richie5um
Copy link

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 {
     ...
  }
  ...

@morrisonbrett
Copy link
Author

That's great feedback, thank you. I'm going to write some tests, and then refactor per your suggestion. Thanks!

@momchil-velikov
Copy link

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.

@morrisonbrett
Copy link
Author

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

@morrisonbrett
Copy link
Author

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