When fetch is run in normal mode, the `git-fetch-pack` plumbing command outputs progress to stderr. Since this is unnecessary progress information for ng-dev usages, it should be suppressed instead. PR Close #39503
		
			
				
	
	
		
			129 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			129 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
/**
 | 
						|
 * @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
 | 
						|
 */
 | 
						|
 | 
						|
import {types as graphQLTypes} from 'typed-graphqlify';
 | 
						|
 | 
						|
import {info} from '../../utils/console';
 | 
						|
import {addTokenToGitHttpsUrl} from '../../utils/git/github-urls';
 | 
						|
import {GitClient} from '../../utils/git/index';
 | 
						|
import {getPr} from '../../utils/github';
 | 
						|
 | 
						|
/* GraphQL schema for the response body for a pending PR. */
 | 
						|
const PR_SCHEMA = {
 | 
						|
  state: graphQLTypes.string,
 | 
						|
  maintainerCanModify: graphQLTypes.boolean,
 | 
						|
  viewerDidAuthor: graphQLTypes.boolean,
 | 
						|
  headRefOid: graphQLTypes.string,
 | 
						|
  headRef: {
 | 
						|
    name: graphQLTypes.string,
 | 
						|
    repository: {
 | 
						|
      url: graphQLTypes.string,
 | 
						|
      nameWithOwner: graphQLTypes.string,
 | 
						|
    },
 | 
						|
  },
 | 
						|
  baseRef: {
 | 
						|
    name: graphQLTypes.string,
 | 
						|
    repository: {
 | 
						|
      url: graphQLTypes.string,
 | 
						|
      nameWithOwner: graphQLTypes.string,
 | 
						|
    },
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
 | 
						|
export class UnexpectedLocalChangesError extends Error {
 | 
						|
  constructor(m: string) {
 | 
						|
    super(m);
 | 
						|
    Object.setPrototypeOf(this, UnexpectedLocalChangesError.prototype);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export class MaintainerModifyAccessError extends Error {
 | 
						|
  constructor(m: string) {
 | 
						|
    super(m);
 | 
						|
    Object.setPrototypeOf(this, MaintainerModifyAccessError.prototype);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/** Options for checking out a PR */
 | 
						|
export interface PullRequestCheckoutOptions {
 | 
						|
  /** Whether the PR should be checked out if the maintainer cannot modify. */
 | 
						|
  allowIfMaintainerCannotModify?: boolean;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Rebase the provided PR onto its merge target branch, and push up the resulting
 | 
						|
 * commit to the PRs repository.
 | 
						|
 */
 | 
						|
export async function checkOutPullRequestLocally(
 | 
						|
    prNumber: number, githubToken: string, opts: PullRequestCheckoutOptions = {}) {
 | 
						|
  /** Authenticated Git client for git and Github interactions. */
 | 
						|
  const git = new GitClient(githubToken);
 | 
						|
 | 
						|
  // In order to preserve local changes, checkouts cannot occur if local changes are present in the
 | 
						|
  // git environment. Checked before retrieving the PR to fail fast.
 | 
						|
  if (git.hasLocalChanges()) {
 | 
						|
    throw new UnexpectedLocalChangesError('Unable to checkout PR due to uncommitted changes.');
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * The branch or revision originally checked out before this method performed
 | 
						|
   * any Git operations that may change the working branch.
 | 
						|
   */
 | 
						|
  const previousBranchOrRevision = git.getCurrentBranchOrRevision();
 | 
						|
  /* The PR information from Github. */
 | 
						|
  const pr = await getPr(PR_SCHEMA, prNumber, git);
 | 
						|
  /** The branch name of the PR from the repository the PR came from. */
 | 
						|
  const headRefName = pr.headRef.name;
 | 
						|
  /** The full ref for the repository and branch the PR came from. */
 | 
						|
  const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`;
 | 
						|
  /** The full URL path of the repository the PR came from with github token as authentication. */
 | 
						|
  const headRefUrl = addTokenToGitHttpsUrl(pr.headRef.repository.url, githubToken);
 | 
						|
  // Note: Since we use a detached head for rebasing the PR and therefore do not have
 | 
						|
  // remote-tracking branches configured, we need to set our expected ref and SHA. This
 | 
						|
  // allows us to use `--force-with-lease` for the detached head while ensuring that we
 | 
						|
  // never accidentally override upstream changes that have been pushed in the meanwhile.
 | 
						|
  // See:
 | 
						|
  // https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt
 | 
						|
  /** Flag for a force push with leage back to upstream. */
 | 
						|
  const forceWithLeaseFlag = `--force-with-lease=${headRefName}:${pr.headRefOid}`;
 | 
						|
 | 
						|
  // If the PR does not allow maintainers to modify it, exit as the rebased PR cannot
 | 
						|
  // be pushed up.
 | 
						|
  if (!pr.maintainerCanModify && !pr.viewerDidAuthor && !opts.allowIfMaintainerCannotModify) {
 | 
						|
    throw new MaintainerModifyAccessError('PR is not set to allow maintainers to modify the PR');
 | 
						|
  }
 | 
						|
 | 
						|
  try {
 | 
						|
    // Fetch the branch at the commit of the PR, and check it out in a detached state.
 | 
						|
    info(`Checking out PR #${prNumber} from ${fullHeadRef}`);
 | 
						|
    git.run(['fetch', '-q', headRefUrl, headRefName]);
 | 
						|
    git.run(['checkout', '--detach', 'FETCH_HEAD']);
 | 
						|
  } catch (e) {
 | 
						|
    git.checkout(previousBranchOrRevision, true);
 | 
						|
    throw e;
 | 
						|
  }
 | 
						|
 | 
						|
  return {
 | 
						|
    /**
 | 
						|
     * Pushes the current local branch to the PR on the upstream repository.
 | 
						|
     *
 | 
						|
     * @returns true If the command did not fail causing a GitCommandError to be thrown.
 | 
						|
     * @throws GitCommandError Thrown when the push back to upstream fails.
 | 
						|
     */
 | 
						|
    pushToUpstream: (): true => {
 | 
						|
      git.run(['push', headRefUrl, `HEAD:${headRefName}`, forceWithLeaseFlag]);
 | 
						|
      return true;
 | 
						|
    },
 | 
						|
    /** Restores the state of the local repository to before the PR checkout occured. */
 | 
						|
    resetGitState: (): boolean => {
 | 
						|
      return git.checkout(previousBranchOrRevision, true);
 | 
						|
    }
 | 
						|
  };
 | 
						|
}
 |