| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							| 
									
										
										
										
											2020-05-19 12:08:49 -07:00
										 |  |  |  * Copyright Google LLC All Rights Reserved. | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |  * | 
					
						
							|  |  |  |  * 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 {getConfig, NgDevConfig} from '../../utils/config'; | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  | import {error, info, promptConfirm} from '../../utils/console'; | 
					
						
							| 
									
										
										
										
											2020-09-01 10:39:35 +02:00
										 |  |  | import {addTokenToGitHttpsUrl} from '../../utils/git/github-urls'; | 
					
						
							| 
									
										
										
										
											2020-10-01 16:06:56 -07:00
										 |  |  | import {GitClient} from '../../utils/git/index'; | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  | import {getPr} from '../../utils/github'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* GraphQL schema for the response body for each pending PR. */ | 
					
						
							|  |  |  | const PR_SCHEMA = { | 
					
						
							|  |  |  |   state: graphQLTypes.string, | 
					
						
							|  |  |  |   maintainerCanModify: graphQLTypes.boolean, | 
					
						
							|  |  |  |   viewerDidAuthor: graphQLTypes.boolean, | 
					
						
							| 
									
										
										
										
											2020-06-05 23:41:44 +02:00
										 |  |  |   headRefOid: graphQLTypes.string, | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |   headRef: { | 
					
						
							|  |  |  |     name: graphQLTypes.string, | 
					
						
							|  |  |  |     repository: { | 
					
						
							|  |  |  |       url: graphQLTypes.string, | 
					
						
							|  |  |  |       nameWithOwner: graphQLTypes.string, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  |   baseRef: { | 
					
						
							|  |  |  |     name: graphQLTypes.string, | 
					
						
							|  |  |  |     repository: { | 
					
						
							|  |  |  |       url: graphQLTypes.string, | 
					
						
							|  |  |  |       nameWithOwner: graphQLTypes.string, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Rebase the provided PR onto its merge target branch, and push up the resulting | 
					
						
							|  |  |  |  * commit to the PRs repository. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export async function rebasePr( | 
					
						
							|  |  |  |     prNumber: number, githubToken: string, config: Pick<NgDevConfig, 'github'> = getConfig()) { | 
					
						
							| 
									
										
										
										
											2020-06-05 23:41:44 +02:00
										 |  |  |   const git = new GitClient(githubToken); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |   // TODO: Rely on a common assertNoLocalChanges function.
 | 
					
						
							| 
									
										
										
										
											2020-05-27 15:39:31 -07:00
										 |  |  |   if (git.hasLocalChanges()) { | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |     error('Cannot perform rebase of PR with local changes.'); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |     process.exit(1); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							| 
									
										
										
										
											2020-06-25 16:15:14 +02:00
										 |  |  |    * The branch or revision originally checked out before this method performed | 
					
						
							|  |  |  |    * any Git operations that may change the working branch. | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |    */ | 
					
						
							| 
									
										
										
										
											2020-06-25 16:15:14 +02:00
										 |  |  |   const previousBranchOrRevision = git.getCurrentBranchOrRevision(); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |   /* Get the PR information from Github. */ | 
					
						
							| 
									
										
										
										
											2020-08-14 16:49:07 -07:00
										 |  |  |   const pr = await getPr(PR_SCHEMA, prNumber, git); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-05 23:41:44 +02:00
										 |  |  |   const headRefName = pr.headRef.name; | 
					
						
							|  |  |  |   const baseRefName = pr.baseRef.name; | 
					
						
							|  |  |  |   const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`; | 
					
						
							|  |  |  |   const fullBaseRef = `${pr.baseRef.repository.nameWithOwner}:${baseRefName}`; | 
					
						
							| 
									
										
										
										
											2020-09-01 10:39:35 +02:00
										 |  |  |   const headRefUrl = addTokenToGitHttpsUrl(pr.headRef.repository.url, githubToken); | 
					
						
							|  |  |  |   const baseRefUrl = addTokenToGitHttpsUrl(pr.baseRef.repository.url, githubToken); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-05 23:41:44 +02:00
										 |  |  |   // 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
 | 
					
						
							|  |  |  |   const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |   // If the PR does not allow maintainers to modify it, exit as the rebased PR cannot
 | 
					
						
							|  |  |  |   // be pushed up.
 | 
					
						
							|  |  |  |   if (!pr.maintainerCanModify && !pr.viewerDidAuthor) { | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |     error( | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |         `Cannot rebase as you did not author the PR and the PR does not allow maintainers` + | 
					
						
							|  |  |  |         `to modify the PR`); | 
					
						
							|  |  |  |     process.exit(1); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   try { | 
					
						
							|  |  |  |     // Fetch the branch at the commit of the PR, and check it out in a detached state.
 | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |     info(`Checking out PR #${prNumber} from ${fullHeadRef}`); | 
					
						
							| 
									
										
										
										
											2020-10-29 15:43:44 -07:00
										 |  |  |     git.run(['fetch', '-q', headRefUrl, headRefName]); | 
					
						
							| 
									
										
										
										
											2020-06-05 23:41:44 +02:00
										 |  |  |     git.run(['checkout', '--detach', 'FETCH_HEAD']); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Fetch the PRs target branch and rebase onto it.
 | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |     info(`Fetching ${fullBaseRef} to rebase #${prNumber} on`); | 
					
						
							| 
									
										
										
										
											2020-10-29 15:43:44 -07:00
										 |  |  |     git.run(['fetch', '-q', baseRefUrl, baseRefName]); | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |     info(`Attempting to rebase PR #${prNumber} on ${fullBaseRef}`); | 
					
						
							| 
									
										
										
										
											2020-06-05 23:41:44 +02:00
										 |  |  |     const rebaseResult = git.runGraceful(['rebase', 'FETCH_HEAD']); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // If the rebase was clean, push the rebased PR up to the authors fork.
 | 
					
						
							| 
									
										
										
										
											2020-06-05 23:41:44 +02:00
										 |  |  |     if (rebaseResult.status === 0) { | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |       info(`Rebase was able to complete automatically without conflicts`); | 
					
						
							|  |  |  |       info(`Pushing rebased PR #${prNumber} to ${fullHeadRef}`); | 
					
						
							| 
									
										
										
										
											2020-06-05 23:41:44 +02:00
										 |  |  |       git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]); | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |       info(`Rebased and updated PR #${prNumber}`); | 
					
						
							| 
									
										
										
										
											2020-09-29 16:06:50 -07:00
										 |  |  |       git.checkout(previousBranchOrRevision, true); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |       process.exit(0); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } catch (err) { | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |     error(err.message); | 
					
						
							| 
									
										
										
										
											2020-09-29 16:06:50 -07:00
										 |  |  |     git.checkout(previousBranchOrRevision, true); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |     process.exit(1); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // On automatic rebase failures, prompt to choose if the rebase should be continued
 | 
					
						
							|  |  |  |   // manually or aborted now.
 | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |   info(`Rebase was unable to complete automatically without conflicts.`); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |   // If the command is run in a non-CI environment, prompt to format the files immediately.
 | 
					
						
							|  |  |  |   const continueRebase = | 
					
						
							|  |  |  |       process.env['CI'] === undefined && await promptConfirm('Manually complete rebase?'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (continueRebase) { | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |     info(`After manually completing rebase, run the following command to update PR #${prNumber}:`); | 
					
						
							| 
									
										
										
										
											2020-06-05 23:41:44 +02:00
										 |  |  |     info(` $ git push ${pr.headRef.repository.url} HEAD:${headRefName} ${forceWithLeaseFlag}`); | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |     info(); | 
					
						
							|  |  |  |     info(`To abort the rebase and return to the state of the repository before this command`); | 
					
						
							|  |  |  |     info(`run the following command:`); | 
					
						
							| 
									
										
										
										
											2020-06-25 16:15:14 +02:00
										 |  |  |     info(` $ git rebase --abort && git reset --hard && git checkout ${previousBranchOrRevision}`); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |     process.exit(1); | 
					
						
							|  |  |  |   } else { | 
					
						
							| 
									
										
										
										
											2020-05-20 14:32:04 -07:00
										 |  |  |     info(`Cleaning up git state, and restoring previous state.`); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-29 16:06:50 -07:00
										 |  |  |   git.checkout(previousBranchOrRevision, true); | 
					
						
							| 
									
										
										
										
											2020-05-11 15:21:18 -07:00
										 |  |  |   process.exit(1); | 
					
						
							|  |  |  | } |