Creating a CLI tool as an API client for the GitHub REST API

After looking at this example, we'll be able to easily access the GitHub API from our Go client. We can combine both of the techniques we've learned about in this chapter to come up with a command-line tool that consumes the GitHub API. Let's create a new command-line application that does the following:

We'll use the cli package and grequests to build this tool. You can re-implement the same example in cobra too.

Gist are snippets provided by GitHub that store text content. For more details, visit https://gist.github.com.

Create a directory called gitTool in this chapter's directory and add the main file to it, like so:

mkdir -p $GOPATH/src/github.com/git-user/chapter8/gitTool
touch $GOPATH/src/github.com/git-user/chapter8/gitTool/main.go

First, let's define the main block with a few cli commands so that we can input commands for repository details and gist upload actions. Here, we're using app from the cli package and creating Commands. We're defining two commands here:

func main() {
app := cli.NewApp()
// define command for our client
app.Commands = []cli.Command{
{
Name: "fetch",
Aliases: []string{"f"},
Usage: "Fetch the repo details with user. [Usage]: githubAPI
fetch user",
Action: func(c *cli.Context) error {
if c.NArg() > 0 {
// Github API Logic
var repos []Repo
user := c.Args()[0]
var repoUrl = fmt.Sprintf("https://api.github.com/
users/%s/repos", user)
resp := getStats(repoUrl)
resp.JSON(&repos)
log.Println(repos)
} else {
log.Println("Please give a username. See -h to
see help")
}
return nil
},
},
{
Name: "create",
Aliases: []string{"c"},
Usage: "Creates a gist from the given text.
[Usage]: githubAPI name 'description' sample.txt",
Action: func(c *cli.Context) error {
if c.NArg() > 1 {
// Github API Logic
args := c.Args()
var postUrl = "https://api.github.com/gists"
resp := createGist(postUrl, args)
log.Println(resp.String())
} else {
log.Println("Please give sufficient arguments.
See -h to see help")
}
return nil
},
},
}

app.Version = "1.0"
app.Run(os.Args)
}

As you can see, getStats and createGist are the functions that are used for actual API calls. We'll define these next, but, before we do, we should prepare a few data structures that hold information about the following:

Now, we need to create three structs that hold the preceding information, as follows:

// Struct for holding response of repositories fetch API
type Repo struct {
ID int `json:"id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Forks int `json:"forks"`
Private bool `json:"private"`
}

// Structs for modelling JSON body in create Gist
type File struct {
Content string `json:"content"`
}

type Gist struct {
Description string `json:"description"`
Public bool `json:"public"`
Files map[string]File `json:"files"`
}

Now, create a request option that builds a header and uses GitHub tokens from environment variables:

var GITHUB_TOKEN = os.Getenv("GITHUB_TOKEN")
var requestOptions = &grequests.RequestOptions{Auth: []string{GITHUB_TOKEN, "x-oauth-basic"}}

Now, it's time to write the getStats and createGist functions. Let's code getStats first:

// Fetches the repos for the given Github users
func getStats(url string) *grequests.Response {
resp, err := grequests.Get(url, requestOptions)
// you can modify the request by passing an optional
// RequestOptions struct
if err != nil {
log.Fatalln("Unable to make request: ", err)
}
return resp
}

This function makes a GET request and returns the response object. The code is simple and is a generic GET request.

Now, let's look at createGist. Here, we have to do more. A gist contains multiple files. Due to this, we need to do the following in our program:

  1. Get the list of files from command-line arguments.
  2. Read the file content and store it in a map of files with the filename as the key and content as the value.
  3. Convert this map into JSON.
  4. Make a POST request to the Gist API with the preceding JSON as the body.

We have to make a POST request to the Gist API. The createGist function takes a URL string and other arguments. The function should return the response of the POST request:

// Reads the files provided and creates Gist on github
func createGist(url string, args []string) *grequests.Response {
// get first two arguments
description := args[0]
// remaining arguments are file names with path
var fileContents = make(map[string]File)
for i := 1; i < len(args); i++ {
dat, err := ioutil.ReadFile(args[i])
if err != nil {
log.Println("Please check the filenames. Absolute path
(or) same directory are allowed")
return nil
}
var file File
file.Content = string(dat)
fileContents[args[i]] = file
}
var gist = Gist{Description: description, Public: true,
Files: fileContents}
var postBody, _ = json.Marshal(gist)
var requestOptions_copy = requestOptions
// Add data to JSON field
requestOptions_copy.JSON = string(postBody)
// make a Post request to Github
resp, err := grequests.Post(url, requestOptions_copy)
if err != nil {
log.Println("Create request failed for Github API")
}
return resp
}

We are using grequests.Post to pass files to GitHub's Gist API. It returns Status: 201 Created on successful creation with gist details in the response body.

Now, let's build the command-line tool:

go build $GOPATH/src/github.com/git-user/chapter8/gitTool

This creates a binary in the same directory. If we type in ./gitTool -h, it shows us the following:

NAME:
gitTool - A new cli application

USAGE:
gitTool [global options] command [command options] [arguments...]

VERSION:
1.0

COMMANDS:
fetch, f Fetch the repo details with user. [Usage]: goTool fetch user
create, c Creates a gist from the given text. [Usage]: goTool name
'description' sample.txt

help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
--help, -h show help
--version, -v print the version

If you take a look at the help commands, you'll see two commands, fetch and create. The fetch command fetches the repositories of a given user, while the create command creates a gist with the supplied files. Let's create two sample files in the same directory of the program to test the create command:

echo 'I am sample1 file text' > githubAPI/sample1.txt
echo 'I am sample2 file text' > githubAPI/sample2.txt

Run the tool with the first command:

./gitTool f torvalds

This returns all the repositories that belong to the great Linus Torvalds. The log message prints the struct that was filled:

[{79171906 libdc-for-dirk torvalds/libdc-for-dirk 10 false} {2325298 linux torvalds/linux 18310 false} {78665021 subsurface-for-dirk torvalds/subsurface-for-dirk 16 false} {86106493 test-tlb torvalds/test-tlb 25 false}]

Now, let's check the second command. This creates the gist with the given description and a set of files as arguments:

./gitTool c "I am doing well" sample1.txt sample2.txt

It returns JSON details about the created gist. It is a very lengthy JSON, so the output has been skipped here. Now, if you open your gist.github.com account, you will see the created gist:

Remember, the GitHub gists API expects JSON data as a body in the following format:

{
"description": "the description for this gist",
"public": true,
"files": {
"file1.txt": {
"content": "String file contents"
}
}
}
For any Go program to read and comprehend quickly, follow the main function and then step into the other functions. By doing this, we can read the code from the whole application.

As an exercise, build a command-line tool for the preceding requirements in cobra.