diff --git a/.editorconfig b/.editorconfig index a5a6e6eb..ec408cf1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,7 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +max_line_length = 120 [{*.yml,package.json}] indent_size = 2 diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 5a061f61..00000000 --- a/.eslintrc +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "eslint:recommended", - - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - - "rules": { - "no-bitwise": 1, - "eqeqeq": 2, - "guard-for-in": 2, - "no-extend-native": 2 - }, - - "env": { - "browser": true, - "node": true - }, - - "globals": { - "require": false, - "define": false, - "escape": false - } -} diff --git a/.eslintrc.yaml b/.eslintrc.yaml new file mode 100644 index 00000000..2718e64e --- /dev/null +++ b/.eslintrc.yaml @@ -0,0 +1,36 @@ +--- + extends: + - eslint:recommended + - google + parserOptions: + ecmaVersion: 6 + sourceType: module + rules: + arrow-parens: [error, always] + eqeqeq: error + guard-for-in: error + indent: [error, 3, {SwitchCase: 1}] + max-len: [error, {code: 120, ignoreTrailingComments: true}] + no-bitwise: warn + no-extend-native: error + no-useless-constructor: off + no-var: error + padded-blocks: off + quotes: [error, single, {avoidEscape: true}] + require-jsdoc: + - error + - require: + FunctionDeclaration: false + ClassDeclaration: true + MethodDefinition: true + spaced-comment: error + valid-jsdoc: [error, {requireParamDescription: true}] + + env: + es6: true + node: true + browser: true + # globals: + # require: false + # define: false + # escape: false diff --git a/.gitignore b/.gitignore index 6d462a19..db3cda2f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,8 @@ docs/ dist/ coverage/ node_modules/ - +.nyc_output/ +/out/ .DS_Store npm-debug.log sauce.json diff --git a/.jscsrc b/.jscsrc deleted file mode 100644 index 0a7963d2..00000000 --- a/.jscsrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "preset": "google", - - "disallowVar": true, - "jsDoc": { - "checkParamExistence": true, - "checkParamNames": true, - "checkTypes": true, - "requireParamTypes": true, - "requireHyphenBeforeDescription": true, - }, - "maximumLineLength": 120, - "requireSpaceAfterLineComment": true, - "safeContextKeyword": ["that"], - "validateIndentation": 3, -} diff --git a/.npmignore b/.npmignore index 15fff57d..fe969272 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,7 @@ docs/ coverage/ node_modules/ - +lib/ +.nyc_output/ .DS_Store sauce.json diff --git a/.travis.yml b/.travis.yml index 3ee3bdd3..6b6bad91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,21 @@ +sudo: false language: node_js node_js: - - '5' - - '4' - - '0.12' - - '0.10' +- '11' +- '10' +- '8' cache: directories: - - node_modules + - node_modules before_install: npm install -g npm@latest before_script: - - npm run lint - # - npm run build # will need this when we do sauce testing of compiled files +- npm run lint script: - - npm test - # - npm run test-dist # test the compiled files -# after_success: -# - npm run codecov # disabled temporarialy while I work out how to generate accurate coverage of ES2015 code +- npm run test-coverage +after_success: +- npm run codecov before_deploy: - - npm run build +- npm run build deploy: provider: npm skip_cleanup: true @@ -25,4 +23,4 @@ deploy: tags: true email: clayreimann@gmail.com api_key: - secure: TZHqJ9Kh2Qf0GAVDjEOQ01Ez6rGMYHKwVLOKTbnb7nSzF7iiGNT4UwzvYawm0T9p1k7X1WOqW3l7OEbIwoKl7/9azT4BBJm7qUMRfB9Zio5cL3rKubJVz7+LEEIW4iBeDWLanhUDgy9BO2JKCt8bfp/U2tltgXtu9Fm/UFPALI8= + secure: WnLh1m02aF7NvFNILCZ8KsjPuDeSddQI87y8dwAixStr2FhQyz8FIKZN2Qj1N1Q9ZJvBETe5HWs1c9yOjTKBkD0d/eU2hlpnB9WXEFRJVDjiUuMnpAMMvuqTZwYg6kXq5N+of95PX58AYiBiV/qwsdUr/MgjEEYLt5UZgRYQRvE= diff --git a/CHANGELOG.md b/CHANGELOG.md index 5650c231..035944d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,68 @@ # Change Log -## master +## 2.4.0 - 2016/09/16 ### Features -Added Milestone API -* `Issue.listMilestones` -* `Issue.getMilestone` -* `Issue.editMilestone` -* `Issue.deleteMilestone` +* add `Issue.createLabel` +* add `Repository.createKey` +* add `Repository.deleteKey` +* add `Repository.getBranch` +* add `Repository.listKeys` +* add `Repository.getKey` +* add `Repository.updatePullRequest` +* deprecate `Repository.updatePullRequst` ### Fixes +* Request URL for deleting a hook (`Repository.deleteHook`) + +## 2.3.0 - 2016/06/17 +### Features +* add `Repository.mergePullRequest` +* add `Repository.updatePullRequest` +* add `Repository.listPullRequestFiles` +* add `Repository.getReadme` + +## 2.2.0 - 2016/05/27 +### Features +* add `Issue.listIssueEvents` + +### Fixes +* Search returns results again + +## 2.1.0 - 2016/05/26 +### Features +Team API +* `Organization.createTeam` +* `Organization.getTeams` +* `Team.getTeam` +* `Team.listRepos` +* `Team.editTeam` +* `Team.listMembers` +* `Team.getMembership` +* `Team.addMembership` +* `Team.isManagedRepo` +* `Team.manageRepo` +* `Team.unmanageRepo` +* `Team.deleteTeam` + +## 2.0.0 +### Breaking +* `Repository#move` has a new argument list +User +* `getRepos` → `listRepos` +* `getOrgs` → `listOrgs` +* `getGists` → `listGists` +* `getNotifications` → `listNotifications` +* `getStarredRepos` → `listStarredRepos` + +### Fixes +* `Repository`: `move` now works +* `User`: `listRepos` ## 1.2.1 ### Fixes * `Repository`: Replace invalid references to `postTree` with `createTree` -## 1.2.0 - 2015/05/11 +## 1.2.0 - 2016/05/11 ### Features * Search API now returns all pages of results * Added `Repository.listReleases` @@ -32,7 +80,7 @@ Added functions for issue comments ### Fixes * all functions now return a Promise -## 1.1.0 - 2015/05/03 +## 1.1.0 - 2016/05/03 ### Features Added methods for commenting on Gists: * `Gist.listComments` @@ -44,7 +92,7 @@ Added methods for commenting on Gists: ### Fixes * `Repository.deleteFile` now correctly returns a promise. -## 1.0.0 - 2015/04/27 +## 1.0.0 - 2016/04/27 Complete rewrite in ES2015. * Promise-ified the API diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..09827eb1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/README.md b/README.md index 98716949..5e5f527f 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,44 @@ +# Maintainers wanted +[Apply within](https://github.com/github-tools/github/issues/539) + # Github.js [![Downloads per month](https://img.shields.io/npm/dm/github-api.svg?maxAge=2592000)][npm-package] [![Latest version](https://img.shields.io/npm/v/github-api.svg?maxAge=3600)][npm-package] -[![Gitter](https://img.shields.io/gitter/room/michael/github.js.svg?maxAge=2592000)][gitter] -[![Travis](https://img.shields.io/travis/michael/github.svg?maxAge=60)][travis-ci] - - -Github.js provides a minimal higher-level wrapper around Github's API. It was concieved in the context of -[Prose][prose], a content editor for GitHub. +[![Gitter](https://img.shields.io/gitter/room/github-tools/github.js.svg?maxAge=2592000)][gitter] +[![Travis](https://img.shields.io/travis/github-tools/github.svg?maxAge=60)][travis-ci] +[![Codecov](https://img.shields.io/codecov/c/github/github-tools/github.svg?maxAge=2592000)][codecov] -## [Read the docs][docs] - -## Installation -Github.js is available from `npm` or [npmcdn][npmcdn]. +`Github.js` provides a minimal higher-level wrapper around Github's API. -```shell -npm install github-api -``` - -```html - - - - - -``` - -## Compatibility -Github.js is tested on Node: -* 5.x -* 4.x -* 0.12 -* 0.10 - -## GitHub Tools - -The team behind Github.js has created a whole organization, called [GitHub Tools](https://github.com/github-tools), -dedicated to GitHub and its API. In the near future this repository could be moved under the GitHub Tools organization -as well. In the meantime, we recommend you to take a look at other projects of the organization. - -## Samples +## Usage ```javascript /* Data can be retrieved from the API either using callbacks (as in versions < 1.0) - or using a new promise-based API. For now the promise-based API just returns the - raw HTTP request promise; this might change in the next version. + or using a new promise-based API. The promise-based API returns the raw Axios + request promise. */ -var GitHub = require('github-api'); +import GitHub from 'github-api'; // unauthenticated client -var gh = new GitHub(); -var gist = gh.getGist(); // not a gist yet +const gh = new GitHub(); +let gist = gh.getGist(); // not a gist yet gist.create({ public: true, description: 'My first gist', files: { "file1.txt": { - contents: "Aren't gists great!" + content: "Aren't gists great!" } } -}).then(function(httpResponse) { +}).then(function({data}) { // Promises! - var gist = httpResponse.data; - gist.read(function(err, gist, xhr) { - // if no error occurred then err == null - - // gist == httpResponse.data - - // xhr == httpResponse - }); + let createdGist = data; + return gist.read(); +}).then(function({data}) { + let retrievedGist = data; + // do interesting things }); ``` @@ -80,39 +49,78 @@ var GitHub = require('github-api'); var gh = new GitHub({ username: 'FOO', password: 'NotFoo' + /* also acceptable: + token: 'MY_OAUTH_TOKEN' + */ }); -var me = gh.getUser(); -me.getNotification(function(err, notifcations) { +var me = gh.getUser(); // no user specified defaults to the user for whom credentials were provided +me.listNotifications(function(err, notifications) { // do some stuff }); var clayreimann = gh.getUser('clayreimann'); -clayreimann.getStarredRepos() - .then(function(httpPromise) { - var repos = httpPromise.data; - }); +clayreimann.listStarredRepos(function(err, repos) { + // look at all the starred repos! +}); ``` -```javascript -var GitHub = require('github-api'); +## API Documentation -// token auth -var gh = new GitHub({ - token: 'MY_OAUTH_TOKEN' -}); +[API documentation][docs] is hosted on github pages, and is generated from JSDoc; any contributions +should include updated JSDoc. -var yahoo = gh.getOrganization('yahoo'); -yahoo.getRepos(function(err, repos) { - // look at all the repos! -}) +## Installation +`Github.js` is available from `npm` or [unpkg][unpkg]. + +```shell +npm install github-api +``` + +```html + + + + + ``` -[codecov]: https://codecov.io/github/michael/github?branch=master -[docs]: http://michael.github.io/github/ -[gitter]: https://gitter.im/michael/github +## Compatibility +`Github.js` is tested on node's LTS and current versions. + +[codecov]: https://codecov.io/github/github-tools/github?branch=master +[docs]: http://github-tools.github.io/github/ +[gitter]: https://gitter.im/github-tools/github [npm-package]: https://www.npmjs.com/package/github-api/ -[npmcdn]: https://npmcdn.com/github-api/ -[prose]: http://prose.io -[travis-ci]: https://travis-ci.org/michael/github -[xhr-link]: http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx +[unpkg]: https://unpkg.com/github-api/ +[travis-ci]: https://travis-ci.org/github-tools/github + +## Contributing + +We welcome contributions of all types! This section will guide you through setting up your development environment. + +### Setup + +1. [Install Node](https://nodejs.org/en/) version 8,10 or 11. It can often help to use a Node version switcher such as [NVM](https://github.com/nvm-sh/nvm). +2. Fork this repo to your GitHub account. +3. Clone the fork to your development machine (`git clone https://github.com/{YOUR_USERNAME}/github`). +4. From the root of the cloned repo, run `npm install`. +5. Email jaredrewerts@gmail.com with the subject **GitHub API - Personal Access Token Request** + +A personal access token for our test user, @github-tools-test, will be generated for you. + +6. Set the environment variable `GHTOOLS_USER` to `github-tools-test`. + +`export GHTOOLS_USER=github-tools-test` + +7. Set the environment variable `GHTOOLS_PASSWORD` to the personal access token that was generated for you. + +`export GHTOOLS_PASSWORD={YOUR_PAT}` + +**NOTE** Windows users can use [this guide](http://www.dowdandassociates.com/blog/content/howto-set-an-environment-variable-in-windows-command-line-and-registry/) to learn about setting environment variables on Windows. + +### Tests + +The main way we write code for `github-api` is using test-driven development. We use Mocha to run our tests. Given that the bulk of this library is just interacting with GitHub's API, nearly all of our tests are integration tests. + +To run the test suite, run `npm run test`. diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 9055f7b3..9965f7aa 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -1,35 +1,26 @@ import gulp from 'gulp'; -import jscs from 'gulp-jscs'; import eslint from 'gulp-eslint'; -import stylish from 'gulp-jscs-stylish'; - import babel from 'gulp-babel'; import rename from 'gulp-rename'; import browserify from 'browserify'; import buffer from 'vinyl-buffer'; import del from 'del'; -import path from 'path'; -import {Promise} from 'es6-promise'; import source from 'vinyl-source-stream'; import sourcemaps from 'gulp-sourcemaps'; import uglify from 'gulp-uglify'; const ALL_SOURCES = [ - path.join(__dirname, '/*.js'), - path.join(__dirname, '/src/*.js'), - path.join(__dirname, '/test/*.js') + '*.js', + 'lib/*.js', + 'test/*.js', ]; gulp.task('lint', function() { - const opts = { - base: './' - }; - return gulp.src(ALL_SOURCES, opts) + return gulp.src(ALL_SOURCES) .pipe(eslint()) - .pipe(jscs()) - .pipe(stylish.combineWithHintResults()) - .pipe(stylish()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()) ; }); @@ -42,13 +33,13 @@ gulp.task('build', [ 'build:external:min', 'build:bundled:debug', 'build:external:debug', - 'build:components' + 'build:components', ]); const bundledConfig = { debug: true, entries: 'lib/GitHub.js', - standalone: 'GitHub' + standalone: 'GitHub', }; const externalConfig = { debug: true, @@ -59,9 +50,9 @@ const externalConfig = { 'js-base64', 'es6-promise', 'debug', - 'utf8' + 'utf8', ], - bundleExternal: false + bundleExternal: false, }; gulp.task('build:bundled:min', function() { return buildBundle(bundledConfig, '.bundle.min.js', true); @@ -91,7 +82,7 @@ function buildBundle(options, extname, minify) { .pipe(source('GitHub.js')) .pipe(buffer()) .pipe(sourcemaps.init({ - loadMaps: true + loadMaps: true, })); if (minify) { diff --git a/lib/Gist.js b/lib/Gist.js index 14b71322..60f18cf5 100644 --- a/lib/Gist.js +++ b/lib/Gist.js @@ -108,6 +108,27 @@ class Gist extends Requestable { return this._request204or404(`/gists/${this.__id}/star`, null, cb); } + /** + * List the gist's commits + * @see https://developer.github.com/v3/gists/#list-gist-commits + * @param {Requestable.callback} [cb] - will receive the array of commits + * @return {Promise} - the Promise for the http request + */ + listCommits(cb) { + return this._requestAllPages(`/gists/${this.__id}/commits`, null, cb); + } + + /** + * Fetch one of the gist's revision. + * @see https://developer.github.com/v3/gists/#get-a-specific-revision-of-a-gist + * @param {string} revision - the id of the revision + * @param {Requestable.callback} [cb] - will receive the revision + * @return {Promise} - the Promise for the http request + */ + getRevision(revision, cb) { + return this._request('GET', `/gists/${this.__id}/${revision}`, null, cb); + } + /** * List the gist's comments * @see https://developer.github.com/v3/gists/comments/#list-comments-on-a-gist diff --git a/lib/GitHub.js b/lib/GitHub.js index 36829b0c..7c3ac24c 100644 --- a/lib/GitHub.js +++ b/lib/GitHub.js @@ -4,6 +4,7 @@ * @license Licensed under {@link https://spdx.org/licenses/BSD-3-Clause-Clear.html BSD-3-Clause-Clear}. * Github.js is freely distributable. */ +/* eslint valid-jsdoc: ["error", {"requireReturnDescription": false}] */ import Gist from './Gist'; import User from './User'; @@ -12,7 +13,9 @@ import Search from './Search'; import RateLimit from './RateLimit'; import Repository from './Repository'; import Organization from './Organization'; +import Team from './Team'; import Markdown from './Markdown'; +import Project from './Project'; /** * GitHub encapsulates the functionality to create various API wrapper objects. @@ -31,7 +34,7 @@ class GitHub { /** * Create a new Gist wrapper - * @param {number} [id] - the id for the gist, leave undefined when creating a new gist + * @param {string} [id] - the id for the gist, leave undefined when creating a new gist * @return {Gist} */ getGist(id) { @@ -57,9 +60,18 @@ class GitHub { return new Organization(organization, this.__auth, this.__apiBase); } + /** + * create a new Team wrapper + * @param {string} teamId - the name of the team + * @return {team} + */ + getTeam(teamId) { + return new Team(teamId, this.__auth, this.__apiBase); + } + /** * Create a new Repository wrapper - * @param {string} user - the user who owns the respository + * @param {string} user - the user who owns the repository * @param {string} repo - the name of the repository * @return {Repository} */ @@ -69,7 +81,7 @@ class GitHub { /** * Create a new Issue wrapper - * @param {string} user - the user who owns the respository + * @param {string} user - the user who owns the repository * @param {string} repo - the name of the repository * @return {Issue} */ @@ -99,9 +111,24 @@ class GitHub { * @return {Markdown} */ getMarkdown() { - return new Markdown(this.__auth, this.__apiBase); + return new Markdown(this.__auth, this.__apiBase); + } + + /** + * Create a new Project wrapper + * @param {string} id - the id of the project + * @return {Project} + */ + getProject(id) { + return new Project(id, this.__auth, this.__apiBase); } + /** + * Computes the full repository name + * @param {string} user - the username (or the full name) + * @param {string} repo - the repository name, must not be passed if `user` is the full name + * @return {string} the repository's full name + */ _getFullName(user, repo) { let fullname = user; diff --git a/lib/Issue.js b/lib/Issue.js index d1e35c89..c0151b5f 100644 --- a/lib/Issue.js +++ b/lib/Issue.js @@ -44,6 +44,17 @@ class Issue extends Requestable { return this._requestAllPages(`/repos/${this.__repository}/issues`, options, cb); } + /** + * List the events for an issue + * @see https://developer.github.com/v3/issues/events/#list-events-for-an-issue + * @param {number} issue - the issue to get events for + * @param {Requestable.callback} [cb] - will receive the list of events + * @return {Promise} - the promise for the http request + */ + listIssueEvents(issue, cb) { + return this._request('GET', `/repos/${this.__repository}/issues/${issue}/events`, null, cb); + } + /** * List comments on an issue * @see https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue @@ -52,7 +63,7 @@ class Issue extends Requestable { * @return {Promise} - the promise for the http request */ listIssueComments(issue, cb) { - return this._request('GET', `/repos/${this.__repository}/issues/${issue}/comments`, null, cb); // jscs:ignore + return this._request('GET', `/repos/${this.__repository}/issues/${issue}/comments`, null, cb); } /** @@ -63,7 +74,7 @@ class Issue extends Requestable { * @return {Promise} - the promise for the http request */ getIssueComment(id, cb) { - return this._request('GET', `/repos/${this.__repository}/issues/comments/${id}`, null, cb); // jscs:ignore + return this._request('GET', `/repos/${this.__repository}/issues/comments/${id}`, null, cb); } /** @@ -75,7 +86,7 @@ class Issue extends Requestable { * @return {Promise} - the promise for the http request */ createIssueComment(issue, comment, cb) { - return this._request('POST', `/repos/${this.__repository}/issues/${issue}/comments`, {body: comment}, cb); // jscs:ignore + return this._request('POST', `/repos/${this.__repository}/issues/${issue}/comments`, {body: comment}, cb); } /** @@ -87,7 +98,7 @@ class Issue extends Requestable { * @return {Promise} - the promise for the http request */ editIssueComment(id, comment, cb) { - return this._request('PATCH', `/repos/${this.__repository}/issues/comments/${id}`, {body: comment}, cb); // jscs:ignore + return this._request('PATCH', `/repos/${this.__repository}/issues/comments/${id}`, {body: comment}, cb); } /** @@ -98,7 +109,7 @@ class Issue extends Requestable { * @return {Promise} - the promise for the http request */ deleteIssueComment(id, cb) { - return this._request('DELETE', `/repos/${this.__repository}/issues/comments/${id}`, null, cb); // jscs:ignore + return this._request('DELETE', `/repos/${this.__repository}/issues/comments/${id}`, null, cb); } /** @@ -139,7 +150,7 @@ class Issue extends Requestable { * Get a milestone * @see https://developer.github.com/v3/issues/milestones/#get-a-single-milestone * @param {string} milestone - the id of the milestone to fetch - * @param {Requestable.callback} [cb] - will receive the array of milestones + * @param {Requestable.callback} [cb] - will receive the milestone * @return {Promise} - the promise for the http request */ getMilestone(milestone, cb) { @@ -150,7 +161,7 @@ class Issue extends Requestable { * Create a new milestone * @see https://developer.github.com/v3/issues/milestones/#create-a-milestone * @param {Object} milestoneData - the milestone definition - * @param {Requestable.callback} [cb] - will receive the array of milestones + * @param {Requestable.callback} [cb] - will receive the milestone * @return {Promise} - the promise for the http request */ createMilestone(milestoneData, cb) { @@ -162,7 +173,7 @@ class Issue extends Requestable { * @see https://developer.github.com/v3/issues/milestones/#update-a-milestone * @param {string} milestone - the id of the milestone to edit * @param {Object} milestoneData - the updates to make to the milestone - * @param {Requestable.callback} [cb] - will receive the array of milestones + * @param {Requestable.callback} [cb] - will receive the updated milestone * @return {Promise} - the promise for the http request */ editMilestone(milestone, milestoneData, cb) { @@ -173,12 +184,68 @@ class Issue extends Requestable { * Delete a milestone (this is distinct from closing a milestone) * @see https://developer.github.com/v3/issues/milestones/#delete-a-milestone * @param {string} milestone - the id of the milestone to delete - * @param {Requestable.callback} [cb] - will receive the array of milestones + * @param {Requestable.callback} [cb] - will receive the status * @return {Promise} - the promise for the http request */ deleteMilestone(milestone, cb) { return this._request('DELETE', `/repos/${this.__repository}/milestones/${milestone}`, null, cb); } + + /** + * Create a new label + * @see https://developer.github.com/v3/issues/labels/#create-a-label + * @param {Object} labelData - the label definition + * @param {Requestable.callback} [cb] - will receive the object representing the label + * @return {Promise} - the promise for the http request + */ + createLabel(labelData, cb) { + return this._request('POST', `/repos/${this.__repository}/labels`, labelData, cb); + } + + /** + * List the labels for the repository + * @see https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository + * @param {Object} options - filtering options + * @param {Requestable.callback} [cb] - will receive the array of labels + * @return {Promise} - the promise for the http request + */ + listLabels(options, cb) { + return this._request('GET', `/repos/${this.__repository}/labels`, options, cb); + } + + /** + * Get a label + * @see https://developer.github.com/v3/issues/labels/#get-a-single-label + * @param {string} label - the name of the label to fetch + * @param {Requestable.callback} [cb] - will receive the label + * @return {Promise} - the promise for the http request + */ + getLabel(label, cb) { + return this._request('GET', `/repos/${this.__repository}/labels/${label}`, null, cb); + } + + /** + * Edit a label + * @see https://developer.github.com/v3/issues/labels/#update-a-label + * @param {string} label - the name of the label to edit + * @param {Object} labelData - the updates to make to the label + * @param {Requestable.callback} [cb] - will receive the updated label + * @return {Promise} - the promise for the http request + */ + editLabel(label, labelData, cb) { + return this._request('PATCH', `/repos/${this.__repository}/labels/${label}`, labelData, cb); + } + + /** + * Delete a label + * @see https://developer.github.com/v3/issues/labels/#delete-a-label + * @param {string} label - the name of the label to delete + * @param {Requestable.callback} [cb] - will receive the status + * @return {Promise} - the promise for the http request + */ + deleteLabel(label, cb) { + return this._request('DELETE', `/repos/${this.__repository}/labels/${label}`, null, cb); + } } module.exports = Issue; diff --git a/lib/Markdown.js b/lib/Markdown.js index e2778169..ebfbb512 100644 --- a/lib/Markdown.js +++ b/lib/Markdown.js @@ -8,11 +8,11 @@ import Requestable from './Requestable'; /** - * RateLimit allows users to query their rate-limit status + * Renders html from Markdown text */ class Markdown extends Requestable { /** - * construct a RateLimit + * construct a Markdown * @param {Requestable.auth} auth - the credentials to authenticate to GitHub * @param {string} [apiBase] - the base Github API URL * @return {Promise} - the promise for the http request @@ -24,7 +24,7 @@ class Markdown extends Requestable { /** * Render html from Markdown text. * @see https://developer.github.com/v3/markdown/#render-an-arbitrary-markdown-document - * @param {Object} options + * @param {Object} options - conversion options * @param {string} [options.text] - the markdown text to convert * @param {string} [options.mode=markdown] - can be either `markdown` or `gfm` * @param {string} [options.context] - repository name if mode is gfm @@ -32,7 +32,7 @@ class Markdown extends Requestable { * @return {Promise} - the promise for the http request */ render(options, cb) { - return this._request('POST', '/markdown', options, cb); + return this._request('POST', '/markdown', options, cb, true); } } diff --git a/lib/Organization.js b/lib/Organization.js index 1e85dc97..0a8177b4 100644 --- a/lib/Organization.js +++ b/lib/Organization.js @@ -19,7 +19,7 @@ class Organization extends Requestable { */ constructor(organization, auth, apiBase) { super(auth, apiBase); - this.__name = organization; + this.__name = organization; } /** @@ -58,7 +58,7 @@ class Organization extends Requestable { /** * List the users who are members of the company * @see https://developer.github.com/v3/orgs/members/#members-list - * @param {object} options + * @param {object} options - filtering options * @param {string} [options.filter=all] - can be either `2fa_disabled` or `all` * @param {string} [options.role=all] - can be one of: `all`, `admin`, or `member` * @param {Requestable.callback} [cb] - will receive the list of users @@ -67,6 +67,55 @@ class Organization extends Requestable { listMembers(options, cb) { return this._request('GET', `/orgs/${this.__name}/members`, options, cb); } + + /** + * List the Teams in the Organization + * @see https://developer.github.com/v3/orgs/teams/#list-teams + * @param {Requestable.callback} [cb] - will receive the list of teams + * @return {Promise} - the promise for the http request + */ + getTeams(cb) { + return this._requestAllPages(`/orgs/${this.__name}/teams`, undefined, cb); + } + + /** + * Create a team + * @see https://developer.github.com/v3/orgs/teams/#create-team + * @param {object} options - Team creation parameters + * @param {string} options.name - The name of the team + * @param {string} [options.description] - Team description + * @param {string} [options.repo_names] - Repos to add the team to + * @param {string} [options.privacy=secret] - The level of privacy the team should have. Can be either one + * of: `secret`, or `closed` + * @param {Requestable.callback} [cb] - will receive the created team + * @return {Promise} - the promise for the http request + */ + createTeam(options, cb) { + return this._request('POST', `/orgs/${this.__name}/teams`, options, cb); + } + + /** + * Get information about all projects + * @see https://developer.github.com/v3/projects/#list-organization-projects + * @param {Requestable.callback} [cb] - will receive the list of projects + * @return {Promise} - the promise for the http request + */ + listProjects(cb) { + return this._requestAllPages(`/orgs/${this.__name}/projects`, {AcceptHeader: 'inertia-preview'}, cb); + } + + /** + * Create a new project + * @see https://developer.github.com/v3/repos/projects/#create-a-project + * @param {Object} options - the description of the project + * @param {Requestable.callback} cb - will receive the newly created project + * @return {Promise} - the promise for the http request + */ + createProject(options, cb) { + options = options || {}; + options.AcceptHeader = 'inertia-preview'; + return this._request('POST', `/orgs/${this.__name}/projects`, options, cb); + } } module.exports = Organization; diff --git a/lib/Project.js b/lib/Project.js new file mode 100644 index 00000000..ab31a078 --- /dev/null +++ b/lib/Project.js @@ -0,0 +1,236 @@ +/** + * @file + * @copyright 2013 Michael Aufreiter (Development Seed) and 2016 Yahoo Inc. + * @license Licensed under {@link https://spdx.org/licenses/BSD-3-Clause-Clear.html BSD-3-Clause-Clear}. + * Github.js is freely distributable. + */ + +import Requestable from './Requestable'; + +/** + * Project encapsulates the functionality to create, query, and modify cards and columns. + */ +class Project extends Requestable { + /** + * Create a Project. + * @param {string} id - the id of the project + * @param {Requestable.auth} [auth] - information required to authenticate to Github + * @param {string} [apiBase=https://api.github.com] - the base Github API URL + */ + constructor(id, auth, apiBase) { + super(auth, apiBase, 'inertia-preview'); + this.__id = id; + } + + /** + * Get information about a project + * @see https://developer.github.com/v3/projects/#get-a-project + * @param {Requestable.callback} cb - will receive the project information + * @return {Promise} - the promise for the http request + */ + getProject(cb) { + return this._request('GET', `/projects/${this.__id}`, null, cb); + } + + /** + * Edit a project + * @see https://developer.github.com/v3/projects/#update-a-project + * @param {Object} options - the description of the project + * @param {Requestable.callback} cb - will receive the modified project + * @return {Promise} - the promise for the http request + */ + updateProject(options, cb) { + return this._request('PATCH', `/projects/${this.__id}`, options, cb); + } + + /** + * Delete a project + * @see https://developer.github.com/v3/projects/#delete-a-project + * @param {Requestable.callback} cb - will receive true if the operation is successful + * @return {Promise} - the promise for the http request + */ + deleteProject(cb) { + return this._request('DELETE', `/projects/${this.__id}`, null, cb); + } + + /** + * Get information about all columns of a project + * @see https://developer.github.com/v3/projects/columns/#list-project-columns + * @param {Requestable.callback} [cb] - will receive the list of columns + * @return {Promise} - the promise for the http request + */ + listProjectColumns(cb) { + return this._requestAllPages(`/projects/${this.__id}/columns`, null, cb); + } + + /** + * Get information about a column + * @see https://developer.github.com/v3/projects/columns/#get-a-project-column + * @param {string} colId - the id of the column + * @param {Requestable.callback} cb - will receive the column information + * @return {Promise} - the promise for the http request + */ + getProjectColumn(colId, cb) { + return this._request('GET', `/projects/columns/${colId}`, null, cb); + } + + /** + * Create a new column + * @see https://developer.github.com/v3/projects/columns/#create-a-project-column + * @param {Object} options - the description of the column + * @param {Requestable.callback} cb - will receive the newly created column + * @return {Promise} - the promise for the http request + */ + createProjectColumn(options, cb) { + return this._request('POST', `/projects/${this.__id}/columns`, options, cb); + } + + /** + * Edit a column + * @see https://developer.github.com/v3/projects/columns/#update-a-project-column + * @param {string} colId - the column id + * @param {Object} options - the description of the column + * @param {Requestable.callback} cb - will receive the modified column + * @return {Promise} - the promise for the http request + */ + updateProjectColumn(colId, options, cb) { + return this._request('PATCH', `/projects/columns/${colId}`, options, cb); + } + + /** + * Delete a column + * @see https://developer.github.com/v3/projects/columns/#delete-a-project-column + * @param {string} colId - the column to be deleted + * @param {Requestable.callback} cb - will receive true if the operation is successful + * @return {Promise} - the promise for the http request + */ + deleteProjectColumn(colId, cb) { + return this._request('DELETE', `/projects/columns/${colId}`, null, cb); + } + + /** + * Move a column + * @see https://developer.github.com/v3/projects/columns/#move-a-project-column + * @param {string} colId - the column to be moved + * @param {string} position - can be one of first, last, or after:, + * where is the id value of a column in the same project. + * @param {Requestable.callback} cb - will receive true if the operation is successful + * @return {Promise} - the promise for the http request + */ + moveProjectColumn(colId, position, cb) { + return this._request( + 'POST', + `/projects/columns/${colId}/moves`, + {position: position}, + cb + ); + } + + /** + * Get information about all cards of a project + * @see https://developer.github.com/v3/projects/cards/#list-project-cards + * @param {Requestable.callback} [cb] - will receive the list of cards + * @return {Promise} - the promise for the http request + */ + listProjectCards(cb) { + return this.listProjectColumns() + .then(({data}) => { + return Promise.all(data.map((column) => { + return this._requestAllPages(`/projects/columns/${column.id}/cards`, null); + })); + }).then((cardsInColumns) => { + const cards = cardsInColumns.reduce((prev, {data}) => { + prev.push(...data); + return prev; + }, []); + if (cb) { + cb(null, cards); + } + return cards; + }).catch((err) => { + if (cb) { + cb(err); + return; + } + throw err; + }); + } + + /** + * Get information about all cards of a column + * @see https://developer.github.com/v3/projects/cards/#list-project-cards + * @param {string} colId - the id of the column + * @param {Requestable.callback} [cb] - will receive the list of cards + * @return {Promise} - the promise for the http request + */ + listColumnCards(colId, cb) { + return this._requestAllPages(`/projects/columns/${colId}/cards`, null, cb); + } + + /** + * Get information about a card + * @see https://developer.github.com/v3/projects/cards/#get-a-project-card + * @param {string} cardId - the id of the card + * @param {Requestable.callback} cb - will receive the card information + * @return {Promise} - the promise for the http request + */ + getProjectCard(cardId, cb) { + return this._request('GET', `/projects/columns/cards/${cardId}`, null, cb); + } + + /** + * Create a new card + * @see https://developer.github.com/v3/projects/cards/#create-a-project-card + * @param {string} colId - the column id + * @param {Object} options - the description of the card + * @param {Requestable.callback} cb - will receive the newly created card + * @return {Promise} - the promise for the http request + */ + createProjectCard(colId, options, cb) { + return this._request('POST', `/projects/columns/${colId}/cards`, options, cb); + } + + /** + * Edit a card + * @see https://developer.github.com/v3/projects/cards/#update-a-project-card + * @param {string} cardId - the card id + * @param {Object} options - the description of the card + * @param {Requestable.callback} cb - will receive the modified card + * @return {Promise} - the promise for the http request + */ + updateProjectCard(cardId, options, cb) { + return this._request('PATCH', `/projects/columns/cards/${cardId}`, options, cb); + } + + /** + * Delete a card + * @see https://developer.github.com/v3/projects/cards/#delete-a-project-card + * @param {string} cardId - the card to be deleted + * @param {Requestable.callback} cb - will receive true if the operation is successful + * @return {Promise} - the promise for the http request + */ + deleteProjectCard(cardId, cb) { + return this._request('DELETE', `/projects/columns/cards/${cardId}`, null, cb); + } + + /** + * Move a card + * @see https://developer.github.com/v3/projects/cards/#move-a-project-card + * @param {string} cardId - the card to be moved + * @param {string} position - can be one of top, bottom, or after:, + * where is the id value of a card in the same project. + * @param {string} colId - the id value of a column in the same project. + * @param {Requestable.callback} cb - will receive true if the operation is successful + * @return {Promise} - the promise for the http request + */ + moveProjectCard(cardId, position, colId, cb) { + return this._request( + 'POST', + `/projects/columns/cards/${cardId}/moves`, + {position: position, column_id: colId}, // eslint-disable-line camelcase + cb + ); + } +} + +module.exports = Project; diff --git a/lib/Repository.js b/lib/Repository.js index 8b302558..74452dde 100644 --- a/lib/Repository.js +++ b/lib/Repository.js @@ -1,4 +1,3 @@ -'use strict'; /** * @file * @copyright 2013 Michael Aufreiter (Development Seed) and 2016 Yahoo Inc. @@ -8,12 +7,14 @@ import Requestable from './Requestable'; import Utf8 from 'utf8'; -import {Base64} from 'js-base64'; +import { + Base64, +} from 'js-base64'; import debug from 'debug'; const log = debug('github:repository'); /** - * Respository encapsulates the functionality to create, query, and modify files. + * Repository encapsulates the functionality to create, query, and modify files. */ class Repository extends Requestable { /** @@ -27,7 +28,7 @@ class Repository extends Requestable { this.__fullname = fullname; this.__currentTree = { branch: null, - sha: null + sha: null, }; } @@ -107,6 +108,17 @@ class Repository extends Requestable { return this._request('GET', `/repos/${this.__fullname}/pulls/${number}`, null, cb); } + /** + * List the files of a specific pull request + * @see https://developer.github.com/v3/pulls/#list-pull-requests-files + * @param {number|string} number - the PR you wish to fetch + * @param {Requestable.callback} [cb] - will receive the list of files from the API + * @return {Promise} - the promise for the http request + */ + listPullRequestFiles(number, cb) { + return this._request('GET', `/repos/${this.__fullname}/pulls/${number}/files`, null, cb); + } + /** * Compare two branches/commits/repositories * @see https://developer.github.com/v3/repos/commits/#compare-two-commits @@ -140,6 +152,17 @@ class Repository extends Requestable { return this._request('GET', `/repos/${this.__fullname}/git/blobs/${sha}`, null, cb, 'raw'); } + /** + * Get a single branch + * @see https://developer.github.com/v3/repos/branches/#get-branch + * @param {string} branch - the name of the branch to fetch + * @param {Requestable.callback} cb - will receive the branch from the API + * @returns {Promise} - the promise for the http request + */ + getBranch(branch, cb) { + return this._request('GET', `/repos/${this.__fullname}/branches/${branch}`, null, cb); + } + /** * Get a commit from the repository * @see https://developer.github.com/v3/repos/commits/#get-a-single-commit @@ -154,7 +177,7 @@ class Repository extends Requestable { /** * List the commits on a repository, optionally filtering by path, author or time range * @see https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository - * @param {Object} [options] + * @param {Object} [options] - the filtering options for commits * @param {string} [options.sha] - the SHA or branch to start from * @param {string} [options.path] - the path to search on * @param {string} [options.author] - the commit author @@ -165,13 +188,45 @@ class Repository extends Requestable { */ listCommits(options, cb) { options = options || {}; - + if (typeof options === 'function') { + cb = options; + options = {}; + } options.since = this._dateToISO(options.since); options.until = this._dateToISO(options.until); return this._request('GET', `/repos/${this.__fullname}/commits`, options, cb); } + /** + * List the commits on a pull request + * @see https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository + * @param {number|string} number - the number of the pull request to list the commits + * @param {Object} [options] - the filtering options for commits + * @param {Requestable.callback} [cb] - will receive the commits information + * @return {Promise} - the promise for the http request + */ + listCommitsOnPR(number, options, cb) { + options = options || {}; + if (typeof options === 'function') { + cb = options; + options = {}; + } + return this._request('GET', `/repos/${this.__fullname}/pulls/${number}/commits`, options, cb); + } + + /** + * Gets a single commit information for a repository + * @see https://developer.github.com/v3/repos/commits/#get-a-single-commit + * @param {string} ref - the reference for the commit-ish + * @param {Requestable.callback} cb - will receive the commit information + * @return {Promise} - the promise for the http request + */ + getSingleCommit(ref, cb) { + ref = ref || ''; + return this._request('GET', `/repos/${this.__fullname}/commits/${ref}`, null, cb); + } + /** * Get tha sha for a particular object in the repository. This is a convenience function * @see https://developer.github.com/v3/repos/contents/#get-contents @@ -196,6 +251,17 @@ class Repository extends Requestable { return this._request('GET', `/repos/${this.__fullname}/commits/${sha}/statuses`, null, cb); } + /** + * Get the combined view of commit statuses for a particular sha, branch, or tag + * @see https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref + * @param {string} sha - the sha, branch, or tag to get the combined status for + * @param {Requestable.callback} cb - will receive the combined status + * @returns {Promise} - the promise for the http request + */ + getCombinedStatus(sha, cb) { + return this._request('GET', `/repos/${this.__fullname}/commits/${sha}/status`, null, cb); + } + /** * Get a description of a git tree * @see https://developer.github.com/v3/git/trees/#get-a-tree @@ -221,26 +287,34 @@ class Repository extends Requestable { return this._request('POST', `/repos/${this.__fullname}/git/blobs`, postBody, cb); } + /** + * Get the object that represents the provided content + * @param {string|Buffer|Blob} content - the content to send to the server + * @return {Object} the representation of `content` for the GitHub API + */ _getContentObject(content) { if (typeof content === 'string') { log('contet is a string'); return { content: Utf8.encode(content), - encoding: 'utf-8' + encoding: 'utf-8', }; + } else if (typeof Buffer !== 'undefined' && content instanceof Buffer) { log('We appear to be in Node'); return { content: content.toString('base64'), - encoding: 'base64' + encoding: 'base64', }; + } else if (typeof Blob !== 'undefined' && content instanceof Blob) { log('We appear to be in the browser'); return { content: Base64.encode(content), - encoding: 'base64' + encoding: 'base64', }; - } else { + + } else { // eslint-disable-line log(`Not sure what this content is: ${typeof content}, ${JSON.stringify(content)}`); throw new Error('Unknown content passed to postBlob. Must be string or Buffer (node) or Blob (web)'); } @@ -258,13 +332,13 @@ class Repository extends Requestable { */ updateTree(baseTreeSHA, path, blobSHA, cb) { let newTree = { - 'base_tree': baseTreeSHA, - 'tree': [{ + base_tree: baseTreeSHA, // eslint-disable-line + tree: [{ path: path, sha: blobSHA, mode: '100644', - type: 'blob' - }] + type: 'blob', + }], }; return this._request('POST', `/repos/${this.__fullname}/git/trees`, newTree, cb); @@ -279,28 +353,41 @@ class Repository extends Requestable { * @return {Promise} - the promise for the http request */ createTree(tree, baseSHA, cb) { - return this._request('POST', `/repos/${this.__fullname}/git/trees`, {tree, base_tree: baseSHA}, cb); // jscs:ignore + return this._request('POST', `/repos/${this.__fullname}/git/trees`, { + tree, + base_tree: baseSHA, // eslint-disable-line camelcase + }, cb); } /** * Add a commit to the repository * @see https://developer.github.com/v3/git/commits/#create-a-commit * @param {string} parent - the SHA of the parent commit - * @param {Object} tree - the tree that describes this commit + * @param {string} tree - the SHA of the tree for this commit * @param {string} message - the commit message - * @param {Function} cb - will receive the commit that is created + * @param {Object} [options] - commit options + * @param {Object} [options.author] - the author of the commit + * @param {Object} [options.commiter] - the committer + * @param {Requestable.callback} cb - will receive the commit that is created * @return {Promise} - the promise for the http request */ - commit(parent, tree, message, cb) { + commit(parent, tree, message, options, cb) { + if (typeof options === 'function') { + cb = options; + options = {}; + } + let data = { message, tree, - parents: [parent] + parents: [parent], }; + data = Object.assign({}, options, data); + return this._request('POST', `/repos/${this.__fullname}/git/commits`, data, cb) .then((response) => { - this.__currentTree.sha = response.sha; // Update latest commit + this.__currentTree.sha = response.data.sha; // Update latest commit return response; }); } @@ -310,17 +397,56 @@ class Repository extends Requestable { * @see https://developer.github.com/v3/git/refs/#update-a-reference * @param {string} ref - the ref to update * @param {string} commitSHA - the SHA to point the reference to - * @param {Function} cb - will receive the updated ref back + * @param {boolean} force - indicates whether to force or ensure a fast-forward update + * @param {Requestable.callback} cb - will receive the updated ref back + * @return {Promise} - the promise for the http request + */ + updateHead(ref, commitSHA, force, cb) { + return this._request('PATCH', `/repos/${this.__fullname}/git/refs/${ref}`, { + sha: commitSHA, + force: force, + }, cb); + } + + /** + * Update commit status + * @see https://developer.github.com/v3/repos/statuses/ + * @param {string} commitSHA - the SHA of the commit that should be updated + * @param {object} options - Commit status parameters + * @param {string} options.state - The state of the status. Can be one of: pending, success, error, or failure. + * @param {string} [options.target_url] - The target URL to associate with this status. + * @param {string} [options.description] - A short description of the status. + * @param {string} [options.context] - A string label to differentiate this status among CI systems. + * @param {Requestable.callback} cb - will receive the updated commit back * @return {Promise} - the promise for the http request */ - updateHead(ref, commitSHA, cb) { - return this._request('PATCH', `/repos/${this.__fullname}/git/refs/${ref}`, {sha: commitSHA}, cb); + updateStatus(commitSHA, options, cb) { + return this._request('POST', `/repos/${this.__fullname}/statuses/${commitSHA}`, options, cb); } /** + * Update repository information + * @see https://developer.github.com/v3/repos/#edit + * @param {object} options - New parameters that will be set to the repository + * @param {string} options.name - Name of the repository + * @param {string} [options.description] - A short description of the repository + * @param {string} [options.homepage] - A URL with more information about the repository + * @param {boolean} [options.private] - Either true to make the repository private, or false to make it public. + * @param {boolean} [options.has_issues] - Either true to enable issues for this repository, false to disable them. + * @param {boolean} [options.has_wiki] - Either true to enable the wiki for this repository, false to disable it. + * @param {boolean} [options.has_downloads] - Either true to enable downloads, false to disable them. + * @param {string} [options.default_branch] - Updates the default branch for this repository. + * @param {Requestable.callback} cb - will receive the updated repository back + * @return {Promise} - the promise for the http request + */ + updateRepository(options, cb) { + return this._request('PATCH', `/repos/${this.__fullname}`, options, cb); + } + + /** * Get information about the repository * @see https://developer.github.com/v3/repos/#get - * @param {Function} cb - will receive the information about the repository + * @param {Requestable.callback} cb - will receive the information about the repository * @return {Promise} - the promise for the http request */ getDetails(cb) { @@ -330,10 +456,20 @@ class Repository extends Requestable { /** * List the contributors to the repository * @see https://developer.github.com/v3/repos/#list-contributors - * @param {Function} cb - will receive the list of contributors + * @param {Requestable.callback} cb - will receive the list of contributors * @return {Promise} - the promise for the http request */ getContributors(cb) { + return this._request('GET', `/repos/${this.__fullname}/contributors`, null, cb); + } + + /** + * List the contributor stats to the repository + * @see https://developer.github.com/v3/repos/#list-contributors + * @param {Requestable.callback} cb - will receive the list of contributors + * @return {Promise} - the promise for the http request + */ + getContributorStats(cb) { return this._request('GET', `/repos/${this.__fullname}/stats/contributors`, null, cb); } @@ -341,7 +477,7 @@ class Repository extends Requestable { * List the users who are collaborators on the repository. The currently authenticated user must have * push access to use this method * @see https://developer.github.com/v3/repos/collaborators/#list-collaborators - * @param {Function} cb - will receive the list of collaborators + * @param {Requestable.callback} cb - will receive the list of collaborators * @return {Promise} - the promise for the http request */ getCollaborators(cb) { @@ -352,7 +488,7 @@ class Repository extends Requestable { * Check if a user is a collaborator on the repository * @see https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator * @param {string} username - the user to check - * @param {Function} cb - will receive true if the user is a collaborator and false if they are not + * @param {Requestable.callback} cb - will receive true if the user is a collaborator and false if they are not * @return {Promise} - the promise for the http request {Boolean} [description] */ isCollaborator(username, cb) { @@ -365,28 +501,56 @@ class Repository extends Requestable { * @param {string} ref - the ref to check * @param {string} path - the path containing the content to fetch * @param {boolean} raw - `true` if the results should be returned raw instead of GitHub's normalized format - * @param {Function} cb - will receive the fetched data + * @param {Requestable.callback} cb - will receive the fetched data * @return {Promise} - the promise for the http request */ getContents(ref, path, raw, cb) { path = path ? `${encodeURI(path)}` : ''; - return this._request('GET', `/repos/${this.__fullname}/contents/${path}`, {ref}, cb, raw); + return this._request('GET', `/repos/${this.__fullname}/contents/${path}`, { + ref, + }, cb, raw); + } + + /** + * Get the README of a repository + * @see https://developer.github.com/v3/repos/contents/#get-the-readme + * @param {string} ref - the ref to check + * @param {boolean} raw - `true` if the results should be returned raw instead of GitHub's normalized format + * @param {Requestable.callback} cb - will receive the fetched data + * @return {Promise} - the promise for the http request + */ + getReadme(ref, raw, cb) { + return this._request('GET', `/repos/${this.__fullname}/readme`, { + ref, + }, cb, raw); } /** * Fork a repository * @see https://developer.github.com/v3/repos/forks/#create-a-fork - * @param {Function} cb - will receive the information about the newly created fork + * @param {Requestable.callback} cb - will receive the information about the newly created fork * @return {Promise} - the promise for the http request */ fork(cb) { return this._request('POST', `/repos/${this.__fullname}/forks`, null, cb); } + /** + * Fork a repository to an organization + * @see https://developer.github.com/v3/repos/forks/#create-a-fork + * @param {String} org - organization where you'd like to create the fork. + * @param {Requestable.callback} cb - will receive the information about the newly created fork + * @return {Promise} - the promise for the http request + * + */ + forkToOrg(org, cb) { + return this._request('POST', `/repos/${this.__fullname}/forks?organization=${org}`, null, cb); + } + /** * List a repository's forks * @see https://developer.github.com/v3/repos/forks/#list-forks - * @param {Function} cb - will receive the list of repositories forked from this one + * @param {Requestable.callback} cb - will receive the list of repositories forked from this one * @return {Promise} - the promise for the http request */ listForks(cb) { @@ -397,7 +561,7 @@ class Repository extends Requestable { * Create a new branch from an existing branch. * @param {string} [oldBranch=master] - the name of the existing branch * @param {string} newBranch - the name of the new branch - * @param {Function} cb - will receive the commit data for the head of the new branch + * @param {Requestable.callback} cb - will receive the commit data for the head of the new branch * @return {Promise} - the promise for the http request */ createBranch(oldBranch, newBranch, cb) { @@ -410,7 +574,10 @@ class Repository extends Requestable { return this.getRef(`heads/${oldBranch}`) .then((response) => { let sha = response.data.object.sha; - return this.createRef({sha, ref: `refs/heads/${newBranch}`}, cb); + return this.createRef({ + sha, + ref: `refs/heads/${newBranch}`, + }, cb); }); } @@ -418,17 +585,29 @@ class Repository extends Requestable { * Create a new pull request * @see https://developer.github.com/v3/pulls/#create-a-pull-request * @param {Object} options - the pull request description - * @param {Function} cb - will receive the new pull request + * @param {Requestable.callback} cb - will receive the new pull request * @return {Promise} - the promise for the http request */ createPullRequest(options, cb) { return this._request('POST', `/repos/${this.__fullname}/pulls`, options, cb); } + /** + * Update a pull request + * @see https://developer.github.com/v3/pulls/#update-a-pull-request + * @param {number|string} number - the number of the pull request to update + * @param {Object} options - the pull request description + * @param {Requestable.callback} [cb] - will receive the pull request information + * @return {Promise} - the promise for the http request + */ + updatePullRequest(number, options, cb) { + return this._request('PATCH', `/repos/${this.__fullname}/pulls/${number}`, options, cb); + } + /** * List the hooks for the repository * @see https://developer.github.com/v3/repos/hooks/#list-hooks - * @param {Function} cb - will receive the list of hooks + * @param {Requestable.callback} cb - will receive the list of hooks * @return {Promise} - the promise for the http request */ listHooks(cb) { @@ -439,7 +618,7 @@ class Repository extends Requestable { * Get a hook for the repository * @see https://developer.github.com/v3/repos/hooks/#get-single-hook * @param {number} id - the id of the webook - * @param {Function} cb - will receive the details of the webook + * @param {Requestable.callback} cb - will receive the details of the webook * @return {Promise} - the promise for the http request */ getHook(id, cb) { @@ -450,7 +629,7 @@ class Repository extends Requestable { * Add a new hook to the repository * @see https://developer.github.com/v3/repos/hooks/#create-a-hook * @param {Object} options - the configuration describing the new hook - * @param {Function} cb - will receive the new webhook + * @param {Requestable.callback} cb - will receive the new webhook * @return {Promise} - the promise for the http request */ createHook(options, cb) { @@ -462,7 +641,7 @@ class Repository extends Requestable { * @see https://developer.github.com/v3/repos/hooks/#edit-a-hook * @param {number} id - the id of the webhook * @param {Object} options - the new description of the webhook - * @param {Function} cb - will receive the updated webhook + * @param {Requestable.callback} cb - will receive the updated webhook * @return {Promise} - the promise for the http request */ updateHook(id, options, cb) { @@ -473,11 +652,54 @@ class Repository extends Requestable { * Delete a webhook * @see https://developer.github.com/v3/repos/hooks/#delete-a-hook * @param {number} id - the id of the webhook to be deleted - * @param {Function} cb - will receive true if the call is successful + * @param {Requestable.callback} cb - will receive true if the call is successful * @return {Promise} - the promise for the http request */ deleteHook(id, cb) { - return this._request('DELETE', `${this.__repoPath}/hooks/${id}`, null, cb); + return this._request('DELETE', `/repos/${this.__fullname}/hooks/${id}`, null, cb); + } + + /** + * List the deploy keys for the repository + * @see https://developer.github.com/v3/repos/keys/#list-deploy-keys + * @param {Requestable.callback} cb - will receive the list of deploy keys + * @return {Promise} - the promise for the http request + */ + listKeys(cb) { + return this._request('GET', `/repos/${this.__fullname}/keys`, null, cb); + } + + /** + * Get a deploy key for the repository + * @see https://developer.github.com/v3/repos/keys/#get-a-deploy-key + * @param {number} id - the id of the deploy key + * @param {Requestable.callback} cb - will receive the details of the deploy key + * @return {Promise} - the promise for the http request + */ + getKey(id, cb) { + return this._request('GET', `/repos/${this.__fullname}/keys/${id}`, null, cb); + } + + /** + * Add a new deploy key to the repository + * @see https://developer.github.com/v3/repos/keys/#add-a-new-deploy-key + * @param {Object} options - the configuration describing the new deploy key + * @param {Requestable.callback} cb - will receive the new deploy key + * @return {Promise} - the promise for the http request + */ + createKey(options, cb) { + return this._request('POST', `/repos/${this.__fullname}/keys`, options, cb); + } + + /** + * Delete a deploy key + * @see https://developer.github.com/v3/repos/keys/#remove-a-deploy-key + * @param {number} id - the id of the deploy key to be deleted + * @param {Requestable.callback} cb - will receive true if the call is successful + * @return {Promise} - the promise for the http request + */ + deleteKey(id, cb) { + return this._request('DELETE', `/repos/${this.__fullname}/keys/${id}`, null, cb); } /** @@ -485,7 +707,7 @@ class Repository extends Requestable { * @see https://developer.github.com/v3/repos/contents/#delete-a-file * @param {string} branch - the branch to delete from, or the default branch if not specified * @param {string} path - the path of the file to remove - * @param {Function} cb - will receive the commit in which the delete occurred + * @param {Requestable.callback} cb - will receive the commit in which the delete occurred * @return {Promise} - the promise for the http request */ deleteFile(branch, path, cb) { @@ -494,47 +716,39 @@ class Repository extends Requestable { const deleteCommit = { message: `Delete the file at '${path}'`, sha: response.data.sha, - branch + branch, }; return this._request('DELETE', `/repos/${this.__fullname}/contents/${path}`, deleteCommit, cb); }); } - // Move a file to a new location - // ------- - move(branch, path, newPath, cb) { - return this._updateTree(branch, function(err, latestCommit) { - this.getTree(latestCommit + '?recursive=true', function(err, tree) { - // Update Tree - tree.forEach(function(ref) { - if (ref.path === path) { + /** + * Change all references in a repo from oldPath to new_path + * @param {string} branch - the branch to carry out the reference change, or the default branch if not specified + * @param {string} oldPath - original path + * @param {string} newPath - new reference path + * @param {Requestable.callback} cb - will receive the commit in which the move occurred + * @return {Promise} - the promise for the http request + */ + move(branch, oldPath, newPath, cb) { + let oldSha; + return this.getRef(`heads/${branch}`) + .then(({data: {object}}) => this.getTree(`${object.sha}?recursive=true`)) + .then(({data: {tree, sha}}) => { + oldSha = sha; + let newTree = tree.map((ref) => { + if (ref.path === oldPath) { ref.path = newPath; } - if (ref.type === 'tree') { delete ref.sha; } + return ref; }); - - this.createTree(tree, function(err, rootTree) { - this.commit(latestCommit, rootTree, 'Deleted ' + path, function(err, commit) { - this.updateHead(branch, commit, cb); - }); - }); - }); - }); - } - - _updateTree(branch, cb) { - if (branch === this.__currentTree.branch && this.__currentTree.sha) { - return cb(null, this.__currentTree.sha); - } - - return this.getRef(`heads/${branch}`, function(err, sha) { - this.__currentTree.branch = branch; - this.__currentTree.sha = sha; - cb(err, sha); - }); + return this.createTree(newTree); + }) + .then(({data: tree}) => this.commit(oldSha, tree.sha, `Renamed '${oldPath}' to '${newPath}'`)) + .then(({data: commit}) => this.updateHead(`heads/${branch}`, commit.sha, true, cb)); } /** @@ -544,14 +758,15 @@ class Repository extends Requestable { * @param {string} path - the path for the file * @param {string} content - the contents of the file * @param {string} message - the commit message - * @param {Object} [options] + * @param {Object} [options] - commit options * @param {Object} [options.author] - the author of the commit * @param {Object} [options.commiter] - the committer * @param {boolean} [options.encode] - true if the content should be base64 encoded - * @param {Function} cb - will receive the new commit + * @param {Requestable.callback} cb - will receive the new commit * @return {Promise} - the promise for the http request */ writeFile(branch, path, content, message, options, cb) { + options = options || {}; if (typeof options === 'function') { cb = options; options = {}; @@ -563,7 +778,7 @@ class Repository extends Requestable { message, author: options.author, committer: options.committer, - content: shouldEncode ? Base64.encode(content) : content + content: shouldEncode ? Base64.encode(content) : content, }; return this.getSha(branch, filePath) @@ -642,7 +857,7 @@ class Repository extends Requestable { /** * Get information about a release * @see https://developer.github.com/v3/repos/releases/#get-a-single-release - * @param {strign} id - the id of the release + * @param {string} id - the id of the release * @param {Requestable.callback} cb - will receive the release information * @return {Promise} - the promise for the http request */ @@ -660,6 +875,42 @@ class Repository extends Requestable { deleteRelease(id, cb) { return this._request('DELETE', `/repos/${this.__fullname}/releases/${id}`, null, cb); } + + /** + * Merge a pull request + * @see https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button + * @param {number|string} number - the number of the pull request to merge + * @param {Object} options - the merge options for the pull request + * @param {Requestable.callback} [cb] - will receive the merge information if the operation is successful + * @return {Promise} - the promise for the http request + */ + mergePullRequest(number, options, cb) { + return this._request('PUT', `/repos/${this.__fullname}/pulls/${number}/merge`, options, cb); + } + + /** + * Get information about all projects + * @see https://developer.github.com/v3/projects/#list-repository-projects + * @param {Requestable.callback} [cb] - will receive the list of projects + * @return {Promise} - the promise for the http request + */ + listProjects(cb) { + return this._requestAllPages(`/repos/${this.__fullname}/projects`, {AcceptHeader: 'inertia-preview'}, cb); + } + + /** + * Create a new project + * @see https://developer.github.com/v3/projects/#create-a-repository-project + * @param {Object} options - the description of the project + * @param {Requestable.callback} cb - will receive the newly created project + * @return {Promise} - the promise for the http request + */ + createProject(options, cb) { + options = options || {}; + options.AcceptHeader = 'inertia-preview'; + return this._request('POST', `/repos/${this.__fullname}/projects`, options, cb); + } + } module.exports = Repository; diff --git a/lib/Requestable.js b/lib/Requestable.js index b42dbe66..4d6e8d9d 100644 --- a/lib/Requestable.js +++ b/lib/Requestable.js @@ -8,12 +8,26 @@ import axios from 'axios'; import debug from 'debug'; import {Base64} from 'js-base64'; -import {polyfill} from 'es6-promise'; const log = debug('github:request'); -if (typeof Promise === 'undefined') { - polyfill(); +/** + * The error structure returned when a network call fails + */ +class ResponseError extends Error { + /** + * Construct a new ResponseError + * @param {string} message - an message to return instead of the the default error message + * @param {string} path - the requested path + * @param {Object} response - the object returned by Axios + */ + constructor(message, path, response) { + super(message); + this.path = path; + this.request = response.config; + this.response = (response || {}).response || response; + this.status = response.status; + } } /** @@ -32,14 +46,16 @@ class Requestable { * @param {Requestable.auth} [auth] - the credentials to authenticate to Github. If auth is * not provided request will be made unauthenticated * @param {string} [apiBase=https://api.github.com] - the base Github API URL + * @param {string} [AcceptHeader=v3] - the accept header for the requests */ - constructor(auth, apiBase) { + constructor(auth, apiBase, AcceptHeader) { this.__apiBase = apiBase || 'https://api.github.com'; this.__auth = { token: auth.token, username: auth.username, - password: auth.password + password: auth.password, }; + this.__AcceptHeader = AcceptHeader || 'v3'; if (auth.token) { this.__authorizationHeader = 'token ' + auth.token; @@ -69,14 +85,20 @@ class Requestable { * Compute the headers required for an API request. * @private * @param {boolean} raw - if the request should be treated as JSON or as a raw request + * @param {string} AcceptHeader - the accept header for the request * @return {Object} - the headers to use in the request */ - __getRequestHeaders(raw) { + __getRequestHeaders(raw, AcceptHeader) { let headers = { - 'Accept': raw ? 'application/vnd.github.v3.raw+json' : 'application/vnd.github.v3+json', - 'Content-Type': 'application/json;charset=UTF-8' + 'Content-Type': 'application/json;charset=UTF-8', + 'Accept': 'application/vnd.github.' + (AcceptHeader || this.__AcceptHeader), }; + if (raw) { + headers.Accept += '.raw'; + } + headers.Accept += '+json'; + if (this.__authorizationHeader) { headers.Authorization = this.__authorizationHeader; } @@ -88,19 +110,21 @@ class Requestable { * Sets the default options for API requests * @protected * @param {Object} [requestOptions={}] - the current options for the request - * @return - the options to pass to the request + * @return {Object} - the options to pass to the request */ _getOptionsWithDefaults(requestOptions = {}) { - requestOptions.type = requestOptions.type || 'all'; + if (!(requestOptions.visibility || requestOptions.affiliation)) { + requestOptions.type = requestOptions.type || 'all'; + } requestOptions.sort = requestOptions.sort || 'updated'; - requestOptions.per_page = requestOptions.per_page || '100'; // jscs:ignore + requestOptions.per_page = requestOptions.per_page || '100'; // eslint-disable-line return requestOptions; } /** * if a `Date` is passed to this function it will be converted to an ISO string - * @param {*} date - the object to attempt to cooerce into an ISO date string + * @param {*} date - the object to attempt to coerce into an ISO date string * @return {string} - the ISO representation of `date` or whatever was passed in if it was not a date */ _dateToISO(date) { @@ -131,7 +155,13 @@ class Requestable { */ _request(method, path, data, cb, raw) { const url = this.__getURL(path); - const headers = this.__getRequestHeaders(raw); + + const AcceptHeader = (data || {}).AcceptHeader; + if (AcceptHeader) { + delete data.AcceptHeader; + } + const headers = this.__getRequestHeaders(raw, AcceptHeader); + let queryParams = {}; const shouldUseDataAsParams = data && (typeof data === 'object') && methodHasNoBody(method); @@ -146,7 +176,7 @@ class Requestable { headers: headers, params: queryParams, data: data, - responseType: raw ? 'text' : 'json' + responseType: raw ? 'text' : 'json', }; log(`${config.method} to ${config.url}`); @@ -154,7 +184,15 @@ class Requestable { if (cb) { requestPromise.then((response) => { - cb(null, response.data || true, response); + if (response.data && Object.keys(response.data).length > 0) { + // When data has results + cb(null, response.data, response); + } else if (config.method !== 'GET' && Object.keys(response.data).length < 1) { + // True when successful submit a request and receive a empty object + cb(null, (response.status < 300), response); + } else { + cb(null, response.data, response); + } }); } @@ -166,17 +204,18 @@ class Requestable { * @param {string} path - the path to request * @param {Object} data - any query parameters for the request * @param {Requestable.callback} cb - the callback that will receive `true` or `false` + * @param {method} [method=GET] - HTTP Method to use * @return {Promise} - the promise for the http request */ - _request204or404(path, data, cb) { - return this._request('GET', path, data) + _request204or404(path, data, cb, method = 'GET') { + return this._request(method, path, data) .then(function success(response) { if (cb) { cb(null, true, response); } return true; }, function failure(response) { - if (response.status === 404) { + if (response.response.status === 404) { if (cb) { cb(null, false, response); } @@ -196,7 +235,7 @@ class Requestable { * @param {string} path - the path to request * @param {Object} options - the query parameters to include * @param {Requestable.callback} [cb] - the function to receive the data. The returned data will always be an array. - * @param {Object[]} results - the partial results. This argument is intended for interal use only. + * @param {Object[]} results - the partial results. This argument is intended for internal use only. * @return {Promise} - a promise which will resolve when all pages have been fetched * @deprecated This will be folded into {@link Requestable#_request} in the 2.0 release. */ @@ -205,12 +244,32 @@ class Requestable { return this._request('GET', path, options) .then((response) => { - results.push.apply(results, response.data); + let thisGroup; + if (response.data instanceof Array) { + thisGroup = response.data; + } else if (response.data.items instanceof Array) { + thisGroup = response.data.items; + } else { + let message = `cannot figure out how to append ${response.data} to the result set`; + throw new ResponseError(message, path, response); + } + results.push(...thisGroup); const nextUrl = getNextPage(response.headers.link); - if (nextUrl) { - log(`getting next page: ${nextUrl}`); - return this._requestAllPages(nextUrl, options, cb, results); + if(nextUrl) { + if (!options) { + options = {}; + } + options.page = parseInt( + nextUrl.match(/([&\?]page=[0-9]*)/g) + .shift() + .split('=') + .pop() + ); + if (!(options && typeof options.page !== 'number')) { + log(`getting next page: ${nextUrl}`); + return this._requestAllPages(nextUrl, options, cb, results); + } } if (cb) { @@ -228,16 +287,6 @@ module.exports = Requestable; // ////////////////////////// // // Private helper functions // // ////////////////////////// // -class ResponseError extends Error { - constructor(path, response) { - super(`error making request ${response.config.method} ${response.config.url}`); - this.path = path; - this.request = response.config; - this.response = response; - this.status = response.status; - } -} - const METHODS_WITH_NO_BODY = ['GET', 'HEAD', 'DELETE']; function methodHasNoBody(method) { return METHODS_WITH_NO_BODY.indexOf(method) !== -1; @@ -255,9 +304,16 @@ function getNextPage(linksHeader = '') { } function callbackErrorOrThrow(cb, path) { - return function handler(response) { - log(`error making request ${response.config.method} ${response.config.url} ${JSON.stringify(response.data)}`); - let error = new ResponseError(path, response); + return function handler(object) { + let error; + if (object.hasOwnProperty('config')) { + const {response: {status, statusText}, config: {method, url}} = object; + let message = (`${status} error making request ${method} ${url}: "${statusText}"`); + error = new ResponseError(message, path, object); + log(`${message} ${JSON.stringify(object.data)}`); + } else { + error = object; + } if (cb) { log('going to error callback'); cb(error); diff --git a/lib/Search.js b/lib/Search.js index f134087e..e0bde4fb 100644 --- a/lib/Search.js +++ b/lib/Search.js @@ -43,8 +43,12 @@ class Search extends Requestable { */ _search(path, withOptions = {}, cb = undefined) { let requestOptions = {}; - Object.keys(this.__defaults).forEach((prop) => requestOptions[prop] = this.__defaults[prop]); - Object.keys(withOptions).forEach((prop) => requestOptions[prop] = withOptions[prop]); + Object.keys(this.__defaults).forEach((prop) => { + requestOptions[prop] = this.__defaults[prop]; + }); + Object.keys(withOptions).forEach((prop) => { + requestOptions[prop] = withOptions[prop]; + }); log(`searching ${path} with options:`, requestOptions); return this._requestAllPages(`/search/${path}`, requestOptions, cb); diff --git a/lib/Team.js b/lib/Team.js new file mode 100644 index 00000000..f91c9173 --- /dev/null +++ b/lib/Team.js @@ -0,0 +1,160 @@ +/** + * @file + * @copyright 2016 Matt Smith (Development Seed) + * @license Licensed under {@link https://spdx.org/licenses/BSD-3-Clause-Clear.html BSD-3-Clause-Clear}. + * Github.js is freely distributable. + */ + +import Requestable from './Requestable'; +import debug from 'debug'; +const log = debug('github:team'); + +/** + * A Team allows scoping of API requests to a particular Github Organization Team. + */ +class Team extends Requestable { + /** + * Create a Team. + * @param {string} [teamId] - the id for the team + * @param {Requestable.auth} [auth] - information required to authenticate to Github + * @param {string} [apiBase=https://api.github.com] - the base Github API URL + */ + constructor(teamId, auth, apiBase) { + super(auth, apiBase); + this.__teamId = teamId; + } + + /** + * Get Team information + * @see https://developer.github.com/v3/orgs/teams/#get-team + * @param {Requestable.callback} [cb] - will receive the team + * @return {Promise} - the promise for the http request + */ + getTeam(cb) { + log(`Fetching Team ${this.__teamId}`); + return this._request('Get', `/teams/${this.__teamId}`, undefined, cb); + } + + /** + * List the Team's repositories + * @see https://developer.github.com/v3/orgs/teams/#list-team-repos + * @param {Requestable.callback} [cb] - will receive the list of repositories + * @return {Promise} - the promise for the http request + */ + listRepos(cb) { + log(`Fetching repositories for Team ${this.__teamId}`); + return this._requestAllPages(`/teams/${this.__teamId}/repos`, undefined, cb); + } + + /** + * Edit Team information + * @see https://developer.github.com/v3/orgs/teams/#edit-team + * @param {object} options - Parameters for team edit + * @param {string} options.name - The name of the team + * @param {string} [options.description] - Team description + * @param {string} [options.repo_names] - Repos to add the team to + * @param {string} [options.privacy=secret] - The level of privacy the team should have. Can be either one + * of: `secret`, or `closed` + * @param {Requestable.callback} [cb] - will receive the updated team + * @return {Promise} - the promise for the http request + */ + editTeam(options, cb) { + log(`Editing Team ${this.__teamId}`); + return this._request('PATCH', `/teams/${this.__teamId}`, options, cb); + } + + /** + * List the users who are members of the Team + * @see https://developer.github.com/v3/orgs/teams/#list-team-members + * @param {object} options - Parameters for listing team users + * @param {string} [options.role=all] - can be one of: `all`, `maintainer`, or `member` + * @param {Requestable.callback} [cb] - will receive the list of users + * @return {Promise} - the promise for the http request + */ + listMembers(options, cb) { + log(`Getting members of Team ${this.__teamId}`); + return this._requestAllPages(`/teams/${this.__teamId}/members`, options, cb); + } + + /** + * Get Team membership status for a user + * @see https://developer.github.com/v3/orgs/teams/#get-team-membership + * @param {string} username - can be one of: `all`, `maintainer`, or `member` + * @param {Requestable.callback} [cb] - will receive the membership status of a user + * @return {Promise} - the promise for the http request + */ + getMembership(username, cb) { + log(`Getting membership of user ${username} in Team ${this.__teamId}`); + return this._request('GET', `/teams/${this.__teamId}/memberships/${username}`, undefined, cb); + } + + /** + * Add a member to the Team + * @see https://developer.github.com/v3/orgs/teams/#add-team-membership + * @param {string} username - can be one of: `all`, `maintainer`, or `member` + * @param {object} options - Parameters for adding a team member + * @param {string} [options.role=member] - The role that this user should have in the team. Can be one + * of: `member`, or `maintainer` + * @param {Requestable.callback} [cb] - will receive the membership status of added user + * @return {Promise} - the promise for the http request + */ + addMembership(username, options, cb) { + log(`Adding user ${username} to Team ${this.__teamId}`); + return this._request('PUT', `/teams/${this.__teamId}/memberships/${username}`, options, cb); + } + + /** + * Get repo management status for team + * @see https://developer.github.com/v3/orgs/teams/#remove-team-membership + * @param {string} owner - Organization name + * @param {string} repo - Repo name + * @param {Requestable.callback} [cb] - will receive the membership status of added user + * @return {Promise} - the promise for the http request + */ + isManagedRepo(owner, repo, cb) { + log(`Getting repo management by Team ${this.__teamId} for repo ${owner}/${repo}`); + return this._request204or404(`/teams/${this.__teamId}/repos/${owner}/${repo}`, undefined, cb); + } + + /** + * Add or Update repo management status for team + * @see https://developer.github.com/v3/orgs/teams/#add-or-update-team-repository + * @param {string} owner - Organization name + * @param {string} repo - Repo name + * @param {object} options - Parameters for adding or updating repo management for the team + * @param {string} [options.permission] - The permission to grant the team on this repository. Can be one + * of: `pull`, `push`, or `admin` + * @param {Requestable.callback} [cb] - will receive the membership status of added user + * @return {Promise} - the promise for the http request + */ + manageRepo(owner, repo, options, cb) { + log(`Adding or Updating repo management by Team ${this.__teamId} for repo ${owner}/${repo}`); + return this._request204or404(`/teams/${this.__teamId}/repos/${owner}/${repo}`, options, cb, 'PUT'); + } + + /** + * Remove repo management status for team + * @see https://developer.github.com/v3/orgs/teams/#remove-team-repository + * @param {string} owner - Organization name + * @param {string} repo - Repo name + * @param {Requestable.callback} [cb] - will receive the membership status of added user + * @return {Promise} - the promise for the http request + */ + unmanageRepo(owner, repo, cb) { + log(`Remove repo management by Team ${this.__teamId} for repo ${owner}/${repo}`); + return this._request204or404(`/teams/${this.__teamId}/repos/${owner}/${repo}`, undefined, cb, 'DELETE'); + } + + /** + * Delete Team + * @see https://developer.github.com/v3/orgs/teams/#delete-team + * @param {Requestable.callback} [cb] - will receive the list of repositories + * @return {Promise} - the promise for the http request + */ + deleteTeam(cb) { + log(`Deleting Team ${this.__teamId}`); + return this._request204or404(`/teams/${this.__teamId}`, undefined, cb, 'DELETE'); + } +} + +module.exports = Team; diff --git a/lib/User.js b/lib/User.js index aea35d8a..a6f22324 100644 --- a/lib/User.js +++ b/lib/User.js @@ -33,10 +33,11 @@ class User extends Requestable { __getScopedUrl(endpoint) { if (this.__user) { return endpoint ? - `/users/${this.__user}/${endpoint}` - : `/users/${this.__user}` + `/users/${this.__user}/${endpoint}` : + `/users/${this.__user}` ; - } else { + + } else { // eslint-disable-line switch (endpoint) { case '': return '/user'; @@ -58,7 +59,7 @@ class User extends Requestable { * @param {Requestable.callback} [cb] - will receive the list of repositories * @return {Promise} - the promise for the http request */ - getRepos(options, cb) { + listRepos(options, cb) { if (typeof options === 'function') { cb = options; options = {}; @@ -76,17 +77,37 @@ class User extends Requestable { * @param {Requestable.callback} [cb] - will receive the list of organizations * @return {Promise} - the promise for the http request */ - getOrgs(cb) { + listOrgs(cb) { return this._request('GET', this.__getScopedUrl('orgs'), null, cb); } + /** + * List followers of a user + * @see https://developer.github.com/v3/users/followers/#list-followers-of-a-user + * @param {Requestable.callback} [cb] - will receive the list of followers + * @return {Promise} - the promise for the http request + */ + listFollowers(cb) { + return this._request('GET', this.__getScopedUrl('followers'), null, cb); + } + + /** + * Lists the people who the authenticated user follows. + * @see https://docs.github.com/en/rest/reference/users#list-the-people-the-authenticated-user-follows + * @param {Requestable.callback} [cb] - will receive the list of who a user is following + * @return {Promise} - the promise for the http request + */ + listFollowing(cb) { + return this._request('GET', this.__getScopedUrl('following'), null, cb); + } + /** * List the user's gists * @see https://developer.github.com/v3/gists/#list-a-users-gists * @param {Requestable.callback} [cb] - will receive the list of gists * @return {Promise} - the promise for the http request */ - getGists(cb) { + listGists(cb) { return this._request('GET', this.__getScopedUrl('gists'), null, cb); } @@ -97,7 +118,7 @@ class User extends Requestable { * @param {Requestable.callback} [cb] - will receive the list of repositories * @return {Promise} - the promise for the http request */ - getNotifications(options, cb) { + listNotifications(options, cb) { options = options || {}; if (typeof options === 'function') { cb = options; @@ -126,11 +147,38 @@ class User extends Requestable { * @param {Requestable.callback} [cb] - will receive the list of starred repositories * @return {Promise} - the promise for the http request */ - getStarredRepos(cb) { + listStarredRepos(cb) { let requestOptions = this._getOptionsWithDefaults(); return this._requestAllPages(this.__getScopedUrl('starred'), requestOptions, cb); } + /** + * Gets the list of starred gists for the user + * @see https://developer.github.com/v3/gists/#list-starred-gists + * @param {Object} [options={}] - any options to refine the search + * @param {Requestable.callback} [cb] - will receive the list of gists + * @return {Promise} - the promise for the http request + */ + listStarredGists(options, cb) { + options = options || {}; + if (typeof options === 'function') { + cb = options; + options = {}; + } + options.since = this._dateToISO(options.since); + return this._request('GET', '/gists/starred', options, cb); + } + + /** + * List email addresses for a user + * @see https://developer.github.com/v3/users/emails/#list-email-addresses-for-a-user + * @param {Requestable.callback} [cb] - will receive the list of emails + * @return {Promise} - the promise for the http request + */ + getEmails(cb) { + return this._request('GET', '/user/emails', null, cb); + } + /** * Have the authenticated user follow this user * @see https://developer.github.com/v3/users/followers/#follow-a-user @@ -139,7 +187,7 @@ class User extends Requestable { * @return {Promise} - the promise for the http request */ follow(username, cb) { - return this._request('PUT', `/user/following/${this.__user}`, null, cb); + return this._request('PUT', `/user/following/${username}`, null, cb); } /** @@ -150,7 +198,7 @@ class User extends Requestable { * @return {Promise} - the promise for the http request */ unfollow(username, cb) { - return this._request('DELETE', `/user/following/${this.__user}`, null, cb); + return this._request('DELETE', `/user/following/${username}`, null, cb); } /** diff --git a/mocha.opts b/mocha.opts index 4944011d..0377ab2e 100644 --- a/mocha.opts +++ b/mocha.opts @@ -1,3 +1,3 @@ --compilers js:babel-register ---timeout 15000 +--timeout 20000 --slow 5000 diff --git a/package.json b/package.json index 50bc39db..8e54341a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "github-api", - "version": "1.3.0", + "version": "3.4.0", "license": "BSD-3-Clause-Clear", "description": "A higher-level wrapper around the Github API.", "main": "dist/components/GitHub.js", @@ -8,17 +8,20 @@ "Ændrew Rininsland (http://www.aendrew.com)", "Aurelio De Rosa (http://www.audero.it/)", "Clay Reimann (http://clayreimann.me)", - "Michael Aufreiter (http://substance.io)" + "Michael Aufreiter (http://substance.io)", + "Mathieu Dutour (https://github.com/mathieudutour)" ], "readmeFilename": "README.md", "scripts": { "clean": "gulp clean", "build": "gulp build", "test": "mocha --opts ./mocha.opts test/*.spec.js", + "test-coverage": "NODE_ENV=test nyc mocha --opts ./mocha.opts test/*.spec.js", "test-verbose": "DEBUG=github* npm test", "lint": "gulp lint", "make-docs": "node_modules/.bin/jsdoc -c .jsdoc.json --verbose", - "release": "./release.sh" + "release": "./release.sh", + "codecov": "nyc report --reporter=text-lcov > coverage.lcov && codecov" }, "babel": { "presets": [ @@ -26,33 +29,38 @@ ], "plugins": [ [ - "transform-es2015-modules-umd", - { - "globals": { - "es6-promise": "Promise" - } - } + "add-module-exports", + "transform-es2015-modules-umd" ] ], "env": { "development": { "sourceMaps": "inline" + }, + "test": { + "plugins": [ + "istanbul" + ] } } }, + "nyc": { + "sourceMap": false, + "instrument": false + }, "files": [ - "dist/*", - "lib/*" + "dist/*" ], "dependencies": { - "axios": "^0.10.0", + "axios": "^0.21.1", "debug": "^2.2.0", - "es6-promise": "^3.0.2", "js-base64": "^2.1.9", "utf8": "^2.1.1" }, "devDependencies": { "babel-core": "^6.7.7", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-istanbul": "3.0.0", "babel-plugin-transform-es2015-modules-umd": "^6.5.0", "babel-preset-es2015": "^6.5.0", "babel-register": "^6.7.2", @@ -60,31 +68,34 @@ "browserify": "^13.0.0", "codecov": "^1.0.1", "del": "^2.2.0", + "eslint-config-google": "^0.7.0", + "eslint-plugin-mocha": "^4.7.0", "gulp": "^3.9.0", "gulp-babel": "^6.1.2", - "gulp-eslint": "^2.0.0", - "gulp-jscs": "^3.0.2", + "gulp-eslint": "^3.0.1", + "gulp-jscs": "^4.0.0", "gulp-jscs-stylish": "^1.3.0", "gulp-rename": "^1.2.2", - "gulp-sourcemaps": "^1.6.0", - "gulp-uglify": "^1.5.1", + "gulp-sourcemaps": "^2.2.0", + "gulp-uglify": "^2.0.0", "jsdoc": "^3.4.0", "minami": "^1.1.1", - "mocha": "^2.3.4", + "mocha": "^3.1.2", "must": "^0.13.1", + "nock": "^9.0.2", + "nyc": "9.0.1", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0" }, "repository": { "type": "git", - "url": "git://github.com/michael/github.git" + "url": "git://github.com/github-tools/github.git" }, "keywords": [ "github", "api" ], - "gitHead": "aa8aa3c8cd5ce5240373d4fd1d06a7ab4af41a36", "bugs": { - "url": "https://github.com/michael/github/issues" + "url": "https://github.com/github-tools/github/issues" } } diff --git a/release.sh b/release.sh index 0f57621e..63853dae 100755 --- a/release.sh +++ b/release.sh @@ -1,9 +1,18 @@ #!/bin/bash # This is the automated release script +# guard against stupid +if [ -z "$1" ]; then + echo "You must specify a new version level: [patch, minor, major]"; + exit 1; +fi + # make sure all our dependencies are installed so we can publish docs npm install +# try to build to make sure we don't publish something really broken +npm run build + # bump the version echo "npm version $1" npm version $1 @@ -25,5 +34,6 @@ rm -rf docs/ git checkout gh-pages mv out/* docs/ echo $VERSION >> _data/versions.csv -git co -am "adding docs for v$VERSION" +git add . +git commit -m "adding docs for v$VERSION" git push diff --git a/test/.eslintrc b/test/.eslintrc deleted file mode 100644 index 73d8d9b7..00000000 --- a/test/.eslintrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../.eslintrc", - - "env": { - "mocha": true - } -} diff --git a/test/.eslintrc.yaml b/test/.eslintrc.yaml new file mode 100644 index 00000000..2582b354 --- /dev/null +++ b/test/.eslintrc.yaml @@ -0,0 +1,9 @@ +--- + extends: ../.eslintrc.yaml + plugins: + - mocha + env: + mocha: true + rules: + handle-callback-err: off + mocha/no-exclusive-tests: 2 diff --git a/test/auth.spec.js b/test/auth.spec.js index 5f77d365..00842ee9 100644 --- a/test/auth.spec.js +++ b/test/auth.spec.js @@ -1,7 +1,7 @@ import expect from 'must'; import Github from '../lib/GitHub'; -import testUser from './fixtures/user.json'; +import testUser from './fixtures/user.js'; import {assertSuccessful, assertFailure} from './helpers/callbacks'; describe('Github', function() { @@ -13,7 +13,7 @@ describe('Github', function() { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, - auth: 'basic' + auth: 'basic', }); user = github.getUser(); @@ -25,7 +25,7 @@ describe('Github', function() { }); it('should authenticate and return no errors', function(done) { - user.getNotifications(assertSuccessful(done)); + user.listNotifications(assertSuccessful(done)); }); }); @@ -51,7 +51,7 @@ describe('Github', function() { done(); } catch (e) { try { - if (err && err.request.headers['x-ratelimit-remaining'] === '0') { + if (err && err.response.headers['x-ratelimit-remaining'] === '0') { done(); return; } @@ -70,7 +70,7 @@ describe('Github', function() { github = new Github({ username: testUser.USERNAME, password: 'fake124', - auth: 'basic' + auth: 'basic', }); user = github.getUser(); @@ -82,9 +82,9 @@ describe('Github', function() { }); it('should fail authentication and return err', function(done) { - user.getNotifications(assertFailure(done, function(err) { - expect(err.status).to.be.equal(401, 'Return 401 status for bad auth'); - expect(err.response.data.message).to.equal('Bad credentials'); + user.listNotifications(assertFailure(done, function(err) { + expect(err.response.status).to.be.equal(401, 'Return 401 status for bad auth'); + expect(err.response.data.message).to.equal('Requires authentication'); done(); })); diff --git a/test/dist.spec/index.html b/test/dist.spec/index.html index da5a21de..b0a32d7b 100644 --- a/test/dist.spec/index.html +++ b/test/dist.spec/index.html @@ -12,7 +12,7 @@