+ 'use strict';
+/**
+ * @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';
+import Utf8 from 'utf8';
+import {Base64} from 'js-base64';
+import debug from 'debug';
+const log = debug('github:repository');
+
+/**
+ * Respository encapsulates the functionality to create, query, and modify files.
+ */
+class Repository extends Requestable {
+ /**
+ * Create a Repository.
+ * @param {string} fullname - the full name of the repository
+ * @param {Requestable.auth} [auth] - information required to authenticate to Github
+ * @param {string} [apiBase=https://api.github.com] - the base Github API URL
+ */
+ constructor(fullname, auth, apiBase) {
+ super(auth, apiBase);
+ this.__fullname = fullname;
+ this.__currentTree = {
+ branch: null,
+ sha: null
+ };
+ }
+
+ /**
+ * Get a reference
+ * @see https://developer.github.com/v3/git/refs/#get-a-reference
+ * @param {string} ref - the reference to get
+ * @param {Requestable.callback} [cb] - will receive the reference's refSpec or a list of refSpecs that match `ref`
+ * @return {Promise} - the promise for the http request
+ */
+ getRef(ref, cb) {
+ return this._request('GET', `/repos/${this.__fullname}/git/refs/${ref}`, null, cb);
+ }
+
+ /**
+ * Create a reference
+ * @see https://developer.github.com/v3/git/refs/#create-a-reference
+ * @param {Object} options - the object describing the ref
+ * @param {Requestable.callback} [cb] - will receive the ref
+ * @return {Promise} - the promise for the http request
+ */
+ createRef(options, cb) {
+ return this._request('POST', `/repos/${this.__fullname}/git/refs`, options, cb);
+ }
+
+ /**
+ * Delete a reference
+ * @see https://developer.github.com/v3/git/refs/#delete-a-reference
+ * @param {string} ref - the name of the ref to delte
+ * @param {Requestable.callback} [cb] - will receive true if the request is successful
+ * @return {Promise} - the promise for the http request
+ */
+ deleteRef(ref, cb) {
+ return this._request('DELETE', `/repos/${this.__fullname}/git/refs/${ref}`, null, cb);
+ }
+
+ /**
+ * Delete a repository
+ * @see https://developer.github.com/v3/repos/#delete-a-repository
+ * @param {Requestable.callback} [cb] - will receive true if the request is successful
+ * @return {Promise} - the promise for the http request
+ */
+ deleteRepo(cb) {
+ return this._request('DELETE', `/repos/${this.__fullname}`, null, cb);
+ }
+
+ /**
+ * List the tags on a repository
+ * @see https://developer.github.com/v3/repos/#list-tags
+ * @param {Requestable.callback} [cb] - will receive the tag data
+ * @return {Promise} - the promise for the http request
+ */
+ listTags(cb) {
+ return this._request('GET', `/repos/${this.__fullname}/tags`, null, cb);
+ }
+
+ /**
+ * List the open pull requests on the repository
+ * @see https://developer.github.com/v3/pulls/#list-pull-requests
+ * @param {Object} options - options to filter the search
+ * @param {Requestable.callback} [cb] - will receive the list of PRs
+ * @return {Promise} - the promise for the http request
+ */
+ listPullRequests(options, cb) {
+ options = options || {};
+ return this._request('GET', `/repos/${this.__fullname}/pulls`, options, cb);
+ }
+
+ /**
+ * Get information about a specific pull request
+ * @see https://developer.github.com/v3/pulls/#get-a-single-pull-request
+ * @param {number} number - the PR you wish to fetch
+ * @param {Requestable.callback} [cb] - will receive the PR from the API
+ * @return {Promise} - the promise for the http request
+ */
+ getPullRequest(number, cb) {
+ return this._request('GET', `/repos/${this.__fullname}/pulls/${number}`, null, cb);
+ }
+
+ /**
+ * Compare two branches/commits/repositories
+ * @see https://developer.github.com/v3/repos/commits/#compare-two-commits
+ * @param {string} base - the base commit
+ * @param {string} head - the head commit
+ * @param {Requestable.callback} cb - will receive the comparison
+ * @return {Promise} - the promise for the http request
+ */
+ compareBranches(base, head, cb) {
+ return this._request('GET', `/repos/${this.__fullname}/compare/${base}...${head}`, null, cb);
+ }
+
+ /**
+ * List all the branches for the repository
+ * @see https://developer.github.com/v3/repos/#list-branches
+ * @param {Requestable.callback} cb - will receive the list of branches
+ * @return {Promise} - the promise for the http request
+ */
+ listBranches(cb) {
+ return this._request('GET', `/repos/${this.__fullname}/branches`, null, cb);
+ }
+
+ /**
+ * Get a raw blob from the repository
+ * @see https://developer.github.com/v3/git/blobs/#get-a-blob
+ * @param {string} sha - the sha of the blob to fetch
+ * @param {Requestable.callback} cb - will receive the blob from the API
+ * @return {Promise} - the promise for the http request
+ */
+ getBlob(sha, cb) {
+ return this._request('GET', `/repos/${this.__fullname}/git/blobs/${sha}`, null, cb, 'raw');
+ }
+
+ /**
+ * Get a commit from the repository
+ * @see https://developer.github.com/v3/repos/commits/#get-a-single-commit
+ * @param {string} sha - the sha for the commit to fetch
+ * @param {Requestable.callback} cb - will receive the commit data
+ * @return {Promise} - the promise for the http request
+ */
+ getCommit(sha, cb) {
+ return this._request('GET', `/repos/${this.__fullname}/git/commits/${sha}`, null, cb);
+ }
+
+ /**
+ * 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 {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
+ * @param {(Date|string)} [options.since] - only commits after this date will be returned
+ * @param {(Date|string)} [options.until] - only commits before this date will be returned
+ * @param {Requestable.callback} cb - will receive the list of commits found matching the criteria
+ * @return {Promise} - the promise for the http request
+ */
+ listCommits(options, 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);
+ }
+
+ /**
+ * 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
+ * @param {string} [branch] - the branch to look in, or the repository's default branch if omitted
+ * @param {string} path - the path of the file or directory
+ * @param {Requestable.callback} cb - will receive a description of the requested object, including a `SHA` property
+ * @return {Promise} - the promise for the http request
+ */
+ getSha(branch, path, cb) {
+ branch = branch ? `?ref=${branch}` : '';
+ return this._request('GET', `/repos/${this.__fullname}/contents/${path}${branch}`, null, cb);
+ }
+
+ /**
+ * List the commit statuses for a particular sha, branch, or tag
+ * @see https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
+ * @param {string} sha - the sha, branch, or tag to get statuses for
+ * @param {Requestable.callback} cb - will receive the list of statuses
+ * @return {Promise} - the promise for the http request
+ */
+ listStatuses(sha, cb) {
+ return this._request('GET', `/repos/${this.__fullname}/commits/${sha}/statuses`, null, cb);
+ }
+
+ /**
+ * Get a description of a git tree
+ * @see https://developer.github.com/v3/git/trees/#get-a-tree
+ * @param {string} treeSHA - the SHA of the tree to fetch
+ * @param {Requestable.callback} cb - will receive the callback data
+ * @return {Promise} - the promise for the http request
+ */
+ getTree(treeSHA, cb) {
+ return this._request('GET', `/repos/${this.__fullname}/git/trees/${treeSHA}`, null, cb);
+ }
+
+ /**
+ * Create a blob
+ * @see https://developer.github.com/v3/git/blobs/#create-a-blob
+ * @param {(string|Buffer|Blob)} content - the content to add to the repository
+ * @param {Requestable.callback} cb - will receive the details of the created blob
+ * @return {Promise} - the promise for the http request
+ */
+ createBlob(content, cb) {
+ let postBody = this._getContentObject(content);
+
+ log('sending content', postBody);
+ return this._request('POST', `/repos/${this.__fullname}/git/blobs`, postBody, cb);
+ }
+
+ _getContentObject(content) {
+ if (typeof content === 'string') {
+ log('contet is a string');
+ return {
+ content: Utf8.encode(content),
+ 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'
+ };
+ } else if (typeof Blob !== 'undefined' && content instanceof Blob) {
+ log('We appear to be in the browser');
+ return {
+ content: Base64.encode(content),
+ encoding: 'base64'
+ };
+ } else {
+ 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)');
+ }
+ }
+
+ /**
+ * Update a tree in Git
+ * @see https://developer.github.com/v3/git/trees/#create-a-tree
+ * @param {string} baseTreeSHA - the SHA of the tree to update
+ * @param {string} path - the path for the new file
+ * @param {string} blobSHA - the SHA for the blob to put at `path`
+ * @param {Requestable.callback} cb - will receive the new tree that is created
+ * @return {Promise} - the promise for the http request
+ * @deprecated use {@link Repository#postTree} instead
+ */
+ updateTree(baseTreeSHA, path, blobSHA, cb) {
+ let newTree = {
+ 'base_tree': baseTreeSHA,
+ 'tree': [{
+ path: path,
+ sha: blobSHA,
+ mode: '100644',
+ type: 'blob'
+ }]
+ };
+
+ return this._request('POST', `/repos/${this.__fullname}/git/trees`, newTree, cb);
+ }
+
+ /**
+ * Create a new tree in git
+ * @see https://developer.github.com/v3/git/trees/#create-a-tree
+ * @param {Object} tree - the tree to create
+ * @param {string} baseSHA - the root sha of the tree
+ * @param {Requestable.callback} cb - will receive the new tree that is created
+ * @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
+ }
+
+ /**
+ * 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} message - the commit message
+ * @param {Function} cb - will receive the commit that is created
+ * @return {Promise} - the promise for the http request
+ */
+ commit(parent, tree, message, cb) {
+ let data = {
+ message,
+ tree,
+ parents: [parent]
+ };
+
+ return this._request('POST', `/repos/${this.__fullname}/git/commits`, data, cb)
+ .then((response) => {
+ this.__currentTree.sha = response.sha; // Update latest commit
+ return response;
+ });
+ }
+
+ /**
+ * Update a ref
+ * @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
+ * @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);
+ }
+
+ /**
+ * Get information about the repository
+ * @see https://developer.github.com/v3/repos/#get
+ * @param {Function} cb - will receive the information about the repository
+ * @return {Promise} - the promise for the http request
+ */
+ getDetails(cb) {
+ return this._request('GET', `/repos/${this.__fullname}`, null, cb);
+ }
+
+ /**
+ * List the contributors to the repository
+ * @see https://developer.github.com/v3/repos/#list-contributors
+ * @param {Function} cb - will receive the list of contributors
+ * @return {Promise} - the promise for the http request
+ */
+ getContributors(cb) {
+ return this._request('GET', `/repos/${this.__fullname}/stats/contributors`, null, cb);
+ }
+
+ /**
+ * 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
+ * @return {Promise} - the promise for the http request
+ */
+ getCollaborators(cb) {
+ return this._request('GET', `/repos/${this.__fullname}/collaborators`, null, cb);
+ }
+
+ /**
+ * 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
+ * @return {Promise} - the promise for the http request {Boolean} [description]
+ */
+ isCollaborator(username, cb) {
+ return this._request('GET', `/repos/${this.__fullname}/collaborators/${username}`, null, cb);
+ }
+
+ /**
+ * Get the contents of a repository
+ * @see https://developer.github.com/v3/repos/contents/#get-contents
+ * @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
+ * @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);
+ }
+
+ /**
+ * 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
+ * @return {Promise} - the promise for the http request
+ */
+ fork(cb) {
+ return this._request('POST', `/repos/${this.__fullname}/forks`, 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
+ * @return {Promise} - the promise for the http request
+ */
+ listForks(cb) {
+ return this._request('GET', `/repos/${this.__fullname}/forks`, null, cb);
+ }
+
+ /**
+ * 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
+ * @return {Promise} - the promise for the http request
+ */
+ createBranch(oldBranch, newBranch, cb) {
+ if (typeof newBranch === 'function') {
+ cb = newBranch;
+ newBranch = oldBranch;
+ oldBranch = 'master';
+ }
+
+ return this.getRef(`heads/${oldBranch}`)
+ .then((response) => {
+ let sha = response.data.object.sha;
+ return this.createRef({sha, ref: `refs/heads/${newBranch}`}, cb);
+ });
+ }
+
+ /**
+ * 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
+ * @return {Promise} - the promise for the http request
+ */
+ createPullRequest(options, cb) {
+ return this._request('POST', `/repos/${this.__fullname}/pulls`, 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
+ * @return {Promise} - the promise for the http request
+ */
+ listHooks(cb) {
+ return this._request('GET', `/repos/${this.__fullname}/hooks`, null, cb);
+ }
+
+ /**
+ * 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
+ * @return {Promise} - the promise for the http request
+ */
+ getHook(id, cb) {
+ return this._request('GET', `/repos/${this.__fullname}/hooks/${id}`, null, cb);
+ }
+
+ /**
+ * 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
+ * @return {Promise} - the promise for the http request
+ */
+ createHook(options, cb) {
+ return this._request('POST', `/repos/${this.__fullname}/hooks`, options, cb);
+ }
+
+ /**
+ * Edit an existing webhook
+ * @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
+ * @return {Promise} - the promise for the http request
+ */
+ updateHook(id, options, cb) {
+ return this._request('PATCH', `/repos/${this.__fullname}/hooks/${id}`, options, cb);
+ }
+
+ /**
+ * 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
+ * @return {Promise} - the promise for the http request
+ */
+ deleteHook(id, cb) {
+ return this._request('DELETE', `${this.__repoPath}/hooks/${id}`, null, cb);
+ }
+
+ /**
+ * Delete a file from a branch
+ * @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
+ * @return {Promise} - the promise for the http request
+ */
+ deleteFile(branch, path, cb) {
+ this.getSha(branch, path)
+ .then((response) => {
+ const deleteCommit = {
+ message: `Delete the file at '${path}'`,
+ sha: response.data.sha,
+ 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) {
+ ref.path = newPath;
+ }
+
+ if (ref.type === 'tree') {
+ delete ref.sha;
+ }
+ });
+
+ this.postTree(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);
+ }
+
+ this.getRef(`heads/${branch}`, function(err, sha) {
+ this.__currentTree.branch = branch;
+ this.__currentTree.sha = sha;
+ cb(err, sha);
+ });
+ }
+
+ /**
+ * Write a file to the repository
+ * @see https://developer.github.com/v3/repos/contents/#update-a-file
+ * @param {string} branch - the name of the branch
+ * @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.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
+ * @return {Promise} - the promise for the http request
+ */
+ writeFile(branch, path, content, message, options, cb) {
+ if (typeof options === 'function') {
+ cb = options;
+ options = {};
+ }
+ let filePath = path ? encodeURI(path) : '';
+ let shouldEncode = options.encode !== false;
+ let commit = {
+ branch,
+ message,
+ author: options.author,
+ committer: options.committer,
+ content: shouldEncode ? Base64.encode(content) : content
+ };
+
+ return this.getSha(branch, filePath)
+ .then((response) => {
+ commit.sha = response.data.sha;
+ return this._request('PUT', `/repos/${this.__fullname}/contents/${filePath}`, commit, cb);
+ }, () => {
+ return this._request('PUT', `/repos/${this.__fullname}/contents/${filePath}`, commit, cb);
+ });
+ }
+
+ /**
+ * Check if a repository is starred by you
+ * @see https://developer.github.com/v3/activity/starring/#check-if-you-are-starring-a-repository
+ * @param {Requestable.callback} cb - will receive true if the repository is starred and false if the repository
+ * is not starred
+ * @return {Promise} - the promise for the http request {Boolean} [description]
+ */
+ isStarred(cb) {
+ return this._request204or404(`/user/starred/${this.__fullname}`, null, cb);
+ }
+
+ /**
+ * Star a repository
+ * @see https://developer.github.com/v3/activity/starring/#star-a-repository
+ * @param {Requestable.callback} cb - will receive true if the repository is starred
+ * @return {Promise} - the promise for the http request
+ */
+ star(cb) {
+ return this._request('PUT', `/user/starred/${this.__fullname}`, null, cb);
+ }
+
+ /**
+ * Unstar a repository
+ * @see https://developer.github.com/v3/activity/starring/#unstar-a-repository
+ * @param {Requestable.callback} cb - will receive true if the repository is unstarred
+ * @return {Promise} - the promise for the http request
+ */
+ unstar(cb) {
+ return this._request('DELETE', `/user/starred/${this.__fullname}`, null, cb);
+ }
+
+ /**
+ * Create a new release
+ * @see https://developer.github.com/v3/repos/releases/#create-a-release
+ * @param {Object} options - the description of the release
+ * @param {Requestable.callback} cb - will receive the newly created release
+ * @return {Promise} - the promise for the http request
+ */
+ createRelease(options, cb) {
+ return this._request('POST', `/repos/${this.__fullname}/releases`, options, cb);
+ }
+
+ /**
+ * Edit a release
+ * @see https://developer.github.com/v3/repos/releases/#edit-a-release
+ * @param {string} id - the id of the release
+ * @param {Object} options - the description of the release
+ * @param {Requestable.callback} cb - will receive the modified release
+ * @return {Promise} - the promise for the http request
+ */
+ updateRelease(id, options, cb) {
+ return this._request('PATCH', `/repos/${this.__fullname}/releases/${id}`, options, cb);
+ }
+
+ /**
+ * 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 {Requestable.callback} cb - will receive the release information
+ * @return {Promise} - the promise for the http request
+ */
+ getRelease(id, cb) {
+ return this._request('GET', `/repos/${this.__fullname}/releases/${id}`, null, cb);
+ }
+
+ /**
+ * Delete a release
+ * @see https://developer.github.com/v3/repos/releases/#delete-a-release
+ * @param {string} id - the release to be deleted
+ * @param {Requestable.callback} cb - will receive true if the operation is successful
+ * @return {Promise} - the promise for the http request
+ */
+ deleteRelease(id, cb) {
+ return this._request('DELETE', `/repos/${this.__fullname}/releases/${id}`, null, cb);
+ }
+}
+
+module.exports = Repository;
+
+
+