angular-cn/dev-infra/pr/merge/strategies/autosquash-merge.ts
Paul Gschwendtner e19930e70d fix(dev-infra): merge script autosquash should be interactive (#37138)
The components repo does not use the autosquash merge strategy, so
recent changes to that seem to broke the autosquash strategy.

Since we don't run the rebase in interactive mode, the `--autosquash`
flag has no effect. This is by design in Git. We can make it work by
setting the git sequence editor to `true` so that the rebase seems
like an interactive one to Git, while it isn't one for the user.

This matches conceptually with the merge script currently used in
framework. The only difference is that we allow a real interactive
rebase if the `commit message fixup` label is applied. This allows
commit message modifications (and others) if needed.

PR Close #37138
2020-05-18 11:50:08 -07:00

75 lines
4.1 KiB
TypeScript

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> {
const {prNumber, targetBranches, requiredBaseSha, needsCommitMessageFixup} = pullRequest;
// 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();
}
// Git revision range that matches the pull request commits.
const revisionRange = this.getPullRequestRevisionRange(pullRequest);
// Git revision for the first commit the pull request is based on.
const baseRevision = this.getPullRequestBaseRevision(pullRequest);
// 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.
const branchBeforeRebase = this.git.getCurrentBranch();
const rebaseEnv =
needsCommitMessageFixup ? undefined : {...process.env, GIT_SEQUENCE_EDITOR: 'true'};
this.git.run(
['rebase', '--interactive', '--autosquash', baseRevision, TEMP_PR_HEAD_BRANCH],
{stdio: 'inherit', env: rebaseEnv});
// 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.
// **Note**: The filter-branch command relies on the working tree, so we want to make
// sure that we are on the initial branch where the merge script has been run.
this.git.run(['checkout', '-f', branchBeforeRebase]);
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);
return null;
}
}