| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @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
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 17:21:01 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-24 17:59:12 +02:00
										 |  |  | import {getConfig, getRepoBaseDir} from '../../utils/config'; | 
					
						
							| 
									
										
										
										
											2020-05-20 14:48:50 -07:00
										 |  |  | import {error, green, info, promptConfirm, red, yellow} from '../../utils/console'; | 
					
						
							| 
									
										
										
										
											2020-07-24 17:59:12 +02:00
										 |  |  | import {GithubApiRequestError} from '../../utils/git/github'; | 
					
						
							| 
									
										
										
										
											2020-09-01 10:39:35 +02:00
										 |  |  | import {GITHUB_TOKEN_GENERATE_URL} from '../../utils/git/github-urls'; | 
					
						
							| 
									
										
										
										
											2020-10-01 16:06:56 -07:00
										 |  |  | import {GitClient} from '../../utils/git/index'; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:21:01 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-28 11:45:01 -07:00
										 |  |  | import {loadAndValidateConfig, MergeConfigWithRemote} from './config'; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:21:01 +02:00
										 |  |  | import {MergeResult, MergeStatus, PullRequestMergeTask} from './task'; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							| 
									
										
										
										
											2020-05-15 17:21:01 +02:00
										 |  |  |  * Merges a given pull request based on labels configured in the given merge configuration. | 
					
						
							|  |  |  |  * Pull requests can be merged with different strategies such as the Github API merge | 
					
						
							|  |  |  |  * strategy, or the local autosquash strategy. Either strategy has benefits and downsides. | 
					
						
							|  |  |  |  * More information on these strategies can be found in their dedicated strategy classes. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * See {@link GithubApiMergeStrategy} and {@link AutosquashMergeStrategy} | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * @param prNumber Number of the pull request that should be merged. | 
					
						
							|  |  |  |  * @param githubToken Github token used for merging (i.e. fetching and pushing) | 
					
						
							|  |  |  |  * @param projectRoot Path to the local Git project that is used for merging. | 
					
						
							|  |  |  |  * @param config Configuration for merging pull requests. | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |  */ | 
					
						
							| 
									
										
										
										
											2020-05-15 17:21:01 +02:00
										 |  |  | export async function mergePullRequest( | 
					
						
							|  |  |  |     prNumber: number, githubToken: string, projectRoot: string = getRepoBaseDir(), | 
					
						
							|  |  |  |     config?: MergeConfigWithRemote) { | 
					
						
							| 
									
										
										
										
											2020-09-17 10:17:08 -07:00
										 |  |  |   // Set the environment variable to skip all git commit hooks triggered by husky. We are unable to
 | 
					
						
							| 
									
										
										
										
											2020-10-10 22:02:47 +03:00
										 |  |  |   // rely on `--no-verify` as some hooks still run, notably the `prepare-commit-msg` hook.
 | 
					
						
							| 
									
										
										
										
											2020-12-08 09:12:11 -08:00
										 |  |  |   process.env['HUSKY'] = '0'; | 
					
						
							| 
									
										
										
										
											2020-09-17 10:17:08 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-24 17:59:12 +02:00
										 |  |  |   const api = await createPullRequestMergeTask(githubToken, projectRoot, config); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // Perform the merge. Force mode can be activated through a command line flag.
 | 
					
						
							|  |  |  |   // Alternatively, if the merge fails with non-fatal failures, the script
 | 
					
						
							|  |  |  |   // will prompt whether it should rerun in force mode.
 | 
					
						
							|  |  |  |   if (!await performMerge(false)) { | 
					
						
							|  |  |  |     process.exit(1); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Performs the merge and returns whether it was successful or not. */ | 
					
						
							|  |  |  |   async function performMerge(ignoreFatalErrors: boolean): Promise<boolean> { | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       const result = await api.merge(prNumber, ignoreFatalErrors); | 
					
						
							|  |  |  |       return await handleMergeResult(result, ignoreFatalErrors); | 
					
						
							|  |  |  |     } catch (e) { | 
					
						
							|  |  |  |       // Catch errors to the Github API for invalid requests. We want to
 | 
					
						
							|  |  |  |       // exit the script with a better explanation of the error.
 | 
					
						
							|  |  |  |       if (e instanceof GithubApiRequestError && e.status === 401) { | 
					
						
							| 
									
										
										
										
											2020-05-20 14:48:50 -07:00
										 |  |  |         error(red('Github API request failed. ' + e.message)); | 
					
						
							|  |  |  |         error(yellow('Please ensure that your provided token is valid.')); | 
					
						
							|  |  |  |         error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`)); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |         process.exit(1); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       throw e; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Prompts whether the specified pull request should be forcibly merged. If so, merges | 
					
						
							|  |  |  |    * the specified pull request forcibly (ignoring non-critical failures). | 
					
						
							|  |  |  |    * @returns Whether the specified pull request has been forcibly merged. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   async function promptAndPerformForceMerge(): Promise<boolean> { | 
					
						
							|  |  |  |     if (await promptConfirm('Do you want to forcibly proceed with merging?')) { | 
					
						
							|  |  |  |       // Perform the merge in force mode. This means that non-fatal failures
 | 
					
						
							|  |  |  |       // are ignored and the merge continues.
 | 
					
						
							|  |  |  |       return performMerge(true); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Handles the merge result by printing console messages, exiting the process | 
					
						
							|  |  |  |    * based on the result, or by restarting the merge if force mode has been enabled. | 
					
						
							| 
									
										
										
										
											2020-06-16 00:20:36 +02:00
										 |  |  |    * @returns Whether the merge completed without errors or not. | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |    */ | 
					
						
							|  |  |  |   async function handleMergeResult(result: MergeResult, disableForceMergePrompt = false) { | 
					
						
							|  |  |  |     const {failure, status} = result; | 
					
						
							|  |  |  |     const canForciblyMerge = failure && failure.nonFatal; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     switch (status) { | 
					
						
							|  |  |  |       case MergeStatus.SUCCESS: | 
					
						
							| 
									
										
										
										
											2020-06-16 00:20:36 +02:00
										 |  |  |         info(green(`Successfully merged the pull request: #${prNumber}`)); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |         return true; | 
					
						
							|  |  |  |       case MergeStatus.DIRTY_WORKING_DIR: | 
					
						
							| 
									
										
										
										
											2020-05-20 14:48:50 -07:00
										 |  |  |         error( | 
					
						
							|  |  |  |             red(`Local working repository not clean. Please make sure there are ` + | 
					
						
							|  |  |  |                 `no uncommitted changes.`)); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |         return false; | 
					
						
							|  |  |  |       case MergeStatus.UNKNOWN_GIT_ERROR: | 
					
						
							| 
									
										
										
										
											2020-05-20 14:48:50 -07:00
										 |  |  |         error( | 
					
						
							|  |  |  |             red('An unknown Git error has been thrown. Please check the output ' + | 
					
						
							|  |  |  |                 'above for details.')); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |         return false; | 
					
						
							| 
									
										
										
										
											2020-06-03 12:12:52 -07:00
										 |  |  |       case MergeStatus.GITHUB_ERROR: | 
					
						
							|  |  |  |         error(red('An error related to interacting with Github has been discovered.')); | 
					
						
							|  |  |  |         error(failure!.message); | 
					
						
							|  |  |  |         return false; | 
					
						
							| 
									
										
										
										
											2020-06-16 00:20:36 +02:00
										 |  |  |       case MergeStatus.USER_ABORTED: | 
					
						
							|  |  |  |         info(`Merge of pull request has been aborted manually: #${prNumber}`); | 
					
						
							|  |  |  |         return true; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |       case MergeStatus.FAILED: | 
					
						
							| 
									
										
										
										
											2020-05-20 14:48:50 -07:00
										 |  |  |         error(yellow(`Could not merge the specified pull request.`)); | 
					
						
							|  |  |  |         error(red(failure!.message)); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |         if (canForciblyMerge && !disableForceMergePrompt) { | 
					
						
							| 
									
										
										
										
											2020-05-20 14:48:50 -07:00
										 |  |  |           info(); | 
					
						
							|  |  |  |           info(yellow('The pull request above failed due to non-critical errors.')); | 
					
						
							|  |  |  |           info(yellow(`This error can be forcibly ignored if desired.`)); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |           return await promptAndPerformForceMerge(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |       default: | 
					
						
							|  |  |  |         throw Error(`Unexpected merge result: ${status}`); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2020-07-24 17:59:12 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Creates the pull request merge task from the given Github token, project root | 
					
						
							|  |  |  |  * and optional explicit configuration. An explicit configuration can be specified | 
					
						
							|  |  |  |  * when the merge script is used outside of a `ng-dev` configured repository. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | async function createPullRequestMergeTask( | 
					
						
							|  |  |  |     githubToken: string, projectRoot: string, explicitConfig?: MergeConfigWithRemote) { | 
					
						
							|  |  |  |   if (explicitConfig !== undefined) { | 
					
						
							|  |  |  |     const git = new GitClient(githubToken, {github: explicitConfig.remote}, projectRoot); | 
					
						
							|  |  |  |     return new PullRequestMergeTask(explicitConfig, git); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const devInfraConfig = getConfig(); | 
					
						
							|  |  |  |   const git = new GitClient(githubToken, devInfraConfig, projectRoot); | 
					
						
							|  |  |  |   const {config, errors} = await loadAndValidateConfig(devInfraConfig, git.github); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (errors) { | 
					
						
							|  |  |  |     error(red('Invalid merge configuration:')); | 
					
						
							|  |  |  |     errors.forEach(desc => error(yellow(`  -  ${desc}`))); | 
					
						
							|  |  |  |     process.exit(1); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Set the remote so that the merge tool has access to information about
 | 
					
						
							|  |  |  |   // the remote it intends to merge to.
 | 
					
						
							|  |  |  |   config!.remote = devInfraConfig.github; | 
					
						
							|  |  |  |   // We can cast this to a merge config with remote because we always set the
 | 
					
						
							|  |  |  |   // remote above.
 | 
					
						
							|  |  |  |   return new PullRequestMergeTask(config! as MergeConfigWithRemote, git); | 
					
						
							|  |  |  | } |