├── .gitignore ├── index.js ├── nestor.json ├── package.json └── scripts ├── error.js ├── github-activity.js ├── github-committers.js ├── github-issues.js ├── github-merge.js ├── github-pulls.js ├── github-search.js └── github-status.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = function(robot) { 4 | robot.loadFile(path.resolve(__dirname, "scripts"), "github-activity.js"); 5 | robot.loadFile(path.resolve(__dirname, "scripts"), "github-committers.js"); 6 | robot.loadFile(path.resolve(__dirname, "scripts"), "github-issues.js"); 7 | robot.loadFile(path.resolve(__dirname, "scripts"), "github-merge.js"); 8 | robot.loadFile(path.resolve(__dirname, "scripts"), "github-pulls.js"); 9 | robot.loadFile(path.resolve(__dirname, "scripts"), "github-search.js"); 10 | robot.loadFile(path.resolve(__dirname, "scripts"), "github-status.js"); 11 | }; 12 | -------------------------------------------------------------------------------- /nestor.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Github", 3 | "permalink": "github", 4 | "description": "Manage Github with Nestor", 5 | "environment_keys": { 6 | "HUBOT_GITHUB_TOKEN": { 7 | "required": true, 8 | "mode": "oauth", 9 | "extract_from": "token" 10 | }, 11 | "HUBOT_GITHUB_USER": { 12 | "required": false, 13 | "mode": "user" 14 | }, 15 | "HUBOT_GITHUB_REPO": { 16 | "required": false, 17 | "mode": "user" 18 | }, 19 | "HUBOT_GITHUB_API": { 20 | "required": false, 21 | "mode": "user" 22 | }, 23 | "HUBOT_GITHUB_ORG": { 24 | "required": false, 25 | "mode": "user" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github", 3 | "version": "0.0.1", 4 | "description": "Control Github from Nestor", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/asknestor-apps/github.git" 12 | }, 13 | "keywords": [ 14 | "slack", 15 | "hubot", 16 | "nestor" 17 | ], 18 | "author": "Arun Thampi", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/asknestor-apps/github/issues" 22 | }, 23 | "homepage": "https://github.com/asknestor-apps/github#readme", 24 | "dependencies": { 25 | "date-utils": "^1.2.18", 26 | "githubot": "^1.0.0", 27 | "nestorbot": "0.35.0", 28 | "underscore": "^1.8.3", 29 | "underscore.string": "^3.3.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scripts/error.js: -------------------------------------------------------------------------------- 1 | module.exports = function(response, msg, done) { 2 | msg.send(msg.newRichResponse({ 3 | title: "Oops, the Github API returned with an error", 4 | color: 'danger', 5 | fields: [ 6 | { 7 | "title": "Response Code", 8 | "value": response.statusCode, 9 | "short": true 10 | }, 11 | { 12 | "title": "Error", 13 | "value": response.error, 14 | "short": true 15 | } 16 | ] 17 | }), done); 18 | }; 19 | -------------------------------------------------------------------------------- /scripts/github-activity.js: -------------------------------------------------------------------------------- 1 | require('date-utils'); 2 | var githubErrorHandler = require('./error'); 3 | 4 | module.exports = function(robot) { 5 | var github = require("githubot")(robot); 6 | 7 | robot.respond(/github repo show (.*)$/i, { suggestions: ["github repo show "] }, function(msg, done) { 8 | var repo = github.qualified_repo(msg.match[1]); 9 | var base_url = process.env.HUBOT_GITHUB_API || 'https://api.github.com'; 10 | var url = base_url + "/repos/" + repo + "/commits"; 11 | 12 | github.handleErrors(function(response) { githubErrorHandler(response, msg, done); }); 13 | 14 | github.get(url, function(commits) { 15 | if (commits.message) { 16 | msg.send("Achievement unlocked: [NEEDLE IN A HAYSTACK] repository " + commits.message + "!", done); 17 | } else if (commits.length === 0) { 18 | msg.send("Achievement unlocked: [LIKE A BOSS] no commits found!", done); 19 | } else { 20 | msg.send("https://github.com/" + repo).then(function() { 21 | var results = []; 22 | for (var i = 0; i < Math.min(commits.length, 5); i++) { 23 | var c = commits[i]; 24 | var d = new Date(Date.parse(c.commit.committer.date)).toFormat("DD/MM HH24:MI"); 25 | results.push("[" + d + " -> " + c.commit.committer.name + "] " + c.commit.message); 26 | } 27 | msg.send(results, done); 28 | }); 29 | } 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /scripts/github-committers.js: -------------------------------------------------------------------------------- 1 | var githubErrorHandler = require('./error'); 2 | 3 | module.exports = function(robot) { 4 | var github = require("githubot")(robot); 5 | 6 | var read_contributors = function(msg, done, response_handler) { 7 | var repo = github.qualified_repo(msg.match[1]); 8 | var base_url = process.env.HUBOT_GITHUB_API || 'https://api.github.com'; 9 | var url = base_url + "/repos/" + repo + "/contributors"; 10 | 11 | github.handleErrors(function(response) { githubErrorHandler(response, msg, done); }); 12 | 13 | github.get(url, function(commits) { 14 | if (commits.message) { 15 | msg.send("Achievement unlocked: [NEEDLE IN A HAYSTACK] repository " + commits.message + "!", done); 16 | } else if (commits.length === 0) { 17 | msg.send("Achievement unlocked: [LIKE A BOSS] no commits found!", done); 18 | } else { 19 | response_handler(commits); 20 | } 21 | }); 22 | }; 23 | 24 | robot.respond(/github repo committers (.*)$/i, { suggestions: ["github repo committers "] }, function(msg, done) { 25 | read_contributors(msg, done, function(commits) { 26 | var max_length = 20; 27 | var results = []; 28 | 29 | for (var i = 0; i < Math.min(commits.length, max_length); i++) { 30 | commit = commits[i]; 31 | results.push("[" + commit.login + "] Contributions: " + commit.contributions); 32 | } 33 | 34 | msg.send(results, done); 35 | }); 36 | }); 37 | 38 | robot.respond(/github repo top-committers? (.*)$/i, { suggestions: ["github repo top-committers "] }, function(msg, done) { 39 | read_contributors(msg, done, function(commits) { 40 | var top_commiter = null; 41 | for (var i = 0; i < commits.length; i++) { 42 | var commit = commits[i]; 43 | if (top_commiter === null) { 44 | top_commiter = commit; 45 | } 46 | if (commit.contributions > top_commiter.contributions) { 47 | top_commiter = commit; 48 | } 49 | } 50 | msg.send("[" + top_commiter.login + "] " + top_commiter.contributions + " :trophy:", done); 51 | }); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /scripts/github-issues.js: -------------------------------------------------------------------------------- 1 | var _ = require("underscore"); 2 | var _s = require("underscore.string"); 3 | var githubErrorHandler = require('./error'); 4 | 5 | var ASK_REGEX = /github show\s(me)?\s*(\d+|\d+\sof)?\s*(\S+'s|my)?\s*(\S+)?\s*issues\s*(for\s\S+)?\s*(about\s.+)?/i; 6 | 7 | var parse_criteria = function(message) { 8 | var assignee, label, limit, me, query, ref, repo; 9 | ref = message.match(ASK_REGEX).slice(1), me = ref[0], limit = ref[1], assignee = ref[2], label = ref[3], repo = ref[4], query = ref[5]; 10 | return { 11 | me: me, 12 | limit: limit != null ? parseInt(limit.replace(" of", "")) : void 0, 13 | assignee: assignee != null ? assignee.replace("'s", "") : void 0, 14 | label: label, 15 | repo: repo != null ? repo.replace("for ", "") : void 0, 16 | query: query != null ? query.replace("about ", "") : void 0 17 | }; 18 | }; 19 | 20 | var filter_issues = function(issues, arg) { 21 | var limit, query; 22 | limit = arg.limit, query = arg.query; 23 | if (query != null) { 24 | issues = _.filter(issues, function(i) { 25 | return _.any([i.body, i.title], function(s) { 26 | return _s.include(s.toLowerCase(), query.toLowerCase()); 27 | }); 28 | }); 29 | } 30 | if (limit != null) { 31 | issues = _.first(issues, limit); 32 | } 33 | return issues; 34 | }; 35 | 36 | var complete_assignee = function(msg, name) { 37 | var resolve; 38 | if (name === "my") { 39 | name = msg.message.user.name; 40 | } 41 | name = name.replace("@", ""); 42 | resolve = function(n) { 43 | return process.env["HUBOT_GITHUB_USER_" + (n.replace(/\s/g, '_').toUpperCase())]; 44 | }; 45 | return resolve(name) || resolve(name.split(' ')[0]) || name; 46 | }; 47 | 48 | module.exports = function(robot) { 49 | var github = require("githubot")(robot); 50 | 51 | robot.respond(ASK_REGEX, { suggestions: ["github show [me] issues "] }, function(msg, done) { 52 | var base_url = process.env.HUBOT_GITHUB_API || 'https://api.github.com'; 53 | var criteria = parse_criteria(msg.message.text); 54 | criteria.repo = github.qualified_repo(criteria.repo); 55 | if (criteria.assignee != null) { 56 | criteria.assignee = complete_assignee(msg, criteria.assignee); 57 | } 58 | var query_params = { 59 | state: "open", 60 | sort: "created" 61 | }; 62 | if (criteria.label != null) { 63 | query_params.labels = criteria.label; 64 | } 65 | if (criteria.assignee != null) { 66 | query_params.assignee = criteria.assignee; 67 | } 68 | 69 | github.handleErrors(function(response) { githubErrorHandler(response, msg, done); }); 70 | 71 | github.get(base_url + "/repos/" + criteria.repo + "/issues", query_params, function(issues) { 72 | var assignee, issue, j, label, labels, len, results; 73 | var issues = filter_issues(issues, criteria); 74 | if (_.isEmpty(issues)) { 75 | msg.send("No issues found.", done); 76 | } else { 77 | results = []; 78 | for (j = 0, len = issues.length; j < len; j++) { 79 | issue = issues[j]; 80 | labels = ((function() { 81 | var k, len1, ref, results1; 82 | ref = issue.labels; 83 | results1 = []; 84 | for (k = 0, len1 = ref.length; k < len1; k++) { 85 | label = ref[k]; 86 | results1.push("#" + label.name); 87 | } 88 | return results1; 89 | })()).join(" "); 90 | assignee = issue.assignee ? " (" + issue.assignee.login + ")" : ""; 91 | results.push("[" + issue.number + "] " + issue.title + " " + labels + assignee + " = " + issue.html_url); 92 | } 93 | 94 | msg.send(results, done); 95 | } 96 | }); 97 | }); 98 | }; 99 | -------------------------------------------------------------------------------- /scripts/github-merge.js: -------------------------------------------------------------------------------- 1 | var githubErrorHandler = require('./error'); 2 | 3 | module.exports = function(robot) { 4 | var github = require("githubot")(robot); 5 | 6 | robot.respond(/github merge ([-_\.0-9a-zA-Z]+\/[-_\.0-9a-zA-Z]+)(\/([-_\.a-zA-z0-9\/]+))? into ([-_\.a-zA-z0-9\/]+)$/i, { suggestions: ["github merge project_name/ into "] }, function(msg, done) { 7 | var app = msg.match[1]; 8 | var head = msg.match[3] || "master"; 9 | var base = msg.match[4]; 10 | 11 | github.handleErrors(function(response) { githubErrorHandler(response, msg, done); }); 12 | 13 | github.branches(app).merge(head, { 14 | base: base 15 | }, function(merge) { 16 | if (merge.message) { 17 | msg.send(merge.message, done); 18 | } else { 19 | msg.send("Merged the crap out of it", done); 20 | } 21 | }); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /scripts/github-pulls.js: -------------------------------------------------------------------------------- 1 | var githubErrorHandler = require('./error'); 2 | 3 | module.exports = function(robot) { 4 | var github = require("githubot")(robot); 5 | var url_api_base = ""; 6 | 7 | if ((url_api_base = process.env.HUBOT_GITHUB_API) == null) { 8 | url_api_base = "https://api.github.com"; 9 | } 10 | 11 | robot.respond(/github show\s+(me\s+)?(.*)\s+pulls(\s+with\s+)?(.*)?/i, { suggestions: ["github show me pulls"] }, function(msg, done) { 12 | var filter_reg_exp; 13 | var repo = github.qualified_repo(msg.match[2]); 14 | 15 | if (msg.match[3]) { 16 | filter_reg_exp = new RegExp(msg.match[4], "i"); 17 | } 18 | 19 | github.handleErrors(function(response) { githubErrorHandler(response, msg, done); }); 20 | 21 | github.get(url_api_base + "/repos/" + repo + "/pulls", function(pulls) { 22 | var summary = ""; 23 | 24 | if (pulls.length === 0) { 25 | summary = "Achievement unlocked: open pull requests zero!"; 26 | } else { 27 | var filtered_result = []; 28 | for (var i = 0; i < pulls.length; i++) { 29 | var pull = pulls[i]; 30 | if (filter_reg_exp && pull.title.search(filter_reg_exp) < 0) { 31 | continue; 32 | } 33 | filtered_result.push(pull); 34 | } 35 | if (filtered_result.length === 0) { 36 | summary = "There's no open pull request for " + repo + " matching your filter!"; 37 | } else if (filtered_result.length === 1) { 38 | summary = "There's only one open pull request for " + repo + ":"; 39 | } else { 40 | summary = "I found " + filtered_result.length + " open pull requests for " + repo + ":"; 41 | } 42 | for (i = 0; i < filtered_result.length; i++) { 43 | var pull = filtered_result[i]; 44 | summary = summary + ("\n" + pull.title + " - " + pull.user.login + ": " + pull.html_url); 45 | } 46 | } 47 | 48 | msg.send(summary, done); 49 | }); 50 | }); 51 | 52 | robot.respond(/github show\s+(me\s+)?org\-pulls(\s+for\s+)?(.*)?/i, { suggestions: ["github show me org-pulls"] }, function(msg, done) { 53 | var org_name = msg.match[3] || process.env.HUBOT_GITHUB_ORG; 54 | 55 | if (!org_name) { 56 | msg.send("No organization specified, please provide one or set HUBOT_GITHUB_ORG accordingly.", done); 57 | return; 58 | } 59 | 60 | org_name = org_name.trim(); 61 | var url = url_api_base + "/orgs/" + org_name + "/issues?filter=all&state=open&per_page=100"; 62 | 63 | github.handleErrors(function(response) { githubErrorHandler(response, msg, done); }); 64 | 65 | github.get(url, function(issues) { 66 | var summary = ""; 67 | 68 | if (issues.length === 0) { 69 | summary = "Achievement unlocked: open pull requests zero!"; 70 | } else { 71 | var filtered_result = []; 72 | for (var i = 0; i < issues.length; i++) { 73 | var issue = issues[i]; 74 | if (issue.pull_request != null) { 75 | filtered_result.push(issue); 76 | } 77 | } 78 | 79 | if (filtered_result.length === 0) { 80 | summary = "Achievement unlocked: open pull requests zero!"; 81 | } else if (filtered_result.length === 1) { 82 | summary = "There's only one open pull request for " + org_name + ":"; 83 | } else { 84 | summary = "I found " + filtered_result.length + " open pull requests for " + org_name + ":"; 85 | } 86 | for (i = 0; i < filtered_result.length; i++) { 87 | var issue = filtered_result[i]; 88 | summary = summary + ("\n" + issue.repository.name + ": " + issue.title + " (" + issue.user.login + ") -> " + issue.pull_request.html_url); 89 | } 90 | } 91 | 92 | msg.send(summary, done); 93 | }); 94 | }); 95 | }; 96 | -------------------------------------------------------------------------------- /scripts/github-search.js: -------------------------------------------------------------------------------- 1 | var githubErrorHandler = require('./error'); 2 | 3 | module.exports = function(robot) { 4 | var github = require('githubot')(robot); 5 | 6 | robot.respond(/github search (.+\/[^\s]+)\s+(.+)/i, { suggestions: ["github search "] }, function(msg, done) { 7 | var e, error, in_repo, query, repo, repostr; 8 | try { 9 | var repo = msg.match[1]; 10 | var query = msg.match[2].trim(); 11 | var repostr = ''; 12 | var in_repo = ''; 13 | 14 | if (repo) { 15 | repostr = "+repo:" + repo; 16 | in_repo = " in repo " + repo; 17 | } 18 | 19 | github.handleErrors(function(response) { githubErrorHandler(response, msg, done); }); 20 | 21 | github.get("search/code?q=" + (encodeURIComponent(query)) + repostr + "&sort=indexed", function(data) { 22 | var broken, first_n, found, i, item, len, ref, resp; 23 | resp = ''; 24 | found = 0; 25 | if (data.total_count === 0) { 26 | msg.send("Didn't find \"" + query + "\"" + in_repo, done); 27 | return; 28 | } 29 | 30 | ref = data.items; 31 | for (i = 0, len = ref.length; i < len; i++) { 32 | item = ref[i]; 33 | if (found === 5) { 34 | broken = true; 35 | break; 36 | } 37 | resp += "\n - " + item.name + ": " + item.html_url; 38 | found += 1; 39 | } 40 | first_n = ''; 41 | if (broken) { 42 | first_n = ", first 5"; 43 | resp += "\nMore: https://github.com/search?q=" + (encodeURIComponent(query)) + repostr + "&type=Code&s=indexed"; 44 | } 45 | 46 | msg.send("Searched for \"" + query + "\"" + in_repo + " and found " + data.total_count + " results" + first_n + ": " + resp, done); 47 | }); 48 | } catch (error) { 49 | e = error; 50 | console.log("GitHub Search failed", e); 51 | } 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /scripts/github-status.js: -------------------------------------------------------------------------------- 1 | module.exports = function(robot) { 2 | 3 | var formatString = function(string) { 4 | return decodeURIComponent(string.replace(/(\n)/gm, " ")); 5 | }; 6 | 7 | var status = function(msg, done) { 8 | robot.http('https://status.github.com/api/status.json').get()(function(err, res, body) { 9 | var date, json, now, secondsAgo; 10 | json = JSON.parse(body); 11 | now = new Date(); 12 | date = new Date(json['last_updated']); 13 | secondsAgo = Math.round((now.getTime() - date.getTime()) / 1000); 14 | msg.send("Status: " + json['status'] + " (" + secondsAgo + " seconds ago)", done); 15 | }); 16 | }; 17 | 18 | var lastMessage = function(msg, done) { 19 | robot.http('https://status.github.com/api/last-message.json').get()(function(err, res, body) { 20 | var date, json; 21 | json = JSON.parse(body); 22 | date = new Date(json['created_on']); 23 | msg.send(("Status: " + json['status'] + "\n") + ("Message: " + (formatString(json['body'])) + "\n") + ("Date: " + (date.toLocaleString())), done); 24 | }); 25 | }; 26 | 27 | var statusMessages = function(msg, done) { 28 | robot.http('https://status.github.com/api/messages.json').get()(function(err, res, body) { 29 | var buildMessage, json, message; 30 | json = JSON.parse(body); 31 | buildMessage = function(message) { 32 | var date; 33 | date = new Date(message['created_on']); 34 | return "[" + message['status'] + "] " + (formatString(message['body'])) + " (" + (date.toLocaleString()) + ")"; 35 | }; 36 | msg.send(((function() { 37 | var i, len, results; 38 | results = []; 39 | for (i = 0, len = json.length; i < len; i++) { 40 | message = json[i]; 41 | results.push(buildMessage(message)); 42 | } 43 | return results; 44 | })()).join('\n'), done); 45 | }); 46 | }; 47 | 48 | robot.respond(/github status$/i, { suggestions: ["github status"] }, function(msg, done) { 49 | status(msg, done); 50 | }); 51 | 52 | robot.respond(/github status last$/i, { suggestions: ["github status last"] }, function(msg, done) { 53 | lastMessage(msg, done); 54 | }); 55 | 56 | robot.respond(/github status messages$/i, { suggestions: ["github status messages"] }, function(msg, done) { 57 | statusMessages(msg, done); 58 | }); 59 | }; 60 | 61 | --------------------------------------------------------------------------------