diff --git a/tools/rebase-pr.js b/tools/rebase-pr.js index eee42e4611..19c66ec03c 100644 --- a/tools/rebase-pr.js +++ b/tools/rebase-pr.js @@ -59,24 +59,73 @@ _main(...process.argv.slice(2)).catch(err => { // Helpers async function _main(repository, prNumber) { - console.log(`Determining target branch for PR ${prNumber} on ${repository}.`); - const targetBranch = await determineTargetBranch(repository, prNumber); - console.log(`Target branch is ${targetBranch}.`); - await exec(`git fetch origin ${targetBranch}`); - console.log(`Rebasing current branch on ${targetBranch}.`); - await exec(`git rebase origin/${targetBranch}`); + console.log(`Getting refs and SHAs for PR ${prNumber} on ${repository}.`); + const target = await determineTargetRefAndSha(repository, prNumber); + console.log(`Fetching target branch: ${target.baseRef}.`); + await exec(`git fetch origin ${target.baseRef}`); + + // The sha of the latest commit on the target branch. + const {stdout: shaOfTargetBranchLatest} = await exec(`git rev-parse origin/${target.baseRef}`); + // The sha of the latest commit on the PR. + const {stdout: shaOfPRLatest} = await exec(`git rev-parse HEAD`); + // The first common SHA in the history of the target branch and the latest commit in the PR. + const {stdout: commonAncestorSha} = + await exec(`git merge-base origin/${target.baseRef} ${shaOfPRLatest}`); + + // Log known refs and shas + console.log(`--------------------------------`); + console.log(` Target Branch: ${target.baseRef}`); + console.log(` Latest Commit for Target Branch: ${shaOfTargetBranchLatest.trim()}`); + console.log(` Latest Commit for PR: ${shaOfPRLatest.trim()}`); + console.log(` First Common Ancestor SHA: ${commonAncestorSha.trim()}`); + console.log(`--------------------------------`); + console.log(); + + + // Get the count of commits between the latest commit from origin and the common ancestor SHA. + const {stdout: commitCount} = + await exec(`git rev-list --count origin/${target.baseRef}...${commonAncestorSha.trim()}`); + console.log(`Checking ${commitCount.trim()} commits for changes in the CircleCI config file.`); + + // Check if the files changed between the latest commit from origin and the common ancestor SHA + // includes the CircleCI config. + const {stdout: circleCIConfigChanged} = await exec( + `git diff --name-only origin/${target.baseRef} ${commonAncestorSha.trim()} -- .circleci/config.yml`); + + if (!!circleCIConfigChanged) { + throw Error(` + CircleCI config on ${target.baseRef} has been modified since commit ${commonAncestorSha.slice(0, 7)}, + which this PR is based on. + + Please rebase the PR on ${target.baseRef} after fetching from upstream. + + Rebase instructions for PR Author, please run the following commands: + + git fetch upstream ${target.baseRef}; + git checkout ${target.headRef}; + git rebase origin/${target.baseRef}; + git push --force-with-lease; + `); + } else { + console.log('No change found in the CircleCI config file, continuing.'); + } + console.log(); + + // Rebase the PR. + console.log(`Rebasing current branch on ${target.baseRef}.`); + await exec(`git rebase origin/${target.baseRef}`); console.log('Rebase successful.'); + } -function determineTargetBranch(repository, prNumber) { - const pullsUrl = `https://api.github.com/repos/${repository}/pulls/${prNumber}`; +async function requestDataFromGithub(url) { // GitHub requires a user agent: https://developer.github.com/v3/#user-agent-required - const options = {headers: {'User-Agent': repository}}; + const options = {headers: {'User-Agent': 'angular'}}; return new Promise((resolve, reject) => { https .get( - pullsUrl, options, + url, options, (res) => { const {statusCode} = res; const contentType = res.headers['content-type']; @@ -84,7 +133,6 @@ function determineTargetBranch(repository, prNumber) { res.on('data', (chunk) => { rawData += chunk; }); res.on('end', () => { - let error; if (statusCode !== 200) { error = new Error( @@ -101,8 +149,7 @@ function determineTargetBranch(repository, prNumber) { } try { - const parsedData = JSON.parse(rawData); - resolve(parsedData['base']['ref']); + resolve(JSON.parse(rawData)); } catch (e) { reject(e); } @@ -111,3 +158,15 @@ function determineTargetBranch(repository, prNumber) { .on('error', (e) => { reject(e); }); }); } + +async function determineTargetRefAndSha(repository, prNumber) { + const pullsUrl = `https://api.github.com/repos/${repository}/pulls/${prNumber}`; + + const result = await requestDataFromGithub(pullsUrl); + return { + baseRef: result.base.ref, + baseSha: result.base.sha, + headRef: result.head.ref, + headSha: result.head.sha, + }; +}