/**
* The GitHub adapter for issue tracking and pull requests.
* @module github
* @requires module:gitcore
*/
var Gitcore = require('./gitcore.js');
var gitcore = Gitcore(null, null);
exports.query = query;
/**
* Return the GitHub API url for the specified path.
*
* @param {string} path the path of the API request
* @return {string} the full URL for API request
*/
function apiUrl(path) {
return "https://api.github.com/repos/eotl/eotl-mudlib" + path;
}
/**
* Query the GitHub API.
*
* @param {string} query the BitBucket query string
* @param {function} onFulfilled query result will be passed to this
* callback
* @param {function} onRejected errors will be passed to this callback
* @return {boolean} true for valid query string, false
* otherwise
*/
function query(query, onFulfilled, onRejected) {
var onFulfilledWrapper = function(data) {
gitcore.teardownCache();
onFulfilled(data);
};
var onRejectedWrapper = function(data) {
gitcore.teardownCache();
onRejected(data);
};
var runQuery = getRunQuery(query, onFulfilledWrapper, onRejectedWrapper);
if (runQuery) {
gitcore.setupCache();
runQuery();
return true;
}
else {
return false;
}
}
/**
* Get query runner function for specified query and callbacks.
*
* @param {string} query the query to run
* @param {function} onFulfilled query result will be passed to this
* callback
* @param {function} onRejected errors will be passed to this callback
* @return {function} the function which will execute the query
*/
function getRunQuery(query, onFulfilled, onRejected) {
var runQuery;
if (query == "pullRequests") {
runQuery = function() {
getPullRequests(onFulfilled, onRejected)
};
} else if (query.substring(0, 12) == "pullRequest.") {
var pos = query.indexOf(".", 12);
if (pos < 0) {
var id = query.substring(12);
runQuery = function() {
getPullRequest(onFulfilled, onRejected, id)
};
} else {
var id = query.substring(12, pos);
var pos2 = query.indexOf(".", pos + 1);
if (pos2 >= 0) {
var op = query.substring(pos + 1, pos2)
if (op == "review") {
var file = query.substring(pos2 + 1);
runQuery = function() {
getPullReqReview(onFulfilled, onRejected, id, file);
}
}
}
}
}
return runQuery;
}
/**
* Execute API requests for pull request list query.
*
* @param {function} onFulfilled query result will be passed to this callback
* @param {function} onRejected errors will be passed to this callback
*/
function getPullRequests(onFulfilled, onRejected) {
gitcore.apiRequest(onFulfilled,
onRejected,
apiUrl('/pulls'),
marshallPullRequests);
}
/**
* Execute API requests for single pull request query.
*
* @param {function} onFulfilled query result will be passed to this callback
* @param {function} onRejected errors will be passed to this callback
* @param {number} id the pull request id to get
*/
function getPullRequest(onFulfilled, onRejected, id) {
var filesRequest = function(data) {
var diffUrl = data.diff_url;
var filesFulfilled = function(filesData) {
delete data.diff_url;
data.files = filesData;
onFulfilled(data);
};
gitcore.apiRequest(filesFulfilled,
onRejected,
diffUrl,
marshallFiles);
}
var commentsRequest = function(data) {
var commentsFulfilled = function(commentsData) {
data.comments = commentsData;
filesRequest(data);
};
gitcore.apiRequest(commentsFulfilled,
onRejected,
apiUrl('/issues/' + id + '/comments'),
marshallComments);
}
gitcore.apiRequest(commentsRequest,
onRejected,
apiUrl('/pulls/' + id),
marshallPullRequest);
}
/**
* Execute the API requests for a code review on pull request file.
*
* @param {function} onFulfilled query result will be passed to this callback
* @param {function} onRejected errors will be passed to this callback
* @param {number} id the pull request id
* @param {mixed} file the file to review, either the string path
* of the file or the numerical index in the
* file list
*/
function getPullReqReview(onFulfilled, onRejected, id, file) {
var reviewCommentsRequest = function(data) {
var reviewCommentsFulfilled = function(reviewCommentsData) {
data.comments = reviewCommentsData;
onFulfilled(data);
}
var marshallReviewCommentsWrapper = function(buffer) {
return marshallReviewComments(buffer, data.path);
}
gitcore.apiRequest(reviewCommentsFulfilled,
onRejected,
apiUrl('/pulls/' + id + '/comments'),
marshallReviewCommentsWrapper);
}
var diffRequest = function(data) {
var diffUrl = data.diff_url;
delete data.diff_url;
var diffFulfilled = function(diffData) {
delete data.diff_url;
for (var attr in diffData) { data[attr] = diffData[attr]; }
reviewCommentsRequest(data);
};
var marshallDiffWrapper = function(buffer) {
return marshallDiff(buffer, data.path);
}
gitcore.apiRequest(diffFulfilled,
onRejected,
diffUrl,
marshallDiffWrapper);
}
var filesRequest = function(data) {
var diffUrl = data.diff_url;
var filesFulfilled = function(filesData) {
var fileIndex = -1;
var tmp = parseInt(file, 10);
if (file == tmp) {
if ((tmp >= 0) && (tmp < filesData.length)) {
file = filesData[tmp];
fileIndex = tmp;
}
} else {
fileIndex = filesData.indexOf(file);
}
if (fileIndex < 0) {
onRejected(new Error("invalid file for diff"));
} else {
data.path = file;
data.file = fileIndex;
diffRequest(data);
}
};
gitcore.apiRequest(filesFulfilled,
onRejected,
diffUrl,
marshallFiles);
}
gitcore.apiRequest(filesRequest,
onRejected,
apiUrl('/pulls/' + id),
marshallPullRequest);
}
/**
* Marshall the result of the pull requests API call into standard structure.
*
* @param {object} buffer the result of the API call
* @return {object} an object containing information about the pull
* requests
*/
function marshallPullRequests(buffer) {
var data = JSON.parse(buffer);
var out = data.map(function(x) {
return {
"id": x.number,
"state": x.state,
"title": x.title,
"description": x.body,
"created_at": marshallTimestamp(x.created_at),
"updated_at": marshallTimestamp(x.updated_at),
"closed_at": marshallTimestamp(x.closed_at),
"author": x.user.login,
}
});
return out;
}
/**
* Marshall the result of the single pull request API call into standard
* structure.
*
* @param {object} buffer the result of the API call
* @return {object} an object containing information about the pull
* request
*/
function marshallPullRequest(buffer) {
var data = JSON.parse(buffer);
var out = {
"id": data.number,
"state": data.state,
"title": data.title,
"description": data.body,
"created_at": marshallTimestamp(data.created_at),
"updated_at": marshallTimestamp(data.updated_at),
"closed_at": marshallTimestamp(data.closed_at),
"merged_at": marshallTimestamp(data.merged_at),
"author": data.user.login,
"merged": data.merged,
"commits": data.commits,
"additions": data.additions,
"deletions": data.deletions,
"changed_files": data.changed_files,
"diff_url": data.diff_url,
};
return out;
}
/**
* Marshall the result of the pull request comments API call into standard
* structure.
*
* @param {object} buffer the result of the API call
* @return {array} an array of objects containing information about
* each comment
*/
function marshallComments(buffer) {
var data = JSON.parse(buffer);
var out = data.map(function(x) {
return {
"user": x.user.login,
"body": x.body,
"created_at": marshallTimestamp(x.created_at),
"updated_at": marshallTimestamp(x.updated_at),
}
});
return out;
}
/**
* Marshall the result of the pull request diff API call into standard
* structure.
*
* @param {object} buffer the result of the API call
* @return {array} an array of filenames affected by the pull request
*/
function marshallFiles(buffer) {
var files = [ ];
var i = 0;
while (i < buffer.length) {
var j = buffer.indexOf("\n", i);
if (j == -1) { j = buffer.length; }
var line = buffer.substring(i, j);
if (line.substring(0, 6) == "+++ b/") {
files.push(line.substr(6));
}
i = j + 1;
}
return files;
}
/**
* Marshall the result of a unified diff into single diff for specified file.
*
* @param {object} buffer the result of the API call
* @param {string} path the path of the file to extract from diff
* @return {object} an object containing the file's diff
*/
function marshallDiff(buffer, path) {
var out = { };
var i = 0;
var lines = null;
while (i < buffer.length) {
var j = buffer.indexOf("\n", i);
if (j == -1) { j = buffer.length; }
var line = buffer.substring(i, j);
if (lines) {
var token = "diff --git ";
if (line.substring(0, token.length) == token) {
break;
} else {
lines.push(line);
}
} else {
var token = "diff --git a/" + path + " b/" + path;
if (line.substring(0, token.length) == token) {
lines = [ line ];
}
}
i = j + 1;
}
out.diff = lines;
return out;
}
/**
* Marshall the result of a review comments API call into standard structure.
*
* @param {object} buffer the result of the API call
* @param {string} path the path of the file for which to get review
* comments
* @return {object} an object with diff position (line number) as
* properties and comment info objects as values
*/
function marshallReviewComments(buffer, path) {
var data = JSON.parse(buffer);
var out = { };
data.forEach(function(comment) {
if (comment.path == path) {
var pos = comment.position;
if (!(pos in out)) {
out[pos] = [ ];
}
out[pos].push({
"user": comment.user.login,
"body": comment.body,
"created_at": marshallTimestamp(comment.created_at),
"updated_at": marshallTimestamp(comment.updated_at),
});
}
});
return out;
}
/**
* Marshall a timestamp back into unixtime.
*
* @param {string} timestamp a timestamp from API call
* @return {number} the unixtime of that timestamp
*/
function marshallTimestamp(timestamp) {
return Date.parse(timestamp) / 1000;
}