angular/angular.js

View on GitHub
lib/versions/version-info.js

Summary

Maintainability
A
3 hrs
Test Coverage
'use strict';

var fs = require('fs');
var path = require('path');
var shell = require('shelljs');
var semver = require('semver');
var _ = require('lodash');

var process = require('process');
// We are only interested in whether this environment variable exists, hence the !!
var NO_REMOTE_REQUESTS = !!process.env['NG1_BUILD_NO_REMOTE_VERSION_REQUESTS'];
var versionSource = NO_REMOTE_REQUESTS ? 'local' : 'remote';

var currentPackage, previousVersions, cdnVersion;


/**
 * Load information about this project from the package.json
 * @return {Object} The package information
 */
var getPackage = function() {
  // Search up the folder hierarchy for the first package.json
  var packageFolder = path.resolve('.');
  while (!fs.existsSync(path.join(packageFolder, 'package.json'))) {
    var parent = path.dirname(packageFolder);
    if (parent === packageFolder) { break; }
    packageFolder = parent;
  }
  return JSON.parse(fs.readFileSync(path.join(packageFolder,'package.json'), 'UTF-8'));
};


/**
 * Parse the github URL for useful information
 * @return {Object} An object containing the github owner and repository name
 */
var getGitRepoInfo = function() {
  var GITURL_REGEX = /^https:\/\/github.com\/([^/]+)\/(.+).git$/;
  var match = GITURL_REGEX.exec(currentPackage.repository.url);
  var git = {
    owner: match[1],
    repo: match[2]
  };
  return git;
};



/**
 * Extract the code name from the tagged commit's message - it should contain the text of the form:
 * "codename(some-code-name)"
 * @param  {String} tagName Name of the tag to look in for the codename
 * @return {String}         The codename if found, otherwise null/undefined
 */
var getCodeName = function(tagName) {
  var gitCatOutput = shell.exec('git cat-file -p ' + tagName, {silent:true}).stdout;
  var tagMessage = gitCatOutput.match(/^.*codename.*$/mg)[0];
  var codeName = tagMessage && tagMessage.match(/codename\((.*)\)/)[1];
  if (!codeName) {
    throw new Error('Could not extract release code name. The message of tag ' + tagName +
      ' must match \'*codename(some release name)*\'');
  }
  return codeName;
};


/**
 * Compute a build segment for the version, from the CI build number and current commit SHA
 * @return {String} The build segment of the version
 */
function getBuild() {
  var hash = shell.exec('git rev-parse --short HEAD', {silent: true}).stdout.replace('\n', '');
  return 'sha.' + hash;
}

function checkBranchPattern(version, branchPattern) {
  // check that the version starts with the branch pattern minus its asterisk
  // e.g. branchPattern = '1.6.*'; version = '1.6.0-rc.0' => '1.6.' === '1.6.'
  return version.slice(0, branchPattern.length - 1) === branchPattern.replace('*', '');
}

/**
 * If the current commit is tagged as a version get that version
 * @return {SemVer} The version or null
 */
var getTaggedVersion = function() {
  var gitTagResult = shell.exec('git describe --exact-match', {silent:true});

  if (gitTagResult.code === 0) {
    var tag = gitTagResult.stdout.trim();
    var version = semver.parse(tag);

    if (version && checkBranchPattern(version.version, currentPackage.branchPattern)) {
      version.codeName = getCodeName(tag);
      version.full = version.version;
      version.branch = 'v' + currentPackage.branchPattern.replace('*', 'x');
      return version;
    }
  }

  return null;
};

/**
 * Get a collection of all the previous versions sorted by semantic version
 * @return {Array.<SemVer>} The collection of previous versions
 */
var getPreviousVersions =  function() {
  // If we are allowing remote requests then use the remote tags as the local clone might
  // not contain all commits when cloned with git clone --depth=...
  // Otherwise just use the tags in the local repository
  var repo_url = currentPackage.repository.url;
  var query = NO_REMOTE_REQUESTS ? 'git tag' : 'git ls-remote --tags ' + repo_url;
  var tagResults = shell.exec(query, {silent: true});
  if (tagResults.code === 0) {
    return _(tagResults.stdout.match(/v[0-9].*[0-9]$/mg))
      .map(function(tag) {
        var version = semver.parse(tag);
        return version;
      })
      .filter()
      .map(function(version) {
        // angular.js didn't follow semantic version until 1.20rc1
        if ((version.major === 1 && version.minor === 0 && version.prerelease.length > 0) || (version.major === 1 && version.minor === 2 && version.prerelease[0] === 'rc1')) {
          version.version = [version.major, version.minor, version.patch].join('.') + version.prerelease.join('');
          version.raw = 'v' + version.version;
        }
        version.docsUrl = 'http://code.angularjs.org/' + version.version + '/docs';
        // Versions before 1.0.2 had a different docs folder name
        if (version.major < 1 || (version.major === 1 && version.minor === 0 && version.patch < 2)) {
          version.docsUrl += '-' + version.version;
          version.isOldDocsUrl = true;
        }
        return version;
      })
      .sort(semver.compare)
      .value();
  } else {
    return [];
  }
};

var getCdnVersion = function() {
  return _(previousVersions)
    .filter(function(tag) {
      return semver.satisfies(tag, currentPackage.branchVersion);
    })
    .reverse()
    .reduce(function(cdnVersion, version) {
      if (!cdnVersion) {
        if (NO_REMOTE_REQUESTS) {
          // We do not want to make any remote calls to the CDN so just use the most recent version
          cdnVersion = version;
        } else {
          // Note: need to use shell.exec and curl here
          // as version-infos returns its result synchronously...
          var cdnResult = shell.exec('curl http://ajax.googleapis.com/ajax/libs/angularjs/' + version + '/angular.min.js ' +
                    '--head --write-out "%{http_code}" -silent',
                                      {silent: true});
          if (cdnResult.code === 0) {
            // --write-out appends its content to the general request response, so extract it
            var statusCode = cdnResult.stdout.split('\n').pop().trim();
            if (statusCode === '200') {
              cdnVersion = version;
            }
          }
        }
      }
      return cdnVersion;
    }, null);
};

/**
 * Get the unstable snapshot version
 * @return {SemVer} The snapshot version
 */
var getSnapshotVersion = function() {
  var version = _(previousVersions)
    .filter(function(tag) {
      return semver.satisfies(tag, currentPackage.branchVersion);
    })
    .last();

  if (!version) {
    // a snapshot version before the first tag on the branch
    version = semver(currentPackage.branchPattern.replace('*','0-beta.1'));
  }

  // We need to clone to ensure that we are not modifying another version
  version = semver(version.raw);

  var ciBuild = process.env.CIRCLE_BUILD_NUM || process.env.BUILD_NUMBER;
  if (!version.prerelease || !version.prerelease.length) {
    // last release was a non beta release. Increment the patch level to
    // indicate the next release that we will be doing.
    // E.g. last release was 1.3.0, then the snapshot will be
    // 1.3.1-build.1, which is lesser than 1.3.1 according to the semver!

    // If the last release was a beta release we don't update the
    // beta number by purpose, as otherwise the semver comparison
    // does not work any more when the next beta is released.
    // E.g. don't generate 1.3.0-beta.2.build.1
    // as this is bigger than 1.3.0-beta.2 according to semver
    version.patch++;
  }
  version.prerelease = ciBuild ? ['build', ciBuild] : ['local'];
  version.build = getBuild();
  version.codeName = 'snapshot';
  version.isSnapshot = true;
  version.format();
  version.full = version.version + '+' + version.build;
  version.branch = 'master';
  version.distTag = currentPackage.distTag;

  return version;
};


exports.currentPackage = currentPackage = getPackage();
exports.gitRepoInfo = getGitRepoInfo();
exports.previousVersions = previousVersions = getPreviousVersions();
exports.cdnVersion = cdnVersion = getCdnVersion();
exports.currentVersion = getTaggedVersion() || getSnapshotVersion();

if (NO_REMOTE_REQUESTS) {
  console.log('==============================================================================================');
  console.log('Running with no remote requests for version data:');
  console.log(' - this is due to the "NG1_BUILD_NO_REMOTE_VERSION_REQUESTS" environment variable being defined.');
  console.log(' - be aware that the generated docs may not have valid or the most recent version information.');
  console.log('==============================================================================================');
}

console.log('CDN version (' + versionSource + '):', cdnVersion ? cdnVersion.raw : 'No version found.');
console.log('Current version (' + versionSource + '):', exports.currentVersion.raw);