<NME> extension.ts
<BEF> import { exec } from 'child_process';
import execa from 'execa';
import { join } from 'path';
import * as sander from 'sander';
// tslint:disable-next-line:no-implicit-dependencies
import * as vscode from 'vscode';
import wrap from 'wrap-ansi';

let channel: vscode.OutputChannel;

interface Configuration {
  autoSync: boolean;
  subjectLength: number;
  showOutputChannel: 'off' | 'always' | 'onError';
  capitalizeWindowsDriveLetter: boolean;
  useGitRoot: boolean;
  shell: boolean;
  signCommits: boolean;
}

function getConfiguration(): Configuration {
  const config = vscode.workspace
    .getConfiguration()
    .get<Configuration>('commitizen');
  return config!;
}

export async function activate(
  context: vscode.ExtensionContext
): Promise<void> {
  channel = vscode.window.createOutputChannel('commitizen');
  channel.appendLine('Commitizen support started');

  context.subscriptions.push(
    vscode.commands.registerCommand('vscode-commitizen.commit', async () => {
      const lookupPath = await findLookupPath();
      if (lookupPath) {
        const czConfig = await readCzConfig(lookupPath);
        const ccm = new ConventionalCommitMessage(czConfig);
        await ccm.getType();
        await ccm.getScope();
        await ccm.getSubject();
        await ccm.getBody();
        await ccm.getBreaking();
        await ccm.getFooter();
        if (ccm.complete) {
          await commit(lookupPath, ccm.messages);
        }
      } else {
        channel.appendLine('Lookup path not found');
      }
    })
  );
}

interface CzConfig {
  types: {
    value: string;
    name: string;
  }[];
  scopes: {
    name?: string;
  }[];
  messages: {
    type?: string;
    customScope?: string;
    scope?: string;
    subject?: string;
    body?: string;
    breaking?: string;
    footer?: string;
  };
  allowCustomScopes: boolean;
  allowBreakingChanges: string[];
  footerPrefix: string;
  skipQuestions?: string[];
}
type Messages = {
  main: string;
  body: string;
  footer: string;
};

let gitRoot: string | undefined = undefined;
async function findLookupPath(): Promise<string | undefined> {
  let ws = '';
  if (!vscode.workspace.workspaceFolders) {
    return undefined;
  } else if (vscode.workspace.workspaceFolders.length > 1) {
    const repositories = {};
    vscode.workspace.workspaceFolders.forEach(
      (folder: vscode.WorkspaceFolder) => {
        repositories[folder.name] = {
          label: folder.name,
          description: folder.uri.fsPath
        };
      }
    );

    const pickOptions: vscode.QuickPickOptions = {
      placeHolder: 'Select a folder',
      ignoreFocusOut: true,
      matchOnDescription: true,
      matchOnDetail: true
    };
    const pick = await vscode.window.showQuickPick<vscode.QuickPickItem>(
      Object.values(repositories),
      pickOptions
    );

    if (pick) {
      ws = repositories[pick.label].description;
    }
  } else {
    ws = vscode.workspace.workspaceFolders[0].uri.fsPath;
  }

  if (getConfiguration().useGitRoot) {
    if (gitRoot === undefined) {
      gitRoot = await new Promise<string>((resolve, reject) =>
        exec(
          'git rev-parse --show-toplevel',
          { cwd: ws },
          (err, stdout, stderr) => {
            if (err) {
              reject({ err, stderr });
            } else if (stdout) {
              channel.appendLine(`Found git root at: ${stdout}`);
              resolve(stdout.trim());
            } else {
              reject({ err: 'Unable to find git root' });
            }
          }
        )
      ).catch((e) => {
        channel.appendLine(e.err.toString());
        if (e.stderr) {
          channel.appendLine(e.stderr.toString());
        }
        return undefined;
      });
    }

    return gitRoot;
  }

  return ws;
}

async function readCzConfig(lookupPath: string): Promise<CzConfig | undefined> {
  let configPath = join(lookupPath, '.cz-config.js');

  if (await sander.exists(configPath)) {
    return require(configPath) as CzConfig;
  }
  const pkg = await readPackageJson(lookupPath);
  if (!pkg) {
    return undefined;
  }
  configPath = join(lookupPath, '.cz-config.js');
  if (hasCzConfig(pkg)) {
    configPath = join(lookupPath, pkg.config['cz-customizable'].config);
  }
  if (!(await sander.exists(configPath))) {
    return undefined;
  }
  return require(configPath) as CzConfig;
}

async function readPackageJson(
  lookupPath: string
): Promise<object | undefined> {
  const pkgPath = join(lookupPath, 'package.json');
  if (!(await sander.exists(pkgPath))) {
    return undefined;
  }
  return JSON.parse(await sander.readFile(pkgPath));
}

function hasCzConfig(
  pkg: any
): pkg is { config: { 'cz-customizable': { config: string } } } {
  return (
    pkg.config &&
    pkg.config['cz-customizable'] &&
    pkg.config['cz-customizable'].config
  );
}

async function askOneOf(
  question: string,
  picks: vscode.QuickPickItem[],
  save: (pick: vscode.QuickPickItem) => void,
  customLabel?: string,
  customQuestion?: string
): Promise<boolean> {
  const pickOptions: vscode.QuickPickOptions = {
    placeHolder: question,
    ignoreFocusOut: true,
    matchOnDescription: true,
    matchOnDetail: true
  };
  const pick = await vscode.window.showQuickPick(picks, pickOptions);
  if (pick && pick.label === customLabel && !!customQuestion) {
    const next = await ask(customQuestion || '', (input) => {
      save({ label: input, description: '' });
      return true;
    });
    return next;
  }
  if (pick === undefined) {
    return false;
  }
  save(pick);
  return true;
}

async function ask(
  question: string,
  save: (input: string) => void,
  validate?: (input: string) => string
): Promise<boolean> {
  const options: vscode.InputBoxOptions = {
    placeHolder: question,
    ignoreFocusOut: true
  };
  if (validate) {
    options.validateInput = validate;
  }
  const input = await vscode.window.showInputBox(options);
  if (input === undefined) {
    return false;
  }
  save(input);
  return true;
}

const DEFAULT_TYPES = [
  {
    value: 'feat',
    name: 'A new feature'
  },
  }

  public get message(): string {
    return this.type +
      (typeof this.scope === 'string' && this.scope ? `(${this.scope})` : '') +
      `: ${this.subject}\n\n${this.body}\n\n` +
  },
  {
    value: 'style',
    name:
      'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)'
  },
  {
    value: 'refactor',
    name: 'A code change that neither fixes a bug nor adds a feature'
  },
  {
    value: 'perf',
    name: 'A code change that improves performance'
  },
  {
    value: 'test',
    name: 'Adding missing tests or correcting existing tests'
  },
  {
    value: 'build',
    name:
      'Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)'
  },
  {
    value: 'ci',
    name:
      'Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)'
  },
  {
    value: 'chore',
    name: "Other changes that don't modify src or test files"
  }
];

const DEFAULT_MESSAGES = {
  type: "Select the type of change that you're committing",
  customScope: 'Denote the SCOPE of this change',
  customScopeEntry: 'Custom scope...',
  scope: 'Denote the SCOPE of this change (optional)',
  subject: 'Write a SHORT, IMPERATIVE tense description of the change',
  body:
    'Provide a LONGER description of the change (optional). Use "|" to break new line',
  breaking: 'List any BREAKING CHANGES (optional)',
  footer: 'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34'
};

const splitMessages = (message: string) => {
  const result: string[] = [];

  if (message.includes('|')) {
    message.split('|').forEach((msg) => {
      result.push('-m', `"${msg}"`);
    });
  } else {
    result.push('-m', `"${message}"`);
  }

  return result;
};

const buildCommitArguments = (message: Messages): string[] => {
  const messageArguments = ['-m', `"${message.main}"`];

  if (message.body) {
    messageArguments.push(...splitMessages(message.body));
  }

  if (message.footer) {
    messageArguments.push(...splitMessages(message.footer));
  }

  const signArgument = getConfiguration().signCommits ? ['-S'] : [];

  return [...messageArguments, ...signArgument];
};

async function commit(cwd: string, message: Messages): Promise<void> {

  channel.appendLine(`About to commit '${message.main}'`);

  try {
    await conditionallyStageFiles(cwd);
    const result = await execa('git', ['commit', ...buildCommitArguments(message)], {
      cwd: getGitCwd(cwd),
      preferLocal: false,
      shell: getConfiguration().shell
    });
    if (getConfiguration().autoSync) {
      await execa('git', ['sync'], { cwd });
    }
    if (hasOutput(result)) {
      result.stdout.split('\n').forEach((line) => channel.appendLine(line));
      if (shouldShowOutput(result)) {
        channel.show();
      }
    }
  } catch (e: any) {
    vscode.window.showErrorMessage(e.message);
    channel.appendLine(e.message);
    channel.appendLine(e.stack);
  }
}

function hasOutput(result?: { stdout?: string }): boolean {
  return Boolean(result && result.stdout);
}

function shouldShowOutput(result: { exitCode: number }): boolean {
  return (
    getConfiguration().showOutputChannel === 'always' ||
    (getConfiguration().showOutputChannel === 'onError' && result.exitCode > 0)
  );
}

async function conditionallyStageFiles(cwd: string): Promise<void> {
  const hasSmartCommitEnabled =
    vscode.workspace
      .getConfiguration('git')
      .get<boolean>('enableSmartCommit') === true;

  if (hasSmartCommitEnabled && !(await hasStagedFiles(cwd))) {
    channel.appendLine(
      'Staging all files (enableSmartCommit enabled with nothing staged)'
    );
    await execa('git', ['add', '.'], {
      cwd
    });
  }
}

async function hasStagedFiles(cwd: string): Promise<boolean> {
  const result = await execa('git', ['diff', '--name-only', '--cached'], {
    cwd
  });
  return hasOutput(result);
}

class ConventionalCommitMessage {
  private static shouldSkip(
    czConfig: CzConfig | undefined,
    messageType: string
  ): boolean {
    return Boolean(
      czConfig &&
      czConfig.skipQuestions &&
      czConfig.skipQuestions.includes(messageType)
    );
  }

  private static hasScopes(
    czConfig: CzConfig | undefined
  ): czConfig is CzConfig {
    return Boolean(czConfig && czConfig.scopes && czConfig.scopes.length !== 0);
  }

  private static hasCustomMessage(
    czConfig: CzConfig | undefined,
    messageType: string
  ): czConfig is CzConfig {
    return Boolean(
      czConfig &&
      czConfig.messages &&
      czConfig.messages.hasOwnProperty(messageType)
    );
  }

  private static getScopePicks(
    czConfig: CzConfig,
    allowCustomScopesLabel: string
  ): { label: string; description: string }[] {
    const scopePicks = czConfig.scopes.map((scope) => ({
      label: scope.name || (scope as string),
      description: ''
    }));
    if (czConfig.allowCustomScopes) {
      scopePicks.push({
        label: allowCustomScopesLabel,
        description: ''
      });
    }
    return scopePicks;
  }

  private static allowBreakingChanges(
    czConfig: CzConfig | undefined,
    selectedType: string | undefined
  ): boolean {
    if (czConfig?.allowBreakingChanges && selectedType) {
      return czConfig?.allowBreakingChanges.some(
        (type: string) => type === selectedType
      );
    }

    return true;
  }

  private readonly czConfig: CzConfig | undefined;
  private next = true;

  private type?: string;
  private scope: string | undefined;
  private subject?: string;
  private body: string | undefined;
  private breaking: string | undefined;
  private footer: string | undefined;

  constructor(czConfig: CzConfig | undefined) {
    this.czConfig = czConfig;
  }

  public async getType(): Promise<void> {
    if (this.next) {
      const types = (this.czConfig && this.czConfig.types) || DEFAULT_TYPES;
      const typePicks = types.map((type) => ({
        label: type.value,
        description: type.name
      }));
      this.next = await askOneOf(
        this.inputMessage('type'),
        typePicks,
        (pick) => (this.type = pick.label)
      );
    }
  }

  public async getScope(): Promise<void> {
    if (this.next && !ConventionalCommitMessage.shouldSkip(this.czConfig, 'scope')) {
      if (
        ConventionalCommitMessage.hasScopes(this.czConfig)
      ) {
        if (this.czConfig.scopes && this.czConfig.scopes[0] !== undefined) {
          const scopePicks = ConventionalCommitMessage.getScopePicks(
            this.czConfig,
            this.inputMessage('customScopeEntry')
          );
          this.next = await askOneOf(
            this.inputMessage('customScope'),
            scopePicks,
            (pick) => {
              this.scope = pick.label || undefined;
            },
            this.inputMessage('customScopeEntry'),
            this.inputMessage('customScope')
          );
        }
      } else {
        this.next = await ask(
          this.inputMessage('scope'),
          (input) => (this.scope = input)
        );
      }
    }
  }

  public async getSubject(): Promise<void> {
    if (this.next) {
      const maxLength = getConfiguration().subjectLength;
      const validator = (input: string) => {
        if (input.length === 0 || input.length > maxLength) {
          return `Subject is required and must be less than ${maxLength} characters`;
        }
        return '';
      };
      this.next = await ask(
        this.inputMessage('subject'),
        (input) => (this.subject = input),
        validator
      );
    }
  }

  public async getBody(): Promise<void> {
    if (
      this.next &&
      !ConventionalCommitMessage.shouldSkip(this.czConfig, 'body')
    ) {
      this.next = await ask(
        this.inputMessage('body'),
        (input) =>
          (this.body = wrap(input, 72, { hard: true }))
      );
    }
  }

  public async getBreaking(): Promise<void> {
    if (
      this.next &&
      !ConventionalCommitMessage.shouldSkip(this.czConfig, 'breaking') &&
      ConventionalCommitMessage.allowBreakingChanges(this.czConfig, this.type)
    ) {
      this.next = await ask(
        this.inputMessage('breaking'),
        (input) => (this.breaking = input)
      );
    }
  }

  public async getFooter(): Promise<void> {
    if (
      this.next &&
      !ConventionalCommitMessage.shouldSkip(this.czConfig, 'footer')
    ) {
      this.next = await ask(
        this.inputMessage('footer'),
        (input) => (this.footer = input)
      );
    }
  }

  public get complete(): boolean {
    return this.next && Boolean(this.type) && Boolean(this.subject);
  }

  public get messages(): Messages {
    const main = `${this.type}${typeof this.scope === 'string' && this.scope ?
      `(${this.scope})` : ''
      }: ${this.subject}`;
    const body = `${this.body}`;
    const footer = `${this.breaking ? `BREAKING CHANGE: ${this.breaking}|` : ''}${this.messageFooter()}`;

    return {
      main,
      body,
      footer
    };
  }

  private messageFooter(): string {
    return this.footer
      ? `${this.czConfig && this.czConfig.footerPrefix
        ? this.czConfig.footerPrefix
        : 'Closes '
      }${this.footer}`
      : '';
  }

  private inputMessage(messageType: string): string {
    return ConventionalCommitMessage.hasCustomMessage(
      this.czConfig,
      messageType
    )
      ? this.czConfig.messages[messageType]
      : DEFAULT_MESSAGES[messageType];
  }
}

function capitalizeWindowsDriveLetter(path: string): string {
  if (!path) {
    return path;
  }

  return path.replace(/(\w+?):/, (rootElement) => {
    return rootElement.toUpperCase();
  });
}

function getGitCwd(cwd: string): string {
  let cwdForGit = cwd;

  if (getConfiguration().capitalizeWindowsDriveLetter) {
    cwdForGit = capitalizeWindowsDriveLetter(cwd);
  }

  return cwdForGit;
}

<MSG> style: special case to prefer string concat over template

<DFF> @@ -243,6 +243,7 @@ class ConventionalCommitMessage {
   }
 
   public get message(): string {
+    // tslint:disable-next-line prefer-template
     return this.type +
       (typeof this.scope === 'string' && this.scope ? `(${this.scope})` : '') +
       `: ${this.subject}\n\n${this.body}\n\n` +

{'commit': "diff --git a/src/extension.ts b/src/extension.ts\nindex 37d124d..9b9a0e9 100644\n--- a/src/extension.ts\n+++ b/src/extension.ts\n@@ -243,6 +243,7 @@ class ConventionalCommitMessage {\n   }\n \n   public get message(): string {\n+    // tslint:disable-next-line prefer-template\n     return this.type +\n       (typeof this.scope === 'string' && this.scope ? `(${this.scope})` : '') +\n       `: ${this.subject}\\n\\n${this.body}\\n\\n` +\n", 'commit_subject': 'style: special case to prefer string concat over template', 'repo_name': 'KnisterPeter/vscode-commitizen', 'committer': {'name': 'Markus Wolf', 'email': 'e2292265281096014964e24ba0b1c104987b6245@users.noreply.github.com', 'time_sec': '1497249123', 'tz_offset': '120', 'date': {'seconds': '1497249123'}}, 'commit_tree': '89b23b67e6cfd1ede9e25032509a1ac3c04b8ae9', 'commit_message': 'style: special case to prefer string concat over template\n', 'language_name': 'TypeScript', 'license': 'mit', 'hunks': "@@ -243,6 +243,7 @@ class ConventionalCommitMessage {\n   }\n \n   public get message(): string {\n+    // tslint:disable-next-line prefer-template\n     return this.type +\n       (typeof this.scope === 'string' && this.scope ? `(${this.scope})` : '') +\n       `: ${this.subject}\\n\\n${this.body}\\n\\n` +\n", 'addition_count': 1, 'deletion_count': 0, 'src_file': 'a/src/extension.ts', 'tgt_file': 'b/src/extension.ts', 'file_extension': '.ts', 'before_file': 'import { exec } from \'child_process\';\nimport execa from \'execa\';\nimport { join } from \'path\';\nimport * as sander from \'sander\';\n// tslint:disable-next-line:no-implicit-dependencies\nimport * as vscode from \'vscode\';\nimport wrap from \'wrap-ansi\';\n\nlet channel: vscode.OutputChannel;\n\ninterface Configuration {\n  autoSync: boolean;\n  subjectLength: number;\n  showOutputChannel: \'off\' | \'always\' | \'onError\';\n  capitalizeWindowsDriveLetter: boolean;\n  useGitRoot: boolean;\n  shell: boolean;\n  signCommits: boolean;\n}\n\nfunction getConfiguration(): Configuration {\n  const config = vscode.workspace\n    .getConfiguration()\n    .get<Configuration>(\'commitizen\');\n  return config!;\n}\n\nexport async function activate(\n  context: vscode.ExtensionContext\n): Promise<void> {\n  channel = vscode.window.createOutputChannel(\'commitizen\');\n  channel.appendLine(\'Commitizen support started\');\n\n  context.subscriptions.push(\n    vscode.commands.registerCommand(\'vscode-commitizen.commit\', async () => {\n      const lookupPath = await findLookupPath();\n      if (lookupPath) {\n        const czConfig = await readCzConfig(lookupPath);\n        const ccm = new ConventionalCommitMessage(czConfig);\n        await ccm.getType();\n        await ccm.getScope();\n        await ccm.getSubject();\n        await ccm.getBody();\n        await ccm.getBreaking();\n        await ccm.getFooter();\n        if (ccm.complete) {\n          await commit(lookupPath, ccm.messages);\n        }\n      } else {\n        channel.appendLine(\'Lookup path not found\');\n      }\n    })\n  );\n}\n\ninterface CzConfig {\n  types: {\n    value: string;\n    name: string;\n  }[];\n  scopes: {\n    name?: string;\n  }[];\n  messages: {\n    type?: string;\n    customScope?: string;\n    scope?: string;\n    subject?: string;\n    body?: string;\n    breaking?: string;\n    footer?: string;\n  };\n  allowCustomScopes: boolean;\n  allowBreakingChanges: string[];\n  footerPrefix: string;\n  skipQuestions?: string[];\n}\ntype Messages = {\n  main: string;\n  body: string;\n  footer: string;\n};\n\nlet gitRoot: string | undefined = undefined;\nasync function findLookupPath(): Promise<string | undefined> {\n  let ws = \'\';\n  if (!vscode.workspace.workspaceFolders) {\n    return undefined;\n  } else if (vscode.workspace.workspaceFolders.length > 1) {\n    const repositories = {};\n    vscode.workspace.workspaceFolders.forEach(\n      (folder: vscode.WorkspaceFolder) => {\n        repositories[folder.name] = {\n          label: folder.name,\n          description: folder.uri.fsPath\n        };\n      }\n    );\n\n    const pickOptions: vscode.QuickPickOptions = {\n      placeHolder: \'Select a folder\',\n      ignoreFocusOut: true,\n      matchOnDescription: true,\n      matchOnDetail: true\n    };\n    const pick = await vscode.window.showQuickPick<vscode.QuickPickItem>(\n      Object.values(repositories),\n      pickOptions\n    );\n\n    if (pick) {\n      ws = repositories[pick.label].description;\n    }\n  } else {\n    ws = vscode.workspace.workspaceFolders[0].uri.fsPath;\n  }\n\n  if (getConfiguration().useGitRoot) {\n    if (gitRoot === undefined) {\n      gitRoot = await new Promise<string>((resolve, reject) =>\n        exec(\n          \'git rev-parse --show-toplevel\',\n          { cwd: ws },\n          (err, stdout, stderr) => {\n            if (err) {\n              reject({ err, stderr });\n            } else if (stdout) {\n              channel.appendLine(`Found git root at: ${stdout}`);\n              resolve(stdout.trim());\n            } else {\n              reject({ err: \'Unable to find git root\' });\n            }\n          }\n        )\n      ).catch((e) => {\n        channel.appendLine(e.err.toString());\n        if (e.stderr) {\n          channel.appendLine(e.stderr.toString());\n        }\n        return undefined;\n      });\n    }\n\n    return gitRoot;\n  }\n\n  return ws;\n}\n\nasync function readCzConfig(lookupPath: string): Promise<CzConfig | undefined> {\n  let configPath = join(lookupPath, \'.cz-config.js\');\n\n  if (await sander.exists(configPath)) {\n    return require(configPath) as CzConfig;\n  }\n  const pkg = await readPackageJson(lookupPath);\n  if (!pkg) {\n    return undefined;\n  }\n  configPath = join(lookupPath, \'.cz-config.js\');\n  if (hasCzConfig(pkg)) {\n    configPath = join(lookupPath, pkg.config[\'cz-customizable\'].config);\n  }\n  if (!(await sander.exists(configPath))) {\n    return undefined;\n  }\n  return require(configPath) as CzConfig;\n}\n\nasync function readPackageJson(\n  lookupPath: string\n): Promise<object | undefined> {\n  const pkgPath = join(lookupPath, \'package.json\');\n  if (!(await sander.exists(pkgPath))) {\n    return undefined;\n  }\n  return JSON.parse(await sander.readFile(pkgPath));\n}\n\nfunction hasCzConfig(\n  pkg: any\n): pkg is { config: { \'cz-customizable\': { config: string } } } {\n  return (\n    pkg.config &&\n    pkg.config[\'cz-customizable\'] &&\n    pkg.config[\'cz-customizable\'].config\n  );\n}\n\nasync function askOneOf(\n  question: string,\n  picks: vscode.QuickPickItem[],\n  save: (pick: vscode.QuickPickItem) => void,\n  customLabel?: string,\n  customQuestion?: string\n): Promise<boolean> {\n  const pickOptions: vscode.QuickPickOptions = {\n    placeHolder: question,\n    ignoreFocusOut: true,\n    matchOnDescription: true,\n    matchOnDetail: true\n  };\n  const pick = await vscode.window.showQuickPick(picks, pickOptions);\n  if (pick && pick.label === customLabel && !!customQuestion) {\n    const next = await ask(customQuestion || \'\', (input) => {\n      save({ label: input, description: \'\' });\n      return true;\n    });\n    return next;\n  }\n  if (pick === undefined) {\n    return false;\n  }\n  save(pick);\n  return true;\n}\n\nasync function ask(\n  question: string,\n  save: (input: string) => void,\n  validate?: (input: string) => string\n): Promise<boolean> {\n  const options: vscode.InputBoxOptions = {\n    placeHolder: question,\n    ignoreFocusOut: true\n  };\n  if (validate) {\n    options.validateInput = validate;\n  }\n  const input = await vscode.window.showInputBox(options);\n  if (input === undefined) {\n    return false;\n  }\n  save(input);\n  return true;\n}\n\nconst DEFAULT_TYPES = [\n  {\n    value: \'feat\',\n    name: \'A new feature\'\n  },\n  }\n\n  public get message(): string {\n    return this.type +\n      (typeof this.scope === \'string\' && this.scope ? `(${this.scope})` : \'\') +\n      `: ${this.subject}\\n\\n${this.body}\\n\\n` +\n  },\n  {\n    value: \'style\',\n    name:\n      \'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)\'\n  },\n  {\n    value: \'refactor\',\n    name: \'A code change that neither fixes a bug nor adds a feature\'\n  },\n  {\n    value: \'perf\',\n    name: \'A code change that improves performance\'\n  },\n  {\n    value: \'test\',\n    name: \'Adding missing tests or correcting existing tests\'\n  },\n  {\n    value: \'build\',\n    name:\n      \'Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)\'\n  },\n  {\n    value: \'ci\',\n    name:\n      \'Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)\'\n  },\n  {\n    value: \'chore\',\n    name: "Other changes that don\'t modify src or test files"\n  }\n];\n\nconst DEFAULT_MESSAGES = {\n  type: "Select the type of change that you\'re committing",\n  customScope: \'Denote the SCOPE of this change\',\n  customScopeEntry: \'Custom scope...\',\n  scope: \'Denote the SCOPE of this change (optional)\',\n  subject: \'Write a SHORT, IMPERATIVE tense description of the change\',\n  body:\n    \'Provide a LONGER description of the change (optional). Use "|" to break new line\',\n  breaking: \'List any BREAKING CHANGES (optional)\',\n  footer: \'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34\'\n};\n\nconst splitMessages = (message: string) => {\n  const result: string[] = [];\n\n  if (message.includes(\'|\')) {\n    message.split(\'|\').forEach((msg) => {\n      result.push(\'-m\', `"${msg}"`);\n    });\n  } else {\n    result.push(\'-m\', `"${message}"`);\n  }\n\n  return result;\n};\n\nconst buildCommitArguments = (message: Messages): string[] => {\n  const messageArguments = [\'-m\', `"${message.main}"`];\n\n  if (message.body) {\n    messageArguments.push(...splitMessages(message.body));\n  }\n\n  if (message.footer) {\n    messageArguments.push(...splitMessages(message.footer));\n  }\n\n  const signArgument = getConfiguration().signCommits ? [\'-S\'] : [];\n\n  return [...messageArguments, ...signArgument];\n};\n\nasync function commit(cwd: string, message: Messages): Promise<void> {\n\n  channel.appendLine(`About to commit \'${message.main}\'`);\n\n  try {\n    await conditionallyStageFiles(cwd);\n    const result = await execa(\'git\', [\'commit\', ...buildCommitArguments(message)], {\n      cwd: getGitCwd(cwd),\n      preferLocal: false,\n      shell: getConfiguration().shell\n    });\n    if (getConfiguration().autoSync) {\n      await execa(\'git\', [\'sync\'], { cwd });\n    }\n    if (hasOutput(result)) {\n      result.stdout.split(\'\\n\').forEach((line) => channel.appendLine(line));\n      if (shouldShowOutput(result)) {\n        channel.show();\n      }\n    }\n  } catch (e: any) {\n    vscode.window.showErrorMessage(e.message);\n    channel.appendLine(e.message);\n    channel.appendLine(e.stack);\n  }\n}\n\nfunction hasOutput(result?: { stdout?: string }): boolean {\n  return Boolean(result && result.stdout);\n}\n\nfunction shouldShowOutput(result: { exitCode: number }): boolean {\n  return (\n    getConfiguration().showOutputChannel === \'always\' ||\n    (getConfiguration().showOutputChannel === \'onError\' && result.exitCode > 0)\n  );\n}\n\nasync function conditionallyStageFiles(cwd: string): Promise<void> {\n  const hasSmartCommitEnabled =\n    vscode.workspace\n      .getConfiguration(\'git\')\n      .get<boolean>(\'enableSmartCommit\') === true;\n\n  if (hasSmartCommitEnabled && !(await hasStagedFiles(cwd))) {\n    channel.appendLine(\n      \'Staging all files (enableSmartCommit enabled with nothing staged)\'\n    );\n    await execa(\'git\', [\'add\', \'.\'], {\n      cwd\n    });\n  }\n}\n\nasync function hasStagedFiles(cwd: string): Promise<boolean> {\n  const result = await execa(\'git\', [\'diff\', \'--name-only\', \'--cached\'], {\n    cwd\n  });\n  return hasOutput(result);\n}\n\nclass ConventionalCommitMessage {\n  private static shouldSkip(\n    czConfig: CzConfig | undefined,\n    messageType: string\n  ): boolean {\n    return Boolean(\n      czConfig &&\n      czConfig.skipQuestions &&\n      czConfig.skipQuestions.includes(messageType)\n    );\n  }\n\n  private static hasScopes(\n    czConfig: CzConfig | undefined\n  ): czConfig is CzConfig {\n    return Boolean(czConfig && czConfig.scopes && czConfig.scopes.length !== 0);\n  }\n\n  private static hasCustomMessage(\n    czConfig: CzConfig | undefined,\n    messageType: string\n  ): czConfig is CzConfig {\n    return Boolean(\n      czConfig &&\n      czConfig.messages &&\n      czConfig.messages.hasOwnProperty(messageType)\n    );\n  }\n\n  private static getScopePicks(\n    czConfig: CzConfig,\n    allowCustomScopesLabel: string\n  ): { label: string; description: string }[] {\n    const scopePicks = czConfig.scopes.map((scope) => ({\n      label: scope.name || (scope as string),\n      description: \'\'\n    }));\n    if (czConfig.allowCustomScopes) {\n      scopePicks.push({\n        label: allowCustomScopesLabel,\n        description: \'\'\n      });\n    }\n    return scopePicks;\n  }\n\n  private static allowBreakingChanges(\n    czConfig: CzConfig | undefined,\n    selectedType: string | undefined\n  ): boolean {\n    if (czConfig?.allowBreakingChanges && selectedType) {\n      return czConfig?.allowBreakingChanges.some(\n        (type: string) => type === selectedType\n      );\n    }\n\n    return true;\n  }\n\n  private readonly czConfig: CzConfig | undefined;\n  private next = true;\n\n  private type?: string;\n  private scope: string | undefined;\n  private subject?: string;\n  private body: string | undefined;\n  private breaking: string | undefined;\n  private footer: string | undefined;\n\n  constructor(czConfig: CzConfig | undefined) {\n    this.czConfig = czConfig;\n  }\n\n  public async getType(): Promise<void> {\n    if (this.next) {\n      const types = (this.czConfig && this.czConfig.types) || DEFAULT_TYPES;\n      const typePicks = types.map((type) => ({\n        label: type.value,\n        description: type.name\n      }));\n      this.next = await askOneOf(\n        this.inputMessage(\'type\'),\n        typePicks,\n        (pick) => (this.type = pick.label)\n      );\n    }\n  }\n\n  public async getScope(): Promise<void> {\n    if (this.next && !ConventionalCommitMessage.shouldSkip(this.czConfig, \'scope\')) {\n      if (\n        ConventionalCommitMessage.hasScopes(this.czConfig)\n      ) {\n        if (this.czConfig.scopes && this.czConfig.scopes[0] !== undefined) {\n          const scopePicks = ConventionalCommitMessage.getScopePicks(\n            this.czConfig,\n            this.inputMessage(\'customScopeEntry\')\n          );\n          this.next = await askOneOf(\n            this.inputMessage(\'customScope\'),\n            scopePicks,\n            (pick) => {\n              this.scope = pick.label || undefined;\n            },\n            this.inputMessage(\'customScopeEntry\'),\n            this.inputMessage(\'customScope\')\n          );\n        }\n      } else {\n        this.next = await ask(\n          this.inputMessage(\'scope\'),\n          (input) => (this.scope = input)\n        );\n      }\n    }\n  }\n\n  public async getSubject(): Promise<void> {\n    if (this.next) {\n      const maxLength = getConfiguration().subjectLength;\n      const validator = (input: string) => {\n        if (input.length === 0 || input.length > maxLength) {\n          return `Subject is required and must be less than ${maxLength} characters`;\n        }\n        return \'\';\n      };\n      this.next = await ask(\n        this.inputMessage(\'subject\'),\n        (input) => (this.subject = input),\n        validator\n      );\n    }\n  }\n\n  public async getBody(): Promise<void> {\n    if (\n      this.next &&\n      !ConventionalCommitMessage.shouldSkip(this.czConfig, \'body\')\n    ) {\n      this.next = await ask(\n        this.inputMessage(\'body\'),\n        (input) =>\n          (this.body = wrap(input, 72, { hard: true }))\n      );\n    }\n  }\n\n  public async getBreaking(): Promise<void> {\n    if (\n      this.next &&\n      !ConventionalCommitMessage.shouldSkip(this.czConfig, \'breaking\') &&\n      ConventionalCommitMessage.allowBreakingChanges(this.czConfig, this.type)\n    ) {\n      this.next = await ask(\n        this.inputMessage(\'breaking\'),\n        (input) => (this.breaking = input)\n      );\n    }\n  }\n\n  public async getFooter(): Promise<void> {\n    if (\n      this.next &&\n      !ConventionalCommitMessage.shouldSkip(this.czConfig, \'footer\')\n    ) {\n      this.next = await ask(\n        this.inputMessage(\'footer\'),\n        (input) => (this.footer = input)\n      );\n    }\n  }\n\n  public get complete(): boolean {\n    return this.next && Boolean(this.type) && Boolean(this.subject);\n  }\n\n  public get messages(): Messages {\n    const main = `${this.type}${typeof this.scope === \'string\' && this.scope ?\n      `(${this.scope})` : \'\'\n      }: ${this.subject}`;\n    const body = `${this.body}`;\n    const footer = `${this.breaking ? `BREAKING CHANGE: ${this.breaking}|` : \'\'}${this.messageFooter()}`;\n\n    return {\n      main,\n      body,\n      footer\n    };\n  }\n\n  private messageFooter(): string {\n    return this.footer\n      ? `${this.czConfig && this.czConfig.footerPrefix\n        ? this.czConfig.footerPrefix\n        : \'Closes \'\n      }${this.footer}`\n      : \'\';\n  }\n\n  private inputMessage(messageType: string): string {\n    return ConventionalCommitMessage.hasCustomMessage(\n      this.czConfig,\n      messageType\n    )\n      ? this.czConfig.messages[messageType]\n      : DEFAULT_MESSAGES[messageType];\n  }\n}\n\nfunction capitalizeWindowsDriveLetter(path: string): string {\n  if (!path) {\n    return path;\n  }\n\n  return path.replace(/(\\w+?):/, (rootElement) => {\n    return rootElement.toUpperCase();\n  });\n}\n\nfunction getGitCwd(cwd: string): string {\n  let cwdForGit = cwd;\n\n  if (getConfiguration().capitalizeWindowsDriveLetter) {\n    cwdForGit = capitalizeWindowsDriveLetter(cwd);\n  }\n\n  return cwdForGit;\n}\n', 'message': 'style: special case to prefer string concat over template\n', 'author_name': 'Markus Wolf', 'lang': 'ts'}