github.js

#

Client-side Javascript API wrapper for GitHub

Tries to map one-to-one with the GitHub API V2, but in a Javascripty manner.

(function (globals) {
#

Before we implement the API methods, we will define all of our private variables and helper functions with one var statement.

    var
#

The username and authentication token of the library's user.

    authUsername,
    authToken,
#

To save keystrokes when we make JSONP calls to the HTTP API, we will keep track of the root from which all V2 urls extend.

    apiRoot = "https://github.com/api/v2/json/",
#

Send a JSONP request to the Github API that calls callback with the context argument as this.

The url parameter is concatenated with the apiRoot, for the reasons mentioned above. The way that we are supporting non-global, anonymous functions is by sticking them in the globally exposed gh.__jsonp_callbacks object with a "unique" id that is the current time in milliseconds. Once the callback is called, it is deleted from the object to prevent memory leaks.

    jsonp = function (url, callback, context) {
        var id = +new Date,
        script = document.createElement("script");

        while (gh.__jsonp_callbacks[id] !== undefined)
            id += Math.random(); // Avoid slight possibility of id clashes.

        gh.__jsonp_callbacks[id] = function () {
            delete gh.__jsonp_callbacks[id];
            callback.apply(context, arguments);
        };

        var prefix = "?";
        if (url.indexOf("?") >= 0)
            prefix = "&";

        url += prefix + "callback=" + encodeURIComponent("gh.__jsonp_callbacks[" + id + "]");
        if (authUsername && authToken) {
            url += "&login=" + authUsername + "&authToken=" + authToken;
        }
        script.setAttribute("src", apiRoot + url);

        document.getElementsByTagName('head')[0].appendChild(script);
    },
#

Send an HTTP POST. Unfortunately, it isn't possible to support a callback with the resulting data. (Please prove me wrong if you can!)

This is implemented with a hack to get around the cross-domain restrictions on ajax calls. Basically, a form is created that will POST to the GitHub API URL, stuck inside an iframe so that it won't redirect this page, and then submitted.

    post = function (url, vals) {
        var
        form = document.createElement("form"),
        iframe = document.createElement("iframe"),
        doc = iframe.contentDocument !== undefined ?
            iframe.contentDocument :
            iframe.contentWindow.document,
        key, field;
        vals = vals || {};

        form.setAttribute("method", "post");
        form.setAttribute("action", apiRoot + url);
        for (key in vals) {
            if (vals.hasOwnProperty(key)) {
                field = document.createElement("input");
                field.type = "hidden";
                field.value = encodeURIComponent(vals[key]);
                form.appendChild(field);
            }
        }

        iframe.setAttribute("style", "display: none;");
        doc.body.appendChild(form);
        document.body.appendChild(iframe);
        form.submit();
    },
#

This helper function will throw a TypeError if the library user is not properly authenticated. Otherwise, it silently returns.

    authRequired = function (username) {
        if (!authUsername || !authToken || authUsername !== username) {
            throw new TypeError("gh: Must be authenticated to do that.");
        }
    },
#

Convert an object to a url parameter string.

paramify({foo:1, bar:3}) -> "foo=1&bar=3".
    paramify = function (params) {
        var str = "", key;
        for (key in params) if (params.hasOwnProperty(key))
            str += key + "=" + params[key] + "&";
        return str.replace(/&$/, "");
    },
#

Get around how the GH team haven't migrated all the API to version 2, and how gists use a different api root.

    withTempApiRoot = function (tempApiRoot, fn) {
        return function () {
            var oldRoot = apiRoot;
            apiRoot = tempApiRoot;
            fn.apply(this, arguments);
            apiRoot = oldRoot;
        };
    },
#

Expose the global gh variable, through which every API method is accessed, but keep a local variable around so we can reference it easily.

    gh = globals.gh = {};
#

Psuedo private home for JSONP callbacks (which are required to be global by the nature of JSONP, as discussed earlier).

    gh.__jsonp_callbacks = {};
#

Authenticate as a user. Does not try to validate at any point; that job is up to each individual method, which calls authRequired as needed.

    gh.authenticate = function (username, token) {
        authUsername = username;
        authToken = token;
        return this;
    };
#

Users

#

The constructor for user objects. Just creating an instance of a user doesn't fetch any data from GitHub, you need to get explicit about what you want to do that.

var huddlej = gh.user("huddlej");
    gh.user = function (username) {
        if ( !(this instanceof gh.user)) {
            return new gh.user(username);
        }
        this.username = username;
    };
#

Show basic user info; you can get more info if you are authenticated as this user.

gh.user("fitzgen").show(function (data) { console.log(data.user); });

    gh.user.prototype.show = function (callback, context) {
        jsonp("user/show/" + this.username, callback, context);
        return this;
    };
#

Update a user's info. You must be authenticated as this user for this to succeed.

TODO: example
    gh.user.prototype.update = function (params) {
        authRequired(this.username);
        var key, postData = {
            login: authUsername,
            token: authToken
        };
        for (key in params) {
            if (params.hasOwnProperty(key)) {
                postData["values["+key+"]"] = encodeURIComponent(params[key]);
            }
        }
        post("user/show/" + this.username, postData);
        return this;
    };
#

Get a list of who this user is following.

TODO: example
    gh.user.prototype.following = function (callback, context) {
        jsonp("user/show/" + this.username + "/following", callback, context);
    };
#

Find out what other users are following this user.

TODO: example
    gh.user.prototype.followers = function (callback, context) {
        jsonp("user/show/" + this.username + "/followers", callback, context);
    };
#

Make this user follow some other user. You must be authenticated as this user for this to succeed.

TODO: example
    gh.user.prototype.follow = function (user) {
        authRequired.call(this);
        post("user/follow/" + user);
        return this;
    };
#

Make this user quit following the given user. You must be authenticated as this user to succeed.

TODO: example
    gh.user.prototype.unfollow = function (user) {
        authRequired.call(this);
        post("user/unfollow/" + user);
        return this;
    };
#

Get a list of repositories that this user is watching.

TODO: example
    gh.user.prototype.watching = function (callback, context) {
        jsonp("repos/watched/" + this.username, callback, context);
        return this;
    };
#

Get a list of this user's repositories.

gh.user("fitzgen").repos(function (data) {
    alert(data.repositories.length);
});
    gh.user.prototype.repos = function (callback, context) {
        gh.repo.forUser(this.username, callback, context);
        return this;
    };
#

Make this user fork the repo that lives at http://github.com/user/repo. You must be authenticated as this user for this to succeed.

gh.user("fitzgen").forkRepo("brianleroux", "wtfjs");
    gh.user.prototype.forkRepo = function (user, repo) {
        authRequired(this.username);
        post("repos/fork/" + user + "/" + repo);
        return this;
    };
#

Get a list of all repos that this user can push to (including ones that they are just a collaborator on, and do not own). Must be authenticated as this user.

    gh.user.prototype.pushable = function (callback, context) {
        authRequired(authUsername);
        jsonp("repos/pushable", callback, context);
    };

    gh.user.prototype.publicGists = withTempApiRoot(
        "http://gist.github.com/api/v1/json/gists/",
        function (callback, context) {
            jsonp(this.username, callback, context);
            return this;
        }
    );
#

Search users for query.

    gh.user.search = function (query, callback, context) {
        jsonp("user/search/" + query, callback, context);
        return this;
    };
#

Repositories

#

This is the base constructor for creating repo objects. Note that this won't actually hit the GitHub API until you specify what data you want, or what action you wish to take via a prototype method.

    gh.repo = function (user, repo) {
        if ( !(this instanceof gh.repo)) {
            return new gh.repo(user, repo);
        }
        this.repo = repo;
        this.user = user;
    };
#

Get basic information on this repo.

gh.repo("schacon", "grit").show(function (data) {
    console.log(data.repository.description);
});
    gh.repo.prototype.show = function (callback, context) {
        jsonp("repos/show/" + this.user + "/" + this.repo, callback, context);
        return this;
    };
#

Update the information for this repo. Must be authenticated as the repository owner. Params can include:

  • description
  • homepage
  • has_wiki
  • has_issues
  • has_downloads
    gh.repo.prototype.update = function (params) {
        authRequired(this.user);
        var key, postData = {
            login: authUsername,
            token: authToken
        };
        for (key in params) {
            if (params.hasOwnProperty(key)) {
                postData["values["+key+"]"] = encodeURIComponent(params[key]);
            }
        }
        post("repos/show/" + this.user + "/" + this.repo, postData);
        return this;
    };
#

Get all tags for this repo.

    gh.repo.prototype.tags = function (callback, context) {
        jsonp("repos/show/" + this.user + "/" + this.repo + "/tags",
              callback,
              context);
        return this;
    };
#

Get all branches in this repo.

    gh.repo.prototype.branches = function (callback, context) {
        jsonp("repos/show/" + this.user + "/" + this.repo + "/branches",
              callback,
              context);
        return this;
    };
#

Gather line count information on the language(s) used in this repo.

    gh.repo.prototype.languages = function (callback, context) {
        jsonp("/repos/show/" + this.user + "/" + this.repo + "/languages",
              callback,
              context);
        return this;
    };
#

Gather data on all the forks of this repo.

    gh.repo.prototype.network = function (callback, context) {
        jsonp("repos/show/" + this.user + "/" + this.repo + "/network",
              callback,
              context);
        return this;
    };
#

All users who have contributed to this repo. Pass true to showAnon if you want to see the non-github contributors.

    gh.repo.prototype.contributors = function (callback, context, showAnon) {
        var url = "repos/show/" + this.user + "/" + this.repo + "/contributors";
        if (showAnon)
            url += "/anon";
        jsonp(url,
              callback,
              context);
        return this;
    };
#

Get all of the collaborators for this repo.

    gh.repo.prototype.collaborators = function (callback, context) {
        jsonp("repos/show/" + this.user + "/" + this.repo + "/collaborators",
              callback,
              context);
        return this;
    };
#

Add a collaborator to this project. Must be authenticated.

    gh.repo.prototype.addCollaborator = function (collaborator) {
        authRequired(this.user);
        post("repos/collaborators/" + this.repo + "/add/" + collaborator);
        return this;
    };
#

Remove a collaborator from this project. Must be authenticated.

    gh.repo.prototype.removeCollaborator = function (collaborator) {
        authRequired(this.user);
        post("repos/collaborators/" + this.repo + "/remove/" + collaborator);
        return this;
    };
#

Make this repository private. Authentication required.

    gh.repo.prototype.setPrivate = function () {
        authRequired(this.user);
        post("repo/set/private/" + this.repo);
        return this;
    };
#

Make this repository public. Authentication required.

    gh.repo.prototype.setPublic = function () {
        authRequired(this.user);
        post("repo/set/public/" + this.repo);
        return this;
    };
#

Search for repositories. opts may include start_page or language, which must be capitalized.

    gh.repo.search = function (query, opts, callback, context) {
        var url = "repos/search/" + query.replace(" ", "+");
        if (typeof opts === "function") {
            opts = {};
            callback = arguments[1];
            context = arguments[2];
        }
        url += "?" + paramify(opts);
        return this;
    };
#

Get all the repos that are owned by user.

    gh.repo.forUser = function (user, callback, context) {
        jsonp("repos/show/" + user, callback, context);
        return this;
    };
#

Create a repository. Must be authenticated.

    gh.repo.create = function (name, opts) {
        authRequired(authUsername);
        opts.name = name;
        post("repos/create", opts);
        return this;
    };
#

Delete a repository. Must be authenticated.

    gh.repo.del = function (name) {
        authRequired(authUsername);
        post("repos/delete/" + name);
        return this;
    };
#

Commits

    gh.commit = function (user, repo, sha) {
        if ( !(this instanceof gh.commit) )
            return new gh.commit(user, repo, sha);
        this.user = user;
        this.repo = repo;
        this.sha = sha;
    };

    gh.commit.prototype.show = function (callback, context) {
        jsonp("commits/show/" + this.user + "/" + this.repo + "/" + this.sha,
              callback,
              context);
        return this;
    };
#

Get a list of all commits on a repos branch.

    gh.commit.forBranch = function (user, repo, branch, callback, context) {
        jsonp("commits/list/" + user + "/" + repo + "/" + branch,
              callback,
              context);
        return this;
    };
#

Get a list of all commits on this path (file or dir).

    gh.commit.forPath = function (user, repo, branch, path, callback, context) {
        jsonp("commits/list/" + user + "/" + repo + "/" + branch + "/" + path,
              callback,
              context);
        return this;
    };
#

Issues

    gh.issue = function (user, repo, number) {
        if ( !(this instanceof gh.issue) )
            return new gh.commit(user, repo, number);
        this.user = user;
        this.repo = repo;
        this.number = number;
    };
#

View this issue's info.

    gh.issue.prototype.show = function (callback, context) {
        jsonp("issues/show/" + this.user + "/" + this.repo + "/" + this.number,
              callback,
              context);
        return this;
    };
#

Get a list of all comments on this issue.

    gh.issue.prototype.comments = function (callback, context) {
        jsonp("issues/comments/" + this.user + "/" + this.repo + "/" + this.number,
              callback,
              context);
        return this;
    };
#

Close this issue.

    gh.issue.prototype.close = function () {
        authRequired(this.user);
        post("issues/close/" + this.user + "/" + this.repo + "/" + this.number);
        return this;
    };
#

Reopen this issue.

    gh.issue.prototype.reopen = function () {
        authRequired(this.user);
        post("issues/reopen/" + this.user + "/" + this.repo + "/" + this.number);
        return this;
    };
#

Reopen this issue.

    gh.issue.prototype.update = function (title, body) {
        authRequired(this.user);
        post("issues/edit/" + this.user + "/" + this.repo + "/" + this.number, {
            title: title,
            body: body
        });
        return this;
    };
#

Add label to this issue. If the label is not yet in the system, it will be created.

    gh.issue.prototype.addLabel = function (label) {
        post("issues/label/add/" + this.user + "/" + this.repo + "/" + label + "/" + this.number);
        return this;
    };
#

Remove a label from this issue.

    gh.issue.prototype.removeLabel = function (label) {
        post("issues/label/remove/" + this.user + "/" + this.repo + "/" + label + "/" + this.number);
        return this;
    };
#

Comment on this issue as the user that is authenticated.

    gh.issue.prototype.comment = function (comment) {
        authRequired(authUsername);
        post("/issues/comment/" + user + "/" + repo + "/" + this.number, {
            comment: comment
        });
        return this;
    };
#

Get all issues' labels for the repo.

    gh.issue.labels = function (user, repo) {
        jsonp("issues/labels/" + user + "/" + repo,
              callback,
              context);
        return this;
    };
#

Open an issue. Must be authenticated.

    gh.issue.open = function (repo, title, body) {
        authRequired(authUsername);
        post("issues/open/" + authUsername + "/" + repo, {
            title: title,
            body: body
        });
        return this;
    };
#

Search a repository's issue tracker. state can be "open" or "closed".

    gh.issue.search = function (user, repo, state, query, callback, context) {
        jsonp("/issues/search/" + user + "/" + repo + "/" + state + "/" + query,
              callback,
              context);
        return this;
    };
#

Get a list of issues for the given repo. state can be "open" or "closed".

    gh.issue.list = function (user, repo, state, callback, context) {
        jsonp("issues/list/" + user + "/" + repo + "/" + state,
              callback,
              context);
        return this;
    };
#

Gists

    gh.gist = function (id) {
        if ( !(this instanceof gh.gist) ) {
            return new gh.gist(id);
        }
        this.id = id;
    };

    gh.gist.prototype.show = withTempApiRoot(
        "http://gist.github.com/api/v1/json/",
        function (callback, context) {
            jsonp(this.id, callback, cont);
            return this;
        }
    );

    gh.gist.prototype.file = withTempApiRoot(
        "http://gist.github.com/raw/v1/json/",
        function (filename, callback, context) {
            jsonp(this.id + "/" + filename, callback, cont);
            return this;
        }
    );
#

Objects

    gh.object = function (user, repo, sha) {
        if (!(this instanceof gh.object)) {
            return new gh.object(user, repo, sha);
        }
        this.user = user;
        this.repo = repo;
        this.sha = sha;
    };
#

Get the contents of a tree by tree SHA

    gh.object.prototype.tree = function (callback, context) {
        jsonp("tree/show/" + this.user + "/" + this.repo + "/" + this.sha,
              callback,
              context);
        return this;
    };
#

Get the data about a blob by tree SHA and path

    gh.object.prototype.blob = function (path, callback, context) {
        jsonp("blob/show/" + this.user + "/" + this.repo + "/" + this.sha + "/" + path,
              callback,
              context);
        return this;
    };
#

Get only blob meta

    gh.object.prototype.blobMeta = function (path, callback, context) {
        jsonp("blob/show/" + this.user + "/" + this.repo + "/" + this.sha + "/" + path + "?meta=1",
              callback,
              context);
        return this;
    };
#

Get list of blobs

    gh.object.prototype.blobList = function (callback, context) {
        jsonp("blob/all/" + this.user + "/" + this.repo + "/" + this.sha,
              callback,
              context);
        return this;
    };
#

Get meta of each blob in tree

    gh.object.prototype.blobFull = function (callback, context) {
        jsonp("blob/full/" + this.user + "/" + this.repo + "/" + this.sha,
              callback,
              context);
        return this;
    };
#

Get data for given blob

    gh.object.prototype.blobData = function (sha, callback, context) {
        jsonp("blob/show/" + this.user + "/" + this.repo + "/" + sha,
              callback,
              context);
        return this;
    };
#

Network

    gh.network = function(user, repo) {
        if (!(this instanceof gh.network)) {
            return new gh.network(user, repo);
        }
        this.user = user;
        this.repo = repo;
    };

    gh.network.prototype.meta = withTempApiRoot(
        "http://github.com/",
        function (callback, context) {
            jsonp(this.user + "/" + this.repo + "/network_meta",
                  callback,
                  context);
            return this;
        }
   );

}(window));