yamadapc/pivotal-to-github

View on GitHub
lib/index.js

Summary

Maintainability
C
7 hrs
Test Coverage
var GitHubApi = require('github');
var ProgressBar = require('progress');
var _ = require('lodash');
var assert = require('assert');
var rateLimit = require('function-rate-limit');
var tracker = require('pivotaltracker');
var async = require('async');

exports = module.exports = function pivotalToGithub(options, cb) {
  if(!cb) cb = function() {};
  assert(options, 'Missing required parameter `options`');
  assert(options.githubRepo, 'Missing required option `options.githubRepo`');
  assert(options.githubToken, 'Missing required option `options.githubToken`');
  assert(options.githubUser, 'Missing required option `options.githubUser`');
  assert(options.pivotalId, 'Missing required option `options.pivotalId`');
  assert(options.pivotalToken, 'Missing required option `options.pivotalToken`');

  console.log('Fetching Pivotal tickets...');
  exports.getPivotalTickets(options.pivotalToken, options.pivotalId, function(err, tickets, people) {
    if(err) return cb(err);
    console.log('--> Got ' + tickets.length + ' tickets from Pivotal.');
    console.log('Creating tickets as GitHub issues on ' + options.githubUser + '/' + options.githubRepo);
    console.log('\n    This will take ' + (2 * tickets.length) + ' seconds.');
    console.log('    We\'re conservative about the rate-limit because of GitHub\'s abuse limitting.\n');
    exports.createGithubTickets(options.githubToken, options.githubRepo, options.githubUser, tickets, people);
  });
};

exports.getPivotalTickets = function(token, projectId, cb) {
  var client = new tracker.Client(token);
  console.log('--> Fetching stories...');
  client.project(projectId).stories.all(function(err, stories) {
    if(err) return cb(err);
    console.log('--> Fetching tasks and comments...');
    async.map(stories, function(story, cb) {
      client.project(projectId).story(story.id).comments.all(function(err, comments) {
        if(err) return cb(err);

        client.project(projectId).story(story.id).tasks.all(function(err, tasks) {
          if(err) return cb(err);
          story.comments = comments;
          story.tasks = tasks;
          cb(err, story);
        });
      });
    }, function(err, stories) {
      console.log('--> Fetching people...');
      client.project(projectId).memberships.all(function(err, people) {
        cb(err, stories, _(people).pluck('person').indexBy('id').value());
      });
    });
  });
};

exports.createGithubTicket = rateLimit(1, 2000, function(github, repo, user, ticket, people, cb) {
  //console.log(people); process.exit(1);
  function getComments(ticket) {
    return '### Comments\n\n' + ticket.comments.map(function(comment) {
      var commenter = people[comment.personId] ? people[comment.personId].name : comment.personId;
      return '[' + comment.createdAt + '] **' + commenter + '**: ' + comment.text + '\n';
    }).join('\n');
  }

  function getTasks(ticket) {
    return '### Tasks\n\n' + _.sortBy(ticket.tasks, 'position').map(function(task) {
      return '- [' + (task.complete ? 'x' : ' ') + '] ' + task.description;
    }).join('\n');
  }


  function getDescription(ticket) {
    var requester = people[ticket.requestedById] ? people[ticket.requestedById].name : ticket.requestedById;
    var owner = people[ticket.ownedById] ? people[ticket.ownedById].name : ticket.ownedById;
    return (ticket.description || '') + '\n\n- - -\n\n' +
      '- **Original Ticket**: ' + ticket.url + '\n' +
      '- **Created by:** ' + requester + '\n' +
      (ticket.ownedById ? ('- **Owned by:** ' + owner + '\n') : '') +
      (ticket.estimate != null ? ('- **Estimate:** ' + ticket.estimate + '\n') : '') +
      (ticket.currentState === 'accepted' ?
        '- **Accepted at:** ' + ticket.acceptedAt + '\n' : '') +
      '- **State:** ' + ticket.currentState +
      (ticket.comments.length ? '\n\n- - -\n\n' + getComments(ticket) : '') +
      (ticket.tasks.length ?  '\n\n- - -\n\n' + getTasks(ticket) : '');
  }

  if(!cb) cb = function() {};

  github.issues.create({
    title: ticket.name,
    body: getDescription(ticket),
    repo: repo,
    user: user,
    labels: [ticket.storyType].concat(_.pluck(ticket.labels, 'name').concat([
      'pivotal'
    ])),
  }, function(err, data) {
    if(err) return cb(err);

    if(ticket.currentState === 'accepted') {
      return github.issues.edit({
        repo: repo,
        user: user,
        number: data.number,
        state: 'closed',
      }, cb);
    }

    cb(null, data);
  });
});

exports.createGithubTickets = function(token, repo, user, tickets, people, cb) {
  if(!cb) cb = function() {};

  var github = new GitHubApi({
    version: '3.0.0',
    headers: {
      'user-agent': 'Pivotal-To-GitHub'
    }
  });

  github.authenticate({
    type: 'token',
    token: token,
  });

  var pg = new ProgressBar('Tickets to Issues [:current/:total] [|:bar|] :eta remaining', {
    total: tickets.length,
    width: 40,
  });

  tickets.map(function(ticket) {
    exports.createGithubTicket(github, repo, user, ticket, people, function(err, issue) {
      process.stdout.clearLine();
      process.stdout.cursorTo(0);
      if(err) {
        console.error('ERROR:', err.message);
        console.error('Offending ticket:', JSON.stringify(ticket));
        pg.tick();
        return;
      }

      console.log('--> Created ticket ' + ticket.id + ' as #' + issue.number);
      pg.tick();
    });
  });
};