diff --git a/.gitignore b/.gitignore index 8bcfe81d..7c78223e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ -.DS_Store -.idea -node_modules/ -npm-debug.log +dist/ coverage/ +node_modules/ + +.idea +.DS_Store sauce.json +npm-debug.log diff --git a/.travis.yml b/.travis.yml index 05882674..ad568664 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,9 @@ node_js: - "0.10" script: - - gulp lint - - gulp test:ci - - mocha test/ - - npm run-script codecov + - > + gulp lint && + gulp build && + gulp test:node && + gulp test:ci && + gulp codecov diff --git a/github.js b/github.js index 2e0d3591..ab95c628 100644 --- a/github.js +++ b/github.js @@ -3,1046 +3,82 @@ * * @copyright (c) 2013 Michael Aufreiter, Development Seed * Github.js is freely distributable. - *de * @license Licensed under BSD-3-Clause-Clear * * For all details and documentation: * http://substance.io/michael/github */ - -(function (root, factory) { - // UMD boilerplate from https://github.com/umdjs/umd/blob/master/returnExportsGlobal.js - 'use strict'; - - /* istanbul ignore next */ - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['xmlhttprequest', 'js-base64'], function (XMLHttpRequest, b64encode) { - return (root.Github = factory(XMLHttpRequest.XMLHttpRequest, b64encode.Base64.encode)); - }); - } else if (typeof module === 'object' && module.exports) { - if (typeof window !== 'undefined') { // jscs:ignore - module.exports = factory(window.XMLHttpRequest, window.btoa); - } else { // jscs:ignore - module.exports = factory(require('xmlhttprequest').XMLHttpRequest, require('js-base64').Base64.encode); - } - } else { - // Browser globals - var b64encode = function(str) { - return root.btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { - return String.fromCharCode('0x' + p1); - })); - }; - - root.Github = factory(root.XMLHttpRequest, b64encode); +'use strict'; + +require('es6-promise').polyfill(); + +var User = require('./lib/User'); +var Gist = require('./lib/Gist'); +var Issue = require('./lib/Issue'); +var Search = require('./lib/Search'); +var Repository = require('./lib/Repository'); + +function Github(options) { + this.apiUrl = options.apiUrl || 'https://api.github.com'; + this.username = options.username; + this.password = options.password; + this.token = options.token; +} + +Github.prototype.getIssues = function(user, repo) { + return new Issue({ + apiUrl: this.apiUrl, + username: this.username, + password: this.password, + token: this.token, + user: user, + repo: repo + }); +}; + +Github.prototype.getRepo = function(user, repo) { + var fullname = user; + + if (repo) { + fullname = user + '/' + repo; } -}(this, function (XMLHttpRequest, b64encode) { - 'use strict'; - - // Initial Setup - // ------------- - - var Github = function(options) { - var API_URL = options.apiUrl || 'https://api.github.com'; - - // HTTP Request Abstraction - // ======= - // - // I'm not proud of this and neither should you be if you were responsible for the XMLHttpRequest spec. - - var _request = Github._request = function _request(method, path, data, cb, raw, sync) { - function getURL() { - var url = path.indexOf('//') >= 0 ? path : API_URL + path; - - url += ((/\?/).test(url) ? '&' : '?'); - - if (data && typeof data === 'object' && ['GET', 'HEAD', 'DELETE'].indexOf(method) > -1) { - for(var param in data) { - if (data.hasOwnProperty(param)) - url += '&' + encodeURIComponent(param) + '=' + encodeURIComponent(data[param]); - } - } - - return url.replace(/(×tamp=\d+)/, '') + - (typeof window !== 'undefined' ? '×tamp=' + new Date().getTime() : ''); - } - - var xhr = new XMLHttpRequest(); - - xhr.open(method, getURL(), !sync); - - if (!sync) { - xhr.onreadystatechange = function () { - if (this.readyState === 4) { - if (this.status >= 200 && this.status < 300 || this.status === 304) { - cb(null, raw ? this.responseText : this.responseText ? JSON.parse(this.responseText) : true, this); - } else { - cb({ - path: path, request: this, error: this.status - }); - } - } - }; - } - - if (!raw) { - xhr.dataType = 'json'; - xhr.setRequestHeader('Accept', 'application/vnd.github.v3+json'); - } else { - xhr.setRequestHeader('Accept', 'application/vnd.github.v3.raw+json'); - } - - xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); - - if ((options.token) || (options.username && options.password)) { - var authorization = options.token ? 'token ' + options.token : - 'Basic ' + b64encode(options.username + ':' + options.password); - - xhr.setRequestHeader('Authorization', authorization); - } - - if (data) { - xhr.send(JSON.stringify(data)); - } else { - xhr.send(); - } - - if (sync) { - return xhr.response; - } - }; - - var _requestAllPages = Github._requestAllPages = function _requestAllPages(path, cb) { - var results = []; - - (function iterate() { - _request('GET', path, null, function(err, res, xhr) { - if (err) { - return cb(err); - } - - results.push.apply(results, res); - - var links = (xhr.getResponseHeader('link') || '').split(/\s*,\s*/g); - var next = null; - - links.forEach(function(link) { - next = /rel="next"/.test(link) ? link : next; - }); - - if (next) { - next = (/<(.*)>/.exec(next) || [])[1]; - } - - if (!next) { - cb(err, results); - } else { - path = next; - iterate(); - } - }); - })(); - }; - - // User API - // ======= - - Github.User = function() { - this.repos = function(options, cb) { - if (arguments.length === 1 && typeof arguments[0] === 'function') { - cb = options; - options = {}; - } - - options = options || {}; - - var url = '/user/repos'; - var params = []; - - params.push('type=' + encodeURIComponent(options.type || 'all')); - params.push('sort=' + encodeURIComponent(options.sort || 'updated')); - params.push('per_page=' + encodeURIComponent(options.per_page || '1000')); // jscs:ignore - - if (options.page) { - params.push('page=' + encodeURIComponent(options.page)); - } - - url += '?' + params.join('&'); - - _request('GET', url, null, cb); - }; - - // List user organizations - // ------- - - this.orgs = function(cb) { - _request('GET', '/user/orgs', null, cb); - }; - - // List authenticated user's gists - // ------- - - this.gists = function(cb) { - _request('GET', '/gists', null, cb); - }; - - // List authenticated user's unread notifications - // ------- - - this.notifications = function(options, cb) { - if (arguments.length === 1 && typeof arguments[0] === 'function') { - cb = options; - options = {}; - } - - options = options || {}; - var url = '/notifications'; - var params = []; - - if (options.all) { - params.push('all=true'); - } - - if (options.participating) { - params.push('participating=true'); - } - - if (options.since) { - var since = options.since; - - if (since.constructor === Date) { - since = since.toISOString(); - } - - params.push('since=' + encodeURIComponent(since)); - } - - if (options.before) { - var before = options.before; - - if (before.constructor === Date) { - before = before.toISOString(); - } - - params.push('before=' + encodeURIComponent(before)); - } - - if (options.page) { - params.push('page=' + encodeURIComponent(options.page)); - } - - if (params.length > 0) { - url += '?' + params.join('&'); - } - - _request('GET', url, null, cb); - }; - - // Show user information - // ------- - - this.show = function(username, cb) { - var command = username ? '/users/' + username : '/user'; - - _request('GET', command, null, cb); - }; - - // List user repositories - // ------- - - this.userRepos = function(username, cb) { - // Github does not always honor the 1000 limit so we want to iterate over the data set. - _requestAllPages('/users/' + username + '/repos?type=all&per_page=1000&sort=updated', cb); - }; - - // List user starred repositories - // ------- - - this.userStarred = function(username, cb) { - // Github does not always honor the 1000 limit so we want to iterate over the data set. - _requestAllPages('/users/' + username + '/starred?type=all&per_page=1000', function(err, res) { - cb(err, res); - }); - }; - - // List a user's gists - // ------- - - this.userGists = function(username, cb) { - _request('GET', '/users/' + username + '/gists', null, cb); - }; - - // List organization repositories - // ------- - - this.orgRepos = function(orgname, cb) { - // Github does not always honor the 1000 limit so we want to iterate over the data set. - _requestAllPages('/orgs/' + orgname + '/repos?type=all&&page_num=1000&sort=updated&direction=desc', cb); - }; - - // Follow user - // ------- - - this.follow = function(username, cb) { - _request('PUT', '/user/following/' + username, null, cb); - }; - - // Unfollow user - // ------- - - this.unfollow = function(username, cb) { - _request('DELETE', '/user/following/' + username, null, cb); - }; - - // Create a repo - // ------- - this.createRepo = function(options, cb) { - _request('POST', '/user/repos', options, cb); - }; - }; - - // Repository API - // ======= - - Github.Repository = function(options) { - var repo = options.name; - var user = options.user; - var fullname = options.fullname; - - var that = this; - var repoPath; - - if (fullname) { - repoPath = '/repos/' + fullname; - } else { - repoPath = '/repos/' + user + '/' + repo; - } - - var currentTree = { - branch: null, - sha: null - }; - - // Uses the cache if branch has not been changed - // ------- - - function updateTree(branch, cb) { - if (branch === currentTree.branch && currentTree.sha) { - return cb(null, currentTree.sha); - } - - that.getRef('heads/' + branch, function(err, sha) { - currentTree.branch = branch; - currentTree.sha = sha; - cb(err, sha); - }); - } - - // Get a particular reference - // ------- - - this.getRef = function(ref, cb) { - _request('GET', repoPath + '/git/refs/' + ref, null, function(err, res, xhr) { - if (err) { - return cb(err); - } - - cb(null, res.object.sha, xhr); - }); - }; - - // Create a new reference - // -------- - // - // { - // "ref": "refs/heads/my-new-branch-name", - // "sha": "827efc6d56897b048c772eb4087f854f46256132" - // } - - this.createRef = function(options, cb) { - _request('POST', repoPath + '/git/refs', options, cb); - }; - - // Delete a reference - // -------- - // - // Repo.deleteRef('heads/gh-pages') - // repo.deleteRef('tags/v1.0') - - this.deleteRef = function(ref, cb) { - _request('DELETE', repoPath + '/git/refs/' + ref, options, function(err, res, xhr) { - cb(err, res, xhr); - }); - }; - - // Create a repo - // ------- - - this.createRepo = function(options, cb) { - _request('POST', '/user/repos', options, cb); - }; - - // Delete a repo - // -------- - - this.deleteRepo = function(cb) { - _request('DELETE', repoPath, options, cb); - }; - - // List all tags of a repository - // ------- - - this.listTags = function(cb) { - _request('GET', repoPath + '/tags', null, function(err, tags, xhr) { - if (err) { - return cb(err); - } - - cb(null, tags, xhr); - }); - }; - - // List all pull requests of a respository - // ------- - - this.listPulls = function(options, cb) { - options = options || {}; - var url = repoPath + '/pulls'; - var params = []; - - if (typeof options === 'string') { - // Backward compatibility - params.push('state=' + options); - } else { - if (options.state) { - params.push('state=' + encodeURIComponent(options.state)); - } - - if (options.head) { - params.push('head=' + encodeURIComponent(options.head)); - } - - if (options.base) { - params.push('base=' + encodeURIComponent(options.base)); - } - - if (options.sort) { - params.push('sort=' + encodeURIComponent(options.sort)); - } - - if (options.direction) { - params.push('direction=' + encodeURIComponent(options.direction)); - } - - if (options.page) { - params.push('page=' + options.page); - } - - if (options.per_page) { - params.push('per_page=' + options.per_page); - } - } - - if (params.length > 0) { - url += '?' + params.join('&'); - } - - _request('GET', url, null, function(err, pulls, xhr) { - if (err) return cb(err); - cb(null, pulls, xhr); - }); - }; - - // Gets details for a specific pull request - // ------- - - this.getPull = function(number, cb) { - _request('GET', repoPath + '/pulls/' + number, null, function(err, pull, xhr) { - if (err) return cb(err); - cb(null, pull, xhr); - }); - }; - - // Retrieve the changes made between base and head - // ------- - - this.compare = function(base, head, cb) { - _request('GET', repoPath + '/compare/' + base + '...' + head, null, function(err, diff, xhr) { - if (err) return cb(err); - cb(null, diff, xhr); - }); - }; - - // List all branches of a repository - // ------- - - this.listBranches = function(cb) { - _request('GET', repoPath + '/git/refs/heads', null, function(err, heads, xhr) { - if (err) return cb(err); - cb(null, heads.map(function(head) { - return head.ref.replace(/^refs\/heads\//, ''); - }), xhr); - }); - }; - - // Retrieve the contents of a blob - // ------- - - this.getBlob = function(sha, cb) { - _request('GET', repoPath + '/git/blobs/' + sha, null, cb, 'raw'); - }; - - // For a given file path, get the corresponding sha (blob for files, tree for dirs) - // ------- - - this.getCommit = function(branch, sha, cb) { - _request('GET', repoPath + '/git/commits/' + sha, null, function(err, commit, xhr) { - if (err) return cb(err); - cb(null, commit, xhr); - }); - }; - - // For a given file path, get the corresponding sha (blob for files, tree for dirs) - // ------- - - this.getSha = function(branch, path, cb) { - if (!path || path === '') return that.getRef('heads/' + branch, cb); - _request('GET', repoPath + '/contents/' + path + (branch ? '?ref=' + branch : ''), - null, function(err, pathContent, xhr) { - if (err) return cb(err); - cb(null, pathContent.sha, xhr); - }); - }; - - // Get the statuses for a particular SHA - // ------- - - this.getStatuses = function(sha, cb) { - _request('GET', repoPath + '/statuses/' + sha, null, cb); - }; - - // Retrieve the tree a commit points to - // ------- - - this.getTree = function(tree, cb) { - _request('GET', repoPath + '/git/trees/' + tree, null, function(err, res, xhr) { - if (err) return cb(err); - cb(null, res.tree, xhr); - }); - }; - - // Post a new blob object, getting a blob SHA back - // ------- - - this.postBlob = function(content, cb) { - if (typeof (content) === 'string') { - content = { - content: content, - encoding: 'utf-8' - }; - } else { - content = { - content: b64encode(content), - encoding: 'base64' - }; - } - - _request('POST', repoPath + '/git/blobs', content, function(err, res) { - if (err) return cb(err); - cb(null, res.sha); - }); - }; - - // Update an existing tree adding a new blob object getting a tree SHA back - // ------- - - this.updateTree = function(baseTree, path, blob, cb) { - var data = { - base_tree: baseTree, - tree: [ - { - path: path, - mode: '100644', - type: 'blob', - sha: blob - } - ] - }; - - _request('POST', repoPath + '/git/trees', data, function(err, res) { - if (err) return cb(err); - cb(null, res.sha); - }); - }; - - // Post a new tree object having a file path pointer replaced - // with a new blob SHA getting a tree SHA back - // ------- - - this.postTree = function(tree, cb) { - _request('POST', repoPath + '/git/trees', { - tree: tree - }, function(err, res) { - if (err) return cb(err); - cb(null, res.sha); - }); - }; - - // Create a new commit object with the current commit SHA as the parent - // and the new tree SHA, getting a commit SHA back - // ------- - - this.commit = function(parent, tree, message, cb) { - var user = new Github.User(); - - user.show(null, function(err, userData) { - if (err) return cb(err); - var data = { - message: message, - author: { - name: options.user, - email: userData.email - }, - parents: [ - parent - ], - tree: tree - }; - - _request('POST', repoPath + '/git/commits', data, function(err, res) { - if (err) return cb(err); - currentTree.sha = res.sha; // Update latest commit - cb(null, res.sha); - }); - }); - }; - - // Update the reference of your head to point to the new commit SHA - // ------- - - this.updateHead = function(head, commit, cb) { - _request('PATCH', repoPath + '/git/refs/heads/' + head, { - sha: commit - }, function(err) { - cb(err); - }); - }; - - // Show repository information - // ------- - - this.show = function(cb) { - _request('GET', repoPath, null, cb); - }; - - // Show repository contributors - // ------- - - this.contributors = function (cb, retry) { - retry = retry || 1000; - var that = this; - - _request('GET', repoPath + '/stats/contributors', null, function (err, data, xhr) { - if (err) return cb(err); - - if (xhr.status === 202) { - setTimeout( - function () { - that.contributors(cb, retry); - }, - retry - ); - } else { - cb(err, data, xhr); - } - }); - }; - - // Get contents - // -------- - - this.contents = function(ref, path, cb) { - path = encodeURI(path); - _request('GET', repoPath + '/contents' + (path ? '/' + path : ''), { - ref: ref - }, cb); - }; - - // Fork repository - // ------- - - this.fork = function(cb) { - _request('POST', repoPath + '/forks', null, cb); - }; - - // List forks - // -------- - - this.listForks = function(cb) { - _request('GET', repoPath + '/forks', null, cb); - }; - - // Branch repository - // -------- - - this.branch = function(oldBranch, newBranch, cb) { - if (arguments.length === 2 && typeof arguments[1] === 'function') { - cb = newBranch; - newBranch = oldBranch; - oldBranch = 'master'; - } - - this.getRef('heads/' + oldBranch, function(err, ref) { - if (err && cb) return cb(err); - that.createRef({ - ref: 'refs/heads/' + newBranch, - sha: ref - }, cb); - }); - }; - - // Create pull request - // -------- - - this.createPullRequest = function(options, cb) { - _request('POST', repoPath + '/pulls', options, cb); - }; - - // List hooks - // -------- - - this.listHooks = function(cb) { - _request('GET', repoPath + '/hooks', null, cb); - }; - - // Get a hook - // -------- - - this.getHook = function(id, cb) { - _request('GET', repoPath + '/hooks/' + id, null, cb); - }; - - // Create a hook - // -------- - - this.createHook = function(options, cb) { - _request('POST', repoPath + '/hooks', options, cb); - }; - - // Edit a hook - // -------- - - this.editHook = function(id, options, cb) { - _request('PATCH', repoPath + '/hooks/' + id, options, cb); - }; - - // Delete a hook - // -------- - - this.deleteHook = function(id, cb) { - _request('DELETE', repoPath + '/hooks/' + id, null, cb); - }; - - // Read file at given path - // ------- - - this.read = function(branch, path, cb) { - _request('GET', repoPath + '/contents/' + encodeURI(path) + (branch ? '?ref=' + branch : ''), - null, function(err, obj, xhr) { - if (err && err.error === 404) return cb('not found', null, null); - - if (err) return cb(err); - cb(null, obj, xhr); - }, true); - }; - - // Remove a file - // ------- - - this.remove = function(branch, path, cb) { - that.getSha(branch, path, function(err, sha) { - if (err) return cb(err); - _request('DELETE', repoPath + '/contents/' + path, { - message: path + ' is removed', - sha: sha, - branch: branch - }, cb); - }); - }; - - // Alias for repo.remove for backwards comapt. - // ------- - this.delete = this.remove; - - // Move a file to a new location - // ------- - - this.move = function(branch, path, newPath, cb) { - updateTree(branch, function(err, latestCommit) { - that.getTree(latestCommit + '?recursive=true', function(err, tree) { - // Update Tree - tree.forEach(function(ref) { - if (ref.path === path) ref.path = newPath; - - if (ref.type === 'tree') delete ref.sha; - }); - - that.postTree(tree, function(err, rootTree) { - that.commit(latestCommit, rootTree, 'Deleted ' + path , function(err, commit) { - that.updateHead(branch, commit, function(err) { - cb(err); - }); - }); - }); - }); - }); - }; - - // Write file contents to a given branch and path - // ------- - - this.write = function(branch, path, content, message, options, cb) { - if (typeof cb === 'undefined') { - cb = options; - options = {}; - } - - that.getSha(branch, encodeURI(path), function(err, sha) { - var writeOptions = { - message: message, - content: typeof options.encode === 'undefined' || options.encode ? b64encode(content) : content, - branch: branch, - committer: options && options.committer ? options.committer : undefined, - author: options && options.author ? options.author : undefined - }; - - // If no error, we set the sha to overwrite an existing file - if (!(err && err.error !== 404)) writeOptions.sha = sha; - _request('PUT', repoPath + '/contents/' + encodeURI(path), writeOptions, cb); - }); - }; - - // List commits on a repository. Takes an object of optional paramaters: - // sha: SHA or branch to start listing commits from - // path: Only commits containing this file path will be returned - // since: ISO 8601 date - only commits after this date will be returned - // until: ISO 8601 date - only commits before this date will be returned - // ------- - - this.getCommits = function(options, cb) { - options = options || {}; - var url = repoPath + '/commits'; - var params = []; - - if (options.sha) { - params.push('sha=' + encodeURIComponent(options.sha)); - } - - if (options.path) { - params.push('path=' + encodeURIComponent(options.path)); - } - - if (options.since) { - var since = options.since; - - if (since.constructor === Date) { - since = since.toISOString(); - } - - params.push('since=' + encodeURIComponent(since)); - } - - if (options.until) { - var until = options.until; - - if (until.constructor === Date) { - until = until.toISOString(); - } - - params.push('until=' + encodeURIComponent(until)); - } - - if (options.page) { - params.push('page=' + options.page); - } - - if (options.perpage) { - params.push('per_page=' + options.perpage); - } - - if (params.length > 0) { - url += '?' + params.join('&'); - } - - _request('GET', url, null, cb); - }; - }; - - // Gists API - // ======= - - Github.Gist = function(options) { - var id = options.id; - var gistPath = '/gists/' + id; - - // Read the gist - // -------- - - this.read = function(cb) { - _request('GET', gistPath, null, cb); - }; - - // Create the gist - // -------- - // { - // "description": "the description for this gist", - // "public": true, - // "files": { - // "file1.txt": { - // "content": "String file contents" - // } - // } - // } - - this.create = function(options, cb) { - _request('POST', '/gists', options, cb); - }; - - // Delete the gist - // -------- - - this.delete = function(cb) { - _request('DELETE', gistPath, null, cb); - }; - - // Fork a gist - // -------- - - this.fork = function(cb) { - _request('POST', gistPath + '/fork', null, cb); - }; - - // Update a gist with the new stuff - // -------- - - this.update = function(options, cb) { - _request('PATCH', gistPath, options, cb); - }; - - // Star a gist - // -------- - - this.star = function(cb) { - _request('PUT', gistPath + '/star', null, cb); - }; - - // Untar a gist - // -------- - - this.unstar = function(cb) { - _request('DELETE', gistPath + '/star', null, cb); - }; - - // Check if a gist is starred - // -------- - - this.isStarred = function(cb) { - _request('GET', gistPath + '/star', null, cb); - }; - }; - - // Issues API - // ========== - - Github.Issue = function(options) { - var path = '/repos/' + options.user + '/' + options.repo + '/issues'; - - this.list = function(options, cb) { - var query = []; - - for(var key in options) { - if (options.hasOwnProperty(key)) { - query.push(encodeURIComponent(key) + '=' + encodeURIComponent(options[key])); - } - } - - _requestAllPages(path + '?' + query.join('&'), cb); - }; - - this.comment = function(issue, comment, cb) { - _request('POST', issue.comments_url, { - body: comment - }, function(err, res) { - cb(err, res); - }); - }; - }; - - // Search API - // ========== - - Github.Search = function(options) { - var path = '/search/'; - var query = '?q=' + options.query; - - this.repositories = function(options, cb) { - _request('GET', path + 'repositories' + query, options, cb); - }; - - this.code = function(options, cb) { - _request('GET', path + 'code' + query, options, cb); - }; - - this.issues = function(options, cb) { - _request('GET', path + 'issues' + query, options, cb); - }; - - this.users = function(options, cb) { - _request('GET', path + 'users' + query, options, cb); - }; - }; - - return Github; - }; - - // Top Level API - // ------- - - Github.getIssues = function(user, repo) { - return new Github.Issue({ - user: user, repo: repo - }); - }; - - Github.getRepo = function(user, repo) { - if (!repo) { - var fullname = user; - - return new Github.Repository({ - fullname: fullname - }); - } else { - return new Github.Repository({ - user: user, name: repo - }); - } - }; - - Github.getUser = function() { - return new Github.User(); - }; - - Github.getGist = function(id) { - return new Github.Gist({ - id: id - }); - }; - - Github.getSearch = function(query) { - return new Github.Search({ - query: query - }); - }; - return Github; -})); + return new Repository({ + apiUrl: this.apiUrl, + username: this.username, + password: this.password, + token: this.token, + fullname: fullname + }); +}; + +Github.prototype.getUser = function() { + return new User({ + apiUrl: this.apiUrl, + username: this.username, + password: this.password, + token: this.token + }); +}; + +Github.prototype.getGist = function(id) { + return new Gist({ + apiUrl: this.apiUrl, + username: this.username, + password: this.password, + token: this.token, + id: id + }); +}; + +Github.prototype.getSearch = function(query) { + return new Search({ + apiUrl: this.apiUrl, + username: this.username, + password: this.password, + token: this.token, + query: query + }); +}; + +module.exports = Github; diff --git a/gulpfile.js b/gulpfile.js index ce55e31e..673e3bc5 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,13 +1,77 @@ 'use strict'; var gulp = require('gulp'); -var jshint = require('gulp-jshint'); var jscs = require('gulp-jscs'); -var rename = require('gulp-rename'); -var uglify = require('gulp-uglify'); +var mocha = require('gulp-mocha'); +var jshint = require('gulp-jshint'); +var webpack = require('webpack-stream'); +var codecov = require('gulp-codecov.io'); var stylish = require('gulp-jscs-stylish'); + var path = require('path'); var karma = require('karma'); +var sequence = require('run-sequence'); + +/* + * Code style enforcement + */ +gulp.task('lint', function() { + return gulp.src([ + path.join(__dirname, '/*.js'), + path.join(__dirname, '/test/*.js') + ], + { + base: './' + }) + .pipe(jshint()) + .pipe(jscs({ + fix: true + })) + .pipe(stylish.combineWithHintResults()) + .pipe(jscs.reporter()) + .pipe(jshint.reporter('jshint-stylish')) + .pipe(gulp.dest('.')); +}); + +/* + * Testing fixtures + */ +var sauceLaunchers = { + SL_Chrome: { + base: 'SauceLabs', + browserName: 'chrome', + version: '45' + }, + SL_Firefox: { + base: 'SauceLabs', + browserName: 'firefox', + version: '39' + }, + SL_Safari: { + base: 'SauceLabs', + browserName: 'safari', + platform: 'OS X 10.10', + version: '8' + }, + SL_IE_10: { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 2012', + version: '10' + }, + SL_IE_11: { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 8.1', + version: '11' + }, + SL_iOS: { + base: 'SauceLabs', + browserName: 'iphone', + platform: 'OS X 10.10', + version: '8.1' + } +}; function runTests(singleRun, isCI, done) { var reporters = ['mocha']; @@ -15,8 +79,9 @@ function runTests(singleRun, isCI, done) { var files = [ path.join(__dirname, 'test/vendor/*.js'), // PhantomJS 1.x polyfills - path.join(__dirname, 'github.js'), - path.join(__dirname, 'test/test.*.js') + path.join(__dirname, 'dist', 'github.min.js'), + path.join(__dirname, 'test', 'helpers.js'), + path.join(__dirname, 'test', 'test.*.js') ]; if (singleRun) { @@ -47,7 +112,9 @@ function runTests(singleRun, isCI, done) { localConfig.sauceLabs = { testName: 'GitHub.js UAT tests', idleTimeout: 120000, - recordVideo: false + recordVideo: false, + username: 'clayreimann', + accessKey: '16176d74-6882-4457-b4cd-d8f36aa9d0be' }; // Increase timeouts massively so Karma doesn't timeout in Sauce tunnel. @@ -72,28 +139,19 @@ function runTests(singleRun, isCI, done) { server.start(); } // End runTests() -gulp.task('lint', function() { - return gulp.src([ - path.join(__dirname, '/*.js'), - path.join(__dirname, '/test/*.js') - ], - { - base: './' - }) - .pipe(jshint()) - .pipe(jscs({ - fix: true - })) - .pipe(stylish.combineWithHintResults()) - .pipe(jscs.reporter()) - .pipe(jshint.reporter('jshint-stylish')) - .pipe(gulp.dest('.')); -}); +gulp.task('test', ['test:node', 'test:web']); -gulp.task('test', function(done) { +gulp.task('test:web', ['build'], function(done) { runTests(true, false, done); }); +gulp.task('test:node', function() { + return gulp.src('test/test.*.js') + .pipe(mocha({ + timeout: 10000 + })); +}); + gulp.task('test:ci', function(done) { runTests(true, true, done); }); @@ -102,50 +160,45 @@ gulp.task('test:auto', function(done) { runTests(false, false, done); }); -gulp.task('build', function() { - return gulp.src('github.js') - .pipe(uglify()) - .pipe(rename('github.min.js')) - .pipe(gulp.dest('dist/')); -}); - -gulp.task('default', function() { - gulp.start('lint', 'test', 'build'); +gulp.task('codecov', function() { + return gulp.src('coverage/*/lcov.info') + .pipe(codecov()); }); -var sauceLaunchers = { - SL_Chrome: { - base: 'SauceLabs', - browserName: 'chrome', - version: '45' - }, - SL_Firefox: { - base: 'SauceLabs', - browserName: 'firefox', - version: '39' +/* + * Build + */ +var WEBPACK_CONFIG = { + entry: { + github: './github.js' }, - SL_Safari: { - base: 'SauceLabs', - browserName: 'safari', - platform: 'OS X 10.10', - version: '8' + output: { + library: 'Github', + libraryTarget: 'umd', + filename: '[name].min.js' }, - SL_IE_10: { - base: 'SauceLabs', - browserName: 'internet explorer', - platform: 'Windows 2012', - version: '10' - }, - SL_IE_11: { - base: 'SauceLabs', - browserName: 'internet explorer', - platform: 'Windows 8.1', - version: '11' + resolve: { + extensions: ['', '.js'] }, - SL_iOS: { - base: 'SauceLabs', - browserName: 'iphone', - platform: 'OS X 10.10', - version: '8.1' + plugins: [ + new webpack.webpack.optimize.UglifyJsPlugin() + ], + devtool: 'source-map', + stats: { + colors: 'true' } }; + +gulp.task('build', function() { + return gulp.src('./github.js') + .pipe(webpack(WEBPACK_CONFIG)) + .pipe(gulp.dest('dist/')) + ; +}); + +/* + * Miscellaneous tasks + */ +gulp.task('default', function() { + gulp.start('lint', 'test', 'build'); +}); diff --git a/karma.conf.js b/karma.conf.js index 226a0049..1b3120f9 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -17,16 +17,16 @@ module.exports = function(config) { frameworks: ['mocha', 'chai'], - browsers: ['PhantomJS'], + browsers: ['Chrome'], coverageReporter: { reporters: [ { - type: 'lcov' - }, - { - type: 'text-summary' - } + type: 'lcov' + }, + { + type: 'text-summary' + } ], instrumenterOptions: { istanbul: { diff --git a/lib/Gist.js b/lib/Gist.js new file mode 100644 index 00000000..4fc21ef0 --- /dev/null +++ b/lib/Gist.js @@ -0,0 +1,66 @@ +'use strict'; + +/*! + * @overview Gist.js + * + * @copyright (c) 2015 Michael Aufreiter, Development Seed + * Github.js is freely distributable. + * @license Licensed under BSD-3-Clause-Clear + * + * For all details and documentation: + * http://substance.io/michael/github + */ +var inherits = require('inherits'); +var Requestable = require('./helpers/Requestable'); + +function Gist(options) { + Gist.super_.call(this, options); + this.gistPath = '/gists/' + options.id; +} + +inherits(Gist, Requestable); + +Gist.prototype.read = function(cb) { + this._request('GET', this.gistPath, null, cb); +}; + +// Create the gist +// -------- +// { +// "description": "the description for this gist", +// "public": true, +// "files": { +// "file1.txt": { +// "content": "String file contents" +// } +// } +// } +Gist.prototype.create = function(options, cb) { + this._request('POST', '/gists', options, cb); +}; + +Gist.prototype.delete = function(cb) { + this._request('DELETE', this.gistPath, null, cb); +}; + +Gist.prototype.fork = function(cb) { + this._request('POST', this.gistPath + '/fork', null, cb); +}; + +Gist.prototype.update = function(options, cb) { + this._request('PATCH', this.gistPath, options, cb); +}; + +Gist.prototype.star = function(cb) { + this._request('PUT', this.gistPath + '/star', null, cb); +}; + +Gist.prototype.unstar = function(cb) { + this._request('DELETE', this.gistPath + '/star', null, cb); +}; + +Gist.prototype.isStarred = function(cb) { + this._request('GET', this.gistPath + '/star', null, cb); +}; + +module.exports = Gist; diff --git a/lib/Issue.js b/lib/Issue.js new file mode 100644 index 00000000..88d0f7a1 --- /dev/null +++ b/lib/Issue.js @@ -0,0 +1,40 @@ +'use strict'; + +/*! + * @overview Issue.js + * + * @copyright (c) 2015 Michael Aufreiter, Development Seed + * Github.js is freely distributable. + * @license Licensed under BSD-3-Clause-Clear + * + * For all details and documentation: + * http://substance.io/michael/github + */ + +var inherits = require('inherits'); +var Requestable = require('./helpers/Requestable'); + +function Issue(options) { + Issue.super_.call(this, options); + this.path = '/repos/' + options.user + '/' + options.repo + '/issues'; +} + +inherits(Issue, Requestable); + +Issue.prototype.list = function(options, cb) { + var query = Object.keys(options).map(function(key) { + return encodeURIComponent(key) + '=' + encodeURIComponent(options[key]); + }); + + this._requestAllPages(this.path + '?' + query.join('&'), cb); +}; + +Issue.prototype.comment = function(issue, comment, cb) { + this._request('POST', issue.comments_url, { + body: comment + }, function(err, res) { + cb(err, res); + }); +}; + +module.exports = Issue; diff --git a/lib/Repository.js b/lib/Repository.js new file mode 100644 index 00000000..69c2f72d --- /dev/null +++ b/lib/Repository.js @@ -0,0 +1,493 @@ +'use strict'; + +/*! + * @overview Repository.js + * + * @copyright (c) 2015 Michael Aufreiter, Development Seed + * Github.js is freely distributable. + * @license Licensed under BSD-3-Clause-Clear + * + * For all details and documentation: + * http://substance.io/michael/github + */ + +var debug = require('debug')('Github:Repository'); +var inherits = require('inherits'); +var Base64 = require('js-base64').Base64; + +var Requestable = require('./helpers/Requestable'); +var User = require('./User'); + +function Repository(options) { + Repository.super_.call(this, options); + this.repo = options.name; + this.user = options.user; + this.fullname = options.fullname; + + if (this.fullname) { + this.repoPath = '/repos/' + this.fullname; + } else { + this.repoPath = '/repos/' + this.user + '/' + this.repo; + } + + this.currentTree = { + branch: null, + sha: null + }; +} + +inherits(Repository, Requestable); + +// Uses the cache if branch has not been changed +// ------- +Repository.prototype._updateTree = function(branch, cb) { + if (branch === this.currentTree.branch && this.currentTree.sha) { + return cb(null, this.currentTree.sha); + } + + this.getRef('heads/' + branch, function(err, sha) { + this.currentTree.branch = branch; + this.currentTree.sha = sha; + cb(err, sha); + }); +}; + +Repository.prototype.getRef = function(ref, cb) { + this._request('GET', this.repoPath + '/git/refs/' + ref, null, function(err, res, xhr) { + if (err) { + return cb(err); + } + + cb(null, res.object.sha, xhr); + }); +}; + +// Create a new reference +// -------- +// +// { +// "ref": "refs/heads/my-new-branch-name", +// "sha": "827efc6d56897b048c772eb4087f854f46256132" +// } +Repository.prototype.createRef = function(options, cb) { + this._request('POST', this.repoPath + '/git/refs', options, cb); +}; + +// Delete a reference +// -------- +// +// Repo.deleteRef('heads/gh-pages') +// repo.deleteRef('tags/v1.0') +Repository.prototype.deleteRef = function(ref, cb) { + this._request('DELETE', this.repoPath + '/git/refs/' + ref, null, cb); +}; + +Repository.prototype.createRepo = function(options, cb) { + this._request('POST', '/user/repos', options, cb); +}; + +Repository.prototype.deleteRepo = function(cb) { + this._request('DELETE', this.repoPath, null, cb); +}; + +Repository.prototype.listTags = function(cb) { + this._request('GET', this.repoPath + '/tags', null, function(err, tags, xhr) { + if (err) { + return cb(err); + } + + cb(null, tags, xhr); + }); +}; + +Repository.prototype.listPulls = function(options, cb) { + options = options || {}; + var url = this.repoPath + '/pulls'; + var params = []; + + if (typeof options === 'string') { + // Backward compatibility + params.push('state=' + options); + } else { + if (options.state) { + params.push('state=' + encodeURIComponent(options.state)); + } + + if (options.head) { + params.push('head=' + encodeURIComponent(options.head)); + } + + if (options.base) { + params.push('base=' + encodeURIComponent(options.base)); + } + + if (options.sort) { + params.push('sort=' + encodeURIComponent(options.sort)); + } + + if (options.direction) { + params.push('direction=' + encodeURIComponent(options.direction)); + } + + if (options.page) { + params.push('page=' + options.page); + } + + if (options.per_page) { + params.push('per_page=' + options.per_page); + } + } + + if (params.length > 0) { + url += '?' + params.join('&'); + } + + this._request('GET', url, null, function(err, pulls, xhr) { + if (err) return cb(err); + cb(null, pulls, xhr); + }); +}; + +Repository.prototype.getPull = function(number, cb) { + this._request('GET', this.repoPath + '/pulls/' + number, null, function(err, pull, xhr) { + if (err) return cb(err); + cb(null, pull, xhr); + }); +}; + +Repository.prototype.compare = function(base, head, cb) { + this._request('GET', this.repoPath + '/compare/' + base + '...' + head, null, function(err, diff, xhr) { + if (err) return cb(err); + cb(null, diff, xhr); + }); +}; + +Repository.prototype.listBranches = function(cb) { + this._request('GET', this.repoPath + '/git/refs/heads', null, function(err, heads, xhr) { + if (err) return cb(err); + cb(null, heads.map(function(head) { + return head.ref.replace(/^refs\/heads\//, ''); + }), xhr); + }); +}; + +Repository.prototype.getBlob = function(sha, cb) { + this._request('GET', this.repoPath + '/git/blobs/' + sha, null, cb, 'raw'); +}; + +Repository.prototype.getCommit = function(branch, sha, cb) { + this._request('GET', this.repoPath + '/git/commits/' + sha, null, function(err, commit, xhr) { + if (err) return cb(err); + cb(null, commit, xhr); + }); +}; + +Repository.prototype.getSha = function(branch, path, cb) { + if (!path || path === '') return this.getRef('heads/' + branch, cb); + + this._request('GET', this.repoPath + '/contents/' + path + (branch ? '?ref=' + branch : ''), + null, function(err, pathContent, xhr) { + debug(err); + + if (err) return cb(err); + cb(null, pathContent.sha, xhr); + }); +}; + +Repository.prototype.getStatuses = function(sha, cb) { + this._request('GET', this.repoPath + '/statuses/' + sha, null, cb); +}; + +Repository.prototype.getTree = function(tree, cb) { + this._request('GET', this.repoPath + '/git/trees/' + tree, null, function(err, res, xhr) { + if (err) return cb(err); + cb(null, res.tree, xhr); + }); +}; + +Repository.prototype.postBlob = function(content, cb) { + if (typeof (content) === 'string') { + content = { + content: content, + encoding: 'utf-8' + }; + } else { + content = { + content: Base64.encode(content), + encoding: 'base64' + }; + } + + this._request('POST', this.repoPath + '/git/blobs', content, function(err, res) { + if (err) return cb(err); + cb(null, res.sha); + }); +}; + +Repository.prototype.updateTree = function(baseTree, path, blob, cb) { + var data = { + base_tree: baseTree, + tree: [ + { + path: path, + mode: '100644', + type: 'blob', + sha: blob + } + ] + }; + + this._request('POST', this.repoPath + '/git/trees', data, function(err, res) { + if (err) return cb(err); + cb(null, res.sha); + }); +}; + +Repository.prototype.postTree = function(tree, cb) { + this._request('POST', this.repoPath + '/git/trees', { + tree: tree + }, function(err, res) { + if (err) return cb(err); + cb(null, res.sha); + }); +}; + +Repository.prototype.commit = function(parent, tree, message, cb) { + var user = new User(this.__requestableConfig); + var that = this; + + user.show(null, function(err, userData) { + if (err) return cb(err); + var data = { + message: message, + author: { + name: that.__requestableConfig.user, + email: userData.email + }, + parents: [ + parent + ], + tree: tree + }; + + that._request('POST', that.repoPath + '/git/commits', data, function(err, res) { + if (err) return cb(err); + that.currentTree.sha = res.sha; // Update latest commit + cb(null, res.sha); + }); + }); +}; + +Repository.prototype.updateHead = function(head, commit, cb) { + this._request('PATCH', this.repoPath + '/git/refs/heads/' + head, { + sha: commit + }, function(err) { + cb(err); + }); +}; + +Repository.prototype.show = function(cb) { + this._request('GET', this.repoPath, null, cb); +}; + +Repository.prototype.contributors = function (cb, retry) { + retry = retry || 1000; + var that = this; + + this._request('GET', this.repoPath + '/stats/contributors', null, function (err, data, xhr) { + if (err) return cb(err); + + if (xhr.status === 202) { + setTimeout( + function () { + that.contributors(cb, retry); + }, + retry + ); + } else { + cb(err, data, xhr); + } + }); +}; + +Repository.prototype.contents = function(ref, path, cb) { + path = encodeURI(path); + this._request('GET', this.repoPath + '/contents' + (path ? '/' + path : ''), { + ref: ref + }, cb); +}; + +Repository.prototype.fork = function(cb) { + this._request('POST', this.repoPath + '/forks', null, cb); +}; + +Repository.prototype.listForks = function(cb) { + this._request('GET', this.repoPath + '/forks', null, cb); +}; + +Repository.prototype.branch = function(oldBranch, newBranch, cb) { + var that = this; + + if (arguments.length === 2 && typeof arguments[1] === 'function') { + cb = newBranch; + newBranch = oldBranch; + oldBranch = 'master'; + } + + this.getRef('heads/' + oldBranch, function(err, ref) { + if (err && cb) return cb(err); + that.createRef({ + ref: 'refs/heads/' + newBranch, + sha: ref + }, cb); + }); +}; + +Repository.prototype.createPullRequest = function(options, cb) { + this._request('POST', this.repoPath + '/pulls', options, cb); +}; + +Repository.prototype.listHooks = function(cb) { + this._request('GET', this.repoPath + '/hooks', null, cb); +}; + +Repository.prototype.getHook = function(id, cb) { + this._request('GET', this.repoPath + '/hooks/' + id, null, cb); +}; + +Repository.prototype.createHook = function(options, cb) { + this._request('POST', this.repoPath + '/hooks', options, cb); +}; + +Repository.prototype.editHook = function(id, options, cb) { + this._request('PATCH', this.repoPath + '/hooks/' + id, options, cb); +}; + +Repository.prototype.deleteHook = function(id, cb) { + this._request('DELETE', this.repoPath + '/hooks/' + id, null, cb); +}; + +Repository.prototype.read = function(branch, path, cb) { + this._request('GET', this.repoPath + '/contents/' + encodeURI(path) + (branch ? '?ref=' + branch : ''), + null, function(err, obj, xhr) { + if (err && err.error === 404) return cb('not found', null, null); + + if (err) return cb(err); + cb(null, obj, xhr); + }, true); +}; + +Repository.prototype.remove = Repository.prototype.delete = function(branch, path, cb) { + var that = this; + + this.getSha(branch, path, function(err, sha) { + if (err) return cb(err); + + that._request('DELETE', that.repoPath + '/contents/' + path, { + message: path + ' is removed', + sha: sha, + branch: branch + }, cb); + }); +}; + +Repository.prototype.move = function(branch, path, newPath, cb) { + var that = this; + + this._updateTree(branch, function(err, latestCommit) { + that.getTree(latestCommit + '?recursive=true', function(err, tree) { + // Update Tree + tree.forEach(function(ref) { + if (ref.path === path) ref.path = newPath; + + if (ref.type === 'tree') delete ref.sha; + }); + + that.postTree(tree, function(err, rootTree) { + that.commit(latestCommit, rootTree, 'Deleted ' + path , function(err, commit) { + that.updateHead(branch, commit, function(err) { + cb(err); + }); + }); + }); + }); + }); +}; + +Repository.prototype.write = function(branch, path, content, message, options, cb) { + var that = this; + + if (typeof cb === 'undefined') { + cb = options; + options = {}; + } + + options = options || {}; + + var committer = options.committer; + var author = options.author; + + this.getSha(branch, encodeURI(path), function(err, sha) { + var writeOptions = { + message: message, + content: Base64.encode(content), + branch: branch, + committer: committer, + author: author + }; + + // If no error, we set the sha to overwrite an existing file + if (!(err && err.error !== 404)) writeOptions.sha = sha; + that._request('PUT', that.repoPath + '/contents/' + encodeURI(path), writeOptions, cb); + }); +}; + +Repository.prototype.getCommits = function(options, cb) { + options = options || {}; + var url = this.repoPath + '/commits'; + var params = []; + + if (options.sha) { + params.push('sha=' + encodeURIComponent(options.sha)); + } + + if (options.path) { + params.push('path=' + encodeURIComponent(options.path)); + } + + if (options.since) { + var since = options.since; + + if (since.constructor === Date) { + since = since.toISOString(); + } + + params.push('since=' + encodeURIComponent(since)); + } + + if (options.until) { + var until = options.until; + + if (until.constructor === Date) { + until = until.toISOString(); + } + + params.push('until=' + encodeURIComponent(until)); + } + + if (options.page) { + params.push('page=' + options.page); + } + + if (options.perpage) { + params.push('per_page=' + options.perpage); + } + + if (params.length > 0) { + url += '?' + params.join('&'); + } + + this._request('GET', url, null, cb); +}; + +module.exports = Repository; diff --git a/lib/Search.js b/lib/Search.js new file mode 100644 index 00000000..617c5409 --- /dev/null +++ b/lib/Search.js @@ -0,0 +1,41 @@ +'use strict'; + +/*! + * @overview Search.js + * + * @copyright (c) 2015 Michael Aufreiter, Development Seed + * Github.js is freely distributable. + * @license Licensed under BSD-3-Clause-Clear + * + * For all details and documentation: + * http://substance.io/michael/github + */ + +var inherits = require('inherits'); +var Requestable = require('./helpers/Requestable'); + +function Search(options) { + Search.super_.call(this, options); + this.path = '/search/'; + this.query = '?q=' + options.query; +} + +inherits(Search, Requestable); + +Search.prototype.repositories = function(options, cb) { + this._request('GET', this.path + 'repositories' + this.query, options, cb); +}; + +Search.prototype.code = function(options, cb) { + this._request('GET', this.path + 'code' + this.query, options, cb); +}; + +Search.prototype.issues = function(options, cb) { + this._request('GET', this.path + 'issues' + this.query, options, cb); +}; + +Search.prototype.users = function(options, cb) { + this._request('GET', this.path + 'users' + this.query, options, cb); +}; + +module.exports = Search; diff --git a/lib/User.js b/lib/User.js new file mode 100644 index 00000000..4035a571 --- /dev/null +++ b/lib/User.js @@ -0,0 +1,147 @@ +'use strict'; + +/*! + * @overview User.js + * + * @copyright (c) 2015 Michael Aufreiter, Development Seed + * Github.js is freely distributable. + * @license Licensed under BSD-3-Clause-Clear + * + * For all details and documentation: + * http://substance.io/michael/github + */ + +var inherits = require('inherits'); +var Requestable = require('./helpers/Requestable'); + +function User(options) { + User.super_.call(this, options); +} + +inherits(User, Requestable); + +User.prototype.repos = function(options, cb) { + if (arguments.length === 1 && typeof arguments[0] === 'function') { + cb = options; + options = {}; + } + + options = options || {}; + + var url = '/user/repos'; + var params = []; + + params.push('type=' + encodeURIComponent(options.type || 'all')); + params.push('sort=' + encodeURIComponent(options.sort || 'updated')); + params.push('per_page=' + encodeURIComponent(options.per_page || '1000')); // jscs:ignore + + if (options.page) { + params.push('page=' + encodeURIComponent(options.page)); + } + + url += '?' + params.join('&'); + + this._request('GET', url, null, cb); +}; + +User.prototype.orgs = function(cb) { + this._request('GET', '/user/orgs', null, cb); +}; + +User.prototype.gists = function(cb) { + this._request('GET', '/gists', null, cb); +}; + +User.prototype.notifications = function(options, cb) { + if (arguments.length === 1 && typeof arguments[0] === 'function') { + cb = options; + options = {}; + } + + options = options || {}; + var url = '/notifications'; + var params = []; + + if (options.all) { + params.push('all=true'); + } + + if (options.participating) { + params.push('participating=true'); + } + + if (options.since) { + var since = options.since; + + if (since.constructor === Date) { + since = since.toISOString(); + } + + params.push('since=' + encodeURIComponent(since)); + } + + if (options.before) { + var before = options.before; + + if (before.constructor === Date) { + before = before.toISOString(); + } + + params.push('before=' + encodeURIComponent(before)); + } + + if (options.page) { + params.push('page=' + encodeURIComponent(options.page)); + } + + if (params.length > 0) { + url += '?' + params.join('&'); + } + + this._request('GET', url, null, cb); +}; + +User.prototype.show = function(username, cb) { + var command = username ? '/users/' + username : '/user'; + + this._request('GET', command, null, cb); +}; + +User.prototype.userRepos = function(username, cb) { + // Github does not always honor the 1000 limit so we want to iterate over the data set. + this._requestAllPages('/users/' + username + '/repos?type=all&per_page=1000&sort=updated', cb); +}; + +User.prototype.userStarred = function(username, cb) { + // Github does not always honor the 1000 limit so we want to iterate over the data set. + this._requestAllPages('/users/' + username + '/starred?type=all&per_page=1000', function(err, res) { + cb(err, res); + }); +}; + +User.prototype.userGists = function(username, cb) { + this._request('GET', '/users/' + username + '/gists', null, cb); +}; + +User.prototype.orgRepos = function(orgname, cb) { + // Github does not always honor the 1000 limit so we want to iterate over the data set. + this._requestAllPages('/orgs/' + orgname + '/repos?type=all&&page_num=1000&sort=updated&direction=desc', cb); +}; + +User.prototype.follow = function(username, cb) { + this._request('PUT', '/user/following/' + username, null, cb); +}; + +User.prototype.unfollow = function(username, cb) { + this._request('DELETE', '/user/following/' + username, null, cb); +}; + +User.prototype.createRepo = function(options, cb) { + this._request('POST', '/user/repos', options, cb); +}; + +User.prototype.rateLimit = function(cb) { + this._request('GET', '/rate_limit', null, cb); +}; + +module.exports = User; diff --git a/lib/helpers/Requestable.js b/lib/helpers/Requestable.js new file mode 100644 index 00000000..1519b28a --- /dev/null +++ b/lib/helpers/Requestable.js @@ -0,0 +1,154 @@ +'use strict'; + +/*! + * @overview Requestable.js + * + * @copyright (c) 2015 Michael Aufreiter, Development Seed + * Github.js is freely distributable. + * @license Licensed under BSD-3-Clause-Clear + * + * For all details and documentation: + * http://substance.io/michael/github + */ + +var debug = require('debug')('Github:Requestable'); +var Base64 = require('js-base64').Base64; +var Request = require('axios'); + +function Requestable(options) { + this.__baseUrl = options.apiUrl; + this.__requestableConfig = { + apiUrl: options.apiUrl, + token: options.token, + username: options.username, + password: options.password + }; + + if ((options.token) || (options.username && options.password)) { + this.__authorizationHeader = options.token ? + 'token ' + options.token : + 'Basic ' + Base64.encode(options.username + ':' + options.password); + } +} + +Requestable.prototype._getURL = function(path) { + var url = path.indexOf('//') >= 0 ? path : this.__baseUrl + path; + + url += ((/\?/).test(url) ? '&' : '?'); + + return url; +}; + +function buildError(path, response) { + return { + path: path, + request: response.config, + response: response, + status: response.status + }; +} + +// This is a hack util axios's next update +// (which will include following redirects in NodeJS) +function getRequestPromise(config) { + debug('', config.method, 'to', config.url); + + return Request(config).catch(function(response) { + if (response.status === 301 || response.status === 302) { + debug('', 'Redirecting to ' + response.headers.location); + config.url = response.headers.location; + + return getRequestPromise(config); + } + + throw response; + }); +} + +Requestable.prototype._request = function(method, path, data, cb, raw) { + var headers = { + Accept: raw ? 'application/vnd.github.v3.raw+json' : 'application/vnd.github.v3+json', + 'Content-Type': 'application/json;charset=UTF-8' + }; + var url = this._getURL(path); + var queryParams = {}; + + if (this.__authorizationHeader) { + headers.Authorization = this.__authorizationHeader; + } + + if (data && typeof data === 'object' && ['GET', 'HEAD', 'DELETE'].indexOf(method) !== -1) { + queryParams = data; + data = {}; + url = url.replace(/(×tamp=\d+)/, '') + + (typeof window !== 'undefined' ? '×tamp=' + new Date().getTime() : ''); + } + + var config = { + url: url, + method: method, + headers: headers, + params: queryParams, + data: data, + responseType: raw ? 'text' : 'json' + }; + var requestPromise = getRequestPromise(config); + + if (cb) { + debug('callback provided: making request for user'); + requestPromise.then(function(response) { + debug('', 'response', response.config.method, response.config.url); + cb(null, response.data || true, response); + }).catch(function(response) { + debug('', 'catch', response.config.method, response.config.url); + cb(buildError(path, response)); + }); + } else { + debug('no callback: returning promise'); + + return requestPromise; + } +}; + +Requestable.prototype._requestAllPages = function(path, cb, results) { + var that = this; + + results = results || []; + + return this._request('GET', path, null) + .then(function(response) { + results.push.apply(results, response.data); + + var linksHeader = response.headers.link || ''; + var links = linksHeader.split(/\s*,\s*/g); + var next = null; + + links.forEach(function(link) { + next = link.search(/rel="next"/) !== -1 ? link : next; + }); + + if (next) { + next = (next.match(/<(.*)>/) || [])[1]; + } + + if (next) { + debug('', 'getting next page', next); + + return that._requestAllPages(next, cb, results); + } + + if (cb) { + cb(null, results); + } else { + return Promise.resolve(results); + } + }).catch(function(response) { + if (cb) { + cb(buildError(path, response)); + } else { + throw response; + } + }); +}; + +module.exports = Requestable; diff --git a/package.json b/package.json index 01a8ec3f..e7cefa52 100644 --- a/package.json +++ b/package.json @@ -2,59 +2,61 @@ "name": "github-api", "version": "0.10.7", "description": "A higher-level wrapper around the Github API.", + "license": "BSD-3-Clause-Clear", + "readmeFilename": "README.md", + "scripts": { + "test": "gulp lint && gulp test", + "lint": "gulp lint", + "codecov": "cat coverage/*/lcov.info | codecov" + }, + "bugs": { + "url": "https://github.com/michael/github/issues" + }, + "repository": { + "type": "git", + "url": "git://github.com/michael/github.git" + }, + "keywords": [ + "github", + "api" + ], + "contributors": [ + "Ændrew Rininsland (http://www.aendrew.com)", + "Aurelio De Rosa (http://www.audero.it/)", + "Michael Aufreiter (http://substance.io)", + "Clay Reimann (http://clayreimann.me)" + ], "main": "github.js", "dependencies": { - "js-base64": "^2.1.8", - "xmlhttprequest": "~1.7.0" + "axios": "^0.7.0", + "debug": "^2.2.0", + "es6-promise": "^3.0.2", + "inherits": "^2.0.1", + "js-base64": "^2.1.8" }, "devDependencies": { "chai": "^3.4.0", "codecov": "^1.0.1", "gulp": "^3.9.0", + "gulp-codecov.io": "^1.0.1", "gulp-jscs": "^3.0.1", "gulp-jscs-stylish": "^1.2.1", "gulp-jshint": "^1.11.2", - "gulp-rename": "^1.2.2", - "gulp-uglify": "^1.4.2", + "gulp-mocha": "^2.2.0", "istanbul": "^0.3.13", "jshint": "^2.5.8", "jshint-stylish": "^2.0.1", "karma": "^0.13.14", "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^0.2.2", "karma-coverage": "^0.5.3", "karma-json-fixtures-preprocessor": "0.0.5", "karma-mocha": "^0.2.0", "karma-mocha-reporter": "^1.1.1", - "karma-phantomjs-launcher": "^0.2.1", "karma-sauce-launcher": "^0.3.0", - "mocha": "^2.3.3" - }, - "scripts": { - "test": "gulp test && gulp lint", - "lint": "gulp lint", - "codecov": "cat coverage/*/lcov.info | codecov" - }, - "repository": { - "type": "git", - "url": "git://github.com/michael/github.git" - }, - "keywords": [ - "github", - "api" - ], - "contributors": [ - "Ændrew Rininsland (http://www.aendrew.com)", - "Aurelio De Rosa (http://www.audero.it/)", - "Michael Aufreiter (http://substance.io)" - ], - "license": "BSD-3-Clause-Clear", - "readmeFilename": "README.md", - "gitHead": "aa8aa3c8cd5ce5240373d4fd1d06a7ab4af41a36", - "bugs": { - "url": "https://github.com/michael/github/issues" + "mocha": "^2.3.3", + "run-sequence": "^1.1.5", + "webpack-stream": "^2.3.0" }, - "browser": { - "xmlhttprequest": false, - "base64": false - } + "gitHead": "aa8aa3c8cd5ce5240373d4fd1d06a7ab4af41a36" } diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 00000000..0f695e6f --- /dev/null +++ b/test/helpers.js @@ -0,0 +1,18 @@ +'use strict'; + +function callbackWithError(done, fn) { + var cb = function(err, response, xhr) { + try { + fn(err, response, xhr); + } catch(e) { + done(e); + } + }; + + return cb; +} + +if (typeof window === 'undefined') { + // Export stuff (comment here to make linter happy) + module.exports = callbackWithError; +} diff --git a/test/test.auth.js b/test/test.auth.js index af93d28f..cf2f4f33 100644 --- a/test/test.auth.js +++ b/test/test.auth.js @@ -1,56 +1,52 @@ 'use strict'; -var testUser, github, user; +var testUser; if (typeof window === 'undefined') { - // Module dependencies - var chai = require('chai'); var Github = require('../'); + var callbackWithError = require('./helpers.js'); testUser = require('./user.json'); + // Module dependencies + var chai = require('chai'); + // Use should flavour for Mocha var should = chai.should(); } -describe('Github constructor', function() { +describe('Authentication', function() { before(function() { if (typeof window !== 'undefined') testUser = window.__fixtures__['test/user']; + }); - github = new Github({ + it('should authenticate with valid credentials', function(done) { + var github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, auth: 'basic' }); + var user = github.getUser(); - user = github.getUser(); - }); - - it('should authenticate and return no errors', function(done) { - user.notifications(function(err) { + user.notifications(callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); -}); - -describe('Github constructor (failing case)', function() { - before(function() { - if (typeof window !== 'undefined') testUser = window.__fixtures__['test/user']; - github = new Github({ + it('should fail authentication with invalid credentials', function(done) { + var github = new Github({ username: testUser.USERNAME, password: 'fake124', auth: 'basic' }); - user = github.getUser(); - }); + var user = github.getUser(); + + user.notifications(callbackWithError(done, function(err) { + err.status.should.equal(401, 'Return 401 status for bad auth'); + err.response.data.message.should.equal('Bad credentials'); - it('should fail authentication and return err', function(done) { - user.notifications(function(err) { - err.request.status.should.equal(401, 'Return 401 status for bad auth'); - JSON.parse(err.request.responseText).message.should.equal('Bad credentials'); done(); - }); + })); }); }); diff --git a/test/test.issue.js b/test/test.issue.js index c261a58c..ed2acdb4 100644 --- a/test/test.issue.js +++ b/test/test.issue.js @@ -1,22 +1,25 @@ 'use strict'; -var testUser, github, issues; +var testUser; if (typeof window === 'undefined') { - // Module dependencies - var chai = require('chai'); var Github = require('../'); + var callbackWithError = require('./helpers.js'); testUser = require('./user.json'); + // Module dependencies + var chai = require('chai'); + // Use should flavour for Mocha var should = chai.should(); } -describe('Github.Issue', function() { +describe('Issues', function() { + var github, issues; + before(function() { if (typeof window !== 'undefined') testUser = window.__fixtures__['test/user']; - github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, @@ -27,20 +30,21 @@ describe('Github.Issue', function() { }); it('should list issues', function(done) { - issues.list({}, function(err, issues) { + issues.list({}, callbackWithError(done, function(err, issues) { should.not.exist(err); issues.should.have.length.above(0); done(); - }); + })); }); it('should post issue comment', function(done) { - issues.list({}, function(err, issuesList) { - issues.comment(issuesList[0], 'Comment test', function(err, res) { + issues.list({}, callbackWithError(done, function(err, issuesList) { + should.not.exist(err); + issues.comment(issuesList[0], 'Comment test', callbackWithError(done, function(err, res) { should.not.exist(err); res.body.should.equal('Comment test'); done(); - }); - }); + })); + })); }); }); diff --git a/test/test.repo.js b/test/test.repo.js index d5bab61f..b94239e5 100644 --- a/test/test.repo.js +++ b/test/test.repo.js @@ -1,439 +1,431 @@ 'use strict'; -var github, repo, user, testUser, imageB64, imageBlob; +var testUser, imageB64, imageBlob; if (typeof window === 'undefined') { // We're in NodeJS - // Module dependencies - var chai = require('chai'); var Github = require('../'); - - testUser = require('./user.json'); - - // Use should flavour for Mocha - var should = chai.should(); - + var callbackWithError = require('./helpers.js'); var fs = require('fs'); var path = require('path'); + testUser = require('./user.json'); imageBlob = fs.readFileSync(path.join(__dirname, 'gh.png')); // This is a Buffer(). imageB64 = imageBlob.toString('base64'); -} else { // We're in the browser - if (typeof window._phantom !== 'undefined') { - var xhr = new XMLHttpRequest(); - - xhr.responseType = 'blob'; - xhr.open('GET', 'base/test/gh.png'); - xhr.onload = function() { - var reader = new FileReader(); - - reader.onloadend = function() { - imageB64 = btoa(reader.result); - imageBlob = reader.result; - console.log(imageBlob); - }; - reader.readAsBinaryString(xhr.response); + // Module dependencies + var chai = require('chai'); + + // Use should flavour for Mocha + var should = chai.should(); +} else if (typeof window._phantom !== 'undefined') { + // We're in the Phantom + var xhr = new XMLHttpRequest(); + + xhr.responseType = 'blob'; + xhr.open('GET', 'base/test/gh.png'); + xhr.onload = function() { + var reader = new FileReader(); + + reader.onloadend = function() { + imageB64 = btoa(reader.result); + imageBlob = reader.result; + console.log(imageBlob); }; - xhr.send(); - } else { - // jscs:disable - imageB64 = 'iVBORw0KGgoAAAANSUhEUgAAACsAAAAmCAAAAAB4qD3CAAABgElEQVQ4y9XUsUocURQGYN/pAyMWBhGtrEIMiFiooGuVIoYsSBAsRSQvYGFWC4uFhUBYsilXLERQsDA20YAguIbo5PQp3F3inVFTheSvZoavGO79z+mJP0/Pv2nPtlfLpfLq9tljNquO62S8mj1kmy/8nrHm/Xaz1930bt5n1+SzVmyrilItsod9ON0td1V59xR9hwV2HsMRsbfROLo4amzsRcQw5vO2CZPJEU5CM2cXYTCxg7CY2mwIVhK7AkNZYg9g4CqxVwNwkNg6zOTKMQP1xFZgKWeXoJLYdSjl7BysJ7YBIzk7Ap8TewLOE3oOTtIz6y/64bfQn55ZTIAPd2gNTOTurcbzp7z50v1y/Pq2Q7Wczca8vFjG6LvbMo92hiPL96xO+eYVPkVExMdONetFXZ+l+eP9cuV7RER8a9PZwrloTXv2tfv285ZOt4rnrTXlydxCu9sZmGrdN8eXC3ATERHXsHD5wC7ZL3HdsaX9R3bUzlb7YWvn/9ipf93+An8cHsx3W3WHAAAAAElFTkSuQmCC'; - imageBlob = new Blob(); - // jscs:enable - } + reader.readAsBinaryString(xhr.response); + }; + + xhr.send(); +} else { + // We're in the browser + // jscs:disable + imageB64 = 'iVBORw0KGgoAAAANSUhEUgAAACsAAAAmCAAAAAB4qD3CAAABgElEQVQ4y9XUsUocURQGYN/pAyMWBhGtrEIMiFiooGuVIoYsSBAsRSQvYGFWC4uFhUBYsilXLERQsDA20YAguIbo5PQp3F3inVFTheSvZoavGO79z+mJP0/Pv2nPtlfLpfLq9tljNquO62S8mj1kmy/8nrHm/Xaz1930bt5n1+SzVmyrilItsod9ON0td1V59xR9hwV2HsMRsbfROLo4amzsRcQw5vO2CZPJEU5CM2cXYTCxg7CY2mwIVhK7AkNZYg9g4CqxVwNwkNg6zOTKMQP1xFZgKWeXoJLYdSjl7BysJ7YBIzk7Ap8TewLOE3oOTtIz6y/64bfQn55ZTIAPd2gNTOTurcbzp7z50v1y/Pq2Q7Wczca8vFjG6LvbMo92hiPL96xO+eYVPkVExMdONetFXZ+l+eP9cuV7RER8a9PZwrloTXv2tfv285ZOt4rnrTXlydxCu9sZmGrdN8eXC3ATERHXsHD5wC7ZL3HdsaX9R3bUzlb7YWvn/9ipf93+An8cHsx3W3WHAAAAAElFTkSuQmCC'; + imageBlob = new Blob(); + + // jscs:enable } -describe('Github.Repository', function() { +var repoTest = Math.floor(Math.random() * (100000 - 0)) + 0; + +describe('Repository', function() { + var github, repo, user; + before(function() { if (typeof window !== 'undefined') testUser = window.__fixtures__['test/user']; - github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, auth: 'basic' }); - - repo = github.getRepo('michael', 'github'); + user = github.getUser(); }); - it('should show repo', function(done) { - repo.show(function(err, res) { - should.not.exist(err); - res.full_name.should.equal('michael/github'); // jscs:ignore - done(); + describe('read from repository', function() { + before(function() { + repo = github.getRepo('michael', 'github'); }); - }); - it('should show repo contents', function(done) { - repo.contents('master', './', function(err) { - should.not.exist(err); - - // @TODO write better assertion. - done(); + it('should show repo', function(done) { + repo.show(callbackWithError(done, function(err, res) { + should.not.exist(err); + res.full_name.should.equal('michael/github'); // jscs:ignore + done(); + })); }); - }); - it('should fork repo', function(done) { - repo.fork(function(err) { - should.not.exist(err); + it('should show repo contents', function(done) { + repo.contents('master', './', callbackWithError(done, function(err) { + // @TODO write better assertion. + should.not.exist(err); - // @TODO write better assertion. - done(); + done(); + })); }); - }); - it('should list forks of repo', function(done) { - repo.listForks(function(err) { - should.not.exist(err); + it('should fork repo', function(done) { + repo.fork(callbackWithError(done, function(err) { + // @TODO write better assertion. + should.not.exist(err); - // @TODO write better assertion. - done(); + done(); + })); }); - }); - it('should show repo contributors', function(done) { - repo.contributors(function(err, res) { - should.not.exist(err); - res.should.be.instanceof(Array); - res.should.have.length.above(1); - should.exist(res[0].author); - should.exist(res[0].total); - should.exist(res[0].weeks); - done(); + it('should list forks of repo', function(done) { + repo.listForks(callbackWithError(done, function(err) { + // @TODO write better assertion. + should.not.exist(err); + + done(); + })); }); - }); - // @TODO repo.branch, repo.pull + it('should show repo contributors', function(done) { + repo.contributors(callbackWithError(done, function(err, res) { + should.not.exist(err); + res.should.be.instanceof(Array); + res.should.have.length.above(1); + should.exist(res[0].author); + should.exist(res[0].total); + should.exist(res[0].weeks); - it('should list repo branches', function(done) { - repo.listBranches(function(err) { - should.not.exist(err); - done(); + done(); + })); }); - }); - it('should read repo', function(done) { - repo.read('master', 'README.md', function(err, res) { - res.indexOf('# Github.js').should.be.above(-1); - done(); - }); - }); + // @TODO repo.branch, repo.pull - it('should get commit from repo', function(done) { - repo.getCommit('master', '20fcff9129005d14cc97b9d59b8a3d37f4fb633b', function(err, commit) { - should.not.exist(err); - commit.message.should.equal('v0.10.4'); - commit.author.date.should.equal('2015-03-20T17:01:42Z'); - done(); - }); - }); + it('should list repo branches', function(done) { + repo.listBranches(callbackWithError(done, function(err) { + should.not.exist(err); - it('should get statuses for a SHA from a repo', function(done) { - repo.getStatuses('20fcff9129005d14cc97b9d59b8a3d37f4fb633b', function(err, statuses) { - statuses.length.should.equal(6) - statuses.every(function(status) { - return status.url === 'https://api.github.com/repos/michael/github/statuses/20fcff9129005d14cc97b9d59b8a3d37f4fb633b' - }).should.equal(true); - done(); + done(); + })); }); - }); - it('should get a SHA from a repo', function(done) { - repo.getSha('master', '.gitignore', function(err, sha) { - should.not.exist(err); - done(); + it('should read repo', function(done) { + repo.read('master', 'README.md', callbackWithError(done, function(err, res) { + should.not.exist(err); + res.indexOf('# Github.js').should.be.above(-1); + done(); + })); }); - }); - it('should get a repo by fullname', function(done) { - var repo2 = github.getRepo('michael/github'); - - repo2.show(function(err, res) { - should.not.exist(err); - res.full_name.should.equal('michael/github'); // jscs:ignore - done(); + it('should get commit from repo', function(done) { + repo.getCommit('master', '20fcff9129005d14cc97b9d59b8a3d37f4fb633b', + callbackWithError(done, function(err, commit) { + should.not.exist(err); + commit.message.should.equal('v0.10.4'); + commit.author.date.should.equal('2015-03-20T17:01:42Z'); + done(); + }) + ); }); - }); -}); - -var repoTest = Math.floor(Math.random() * (100000 - 0)) + 0; -describe('Creating new Github.Repository', function() { - before(function() { - if (typeof window !== 'undefined') testUser = window.__fixtures__['test/user']; - - github = new Github({ - username: testUser.USERNAME, - password: testUser.PASSWORD, - auth: 'basic' + it('should get statuses for a SHA from a repo', function(done) { + repo.getStatuses('20fcff9129005d14cc97b9d59b8a3d37f4fb633b', callbackWithError(done, function(err, statuses) { + should.not.exist(err); + statuses.length.should.equal(6); + statuses.every(function(status) { + // jscs:disable + return status.url === 'https://api.github.com/repos/michael/github/statuses/20fcff9129005d14cc97b9d59b8a3d37f4fb633b'; + // jscs:enable + }).should.equal(true); + done(); + })); }); - user = github.getUser(); - repo = github.getRepo(testUser.USERNAME, repoTest); - }); - - it('should create repo', function(done) { - user.createRepo({ - name: repoTest - }, function(err, res) { - should.not.exist(err); - res.name.should.equal(repoTest.toString()); - done(); + it('should get a SHA from a repo', function(done) { + repo.getSha('master', '.gitignore', callbackWithError(done, function(err, sha) { + should.not.exist(err); + done(); + })); }); - }); - it('should write to repo', function(done) { - repo.write('master', 'TEST.md', 'THIS IS A TEST', 'Creating test', function(err) { - should.not.exist(err); + it('should get a repo by fullname', function(done) { + var repo2 = github.getRepo('michael/github'); - // @TODO write a better assertion. - done(); - }); - }); - - it('should write to repo branch', function(done) { - repo.branch('master', 'dev', function(err) { - should.not.exist(err); - repo.write('dev', 'TEST.md', 'THIS IS AN UPDATED TEST', 'Updating test', function(err) { + repo2.show(callbackWithError(done, function(err, res) { should.not.exist(err); - repo.read('dev', 'TEST.md', function(err, res) { - res.should.equal('THIS IS AN UPDATED TEST'); - should.not.exist(err); - done(); - }); - }); + res.full_name.should.equal('michael/github'); // jscs:ignore + done(); + })); }); }); - it('should get ref from repo', function(done) { - repo.getRef('heads/master', function(err) { - should.not.exist(err); - - // @TODO write better assertion - done(); + describe('creating new repository', function() { + before(function() { + repo = github.getRepo(testUser.USERNAME, repoTest); }); - }); - it('should create ref on repo', function(done) { - repo.getRef('heads/master', function(err, sha) { - var refSpec = { - ref: 'refs/heads/new-test-branch', sha: sha + it('should create repo', function(done) { + var newRepo = { + name: repoTest }; - repo.createRef(refSpec, function(err) { + user.createRepo(newRepo, callbackWithError(done, function(err, res) { should.not.exist(err); + res.name.should.equal(repoTest.toString()); - // @TODO write better assertion done(); - }); + })); }); - }); - it('should delete ref on repo', function(done) { - repo.deleteRef('heads/new-test-branch', function(err) { - should.not.exist(err); - - // @TODO write better assertion - done(); + it('should write to repo', function(done) { + repo.write('master', 'TEST.md', 'THIS IS A TEST', 'Creating test', callbackWithError(done, function(err) { + // @TODO write a better assertion. + should.not.exist(err); + done(); + })); }); - }); - - it('should list tags on repo', function(done) { - repo.listTags(function(err) { - should.not.exist(err); - // @TODO write better assertion - done(); + it('should write to repo branch', function(done) { + repo.branch('master', 'dev', callbackWithError(done, function(err) { + should.not.exist(err); + repo.write('dev', 'TEST.md', 'THIS IS AN UPDATED TEST', 'Updating test', + callbackWithError(done, function(err) { + should.not.exist(err); + repo.read('dev', 'TEST.md', callbackWithError(done, function(err, res) { + should.not.exist(err); + res.should.equal('THIS IS AN UPDATED TEST'); + done(); + })); + }) + ); + })); + }); + + it('should get ref from repo', function(done) { + repo.getRef('heads/master', callbackWithError(done, function(err) { + // @TODO write better assertion + should.not.exist(err); + done(); + })); }); - }); - it('should list pulls on repo', function(done) { - var repo = github.getRepo('michael', 'github'); - var options = { - state: 'all', - sort: 'updated', - direction: 'desc', - page: 1, - per_page: 10 - }; + it('should create ref on repo', function(done) { + repo.getRef('heads/master', callbackWithError(done, function(err, sha) { + should.not.exist(err); + var refSpec = { + ref: 'refs/heads/new-test-branch', sha: sha + }; - repo.listPulls(options, function(err, pull_list) { - should.not.exist(err); - pull_list.should.be.instanceof(Array); - pull_list.should.have.length(10); - should.exist(pull_list[0].title); - should.exist(pull_list[0].body); - should.exist(pull_list[0].url); - done(); + repo.createRef(refSpec, callbackWithError(done, function(err) { + // @TODO write better assertion + should.not.exist(err); + done(err); + })); + })); }); - }); - - it('should get pull requests on repo', function(done) { - var repo = github.getRepo('michael', 'github'); - repo.getPull(153, function(err) { - should.not.exist(err); - - // @TODO write better assertion - done(); + it('should delete ref on repo', function(done) { + repo.deleteRef('heads/new-test-branch', callbackWithError(done, function(err) { + // @TODO write better assertion + should.not.exist(err); + done(); + })); }); - }); - it('should delete a file on the repo', function(done) { - repo.write('master', 'REMOVE-TEST.md', 'THIS IS A TEST', 'Remove test', function(err) { - should.not.exist(err); - - repo.remove('master', 'REMOVE-TEST.md', function(err) { + it('should list tags on repo', function(done) { + repo.listTags(callbackWithError(done, function(err) { + // @TODO write better assertion should.not.exist(err); done(); - }); + })); }); - }); - it('should use repo.delete as an alias for repo.remove', function(done) { - repo.write('master', 'REMOVE-TEST.md', 'THIS IS A TEST', 'Remove test', function(err) { - should.not.exist(err); + it('should list pulls on repo', function(done) { + var repo = github.getRepo('michael', 'github'); + var options = { + state: 'all', + sort: 'updated', + direction: 'desc', + page: 1, + per_page: 10 + }; - repo.delete('master', 'REMOVE-TEST.md', function(err) { + repo.listPulls(options, callbackWithError(done, function(err, pull_list) { should.not.exist(err); + pull_list.should.be.instanceof(Array); + pull_list.should.have.length(10); + should.exist(pull_list[0].title); + should.exist(pull_list[0].body); + should.exist(pull_list[0].url); done(); - }); + })); }); - }); - it('should write author and committer to repo', function(done) { - var options = { - author: { - name: 'Author Name', email: 'author@example.com' - }, - committer: { - name: 'Committer Name', email: 'committer@example.com' - } - }; + it('should get pull requests on repo', function(done) { + var repo = github.getRepo('michael', 'github'); - repo.write('dev', 'TEST.md', 'THIS IS A TEST BY AUTHOR AND COMMITTER', 'Updating', options, function(err, res) { - should.not.exist(err); - repo.getCommit('dev', res.commit.sha, function(err, commit) { + repo.getPull(153, callbackWithError(done, function(err) { + // @TODO write better assertion should.not.exist(err); - commit.author.name.should.equal('Author Name'); - commit.author.email.should.equal('author@example.com'); - commit.committer.name.should.equal('Committer Name'); - commit.committer.email.should.equal('committer@example.com'); - done(); - }); + })); }); - }); - it('should be able to write CJK unicode to repo', function(done) { - repo.write('master', '中文测试.md', 'THIS IS A TEST', 'Creating test', function(err) { - should.not.exist(err); - - // @TODO write better assertion - done(); + it('should delete a file on the repo', function(done) { + repo.write('master', 'REMOVE-TEST.md', 'THIS IS A TEST', 'Remove test', callbackWithError(done, function(err) { + should.not.exist(err); + repo.remove('master', 'REMOVE-TEST.md', function(err) { + done(err); + }); + })); }); - }); - - it('should be able to write unicode to repo', function(done) { - repo.write('master', 'TEST_unicode.md', '\u2014', 'Long dash unicode', function(err) { - should.not.exist(err); - if (err) console.log(err); - repo.read('master', 'TEST_unicode.md', function(err, obj) { + it('should use repo.delete as an alias for repo.remove', function(done) { + repo.write('master', 'REMOVE-TEST.md', 'THIS IS A TEST', 'Remove test', callbackWithError(done, function(err) { should.not.exist(err); - obj.should.equal('\u2014'); + repo.delete('master', 'REMOVE-TEST.md', callbackWithError(done, function(err) { + should.not.exist(err); + done(); + })); + })); + }); + + it('should write author and committer to repo', function(done) { + var options = { + author: { + name: 'Author Name', email: 'author@example.com' + }, + committer: { + name: 'Committer Name', email: 'committer@example.com' + } + }; + repo.write('dev', 'TEST.md', 'THIS IS A TEST BY AUTHOR AND COMMITTER', 'Updating', options, + callbackWithError(done, function(err, res) { + should.not.exist(err); + repo.getCommit('dev', res.commit.sha, callbackWithError(done, function(err, commit) { + should.not.exist(err); + commit.author.name.should.equal('Author Name'); + commit.author.email.should.equal('author@example.com'); + commit.committer.name.should.equal('Committer Name'); + commit.committer.email.should.equal('committer@example.com'); + + done(); + })); + }) + ); + }); + + it('should be able to write CJK unicode to repo', function(done) { + repo.write('master', '中文测试.md', 'THIS IS A TEST', 'Creating test', callbackWithError(done, function(err) { + // @TODO write better assertion + should.not.exist(err); done(); - }); + })); }); - }); - it('should pass a regression test for _request (#14)', function(done) { - repo.getRef('heads/master', function(err, sha) { - var refSpec = { - ref: 'refs/heads/testing-14', sha: sha - }; + it('should be able to write unicode to repo', function(done) { + repo.write('master', 'TEST_unicode.md', '\u2014', 'Long dash unicode', callbackWithError(done, function(err) { + if (err) console.log(err); + should.not.exist(err); + repo.read('master', 'TEST_unicode.md', callbackWithError(done, function(err, obj) { + should.not.exist(err); + obj.should.equal('\u2014'); + done(); + })); + })); + }); - repo.createRef(refSpec, function(err) { + it('should pass a regression test for _request (#14)', function(done) { + repo.getRef('heads/master', callbackWithError(done, function(err, sha) { should.not.exist(err); + var refSpec = { + ref: 'refs/heads/testing-14', sha: sha + }; - // Triggers GET: - // https://api.github.com/repos/michael/cmake_cdt7_stalled/git/refs/heads/prose-integration - repo.getRef('heads/master', function(err) { + repo.createRef(refSpec, callbackWithError(done, function(err) { should.not.exist(err); - // Triggers DELETE: + // Triggers GET: // https://api.github.com/repos/michael/cmake_cdt7_stalled/git/refs/heads/prose-integration - repo.deleteRef('heads/testing-14', function(err, res, xhr) { + repo.getRef('heads/master', callbackWithError(done, function(err) { should.not.exist(err); - xhr.status.should.equal(204); - done(); - }); - }); - }); - }); - }); - - it('should be able to write an image to the repo', function(done) { - repo.write('master', 'TEST_image.png', imageB64, 'Image test', { - encode: false - }, function(err) { - if (err) console.log(err); - should.not.exist(err); - done(); - }); - }); - it('should be able to write a blob to the repo', function(done) { - repo.postBlob('String test', function(err) { // Test strings - should.not.exist(err); + // Triggers DELETE: + // https://api.github.com/repos/michael/cmake_cdt7_stalled/git/refs/heads/prose-integration + repo.deleteRef('heads/testing-14', callbackWithError(done, function(err, res, xhr) { + should.not.exist(err); + xhr.status.should.equal(204); + done(); + })); + })); + })); + })); + }); + + it('should be able to write an image to the repo', function(done) { + var options = { + encode: false + }; - repo.postBlob(imageBlob, function(err) { // Test non-strings + repo.write('master', 'TEST_image.png', imageB64, 'Image test', options, callbackWithError(done, function(err) { + if (err) console.log(err); should.not.exist(err); done(); - }); + })); }); - }); -}); -describe('deleting a Github.Repository', function() { - before(function() { - if (typeof window !== 'undefined') testUser = window.__fixtures__['test/user']; - github = new Github({ - username: testUser.USERNAME, - password: testUser.PASSWORD, - auth: 'basic' + it('should be able to write a blob to the repo', function(done) { + repo.postBlob('String test', callbackWithError(done, function(err) { // Test strings + should.not.exist(err); + repo.postBlob(imageBlob, callbackWithError(done, function(err) { // Test non-strings + should.not.exist(err); + done(err); + })); + })); }); - repo = github.getRepo(testUser.USERNAME, repoTest); }); - it('should delete the repo', function(done) { - repo.deleteRepo(function(err, res) { - should.not.exist(err); - res.should.be.true; // jshint ignore:line - done(); + describe('deleting a repository', function() { + before(function() { + repo = github.getRepo(testUser.USERNAME, repoTest); }); - }); -}); -describe('Repo returns commit errors correctly', function() { - before(function() { - if (typeof window !== 'undefined') testUser = window.__fixtures__['test/user']; - github = new Github({ - username: testUser.USERNAME, - password: testUser.PASSWORD, - auth: 'basic' + it('should delete the repo', function(done) { + repo.deleteRepo(callbackWithError(done, function(err, res) { + should.not.exist(err); + res.should.be.true; // jshint ignore:line + done(); + })); }); - repo = github.getRepo(testUser.USERNAME, testUser.REPO); }); - it('should fail on broken commit', function(done) { - repo.commit('broken-parent-hash', 'broken-tree-hash', 'commit message', function(err) { - should.exist(err); - should.exist(err.request); - err.request.status.should.equal(422); - done(); + describe('returns commit errors correctly', function() { + before(function() { + repo = github.getRepo(testUser.USERNAME, testUser.REPO); + }); + + it('should fail on broken commit', function(done) { + repo.commit('broken-parent-hash', 'broken-tree-hash', 'commit message', callbackWithError(done, function(err) { + should.exist(err); + should.exist(err.request); + err.status.should.equal(422); + done(); + })); }); }); }); diff --git a/test/test.search.js b/test/test.search.js index a084d126..5185842f 100644 --- a/test/test.search.js +++ b/test/test.search.js @@ -3,16 +3,18 @@ var github; if (typeof window === 'undefined') { - // Module dependencies - var chai = require('chai'); var Github = require('../'); var testUser = require('./user.json'); + var callbackWithError = require('./helpers.js'); + + // Module dependencies + var chai = require('chai'); // Use should flavour for Mocha var should = chai.should(); } -describe('Github.Search', function() { +describe('Search', function() { before(function() { if (typeof window !== 'undefined') testUser = window.__fixtures__['test/user']; github = new Github({ @@ -22,43 +24,43 @@ describe('Github.Search', function() { }); }); - it('should search.repositories', function(done) { + it('should search repositories', function(done) { var search = github.getSearch('tetris+language:assembly&sort=stars&order=desc'); var options = null; - search.repositories(options, function (err) { + search.repositories(options, callbackWithError(done, function (err) { should.not.exist(err); done(); - }); + })); }); - it('should search.code', function(done) { + it('should search code', function(done) { var search = github.getSearch('addClass+in:file+language:js+repo:jquery/jquery'); var options = null; - search.code(options, function (err) { + search.code(options, callbackWithError(done, function (err) { should.not.exist(err); done(); - }); + })); }); - it('should search.issues', function(done) { + it('should search issues', function(done) { var search = github.getSearch('windows+label:bug+language:python+state:open&sort=created&order=asc'); var options = null; - search.issues(options, function (err) { + search.issues(options, callbackWithError(done, function (err) { should.not.exist(err); done(); - }); + })); }); - it('should search.users', function(done) { + it('should search users', function(done) { var search = github.getSearch('tom+repos:%3E42+followers:%3E1000'); var options = null; - search.users(options, function (err) { + search.users(options, callbackWithError(done, function (err) { should.not.exist(err); done(); - }); + })); }); }); diff --git a/test/test.user.js b/test/test.user.js index c4d3d77e..11bea77a 100644 --- a/test/test.user.js +++ b/test/test.user.js @@ -1,11 +1,12 @@ 'use strict'; -var testUser, user, github; +var testUser; if (typeof window === 'undefined') { // Module dependencies var chai = require('chai'); var Github = require('../'); + var callbackWithError = require('./helpers.js'); testUser = require('./user.json'); @@ -13,7 +14,9 @@ if (typeof window === 'undefined') { var should = chai.should(); } -describe('Github.User', function() { +describe('User', function() { + var user, github; + before(function() { if (typeof window !== 'undefined') testUser = window.__fixtures__['test/user']; github = new Github({ @@ -24,14 +27,14 @@ describe('Github.User', function() { user = github.getUser(); }); - it('should get user.repos', function(done) { - user.repos(function(err) { + it('should get user repos', function(done) { + user.repos(callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); - it('should get user.repos with options', function(done) { + it('should get user repos with options', function(done) { var options = { type: 'owner', sort: 'updated', @@ -39,36 +42,36 @@ describe('Github.User', function() { page: 1 }; - user.repos(options, function(err, repos) { + user.repos(options, callbackWithError(done, function(err, repos) { repos.should.have.length(10); should.not.exist(err); done(); - }); + })); }); - it('should get user.orgs', function(done) { - user.orgs(function(err) { + it('should get user orgs', function(done) { + user.orgs(callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); - it('should get user.gists', function(done) { + it('should get user gists', function(done) { user.gists(function(err) { should.not.exist(err); done(); }); }); - it('should get user.notifications', function(done) { - user.notifications(function(err) { + it('should get user notifications', function(done) { + user.notifications(callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); - it('should get user.notifications with options', function(done) { + it('should get user notifications with options', function(done) { var options = { all: true, participating: true, @@ -76,77 +79,72 @@ describe('Github.User', function() { before: '2015-02-01T00:00:00Z' }; - user.notifications(options, function(err) { + user.notifications(options, callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); it('should show user', function(done) { - user.show('ingalls', function(err) { + user.show('ingalls', callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); it('should show user\'s repos', function(done) { // This is odd; userRepos times out on the test user, but user.repos does not. - user.userRepos('aendrew', function(err) { + user.userRepos('aendrew', callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); it('should show user\'s starred repos', function(done) { - user.userStarred(testUser.USERNAME, function(err) { + user.userStarred(testUser.USERNAME, callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); it('should show user\'s gists', function(done) { - user.userGists(testUser.USERNAME, function(err) { + user.userGists(testUser.USERNAME, callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); it('should show user\'s organisation repos', function(done) { - user.orgRepos('openaddresses', function(err) { + user.orgRepos('openaddresses', callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); it('should follow user', function(done) { - user.follow('ingalls', function(err) { + user.follow('ingalls', callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); it('should unfollow user', function(done) { - user.unfollow('ingalls', function(err) { + user.unfollow('ingalls', callbackWithError(done, function(err) { should.not.exist(err); done(); - }); + })); }); it('should create a repo', function(done) { var repoTest = Math.floor(Math.random() * (100000 - 0)) + 0; - var github = new Github({ - username: testUser.USERNAME, - password: testUser.PASSWORD, - auth: 'basic' - }); - var user = github.getUser(); - - user.createRepo({ + var testRepo = { name: repoTest - }, function (err, res) { + }; + + user.createRepo(testRepo, callbackWithError(done, function (err, res) { should.not.exist(err); res.name.should.equal(repoTest.toString()); done(); - }); + })); }); });