async merge()

in ng-dev/pr/merge/task.ts [73:169]


  async merge(prNumber: number, force = false): Promise<MergeResult> {
    if (this.git.hasUncommittedChanges()) {
      return {status: MergeStatus.DIRTY_WORKING_DIR};
    }

    if (this.git.isShallowRepo()) {
      return {status: MergeStatus.UNEXPECTED_SHALLOW_REPO};
    }

    // Check whether the given Github token has sufficient permissions for writing
    // to the configured repository. If the repository is not private, only the
    // reduced `public_repo` OAuth scope is sufficient for performing merges.
    const hasOauthScopes = await this.git.hasOauthScopes((scopes, missing) => {
      if (!scopes.includes('repo')) {
        if (this.config.github.private) {
          missing.push('repo');
        } else if (!scopes.includes('public_repo')) {
          missing.push('public_repo');
        }
      }

      // Pull requests can modify Github action workflow files. In such cases Github requires us to
      // push with a token that has the `workflow` oauth scope set. To avoid errors when the
      // caretaker intends to merge such PRs, we ensure the scope is always set on the token before
      // the merge process starts.
      // https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes
      if (!scopes.includes('workflow')) {
        missing.push('workflow');
      }
    });

    if (hasOauthScopes !== true) {
      return {
        status: MergeStatus.GITHUB_ERROR,
        failure: PullRequestFailure.insufficientPermissionsToMerge(hasOauthScopes.error),
      };
    }

    const pullRequest = await loadAndValidatePullRequest(this, prNumber, force);

    if (!isPullRequest(pullRequest)) {
      return {status: MergeStatus.FAILED, failure: pullRequest};
    }

    if (
      this.flags.branchPrompt &&
      !(await promptConfirm(getTargettedBranchesConfirmationPromptMessage(pullRequest)))
    ) {
      return {status: MergeStatus.USER_ABORTED};
    }

    // If the pull request has a caretaker note applied, raise awareness by prompting
    // the caretaker. The caretaker can then decide to proceed or abort the merge.
    if (
      pullRequest.hasCaretakerNote &&
      !(await promptConfirm(getCaretakerNotePromptMessage(pullRequest)))
    ) {
      return {status: MergeStatus.USER_ABORTED};
    }

    const strategy = this.config.pullRequest.githubApiMerge
      ? new GithubApiMergeStrategy(this.git, this.config.pullRequest.githubApiMerge)
      : new AutosquashMergeStrategy(this.git);

    // Branch or revision that is currently checked out so that we can switch back to
    // it once the pull request has been merged.
    const previousBranchOrRevision = this.git.getCurrentBranchOrRevision();

    // The following block runs Git commands as child processes. These Git commands can fail.
    // We want to capture these command errors and return an appropriate merge request status.
    try {
      // Run preparations for the merge (e.g. fetching branches).
      await strategy.prepare(pullRequest);

      // Perform the merge and capture potential failures.
      const failure = await strategy.merge(pullRequest);
      if (failure !== null) {
        return {status: MergeStatus.FAILED, failure};
      }

      // Return a successful merge status.
      return {status: MergeStatus.SUCCESS};
    } catch (e) {
      // Catch all git command errors and return a merge result w/ git error status code.
      // Other unknown errors which aren't caused by a git command are re-thrown.
      if (e instanceof GitCommandError) {
        return {status: MergeStatus.UNKNOWN_GIT_ERROR};
      }
      throw e;
    } finally {
      // Switch back to the previous branch. We need to do this before deleting the temporary
      // branches because we cannot delete branches which are currently checked out.
      this.git.run(['checkout', '-f', previousBranchOrRevision]);

      await strategy.cleanup(pullRequest);
    }
  }