# ng-maintain-utils [![Build Status][build-status-image]][build-status]

## Description

A private collection of utilities for developing tools to help maintain (AngularJS-related) GitHub
repositories.

## Usage

**You** should generally not use it. You would use tools built on top of it, for example:

- [ng-cla-check][ng-cla-check]
- [ng-maintain][ng-maintain]
- [ng-pr-merge][ng-pr-merge]

**I** may use it for building other tools (see above). Here is a brief overview of what's in the box:

- **`AbstractCli`:** Can serve as a base-class for creating a `Cli` object that can orchestrate the
  execution of some type of work, based on a list of "raw" command-line arguments. It can display
  version info (with `--version`), show usage instructions (with `--usage`), outline the commands
  that need to be executed to complete the task at hand (with `--instructions`) - a sort of
  "dry-run", report the beginning/end of execution, etc.

  It exposes the following (public) methods:

  - `getPhases(): Phase[]` (**abstract**): This method _must_ be overwritten and return an array of `Phase`
    objects (to be used for displaying instructions in the "dry-run" mode).
  - `run(rawArgs: string[], doWork?: ({[key: string]: string}) => any): Promise`: Parse the arguments
    and take appropriate action (see above).

  It also provides a number of "protected" methods, that can be overwritten by sub-classes:

  - `_displayExperimentalTool(): void`
  - `_displayHeader(headerTmpl: string, input: {[key: string]: string}): void`
  - `_displayInstructions(phases: Phase[], input: {[key: string]: string}): void`
  - `_displayUsage(usageMessage: string): void`
  - `_displayVersionInfo(): void`
  - `_getAndValidateInput(rawArgs: string[], argSpecs: ArgSpec[]): Promise<{[key: string]: string}>`
  - `_insertEmptyLine<T>(value: T, isRejection?: boolean): T|Promise<T>`
  - `_theHappyEnd<T>(value: T): T`
  - `_theUnhappyEnd(err: any): Promise<any>`

  Requires:
    - _config_: `Config`

- **`ArgSpec`/`ArgSpec.Unnamed`:** Represents the specification for a command-line argument. When
  applied on a parsed arguments object (such as the ones returned by `Utils#parseArgs()`) it will
  extract the corresponding argument's value (either by name (`ArgSpec`) or by position
  (`ArgSpec.Unnamed`)), fall back to a default value if necessary, vaidate the value and assign it
  to the specified `input` object under the appropriate `key`.

  Requires:
    - _index_: `number` (`ArgSpec.Unnamed` only)
    - _key_: `string`
    - _validator_: `(value: any) => boolean`
    - _errorCode_: `string`
    - _defaultValue?_: `string|boolean`

- **`CleanUper`:** A utility to help coordinate arbitrary tasks with their associated clean-up
  process.

  The general idea is this:
  1. Schedule a task to clean up after `something`.
  2. Do `something`.
  3. ...possibly do other things here...
  4. If anything goes wrong, the app will be able to clean up (or show instructions to the user).
  5. When cleaning up after `something` is no longer necessary, unschedule the clean-up task.

  Provides the following methods:
  - `cleanUp(listOnly: boolean): Promise`: Perform all clean-up tasks (or just list them).
    <sub>(Either way, the clean-up task queue is emptied.)</sub>
  - `getCleanUpPhase(): Phase`: Returns a clean-up `Phase` object (suitable for `UiUtils#phase()`).
  - `hasTasks(): boolean`: Returns whether or not there are any clean-up tasks scheduled.
  - `registerTask(description: string, cb: () => Promise): TaskId`: Register a task with the
    `CleanUper`. You can use the returned, unique `TaskId` for scheduling/unscheduling the task.
  - `schedule(taskId: TaskId): void`: Schedule a clean-up task.
  - `unschedule(taskId: TaskId): void`: Schedule (the last instance of) a clean-up task.
  - `withTask(taskId: TaskId, fn: () => any): Promise`: Schedule `taskId` and execute `fn` (can also
    return a promise). If all goes well, unschedule `taskId`. If an error occurs, leave `taskId`
    in the clean-up task queue.

  Requires:
    - _logger_: `Logger`

- **`Config`:** Creates a `Config` object based on the specified `messages` and `argSpecs` (falling
  back to some default values if necessary). It exposes the following properties:

  - `argSpecs`: A (possibly empty) array of `ArgSpec` objects.
  - `defaults`: A `{[argument: string]: string|number}` map of default values per command-line
    argument keys. <sub>(Automatically extracted for `argSpecs`.)</sub>
  - `messages`: A (possibly nested) `{[messageKey: string]: string}` map with at least the following
    messages:
    - `usage`
    - `instructionsHeaderTmpl`
    - `headerTmpl`
    - `errors`:
      - `ERROR_unexpected`
    - `warnings`:
      - `WARN_experimentalTool`
  - `versionInfo`: A `{name: string, version: string}` map with values retrieved from the main
    module's `package.json` (i.e. the first `package.json` to be found starting from the main file's
    directory and moving upwards).

  Requires:
    - _messages_: `{[messageKey: string]: string}`
    - _argSpecs_: `ArgSpec[]`

- **`GitUtils`:** A collection of `Git`-related command-wrappers and utilities. Mainly spawns `Git`
  commands in a separate process and (promises to) return the output. Support for commands is added
  in an "as-needed" basis. Currently, the available commands/utilities include:

  - `abortAm(): Promise`
  - `abortRebase(): Promise`
  - `checkout(branch: string): Promise`
  - `clean(mode?: string = 'interactive'): Promise`
  - `countCommitsSince(commit: string): Promise<number>`
  - `createBranch(branch: string): Promise`
  - `deleteBranch(branch: string, force?: boolean): Promise`
  - `diff(commit: string, noColor?: boolean): Promise`
  - `diffWithHighlight(commit: string): Promise`
  - `diffWithHighlight2(commit: string): Promise`
  - `getCommitMessage(commit: string): Promise<string>`
  - `getLastCommitMessage(): Promise<string>`
  - `log(oneline?: boolean, count?: number, noDecorate?: boolean): Promise`
  - `mergePullRequest(prUrl: string): Promise`
  - `pull(branch: string, rebase?: boolean): Promise`
  - `push(branch: string): Promise`
  - `rebase(commit: string|number, interactive?: boolean): Promise`
  - `reset(commit: string, hard?: boolean): Promise`
  - `setLastCommitMessage(message: string): Promise`
  - `updateLastCommitMessage(getNewMessage: (oldMessage: string) => string): Promise`

  Requires:
    - _cleanUper_: `CleanUper`
    - _utils_: `Utils`

- **`GitUtils.DiffHighlighter`:** Can be used to enhance a diff by highlighting areas of interest.
  The general implementation is loosely based on the idea of [diff-highlight][diff-highlight],
  although the matching heuristics and coloring (among other things) are different.

  It exposes the underlying streams via:

  - `getInputStream(): stream.PassThrough`
  - `getOutputStream(): stream.PassThrough`

  Requires:
  - _styles?_:

    ```ts
    {
      lineRemoved?: (text: string) => string,
      lineAdded?: (text: string) => string,
      areaRemoved?: (text: string) => string,
      areaAdded?: (text: string) => string
    }
    ```

- **`GitUtils.DiffHighlighter2`:** An alternative, API-compatible implementation of
  `GitUtils.DiffHighlighter`. The highlighting is more accurate, as it is able to only highlight the
  regions that have changed. The main drawback is that it fails to show removed/added empty lines,
  because of its dependency on `Git`s `--word-diff=plain` option.

  Similar to `GitUtils.DiffHighlighter`, it exposes the underlying streams via:

  - `getInputStream(): stream.PassThrough`
  - `getOutputStream(): stream.PassThrough`

  Requires:
  - _styles?_:

    ```ts
    {
      lineRemoved?: (text: string) => string,
      lineAdded?: (text: string) => string,
      areaRemoved?: (text: string) => string,
      areaAdded?: (text: string) => string
    }
    ```

- **`Logger`:** A simple helper providing minimal logging utilities. This is mostly used in order to
  make it easier to test logging behavior without affecting `console` methods. Provides the
  following methods:

  - `debug()`: Delegate to `console.debug()`.
  - `error()`: Delegate to `console.error()`.
  - `info()`: Delegate to `console.info()`.
  - `log()`: Delegate to `console.log()`.
  - `warn()`: Delegate to `console.warn()`.

- **`Phase`:** A simple wrapper for "phase" entities (with validation). A "phase" is a description
  of a unit of work, including an ID, a short description, a list of the tasks involved and an error
  message (or code) specific to this "phase".

  Requires:
    - _id_: `string`
    - _description_: `string`
    - _instructions?_: `string[]`
    - _error?_: `string` <sub>(Can be either an error message or an error code.)</sub>

- **`UiUtils`:** A collection of utilities useful for interacting with the user, including:

  - `askQuestion()`: Prompt the user with a question and (promise to) return the answer.
  - `askYesOrNoQuestion()`: Prompt the user with a yes-or-no question (e.g. a confirmation) and
    (promise to) resolve (for "yes") or reject (for "no").
  - `offerToCleanUp()`: Requests confirmation to perform the scheduled clean-up tasks. If the user
    turns the offer down, it will just list the pending tasks instead.
  - `phase()`: It will report the beginning and end of a "phase" (see `Phase`), do some work and
    properly handle possible errors (by means of `reportAndRejectFnGen()`).
  - `reportAndRejectFnGen()`: Generates a callback that will report the specified error (plus any
    extra error provided during invokation), will offer to clean up (if there are pending tasks and
    not configured otherwise) and return a rejection.

  Requires:
    - _logger_: `Logger`
    - _cleanUper_: `CleanUper`
    - _errorMessages_: `{[errorCode: string]: string}`

- **`Utils`:** A collection of low-level, specific-purpose utilities, including:

  - `asPromised()`: Convert callback-based functions to promise-based.
  - `interpolate()`: Replace `{{...}}` placeholders in a string with values.
  - `parseArgs()`: Parse command-line arguments (and remove surrounding quotes).
  - `resetOutputStyleOnExit()`: Ensure the output style is reset when a process exists.
  - `spawnAsPromised()`: Spawn a sub-shell to run a (series of) command(s) with support for piping.
  - `waitAsPromised()`: `setTimeout()` wrapped in a promise.

## Testing

The following test-types/modes are available:

- **Code-linting:** `npm run lint`
  _Lint JavaScript files using ESLint._

- **Unit tests:** `npm run test-unit`
  _Run all the unit tests once. These tests are quick and suitable to be run on every change._

- **E2E tests:** `npm run test-e2e`
  _Run all the end-to-end tests once. These test may hit actual API endpoints or perform expensive
  I/O operations and are considerably slower than unit tests._

- **All tests:** `npm test` / `npm run test`
  _Run all of the above tests (code-linting, unit tests, e2e tests). This command is automatically
  run before `npm version` and `npm publish`._

- **"Watch" mode:** `npm run test-watch`
  _Watch all files and rerun the unit tests whenever something changes. For performance reasons,
  code-linting and e2e tests are omitted._


[build-status]: https://github.com/gkalpak/ng-maintain-utils/actions/workflows/ci.yml
[build-status-image]: https://github.com/gkalpak/ng-maintain-utils/actions/workflows/ci.yml/badge.svg?branch=master&event=push
[diff-highlight]: https://github.com/git/git/blob/master/contrib/diff-highlight/README
[ng-cla-check]: https://www.npmjs.com/package/@gkalpak/ng-cla-check
[ng-maintain]: https://www.npmjs.com/package/@gkalpak/ng-maintain
[ng-pr-merge]: https://www.npmjs.com/package/@gkalpak/ng-pr-merge
