3 var env = require('gitter-web-env');
4 var logger = env.logger;
7 var format = require('util').format;
8 var github = require('octonode');
9 var _ = require('lodash');
10 var Promise = require('bluebird');
11 var troupeTemplate = require('gitter-web-templates/lib/troupe-template');
13 var templatePromise = troupeTemplate.compile(
14 __dirname + '/../../../public/templates/github-pull-request-body.hbs'
17 var StatusError = require('statuserror');
18 var readmeBadger = require('readme-badger');
19 var path = require('path');
21 function insertBadge(roomUri, content, fileExt, user) {
22 var imageUrl = conf.get('web:badgeBaseUrl') + '/' + roomUri + '.svg';
24 conf.get('web:basepath') +
27 '?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge';
28 var altText = 'Join the chat at ' + conf.get('web:basepath') + '/' + roomUri;
30 if (!readmeBadger.hasImageSupport(fileExt)) {
31 stats.event('badger.inserted_plaintext', { userId: user.id, fileExt: fileExt });
34 return readmeBadger.addBadge(content, fileExt, imageUrl, linkUrl, altText);
37 function Client(token) {
38 var client = github.client(token);
41 ['get', 'post', 'patch', 'put', 'del'].forEach(function(operation) {
42 self[operation] = function(url, options) {
43 return new Promise(function(resolve, reject) {
44 client[operation](url, options, function(err, status, body) {
45 if (err) return reject(err);
48 return reject(new StatusError(status, (body && body.message) || 'HTTP ' + status));
55 var client = new Client(conf.get('badger:githubToken'));
57 function findReadme(tree, path) {
58 if (!path) path = 'README.md';
60 // Case sensitive first
61 var result = _.find(tree, function(t) {
62 return t.path === path;
64 if (result) return result;
66 // Case insensitive second
67 return _.find(tree, function(t) {
68 return t.path.toLowerCase() === path.toLowerCase();
72 function urlForGithubClient(url) {
73 url = url.replace(/^https?:\/\/[A-Za-z0-9\-\.]+/, '');
77 function pullRequestHeadFromBranch(branch) {
78 var branchName = branch.ref.replace(/^.*\//, '');
79 var m = /\/repos\/([\w\-]+)\/.*/.exec(branch.url);
82 return m[1] + ':' + branchName;
85 function ReadmeUpdater(context) {
86 function findMainBranch() {
87 return client.get(format('/repos/%s', context.sourceRepo), {}).then(function(repo) {
88 if (!context.primaryBranch) {
89 context.primaryBranch = repo.default_branch;
96 function doForkIfRequired() {
98 return client.post(format('/repos/%s/forks', context.sourceRepo), {}).then(function(fork) {
99 return fork.full_name;
103 // GitHub can take some time after a fork to setup the gitrefs
104 // Await the operation completion
105 function getRefsAwait(repo) {
107 var url = format('/repos/%s/git/refs/', repo);
108 var start = Date.now();
111 var timeTaken = (Date.now() - start) / 1000;
112 if (timeTaken > 300 /* 5 minutes */) {
113 return Promise.reject(
114 new Error('Timeout awaiting git data for ' + repo + ' after ' + timeTaken + 's')
118 // Exponential backoff
119 delay = Math.floor(delay * 1.1);
121 return client.get(url, {}).then(
123 if (!refs || !Array.isArray(refs) || !refs.length) {
124 return Promise.delay(delay).then(get);
130 logger.info('Ignoring failed GitHub request to ' + url + ' failed: ' + err);
131 return Promise.delay(delay).then(get);
139 function createBranch(repo) {
141 return getRefsAwait(repo).then(function(refs) {
142 function findRef(name) {
143 return _.find(refs, function(r) {
144 return r.ref === 'refs/heads/' + name;
148 // Find the correct ref
149 var parentBranchRef = findRef(context.primaryBranch);
152 if (!parentBranchRef) {
153 throw new Error('Cannot find branch ' + context.primaryBranch);
156 // Ensure the new branch name is unique
158 for (var c = 0, found = true; found; c++, found = findRef(newBranchName)) {
159 newBranchName = c ? context.branchPrefix + '-' + c : context.branchPrefix;
163 ref: 'refs/heads/' + newBranchName,
164 sha: parentBranchRef.object.sha
167 // Create the new branch
168 return client.post(format('/repos/%s/git/refs', repo), ref);
172 function generatePRBody() {
173 return templatePromise.then(function(template) {
174 return template(context);
178 function createPullRequest(branchRef) {
179 var pullRequestHead = pullRequestHeadFromBranch(branchRef);
181 return generatePRBody().then(function(body) {
182 var badgeOrLink = context.insertedPlaintext ? 'link' : 'badge';
185 title: 'Add a Gitter chat ' + badgeOrLink + ' to ' + context.readmeFileName,
187 base: context.primaryBranch,
188 head: pullRequestHead
191 return client.post(format('/repos/%s/pulls', context.sourceRepo), prRequest);
195 function getExistingReadme() {
196 return client.get(format('/repos/%s/readme', context.sourceRepo), {}).catch(function(err) {
197 if (err.status === 404) return null;
201 function updateReadme(treeUrl) {
202 return Promise.all([client.get(treeUrl, {}), getExistingReadme()]).spread(function(
206 var existingReadme = findReadme(tree.tree, readme && readme.path);
208 if (existingReadme) {
209 var content = new Buffer(readme.content, 'base64').toString('utf8');
210 var fileExt = path.extname(existingReadme.path).substring(1);
212 content = insertBadge(context.roomUri, content, fileExt, context.user);
213 context.insertedPlaintext = !readmeBadger.hasImageSupport(fileExt);
215 context.readmeFileName = existingReadme.path;
217 path: existingReadme.path,
218 mode: existingReadme.mode,
228 // No readme file exists
229 context.readmeFileName = 'README.md';
230 context.insertedPlaintext = false;
235 content: '# ' + context.sourceRepo.replace(/^.*\//, '') + '\n' + context.badgeContent
245 function prepareTreeForCommit(branchRef) {
246 return client.get(urlForGithubClient(branchRef.object.url), {}).then(function(commit) {
247 var treeUrl = urlForGithubClient(commit.tree.url);
249 return [updateReadme(treeUrl), commit.sha];
253 function commitTree(tree, branchRef, latestCommitSha) {
256 .post(format('/repos/%s/git/trees', context.destinationRepo), tree)
257 .then(function(tree) {
258 var commitRequest = {
259 message: context.insertedPlaintext ? 'Add Gitter link' : 'Add Gitter badge',
261 name: 'The Gitter Badger',
262 email: 'badger@gitter.im',
263 date: new Date().toISOString().replace(/\.\d+/, '') // GitHub doesn't consider milliseconds as part of ISO8601
265 parents: [latestCommitSha],
269 // Create a commit for the tree
270 return client.post(format('/repos/%s/git/commits', context.destinationRepo), commitRequest);
272 .then(function(commit) {
278 format('/repos/%s/git/%s', context.destinationRepo, branchRef.ref),
284 this.perform = function() {
285 return findMainBranch()
286 .then(doForkIfRequired)
287 .then(function(destinationRepo) {
288 context.destinationRepo = destinationRepo;
290 // Create the branch where we'll do the work
291 return createBranch(destinationRepo);
293 .then(function(branchRef) {
294 // Prepare a tree for the commit
295 return prepareTreeForCommit(branchRef)
296 .spread(function(treeForCommit, latestCommitSha) {
298 return commitTree(treeForCommit, branchRef, latestCommitSha);
301 // Finally, create a pull-request
302 return createPullRequest(branchRef);
305 .then(function(pullRequest) {
311 function updateFileAndCreatePullRequest(sourceRepo, roomUri, user, branchPrefix) {
312 return new ReadmeUpdater({
313 token: conf.get('badger:githubToken'),
314 sourceRepo: sourceRepo,
317 branchPrefix: branchPrefix,
318 badgeContent: getBadgeMarkdown(roomUri, 'badge'),
319 badgeContentBody: getBadgeMarkdown(roomUri, 'body_badge')
323 function getBadgeMarkdown(roomUri, content) {
324 var contentLink = content ? '&utm_content=' + content : '';
326 var imageUrl = conf.get('web:badgeBaseUrl') + '/' + roomUri + '.svg';
328 conf.get('web:basepath') +
331 '?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge' +
334 return '\n[![Gitter](' + imageUrl + ')](' + linkUrl + ')';
337 function sendBadgePullRequest(repo, roomUri, user) {
338 // The name of this stat is due to historical reasons
339 stats.event('badger.clicked', { userId: user.id, repo: repo, roomUri: roomUri });
341 return updateFileAndCreatePullRequest(repo, roomUri, user.username, 'gitter-badge')
343 stats.event('badger.succeeded', { userId: user.id, repo: repo, roomUri: roomUri });
346 .catch(function(err) {
347 stats.event('badger.failed', { userId: user.id, repo: repo, roomUri: roomUri });
348 logger.error('Badger failed', { exception: err, uri: repo, roomUri: roomUri });
350 // dont swollow this error, the client needs to be notified of our failure
355 exports.sendBadgePullRequest = sendBadgePullRequest;