| 
									
										
										
										
											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
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-03 15:59:20 +02:00
										 |  |  | import {AuthenticatedGitClient} from '../../../utils/git/authenticated-git-client'; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | import {PullRequestFailure} from '../failures'; | 
					
						
							|  |  |  | import {PullRequest} from '../pull-request'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Name of a temporary branch that contains the head of a currently-processed PR. Note | 
					
						
							|  |  |  |  * that a branch name should be used that most likely does not conflict with other local | 
					
						
							|  |  |  |  * development branches. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export const TEMP_PR_HEAD_BRANCH = 'merge_pr_head'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Base class for merge strategies. A merge strategy accepts a pull request and | 
					
						
							|  |  |  |  * merges it into the determined target branches. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export abstract class MergeStrategy { | 
					
						
							| 
									
										
										
										
											2021-06-03 15:59:20 +02:00
										 |  |  |   constructor(protected git: AuthenticatedGitClient) {} | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Prepares a merge of the given pull request. The strategy by default will | 
					
						
							|  |  |  |    * fetch all target branches and the pull request into local temporary branches. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   async prepare(pullRequest: PullRequest) { | 
					
						
							|  |  |  |     this.fetchTargetBranches( | 
					
						
							|  |  |  |         pullRequest.targetBranches, `pull/${pullRequest.prNumber}/head:${TEMP_PR_HEAD_BRANCH}`); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Performs the merge of the given pull request. This needs to be implemented | 
					
						
							|  |  |  |    * by individual merge strategies. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   abstract merge(pullRequest: PullRequest): Promise<null|PullRequestFailure>; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Cleans up the pull request merge. e.g. deleting temporary local branches. */ | 
					
						
							|  |  |  |   async cleanup(pullRequest: PullRequest) { | 
					
						
							|  |  |  |     // Delete all temporary target branches.
 | 
					
						
							|  |  |  |     pullRequest.targetBranches.forEach( | 
					
						
							|  |  |  |         branchName => this.git.run(['branch', '-D', this.getLocalTargetBranchName(branchName)])); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Delete temporary branch for the pull request head.
 | 
					
						
							|  |  |  |     this.git.run(['branch', '-D', TEMP_PR_HEAD_BRANCH]); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Gets the revision range for all commits in the given pull request. */ | 
					
						
							|  |  |  |   protected getPullRequestRevisionRange(pullRequest: PullRequest): string { | 
					
						
							|  |  |  |     return `${this.getPullRequestBaseRevision(pullRequest)}..${TEMP_PR_HEAD_BRANCH}`; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Gets the base revision of a pull request. i.e. the commit the PR is based on. */ | 
					
						
							|  |  |  |   protected getPullRequestBaseRevision(pullRequest: PullRequest): string { | 
					
						
							|  |  |  |     return `${TEMP_PR_HEAD_BRANCH}~${pullRequest.commitCount}`; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Gets a deterministic local branch name for a given branch. */ | 
					
						
							|  |  |  |   protected getLocalTargetBranchName(targetBranch: string): string { | 
					
						
							|  |  |  |     return `merge_pr_target_${targetBranch.replace(/\//g, '_')}`; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Cherry-picks the given revision range into the specified target branches. | 
					
						
							|  |  |  |    * @returns A list of branches for which the revisions could not be cherry-picked into. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   protected cherryPickIntoTargetBranches(revisionRange: string, targetBranches: string[], options: { | 
					
						
							| 
									
										
										
										
											2020-07-02 13:48:14 +02:00
										 |  |  |     dryRun?: boolean, | 
					
						
							|  |  |  |     linkToOriginalCommits?: boolean, | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |   } = {}) { | 
					
						
							|  |  |  |     const cherryPickArgs = [revisionRange]; | 
					
						
							|  |  |  |     const failedBranches: string[] = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (options.dryRun) { | 
					
						
							|  |  |  |       // https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt---no-commit
 | 
					
						
							|  |  |  |       // This causes `git cherry-pick` to not generate any commits. Instead, the changes are
 | 
					
						
							|  |  |  |       // applied directly in the working tree. This allow us to easily discard the changes
 | 
					
						
							|  |  |  |       // for dry-run purposes.
 | 
					
						
							|  |  |  |       cherryPickArgs.push('--no-commit'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-02 13:48:14 +02:00
										 |  |  |     if (options.linkToOriginalCommits) { | 
					
						
							|  |  |  |       // We add `-x` when cherry-picking as that will allow us to easily jump to original
 | 
					
						
							|  |  |  |       // commits for cherry-picked commits. With that flag set, Git will automatically append
 | 
					
						
							|  |  |  |       // the original SHA/revision to the commit message. e.g. `(cherry picked from commit <..>)`.
 | 
					
						
							|  |  |  |       // https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt--x.
 | 
					
						
							|  |  |  |       cherryPickArgs.push('-x'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |     // Cherry-pick the refspec into all determined target branches.
 | 
					
						
							|  |  |  |     for (const branchName of targetBranches) { | 
					
						
							|  |  |  |       const localTargetBranch = this.getLocalTargetBranchName(branchName); | 
					
						
							|  |  |  |       // Checkout the local target branch.
 | 
					
						
							|  |  |  |       this.git.run(['checkout', localTargetBranch]); | 
					
						
							|  |  |  |       // Cherry-pick the refspec into the target branch.
 | 
					
						
							|  |  |  |       if (this.git.runGraceful(['cherry-pick', ...cherryPickArgs]).status !== 0) { | 
					
						
							|  |  |  |         // Abort the failed cherry-pick. We do this because Git persists the failed
 | 
					
						
							|  |  |  |         // cherry-pick state globally in the repository. This could prevent future
 | 
					
						
							|  |  |  |         // pull request merges as a Git thinks a cherry-pick is still in progress.
 | 
					
						
							|  |  |  |         this.git.runGraceful(['cherry-pick', '--abort']); | 
					
						
							|  |  |  |         failedBranches.push(branchName); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       // If we run with dry run mode, we reset the local target branch so that all dry-run
 | 
					
						
							|  |  |  |       // cherry-pick changes are discard. Changes are applied to the working tree and index.
 | 
					
						
							|  |  |  |       if (options.dryRun) { | 
					
						
							|  |  |  |         this.git.run(['reset', '--hard', 'HEAD']); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return failedBranches; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Fetches the given target branches. Also accepts a list of additional refspecs that | 
					
						
							|  |  |  |    * should be fetched. This is helpful as multiple slow fetches could be avoided. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   protected fetchTargetBranches(names: string[], ...extraRefspecs: string[]) { | 
					
						
							|  |  |  |     const fetchRefspecs = names.map(targetBranch => { | 
					
						
							|  |  |  |       const localTargetBranch = this.getLocalTargetBranchName(targetBranch); | 
					
						
							|  |  |  |       return `refs/heads/${targetBranch}:${localTargetBranch}`; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     // Fetch all target branches with a single command. We don't want to fetch them
 | 
					
						
							|  |  |  |     // individually as that could cause an unnecessary slow-down.
 | 
					
						
							| 
									
										
										
										
											2021-04-08 12:34:55 -07:00
										 |  |  |     this.git.run( | 
					
						
							|  |  |  |         ['fetch', '-q', '-f', this.git.getRepoGitUrl(), ...fetchRefspecs, ...extraRefspecs]); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** Pushes the given target branches upstream. */ | 
					
						
							|  |  |  |   protected pushTargetBranchesUpstream(names: string[]) { | 
					
						
							|  |  |  |     const pushRefspecs = names.map(targetBranch => { | 
					
						
							|  |  |  |       const localTargetBranch = this.getLocalTargetBranchName(targetBranch); | 
					
						
							|  |  |  |       return `${localTargetBranch}:refs/heads/${targetBranch}`; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     // Push all target branches with a single command if we don't run in dry-run mode.
 | 
					
						
							|  |  |  |     // We don't want to push them individually as that could cause an unnecessary slow-down.
 | 
					
						
							| 
									
										
										
										
											2021-04-08 12:34:55 -07:00
										 |  |  |     this.git.run(['push', this.git.getRepoGitUrl(), ...pushRefspecs]); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |   } | 
					
						
							|  |  |  | } |