| 
									
										
										
										
											2020-05-20 16:04:46 -07: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:19:13 +02:00
										 |  |  | import {join} from 'path'; | 
					
						
							|  |  |  | import {PullRequestFailure} from '../failures'; | 
					
						
							|  |  |  | import {PullRequest} from '../pull-request'; | 
					
						
							|  |  |  | import {MergeStrategy, TEMP_PR_HEAD_BRANCH} from './strategy'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** Path to the commit message filter script. Git expects this paths to use forward slashes. */ | 
					
						
							|  |  |  | const MSG_FILTER_SCRIPT = join(__dirname, './commit-message-filter.js').replace(/\\/g, '/'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Merge strategy that does not use the Github API for merging. Instead, it fetches | 
					
						
							|  |  |  |  * all target branches and the PR locally. The PR is then cherry-picked with autosquash | 
					
						
							|  |  |  |  * enabled into the target branches. The benefit is the support for fixup and squash commits. | 
					
						
							|  |  |  |  * A notable downside though is that Github does not show the PR as `Merged` due to non | 
					
						
							|  |  |  |  * fast-forward merges | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export class AutosquashMergeStrategy extends MergeStrategy { | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Merges the specified pull request into the target branches and pushes the target | 
					
						
							|  |  |  |    * branches upstream. This method requires the temporary target branches to be fetched | 
					
						
							|  |  |  |    * already as we don't want to fetch the target branches per pull request merge. This | 
					
						
							|  |  |  |    * would causes unnecessary multiple fetch requests when multiple PRs are merged. | 
					
						
							|  |  |  |    * @throws {GitCommandError} An unknown Git command error occurred that is not | 
					
						
							|  |  |  |    *   specific to the pull request merge. | 
					
						
							|  |  |  |    * @returns A pull request failure or null in case of success. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   async merge(pullRequest: PullRequest): Promise<PullRequestFailure|null> { | 
					
						
							| 
									
										
										
										
											2020-12-04 15:16:14 -08:00
										 |  |  |     const {prNumber, targetBranches, requiredBaseSha, needsCommitMessageFixup, githubTargetBranch} = | 
					
						
							|  |  |  |         pullRequest; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |     // In case a required base is specified for this pull request, check if the pull
 | 
					
						
							|  |  |  |     // request contains the given commit. If not, return a pull request failure. This
 | 
					
						
							|  |  |  |     // check is useful for enforcing that PRs are rebased on top of a given commit. e.g.
 | 
					
						
							|  |  |  |     // a commit that changes the codeowner ship validation. PRs which are not rebased
 | 
					
						
							|  |  |  |     // could bypass new codeowner ship rules.
 | 
					
						
							|  |  |  |     if (requiredBaseSha && !this.git.hasCommit(TEMP_PR_HEAD_BRANCH, requiredBaseSha)) { | 
					
						
							|  |  |  |       return PullRequestFailure.unsatisfiedBaseSha(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 16:19:19 +02:00
										 |  |  |     // SHA for the first commit the pull request is based on. Usually we would able
 | 
					
						
							|  |  |  |     // to just rely on the base revision provided by `getPullRequestBaseRevision`, but
 | 
					
						
							|  |  |  |     // the revision would rely on the amount of commits in a pull request. This is not
 | 
					
						
							|  |  |  |     // reliable as we rebase the PR with autosquash where the amount of commits could
 | 
					
						
							|  |  |  |     // change. We work around this by parsing the base revision so that we have a fixated
 | 
					
						
							|  |  |  |     // SHA before the autosquash rebase is performed.
 | 
					
						
							|  |  |  |     const baseSha = | 
					
						
							|  |  |  |         this.git.run(['rev-parse', this.getPullRequestBaseRevision(pullRequest)]).stdout.trim(); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |     // Git revision range that matches the pull request commits.
 | 
					
						
							| 
									
										
										
										
											2020-05-15 16:19:19 +02:00
										 |  |  |     const revisionRange = `${baseSha}..${TEMP_PR_HEAD_BRANCH}`; | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 15:49:38 +02:00
										 |  |  |     // We always rebase the pull request so that fixup or squash commits are automatically
 | 
					
						
							|  |  |  |     // collapsed. Git's autosquash functionality does only work in interactive rebases, so
 | 
					
						
							|  |  |  |     // our rebase is always interactive. In reality though, unless a commit message fixup
 | 
					
						
							|  |  |  |     // is desired, we set the `GIT_SEQUENCE_EDITOR` environment variable to `true` so that
 | 
					
						
							|  |  |  |     // the rebase seems interactive to Git, while it's not interactive to the user.
 | 
					
						
							|  |  |  |     // See: https://github.com/git/git/commit/891d4a0313edc03f7e2ecb96edec5d30dc182294.
 | 
					
						
							| 
									
										
										
										
											2020-06-25 16:15:14 +02:00
										 |  |  |     const branchOrRevisionBeforeRebase = this.git.getCurrentBranchOrRevision(); | 
					
						
							| 
									
										
										
										
											2020-05-15 15:49:38 +02:00
										 |  |  |     const rebaseEnv = | 
					
						
							|  |  |  |         needsCommitMessageFixup ? undefined : {...process.env, GIT_SEQUENCE_EDITOR: 'true'}; | 
					
						
							|  |  |  |     this.git.run( | 
					
						
							| 
									
										
										
										
											2020-05-15 16:19:19 +02:00
										 |  |  |         ['rebase', '--interactive', '--autosquash', baseSha, TEMP_PR_HEAD_BRANCH], | 
					
						
							| 
									
										
										
										
											2020-05-15 15:49:38 +02:00
										 |  |  |         {stdio: 'inherit', env: rebaseEnv}); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Update pull requests commits to reference the pull request. This matches what
 | 
					
						
							|  |  |  |     // Github does when pull requests are merged through the Web UI. The motivation is
 | 
					
						
							|  |  |  |     // that it should be easy to determine which pull request contained a given commit.
 | 
					
						
							| 
									
										
										
										
											2020-06-25 16:15:14 +02:00
										 |  |  |     // Note: The filter-branch command relies on the working tree, so we want to make sure
 | 
					
						
							|  |  |  |     // that we are on the initial branch or revision where the merge script has been invoked.
 | 
					
						
							|  |  |  |     this.git.run(['checkout', '-f', branchOrRevisionBeforeRebase]); | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |     this.git.run( | 
					
						
							|  |  |  |         ['filter-branch', '-f', '--msg-filter', `${MSG_FILTER_SCRIPT} ${prNumber}`, revisionRange]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Cherry-pick the pull request into all determined target branches.
 | 
					
						
							|  |  |  |     const failedBranches = this.cherryPickIntoTargetBranches(revisionRange, targetBranches); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (failedBranches.length) { | 
					
						
							|  |  |  |       return PullRequestFailure.mergeConflicts(failedBranches); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     this.pushTargetBranchesUpstream(targetBranches); | 
					
						
							| 
									
										
										
										
											2020-12-04 15:16:14 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // For PRs which do not target the `master` branch on Github, Github does not automatically
 | 
					
						
							|  |  |  |     // close the PR when its commit is pushed into the repository.  To ensure these PRs are
 | 
					
						
							|  |  |  |     // correctly marked as closed, we must detect this situation and close the PR via the API after
 | 
					
						
							|  |  |  |     // the upstream pushes are completed.
 | 
					
						
							|  |  |  |     if (githubTargetBranch !== 'master') { | 
					
						
							|  |  |  |       /** The local branch name of the github targeted branch. */ | 
					
						
							|  |  |  |       const localBranch = this.getLocalTargetBranchName(githubTargetBranch); | 
					
						
							|  |  |  |       /** The SHA of the commit pushed to github which represents closing the PR. */ | 
					
						
							|  |  |  |       const sha = this.git.run(['rev-parse', localBranch]).stdout.trim(); | 
					
						
							|  |  |  |       // Create a comment saying the PR was closed by the SHA.
 | 
					
						
							|  |  |  |       await this.git.github.issues.createComment({ | 
					
						
							|  |  |  |         ...this.git.remoteParams, | 
					
						
							|  |  |  |         issue_number: pullRequest.prNumber, | 
					
						
							|  |  |  |         body: `Closed by commit ${sha}` | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |       // Actually close the PR.
 | 
					
						
							|  |  |  |       await this.git.github.pulls.update({ | 
					
						
							|  |  |  |         ...this.git.remoteParams, | 
					
						
							|  |  |  |         pull_number: pullRequest.prNumber, | 
					
						
							|  |  |  |         state: 'closed', | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 17:19:13 +02:00
										 |  |  |     return null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |