Creates a tool within ng-dev to checkout a pending PR from the upstream repository. This automates an action that many developers on the Angular team need to do periodically in the process of testing and reviewing incoming PRs. Example usage: ng-dev pr checkout <pr-number> PR Close #38474
136 lines
4.9 KiB
TypeScript
136 lines
4.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 {types as graphQLTypes} from 'typed-graphqlify';
|
|
import {URL} from 'url';
|
|
|
|
import {info} from '../../utils/console';
|
|
import {GitClient} from '../../utils/git';
|
|
import {getPr} from '../../utils/github';
|
|
|
|
/* GraphQL schema for the response body for a pending PR. */
|
|
const PR_SCHEMA = {
|
|
state: graphQLTypes.string,
|
|
maintainerCanModify: graphQLTypes.boolean,
|
|
viewerDidAuthor: graphQLTypes.boolean,
|
|
headRefOid: graphQLTypes.string,
|
|
headRef: {
|
|
name: graphQLTypes.string,
|
|
repository: {
|
|
url: graphQLTypes.string,
|
|
nameWithOwner: graphQLTypes.string,
|
|
},
|
|
},
|
|
baseRef: {
|
|
name: graphQLTypes.string,
|
|
repository: {
|
|
url: graphQLTypes.string,
|
|
nameWithOwner: graphQLTypes.string,
|
|
},
|
|
},
|
|
};
|
|
|
|
|
|
export class UnexpectedLocalChangesError extends Error {
|
|
constructor(m: string) {
|
|
super(m);
|
|
Object.setPrototypeOf(this, UnexpectedLocalChangesError.prototype);
|
|
}
|
|
}
|
|
|
|
export class MaintainerModifyAccessError extends Error {
|
|
constructor(m: string) {
|
|
super(m);
|
|
Object.setPrototypeOf(this, MaintainerModifyAccessError.prototype);
|
|
}
|
|
}
|
|
|
|
/** Options for checking out a PR */
|
|
export interface PullRequestCheckoutOptions {
|
|
/** Whether the PR should be checked out if the maintainer cannot modify. */
|
|
allowIfMaintainerCannotModify?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Rebase the provided PR onto its merge target branch, and push up the resulting
|
|
* commit to the PRs repository.
|
|
*/
|
|
export async function checkOutPullRequestLocally(
|
|
prNumber: number, githubToken: string, opts: PullRequestCheckoutOptions = {}) {
|
|
/** Authenticated Git client for git and Github interactions. */
|
|
const git = new GitClient(githubToken);
|
|
|
|
// In order to preserve local changes, checkouts cannot occur if local changes are present in the
|
|
// git environment. Checked before retrieving the PR to fail fast.
|
|
if (git.hasLocalChanges()) {
|
|
throw new UnexpectedLocalChangesError('Unable to checkout PR due to uncommitted changes.');
|
|
}
|
|
|
|
/**
|
|
* The branch or revision originally checked out before this method performed
|
|
* any Git operations that may change the working branch.
|
|
*/
|
|
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
|
|
/* The PR information from Github. */
|
|
const pr = await getPr(PR_SCHEMA, prNumber, git);
|
|
/** The branch name of the PR from the repository the PR came from. */
|
|
const headRefName = pr.headRef.name;
|
|
/** The full ref for the repository and branch the PR came from. */
|
|
const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`;
|
|
/** The full URL path of the repository the PR came from with github token as authentication. */
|
|
const headRefUrl = addAuthenticationToUrl(pr.headRef.repository.url, githubToken);
|
|
// Note: Since we use a detached head for rebasing the PR and therefore do not have
|
|
// remote-tracking branches configured, we need to set our expected ref and SHA. This
|
|
// allows us to use `--force-with-lease` for the detached head while ensuring that we
|
|
// never accidentally override upstream changes that have been pushed in the meanwhile.
|
|
// See:
|
|
// https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt
|
|
/** Flag for a force push with leage back to upstream. */
|
|
const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`;
|
|
|
|
// If the PR does not allow maintainers to modify it, exit as the rebased PR cannot
|
|
// be pushed up.
|
|
if (!pr.maintainerCanModify && !pr.viewerDidAuthor && !opts.allowIfMaintainerCannotModify) {
|
|
throw new MaintainerModifyAccessError('PR is not set to allow maintainers to modify the PR');
|
|
}
|
|
|
|
try {
|
|
// Fetch the branch at the commit of the PR, and check it out in a detached state.
|
|
info(`Checking out PR #${prNumber} from ${fullHeadRef}`);
|
|
git.run(['fetch', headRefUrl, headRefName]);
|
|
git.run(['checkout', '--detach', 'FETCH_HEAD']);
|
|
} catch (e) {
|
|
git.checkout(previousBranchOrRevision, true);
|
|
throw e;
|
|
}
|
|
|
|
return {
|
|
/**
|
|
* Pushes the current local branch to the PR on the upstream repository.
|
|
*
|
|
* @returns true If the command did not fail causing a GitCommandError to be thrown.
|
|
* @throws GitCommandError Thrown when the push back to upstream fails.
|
|
*/
|
|
pushToUpstream: (): true => {
|
|
git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]);
|
|
return true;
|
|
},
|
|
/** Restores the state of the local repository to before the PR checkout occured. */
|
|
resetGitState: (): boolean => {
|
|
return git.checkout(previousBranchOrRevision, true);
|
|
}
|
|
};
|
|
}
|
|
|
|
/** Adds the provided token as username to the provided url. */
|
|
function addAuthenticationToUrl(urlString: string, token: string) {
|
|
const url = new URL(urlString);
|
|
url.username = token;
|
|
return url.toString();
|
|
}
|