angular-cn/tools/utils/git-get-changeset-refs.js

182 lines
5.2 KiB
JavaScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const {execSync} = require('child_process');
/** A regex to select a ref that matches our semver refs. */
const semverRegex = /^(\d+)\.(\d+)\.x$/;
/**
* Synchronously executes the command.
*
* Return the trimmed stdout as a string, with an added attribute of the exit code.
*/
function exec(command, allowStderr = true) {
let output = new String();
output.code = 0;
try {
output += execSync(command, {stdio: ['pipe', 'pipe', 'pipe']}).toString().trim();
} catch (err) {
allowStderr && console.error(err.stderr.toString());
output.code = err.status;
}
return output;
}
/**
* Sort a list of fullpath refs into a list and then provide the first entry.
*
* The sort order will first find master ref, and then any semver ref, followed
* by the rest of the refs in the order provided.
*
* Branches are sorted in this order as work is primarily done on master, and
* otherwise on a semver branch. If neither of those were to match, the most
* likely correct branch will be the first one encountered in the list.
*/
function getRefFromBranchList(gitOutput, remote) {
const branches = gitOutput.split('\n').map(b => b.split('/').slice(1).join('').trim());
return branches.sort((a, b) => {
if (a === 'master') {
return -1;
}
if (b === 'master') {
return 1;
}
const aIsSemver = semverRegex.test(a);
const bIsSemver = semverRegex.test(b);
if (aIsSemver && bIsSemver) {
const [, aMajor, aMinor] = a.match(semverRegex);
const [, bMajor, bMinor] = b.match(semverRegex);
return parseInt(bMajor, 10) - parseInt(aMajor, 10) ||
parseInt(aMinor, 10) - parseInt(bMinor, 10) || 0;
}
if (aIsSemver) {
return -1;
}
if (bIsSemver) {
return 1;
}
return 0;
})[0];
}
/**
* Get the full sha of the ref provided.
*
* example: 1bc0c1a6c01ede7168f22fa9b3508ba51f1f464e
*/
function getShaFromRef(ref) {
return exec(`git rev-parse ${ref}`);
}
/**
* Get the list of branches which contain the provided sha, sorted in descending order
* by committerdate.
*
* example:
* upstream/master
* upstream/9.0.x
* upstream/test
* upstream/1.1.x
*/
function getBranchListForSha(sha, remote) {
return exec(`git branch -r '${remote}/*' --sort=-committerdate --contains ${sha}`);
}
/** Get the common ancestor sha of the two provided shas. */
function getCommonAncestorSha(sha1, sha2) {
return exec(`git merge-base ${sha1} ${sha2}`);
}
/** Removes the remote from git. */
function removeRemote(remote) {
exec(`git remote remove ${remote}`);
}
/**
* Adds the remote to git, if it doesn't already exist. Returns a boolean indicating
* whether the remote was added by the command.
*/
function addRemote(remote) {
return !exec(`git remote add ${remote} https://github.com/${remote}/angular.git`, false).code;
}
/** Fetch latest from the remote. */
function fetchRemote(remote) {
exec(`git fetch ${remote}`);
}
/**
* Get the nearest ref which the HEAD has a parent commit.
*
* Checks up to a limit of 100 previous shas.
*/
function getParentBranchForHead(remote) {
// Get the latest for the remote.
fetchRemote(remote);
let headCount = 0;
while (headCount < 100) {
// Attempt to get the ref on the remote for the sha.
const branches = getBranchListForSha(`HEAD~${headCount}`, remote);
const ref = getRefFromBranchList(branches, remote);
// If the ref exists, get the sha and latest sha for the remote ref.
if (ref) {
const sha = getShaFromRef(`HEAD~${headCount}`);
const latestSha = getShaFromRef(`${remote}/${ref}`);
return {ref, sha, latestSha, remote};
}
headCount++;
}
return {ref: '', latestSha: '', sha, remote};
}
/** Get the ref and latest shas for the provided sha on a specific remote. */
function getRefAndShas(sha, remote) {
// Ensure the remote is defined in git.
let markRemoteForClean = addRemote(remote);
// Get the latest from the remote.
fetchRemote(remote);
// Get the ref on the remote for the sha provided.
const branches = getBranchListForSha(sha, remote);
const ref = getRefFromBranchList(branches, remote);
// Get the latest sha on the discovered remote ref.
const latestSha = getShaFromRef(`${remote}/${ref}`);
// Clean up the remote if it didn't exist before execution.
if (markRemoteForClean) {
removeRemote(remote);
}
return {remote, ref, latestSha, sha};
}
/** Gets the refs and shas for the base and target of the current environment. */
function getRefsAndShasForChange() {
let base, target;
if (process.env['CI']) {
base = getRefAndShas(process.env['CI_GIT_BASE_REVISION'], process.env['CI_REPO_OWNER']);
target = getRefAndShas(process.env['CI_GIT_REVISION'], process.env['CI_PR_USERNAME']);
} else {
const originSha = getShaFromRef(`HEAD`);
target = getRefAndShas(originSha, 'origin');
base = getParentBranchForHead('upstream');
}
const commonAncestorSha = getCommonAncestorSha(base.sha, target.sha);
return {
base,
target,
commonAncestorSha,
};
}
module.exports = getRefsAndShasForChange;