Repository: steveukx/git-js
Pull request #660 (2021-07-31 10:48:08)

listConfig with scope, update log to builder pattern


Patch:
diff --git a/readme.md b/readme.md
index ba80a06..e6da214 100644
--- a/readme.md
+++ b/readme.md
@@ -240,7 +240,9 @@ For type details of the response for each of the tasks, please see the [TypeScri
   `true` the configuration setting is appended to rather than overwritten in the local config. Use the `scope` argument
   to pick where to save the new configuration setting (use the exported `GitConfigScope` enum, or equivalent string
   values - `worktree | local | global | system`).
+
 - `.listConfig()` reads the current configuration and returns a [ConfigListSummary](./src/lib/responses/ConfigList.ts)
+- `.listConfig(scope: GitConfigScope)` as with `listConfig` but returns only those items in a specified scope (note that configuration values are overlaid on top of each other to build the config `git` will actually use - to resolve the configuration you are using use `(await listConfig()).all` without the scope argument)
 
 ## git hash-object
 
diff --git a/src/git.js b/src/git.js
index 63cd3fe..ad60c9f 100644
--- a/src/git.js
+++ b/src/git.js
@@ -24,7 +24,6 @@ const {cleanWithOptionsTask, isCleanOptionsArray} = require('./lib/tasks/clean')
 const {commitTask} = require('./lib/tasks/commit');
 const {diffSummaryTask} = require('./lib/tasks/diff');
 const {fetchTask} = require('./lib/tasks/fetch');
-const {logTask, parseLogOptions} = require('./lib/tasks/log');
 const {moveTask} = require("./lib/tasks/move");
 const {pullTask} = require('./lib/tasks/pull');
 const {pushTagsTask} = require('./lib/tasks/push');
@@ -667,38 +666,6 @@ Git.prototype.exec = function (then) {
    return this._runTask(task);
 };
 
-/**
- * Show commit logs from `HEAD` to the first commit.
- * If provided between `options.from` and `options.to` tags or branch.
- *
- * Additionally you can provide options.file, which is the path to a file in your repository. Then only this file will be considered.
- *
- * To use a custom splitter in the log format, set `options.splitter` to be the string the log should be split on.
- *
- * Options can also be supplied as a standard options object for adding custom properties supported by the git log command.
- * For any other set of options, supply options as an array of strings to be appended to the git log command.
- */
-Git.prototype.log = function (options) {
-   const next = trailingFunctionArgument(arguments);
-
-   if (filterString(arguments[0]) && filterString(arguments[1])) {
-      return this._runTask(
-         configurationErrorTask(`git.log(string, string) should be replaced with git.log({ from: string, to: string })`),
-         next
-      );
-   }
-
-   const parsedOptions = parseLogOptions(
-      trailingOptionsArgument(arguments) || {},
-      filterArray(options) && options || []
-   );
-
-   return this._runTask(
-      logTask(parsedOptions.splitter, parsedOptions.fields, parsedOptions.commands),
-      next,
-   )
-};
-
 /**
  * Clears the queue of pending commands and returns the wrapper instance for chaining.
  *
diff --git a/src/lib/simple-git-api.ts b/src/lib/simple-git-api.ts
index c897716..68f7e41 100644
--- a/src/lib/simple-git-api.ts
+++ b/src/lib/simple-git-api.ts
@@ -4,6 +4,7 @@ import { changeWorkingDirectoryTask } from './tasks/change-working-directory';
 import config from './tasks/config';
 import { hashObjectTask } from './tasks/hash-object';
 import { initTask } from './tasks/init';
+import log from './tasks/log';
 import { mergeTask } from './tasks/merge';
 import { pushTask } from './tasks/push';
 import { statusTask } from './tasks/status';
@@ -121,4 +122,4 @@ export class SimpleGitApi implements SimpleGitBase {
    }
 }
 
-Object.assign(SimpleGitApi.prototype, config());
+Object.assign(SimpleGitApi.prototype, config(), log());
diff --git a/src/lib/tasks/config.ts b/src/lib/tasks/config.ts
index f1baa91..d46aed9 100644
--- a/src/lib/tasks/config.ts
+++ b/src/lib/tasks/config.ts
@@ -11,11 +11,11 @@ export enum GitConfigScope {
    worktree = 'worktree',
 }
 
-function asConfigScope(scope: GitConfigScope | unknown): GitConfigScope {
+function asConfigScope<T extends GitConfigScope | undefined>(scope: GitConfigScope | unknown, fallback: T): GitConfigScope | T {
    if (typeof scope === 'string' && GitConfigScope.hasOwnProperty(scope)) {
       return scope as GitConfigScope;
    }
-   return GitConfigScope.local;
+   return fallback;
 }
 
 function addConfigTask(key: string, value: string, append: boolean, scope: GitConfigScope): StringTask<string> {
@@ -36,9 +36,15 @@ function addConfigTask(key: string, value: string, append: boolean, scope: GitCo
    }
 }
 
-function listConfigTask(): StringTask<ConfigListSummary> {
+function listConfigTask(scope?: GitConfigScope): StringTask<ConfigListSummary> {
+   const commands = ['config', '--list', '--show-origin', '--null'];
+
+   if (scope) {
+      commands.push(`--${scope}`);
+   }
+
    return {
-      commands: ['config', '--list', '--show-origin', '--null'],
+      commands,
       format: 'utf-8',
       parser(text: string): any {
          return configListParser(text);
@@ -50,14 +56,14 @@ export default function (): Pick<SimpleGit, 'addConfig' | 'listConfig'> {
    return {
       addConfig(this: SimpleGitApi, key: string, value: string, ...rest: unknown[]) {
          return this._runTask(
-            addConfigTask(key, value, rest[0] === true, asConfigScope(rest[1])),
+            addConfigTask(key, value, rest[0] === true, asConfigScope(rest[1], GitConfigScope.local)),
             trailingFunctionArgument(arguments),
          );
       },
 
-      listConfig(this: SimpleGitApi) {
+      listConfig(this: SimpleGitApi, ...rest: unknown[]) {
          return this._runTask(
-            listConfigTask(),
+            listConfigTask(asConfigScope(rest[0], undefined)),
             trailingFunctionArgument(arguments),
          );
       },
diff --git a/src/lib/tasks/log.ts b/src/lib/tasks/log.ts
index 7396179..d2bd6b8 100644
--- a/src/lib/tasks/log.ts
+++ b/src/lib/tasks/log.ts
@@ -1,12 +1,21 @@
 import { Options, StringTask } from '../types';
-import { LogResult } from '../../../typings';
+import { LogResult, SimpleGit } from '../../../typings';
 import {
    COMMIT_BOUNDARY,
    createListLogSummaryParser,
    SPLITTER,
    START_BOUNDARY
 } from '../parsers/parse-list-log-summary';
-import { appendTaskOptions } from '../utils';
+import {
+   appendTaskOptions,
+   filterArray,
+   filterString,
+   filterType,
+   trailingFunctionArgument,
+   trailingOptionsArgument
+} from '../utils';
+import { SimpleGitApi } from '../simple-git-api';
+import { configurationErrorTask } from './task';
 
 enum excludeOptions {
    '--pretty',
@@ -45,7 +54,13 @@ export type LogOptions<T = DefaultLogFields> = {
    to?: string;
 };
 
-function prettyFormat(format: {[key: string]: string | unknown}, splitter: string): [string[], string] {
+interface ParsedLogOptions {
+   fields: string[];
+   splitter: string;
+   commands: string[]
+}
+
+function prettyFormat(format: { [key: string]: string | unknown }, splitter: string): [string[], string] {
    const fields: string[] = [];
    const formatStr: string[] = [];
 
@@ -69,7 +84,7 @@ function userOptions<T>(input: T): Exclude<Omit<T, keyof typeof excludeOptions>,
    return output;
 }
 
-export function parseLogOptions<T extends Options>(opt: LogOptions<T> = {}, customArgs: string[] = []) {
+export function parseLogOptions<T extends Options>(opt: LogOptions<T> = {}, customArgs: string[] = []): ParsedLogOptions {
    const splitter = opt.splitter || SPLITTER;
    const format = opt.format || {
       hash: '%H',
@@ -91,7 +106,7 @@ export function parseLogOptions<T extends Options>(opt: LogOptions<T> = {}, cust
 
    const maxCount: number | undefined = (opt as any).n || (opt as any)['max-count'] || opt.maxCount;
    if (maxCount) {
-      command.push(`--max-count=${ maxCount }`);
+      command.push(`--max-count=${maxCount}`);
    }
 
    if (opt.from && opt.to) {
@@ -122,3 +137,27 @@ export function logTask<T>(splitter: string, fields: string[], customArgs: strin
       parser: createListLogSummaryParser(splitter, fields),
    };
 }
+
+export default function (): Pick<SimpleGit, 'log'> {
+   return {
+      log<T extends Options>(this: SimpleGitApi, ...rest: unknown[]) {
+         const next = trailingFunctionArgument(arguments);
+         const task = rejectDeprecatedSignatures(...rest) ||
+            createLogTask(parseLogOptions<T>(trailingOptionsArgument(arguments), filterType(arguments[0], filterArray)))
+
+         return this._runTask(task, next);
+      }
+   }
+
+   function createLogTask(options: ParsedLogOptions) {
+      return logTask(options.splitter, options.fields, options.commands);
+   }
+
+   function rejectDeprecatedSignatures(from?: unknown, to?: unknown) {
+      return (
+         filterString(from) &&
+         filterString(to) &&
+         configurationErrorTask(`git.log(string, string) should be replaced with git.log({ from: string, to: string })`)
+      );
+   }
+}
diff --git a/typings/simple-git.d.ts b/typings/simple-git.d.ts
index 3f9ff4d..a0cea6b 100644
--- a/typings/simple-git.d.ts
+++ b/typings/simple-git.d.ts
@@ -134,6 +134,8 @@ export interface SimpleGit extends SimpleGitBase {
    /**
     * Configuration values visible to git in the current working directory
     */
+   listConfig(scope: types.GitConfigScope | string, callback?: types.SimpleGitTaskCallback<resp.ConfigListSummary>): Response<resp.ConfigListSummary>;
+
    listConfig(callback?: types.SimpleGitTaskCallback<resp.ConfigListSummary>): Response<resp.ConfigListSummary>;
 
    /**


Test patch:
diff --git a/test/unit/config.spec.ts b/test/unit/config.spec.ts
index f2ba56e..f412211 100644
--- a/test/unit/config.spec.ts
+++ b/test/unit/config.spec.ts
@@ -1,5 +1,5 @@
 import { SimpleGit } from 'typings';
-import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__';
+import { assertExecutedCommands, closeWithSuccess, like, newSimpleGit } from './__fixtures__';
 import { GitConfigScope } from '../..';
 import { configListParser } from '../../src/lib/responses/ConfigList';
 
@@ -88,4 +88,50 @@ describe('config', () => {
       assertExecutedCommands('config', expected, 'key', 'value');
    });
 
+   it('lists', async () => {
+      const queue = git.listConfig();
+      await closeWithSuccess(`file:/Users/me/.gitconfig\0user.email
+steve@mydev.co\0file:/Users/me/.gitconfig\0init.defaultbranch
+main\0file:.git/config\0core.bare
+false\0file:.git/config\0user.email
+custom@mydev.co\0`);
+
+      assertExecutedCommands('config', '--list', '--show-origin', '--null')
+      expect(await queue).toEqual(like({
+         files: [
+            '/Users/me/.gitconfig',
+            '.git/config',
+         ],
+         all: {
+            'user.email': 'custom@mydev.co',
+            'init.defaultbranch': 'main',
+            'core.bare': 'false',
+         },
+         values: {
+            '/Users/me/.gitconfig': {
+               'user.email': 'steve@mydev.co',
+               'init.defaultbranch': 'main',
+            },
+            '.git/config': {
+               'user.email': 'custom@mydev.co',
+               'core.bare': 'false',
+            },
+         }
+      }))
+   });
+
+   it('lists with string scope', async () => {
+      git.listConfig('local');
+      await closeWithSuccess();
+
+      assertExecutedCommands('config', '--list', '--show-origin', '--null', '--local');
+   });
+
+   it('lists with scope', async () => {
+      git.listConfig(GitConfigScope.system);
+      await closeWithSuccess();
+
+      assertExecutedCommands('config', '--list', '--show-origin', '--null', '--system');
+   });
+
 });
diff --git a/test/unit/log.spec.ts b/test/unit/log.spec.ts
index 9a87620..9a10f42 100644
--- a/test/unit/log.spec.ts
+++ b/test/unit/log.spec.ts
@@ -1,11 +1,15 @@
+import { promiseError } from '@kwsites/promise-result';
 import { LogResult, SimpleGit } from 'typings';
 import {
    assertExecutedCommands,
    assertExecutedCommandsContains,
+   assertGitError,
+   assertNoExecutedTasks,
    closeWithSuccess,
    like,
    newSimpleGit
 } from './__fixtures__';
+import { TaskConfigurationError } from '../..'
 import {
    COMMIT_BOUNDARY,
    createListLogSummaryParser,
@@ -522,6 +526,12 @@ ${START_BOUNDARY}207601debebc170830f2921acf2b6b27034c3b1f::2016-01-03 15:50:58 +
    });
 
    describe('deprecations', () => {
+      it('rejects from and to as strings', async () => {
+         const queue = promiseError((git.log as any)('FROM', 'TO'));
+         assertGitError(await queue, 'should be replaced with', TaskConfigurationError);
+         assertNoExecutedTasks();
+      });
+
       it('supports ListLogSummary without generic type', async () => {
          const summary: Promise<LogResult> = git.log({from: 'from', to: 'to'});
          await closeWithSuccess();