override async merge()

in ng-dev/pr/merge/strategies/api-merge.ts [40:154]


  override async merge(pullRequest: PullRequest): Promise<PullRequestFailure | null> {
    const {githubTargetBranch, prNumber, targetBranches, requiredBaseSha, needsCommitMessageFixup} =
      pullRequest;
    // If the pull request does not have its base branch set to any determined target
    // branch, we cannot merge using the API.
    if (targetBranches.every((t) => t !== githubTargetBranch)) {
      return PullRequestFailure.mismatchingTargetBranch(targetBranches);
    }

    // In cases where a required base commit 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 code ownership 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();
    }

    const method = this._getMergeActionFromPullRequest(pullRequest);
    const cherryPickTargetBranches = targetBranches.filter((b) => b !== githubTargetBranch);

    // First cherry-pick the PR into all local target branches in dry-run mode. This is
    // purely for testing so that we can figure out whether the PR can be cherry-picked
    // into the other target branches. We don't want to merge the PR through the API, and
    // then run into cherry-pick conflicts after the initial merge already completed.
    const failure = await this._checkMergability(pullRequest, cherryPickTargetBranches);

    // If the PR could not be cherry-picked into all target branches locally, we know it can't
    // be done through the Github API either. We abort merging and pass-through the failure.
    if (failure !== null) {
      return failure;
    }

    const mergeOptions: OctokitMergeParams = {
      pull_number: prNumber,
      merge_method: method,
      ...this.git.remoteParams,
    };

    if (needsCommitMessageFixup) {
      // Commit message fixup does not work with other merge methods as the Github API only
      // allows commit message modifications for squash merging.
      if (method !== 'squash') {
        return PullRequestFailure.unableToFixupCommitMessageSquashOnly();
      }
      await this._promptCommitMessageEdit(pullRequest, mergeOptions);
    }

    let mergeStatusCode: number;
    let targetSha: string;

    try {
      // Merge the pull request using the Github API into the selected base branch.
      const result = await this.git.github.pulls.merge(mergeOptions);

      mergeStatusCode = result.status;
      targetSha = result.data.sha;
    } catch (e) {
      // Note: Github usually returns `404` as status code if the API request uses a
      // token with insufficient permissions. Github does this because it doesn't want
      // to leak whether a repository exists or not. In our case we expect a certain
      // repository to exist, so we always treat this as a permission failure.
      if (e instanceof GithubApiRequestError && (e.status === 403 || e.status === 404)) {
        return PullRequestFailure.insufficientPermissionsToMerge();
      }
      throw e;
    }

    // https://developer.github.com/v3/pulls/#response-if-merge-cannot-be-performed
    // Pull request cannot be merged due to merge conflicts.
    if (mergeStatusCode === 405) {
      return PullRequestFailure.mergeConflicts([githubTargetBranch]);
    }
    if (mergeStatusCode !== 200) {
      return PullRequestFailure.unknownMergeError();
    }

    // If the PR does  not need to be merged into any other target branches,
    // we exit here as we already completed the merge.
    if (!cherryPickTargetBranches.length) {
      return null;
    }

    // Refresh the target branch the PR has been merged into through the API. We need
    // to re-fetch as otherwise we cannot cherry-pick the new commits into the remaining
    // target branches.
    this.fetchTargetBranches([githubTargetBranch]);

    // Number of commits that have landed in the target branch. This could vary from
    // the count of commits in the PR due to squashing.
    const targetCommitsCount = method === 'squash' ? 1 : pullRequest.commitCount;

    // Cherry pick the merged commits into the remaining target branches.
    const failedBranches = await this.cherryPickIntoTargetBranches(
      `${targetSha}~${targetCommitsCount}..${targetSha}`,
      cherryPickTargetBranches,
      {
        // Commits that have been created by the Github API do not necessarily contain
        // a reference to the source pull request (unless the squash strategy is used).
        // To ensure that original commits can be found when a commit is viewed in a
        // target branch, we add a link to the original commits when cherry-picking.
        linkToOriginalCommits: true,
      },
    );

    // We already checked whether the PR can be cherry-picked into the target branches,
    // but in case the cherry-pick somehow fails, we still handle the conflicts here. The
    // commits created through the Github API could be different (i.e. through squash).
    if (failedBranches.length) {
      return PullRequestFailure.mergeConflicts(failedBranches);
    }

    this.pushTargetBranchesUpstream(cherryPickTargetBranches);
    return null;
  }