2020-05-15 11:19:13 -04: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> {
|
|
|
|
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);
|
|
|
|
|
2020-05-15 09:49:38 -04: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-05-15 11:19:13 -04:00
|
|
|
const branchBeforeRebase = this.git.getCurrentBranch();
|
2020-05-15 09:49:38 -04:00
|
|
|
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});
|
2020-05-15 11:19:13 -04: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.
|
|
|
|
// **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;
|
|
|
|
}
|
|
|
|
}
|