Repository: steveukx/git-js (ts)

Issue:
Feature request: Global parameters
Firstly, thank you very much for simple-git!

I'd love to be able to pass global parameters to every invocation of `git`. In particular it'd be extremely useful to be able to pass configuration parameters instead of modifying the local config file (e.g. `git -c foo=bar`).

I imagined this as another field in `SimpleGitOptions`.
Allow to override git config options for a single command
Example of a command I want to run:
```shell
$ git -c http.proxy=someproxy pull
```

When options added via `.addConfig(key, value)`, they are persisted in the local config file.

`git.pull({'http.proxy': 'someproxy'})` fails because the config options need to specified before the command name.

`git.raw(['-c', 'http.proxy=someproxy', 'pull'])` works, but you have to work with the raw output.

---

My use case is disabling the global credentials helper when authenticating over HTTPS with `GIT_ASKPASS`:
```shell
$ export GIT_ASKPASS=/path/to/askpass
$ export GIT_TERMINAL_PROMPT=0
$ git -c credential.helper= pull
```

I'm using `GIT_ASKPASS` instead of `git().clone('https://${USER}:${PASS}@${REPO}')` because when you clone like that the password gets written in plain text to `.git/config` of the cloned repo.


Pull request:
feat: custom / inline configuration
Add `config: string[]` property to the `SimpleGitOptions` object used to configure the `simple-git` instance. When supplied, the strings are prefixed to all commands run by that instance as configuration options (ie: they are prefixed themselves with the `-c` flag).

Closes #562 - allows supplying custom proxies to the full set of API commands
Closes #559 - allows global commands

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d35a81d..3230a7a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,11 @@
 
 # Change History & Release Notes
 
-## 2.32.0  
+## 2.32.0 Per-command Configuration
 
+- Supports passing configuration arguments to the `git` binary (via its `-c` argument as a prefix to any other
+  arguments). Eg: to supply some custom http proxy to a `git pull` command, use
+  `simpleGit('/some/path', { config: ['http.proxy=someproxy'] }).pull()` 
 - Add deprecation notice to `git.silent`
 - Internal Updates:
   - switch from `run` to `runTask` in `git` core
diff --git a/readme.md b/readme.md
index e2d6867..2d26957 100644
--- a/readme.md
+++ b/readme.md
@@ -6,9 +6,12 @@ A lightweight interface for running `git` commands in any [node.js](https://node
 
 # Installation
 
-Easiest through [npm](https://npmjs.org): `npm install simple-git`
+Use your favourite package manager:
 
-# Dependencies
+- [npm](https://npmjs.org): `npm install simple-git`
+- [yarn](https://yarnpkg.com/): `yarn add simple-git`
+
+# System Dependencies
 
 Requires [git](https://git-scm.com/downloads) to be installed and that it can be called using the command `git`.
 
@@ -60,6 +63,20 @@ The first argument can be either a string (representing the working directory fo
 
 All configuration properties are optional, the default values are shown in the example above.
 
+## Per-command Configuration
+
+To prefix the commands run by `simple-git` with custom configuration not saved in the git config (ie: using the
+`-c` command) supply a `config` option to the instance builder:
+
+```typescript
+// configure the instance with a custom configuration property
+const git: SimpleGit = simpleGit('/some/path', { config: ['http.proxy=someproxy'] });
+
+// any command executed will be prefixed with this config
+// runs: git -c http.proxy=someproxy pull
+await git.pull();
+```
+
 ## Using task callbacks
 
 Each of the methods in the API listing below can be called in a chain and will be called in series after each other.
diff --git a/src/git-factory.js b/src/git-factory.js
index e73c931..b4ef6c5 100644
--- a/src/git-factory.js
+++ b/src/git-factory.js
@@ -1,5 +1,7 @@
 const Git = require('./git');
 const {GitConstructError} = require('./lib/api');
+const {PluginStore} = require("./lib/plugins/plugin-store");
+const {commandConfigPrefixingPlugin} = require('./lib/plugins/command-config-prefixing-plugin');
 const {createInstanceConfig, folderExists} = require('./lib/utils');
 
 const api = Object.create(null);
@@ -33,6 +35,7 @@ module.exports.gitExportFactory = function gitExportFactory (factory, extra) {
 };
 
 module.exports.gitInstanceFactory = function gitInstanceFactory (baseDir, options) {
+   const plugins = new PluginStore();
    const config = createInstanceConfig(
       baseDir && (typeof baseDir === 'string' ? {baseDir} : baseDir),
       options
@@ -42,5 +45,9 @@ module.exports.gitInstanceFactory = function gitInstanceFactory (baseDir, option
       throw new GitConstructError(config, `Cannot use simple-git on a directory that does not exist`);
    }
 
-   return new Git(config);
+   if (config.config) {
+      plugins.add(commandConfigPrefixingPlugin(config.config));
+   }
+
+   return new Git(config, plugins);
 };
diff --git a/src/git.js b/src/git.js
index 57a77f1..9fe3c10 100644
--- a/src/git.js
+++ b/src/git.js
@@ -3,7 +3,19 @@ const {GitExecutor} = require('./lib/runners/git-executor');
 const {Scheduler} = require('./lib/runners/scheduler');
 const {GitLogger} = require('./lib/git-logger');
 const {adhocExecTask, configurationErrorTask} = require('./lib/tasks/task');
-const {NOOP, asArray, filterArray, filterPrimitives, filterString, filterStringOrStringArray, filterType, folderExists, getTrailingOptions, trailingFunctionArgument, trailingOptionsArgument} = require('./lib/utils');
+const {
+   NOOP,
+   asArray,
+   filterArray,
+   filterPrimitives,
+   filterString,
+   filterStringOrStringArray,
+   filterType,
+   folderExists,
+   getTrailingOptions,
+   trailingFunctionArgument,
+   trailingOptionsArgument
+} = require('./lib/utils');
 const {applyPatchTask} = require('./lib/tasks/apply-patch')
 const {branchTask, branchLocalTask, deleteBranchesTask, deleteBranchTask} = require('./lib/tasks/branch');
 const {taskCallback} = require('./lib/task-callback');
@@ -32,17 +44,10 @@ const {straightThroughBufferTask, straightThroughStringTask} = require('./lib/ta
 
 const ChainedExecutor = Symbol('ChainedExecutor');
 
-/**
- * Git handling for node. All public functions can be chained and all `then` handlers are optional.
- *
- * @param {SimpleGitOptions} options Configuration settings for this instance
- *
- * @constructor
- */
-function Git (options) {
+function Git (options, plugins) {
    this._executor = new GitExecutor(
       options.binary, options.baseDir,
-      new Scheduler(options.maxConcurrentProcesses)
+      new Scheduler(options.maxConcurrentProcesses), plugins,
    );
    this._logger = new GitLogger();
 }
diff --git a/src/lib/plugins/command-config-prefixing-plugin.ts b/src/lib/plugins/command-config-prefixing-plugin.ts
new file mode 100644
index 0000000..709238f
--- /dev/null
+++ b/src/lib/plugins/command-config-prefixing-plugin.ts
@@ -0,0 +1,13 @@
+import { prefixedArray } from '../utils';
+import { SimpleGitPlugin } from './simple-git-plugin';
+
+export function commandConfigPrefixingPlugin(configuration: string[]): SimpleGitPlugin<'spawn.args'> {
+   const prefix = prefixedArray(configuration, '-c');
+
+   return {
+      type: 'spawn.args',
+      action(data) {
+         return [...prefix, ...data];
+      },
+   };
+}
diff --git a/src/lib/plugins/index.ts b/src/lib/plugins/index.ts
new file mode 100644
index 0000000..bbee2f2
--- /dev/null
+++ b/src/lib/plugins/index.ts
@@ -0,0 +1,3 @@
+export * from './command-config-prefixing-plugin';
+export * from './plugin-store';
+export * from './simple-git-plugin';
diff --git a/src/lib/plugins/plugin-store.ts b/src/lib/plugins/plugin-store.ts
new file mode 100644
index 0000000..6ea53d1
--- /dev/null
+++ b/src/lib/plugins/plugin-store.ts
@@ -0,0 +1,27 @@
+import { SimpleGitPlugin, SimpleGitPluginType, SimpleGitPluginTypes } from './simple-git-plugin';
+
+export class PluginStore {
+
+   private plugins: Set<SimpleGitPlugin<SimpleGitPluginType>> = new Set();
+
+   public add<T extends SimpleGitPluginType>(plugin: SimpleGitPlugin<T>) {
+      this.plugins.add(plugin);
+      return () => {
+         this.plugins.delete(plugin);
+      };
+   }
+
+   public exec<T extends SimpleGitPluginType>(type: T, data: SimpleGitPluginTypes[T]['data'], context: SimpleGitPluginTypes[T]['context']): typeof data {
+      let output = data;
+      const contextual = Object.freeze(Object.create(context));
+
+      for (const plugin of this.plugins) {
+         if (plugin.type === type) {
+            output = plugin.action(output, contextual);
+         }
+      }
+
+      return output;
+   }
+
+}
diff --git a/src/lib/plugins/simple-git-plugin.ts b/src/lib/plugins/simple-git-plugin.ts
new file mode 100644
index 0000000..d40e888
--- /dev/null
+++ b/src/lib/plugins/simple-git-plugin.ts
@@ -0,0 +1,14 @@
+export interface SimpleGitPluginTypes {
+   'spawn.args': {
+      data: string[];
+      context: {};
+   };
+}
+
+export type SimpleGitPluginType = keyof SimpleGitPluginTypes;
+
+export interface SimpleGitPlugin<T extends SimpleGitPluginType> {
+   action(data: SimpleGitPluginTypes[T]['data'], context: SimpleGitPluginTypes[T]['context']): typeof data;
+
+   type: T;
+}
diff --git a/src/lib/runners/git-executor-chain.ts b/src/lib/runners/git-executor-chain.ts
index 12a1b9b..cdaaac7 100644
--- a/src/lib/runners/git-executor-chain.ts
+++ b/src/lib/runners/git-executor-chain.ts
@@ -14,6 +14,7 @@ import {
    TaskResponseFormat
 } from '../types';
 import { callTaskParser, GitOutputStreams, objectToString } from '../utils';
+import { PluginStore } from '../plugins/plugin-store';
 
 export class GitExecutorChain implements SimpleGitExecutor {
 
@@ -24,10 +25,6 @@ export class GitExecutorChain implements SimpleGitExecutor {
       return this._executor.binary;
    }
 
-   public get outputHandler() {
-      return this._executor.outputHandler;
-   }
-
    public get cwd() {
       return this._executor.cwd;
    }
@@ -36,7 +33,15 @@ export class GitExecutorChain implements SimpleGitExecutor {
       return this._executor.env;
    }
 
-   constructor(private _executor: SimpleGitExecutor, private _scheduler: Scheduler) {
+   public get outputHandler() {
+      return this._executor.outputHandler;
+   }
+
+   constructor(
+      private _executor: SimpleGitExecutor,
+      private _scheduler: Scheduler,
+      private _plugins: PluginStore
+   ) {
    }
 
    public push<R>(task: SimpleGitTask<R>): Promise<void | R> {
@@ -73,7 +78,11 @@ export class GitExecutorChain implements SimpleGitExecutor {
    }
 
    private async attemptRemoteTask<R>(task: RunnableTask<R>, logger: OutputLogger) {
-      const raw = await this.gitResponse(this.binary, task.commands, this.outputHandler, logger.step('SPAWN'));
+      const args = this._plugins.exec('spawn.args', task.commands, {});
+
+      const raw = await this.gitResponse(
+         this.binary, args, this.outputHandler, logger.step('SPAWN'),
+      );
       const outputStreams = await this.handleTaskData(task, raw, logger.step('HANDLE'));
 
       logger(`passing response to task's parser as a %s`, task.format);
diff --git a/src/lib/runners/git-executor.ts b/src/lib/runners/git-executor.ts
index ce5be56..671fc39 100644
--- a/src/lib/runners/git-executor.ts
+++ b/src/lib/runners/git-executor.ts
@@ -1,10 +1,11 @@
 import { GitExecutorEnv, outputHandler, SimpleGitExecutor, SimpleGitTask } from '../types';
 import { GitExecutorChain } from './git-executor-chain';
 import { Scheduler } from './scheduler';
+import { PluginStore } from '../plugins/plugin-store';
 
 export class GitExecutor implements SimpleGitExecutor {
 
-   private _chain = new GitExecutorChain(this, this._scheduler);
+   private _chain = new GitExecutorChain(this, this._scheduler, this._plugins);
 
    public env: GitExecutorEnv;
    public outputHandler?: outputHandler;
@@ -13,11 +14,12 @@ export class GitExecutor implements SimpleGitExecutor {
       public binary: string = 'git',
       public cwd: string,
       private _scheduler: Scheduler,
+      private _plugins: PluginStore,
    ) {
    }
 
    chain(): SimpleGitExecutor {
-      return new GitExecutorChain(this, this._scheduler);
+      return new GitExecutorChain(this, this._scheduler, this._plugins);
    }
 
    push<R>(task: SimpleGitTask<R>): Promise<void | R> {
diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts
index 02c3912..0b50ccf 100644
--- a/src/lib/types/index.ts
+++ b/src/lib/types/index.ts
@@ -36,6 +36,10 @@ export type outputHandler = (
  */
 export type GitExecutorEnv = NodeJS.ProcessEnv | undefined;
 
+
+
+
+
 /**
  * Public interface of the Executor
  */
@@ -65,6 +69,7 @@ export interface SimpleGitOptions {
    baseDir: string;
    binary: string;
    maxConcurrentProcesses: number;
+   config: string[];
 }
 
 export type Maybe<T> = T | undefined;
diff --git a/src/lib/utils/simple-git-options.ts b/src/lib/utils/simple-git-options.ts
index 6cbf023..ee5fb66 100644
--- a/src/lib/utils/simple-git-options.ts
+++ b/src/lib/utils/simple-git-options.ts
@@ -3,6 +3,7 @@ import { SimpleGitOptions } from '../types';
 const defaultOptions: Omit<SimpleGitOptions, 'baseDir'> = {
    binary: 'git',
    maxConcurrentProcesses: 5,
+   config: [],
 };
 
 export function createInstanceConfig(...options: Array<Partial<SimpleGitOptions>>): SimpleGitOptions {
diff --git a/src/lib/utils/util.ts b/src/lib/utils/util.ts
index 33f4159..7423690 100644
--- a/src/lib/utils/util.ts
+++ b/src/lib/utils/util.ts
@@ -118,3 +118,11 @@ export function asNumber(source: string | null | undefined, onNaN = 0) {
    const num = parseInt(source, 10);
    return isNaN(num) ? onNaN : num;
 }
+
+export function prefixedArray<T>(input: T[], prefix: T): T[] {
+   const output: T[] = [];
+   for (let i = 0, max = input.length; i < max; i++) {
+      output.push(prefix, input[i]);
+   }
+   return output;
+}


Test patch:
diff --git a/test/unit/plugins.spec.ts b/test/unit/plugins.spec.ts
new file mode 100644
index 0000000..d3590b7
--- /dev/null
+++ b/test/unit/plugins.spec.ts
@@ -0,0 +1,16 @@
+import { SimpleGit } from '../../typings';
+import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__';
+
+describe('plugins', () => {
+
+   let git: SimpleGit;
+
+   it('allows configuration prefixing', async () => {
+      git = newSimpleGit({ config: ['a', 'bcd'] });
+      git.raw('foo');
+
+      await closeWithSuccess();
+      assertExecutedCommands('-c', 'a', '-c', 'bcd', 'foo');
+   })
+
+})