<commit_before>import * as Path from 'path'
import { Account } from '../../models/account'
import { assertNever } from '../fatal-error'
import * as GitPerf from '../../ui/lib/git-perf'

import {
  GitProcess,
  IGitResult as DugiteResult,
  GitError as DugiteError,
  IGitExecutionOptions as DugiteExecutionOptions,
} from 'dugite'

/**
 * An extension of the execution options in dugite that
 * allows us to piggy-back our own configuration options in the
 * same object.
 */
export interface IGitExecutionOptions extends DugiteExecutionOptions {
  /**
   * The exit codes which indicate success to the
   * caller. Unexpected exit codes will be logged and an
   * error thrown. Defaults to 0 if undefined.
   */
  readonly successExitCodes?: Set<number>

  /**
   * The git errors which are expected by the caller. Unexpected errors will
   * be logged and an error thrown.
   */
  readonly expectedErrors?: Set<DugiteError>
}

/**
 * The result of using `git`. This wraps dugite's results to provide
 * the parsed error if one occurs.
 */
export interface IGitResult extends DugiteResult {
  /**
   * The parsed git error. This will be null when the exit code is include in
   * the `successExitCodes`, or when dugite was unable to parse the
   * error.
   */
  readonly gitError: DugiteError | null

  /** The human-readable error description, based on `gitError`. */
  readonly gitErrorDescription: string | null
}

function getResultMessage(result: IGitResult) {
  const description = result.gitErrorDescription
  if (description) {
    return description
  }

  if (result.stderr.length) {
    return result.stderr
  } else if (result.stdout.length) {
    return result.stdout
  } else {
    return 'Unknown error'
  }
}

export class GitError extends Error {
  /** The result from the failed command. */
  public readonly result: IGitResult

  /** The args for the failed command. */
  public readonly args: ReadonlyArray<string>

  public constructor(result: IGitResult, args: ReadonlyArray<string>) {
    super(getResultMessage(result))

    this.name = 'GitError'
    this.result = result
    this.args = args
  }
}

/**
 * Shell out to git with the given arguments, at the given path.
 *
 * @param {args}             The arguments to pass to `git`.
 *
 * @param {path}             The working directory path for the execution of the
 *                           command.
 *
 * @param {name}             The name for the command based on its caller's
 *                           context. This will be used for performance
 *                           measurements and debugging.
 *
 * @param {options}          Configuration options for the execution of git,
 *                           see IGitExecutionOptions for more information.
 *
 * Returns the result. If the command exits with a code not in
 * `successExitCodes` or an error not in `expectedErrors`, a `GitError` will be
 * thrown.
 */
export async function git(args: string[], path: string, name: string, options?: IGitExecutionOptions): Promise<IGitResult> {

  const defaultOptions: IGitExecutionOptions = {
    successExitCodes: new Set([ 0 ]),
    expectedErrors: new Set(),
  }

  const opts = { ...defaultOptions, ...options }

  const startTime = (performance && performance.now) ? performance.now() : null

  const commandName = `${name}: git ${args.join(' ')}`

  const result = await GitPerf.measure(commandName, () => GitProcess.exec(args, path, options))

  if (console.debug && startTime) {
    const rawTime = performance.now() - startTime
    if (rawTime > 100) {
     const timeInSeconds = (rawTime / 1000).toFixed(3)
     console.debug(`executing: ${commandName} (took ${timeInSeconds}s)`)
    }
  }

  const exitCode = result.exitCode

  let gitError: DugiteError | null = null
  const acceptableExitCode = opts.successExitCodes ? opts.successExitCodes.has(exitCode) : false
  if (!acceptableExitCode) {
    gitError = GitProcess.parseError(result.stderr)
    if (!gitError) {
      gitError = GitProcess.parseError(result.stdout)
    }
  }

  const gitErrorDescription = gitError ? getDescriptionForError(gitError) : null
  const gitResult = { ...result, gitError, gitErrorDescription }

  let acceptableError = true
  if (gitError && opts.expectedErrors) {
    acceptableError = opts.expectedErrors.has(gitError)
  }

  if ((gitError && acceptableError) || acceptableExitCode) {
    return gitResult
  }

  console.error(`The command \`git ${args.join(' ')}\` exited with an unexpected code: ${exitCode}. The caller should either handle this error, or expect that exit code.`)
  if (result.stdout.length) {
    console.error(result.stdout)
  }

  if (result.stderr.length) {
    console.error(result.stderr)
  }

  if (gitError) {
    console.error(`(The error was parsed as ${gitError}: ${gitErrorDescription})`)
  }

  throw new GitError(gitResult, args)
}

function getDescriptionForError(error: DugiteError): string {
  switch (error) {
    case DugiteError.SSHKeyAuditUnverified: return 'The SSH key is unverified.'
    case DugiteError.SSHAuthenticationFailed:
    case DugiteError.SSHPermissionDenied:
    case DugiteError.HTTPSAuthenticationFailed: return `Authentication failed. You may not have permission to access the repository. Open ${__DARWIN__ ? 'preferences' : 'options'} and verify that you're signed in with an account that has permission to access this repository.`
    case DugiteError.RemoteDisconnection: return 'The remote disconnected. Check your Internet connection and try again.'
    case DugiteError.HostDown: return 'The host is down. Check your Internet connection and try again.'
    case DugiteError.RebaseConflicts: return 'We found some conflicts while trying to rebase. Please resolve the conflicts before continuing.'
    case DugiteError.MergeConflicts: return 'We found some conflicts while trying to merge. Please resolve the conflicts and commit the changes.'
    case DugiteError.HTTPSRepositoryNotFound:
    case DugiteError.SSHRepositoryNotFound: return 'The repository does not seem to exist anymore. You may not have access, or it may have been deleted or renamed.'
    case DugiteError.PushNotFastForward: return 'The repository has been updated since you last pulled. Try pulling before pushing.'<commit_msg>Log git exection and errors to disk<commit_after>import * as Path from 'path'
import { Account } from '../../models/account'
import { assertNever } from '../fatal-error'
import * as GitPerf from '../../ui/lib/git-perf'
import { logDebug, logError } from '../../lib/logging/renderer'

import {
  GitProcess,
  IGitResult as DugiteResult,
  GitError as DugiteError,
  IGitExecutionOptions as DugiteExecutionOptions,
} from 'dugite'

/**
 * An extension of the execution options in dugite that
 * allows us to piggy-back our own configuration options in the
 * same object.
 */
export interface IGitExecutionOptions extends DugiteExecutionOptions {
  /**
   * The exit codes which indicate success to the
   * caller. Unexpected exit codes will be logged and an
   * error thrown. Defaults to 0 if undefined.
   */
  readonly successExitCodes?: Set<number>

  /**
   * The git errors which are expected by the caller. Unexpected errors will
   * be logged and an error thrown.
   */
  readonly expectedErrors?: Set<DugiteError>
}

/**
 * The result of using `git`. This wraps dugite's results to provide
 * the parsed error if one occurs.
 */
export interface IGitResult extends DugiteResult {
  /**
   * The parsed git error. This will be null when the exit code is include in
   * the `successExitCodes`, or when dugite was unable to parse the
   * error.
   */
  readonly gitError: DugiteError | null

  /** The human-readable error description, based on `gitError`. */
  readonly gitErrorDescription: string | null
}

function getResultMessage(result: IGitResult) {
  const description = result.gitErrorDescription
  if (description) {
    return description
  }

  if (result.stderr.length) {
    return result.stderr
  } else if (result.stdout.length) {
    return result.stdout
  } else {
    return 'Unknown error'
  }
}

export class GitError extends Error {
  /** The result from the failed command. */
  public readonly result: IGitResult

  /** The args for the failed command. */
  public readonly args: ReadonlyArray<string>

  public constructor(result: IGitResult, args: ReadonlyArray<string>) {
    super(getResultMessage(result))

    this.name = 'GitError'
    this.result = result
    this.args = args
  }
}

/**
 * Shell out to git with the given arguments, at the given path.
 *
 * @param {args}             The arguments to pass to `git`.
 *
 * @param {path}             The working directory path for the execution of the
 *                           command.
 *
 * @param {name}             The name for the command based on its caller's
 *                           context. This will be used for performance
 *                           measurements and debugging.
 *
 * @param {options}          Configuration options for the execution of git,
 *                           see IGitExecutionOptions for more information.
 *
 * Returns the result. If the command exits with a code not in
 * `successExitCodes` or an error not in `expectedErrors`, a `GitError` will be
 * thrown.
 */
export async function git(args: string[], path: string, name: string, options?: IGitExecutionOptions): Promise<IGitResult> {

  const defaultOptions: IGitExecutionOptions = {
    successExitCodes: new Set([ 0 ]),
    expectedErrors: new Set(),
  }

  const opts = { ...defaultOptions, ...options }

  const startTime = (performance && performance.now) ? performance.now() : null

  const commandName = `${name}: git ${args.join(' ')}`
  logDebug(`Executing ${commandName}`)

  const result = await GitPerf.measure(commandName, () => GitProcess.exec(args, path, options))

  if (console.debug && startTime) {
    const rawTime = performance.now() - startTime
    if (rawTime > 100) {
     const timeInSeconds = (rawTime / 1000).toFixed(3)
     console.debug(`executing: ${commandName} (took ${timeInSeconds}s)`)
    }
  }

  const exitCode = result.exitCode

  let gitError: DugiteError | null = null
  const acceptableExitCode = opts.successExitCodes ? opts.successExitCodes.has(exitCode) : false
  if (!acceptableExitCode) {
    gitError = GitProcess.parseError(result.stderr)
    if (!gitError) {
      gitError = GitProcess.parseError(result.stdout)
    }
  }

  const gitErrorDescription = gitError ? getDescriptionForError(gitError) : null
  const gitResult = { ...result, gitError, gitErrorDescription }

  let acceptableError = true
  if (gitError && opts.expectedErrors) {
    acceptableError = opts.expectedErrors.has(gitError)
  }

  if ((gitError && acceptableError) || acceptableExitCode) {
    return gitResult
  }

  // The caller should either handle this error, or expect that exit code.
  const errorMessage = []
  errorMessage.push(`\`git ${args.join(' ')}\` exited with an unexpected code: ${exitCode}.`)

  if (result.stdout) {
    errorMessage.push(result.stdout)
  }

  if (result.stderr) {
    errorMessage.push(result.stderr)
  }

  if (gitError) {
    errorMessage.push(`(The error was parsed as ${gitError}: ${gitErrorDescription})`)
  }

  logError(errorMessage.join('\n'))

  throw new GitError(gitResult, args)
}

function getDescriptionForError(error: DugiteError): string {
  switch (error) {
    case DugiteError.SSHKeyAuditUnverified: return 'The SSH key is unverified.'
    case DugiteError.SSHAuthenticationFailed:
    case DugiteError.SSHPermissionDenied:
    case DugiteError.HTTPSAuthenticationFailed: return `Authentication failed. You may not have permission to access the repository. Open ${__DARWIN__ ? 'preferences' : 'options'} and verify that you're signed in with an account that has permission to access this repository.`
    case DugiteError.RemoteDisconnection: return 'The remote disconnected. Check your Internet connection and try again.'
    case DugiteError.HostDown: return 'The host is down. Check your Internet connection and try again.'
    case DugiteError.RebaseConflicts: return 'We found some conflicts while trying to rebase. Please resolve the conflicts before continuing.'
    case DugiteError.MergeConflicts: return 'We found some conflicts while trying to merge. Please resolve the conflicts and commit the changes.'
    case DugiteError.HTTPSRepositoryNotFound:
    case DugiteError.SSHRepositoryNotFound: return 'The repository does not seem to exist anymore. You may not have access, or it may have been deleted or renamed.'
    case DugiteError.PushNotFastForward: return 'The repository has been updated since you last pulled. Try pulling before pushing.'
{'fraction_non_alphanumeric': 0.09296640908374074, 'fraction_numerical': 0.0015770383220312253, 'mean_word_length': 3.465845070422535, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4505602', 'n_tokens_mistral': 3539, 'n_tokens_neox': 3447, 'n_words': 1261}