npm-git-version
Version:
NPM utility that gets next version of application according git branch and tags
507 lines (506 loc) • 22.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.VersionsExtractor = exports.processArguments = exports.updateBuildNumber = void 0;
var git = require("simple-git");
var semver = require("semver");
var extend = require("extend");
var commandLineArgs = require("command-line-args");
var path = require("path");
var moment = require("moment");
var commandLineUsage = require("command-line-usage");
/**
* Reads configuration from ENV variables
*/
function processEnvCfg() {
var result = {
config: 'ngv.config.json'
};
if (process.env.NGV_BRANCH_NAME) {
result.branchName = process.env.NGV_BRANCH_NAME;
}
if (process.env.NGV_BUILD_NUMBER) {
result.buildNumber = parseInt(process.env.NGV_BUILD_NUMBER);
}
if (process.env.NGV_TAG_PREFIX) {
result.tagPrefix = process.env.NGV_TAG_PREFIX;
}
if (process.env.NGV_IGNORE_BRANCH_PREFIX) {
result.ignoreBranchPrefix = process.env.NGV_IGNORE_BRANCH_PREFIX;
}
if (process.env.NGV_PRE) {
result.pre = process.env.NGV_PRE.toLowerCase() == "true";
}
if (process.env.NGV_SUFFIX) {
result.suffix = process.env.NGV_SUFFIX;
}
if (process.env.NGV_WORKING_DIRECTORY) {
result.workingDirectory = process.env.NGV_WORKING_DIRECTORY;
}
if (process.env.NGV_CURRENT_VERSION) {
result.currentVersion = process.env.NGV_CURRENT_VERSION;
}
if (process.env.NGV_NO_INCREMENT) {
result.noIncrement = process.env.NGV_NO_INCREMENT.toLowerCase() == "true";
}
if (process.env.NGV_NO_STDOUT) {
result.noStdOut = process.env.NGV_NO_STDOUT.toLowerCase() == "true";
}
if (process.env.NGV_EXECUTE) {
result.execute = process.env.NGV_EXECUTE;
}
return result;
}
/**
* Updates build number, handling special build number -1
* @param args Arguments that are being processed
*/
function updateBuildNumber(args) {
if (args.buildNumber == -1) {
args.buildNumber = parseInt(moment().format("YYYYMMDDHHmmss"));
}
}
exports.updateBuildNumber = updateBuildNumber;
/**
* Process arguments and returns parsed object
*/
function processArguments() {
var definitions = [
{ name: "help", alias: "h", type: Boolean, description: "Displays help for this command line tool." },
{ name: "branchName", type: String, description: "Name of branch used for getting version, if not specified current branch will be used, if detached error will be thrown.", typeLabel: "<branchName>", defaultOption: true },
{ name: "buildNumber", alias: "b", type: Number, description: "Build number will be used if suffix is specified, defaults to 0, possible to use -1 to autogenerate date time stamp.", typeLabel: "<buildNumber>" },
{ name: "tagPrefix", alias: "t", type: String, description: "Tag prefix (RegExp) used for pairing branch and tag for getting version.", typeLabel: "<prefix>" },
{ name: "ignoreBranchPrefix", alias: "i", type: String, description: "Branch prefix name that will be ignored (RegExp) and stripped of branch during version paring.", typeLabel: "<ignorePrefix>" },
{ name: "pre", alias: "p", type: Boolean, description: "Indication that prerelease version should be returned." },
{ name: "suffix", alias: "s", type: String, description: "Suffix that is used when prerelease version is requested, will be used as prerelease (suffix) name.", typeLabel: "<suffix>" },
{ name: "currentVersion", alias: "v", type: String, description: "Current version that will be used if it matches branch and tag as source for next version.", typeLabel: "<version>" },
{ name: "noIncrement", alias: "r", type: Boolean, description: "Indication whether no new version should be incremented/calculated." },
{ name: "workingDirectory", alias: "w", type: String, description: "Working directory where git repository is located.", typeLabel: "<workingDirectory>" },
{ name: "noStdOut", alias: "n", type: Boolean, description: "Indication that no stdout should be written in case of success run." },
{ name: "config", alias: "c", type: String, description: "Path to configuration file." },
{ name: "execute", alias: "e", type: String, description: "Execute command with variable '$GIT_VERSION' available." }
];
var args = commandLineArgs(definitions);
var envConfig = processEnvCfg();
var fileConfig = {};
try {
var configPath = path.join(process.cwd(), envConfig.config);
require.resolve(configPath);
fileConfig = require(configPath);
}
catch (e) {
if (!args.noStdOut) {
console.log("No config file '" + envConfig.config + "' found.");
}
}
args = extend({}, fileConfig, envConfig, args);
updateBuildNumber(args);
if (args.help) {
console.log(commandLineUsage([
{
header: "description",
content: [
"Gets version of application based on branch name and existing tags",
"",
"Command:",
"ngv <branchName> [options]"
]
},
{
header: "options",
optionList: definitions
}
]));
process.exit(0);
}
return args;
}
exports.processArguments = processArguments;
/**
* Extractor that extracts version number for current git repository
*/
var VersionsExtractor = /** @class */ (function () {
//######################### constructor #########################
function VersionsExtractor(_config) {
this._config = _config;
/**
* Stripped branch prefix from branch name
*/
this._branchPrefix = "";
/**
* Indication that tag is on current HEAD
*/
this._currentTag = false;
/**
* Indication that version is new for this branch
*/
this._startingVersion = false;
if (this._config.pre && !this._config.suffix) {
console.error("Prerelease version was set, but no suffix was provided!");
process.exit(-1);
}
}
Object.defineProperty(VersionsExtractor.prototype, "branchPrefix", {
//######################### public properties #########################
/**
* Gets stripped branch prefix from branch name
*/
get: function () {
return this._branchPrefix;
},
enumerable: false,
configurable: true
});
Object.defineProperty(VersionsExtractor.prototype, "branchName", {
/**
* Gets name of current branch
*/
get: function () {
return this._branchName;
},
enumerable: false,
configurable: true
});
Object.defineProperty(VersionsExtractor.prototype, "branchVersion", {
/**
* Gets name of current branch stripped only to version
*/
get: function () {
return this._branchVersion;
},
enumerable: false,
configurable: true
});
Object.defineProperty(VersionsExtractor.prototype, "version", {
/**
* Gets computed version for next build
*/
get: function () {
return this._version;
},
enumerable: false,
configurable: true
});
Object.defineProperty(VersionsExtractor.prototype, "lastMatchingVersion", {
/**
* Gets number of last matching version
*/
get: function () {
return this._lastMatchingVersion;
},
enumerable: false,
configurable: true
});
Object.defineProperty(VersionsExtractor.prototype, "executeCommand", {
/**
* Command that should be executed after version is computed
*/
get: function () {
return this._config.execute;
},
enumerable: false,
configurable: true
});
Object.defineProperty(VersionsExtractor.prototype, "prereleaseSuffix", {
//######################### private properties #########################
/**
* Gets prerelease suffix
*/
get: function () {
if (this._config.pre) {
return "" + (this._branchPrefix ? this._branchPrefix + "-" : "") + this._config.suffix;
}
return "";
},
enumerable: false,
configurable: true
});
//######################### public methods #########################
/**
* Process extraction of version
*/
VersionsExtractor.prototype.process = function () {
var _this = this;
var result = new Promise(function (resolve, reject) {
_this._processResolve = resolve;
_this._processReject = reject;
});
//Tests whether application is run inside git repository
this._git = git(this._config.workingDirectory).status(this._errorHandle(function () {
if (!_this._config.noStdOut) {
console.log("Git repository available.");
}
}));
this._runProcessing();
return result;
};
//######################### private methods #########################
/**
* Runs internal git processing
*/
VersionsExtractor.prototype._runProcessing = function () {
return __awaiter(this, void 0, void 0, function () {
var _a, _b;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
_a = this;
return [4 /*yield*/, this._getBranchName()];
case 1:
_a._branchName = _c.sent();
this._processBranchName();
_b = this;
return [4 /*yield*/, this._getLastMatchingTag()];
case 2:
_b._lastMatchingVersion = _c.sent();
this._computeVersion();
this._applyBuildNumber();
this._processResolve(this);
return [2 /*return*/];
}
});
});
};
/**
* Applies specified build number for prerelease version
*/
VersionsExtractor.prototype._applyBuildNumber = function () {
//release version or current tag is on same commit as HEAD
if (!this._config.pre || this._currentTag || this._config.noIncrement) {
return;
}
//no build number or current version
if (!this._config.buildNumber && !this._config.currentVersion) {
return;
}
//build number higher priority
if (this._config.buildNumber) {
this._version = this._version.replace(/\d+$/g, "" + this._config.buildNumber);
return;
}
//current version specified as parameter
if (this._config.currentVersion) {
var currentVersionSemver = semver.parse(this._version);
var originalVersionSemver = semver.parse(this._config.currentVersion);
//invalid one of versions
if (!currentVersionSemver || !originalVersionSemver) {
this._processReject("Unable to parse version '" + this._version + "' or '" + this._config.currentVersion + "'!");
return;
}
//version are in match use it as base for increment
if (currentVersionSemver.major + "." + currentVersionSemver.minor + "." + currentVersionSemver.patch + "-" + currentVersionSemver.prerelease[0] == originalVersionSemver.major + "." + originalVersionSemver.minor + "." + originalVersionSemver.patch + "-" + originalVersionSemver.prerelease[0]) {
this._version = semver.inc(this._config.currentVersion, "prerelease", false, this.prereleaseSuffix);
}
}
};
/**
* Computes next version for build
*/
VersionsExtractor.prototype._computeVersion = function () {
//use current version without change
if (this._config.currentVersion && this._config.noIncrement) {
this._version = this._config.currentVersion;
return;
}
//commit and matching tag are same, always use release version
if (this._currentTag) {
this._version = this._lastMatchingVersion;
return;
}
//new version for current branch, first for current major.minor number
if (this._startingVersion) {
if (this._config.pre) {
this._version = this._lastMatchingVersion + "-" + this.prereleaseSuffix + ".0";
}
else {
this._version = this._lastMatchingVersion;
}
return;
}
//version will not be incremented
if (this._config.noIncrement) {
return;
}
var version = this._config.pre ? "prerelease" : "patch";
this._version = semver.inc(this._lastMatchingVersion, version, false, this.prereleaseSuffix);
};
/**
* Processes branch name and extracts prefix and pure version number
*/
VersionsExtractor.prototype._processBranchName = function () {
this._branchVersion = this._branchName;
//no prefix and wrong format
if (!this._config.ignoreBranchPrefix && !/^\d+\.\d+$/g.test(this._branchName)) {
this._processReject("Wrong branch name '" + this._branchName + "', no prefix specified and branch is not in correct format!");
return;
}
var regex = new RegExp("^" + this._config.ignoreBranchPrefix, 'gi');
var matches = regex.exec(this._branchName);
//prefix present
if (matches) {
this._branchPrefix = matches[0].replace(/\/$/g, '');
this._branchVersion = this._branchName.replace(regex, '');
}
//after prefix strip still wrong format
if (!/^\d+\.\d+$/g.test(this._branchVersion)) {
this._processReject("Wrong branch name '" + this._branchName + "', probably wrong prefix regex, extracted version '" + this._branchVersion + "' is not ok!");
return;
}
};
/**
* Gets last matching tag
*/
VersionsExtractor.prototype._getLastMatchingTag = function () {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
return [2 /*return*/, new Promise(function (resolve) {
//gets all tags
_this._git.raw([
"log",
'--pretty="%D"',
"--tags",
"--no-walk"
], _this._errorHandle(function (result) {
result = result || '';
//splits string into array of decorated revisions names
var tmpArray = result.split('\n')
.filter(function (itm) { return itm; })
.map(function (itm) { return itm.replace(/^"|"$/g, ''); });
var resultArray = tmpArray;
//current HEAD is on commit with existing old tag
if (tmpArray.some(function (itm) { return itm.startsWith('HEAD'); })) {
resultArray = [];
for (var _i = 0, _a = tmpArray.reverse(); _i < _a.length; _i++) {
var x = _a[_i];
resultArray.push(x);
if (x.startsWith('HEAD')) {
resultArray.reverse();
break;
}
}
}
//get array of tag names
var tags = resultArray
.map(function (itm) { return itm.replace(/^.*?tag:\s?(.*?)(?:$|,.*)/g, '$1'); })
.filter(function (itm) { return itm; });
var _loop_1 = function (x) {
var matches = new RegExp("^" + _this._config.tagPrefix + "(" + _this._branchVersion + ".*?)$", 'gi')
.exec(tags[x]);
//tag and branch match
if (matches) {
var match_1 = matches[1];
var promises = [
new Promise(function (resolveHash) {
_this._git.raw([
"rev-list",
"-n",
"1",
tags[x]
], _this._errorHandle(function (tagResult) {
resolveHash(tagResult.trim());
}));
}),
new Promise(function (resolveHash) {
_this._git.revparse([
"HEAD"
], _this._errorHandle(function (tagResult) {
resolveHash(tagResult.trim());
}));
})
];
Promise.all(promises).then(function (value) {
if (value[0] == value[1]) {
_this._currentTag = true;
}
resolve(match_1);
});
return { value: void 0 };
}
};
for (var x = 0; x < tags.length; x++) {
var state_1 = _loop_1(x);
if (typeof state_1 === "object")
return state_1.value;
}
_this._startingVersion = true;
resolve(_this._branchVersion + ".0");
}));
})];
});
});
};
/**
* Gets requested branch name
*/
VersionsExtractor.prototype._getBranchName = function () {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
if (this._config.branchName) {
return [2 /*return*/, this._config.branchName];
}
return [2 /*return*/, new Promise(function (resolve) {
_this._git.branch(_this._errorHandle(function (result) {
if (result.detached) {
_this._processReject("Unable to proceed if 'HEAD' is detached and no 'branchName' was specified!");
return;
}
resolve(result.current);
}));
})];
});
});
};
/**
* Creates handle function with automatic error handling
* @param callback Callback called if result was success
*/
VersionsExtractor.prototype._errorHandle = function (callback) {
var _this = this;
return function (error, result) {
if (error) {
_this._processReject(error);
return;
}
callback(result);
};
};
return VersionsExtractor;
}());
exports.VersionsExtractor = VersionsExtractor;