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(); | ||
|  | } |