dbc2364d16
Scripts provided in the `ng-dev` command might use local `git` commands. For such scripts, we keep track of the branch that has been checked out before the command has been invoked. We do this so that we can later (upon command completion) restore back to the original branch. We do not want to leave the Git repository in a dirty state. It looks like this logic currently only deals with branches but does not work properly when a command is invoked from a detached head. We can make it work by just checking out the previous revision (if no branch is checked out). PR Close #37737
161 lines
5.8 KiB
TypeScript
161 lines
5.8 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 {getConfig, NgDevConfig} from '../../utils/config';
|
|
import {error, info} from '../../utils/console';
|
|
import {GitClient} from '../../utils/git';
|
|
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, config: Pick<NgDevConfig, 'github'> = getConfig()) {
|
|
const git = new GitClient();
|
|
// If there are any local changes in the current repository state, the
|
|
// check cannot run as it needs to move between branches.
|
|
if (git.hasLocalChanges()) {
|
|
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, config.github)).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}`);
|
|
}
|