Paul Gschwendtner 318e9372c9 feat(dev-infra): move merge script over from components repo (#37138)
Moves the merge script from the components repository over
to the shared dev-infra package. The merge script has been
orginally built for all Angular repositories, but we just
kept it in the components repo temporarily to test it.

Since everything went well on the components side, we now
move the script over and integrate it into the dev-infra package.

PR Close #37138
2020-05-18 11:50:07 -07:00

132 lines
5.5 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 {PullRequestFailure} from '../failures';
import {GitClient} from '../git';
import {PullRequest} from '../pull-request';
/**
* Name of a temporary branch that contains the head of a currently-processed PR. Note
* that a branch name should be used that most likely does not conflict with other local
* development branches.
*/
export const TEMP_PR_HEAD_BRANCH = 'merge_pr_head';
/**
* Base class for merge strategies. A merge strategy accepts a pull request and
* merges it into the determined target branches.
*/
export abstract class MergeStrategy {
constructor(protected git: GitClient) {}
/**
* Prepares a merge of the given pull request. The strategy by default will
* fetch all target branches and the pull request into local temporary branches.
*/
async prepare(pullRequest: PullRequest) {
this.fetchTargetBranches(
pullRequest.targetBranches, `pull/${pullRequest.prNumber}/head:${TEMP_PR_HEAD_BRANCH}`);
}
/**
* Performs the merge of the given pull request. This needs to be implemented
* by individual merge strategies.
*/
abstract merge(pullRequest: PullRequest): Promise<null|PullRequestFailure>;
/** Cleans up the pull request merge. e.g. deleting temporary local branches. */
async cleanup(pullRequest: PullRequest) {
// Delete all temporary target branches.
pullRequest.targetBranches.forEach(
branchName => this.git.run(['branch', '-D', this.getLocalTargetBranchName(branchName)]));
// Delete temporary branch for the pull request head.
this.git.run(['branch', '-D', TEMP_PR_HEAD_BRANCH]);
}
/** Gets the revision range for all commits in the given pull request. */
protected getPullRequestRevisionRange(pullRequest: PullRequest): string {
return `${this.getPullRequestBaseRevision(pullRequest)}..${TEMP_PR_HEAD_BRANCH}`;
}
/** Gets the base revision of a pull request. i.e. the commit the PR is based on. */
protected getPullRequestBaseRevision(pullRequest: PullRequest): string {
return `${TEMP_PR_HEAD_BRANCH}~${pullRequest.commitCount}`;
}
/** Gets a deterministic local branch name for a given branch. */
protected getLocalTargetBranchName(targetBranch: string): string {
return `merge_pr_target_${targetBranch.replace(/\//g, '_')}`;
}
/**
* Cherry-picks the given revision range into the specified target branches.
* @returns A list of branches for which the revisions could not be cherry-picked into.
*/
protected cherryPickIntoTargetBranches(revisionRange: string, targetBranches: string[], options: {
dryRun?: boolean
} = {}) {
const cherryPickArgs = [revisionRange];
const failedBranches: string[] = [];
if (options.dryRun) {
// https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt---no-commit
// This causes `git cherry-pick` to not generate any commits. Instead, the changes are
// applied directly in the working tree. This allow us to easily discard the changes
// for dry-run purposes.
cherryPickArgs.push('--no-commit');
}
// Cherry-pick the refspec into all determined target branches.
for (const branchName of targetBranches) {
const localTargetBranch = this.getLocalTargetBranchName(branchName);
// Checkout the local target branch.
this.git.run(['checkout', localTargetBranch]);
// Cherry-pick the refspec into the target branch.
if (this.git.runGraceful(['cherry-pick', ...cherryPickArgs]).status !== 0) {
// Abort the failed cherry-pick. We do this because Git persists the failed
// cherry-pick state globally in the repository. This could prevent future
// pull request merges as a Git thinks a cherry-pick is still in progress.
this.git.runGraceful(['cherry-pick', '--abort']);
failedBranches.push(branchName);
}
// If we run with dry run mode, we reset the local target branch so that all dry-run
// cherry-pick changes are discard. Changes are applied to the working tree and index.
if (options.dryRun) {
this.git.run(['reset', '--hard', 'HEAD']);
}
}
return failedBranches;
}
/**
* Fetches the given target branches. Also accepts a list of additional refspecs that
* should be fetched. This is helpful as multiple slow fetches could be avoided.
*/
protected fetchTargetBranches(names: string[], ...extraRefspecs: string[]) {
const fetchRefspecs = names.map(targetBranch => {
const localTargetBranch = this.getLocalTargetBranchName(targetBranch);
return `refs/heads/${targetBranch}:${localTargetBranch}`;
});
// Fetch all target branches with a single command. We don't want to fetch them
// individually as that could cause an unnecessary slow-down.
this.git.run(['fetch', '-f', this.git.repoGitUrl, ...fetchRefspecs, ...extraRefspecs]);
}
/** Pushes the given target branches upstream. */
protected pushTargetBranchesUpstream(names: string[]) {
const pushRefspecs = names.map(targetBranch => {
const localTargetBranch = this.getLocalTargetBranchName(targetBranch);
return `${localTargetBranch}:refs/heads/${targetBranch}`;
});
// Push all target branches with a single command if we don't run in dry-run mode.
// We don't want to push them individually as that could cause an unnecessary slow-down.
this.git.run(['push', this.git.repoGitUrl, ...pushRefspecs]);
}
}