diff --git a/README.md b/README.md index a9d8629..e9ba04e 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,21 @@ Implemented Planned * List tags for repos + +## Developing + +1. Make a secrets file: + + ```npm + cp example.config.json config.json + ``` + +2. Make an [new personal access token](https://github.com/settings/tokens/new) on GitHub. +3. Paste your access token into `config.json`. +4. Run: + + ```bash + npm install + ``` + + diff --git a/bin/polymanage-github b/bin/polymanage-github index 3f757ec..d9bda07 100755 --- a/bin/polymanage-github +++ b/bin/polymanage-github @@ -7,25 +7,29 @@ var myPackage = require('../package.json'); var config = require('../config.json'); var GithubManager = require('../src/githubManager'); +GithubManager.auth({ + type: "oauth", + token: config.github.token +}); program .version(myPackage.version) .command('issues [otherRepos...]') .option('-l, --list', 'List all issues across repos') + .option('-a, --assign-unassigned-to ', 'Assign all unassigned issues across repos to a user') + .option('-v, --verbose', 'Verbose') .action(function (repoRegex, otherRepos) { - console.log(repoRegex, otherRepos); - }) + var manager = GithubManager.getGithubManager(repoRegex, otherRepos); + if (this.assignUnassignedTo) { + manager.assignAll(this.assignUnassignedTo, this.verbose); + } + }); program .command('list [otherRepos...]') .option('-p, --properties ', 'List of properties to emit', function(val) { return val.split(' ')}) .option('-v, --verbose', 'Verbose') .action(function (repoRegex, otherRepos) { - GithubManager.auth({ - type: "oauth", - token: config.github.token - }); - var manager = GithubManager.getGithubManager(repoRegex, otherRepos); manager.list(this.properties, this.verbose); }); @@ -33,6 +37,8 @@ program program .command('labels [command]', 'act on github labels for one or more repos'); +program + .command('assign [command]', 'act on github labels for one or more repos'); program.parse(process.argv); diff --git a/example.config.json b/example.config.json new file mode 100644 index 0000000..6a79537 --- /dev/null +++ b/example.config.json @@ -0,0 +1,5 @@ +{ + "github": { + "token": "" + } +} diff --git a/package.json b/package.json index dc5639f..f47dbc0 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.0.1", "description": "A command-line tool for managing polymer repositories", "author": "The Polymer Authors", + "repository": "https://github.com/tjsavage/polymanage", "dependencies": { "cli-color": "^1.0.0", "commander": "^2.8.1", diff --git a/src/githubManager.js b/src/githubManager.js index f3c9e54..c6ae7ec 100755 --- a/src/githubManager.js +++ b/src/githubManager.js @@ -52,6 +52,71 @@ GithubManager.prototype = { } } }); + }, + + assignAll: function assignAll(assignee, verbose) { + this._reposPromise.then(GithubManager._retrieveIssuesFromRepos).then(function(repos) { + var repoPromises = []; + var totalFailures = 0; + var totalSuccesses = 0; + for(var i = 0; i < repos.length; i++) { + var repoPromise = new Promise(function (resolve, reject){ + var repo = repos[i]; + var repoOutput = ""; + repo.successes = 0; + repo.failures = 0; + var issues = repo.issues; + // String like: "= owner/repo (i/n) =" + var displayName = "= " + repo.owner.login + "/" + repo.name + " (" + (i + 1) + "/" + repos.length + ") ="; + if(verbose) { + repoOutput += GithubManager._repeat("=", displayName.length) + "\n"; + repoOutput += displayName + "\n"; + repoOutput += GithubManager._repeat("=", displayName.length) + "\n"; + repoOutput += "Found " + issues.length + " unassigned issues.\n"; + } + var issuePromises = []; + for (var j = 0; j < issues.length; j++) { + var issuePromise = new Promise(function(res, rej) { + var issue = issues[j]; + GithubManager._githubAPI.issues.edit({ + user: repo.owner.login, + repo: repo.name, + number: issue.number, + assignee: assignee + }, function(err, data) { + if (err && err.hasOwnProperty("message") && /Validation Failed/.test(err.message)) { + if (verbose) { + repoOutput += "#" + issue.number + ": fail\n"; + } + repo.failures += 1; + } else { + if (verbose) { + repoOutput += "#" + issue.number + ": assigned to " + assignee + "\n"; + } + repo.successes += 1; + } + res(issue); + }); + }); + issuePromises.push(issue); + } + Promise.all(issuePromises).then(function(issues) { + if (verbose) { + console.log(repoOutput); + console.log("> " + repo.successes + " successes and " + repo.failures + " failures."); + } + totalSuccesses += repo.successes; + totalFailures += repo.failures; + resolve(repo); + }); + }); + repoPromises.push(repoPromise); + } + return Promise.all(repoPromises).then(function(repos) { + console.log("Done: " + totalSuccesses + " successes and " + totalFailures + " failures."); + return repos; + }) + }); } } @@ -100,6 +165,102 @@ GithubManager._retrieveLabelsFromRepos = function(repos) { return repos; }); } + +/* +* @param {Array} repos A list of repo objects to retrieve tags for +* @return {Promise>} A promise that resolves to a list of repo +* objects, with the list of tags added as a top-level property +*/ +GithubManager._retrieveIssuesFromRepos = function(repos) { + var issuePromises = []; + + for (var i = 0; i < repos.length; i++) { + var issuePromise = new Promise(function(resolve, reject) { + var repo = repos[i]; + + GithubManager._githubAPI.issues.repoIssues({ + user: repo.owner.login, + repo: repo.name, + assignee: 'none', + state: 'open', + per_page: 100 + }, GithubManager._promoteError(reject, function(res) { + GithubManager._followPages(function() {}, reject, [], res); + repo["issues"] = res; + resolve(repo); + })); + }); + + issuePromises.push(issuePromise); + } + + return Promise.all(issuePromises).then(function(repos) { + return repos; + }); +} + +/* + * @param {String} pattern A string to repeat + * @param {Number} count The number of times to repeat it + * @return {String} the pattern, repeated count times. + */ +GithubManager._repeat = function(pattern, count) { + if (count < 1) return ''; + var result = ''; + while (count > 1) { + if (count & 1) result += pattern; + count >>= 1, pattern += pattern; + } + return result + pattern; +} + +/* + * @param {function} reject The Promise reject + * @param {function} resolve The Promise resolve + * @return a GithubManager callback function that prints errors. + */ +GithubManager._promoteError = function(reject, resolve) { + return function(err, res) { + if (err) { + if (err.hasOwnProperty("message") && /rate limit exceeded/.test(err.message)) { + rateLimitExceeded = true; + } + + console.error("caught error: %s", err); + reject(err); + } + else { + resolve(res); + } + }; +} + +/* + * @param {function} reject The Promise reject + * @param {function} resolve The Promise resolve + * @param {Array} result an array of JSON responses + * @param {Object} res The response from the response + * @return a GithubManager callback function that prints errors. + */ +GithubManager._followPages = function(resolve, reject, result, res) { + var i; + + for (i = 0; i < res.length; ++i) { + result.push(res[i]); + } + + if (GithubManager._githubAPI.hasNextPage(res)) { + GithubManager._githubAPI.getNextPage(res, GithubManager._promoteError(reject, function(res) { + GithubManager._followPages(resolve, reject, result, res); + })); + } + else { + resolve(result); + } +} + + + /* * @param {Array} repoStrings A list of strings to resolve to repos * @return {Promise} Returns a promise that resolves to a list of repo objects