| 
									
										
										
										
											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-06-16 00:20:36 +02:00
										 |  |  | import {promptConfirm} from '../../utils/console'; | 
					
						
							| 
									
										
										
										
											2020-10-01 16:06:56 -07:00
										 |  |  | import {GitClient, GitCommandError} from '../../utils/git/index'; | 
					
						
							| 
									
										
										
										
											2020-05-27 15:39:31 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-15 10:58:51 -07:00
										 |  |  | import {MergeConfigWithRemote} from './config'; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | import {PullRequestFailure} from './failures'; | 
					
						
							| 
									
										
										
										
											2020-10-15 10:58:51 -07:00
										 |  |  | import {getCaretakerNotePromptMessage, getTargettedBranchesConfirmationPromptMessage} from './messages'; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | import {isPullRequest, loadAndValidatePullRequest,} from './pull-request'; | 
					
						
							|  |  |  | import {GithubApiMergeStrategy} from './strategies/api-merge'; | 
					
						
							|  |  |  | import {AutosquashMergeStrategy} from './strategies/autosquash-merge'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** Describes the status of a pull request merge. */ | 
					
						
							|  |  |  | export const enum MergeStatus { | 
					
						
							|  |  |  |   UNKNOWN_GIT_ERROR, | 
					
						
							|  |  |  |   DIRTY_WORKING_DIR, | 
					
						
							|  |  |  |   SUCCESS, | 
					
						
							|  |  |  |   FAILED, | 
					
						
							| 
									
										
										
										
											2020-06-16 00:20:36 +02:00
										 |  |  |   USER_ABORTED, | 
					
						
							| 
									
										
										
										
											2020-06-03 12:12:52 -07:00
										 |  |  |   GITHUB_ERROR, | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** Result of a pull request merge. */ | 
					
						
							|  |  |  | export interface MergeResult { | 
					
						
							|  |  |  |   /** Overall status of the merge. */ | 
					
						
							|  |  |  |   status: MergeStatus; | 
					
						
							|  |  |  |   /** List of pull request failures. */ | 
					
						
							|  |  |  |   failure?: PullRequestFailure; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Class that accepts a merge script configuration and Github token. It provides | 
					
						
							|  |  |  |  * a programmatic interface for merging multiple pull requests based on their | 
					
						
							|  |  |  |  * labels that have been resolved through the merge script configuration. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export class PullRequestMergeTask { | 
					
						
							| 
									
										
										
										
											2020-07-24 17:59:12 +02:00
										 |  |  |   constructor(public config: MergeConfigWithRemote, public git: GitClient) {} | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Merges the given pull request and pushes it upstream. | 
					
						
							|  |  |  |    * @param prNumber Pull request that should be merged. | 
					
						
							|  |  |  |    * @param force Whether non-critical pull request failures should be ignored. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   async merge(prNumber: number, force = false): Promise<MergeResult> { | 
					
						
							| 
									
										
										
										
											2020-06-25 00:40:15 +02:00
										 |  |  |     // Check whether the given Github token has sufficient permissions for writing
 | 
					
						
							|  |  |  |     // to the configured repository. If the repository is not private, only the
 | 
					
						
							|  |  |  |     // reduced `public_repo` OAuth scope is sufficient for performing merges.
 | 
					
						
							|  |  |  |     const hasOauthScopes = await this.git.hasOauthScopes((scopes, missing) => { | 
					
						
							|  |  |  |       if (!scopes.includes('repo')) { | 
					
						
							|  |  |  |         if (this.config.remote.private) { | 
					
						
							|  |  |  |           missing.push('repo'); | 
					
						
							|  |  |  |         } else if (!scopes.includes('public_repo')) { | 
					
						
							|  |  |  |           missing.push('public_repo'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-03 12:12:52 -07:00
										 |  |  |     if (hasOauthScopes !== true) { | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         status: MergeStatus.GITHUB_ERROR, | 
					
						
							|  |  |  |         failure: PullRequestFailure.insufficientPermissionsToMerge(hasOauthScopes.error) | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |     if (this.git.hasUncommittedChanges()) { | 
					
						
							|  |  |  |       return {status: MergeStatus.DIRTY_WORKING_DIR}; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const pullRequest = await loadAndValidatePullRequest(this, prNumber, force); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!isPullRequest(pullRequest)) { | 
					
						
							|  |  |  |       return {status: MergeStatus.FAILED, failure: pullRequest}; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-15 10:58:51 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (!await promptConfirm(getTargettedBranchesConfirmationPromptMessage(pullRequest))) { | 
					
						
							|  |  |  |       return {status: MergeStatus.USER_ABORTED}; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-16 00:20:36 +02:00
										 |  |  |     // If the pull request has a caretaker note applied, raise awareness by prompting
 | 
					
						
							|  |  |  |     // the caretaker. The caretaker can then decide to proceed or abort the merge.
 | 
					
						
							|  |  |  |     if (pullRequest.hasCaretakerNote && | 
					
						
							| 
									
										
										
										
											2020-10-15 10:58:51 -07:00
										 |  |  |         !await promptConfirm(getCaretakerNotePromptMessage(pullRequest))) { | 
					
						
							| 
									
										
										
										
											2020-06-16 00:20:36 +02:00
										 |  |  |       return {status: MergeStatus.USER_ABORTED}; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |     const strategy = this.config.githubApiMerge ? | 
					
						
							|  |  |  |         new GithubApiMergeStrategy(this.git, this.config.githubApiMerge) : | 
					
						
							|  |  |  |         new AutosquashMergeStrategy(this.git); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-25 16:15:14 +02:00
										 |  |  |     // Branch or revision that is currently checked out so that we can switch back to
 | 
					
						
							|  |  |  |     // it once the pull request has been merged.
 | 
					
						
							|  |  |  |     let previousBranchOrRevision: null|string = null; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // The following block runs Git commands as child processes. These Git commands can fail.
 | 
					
						
							|  |  |  |     // We want to capture these command errors and return an appropriate merge request status.
 | 
					
						
							|  |  |  |     try { | 
					
						
							| 
									
										
										
										
											2020-06-25 16:15:14 +02:00
										 |  |  |       previousBranchOrRevision = this.git.getCurrentBranchOrRevision(); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // Run preparations for the merge (e.g. fetching branches).
 | 
					
						
							|  |  |  |       await strategy.prepare(pullRequest); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Perform the merge and capture potential failures.
 | 
					
						
							|  |  |  |       const failure = await strategy.merge(pullRequest); | 
					
						
							|  |  |  |       if (failure !== null) { | 
					
						
							|  |  |  |         return {status: MergeStatus.FAILED, failure}; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Switch back to the previous branch. We need to do this before deleting the temporary
 | 
					
						
							|  |  |  |       // branches because we cannot delete branches which are currently checked out.
 | 
					
						
							| 
									
										
										
										
											2020-06-25 16:15:14 +02:00
										 |  |  |       this.git.run(['checkout', '-f', previousBranchOrRevision]); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |       await strategy.cleanup(pullRequest); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Return a successful merge status.
 | 
					
						
							|  |  |  |       return {status: MergeStatus.SUCCESS}; | 
					
						
							|  |  |  |     } catch (e) { | 
					
						
							|  |  |  |       // Catch all git command errors and return a merge result w/ git error status code.
 | 
					
						
							|  |  |  |       // Other unknown errors which aren't caused by a git command are re-thrown.
 | 
					
						
							|  |  |  |       if (e instanceof GitCommandError) { | 
					
						
							|  |  |  |         return {status: MergeStatus.UNKNOWN_GIT_ERROR}; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       throw e; | 
					
						
							|  |  |  |     } finally { | 
					
						
							|  |  |  |       // Always try to restore the branch if possible. We don't want to leave
 | 
					
						
							|  |  |  |       // the repository in a different state than before.
 | 
					
						
							| 
									
										
										
										
											2020-06-25 16:15:14 +02:00
										 |  |  |       if (previousBranchOrRevision !== null) { | 
					
						
							|  |  |  |         this.git.runGraceful(['checkout', '-f', previousBranchOrRevision]); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |