Integrates the merge script into the `ng-dev` CLI. The goal is that caretakers can run the same command across repositories to merge a pull request. The command is as followed: `yarn ng-dev pr merge <number>`. PR Close #37138
		
			
				
	
	
		
			118 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			118 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 * as Octokit from '@octokit/rest';
 | 
						|
import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
 | 
						|
import {MergeConfigWithRemote} from './config';
 | 
						|
 | 
						|
/** Error for failed Github API requests. */
 | 
						|
export class GithubApiRequestError extends Error {
 | 
						|
  constructor(public status: number, message: string) {
 | 
						|
    super(message);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/** Error for failed Git commands. */
 | 
						|
export class GitCommandError extends Error {
 | 
						|
  constructor(client: GitClient, public args: string[]) {
 | 
						|
    // Errors are not guaranteed to be caught. To ensure that we don't
 | 
						|
    // accidentally leak the Github token that might be used in a command,
 | 
						|
    // we sanitize the command that will be part of the error message.
 | 
						|
    super(`Command failed: git ${client.omitGithubTokenFromMessage(args.join(' '))}`);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export class GitClient {
 | 
						|
  /** Short-hand for accessing the remote configuration. */
 | 
						|
  remoteConfig = this._config.remote;
 | 
						|
  /** Octokit request parameters object for targeting the configured remote. */
 | 
						|
  remoteParams = {owner: this.remoteConfig.owner, repo: this.remoteConfig.name};
 | 
						|
  /** URL that resolves to the configured repository. */
 | 
						|
  repoGitUrl = this.remoteConfig.useSsh ?
 | 
						|
      `git@github.com:${this.remoteConfig.owner}/${this.remoteConfig.name}.git` :
 | 
						|
      `https://${this._githubToken}@github.com/${this.remoteConfig.owner}/${
 | 
						|
          this.remoteConfig.name}.git`;
 | 
						|
  /** Instance of the authenticated Github octokit API. */
 | 
						|
  api: Octokit;
 | 
						|
 | 
						|
  /** Regular expression that matches the provided Github token. */
 | 
						|
  private _tokenRegex = new RegExp(this._githubToken, 'g');
 | 
						|
 | 
						|
  constructor(
 | 
						|
      private _projectRoot: string, private _githubToken: string,
 | 
						|
      private _config: MergeConfigWithRemote) {
 | 
						|
    this.api = new Octokit({auth: _githubToken});
 | 
						|
    this.api.hook.error('request', error => {
 | 
						|
      // Wrap API errors in a known error class. This allows us to
 | 
						|
      // expect Github API errors better and in a non-ambiguous way.
 | 
						|
      throw new GithubApiRequestError(error.status, error.message);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  /** Executes the given git command. Throws if the command fails. */
 | 
						|
  run(args: string[], options?: SpawnSyncOptions): Omit<SpawnSyncReturns<string>, 'status'> {
 | 
						|
    const result = this.runGraceful(args, options);
 | 
						|
    if (result.status !== 0) {
 | 
						|
      throw new GitCommandError(this, args);
 | 
						|
    }
 | 
						|
    // Omit `status` from the type so that it's obvious that the status is never
 | 
						|
    // non-zero as explained in the method description.
 | 
						|
    return result as Omit<SpawnSyncReturns<string>, 'status'>;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Spawns a given Git command process. Does not throw if the command fails. Additionally,
 | 
						|
   * if there is any stderr output, the output will be printed. This makes it easier to
 | 
						|
   * debug failed commands.
 | 
						|
   */
 | 
						|
  runGraceful(args: string[], options: SpawnSyncOptions = {}): SpawnSyncReturns<string> {
 | 
						|
    // To improve the debugging experience in case something fails, we print all executed
 | 
						|
    // Git commands. Note that we do not want to print the token if is contained in the
 | 
						|
    // command. It's common to share errors with others if the tool failed.
 | 
						|
    console.info('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
 | 
						|
 | 
						|
    const result = spawnSync('git', args, {
 | 
						|
      cwd: this._projectRoot,
 | 
						|
      stdio: 'pipe',
 | 
						|
      ...options,
 | 
						|
      // Encoding is always `utf8` and not overridable. This ensures that this method
 | 
						|
      // always returns `string` as output instead of buffers.
 | 
						|
      encoding: 'utf8',
 | 
						|
    });
 | 
						|
 | 
						|
    if (result.stderr !== null) {
 | 
						|
      // Git sometimes prints the command if it failed. This means that it could
 | 
						|
      // potentially leak the Github token used for accessing the remote. To avoid
 | 
						|
      // printing a token, we sanitize the string before printing the stderr output.
 | 
						|
      process.stderr.write(this.omitGithubTokenFromMessage(result.stderr));
 | 
						|
    }
 | 
						|
 | 
						|
    return result;
 | 
						|
  }
 | 
						|
 | 
						|
  /** Whether the given branch contains the specified SHA. */
 | 
						|
  hasCommit(branchName: string, sha: string): boolean {
 | 
						|
    return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
 | 
						|
  }
 | 
						|
 | 
						|
  /** Gets the currently checked out branch. */
 | 
						|
  getCurrentBranch(): string {
 | 
						|
    return this.run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
 | 
						|
  }
 | 
						|
 | 
						|
  /** Gets whether the current Git repository has uncommitted changes. */
 | 
						|
  hasUncommittedChanges(): boolean {
 | 
						|
    return this.runGraceful(['diff-index', '--quiet', 'HEAD']).status !== 0;
 | 
						|
  }
 | 
						|
 | 
						|
  /** Sanitizes a given message by omitting the provided Github token if present. */
 | 
						|
  omitGithubTokenFromMessage(value: string): string {
 | 
						|
    return value.replace(this._tokenRegex, '<TOKEN>');
 | 
						|
  }
 | 
						|
}
 |