cli icon indicating copy to clipboard operation
cli copied to clipboard

feat(cli): add prompts to history commands

Open BioPhoton opened this issue 10 months ago • 0 comments

User story

As a user of the CLI on a local setup, I want to have as less setup and reading tie as possible. When using the history command npx code-pusup history I could get prompted for the different options and avoid reading the docs completely.

The CLI prompt could list branches and filter options and automatically derive it from the code base.

Acceptance criteria

Use the library @inquirer/prompts to get beautifully looking prompt messages in the terminal or CI.

The following changes are required:

  • [ ] add interactive flag to the GlobalCLIOptions
    • [ ] add docs to global options in main readme
  • [ ] add prompts to history command
    • [ ] if CLI argument targetBranch is NOT used prompt user for branch name. use the @inquirer/prompts#input helper
    • [ ] prompt user if he want to list commit or tag. use the selecte prompt helper
    • [ ] if CLI argument maxCount is NOT used prompt user for number of commits the history should include
    • [ ] if CLI argument from is NOT used prompt user for number of commits the history should include. use the @inquirer/prompts#select helper
      • [ ] if tags is selected list only tagged commits matching the semver pattern
      • [ ] if commit is selected list all commits.
      • [ ] filter items by maxCount
    • [ ] if CLI argument to is NOT used prompt user for number of commits the history should include. use the @inquirer/prompts#select helper
      • [ ] if tags is selected list only tagged commits matching the semver pattern
      • [ ] if commit is selected list all commits.
      • [ ] filter items by maxCount
      • [ ] filter items by excluded by from
  • [ ] all prompts are integration tested (input into process) e.g. https://github.com/SBoudrias/Inquirer.js/blob/3374a2bebb355ea242a13e873418962323452734/packages/testing/src/index.mts#L45

Implementation details

import {confirm, input, select} from '@inquirer/prompts';
import simpleGit, {LogOptions} from 'simple-git';
import {getMergedSemverTagsFromBranch} from '@code-pushup/utils';
import {HistoryCliOnlyOptions} from "../history/history.model";

async function promptTargetBranch() {
  const summary = await simpleGit().branch(['-r']);

  return select({
    message: 'Select a branch:',
    choices: summary.all.map(branch => ({value: branch})),
    default: 'origin/main',
  });
}

function promptCommitTypeFilter() {
  return confirm({
    message: 'Do you want to by semver tagged commits?',
  });
}

async function promptMaxCount() {
  const prompResult = await input({
    message:
      'How many commits/tags should the history include? (Leave empty for all)',
    validate: (v: string | number) => v === '' || !Number.isNaN(Number(v)),
    transformer: (v: string) => (v === '' ? '-1' : v),
  });
  return Number(prompResult);
}

function promptFrom(tagsOrCommits: string[], {semverTag}: { semverTag: boolean, maxCount?: number }) {
  return select({
    message: `Select a ${semverTag ? 'tag' : 'commit'} from which the history should start crawling:`,
    choices: tagsOrCommits.map(tagOrCommit => ({value: tagOrCommit})),
  });
}

async function promptTo(tagsOrCommits: string[], {semverTag, maxCount, from}: {
  semverTag: boolean,
  maxCount: number,
  from: string
}) {
  const toIndex = tagsOrCommits.indexOf(from);
  const filteredTagsOrCommits = tagsOrCommits.slice(
    Math.min(maxCount, toIndex + 1),
  );

  if (filteredTagsOrCommits.length > maxCount) {
    const toNeeded = await confirm({
      message: `Do you want to specify until where the history should crawl?`,
      default: false,
    });
    if (toNeeded) {
      return (await select({
          message: `Select a ${semverTag ? 'tag' : 'commit'} until which the history should crawl:`,
          choices: filteredTagsOrCommits.map(tagOrCommit => ({
            value: tagOrCommit,
          })),
        }));
    }
  }

  return '';
}

async function filterByCommitType(targetBranch: string, {semverTag = true}: { semverTag: boolean }) {
  return semverTag ?
    await getMergedSemverTagsFromBranch(targetBranch) :
    (await simpleGit().log()).all.map(({hash}) => hash)
}

export async function historyPrompt<
  O extends Partial<LogOptions> & HistoryCliOnlyOptions,
>(options?: O) {
  const {targetBranch, maxCount = -1, from, to, semverTag} = options ?? {};

  // 1. branch name
  const targetBranchInput = targetBranch ?? await promptTargetBranch();

  // 2. list tags only or all commits
  const semverTagInput = semverTag ?? await promptCommitTypeFilter();

  // 3. number of history walks
  const maxCountInput = maxCount < 0 ? await promptMaxCount() : maxCount;

  const tagsOrCommits = await filterByCommitType(targetBranchInput, {semverTag: semverTagInput});

  // 4. select start
  const fromInput = from ?? await promptFrom(tagsOrCommits, {semverTag: semverTagInput, maxCount: maxCountInput});

  // 5. select optional end
  // eslint-disable-next-line functional/no-let
  const toInput = to ?? await promptTo(tagsOrCommits, {
    semverTag: semverTagInput,
    from: fromInput,
    maxCount: maxCountInput
  });

  // create partial history options
  return {
    ...(targetBranchInput === '' ? {} : {branch: targetBranchInput}),
    ...(fromInput === '' ? {} : {from: fromInput}),
    ...(toInput === '' ? {} : {to: toInput}),
    ...(maxCountInput >= 0 ? {maxCount: maxCountInput} : {}),
  };
}

BioPhoton avatar Apr 11 '24 15:04 BioPhoton