<commit_before>/*
 Documentation coming soon.
*/

var XML        = require('xml'),
    mime       = require('mime'),
    fs         = require('fs');

function Podcast (options, items) {
    options = options || {};

    this.title          = options.title || 'Untitled Podcast Feed';
    this.description    = options.description || '';
    this.generator      = options.generator || 'Podcast for Node';
    this.feed_url       = options.feed_url;
    this.site_url       = options.site_url;
    this.image_url      = options.image_url;
    this.author         = options.author;
    this.categories     = options.categories;
    this.pubDate        = options.pubDate;
    this.docs           = options.docs;
    this.copyright      = options.copyright;
    this.language       = options.language;
    this.managingEditor = options.managingEditor;
    this.webMaster      = options.webMaster;
    this.ttl            = options.ttl;
    //option to return feed as GeoRSS is set automatically if feed.lat/long is used
    this.geoRSS         = options.geoRSS || false;

    this.itunesAuthor   = options.itunesAuthor || options.author;
    this.itunesSubtitle = options.itunesSubtitle;
    this.itunesSummary  = options.itunesSummary;
    this.itunesOwner    = options.itunesOwner; // {name:String, email:String}
    this.itunesExplicit = options.itunesExplicit || false;
    this.itunesCategory = options.itunesCategory || options.categories; // [{text:String, subcats:[{name:String, subcat:Array}]}]
    this.itunesImage    = options.itunesImage;

    this.items          = items || [];

    this.item = function (options) {
        options = options || {};
        var item = {
            title:          options.title || 'No title',
            description:    options.description || '',
            url:            options.url,
            guid:           options.guid,
            categories:     options.categories || [],
            author:         options.author,
            date:           options.date,
            lat:            options.lat,
            long:           options.long,
            enclosure:      options.enclosure || false,
            itunesAuthor:   options.itunesAuthor || options.author,
            itunesExplicit: options.itunesExplicit || false,
            itunesSubtitle: options.itunesSubtitle,
            itunesSummary:  options.itunesSummary,
            itunesDuration: options.itunesDuration,
            itunesKeywords: options.itunesKeywords // string array
        };

        this.items.push(item);
        return this;
    };

    this.xml = function(indent) {
        return '<?xml version="1.0" encoding="UTF-8"?>\n'
                + XML(generateXML(this), indent);
    }

}

function ifTruePush(bool, array, data) {
    if (bool) {
        array.push(data);
    }
}

function generateXML (data){

    var channel = [];
    channel.push({ title:           { _cdata: data.title } });
    channel.push({ description:     { _cdata: data.description || data.title } });
    channel.push({ link:            data.site_url || 'http://github.com/maxnowack/node-podcast' });
    // image_url set?
    if (data.image_url) {
        channel.push({ image:  [ {url: data.image_url}, {title: data.title},  {link: data.site_url} ] });
    }
    channel.push({ generator:       data.generator });
    channel.push({ lastBuildDate:   new Date().toGMTString() });

    ifTruePush(data.feed_url, channel, { 'atom:link': { _attr: { href: data.feed_url, rel: 'self', type: 'application/rss+xml' } } });
    ifTruePush(data.author, channel, { 'author': { _cdata: data.author } });
    ifTruePush(data.pubDate, channel, { 'pubDate': new Date(data.pubDate).toGMTString() });
    ifTruePush(data.copyright, channel, { 'copyright':  { _cdata: data.copyright } });
    ifTruePush(data.language, channel, { 'language': { _cdata: data.language } });
    ifTruePush(data.managingEditor, channel, { 'managingEditor': { _cdata: data.managingEditor } });
    ifTruePush(data.webMaster, channel, { 'webMaster': { _cdata: data.webMaster } });
    ifTruePush(data.docs, channel, { 'docs': data.docs });
    ifTruePush(data.ttl, channel, { 'ttl': data.ttl });

    if (data.categories) {
        data.categories.forEach(function(category) {
            ifTruePush(category, channel, { category: { _cdata: category } });
        });
    }

    data.items.forEach(function(item) {
        var item_values = [
                    { title:        { _cdata: item.title } }
                ];
        ifTruePush(item.description, item_values, { description:  { _cdata: item.description } });
        ifTruePush(item.url, item_values, { link: item.url });
        ifTruePush(item.link || item.guid || item.title, item_values, { guid:         [ { _attr: { isPermaLink: !item.guid && !!item.url } }, item.guid || item.url || item.title ]  });

        item.categories.forEach(function(category) {
            ifTruePush(category, item_values, { category: { _cdata: category } });
        });

        ifTruePush(item.author || data.author, item_values, { 'dc:creator': { _cdata: item.author || data.author } });
        ifTruePush(item.date, item_values, { pubDate:      new Date(item.date).toGMTString() });

        //Set GeoRSS to true if lat and long are set
        data.geoRSS = data.geoRSS || (item.lat && item.long);
        ifTruePush(item.lat, item_values, {'geo:lat': item.lat});
        ifTruePush(item.long, item_values, {'geo:long': item.long});

        if( item.enclosure && item.enclosure.url ) {
            if( item.enclosure.file ) {
                item_values.push({
                    enclosure : {
                        _attr : {
                            url : item.enclosure.url,
                            length : fs.statSync(item.enclosure.file).size,
                            type : mime.lookup(item.enclosure.file)
                        }
                    }
                });
            } else {
                item_values.push({
                    enclosure : {
                        _attr : {
                            url : item.enclosure.url,
                            length : item.enclosure.size || 0,
                            type : mime.lookup(item.enclosure.url)
                        }
                    }
                });
            }
        }

        channel.push({ item: item_values });

    });

    //set up the attributes for the RSS feed.
    var _attr = {
        'xmlns:dc':         'http://purl.org/dc/elements/1.1/',
        'xmlns:content':    'http://purl.org/rss/1.0/modules/content/',
        'xmlns:atom':       'http://www.w3.org/2005/Atom',
        version: '2.0'
    };

    //only add namespace if GeoRSS is true
    if(data.geoRSS){
        _attr['xmlns:geo'] = 'http://www.w3.org/2003/01/geo/wgs84_pos#';
    }

    return {
        rss: [
            { _attr: _attr },
            { channel: channel }
        ]
    };
}

module.exports = RSS;
<commit_msg>added functionality to push the new options to output xml<commit_after>/*
 Documentation coming soon.
*/

var XML        = require('xml'),
    mime       = require('mime'),
    fs         = require('fs');

function Podcast (options, items) {
    options = options || {};

    this.title          = options.title || 'Untitled Podcast Feed';
    this.description    = options.description || '';
    this.generator      = options.generator || 'Podcast for Node';
    this.feed_url       = options.feed_url;
    this.site_url       = options.site_url;
    this.image_url      = options.image_url;
    this.author         = options.author;
    this.categories     = options.categories;
    this.pubDate        = options.pubDate;
    this.docs           = options.docs;
    this.copyright      = options.copyright;
    this.language       = options.language;
    this.managingEditor = options.managingEditor;
    this.webMaster      = options.webMaster;
    this.ttl            = options.ttl;
    //option to return feed as GeoRSS is set automatically if feed.lat/long is used
    this.geoRSS         = options.geoRSS || false;

    this.itunesAuthor   = options.itunesAuthor || options.author;
    this.itunesSubtitle = options.itunesSubtitle;
    this.itunesSummary  = options.itunesSummary;
    this.itunesOwner    = options.itunesOwner; // {name:String, email:String}
    this.itunesExplicit = options.itunesExplicit || false;
    this.itunesCategory = options.itunesCategory || options.categories; // [{text:String, subcats:[{name:String, subcat:Array}]}]
    this.itunesImage    = options.itunesImage;

    this.items          = items || [];

    this.item = function (options) {
        options = options || {};
        var item = {
            title:          options.title || 'No title',
            description:    options.description || '',
            url:            options.url,
            guid:           options.guid,
            categories:     options.categories || [],
            author:         options.author,
            date:           options.date,
            lat:            options.lat,
            long:           options.long,
            enclosure:      options.enclosure || false,
            itunesAuthor:   options.itunesAuthor || options.author,
            itunesExplicit: options.itunesExplicit || false,
            itunesSubtitle: options.itunesSubtitle,
            itunesSummary:  options.itunesSummary,
            itunesDuration: options.itunesDuration,
            itunesKeywords: options.itunesKeywords // string array
        };

        this.items.push(item);
        return this;
    };

    this.xml = function(indent) {
        return '<?xml version="1.0" encoding="UTF-8"?>\n'
                + XML(generateXML(this), indent);
    }

}

function ifTruePush(bool, array, data) {
    if (bool) {
        array.push(data);
    }
}

function buildiTunesCategories(categories)
{
    var arr = [];
    for(var i=0; i<categories.length;i++)
    {
        if(categories[i].subcats)
        {
            arr.push({'itunes:category': [{ _attr: {text: categories[i].text }},buildiTunesCategories(categories[i].subcats)]});
        }
        else
        {
            arr.push({'itunes:category': { _attr: {text: categories[i].text }}});
        }
    }
    return arr;
}

function generateXML (data){

    var channel = [];
    channel.push({ title:           { _cdata: data.title } });
    channel.push({ description:     { _cdata: data.description || data.title } });
    channel.push({ link:            data.site_url || 'http://github.com/maxnowack/node-podcast' });
    // image_url set?
    if (data.image_url) {
        channel.push({ image:  [ {url: data.image_url}, {title: data.title},  {link: data.site_url} ] });
    }
    channel.push({ generator:       data.generator });
    channel.push({ lastBuildDate:   new Date().toGMTString() });

    ifTruePush(data.feed_url, channel, { 'atom:link': { _attr: { href: data.feed_url, rel: 'self', type: 'application/rss+xml' } } });
    ifTruePush(data.author, channel, { 'author': { _cdata: data.author } });
    ifTruePush(data.pubDate, channel, { 'pubDate': new Date(data.pubDate).toGMTString() });
    ifTruePush(data.copyright, channel, { 'copyright':  { _cdata: data.copyright } });
    ifTruePush(data.language, channel, { 'language': { _cdata: data.language } });
    ifTruePush(data.managingEditor, channel, { 'managingEditor': { _cdata: data.managingEditor } });
    ifTruePush(data.webMaster, channel, { 'webMaster': { _cdata: data.webMaster } });
    ifTruePush(data.docs, channel, { 'docs': data.docs });
    ifTruePush(data.ttl, channel, { 'ttl': data.ttl });

    ifTruePush(data.itunesAuthor, channel, { 'itunes:author': data.itunesAuthor });
    ifTruePush(data.itunesSubtitle, channel, { 'itunes:subtitle': data.itunesSubtitle });
    ifTruePush(data.itunesSummary, channel, { 'itunes:summary': data.itunesSummary });
    ifTruePush(data.itunesImage, channel, { 'itunes:image': { _attr:{href: data.itunesImage }}});
    channel.push({ 'itunes:explicit': (data.itunesExplicit ? 'Yes' : 'No'));

    ifTruePush(data.itunesOwner.name && data.itunesOwner.email, channel, {'itunes:owner': [{'itunes:name':data.itunesOwner.name},{'itunes:email':data.itunesOwner.email}]);
    ifTruePush(data.itunesCategory, channel, buildiTunesCategories(data.itunesCategory));

    if (data.categories) {
        data.categories.forEach(function(category) {
            ifTruePush(category, channel, { category: { _cdata: category } });
        });
    }

    data.items.forEach(function(item) {
        var item_values = [
                    { title:        { _cdata: item.title } }
                ];
        ifTruePush(item.description, item_values, { description:  { _cdata: item.description } });
        ifTruePush(item.url, item_values, { link: item.url });
        ifTruePush(item.link || item.guid || item.title, item_values, { guid:         [ { _attr: { isPermaLink: !item.guid && !!item.url } }, item.guid || item.url || item.title ]  });

        item.categories.forEach(function(category) {
            ifTruePush(category, item_values, { category: { _cdata: category } });
        });

        ifTruePush(item.author || data.author, item_values, { 'dc:creator': { _cdata: item.author || data.author } });
        ifTruePush(item.date, item_values, { pubDate:      new Date(item.date).toGMTString() });

        ifTruePush(item.itunesAuthor || data.itunesAuthor, item_values, { 'itunes:author': item.itunesAuthor || data.itunesAuthor });
        item_values.push({ 'itunes:explicit': (item.itunesExplicit || data.itunesExplicit ? 'Yes' : 'No'));
        ifTruePush(item.itunesSubtitle || data.itunesSubtitle, item_values, { 'itunes:subtitle': item.itunesSubtitle || data.itunesSubtitle });
        ifTruePush(item.itunesSummary || data.itunesSummary, item_values, { 'itunes:summary': item.itunesSummary || data.itunesSummary });
        ifTruePush(item.itunesDuration, item_values, { 'itunes:duration': item.itunesDuration });
        ifTruePush(item.itunesKeywords, item_values, { 'itunes:keywords': item.itunesKeywords });

        //Set GeoRSS to true if lat and long are set
        data.geoRSS = data.geoRSS || (item.lat && item.long);
        ifTruePush(item.lat, item_values, {'geo:lat': item.lat});
        ifTruePush(item.long, item_values, {'geo:long': item.long});

        if( item.enclosure && item.enclosure.url ) {
            if( item.enclosure.file ) {
                item_values.push({
                    enclosure : {
                        _attr : {
                            url : item.enclosure.url,
                            length : fs.statSync(item.enclosure.file).size,
                            type : mime.lookup(item.enclosure.file)
                        }
                    }
                });
            } else {
                item_values.push({
                    enclosure : {
                        _attr : {
                            url : item.enclosure.url,
                            length : item.enclosure.size || 0,
                            type : mime.lookup(item.enclosure.url)
                        }
                    }
                });
            }
        }

        channel.push({ item: item_values });

    });

    //set up the attributes for the RSS feed.
    var _attr = {
        'xmlns:dc':         'http://purl.org/dc/elements/1.1/',
        'xmlns:content':    'http://purl.org/rss/1.0/modules/content/',
        'xmlns:atom':       'http://www.w3.org/2005/Atom',
        'xmlns:itunes':     'http://www.itunes.com/dtds/podcast-1.0.dtd',
        version: '2.0'
    };

    //only add namespace if GeoRSS is true
    if(data.geoRSS){
        _attr['xmlns:geo'] = 'http://www.w3.org/2003/01/geo/wgs84_pos#';
    }

    return {
        rss: [
            { _attr: _attr },
            { channel: channel }
        ]
    };
}

module.exports = RSS;
<|endoftext|>


<commit_before>/**
 *  class Pointer
 **/

'use strict';

var Route = require('./pointer/route');
var URL   = require('./pointer/url');

/**
 *  new Pointer([routes])
 *  - routes (Object): Routes map in form of `pattern: options` pairs.
 *
 *  Creates new instance of router, prefilled with given routes (if provided).
 *
 *
 *  ##### See Also
 *
 *  - [[Pointer#addRoute]]
 **/
function Pointer(routes) {
  var k;

  // all routes grouped by prefixes
  //
  //  {
  //    '*': [ Route(), ... ],
  //    '//example.com': [ Route(), ... ]
  //  }
  //
  this.__routes__ = {};

  // cache of RegExps for known prefixes, grouped by host and protocol:
  //
  //  {
  //    'example.com': {
  //      'https': {
  //        'https://users.example.com': null,
  //      },
  //      '*': { // any protocol
  //        '//example.com': null,
  //        '//example.com/admin': /^\/admin/
  //      }
  //    }
  //  }
  //
  this.__prefixes__ = {};

  // routes grouped by names (needed for linkTo)
  this.__named_routes__ = {};

  // prefill routes if map provided
  if (routes) {
    for (k in routes) {
      if (routes.hasOwnProperty(k)) {
        this.addRoute(k, routes[k]);
      }
    }
  }
}

/** alias of: Pointer.new
 *  Pointer.create([routes]) -> Pointer
 *
 *  Constructor proxy.
 **/
Pointer.create = function create(routes) {
  return new Pointer(routes);
};

// helper to parse URL into proto, host and path respecting our internal
// structures, so that proto|path is `*` if it's not provided.
//
function getURLParts(url) {
  var parsed = URL(url);

  return {
    proto:  parsed.attr('protocol') || '*',
    host:   parsed.attr('host') || '*',
    path:   parsed.attr('relative') || ''
  };
}

// prepares and saves regexp for the prefix under internal cache
//
//    var obj = {};
//
//    setPrefixRegExp(obj, '//example.com');
//    setPrefixRegExp(obj, 'https://example.com/foobar');
//
//    // {
//    //   'example.com': {
//    //     'https': { 'https://example.com/foobar': /^\/foobar/ },
//    //     '*':     { '//example.com': null }
//    //   }
//    // }
//
function setPrefixRegExp(store, prefix) {
  var parsed = getURLParts(prefix);

  store = store[parsed.host] || (store[parsed.host] = {});
  store = store[parsed.proto] || (store[parsed.proto] = {});
  store[prefix] = parsed.path ? new RegExp('^' + parsed.path) : null;
}

/**
 *  Pointer#addRoute(pattern[, options]) -> Route
 *  - pattern (String): Pattern as for [[Route.new]]
 *  - options (Object): Route options, see below.
 *
 *  Create and add new route.
 *
 *
 *  ##### Options
 *
 *  Second argument of `addRoute` is options. It consist of following keys:
 *
 *  -   *name*    (Optional, String): name of the route.
 *      Used to build URLs with [[Pointer#linkTo]].
 *      Several routes may have same name, see [[Pointer#linkTo]] for
 *      detailed information.
 *
 *  -   *prefix*  (Optional, String): think of it as of mount point.
 *      You can use `prefix()` as well, which is a syntax sugar
 *      that sets this option.
 *
 *  -   *params*  (Optional, Object): options of params in the route.
 *      See [[Route.new]] for details.
 *
 *  -   *meta* (Optional, Mixed): meta data to mix into match object.
 *      See [[Route.new]] for details.
 *
 *
 *  ##### See Also:
 *
 *  - [[Route.new]]
 **/
Pointer.prototype.addRoute = function addRoute(pattern, options) {
  var route;

  options = options || {};
  route   = new Route(pattern, options.params, options.meta, options.prefix);

  //
  // save route under prefix (* - means no prefix)
  //

  if (!this.__routes__[options.prefix || '*']) {
    this.__routes__[options.prefix || '*'] = [];
  }

  this.__routes__[options.prefix || '*'].push(route);

  // save named route
  if (options.name) {
    if (!this.__named_routes__[options.name]) {
      this.__named_routes__[options.name] = [];
    }

    this.__named_routes__[options.name].push(route);
  }

  if (options.prefix) {
    setPrefixRegExp(this.__prefixes__, options.prefix);
  }

  return route;
};

// returns protocol|host alternatives where to search prefix regexps
//
function variants(str) {
  return '*' === str ? [ '*' ] : [ str, '*' ];
}

// sorter function for the Array#sort to sort strings by their length.
// longest comes first:
//
//      [ 'a', 'cba', 'ba' ].sort(reverseSortByStringLength);
//      // -> [ 'cba', 'ba', 'a' ]
//
function reverseSortByStringLength(a, b) {
  a = String(a).length;
  b = String(b).length;

  if (a === b) {
    return 0;
  }

  // longest strings comes first
  return (a > b) ? -1 : 1;
}

// returns object's keys sorted alphabeticaly in descending order
//
function getSortedKeys(obj) {
  var keys = [], k;

  for (k in obj) {
    if (obj.hasOwnProperty(k)) {
      keys.push(k);
    }
  }

  return keys.sort(reverseSortByStringLength);
}

// iterates through array calling iterator on each element.
// stops as soon as iterator return non-falsy value, and returns this value
//
function find(arr, iter) {
  var i, l, result;

  for (i = 0, l = arr.length; i < l; i++) {
    result = iter(arr[i]);

    if (result) {
      return result;
    }
  }

  return result;
}

/**
 *  Pointer#match(url) -> Object|Null
 *  - url (String): URL to find mathing route for.
 *
 *  Returns first matching route or false if none found.
 *  See [[Route#match]] for details of matched data Object.
 **/
Pointer.prototype.match = function match(url) {
  var self = this,
      data = getURLParts(url);

  return find(variants(data.host), function (host) {
    if (!self.__prefixes__[host]) {
      return;
    }

    return find(variants(data.proto), function (proto) {
      if (!self.__prefixes__[host][proto]) {
        return;
      }

      return find(getSortedKeys(self.__prefixes__[host][proto]), function (prefix) {
        var path = data.path;

        // prefix can be a regexp of path or null
        if (null !== self.__prefixes__[host][proto][prefix]) {
          path = path.replace(self.__prefixes__[host][proto][prefix], '');

          // prefix regexp removed nothing - that means it does not match path
          if (path === data.path) {
            return;
          }
        }

        return find(self.__routes__[prefix], function (route) {
          return route.match(path);
        });
      });
    });
  }) || find(self.__routes__['*'], function (route) {
    return route.match(data.path) || route.match(url);
  }) || null;
};

/**
 *  Pointer#linkTo(name[, params]) -> String|Null
 *  - name (String): Name of the URL (or group of URLs).
 *  - params (Object): Params to feed to URL builder.
 *
 *  Creates pretty URL for the route registered with given `name`.
 *  If there were several routes registered with such `name`, the one that
 *  produce the longest URL is used.
 *
 *
 *  ##### See Also
 *
 *  - [[Route#buildURL]]
 **/
Pointer.prototype.linkTo = function linkTo(name, params) {
  var i, l, routes = this.__named_routes__[name] || [], url = null;

  // find route thet returns longest URL with given params
  for (i = 0, l = routes.length; i < l; i++) {
    var tmp = routes[i].buildURL(params);

    // we want return the longest matching
    if (null === url || url.length < tmp.length) {
      url = tmp;
    }
  }

  return url;
};

/**
 *  Pointer.parseURL(url) -> Object
 *  - url (String): URL to parse
 *
 *  Returns parts of parsed URL:
 *
 *  - `protocol`: eg. `http`, `https`, `file`, etc.
 *  - `host`: eg. `www.mydomain.com`, `localhost`, etc.
 *  - `port`: eg. `80`
 *  - `path`: the path to the file (eg. `/folder/dir/index.html`)
 *  - `query`: the entire querystring, eg. `item=value&item2=value2`
 *  - `anchor`: the entire string after the `#` symbol
 **/
Pointer.parseURL = function parseURL(url) {
  var o = URL(url);

  return {
    protocol: o.attr('protocol'),
    host:     o.attr('host'),
    port:     o.attr('port'),
    path:     o.attr('path'),
    query:    o.attr('query'),
    anchor:   o.attr('fragment')
  };
};

// MODULE EXPORTS //////////////////////////////////////////////////////////////

module.exports = Pointer;
<commit_msg>Refaactor routes storage<commit_after>/**
 *  class Pointer
 **/

'use strict';

var Route = require('./pointer/route');
var URL   = require('./pointer/url');

/**
 *  new Pointer([routes])
 *  - routes (Object): Routes map in form of `pattern: options` pairs.
 *
 *  Creates new instance of router, prefilled with given routes (if provided).
 *
 *
 *  ##### See Also
 *
 *  - [[Pointer#addRoute]]
 **/
function Pointer(routes) {
  var k;

  // all routes grouped by prefixes
  //
  //  {
  //    'example.com': {
  //      'https': {
  //        keys:   [ '' ],
  //        groups: {
  //          '': {
  //            re: null,
  //            routes: [ Route(), ... ]
  //          }
  //        }
  //      },
  //      '*': { // any protocol
  //        keys:   [ '', '/admin' ],
  //        groups: {
  //          '': {
  //            re: null,
  //            routes: [ Route(), ... ]
  //          },
  //          '/admin': {
  //            re: /^\/admin/,
  //            routes: [ Route(), ... ]
  //          }
  //        }
  //      }
  //    }
  //  }
  //
  this.__routes__ = {};

  // routes grouped by names (needed for linkTo)
  this.__named_routes__ = {};

  // prefill routes if map provided
  if (routes) {
    for (k in routes) {
      if (routes.hasOwnProperty(k)) {
        this.addRoute(k, routes[k]);
      }
    }
  }
}

/** alias of: Pointer.new
 *  Pointer.create([routes]) -> Pointer
 *
 *  Constructor proxy.
 **/
Pointer.create = function create(routes) {
  return new Pointer(routes);
};

// helper to parse URL into proto, host and path respecting our internal
// structures, so that proto|path is `*` if it's not provided.
//
function getURLParts(url) {
  var parsed = URL(url);

  return {
    proto:  parsed.attr('protocol') || '*',
    host:   parsed.attr('host') || '*',
    path:   parsed.attr('relative') || ''
  };
}

/**
 *  Pointer#addRoute(pattern[, options]) -> Route
 *  - pattern (String): Pattern as for [[Route.new]]
 *  - options (Object): Route options, see below.
 *
 *  Create and add new route.
 *
 *
 *  ##### Options
 *
 *  Second argument of `addRoute` is options. It consist of following keys:
 *
 *  -   *name*    (Optional, String): name of the route.
 *      Used to build URLs with [[Pointer#linkTo]].
 *      Several routes may have same name, see [[Pointer#linkTo]] for
 *      detailed information.
 *
 *  -   *prefix*  (Optional, String): think of it as of mount point.
 *      You can use `prefix()` as well, which is a syntax sugar
 *      that sets this option.
 *
 *  -   *params*  (Optional, Object): options of params in the route.
 *      See [[Route.new]] for details.
 *
 *  -   *meta* (Optional, Mixed): meta data to mix into match object.
 *      See [[Route.new]] for details.
 *
 *
 *  ##### See Also:
 *
 *  - [[Route.new]]
 **/
Pointer.prototype.addRoute = function addRoute(pattern, options) {
  var route,
      prefix  = getURLParts(options.prefix || ''),
      host    = prefix.host,
      proto   = prefix.proto,
      path    = prefix.path,
      store   = this.__routes__;

  options = options || {};
  route   = new Route(pattern, options.params, options.meta, options.prefix);

  //
  // save named route
  //

  if (options.name) {
    if (!this.__named_routes__[options.name]) {
      this.__named_routes__[options.name] = [];
    }

    this.__named_routes__[options.name].push(route);
  }

  //
  // save route under corresponding host/port
  //

  if (!store[host]) {
    store[host] = {};
  }

  if (!store[host][proto]) {
    store[host][proto] = { groups: {} };
  }

  // invalidate known keys in case if addRoute
  // was called after keys were populated by matcher
  store[host][proto].keys = null;

  if (!store[host][proto].groups[path]) {
    store[host][proto].groups[path] = {
      re:     prefix.path ? new RegExp('^' + prefix.path) : null,
      routes: []
    };
  }

  store[host][proto].groups[path].routes.push(route);

  return route;
};

// sorter function for the Array#sort to sort strings by their length.
// longest comes first:
//
//      [ 'a', 'cba', 'ba' ].sort(reverseSortByStringLength);
//      // -> [ 'cba', 'ba', 'a' ]
//
function reverseSortByStringLength(a, b) {
  a = String(a).length;
  b = String(b).length;

  if (a === b) {
    return 0;
  }

  // longest strings comes first
  return (a > b) ? -1 : 1;
}

// returns object's keys sorted alphabeticaly in descending order
//
function getSortedKeys(obj) {
  var keys = [], k;

  for (k in obj) {
    if (obj.hasOwnProperty(k)) {
      keys.push(k);
    }
  }

  return keys.sort(reverseSortByStringLength);
}

// iterates through array calling iterator on each element.
// stops as soon as iterator return non-falsy value, and returns this value
//
function find(arr, iter) {
  var i, l, result;

  for (i = 0, l = arr.length; i < l; i++) {
    result = iter(arr[i]);

    if (result) {
      return result;
    }
  }

  return result;
}

/**
 *  Pointer#match(url) -> Object|Null
 *  - url (String): URL to find mathing route for.
 *
 *  Returns first matching route or false if none found.
 *  See [[Route#match]] for details of matched data Object.
 **/
Pointer.prototype.match = function match(url) {
  var self = this,
      data = getURLParts(url);

  return find('*' === data.host ? [ '*' ] : [ data.host, '*' ], function (host) {
    if (!self.__routes__[host]) {
      return;
    }

    return find('*' === data.proto ? [ '*' ] : [ data.proto, '*' ], function (proto) {
      var store = self.__routes__[host][proto], keys;

      if (!store) {
        return;
      }

      keys = store.keys || (store.keys = getSortedKeys(store.groups));

      return find(keys, function (prefix) {
        var group = store.groups[prefix], path = data.path;

        // prefix can be a regexp of path or null
        if (null !== group.re) {
          path = path.replace(group.re, '');

          // prefix regexp removed nothing - that means it does not match path
          if (path === data.path) {
            return;
          }
        }

        return find(group.routes, function (route) {
          return route.match(path);
        });
      });
    });
  }) || null;
};

/**
 *  Pointer#linkTo(name[, params]) -> String|Null
 *  - name (String): Name of the URL (or group of URLs).
 *  - params (Object): Params to feed to URL builder.
 *
 *  Creates pretty URL for the route registered with given `name`.
 *  If there were several routes registered with such `name`, the one that
 *  produce the longest URL is used.
 *
 *
 *  ##### See Also
 *
 *  - [[Route#buildURL]]
 **/
Pointer.prototype.linkTo = function linkTo(name, params) {
  var i, l, routes = this.__named_routes__[name] || [], url = null;

  // find route thet returns longest URL with given params
  for (i = 0, l = routes.length; i < l; i++) {
    var tmp = routes[i].buildURL(params);

    // we want return the longest matching
    if (null === url || url.length < tmp.length) {
      url = tmp;
    }
  }

  return url;
};

/**
 *  Pointer.parseURL(url) -> Object
 *  - url (String): URL to parse
 *
 *  Returns parts of parsed URL:
 *
 *  - `protocol`: eg. `http`, `https`, `file`, etc.
 *  - `host`: eg. `www.mydomain.com`, `localhost`, etc.
 *  - `port`: eg. `80`
 *  - `path`: the path to the file (eg. `/folder/dir/index.html`)
 *  - `query`: the entire querystring, eg. `item=value&item2=value2`
 *  - `anchor`: the entire string after the `#` symbol
 **/
Pointer.parseURL = function parseURL(url) {
  var o = URL(url);

  return {
    protocol: o.attr('protocol'),
    host:     o.attr('host'),
    port:     o.attr('port'),
    path:     o.attr('path'),
    query:    o.attr('query'),
    anchor:   o.attr('fragment')
  };
};

// MODULE EXPORTS //////////////////////////////////////////////////////////////

module.exports = Pointer;
<|endoftext|>


<commit_before>/**
 *  class Pointer
 **/

'use strict';

var each  = require('./pointer/common').each;
var Route = require('./pointer/route');
var URL   = require('./pointer/url');

/**
 *  new Pointer([routes])
 *  - routes (Object): Routes map in form of `pattern: options` pairs.
 *
 *  Creates new instance of router, prefilled with given routes (if provided).
 *
 *
 *  ##### See Also
 *
 *  - [[Pointer#addRoute]]
 **/
function Pointer(routes) {
  // all routes grouped by prefixes
  //
  //  {
  //    '*': [ Route(), ... ],
  //    '//example.com': [ Route(), ... ]
  //  }
  //
  this.__routes__ = {};

  // cache of RegExps for known prefixes, grouped by host and protocol:
  //
  //  {
  //    'example.com': {
  //      'https': {
  //        'https://users.example.com': null,
  //      },
  //      '*': { // any protocol
  //        '//example.com': null,
  //        '//example.com/admin': /^\/admin/
  //      }
  //    }
  //  }
  //
  this.__prefixes__ = {};

  // routes grouped by names (needed for linkTo)
  this.__named_routes__ = {};

  // prefill routes if map provided
  each(routes, function (options, pattern) {
    this.addRoute(pattern, options);
  }, this);
}

/** alias of: Pointer.new
 *  Pointer.create([routes]) -> Pointer
 *
 *  Constructor proxy.
 **/
Pointer.create = function create(routes) {
  return new Pointer(routes);
};

// helper to lazily initialize named stack inside object and push value into it
//
function lazyStackPush(obj, name, value) {
  if (!obj[name]) {
    obj[name] = [];
  }

  obj[name].push(value);
}

// helper to parse URL into proto, host and path respecting our internal
// structures, so that proto|path is `*` if it's not provided.
//
function parseURL(url) {
  var parsed = URL(url);

  return {
    proto:  parsed.attr('protocol') || '*',
    host:   parsed.attr('host') || '*',
    path:   parsed.attr('relative') || ''
  };
}

// prepares and saves regexp for the prefix under internal cache
//
function setPrefixRegExp(store, prefix) {
  var parsed = parseURL(prefix);

  store = store[parsed.host] || (store[parsed.host] = {});
  store = store[parsed.proto] || (store[parsed.proto] = {});
  store[prefix] = parsed.path ? new RegExp('^' + parsed.path) : null;
}

/**
 *  Pointer#addRoute(pattern[, options]) -> Route
 *  - pattern (String): Pattern as for [[Route.new]]
 *  - options (Object): Route options, see below.
 *
 *  Create and add new route.
 *
 *
 *  ##### Options
 *
 *  Second argument of `addRoute` is options. It consist of following keys:
 *
 *  -   *name*    (Optional, String): name of the route.
 *      Used to build URLs with [[Pointer#linkTo]].
 *      Several routes may have same name, see [[Pointer#linkTo]] for
 *      detailed information.
 *
 *  -   *prefix*  (Optional, String): think of it as of mount point.
 *      You can use `prefix()` as well, which is a syntax sugar
 *      that sets this option.
 *
 *  -   *params*  (Optional, Object): options of params in the route.
 *      See [[Route.new]] for details.
 *
 *  -   *meta* (Optional, Mixed): meta data to mix into match object.
 *      See [[Route.new]] for details.
 *
 *
 *  ##### See Also:
 *
 *  - [[Route.new]]
 **/
Pointer.prototype.addRoute = function addRoute(pattern, options) {
  var route;

  options = options || {};
  route   = new Route(pattern, options.params, options.meta, options.prefix);

  // save route under prefix (* - means no prefix)
  lazyStackPush(this.__routes__, options.prefix || '*', route);

  // save named route
  if (options.name) {
    lazyStackPush(this.__named_routes__, options.name, route);
  }

  if (options.prefix) {
    setPrefixRegExp(this.__prefixes__, options.prefix);
  }

  return route;
};

// returns protocol|host alternatives where to search prefix regexps
//
function variants(str) {
  return '*' === str ? [ '*' ] : [ str, '*' ];
}

// returns object's keys sorted alphabeticaly in descending order
//
function sorted_keys(obj) {
  var keys = [], k;

  for (k in obj) {
    if (obj.hasOwnProperty(k)) {
      keys.push(k);
    }
  }

  keys.sort();
  keys.reverse();

  return keys;
}

// iterates through array calling iterator on each element.
// stops as soon as iterator return non-falsy value, and returns this value
//
function find(arr, iter) {
  var result;

  each(arr, function (val) {
    result = iter(val);
    return !result;
  });

  return result;
}

/**
 *  Pointer#match(url) -> MatchData|Null
 *  - url (String): URL to find mathing route for.
 *
 *  Returns first matching route or false if none found.
 *  See [[Route#match]] for details of _MatchData_ object.
 **/
Pointer.prototype.match = function match(url) {
  var self    = this,
      parsed  = parseURL(url);

  return find(variants(parsed.host), function (host) {
    if (!self.__prefixes__[host]) {
      return;
    }

    return find(variants(parsed.proto), function (proto) {
      if (!self.__prefixes__[host][proto]) {
        return;
      }

      return find(sorted_keys(self.__prefixes__[host][proto]), function (prefix) {
        var path = parsed.path;

        // prefix can be a regexp of path or null
        if (self.__prefixes__[host][proto][prefix]) {
          path = path.replace(self.__prefixes__[host][proto][prefix], '');

          // prefix regexp removed nothing - that means it does not match path
          if (path === parsed.path) {
            return;
          }
        }

        return find(self.__routes__[prefix], function (route) {
          return route.match(path);
        });
      });
    });
  }) || find(self.__routes__['*'], function (route) {
    return route.match(parsed.path) || route.match(url);
  }) || null;
};

/**
 *  Pointer#linkTo(name[, params]) -> String|Null
 *  - name (String): Name of the URL (or group of URLs).
 *  - params (Object): Params to feed to URL builder.
 *
 *  Creates pretty URL for the route registered with given `name` and if
 *  [[Route#isValidParams]] of that route is positive.
 *
 *  If there were several routes registered with such `name`, the one that
 *  produce the longest URL is used.
 *
 *
 *  ##### See Also
 *
 *  - [[Route#buildURL]]
 **/
Pointer.prototype.linkTo = function linkTo(name, params) {
  var url = null;

  // find route thet returns longest URL with given params
  each(this.__named_routes__[name], function (route) {
    var tmp;

    // validate params for the route
    if (route.isValidParams(params)) {
      tmp = route.buildURL(params);
      // we want return the longest matching
      if (null === url || url.length < tmp.length) {
        url = tmp;
      }
    }
  });

  return url;
};

/**
 *  Pointer.createLinkBuilder(pattern[, params]) -> Function
 *  - pattern (String): Pattern as for [[Route.new]]
 *  - params (Object): Params as for [[Route.new]]
 *
 *  Returns function that can be used to build URL for given `pattern` with
 *  provided `params`.
 *
 *
 *  ##### Builder signature
 *
 *      function builder(params) -> String|Null
 *
 *
 *  ##### Example
 *
 *      var builder = Pointer.createLinkBuilder('/foo/{bar}', {
 *        bar: /[a-z]+/
 *      });
 *
 *      builder();              // -> null
 *      builder({bar: 123});    // -> null
 *      builder({bar: 'abc'});  // -> '/foo/abc'
 **/
Pointer.createLinkBuilder = function createLinkBuilder(pattern, params) {
  var r;

  // proxy to URL builder via detached route
  r = new Route(pattern, params);
  return function (params) {
    return r.buildURL(params || {});
  };
};

// MODULE EXPORTS //////////////////////////////////////////////////////////////

module.exports = Pointer;
<commit_msg>Export URL parser<commit_after>/**
 *  class Pointer
 **/

'use strict';

var each  = require('./pointer/common').each;
var Route = require('./pointer/route');
var URL   = require('./pointer/url');

/**
 *  new Pointer([routes])
 *  - routes (Object): Routes map in form of `pattern: options` pairs.
 *
 *  Creates new instance of router, prefilled with given routes (if provided).
 *
 *
 *  ##### See Also
 *
 *  - [[Pointer#addRoute]]
 **/
function Pointer(routes) {
  // all routes grouped by prefixes
  //
  //  {
  //    '*': [ Route(), ... ],
  //    '//example.com': [ Route(), ... ]
  //  }
  //
  this.__routes__ = {};

  // cache of RegExps for known prefixes, grouped by host and protocol:
  //
  //  {
  //    'example.com': {
  //      'https': {
  //        'https://users.example.com': null,
  //      },
  //      '*': { // any protocol
  //        '//example.com': null,
  //        '//example.com/admin': /^\/admin/
  //      }
  //    }
  //  }
  //
  this.__prefixes__ = {};

  // routes grouped by names (needed for linkTo)
  this.__named_routes__ = {};

  // prefill routes if map provided
  each(routes, function (options, pattern) {
    this.addRoute(pattern, options);
  }, this);
}

/** alias of: Pointer.new
 *  Pointer.create([routes]) -> Pointer
 *
 *  Constructor proxy.
 **/
Pointer.create = function create(routes) {
  return new Pointer(routes);
};

// helper to lazily initialize named stack inside object and push value into it
//
function lazyStackPush(obj, name, value) {
  if (!obj[name]) {
    obj[name] = [];
  }

  obj[name].push(value);
}

// helper to parse URL into proto, host and path respecting our internal
// structures, so that proto|path is `*` if it's not provided.
//
function getURLParts(url) {
  var parsed = URL(url);

  return {
    proto:  parsed.attr('protocol') || '*',
    host:   parsed.attr('host') || '*',
    path:   parsed.attr('relative') || ''
  };
}

// prepares and saves regexp for the prefix under internal cache
//
function setPrefixRegExp(store, prefix) {
  var parsed = getURLParts(prefix);

  store = store[parsed.host] || (store[parsed.host] = {});
  store = store[parsed.proto] || (store[parsed.proto] = {});
  store[prefix] = parsed.path ? new RegExp('^' + parsed.path) : null;
}

/**
 *  Pointer#addRoute(pattern[, options]) -> Route
 *  - pattern (String): Pattern as for [[Route.new]]
 *  - options (Object): Route options, see below.
 *
 *  Create and add new route.
 *
 *
 *  ##### Options
 *
 *  Second argument of `addRoute` is options. It consist of following keys:
 *
 *  -   *name*    (Optional, String): name of the route.
 *      Used to build URLs with [[Pointer#linkTo]].
 *      Several routes may have same name, see [[Pointer#linkTo]] for
 *      detailed information.
 *
 *  -   *prefix*  (Optional, String): think of it as of mount point.
 *      You can use `prefix()` as well, which is a syntax sugar
 *      that sets this option.
 *
 *  -   *params*  (Optional, Object): options of params in the route.
 *      See [[Route.new]] for details.
 *
 *  -   *meta* (Optional, Mixed): meta data to mix into match object.
 *      See [[Route.new]] for details.
 *
 *
 *  ##### See Also:
 *
 *  - [[Route.new]]
 **/
Pointer.prototype.addRoute = function addRoute(pattern, options) {
  var route;

  options = options || {};
  route   = new Route(pattern, options.params, options.meta, options.prefix);

  // save route under prefix (* - means no prefix)
  lazyStackPush(this.__routes__, options.prefix || '*', route);

  // save named route
  if (options.name) {
    lazyStackPush(this.__named_routes__, options.name, route);
  }

  if (options.prefix) {
    setPrefixRegExp(this.__prefixes__, options.prefix);
  }

  return route;
};

// returns protocol|host alternatives where to search prefix regexps
//
function variants(str) {
  return '*' === str ? [ '*' ] : [ str, '*' ];
}

// returns object's keys sorted alphabeticaly in descending order
//
function getSortedKeys(obj) {
  var keys = [], k;

  for (k in obj) {
    if (obj.hasOwnProperty(k)) {
      keys.push(k);
    }
  }

  keys.sort();
  keys.reverse();

  return keys;
}

// iterates through array calling iterator on each element.
// stops as soon as iterator return non-falsy value, and returns this value
//
function find(arr, iter) {
  var result;

  each(arr, function (val) {
    result = iter(val);
    return !result;
  });

  return result;
}

/**
 *  Pointer#match(url) -> MatchData|Null
 *  - url (String): URL to find mathing route for.
 *
 *  Returns first matching route or false if none found.
 *  See [[Route#match]] for details of _MatchData_ object.
 **/
Pointer.prototype.match = function match(url) {
  var self = this,
      data = getURLParts(url);

  return find(variants(data.host), function (host) {
    if (!self.__prefixes__[host]) {
      return;
    }

    return find(variants(data.proto), function (proto) {
      if (!self.__prefixes__[host][proto]) {
        return;
      }

      return find(getSortedKeys(self.__prefixes__[host][proto]), function (prefix) {
        var path = data.path;

        // prefix can be a regexp of path or null
        if (null !== self.__prefixes__[host][proto][prefix]) {
          path = path.replace(self.__prefixes__[host][proto][prefix], '');

          // prefix regexp removed nothing - that means it does not match path
          if (path === data.path) {
            return;
          }
        }

        return find(self.__routes__[prefix], function (route) {
          return route.match(path);
        });
      });
    });
  }) || find(self.__routes__['*'], function (route) {
    return route.match(data.path) || route.match(url);
  }) || null;
};

/**
 *  Pointer#linkTo(name[, params]) -> String|Null
 *  - name (String): Name of the URL (or group of URLs).
 *  - params (Object): Params to feed to URL builder.
 *
 *  Creates pretty URL for the route registered with given `name` and if
 *  [[Route#isValidParams]] of that route is positive.
 *
 *  If there were several routes registered with such `name`, the one that
 *  produce the longest URL is used.
 *
 *
 *  ##### See Also
 *
 *  - [[Route#buildURL]]
 **/
Pointer.prototype.linkTo = function linkTo(name, params) {
  var url = null;

  // find route thet returns longest URL with given params
  each(this.__named_routes__[name], function (route) {
    var tmp;

    // validate params for the route
    if (route.isValidParams(params)) {
      tmp = route.buildURL(params);
      // we want return the longest matching
      if (null === url || url.length < tmp.length) {
        url = tmp;
      }
    }
  });

  return url;
};

/**
 *  Pointer.createLinkBuilder(pattern[, params]) -> Function
 *  - pattern (String): Pattern as for [[Route.new]]
 *  - params (Object): Params as for [[Route.new]]
 *
 *  Returns function that can be used to build URL for given `pattern` with
 *  provided `params`.
 *
 *
 *  ##### Builder signature
 *
 *      function builder(params) -> String|Null
 *
 *
 *  ##### Example
 *
 *      var builder = Pointer.createLinkBuilder('/foo/{bar}', {
 *        bar: /[a-z]+/
 *      });
 *
 *      builder();              // -> null
 *      builder({bar: 123});    // -> null
 *      builder({bar: 'abc'});  // -> '/foo/abc'
 **/
Pointer.createLinkBuilder = function createLinkBuilder(pattern, params) {
  var r;

  // proxy to URL builder via detached route
  r = new Route(pattern, params);
  return function (params) {
    return r.buildURL(params || {});
  };
};

/**
 *  Pointer.parseURL(url) -> Object
 *  - url (String): URL to parse
 *
 *  Returns parts of parsed URL:
 *
 *  - `protocol`: eg. `http`, `https`, `file`, etc.
 *  - `host`: eg. `www.mydomain.com`, `localhost`, etc.
 *  - `port`: eg. `80`
 *  - `path`: the path to the file (eg. `/folder/dir/index.html`)
 *  - `query`: the entire querystring, eg. `item=value&item2=value2`
 *  - `anchor`: the entire string after the `#` symbol
 **/
Pointer.parseURL = function parseURL(url) {
  var o = URL(url);

  return {
    protocol: o.attr('protocol'),
    host:     o.attr('host'),
    port:     o.attr('port'),
    path:     o.attr('path'),
    query:    o.attr('query'),
    anchor:   o.attr('fragment')
  };
};

// MODULE EXPORTS //////////////////////////////////////////////////////////////

module.exports = Pointer;
<|endoftext|>


<commit_before>var util = require('util');
var http = require('http');
var https = require('https');
var EventEmitter = require('events').EventEmitter;

var GitHubApi = require('./extern/node-github').GitHubApi;
var sprintf = require('sprintf').sprintf;
var log = require('logmagic').local('buildbot-github.pollers');

var utils = require('./utils');

var BUILDER_STATUS_URL = '/json/builders/%s/builds/_all';

function Poller(name, options, interval) {
  EventEmitter.call(this);

  this._name = name;
  this._options = options;
  this._interval = interval;

  this._pollCache = {};
  this._intervalId = null;
}

util.inherits(Poller, EventEmitter);

Poller.prototype.initialize = function() {
  var self = this;
  log.info('Poller ${name} initialized. Polling for changes every ${interval} ms',
           {'name': this._name, 'interval': this._interval});

  this._intervalId = setInterval(function() {
    self._pollForChanges();
  }, this._interval);
};

Poller.prototype._pollForChanges = function() {
  throw new Error('Not implemented');
};

// Polls Github for new pull request
function GithubPoller(options, interval) {
  Poller.call(this, 'github', options, interval);
}

util.inherits(GithubPoller, Poller);

GithubPoller.prototype.initialize = function() {
  this._github = new GitHubApi(true);
  this._github.authenticate(this._options['username'],
                            this._options['token']);
  Poller.prototype.initialize.call(this);
};

GithubPoller.prototype._pollForChanges = function() {
  var self = this;

  function handleGotPullRequestDiscussions(err, pullRequest, discussion) {
    if (err) {
      log.error('Error while retrieving pull request #${id} discussions: ${err}',
                {'id': pullRequest['num'], 'err': err.message});
      return;
    }

    log.debug('New pull request has been found (#${id}), emitting an event',
             {'id': pullRequest['number']});
    self.emit('new_pull_request', pullRequest);
  }

  function handleGotPullRequestList(err, pullRequests) {
    var i, pullRequest, pullRequestsLen, id, issueUpdatedAt, cache, cacheObj;

    if (err) {
      log.error('Error while retrieving pull requests list: ${err}',
                {'err': err.message});
      return;
    }

    pullRequestsLen = pullRequests.length;
    for (i = 0; i < pullRequestsLen; i++) {
      pullRequest = pullRequests[i];
      id = pullRequest['number'];
      cache = self._pollCache[id];

      if (cache && ((cache['issue_updated_at'] ===
          pullRequest['issue_updated_at']) || (cache['build_forced'] === true) || (cache['build_pending'] === true))) {
        // Nothing has changed or a build has already been forced
        continue;
      }

      cacheObj = {
        'issue_updated_at': pullRequest['issue_updated_at'],
        'user': pullRequest['issue_user'],
        'diff_url': pullRequest['diff_url'],
        'html_url': pullRequest['html_ull'],
        'head_ref': pullRequest['head']['repository']['ref'],
        'head_sha': pullRequest['head']['repository']['sha'],
        'build_pending': false,
        'build_forced': false,
      };

      self._pollCache[id] = cacheObj;

      self._github.getPullApi().getDiscussion(self._options['user'],
                                              self._options['project'],
                                              id,
        function(err, pull) {
          if (err) {
            handleGotPullRequestDiscussions(err);
            return;
          }

          handleGotPullRequestDiscussions(null, pullRequest, pull.discussion);
      });
    }
  }

  this._github.getPullApi().getList(this._options['user'],
                                    this._options['project'],
                                    'open',
                                    handleGotPullRequestList);

};

// Polls buildbot for new builds
function BuildbotPoller(options, interval) {
  Poller.call(this, 'buildbot', options, interval);

  this._fetchBuildDataReqObj = null;
}

util.inherits(BuildbotPoller, Poller);

BuildbotPoller.prototype.initialize = function() {
  this._fetchBuildDataReqObj = {
    'host': this._options['host'],
    'port': this._options['port'],
    'path': sprintf(BUILDER_STATUS_URL, this._options['builder']),
    'method': 'GET'
  };

  this._http = (this._options['secure']) ? https : http;

  if (this._username && this._password) {
    var headers = utils.getAuthHeaders(this._username, this._password);
    this._fetchBuildDataReqObj.headers = headers;
  }

  Poller.prototype.initialize.call(this);
};

BuildbotPoller.prototype._pollForChanges = function() {
  var self = this;

  var request = this._http.get(this._fetchBuildDataReqObj, function(res) {
    var body = '';

    function handleData(chunk) {
      body += chunk;
    }

    function handleEnd() {
      body = body.toString();
      try {
        body = JSON.parse(body);
      }
      catch (err) {
        return;
      }

      self._handleGotBody(body);
    }

    res.on('data', handleData);
    res.on('end', handleEnd);

    if (res.statusCode !== 200) {
      var err = new Error('Failed fetching builder status data, statusCode !== 200');
      res.removeAllListeners('data');
      res.removeAllListeners('end');
      log.error(err.message);
      return;
    }
  });

  request.on('error', function(err) {
    log.error(err.message);
  });
};

BuildbotPoller.prototype._handleGotBody = function(body) {
  var self = this;
  var i, key, cache, build;

  for (key in body) {
    if (body.hasOwnProperty(key)) {
      key = key.toString();
      cache = self._pollCache[key];
      build = body[key];

      if (cache) {
        continue;
      }

      log.debug('Found a new build (${key}), emitting an event', {'key': key});
      self._pollCache[key] = build;
      self.emit('new_build');
    }
  }
};

exports.GithubPoller = GithubPoller;
exports.BuildbotPoller = BuildbotPoller;
<commit_msg>Emit build data.<commit_after>var util = require('util');
var http = require('http');
var https = require('https');
var EventEmitter = require('events').EventEmitter;

var GitHubApi = require('./extern/node-github').GitHubApi;
var sprintf = require('sprintf').sprintf;
var log = require('logmagic').local('buildbot-github.pollers');

var utils = require('./utils');

var BUILDER_STATUS_URL = '/json/builders/%s/builds/_all';

function Poller(name, options, interval) {
  EventEmitter.call(this);

  this._name = name;
  this._options = options;
  this._interval = interval;

  this._pollCache = {};
  this._intervalId = null;
}

util.inherits(Poller, EventEmitter);

Poller.prototype.initialize = function() {
  var self = this;
  log.info('Poller ${name} initialized. Polling for changes every ${interval} ms',
           {'name': this._name, 'interval': this._interval});

  this._intervalId = setInterval(function() {
    self._pollForChanges();
  }, this._interval);
};

Poller.prototype._pollForChanges = function() {
  throw new Error('Not implemented');
};

// Polls Github for new pull request
function GithubPoller(options, interval) {
  Poller.call(this, 'github', options, interval);
}

util.inherits(GithubPoller, Poller);

GithubPoller.prototype.initialize = function() {
  this._github = new GitHubApi(true);
  this._github.authenticate(this._options['username'],
                            this._options['token']);
  Poller.prototype.initialize.call(this);
};

GithubPoller.prototype._pollForChanges = function() {
  var self = this;

  function handleGotPullRequestDiscussions(err, pullRequest, discussion) {
    if (err) {
      log.error('Error while retrieving pull request #${id} discussions: ${err}',
                {'id': pullRequest['num'], 'err': err.message});
      return;
    }

    log.debug('New pull request has been found (#${id}), emitting an event',
             {'id': pullRequest['number']});
    self.emit('new_pull_request', pullRequest);
  }

  function handleGotPullRequestList(err, pullRequests) {
    var i, pullRequest, pullRequestsLen, id, issueUpdatedAt, cache, cacheObj;

    if (err) {
      log.error('Error while retrieving pull requests list: ${err}',
                {'err': err.message});
      return;
    }

    pullRequestsLen = pullRequests.length;
    for (i = 0; i < pullRequestsLen; i++) {
      pullRequest = pullRequests[i];
      id = pullRequest['number'];
      cache = self._pollCache[id];

      if (cache && ((cache['issue_updated_at'] ===
          pullRequest['issue_updated_at']) || (cache['build_forced'] === true) || (cache['build_pending'] === true))) {
        // Nothing has changed or a build has already been forced
        continue;
      }

      cacheObj = {
        'issue_updated_at': pullRequest['issue_updated_at'],
        'user': pullRequest['issue_user'],
        'diff_url': pullRequest['diff_url'],
        'html_url': pullRequest['html_ull'],
        'head_ref': pullRequest['head']['repository']['ref'],
        'head_sha': pullRequest['head']['repository']['sha'],
        'build_pending': false,
        'build_forced': false,
      };

      self._pollCache[id] = cacheObj;

      self._github.getPullApi().getDiscussion(self._options['user'],
                                              self._options['project'],
                                              id,
        function(err, pull) {
          if (err) {
            handleGotPullRequestDiscussions(err);
            return;
          }

          handleGotPullRequestDiscussions(null, pullRequest, pull.discussion);
      });
    }
  }

  this._github.getPullApi().getList(this._options['user'],
                                    this._options['project'],
                                    'open',
                                    handleGotPullRequestList);

};

// Polls buildbot for new builds
function BuildbotPoller(options, interval) {
  Poller.call(this, 'buildbot', options, interval);

  this._fetchBuildDataReqObj = null;
}

util.inherits(BuildbotPoller, Poller);

BuildbotPoller.prototype.initialize = function() {
  this._fetchBuildDataReqObj = {
    'host': this._options['host'],
    'port': this._options['port'],
    'path': sprintf(BUILDER_STATUS_URL, this._options['builder']),
    'method': 'GET'
  };

  this._http = (this._options['secure']) ? https : http;

  if (this._username && this._password) {
    var headers = utils.getAuthHeaders(this._username, this._password);
    this._fetchBuildDataReqObj.headers = headers;
  }

  Poller.prototype.initialize.call(this);
};

BuildbotPoller.prototype._pollForChanges = function() {
  var self = this;

  var request = this._http.get(this._fetchBuildDataReqObj, function(res) {
    var body = '';

    function handleData(chunk) {
      body += chunk;
    }

    function handleEnd() {
      body = body.toString();
      try {
        body = JSON.parse(body);
      }
      catch (err) {
        return;
      }

      self._handleGotBody(body);
    }

    res.on('data', handleData);
    res.on('end', handleEnd);

    if (res.statusCode !== 200) {
      var err = new Error('Failed fetching builder status data, statusCode !== 200');
      res.removeAllListeners('data');
      res.removeAllListeners('end');
      log.error(err.message);
      return;
    }
  });

  request.on('error', function(err) {
    log.error(err.message);
  });
};

BuildbotPoller.prototype._handleGotBody = function(body) {
  var self = this;
  var i, key, cache, build;

  for (key in body) {
    if (body.hasOwnProperty(key)) {
      key = key.toString();
      cache = self._pollCache[key];
      build = body[key];

      if (cache) {
        continue;
      }

      log.debug('Found a new build (${key}), emitting an event', {'key': key});
      self._pollCache[key] = build;
      self.emit('new_build', build);
    }
  }
};

exports.GithubPoller = GithubPoller;
exports.BuildbotPoller = BuildbotPoller;
<|endoftext|>


<commit_before>'use strict';

function makeSymbol(name) {
  if (typeof Symbol === 'function') {
    return Symbol(name);
  }
  return '_' + name;
}

/**
 * Provides provide variable access.
 *
 * Example usage:
 *
 * const $private = require('./private').create();
 * $private(object).connection = {};
 *
 * @return {Accessor}
 */
exports.create = function () {
  var PRIVATE = Symbol('private');
  var $private  = function (obj) { return obj[PRIVATE] || (obj[PRIVATE] = {}); };
  return $private;
};
<commit_msg>0.10.x fix<commit_after>'use strict';

function makeSymbol(name) {
  if (typeof Symbol === 'function') {
    return Symbol(name);
  }
  return '_' + name;
}

/**
 * Provides provide variable access.
 *
 * Example usage:
 *
 * const $private = require('./private').create();
 * $private(object).connection = {};
 *
 * @return {Accessor}
 */
exports.create = function () {
  var PRIVATE = makeSymbol('private');
  var $private  = function (obj) { return obj[PRIVATE] || (obj[PRIVATE] = {}); };
  return $private;
};
<|endoftext|>


<commit_before>/**
 * Created by AlexanderC on 11/16/15.
 */

'use strict';

var cp = require('child_process');
var path = require('path');
var fs = require('fs');
var crypto = require('crypto');

function _process(cmd, args, opts) {

  /**
   * @param {String} str
   * @returns {String}
   */
  function md5(str) {
    var md5sum = crypto.createHash('md5');

    md5sum.update(str);

    return md5sum.digest('hex');
  }

  return {
    _process: null,
    _pidFile: null,

    /**
     * @returns {pid|Function|null}
     */
    pid: function() {
      return this._process ? this._process.pid : null;
    },

    /**
     * @param {String} dir
     * @returns {String}
     */
    getPidFileFromDir: function(dir) {
      var parts = JSON.stringify(args) + JSON.stringify(opts);

      return path.join(dir, '.' + md5(cmd) + md5(parts));
    },

    /**
     * Start process
     *
     * @param {String} pidFile
     * @returns {ChildProcess}
     */
    start: function(pidFile) {
      if (!pidFile) {
        throw new Error('PID file is required');
      }

      this._pidFile = pidFile;

      var runningProcessPid = this.pid() || this._getOldProcessPid();

      if (runningProcessPid) {
        throw new Error('Process has already been started (PID #' + runningProcessPid + ')');
      }

      this._process = cp.spawn(cmd, args, opts);

      this._process.on('exit', this._cleanup);
      this._process.on('uncaughtException', this._cleanup);
      this._process.on('error', this._cleanup);

      this._persistPid();

      return this._process;
    },

    /**
     * Stop process
     */
    stop: function() {
      this._cleanup();

      this._process = null;
      this._pidFile = null;
    },

    /**
     * @private
     */
    _cleanup: function() {
      this._removePidFile();
    },

    /**
     * @private
     */
    _persistPid: function() {
      fs.writeFileSync(this._pidFile, this.pid());
    },

    /**
     * @private
     */
    _removePidFile: function() {
      fs.unlinkSync(this._pidFile);
    },

    /**
     * @returns {String|null}
     * @private
     */
    _getOldProcessPid: function() {
      if (fs.existsSync(this._pidFile)) {
        var oldPid = fs.readFileSync(this._pidFile).toString();

        try {
          process.kill(oldPid, 0);

          return oldPid;
        } catch(error) {
          // @todo: force old pid file removal?
          //fs.unlinkSync(this._pidFile);
        }
      }

      return null;
    }
  };
}

module.exports = _process;
<commit_msg>Update process.js<commit_after>/**
 * Created by AlexanderC on 11/16/15.
 */

'use strict';

var cp = require('child_process');
var path = require('path');
var fs = require('fs');
var crypto = require('crypto');

function _process(cmd, args, opts) {

  /**
   * @param {String} str
   * @returns {String}
   */
  function md5(str) {
    var md5sum = crypto.createHash('md5');

    md5sum.update(str);

    return md5sum.digest('hex');
  }

  return {
    _process: null,
    _pidFile: null,

    /**
     * @returns {pid|Function|null}
     */
    pid: function() {
      return this._process ? this._process.pid : null;
    },

    /**
     * @param {String} dir
     * @returns {String}
     */
    getPidFileFromDir: function(dir) {
      var parts = JSON.stringify(args) + JSON.stringify(opts);

      return path.join(dir, '.' + md5(cmd) + md5(parts));
    },

    /**
     * Start process
     *
     * @param {String} pidFile
     * @returns {ChildProcess}
     */
    start: function(pidFile) {
      if (!pidFile) {
        throw new Error('PID file is required');
      }

      this._pidFile = pidFile;

      var runningProcessPid = this.pid() || this._getOldProcessPid();

      if (runningProcessPid) {
        throw new Error('Process has already been started (PID #' + runningProcessPid + ')');
      }

      this._process = cp.spawn(cmd, args, opts);

      this._process.on('exit', this._cleanup.bind(this));
      this._process.on('uncaughtException', this._cleanup.bind(this));
      this._process.on('error', this._cleanup.bind(this));

      this._persistPid();

      return this._process;
    },

    /**
     * Stop process
     */
    stop: function() {
      this._cleanup();

      this._process = null;
      this._pidFile = null;
    },

    /**
     * @private
     */
    _cleanup: function() {
      this._removePidFile();
    },

    /**
     * @private
     */
    _persistPid: function() {
      fs.writeFileSync(this._pidFile, this.pid());
    },

    /**
     * @private
     */
    _removePidFile: function() {
      try {
        fs.unlinkSync(this._pidFile);
      } catch (error) {   }
    },

    /**
     * @returns {String|null}
     * @private
     */
    _getOldProcessPid: function() {
      if (fs.existsSync(this._pidFile)) {
        var oldPid = fs.readFileSync(this._pidFile).toString();

        try {
          process.kill(oldPid, 0);

          return oldPid;
        } catch(error) {
          // @todo: force old pid file removal?
          //fs.unlinkSync(this._pidFile);
        }
      }

      return null;
    }
  };
}

module.exports = _process;
<|endoftext|>


<commit_before>var path = require('path');
var fs = require('fs');
var _ = require('underscore');
var Dependencies = require('./dependencies/dependencies');
var Config = require('./config');
var Meteor = require('./meteor');
var Command = require('./command');
var wrench = require('wrench');
var exec = require('child_process').exec;

// The project is the current directory's personal version of meteor,
// complete with its own set of packages.
// it installs into ./meteor/meteorite

Project = function(root, meteorArgs) {

  // Figure out all the paths we'll need to know
  this.root = root;
  this.meteorArgs = meteorArgs;
  this.smartJsonPath = path.join(this.root, 'smart.json');
  this.smartLockPath = path.join(this.root, 'smart.lock');
  this.packagesRoot = path.join(this.root, 'packages');

  // set a base meteor if it's specified in the args (or a default one if not)
  this.meteor = new Meteor(meteorArgs);
};

// read the config from smart.lock if it exists
Project.prototype.initFromLock = function() {
  var self = this;

  if (fs.existsSync(this.smartLockPath)) {
    var data = fs.readFileSync(this.smartLockPath).toString();
    var lockData = JSON.parse(data);

    // embed the root path in all this data
    lockData.meteor.root = self.root;
    _.each(lockData.dependencies.basePackages, function(pkg) {
      pkg.root = self.root;
    });
    _.each(lockData.dependencies.packages, function(pkg) {
      pkg.root = self.root;
    });

    this.meteor = new Meteor(lockData.meteor);
    this.dependencies = Dependencies.newFromLockJson(lockData.dependencies);
  }
};

// have a look in smart.json, see if it's different to what we have from smart.lock
Project.prototype.checkSmartJson = function(forceUpdate) {

  // this is the config specified in smart.json
  var config = new Config(this.root);

  var newMeteor = new Meteor(config.meteor);
  // when running in the context of a package, we are the only required package
  if (config.name) {
    var specifier = {};
    specifier[config.name] = {path: "."};
    var newDeps = new Dependencies(specifier);
  } else {
    var newDeps = new Dependencies(config.packages);
  }

  if (forceUpdate || !this.meteor.equals(newMeteor) || !this.dependencies || !this.dependencies.equalBase(newDeps)) {

    if (!forceUpdate && this.dependencies)
      console.log('smart.json changed.. installing from smart.json');

    this.lockChanged = true;
    this.meteor = newMeteor;
    this.dependencies = newDeps;
  }
};

// FIXME - this doesn't actually fetch the packages.
// we only fetch them when we are about to install them, or we need to
// take a look inside them. I think this is reasonable.
Project.prototype.fetch = function(fn, forceUpdate) {
  var self = this;

  // prepare dependencies and meteor
  self.initFromLock();
  self.checkSmartJson(forceUpdate);

  // Ensure the right version of meteor has been fetched
  var buildDevBundle = !! self.meteorArgs['build-dev-bundle'];
  self.meteor.prepare(buildDevBundle, function() {

    // resolving dependencies fetches them. We need to check otherwise
    if (!self.dependencies.resolved()) {

      console.verbose('Resolving dependency tree');

      self.dependencies.resolve(self.meteorArgs.force, function(err, conflicts) {

        _.each(conflicts, function(conflict, name) {
          console.log(('Problem installing ' + name.bold).red);
          console.log(("  ✘ " + conflict).red);
        });

        if (err) {
          console.log(err.message.red);
          process.exit(1);
        }

        fn();
      });
    } else {
      fn();
    }
  });
};

Project.prototype.uninstall = function() {
  var self = this;

  // for now, remove anything in packages/ that is a symlink
  if (fs.existsSync(this.packagesRoot)) {
    var dirs = fs.readdirSync(this.packagesRoot);

    _.each(dirs, function(dir) {
      var dirPath = path.join(self.packagesRoot, dir);

      if (fs.lstatSync(dirPath).isSymbolicLink())
        fs.unlink(dirPath)
    });
  }
};

// either install from smart.lock or prepare smart.lock and do so
Project.prototype.install = function(fn, forceUpdate) {
  var self = this;

  self._optimizeFS();

  // Fetch everything the project needs
  self.fetch(function() {

    // ensure that the installRoot exists
    if (! self.dependencies.isEmpty())
      wrench.mkdirSyncRecursive(self.packagesRoot);

    // Link each package into installRoot
    self.dependencies.installInto(self, function(packagesInstalled) {
      console.log();
      console.log('Done installing smart packages'.bold);

      // install the smart.lock file
      if (fs.existsSync(self.smartJsonPath) && (self.lockChanged || !fs.existsSync(self.smartLockPath)))
        self.writeLockFile();

      fn();
    });
  }, forceUpdate);
};

// if there's no dependencies we don't have to install
Project.prototype.needsToInstall = function() {
  return !this.dependencies.isEmpty();
}

// prepare a new smart.lock, then install
Project.prototype.update = function(fn) {

  this.install(fn, true);
};

Project.prototype.execute = function(args, fn) {
  var self = this;

  if (self.meteorArgs.version)
    console.suppress();

  console.log();
  console.log("Stand back while Meteorite does its thing".bold);

  // TODO -- what do we do here if not installed? I'm not sure we just go ahead
  //   and install, we should probably abort and tell them
  self.install(function() {

    console.log();
    console.log("Ok, everything's ready. Here comes Meteor!".green);
    console.log();

    if (self.meteorArgs.version)
      console.unsuppress();

    self.meteor.execute(args, self.packagesRoot, fn);
  });
};

// assumes that we are installed.
Project.prototype.isUsing = function(packageName, fn) {
  var self = this;

  self.install(function() {
    return self.meteor.isUsing(packageName, fn);
  });
}

// ensure a named package is installed
//
// NOTE: Right now, if the package is already available (included in meteor, already in smart.json)
// we ignore the version, and just stick with what we have
//
// TODO: In the future a version # would override anything in meteor + rewrite smart.json
// but right now it's TBH to overwrite meteor's packages.
Project.prototype.installPackage = function(pkgName, version, fn) {
  var self = this;

  // first ensure we are fetched, so we know _all_ the packages that are available
  self.fetch(function() {

    self.hasPackage(pkgName, function(check) {
      // if we have the package already
      if (check)
        return fn();

      // better check that the package exists on atmosphere
      Atmosphere.package(pkgName, function(atmosphere_defn) {

        if (!atmosphere_defn)
          throw("Package named " + pkgName + " doesn't exist in your meteor installation, smart.json, or on atmosphere");

        // ok, it's not installed. So we need to add it (permanently) to the smart.json
        // and clear our dependencies
        var smartJson = self.readSmartJson();
        var defn = {}
        if (version)
          defn.version = version;
        smartJson.packages = smartJson.packages || {};
        smartJson.packages[pkgName] = defn;
        self.writeSmartJson(smartJson);

        // maybe a hack to read it back out from disk, but not a big deal I don't think
        self.checkSmartJson(true);

        fn();
      });
    });
  });
}

// remove a package that is listed in smart.json (if it is indeed listed there)
Project.prototype.uninstallPackage = function(pkgName, fn) {
  var self = this;

  // we need to be fetched so we have all the information
  // self.fetch(function() {

  var pkg = self.dependencies.basePackages[pkgName];
  if (pkg) {
    // remove that bad boy from smart.json and reset our dependencies
    var smartJson = self.readSmartJson();
    delete smartJson.packages[pkgName];
    self.writeSmartJson(smartJson);

    // maybe a hack to read it back out from disk, but not a big deal I don't think
    self.checkSmartJson(true);

    // now bring everything in line with smart.json
    self.install(function() {
      // finally remove package from packages/
      pkg.removeFrom(self, fn);
    });
  } else {
    fn();
  }
}

// migrate to troposphere! Woot
Project.prototype.migrate = function(fn) {
  var self = this;

  console.log("Updating all references to packages in .meteor/packages".green);
  var packages = fs.readFileSync(path.join(self.root, '.meteor', 'packages')).toString();

  // work from smart.json grabbing all versions of dependencies
  self.initFromLock();
  var migrated = {}, nonMigrated = {}, smartJson = this.readSmartJson();

  var done = function() {
    // write .meteor/packages
    console.log("2.1 Writing packages.js".green);
    fs.writeFileSync(path.join(self.root, '.meteor', 'packages'), packages);

    if (_.keys(nonMigrated).length) {
      console.log("2.2. Rewriting smart.json".green);
      self.writeSmartJson(smartJson);
    } else {
      console.log("2.2. Removing smart.json/smart.lock".green);

      // remove smart.json/smart.lock
      fs.unlinkSync(path.join(self.root, 'smart.json'));
      fs.unlinkSync(path.join(self.root, 'smart.lock'));
    }

    // remove packages/name for each package
    _.each(migrated, function(pkg) {
      var packagePath = path.join(self.root, 'packages', pkg.name);
      if (fs.existsSync(packagePath))
        fs.unlinkSync(packagePath);
    });

    // if packages dir only contains .gitignore, remove it
    var files = fs.readdirSync(path.join(self.root, 'packages'));
    if (files.length === 0 || (files.length === 1 && files[0] === '.gitignore') ) {
      wrench.rmdirSyncRecursive(path.join(self.root, 'packages'));
    }

    self.install(function() {
      console.log("3. Done. For more information, please check out https://hackpad.com/Migrating-Apps-UfPrM192vSQ".green);
      fn();
    });
  }

  var complete = 0;
  _.each(self.dependencies.basePackages, function(pkg) {
    var name = pkg.name;

    if (! pkg._fromAtmosphere) {
      nonMigrated[name] = pkg;
      complete += 1;
      console.log(("1." + complete + "  Didn't migrate " + name + " as it was a non-atmosphere package").yellow);

      if (complete === _.keys(self.dependencies.basePackages).length)
        done()

      return;
    }

    var versionName = pkg.version;

    Atmosphere.package(name, function(pkg) {
      var bail = function() {
        var message = Array.prototype.join.call(arguments, ' ');
        console.log(message.red);
        console.log('');
        var message =
        console.log('If you want to continue, remove the offending package from smart.json, run `mrt install`, and try again.'.yellow);
        console.log('After you have successfully migrated, you can add it back but note:'.yellow);
        console.log('  You will NOT receive further updates to this package.'.red);
        console.log('See https://hackpad.com/Migrating-Apps-UfPrM192vSQ for more information.'.yellow);
        process.exit(1);
      }

      if (! pkg)
        bail("Error: no package named ", name, " found on Atmosphere");

      // either the version specified directly in smart.json, or the one
      //   resolved in smart.lock
      var searchFor = versionName;
      if (! searchFor)
        searchFor = self.dependencies.packages[name].source.config.tag.replace(/^v/, '');
      var version = _.find(pkg.versions, function(v) {
        return v.version === searchFor;
      });
      if (! version)
        bail("Error: couldn't find version", searchFor,
          "of package", name, "on Atmosphere");

      if (! version.troposphereIdentifier)
        bail("Error: The version", searchFor,
          "of package", name, "has not yet been migrated");

      var id = version.troposphereIdentifier;
      // we want to fix the version, not just say at least the version
      if (versionName)
        id = id.replace('@', '@=');

      // we are good, replace references to name with troposphereId
      packages = packages.replace(new RegExp('^' + name + '$', 'm'), id);

      // remove from smart.json
      delete smartJson.packages[name];

      migrated[name] = pkg;
      complete += 1;
      console.log(("1." + complete + ". Updated " + name + " to " + id).green);
      if (complete === _.keys(self.dependencies.basePackages).length)
        done()
    });
  });

  if (_.keys(self.dependencies.packages).length === 0)
    done();
}

// Is the package part of the meteor install, or is it a dependency?
//
// NOTE: assumes we have fetched. FIXME: figure out a better / systematic way
// to write code that has this sort of assumption
Project.prototype.hasPackage = function(pkgName, fn) {
  if (this.dependencies.packages[pkgName]) {
    return fn(true);
  }

  this.meteor.hasPackage(pkgName, fn)
};

// very simple version of what config does
Project.prototype.readSmartJson = function() {

  try {
    var rawConfig = fs.readFileSync(path.join(this.root, 'smart.json')).toString();
    return JSON.parse(rawConfig);

  } catch (err) {
    return {};
  }
};

Project.prototype.smartJson = function() {
  var data = {};

  if (!this.meteor.defaultMeteor)
    data.meteor = this.meteor.toJson();

  if (this.dependencies)
    data.packages = this.dependencies.toJson().basePackages;
  else
    data.packages = {};

  return data;
};

Project.prototype.writeSmartJson = function(json) {
  json = json || this.smartJson();

  // Make a nicely formated default json string
  var smartJsonString = JSON.stringify(json, null, 2) + "\n";

  // Write to disk
  if (fs.existsSync(this.root))
    fs.writeFileSync(this.smartJsonPath, smartJsonString);
};

Project.prototype.lockJson = function() {

  return {
    meteor: this.meteor.toJson(true),
    dependencies: this.dependencies.toJson(true)
  };
};

// write out into smart.lock
Project.prototype.writeLockFile = function() {

  var smartJsonString = JSON.stringify(this.lockJson(), null, 2) + "\n";
  fs.writeFileSync(this.smartLockPath, smartJsonString);
};

Project.prototype._optimizeFS = function() {
  var self = this;

  var deletable = [];

  // remove old .meteor/meteorite directory
  var oldInstallRoot = path.join(this.root, '.meteor', 'meteorite')
  if (fs.existsSync(oldInstallRoot)) {
    deletable.push(oldInstallRoot);
  }

  if (deletable.length > 0)
    console.log("Yay! We're optimizing your installation!".yellow.bold);

  _.each(deletable, function(filePath) {
    console.log("  ✘ ".red + ("Deleting " + filePath).grey);

    if (fs.lstatSync(filePath).isDirectory())
      wrench.rmdirSyncRecursive(filePath);
    else
      fs.unlink(filePath);
  });
};

module.exports = Project;

// var _debug = require('./debug');
// _.debugClass('Project');
<commit_msg>Made errors better.<commit_after>var path = require('path');
var fs = require('fs');
var _ = require('underscore');
var Dependencies = require('./dependencies/dependencies');
var Config = require('./config');
var Meteor = require('./meteor');
var Command = require('./command');
var wrench = require('wrench');
var exec = require('child_process').exec;

// The project is the current directory's personal version of meteor,
// complete with its own set of packages.
// it installs into ./meteor/meteorite

Project = function(root, meteorArgs) {

  // Figure out all the paths we'll need to know
  this.root = root;
  this.meteorArgs = meteorArgs;
  this.smartJsonPath = path.join(this.root, 'smart.json');
  this.smartLockPath = path.join(this.root, 'smart.lock');
  this.packagesRoot = path.join(this.root, 'packages');

  // set a base meteor if it's specified in the args (or a default one if not)
  this.meteor = new Meteor(meteorArgs);
};

// read the config from smart.lock if it exists
Project.prototype.initFromLock = function() {
  var self = this;

  if (fs.existsSync(this.smartLockPath)) {
    var data = fs.readFileSync(this.smartLockPath).toString();
    var lockData = JSON.parse(data);

    // embed the root path in all this data
    lockData.meteor.root = self.root;
    _.each(lockData.dependencies.basePackages, function(pkg) {
      pkg.root = self.root;
    });
    _.each(lockData.dependencies.packages, function(pkg) {
      pkg.root = self.root;
    });

    this.meteor = new Meteor(lockData.meteor);
    this.dependencies = Dependencies.newFromLockJson(lockData.dependencies);
  }
};

// have a look in smart.json, see if it's different to what we have from smart.lock
Project.prototype.checkSmartJson = function(forceUpdate) {

  // this is the config specified in smart.json
  var config = new Config(this.root);

  var newMeteor = new Meteor(config.meteor);
  // when running in the context of a package, we are the only required package
  if (config.name) {
    var specifier = {};
    specifier[config.name] = {path: "."};
    var newDeps = new Dependencies(specifier);
  } else {
    var newDeps = new Dependencies(config.packages);
  }

  if (forceUpdate || !this.meteor.equals(newMeteor) || !this.dependencies || !this.dependencies.equalBase(newDeps)) {

    if (!forceUpdate && this.dependencies)
      console.log('smart.json changed.. installing from smart.json');

    this.lockChanged = true;
    this.meteor = newMeteor;
    this.dependencies = newDeps;
  }
};

// FIXME - this doesn't actually fetch the packages.
// we only fetch them when we are about to install them, or we need to
// take a look inside them. I think this is reasonable.
Project.prototype.fetch = function(fn, forceUpdate) {
  var self = this;

  // prepare dependencies and meteor
  self.initFromLock();
  self.checkSmartJson(forceUpdate);

  // Ensure the right version of meteor has been fetched
  var buildDevBundle = !! self.meteorArgs['build-dev-bundle'];
  self.meteor.prepare(buildDevBundle, function() {

    // resolving dependencies fetches them. We need to check otherwise
    if (!self.dependencies.resolved()) {

      console.verbose('Resolving dependency tree');

      self.dependencies.resolve(self.meteorArgs.force, function(err, conflicts) {

        _.each(conflicts, function(conflict, name) {
          console.log(('Problem installing ' + name.bold).red);
          console.log(("  ✘ " + conflict).red);
        });

        if (err) {
          console.log(err.message.red);
          process.exit(1);
        }

        fn();
      });
    } else {
      fn();
    }
  });
};

Project.prototype.uninstall = function() {
  var self = this;

  // for now, remove anything in packages/ that is a symlink
  if (fs.existsSync(this.packagesRoot)) {
    var dirs = fs.readdirSync(this.packagesRoot);

    _.each(dirs, function(dir) {
      var dirPath = path.join(self.packagesRoot, dir);

      if (fs.lstatSync(dirPath).isSymbolicLink())
        fs.unlink(dirPath)
    });
  }
};

// either install from smart.lock or prepare smart.lock and do so
Project.prototype.install = function(fn, forceUpdate) {
  var self = this;

  self._optimizeFS();

  // Fetch everything the project needs
  self.fetch(function() {

    // ensure that the installRoot exists
    if (! self.dependencies.isEmpty())
      wrench.mkdirSyncRecursive(self.packagesRoot);

    // Link each package into installRoot
    self.dependencies.installInto(self, function(packagesInstalled) {
      console.log();
      console.log('Done installing smart packages'.bold);

      // install the smart.lock file
      if (fs.existsSync(self.smartJsonPath) && (self.lockChanged || !fs.existsSync(self.smartLockPath)))
        self.writeLockFile();

      fn();
    });
  }, forceUpdate);
};

// if there's no dependencies we don't have to install
Project.prototype.needsToInstall = function() {
  return !this.dependencies.isEmpty();
}

// prepare a new smart.lock, then install
Project.prototype.update = function(fn) {

  this.install(fn, true);
};

Project.prototype.execute = function(args, fn) {
  var self = this;

  if (self.meteorArgs.version)
    console.suppress();

  console.log();
  console.log("Stand back while Meteorite does its thing".bold);

  // TODO -- what do we do here if not installed? I'm not sure we just go ahead
  //   and install, we should probably abort and tell them
  self.install(function() {

    console.log();
    console.log("Ok, everything's ready. Here comes Meteor!".green);
    console.log();

    if (self.meteorArgs.version)
      console.unsuppress();

    self.meteor.execute(args, self.packagesRoot, fn);
  });
};

// assumes that we are installed.
Project.prototype.isUsing = function(packageName, fn) {
  var self = this;

  self.install(function() {
    return self.meteor.isUsing(packageName, fn);
  });
}

// ensure a named package is installed
//
// NOTE: Right now, if the package is already available (included in meteor, already in smart.json)
// we ignore the version, and just stick with what we have
//
// TODO: In the future a version # would override anything in meteor + rewrite smart.json
// but right now it's TBH to overwrite meteor's packages.
Project.prototype.installPackage = function(pkgName, version, fn) {
  var self = this;

  // first ensure we are fetched, so we know _all_ the packages that are available
  self.fetch(function() {

    self.hasPackage(pkgName, function(check) {
      // if we have the package already
      if (check)
        return fn();

      // better check that the package exists on atmosphere
      Atmosphere.package(pkgName, function(atmosphere_defn) {

        if (!atmosphere_defn)
          throw("Package named " + pkgName + " doesn't exist in your meteor installation, smart.json, or on atmosphere");

        // ok, it's not installed. So we need to add it (permanently) to the smart.json
        // and clear our dependencies
        var smartJson = self.readSmartJson();
        var defn = {}
        if (version)
          defn.version = version;
        smartJson.packages = smartJson.packages || {};
        smartJson.packages[pkgName] = defn;
        self.writeSmartJson(smartJson);

        // maybe a hack to read it back out from disk, but not a big deal I don't think
        self.checkSmartJson(true);

        fn();
      });
    });
  });
}

// remove a package that is listed in smart.json (if it is indeed listed there)
Project.prototype.uninstallPackage = function(pkgName, fn) {
  var self = this;

  // we need to be fetched so we have all the information
  // self.fetch(function() {

  var pkg = self.dependencies.basePackages[pkgName];
  if (pkg) {
    // remove that bad boy from smart.json and reset our dependencies
    var smartJson = self.readSmartJson();
    delete smartJson.packages[pkgName];
    self.writeSmartJson(smartJson);

    // maybe a hack to read it back out from disk, but not a big deal I don't think
    self.checkSmartJson(true);

    // now bring everything in line with smart.json
    self.install(function() {
      // finally remove package from packages/
      pkg.removeFrom(self, fn);
    });
  } else {
    fn();
  }
}

// migrate to troposphere! Woot
Project.prototype.migrate = function(fn) {
  var self = this;

  console.log("Updating all references to packages in .meteor/packages".green);
  var packages = fs.readFileSync(path.join(self.root, '.meteor', 'packages')).toString();

  // work from smart.json grabbing all versions of dependencies
  self.initFromLock();
  var migrated = {}, nonMigrated = {}, failed = [], smartJson = this.readSmartJson();

  var done = function() {
    if (failed.length) {
      _.each(failed, function(message) {
        console.log(message.red);
      });
      console.log('');
      var message =
      console.log('If you want to continue, remove the offending package(s) from smart.json, run `mrt install`, and try again.'.yellow);
      console.log('After you have successfully migrated, you can add them back but note:'.yellow);
      console.log('  You will NOT receive further updates!.'.red);
      console.log('See https://hackpad.com/Migrating-Apps-UfPrM192vSQ for more information.'.yellow);
      process.exit(1);
    }

    // write .meteor/packages
    console.log("2.1 Writing packages.js".green);
    fs.writeFileSync(path.join(self.root, '.meteor', 'packages'), packages);

    if (_.keys(nonMigrated).length) {
      console.log("2.2. Rewriting smart.json".green);
      self.writeSmartJson(smartJson);
    } else {
      console.log("2.2. Removing smart.json/smart.lock".green);

      // remove smart.json/smart.lock
      fs.unlinkSync(path.join(self.root, 'smart.json'));
      fs.unlinkSync(path.join(self.root, 'smart.lock'));
    }

    // remove packages/name for each package
    _.each(migrated, function(pkg) {
      var packagePath = path.join(self.root, 'packages', pkg.name);
      if (fs.existsSync(packagePath))
        fs.unlinkSync(packagePath);
    });

    // if packages dir only contains .gitignore, remove it
    var files = fs.readdirSync(path.join(self.root, 'packages'));
    if (files.length === 0 || (files.length === 1 && files[0] === '.gitignore') ) {
      wrench.rmdirSyncRecursive(path.join(self.root, 'packages'));
    }

    self.install(function() {
      console.log("3. Done. For more information, please check out https://hackpad.com/Migrating-Apps-UfPrM192vSQ".green);
      fn();
    });
  }

  var complete = 0;
  var bail = function() {
    failed.push(Array.prototype.join.call(arguments, ' '));
    complete += 1;
  }

  _.each(self.dependencies.basePackages, function(pkg) {
    var name = pkg.name;

    if (! pkg._fromAtmosphere) {
      nonMigrated[name] = pkg;
      complete += 1;
      console.log(("1." + complete + "  Didn't migrate " + name + " as it was a non-atmosphere package").yellow);

      if (complete === _.keys(self.dependencies.basePackages).length)
        done()

      return;
    }

    var versionName = pkg.version;
    Atmosphere.package(name, function(pkg) {
      if (! pkg)
        return bail("Error: no package named ", name, " found on Atmosphere");

      // either the version specified directly in smart.json, or the one
      //   resolved in smart.lock
      var searchFor = versionName;
      if (! searchFor)
        searchFor = self.dependencies.packages[name].source.config.tag.replace(/^v/, '');
      var version = _.find(pkg.versions, function(v) {
        return v.version === searchFor;
      });
      if (! version)
       return bail("Error: couldn't find version", searchFor,
          "of package", name, "on Atmosphere");

      if (! version.troposphereIdentifier)
        return bail("Error: The version", searchFor,
          "of package", name, "has not yet been migrated");

      var id = version.troposphereIdentifier;
      // we want to fix the version, not just say at least the version
      if (versionName)
        id = id.replace('@', '@=');

      // we are good, replace references to name with troposphereId
      packages = packages.replace(new RegExp('^' + name + '$', 'm'), id);

      // remove from smart.json
      delete smartJson.packages[name];

      migrated[name] = pkg;
      complete += 1;
      console.log(("1." + complete + ". Will update " + name + " to " + id).green);
      if (complete === _.keys(self.dependencies.basePackages).length)
        done()
    });
  });

  if (_.keys(self.dependencies.packages).length === 0)
    done();
}

// Is the package part of the meteor install, or is it a dependency?
//
// NOTE: assumes we have fetched. FIXME: figure out a better / systematic way
// to write code that has this sort of assumption
Project.prototype.hasPackage = function(pkgName, fn) {
  if (this.dependencies.packages[pkgName]) {
    return fn(true);
  }

  this.meteor.hasPackage(pkgName, fn)
};

// very simple version of what config does
Project.prototype.readSmartJson = function() {

  try {
    var rawConfig = fs.readFileSync(path.join(this.root, 'smart.json')).toString();
    return JSON.parse(rawConfig);

  } catch (err) {
    return {};
  }
};

Project.prototype.smartJson = function() {
  var data = {};

  if (!this.meteor.defaultMeteor)
    data.meteor = this.meteor.toJson();

  if (this.dependencies)
    data.packages = this.dependencies.toJson().basePackages;
  else
    data.packages = {};

  return data;
};

Project.prototype.writeSmartJson = function(json) {
  json = json || this.smartJson();

  // Make a nicely formated default json string
  var smartJsonString = JSON.stringify(json, null, 2) + "\n";

  // Write to disk
  if (fs.existsSync(this.root))
    fs.writeFileSync(this.smartJsonPath, smartJsonString);
};

Project.prototype.lockJson = function() {

  return {
    meteor: this.meteor.toJson(true),
    dependencies: this.dependencies.toJson(true)
  };
};

// write out into smart.lock
Project.prototype.writeLockFile = function() {

  var smartJsonString = JSON.stringify(this.lockJson(), null, 2) + "\n";
  fs.writeFileSync(this.smartLockPath, smartJsonString);
};

Project.prototype._optimizeFS = function() {
  var self = this;

  var deletable = [];

  // remove old .meteor/meteorite directory
  var oldInstallRoot = path.join(this.root, '.meteor', 'meteorite')
  if (fs.existsSync(oldInstallRoot)) {
    deletable.push(oldInstallRoot);
  }

  if (deletable.length > 0)
    console.log("Yay! We're optimizing your installation!".yellow.bold);

  _.each(deletable, function(filePath) {
    console.log("  ✘ ".red + ("Deleting " + filePath).grey);

    if (fs.lstatSync(filePath).isDirectory())
      wrench.rmdirSyncRecursive(filePath);
    else
      fs.unlink(filePath);
  });
};

module.exports = Project;

// var _debug = require('./debug');
// _.debugClass('Project');
<|endoftext|>