161 lines
5.9 KiB
TypeScript
161 lines
5.9 KiB
TypeScript
/**
|
|
* @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
|
|
*/
|
|
|
|
import {Bar} from 'cli-progress';
|
|
import {types as graphqlTypes} from 'typed-graphqlify';
|
|
|
|
import {error, info} from '../../utils/console';
|
|
import {AuthenticatedGitClient} from '../../utils/git/authenticated-git-client';
|
|
import {GitClient} from '../../utils/git/git-client';
|
|
import {getPendingPrs} from '../../utils/github';
|
|
import {exec} from '../../utils/shelljs';
|
|
|
|
|
|
/* Graphql schema for the response body for each pending PR. */
|
|
const PR_SCHEMA = {
|
|
headRef: {
|
|
name: graphqlTypes.string,
|
|
repository: {
|
|
url: graphqlTypes.string,
|
|
nameWithOwner: graphqlTypes.string,
|
|
},
|
|
},
|
|
baseRef: {
|
|
name: graphqlTypes.string,
|
|
repository: {
|
|
url: graphqlTypes.string,
|
|
nameWithOwner: graphqlTypes.string,
|
|
},
|
|
},
|
|
updatedAt: graphqlTypes.string,
|
|
number: graphqlTypes.number,
|
|
mergeable: graphqlTypes.string,
|
|
title: graphqlTypes.string,
|
|
};
|
|
|
|
/* Pull Request response from Github Graphql query */
|
|
type RawPullRequest = typeof PR_SCHEMA;
|
|
|
|
/** Convert raw Pull Request response from Github to usable Pull Request object. */
|
|
function processPr(pr: RawPullRequest) {
|
|
return {...pr, updatedAt: (new Date(pr.updatedAt)).getTime()};
|
|
}
|
|
|
|
/* Pull Request object after processing, derived from the return type of the processPr function. */
|
|
type PullRequest = ReturnType<typeof processPr>;
|
|
|
|
/** Name of a temporary local branch that is used for checking conflicts. **/
|
|
const tempWorkingBranch = '__NgDevRepoBaseAfterChange__';
|
|
|
|
/** Checks if the provided PR will cause new conflicts in other pending PRs. */
|
|
export async function discoverNewConflictsForPr(newPrNumber: number, updatedAfter: number) {
|
|
/** The singleton instance of the authenticated git client. */
|
|
const git = AuthenticatedGitClient.get();
|
|
// If there are any local changes in the current repository state, the
|
|
// check cannot run as it needs to move between branches.
|
|
if (git.hasUncommittedChanges()) {
|
|
error('Cannot run with local changes. Please make sure there are no local changes.');
|
|
process.exit(1);
|
|
}
|
|
|
|
/** The active github branch or revision before we performed any Git commands. */
|
|
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
|
/* Progress bar to indicate progress. */
|
|
const progressBar = new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total}`});
|
|
/* PRs which were found to be conflicting. */
|
|
const conflicts: Array<PullRequest> = [];
|
|
|
|
info(`Requesting pending PRs from Github`);
|
|
/** List of PRs from github currently known as mergable. */
|
|
const allPendingPRs = (await getPendingPrs(PR_SCHEMA, git)).map(processPr);
|
|
/** The PR which is being checked against. */
|
|
const requestedPr = allPendingPRs.find(pr => pr.number === newPrNumber);
|
|
if (requestedPr === undefined) {
|
|
error(
|
|
`The request PR, #${newPrNumber} was not found as a pending PR on github, please confirm`);
|
|
error(`the PR number is correct and is an open PR`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const pendingPrs = allPendingPRs.filter(pr => {
|
|
return (
|
|
// PRs being merged into the same target branch as the requested PR
|
|
pr.baseRef.name === requestedPr.baseRef.name &&
|
|
// PRs which either have not been processed or are determined as mergable by Github
|
|
pr.mergeable !== 'CONFLICTING' &&
|
|
// PRs updated after the provided date
|
|
pr.updatedAt >= updatedAfter);
|
|
});
|
|
info(`Retrieved ${allPendingPRs.length} total pending PRs`);
|
|
info(`Checking ${pendingPrs.length} PRs for conflicts after a merge of #${newPrNumber}`);
|
|
|
|
// Fetch and checkout the PR being checked.
|
|
exec(`git fetch ${requestedPr.headRef.repository.url} ${requestedPr.headRef.name}`);
|
|
exec(`git checkout -B ${tempWorkingBranch} FETCH_HEAD`);
|
|
|
|
// Rebase the PR against the PRs target branch.
|
|
exec(`git fetch ${requestedPr.baseRef.repository.url} ${requestedPr.baseRef.name}`);
|
|
const result = exec(`git rebase FETCH_HEAD`);
|
|
if (result.code) {
|
|
error('The requested PR currently has conflicts');
|
|
cleanUpGitState(previousBranchOrRevision);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Start the progress bar
|
|
progressBar.start(pendingPrs.length, 0);
|
|
|
|
// Check each PR to determine if it can merge cleanly into the repo after the target PR.
|
|
for (const pr of pendingPrs) {
|
|
// Fetch and checkout the next PR
|
|
exec(`git fetch ${pr.headRef.repository.url} ${pr.headRef.name}`);
|
|
exec(`git checkout --detach FETCH_HEAD`);
|
|
// Check if the PR cleanly rebases into the repo after the target PR.
|
|
const result = exec(`git rebase ${tempWorkingBranch}`);
|
|
if (result.code !== 0) {
|
|
conflicts.push(pr);
|
|
}
|
|
// Abort any outstanding rebase attempt.
|
|
exec(`git rebase --abort`);
|
|
|
|
progressBar.increment(1);
|
|
}
|
|
// End the progress bar as all PRs have been processed.
|
|
progressBar.stop();
|
|
info();
|
|
info(`Result:`);
|
|
|
|
cleanUpGitState(previousBranchOrRevision);
|
|
|
|
// If no conflicts are found, exit successfully.
|
|
if (conflicts.length === 0) {
|
|
info(`No new conflicting PRs found after #${newPrNumber} merging`);
|
|
process.exit(0);
|
|
}
|
|
|
|
// Inform about discovered conflicts, exit with failure.
|
|
error.group(`${conflicts.length} PR(s) which conflict(s) after #${newPrNumber} merges:`);
|
|
for (const pr of conflicts) {
|
|
error(` - #${pr.number}: ${pr.title}`);
|
|
}
|
|
error.groupEnd();
|
|
process.exit(1);
|
|
}
|
|
|
|
/** Reset git back to the provided branch or revision. */
|
|
export function cleanUpGitState(previousBranchOrRevision: string) {
|
|
// Ensure that any outstanding rebases are aborted.
|
|
exec(`git rebase --abort`);
|
|
// Ensure that any changes in the current repo state are cleared.
|
|
exec(`git reset --hard`);
|
|
// Checkout the original branch from before the run began.
|
|
exec(`git checkout ${previousBranchOrRevision}`);
|
|
// Delete the generated branch.
|
|
exec(`git branch -D ${tempWorkingBranch}`);
|
|
}
|