Repository: steveukx/git-js
Pull request #636 (2021-06-10 20:24:55)

Allow setting system user and group IDs for spawned processes (proposed)
Currently the spawned `git` child processes all belong to the user/group of the parent process. This prevents using Simple Git on files owned by other uses without changing file ownership.

So, support a `spawnOptions` configuration option that passes through supported values (just `uid` and `gid`) to the `child_process.spawn` call that ultimately invokes `git`.

I made this work with a plugin modeled on the way custom config options get passed to the `git` command because that seemed like the most similar existing behavior. I am happy to switch this to the way environment variables work if that's more appropriate.


Patch:
diff --git a/readme.md b/readme.md
index d6255bb..e9511d2 100644
--- a/readme.md
+++ b/readme.md
@@ -77,6 +77,16 @@ const git: SimpleGit = simpleGit('/some/path', { config: ['http.proxy=someproxy'
 await git.pull();
 ```
 
+To set the user identity or group identity of the spawned git commands to something other than the owner of
+the current Node process, supply a `spawnOptions` option with a `uid`, a `gid`, or both:
+
+```typescript
+const git: SimpleGit = simpleGit('/some/path', { spawnOptions: { uid: 1000 } });
+
+// any command executed will belong to system user 1000
+await git.pull();
+```
+
 ## Configuring Plugins
 
 - [Error Detection](./docs/PLUGIN-ERRORS.md)
diff --git a/src/lib/git-factory.ts b/src/lib/git-factory.ts
index 8216b3c..de1f89c 100644
--- a/src/lib/git-factory.ts
+++ b/src/lib/git-factory.ts
@@ -7,6 +7,7 @@ import {
    errorDetectionPlugin,
    PluginStore,
    progressMonitorPlugin,
+   spawnOptionsPlugin,
    timeoutPlugin
 } from './plugins';
 import { createInstanceConfig, folderExists } from './utils';
@@ -53,6 +54,7 @@ export function gitInstanceFactory(baseDir?: string | Partial<SimpleGitOptions>,
 
    config.progress && plugins.add(progressMonitorPlugin(config.progress));
    config.timeout && plugins.add(timeoutPlugin(config.timeout));
+   config.spawnOptions && plugins.add(spawnOptionsPlugin(config.spawnOptions));
 
    plugins.add(errorDetectionPlugin(errorDetectionHandler(true)));
    config.errors && plugins.add(errorDetectionPlugin(config.errors));
diff --git a/src/lib/plugins/index.ts b/src/lib/plugins/index.ts
index f005ffe..b290bce 100644
--- a/src/lib/plugins/index.ts
+++ b/src/lib/plugins/index.ts
@@ -3,4 +3,5 @@ export * from './error-detection.plugin';
 export * from './plugin-store';
 export * from './progress-monitor-plugin';
 export * from './simple-git-plugin';
+export * from './spawn-options-plugin';
 export * from './timout-plugin';
diff --git a/src/lib/plugins/simple-git-plugin.ts b/src/lib/plugins/simple-git-plugin.ts
index 7e564a0..84362f3 100644
--- a/src/lib/plugins/simple-git-plugin.ts
+++ b/src/lib/plugins/simple-git-plugin.ts
@@ -1,4 +1,4 @@
-import { ChildProcess } from 'child_process';
+import { ChildProcess, SpawnOptions } from 'child_process';
 import { GitExecutorResult } from '../types';
 
 type SimpleGitTaskPluginContext = {
@@ -11,6 +11,10 @@ export interface SimpleGitPluginTypes {
       data: string[];
       context: SimpleGitTaskPluginContext & {};
    };
+   'spawn.options': {
+      data: Partial<SpawnOptions>;
+      context: SimpleGitTaskPluginContext & {};
+   };
    'spawn.after': {
       data: void;
       context: SimpleGitTaskPluginContext & {
diff --git a/src/lib/plugins/spawn-options-plugin.ts b/src/lib/plugins/spawn-options-plugin.ts
new file mode 100644
index 0000000..ab389d6
--- /dev/null
+++ b/src/lib/plugins/spawn-options-plugin.ts
@@ -0,0 +1,14 @@
+import { SpawnOptions } from 'child_process';
+import { pick } from '../utils';
+import { SimpleGitPlugin } from './simple-git-plugin';
+
+export function spawnOptionsPlugin(spawnOptions: Partial<SpawnOptions>): SimpleGitPlugin<'spawn.options'> {
+  const options = pick(spawnOptions, ['uid', 'gid']);
+
+  return {
+     type: 'spawn.options',
+     action(data) {
+        return {...options, ...data};
+     },
+  };
+}
diff --git a/src/lib/runners/git-executor-chain.ts b/src/lib/runners/git-executor-chain.ts
index 6a9d31e..5a23561 100644
--- a/src/lib/runners/git-executor-chain.ts
+++ b/src/lib/runners/git-executor-chain.ts
@@ -150,11 +150,11 @@ export class GitExecutorChain implements SimpleGitExecutor {
 
    private async gitResponse<R>(task: SimpleGitTask<R>, command: string, args: string[], outputHandler: Maybe<outputHandler>, logger: OutputLogger): Promise<GitExecutorResult> {
       const outputLogger = logger.sibling('output');
-      const spawnOptions: SpawnOptions = {
+      const spawnOptions: SpawnOptions = this._plugins.exec('spawn.options', {
          cwd: this.cwd,
          env: this.env,
          windowsHide: true,
-      };
+      }, pluginContext(task, task.commands));
 
       return new Promise((done) => {
          const stdOut: Buffer[] = [];
diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts
index 1aa3c77..c7206fa 100644
--- a/src/lib/types/index.ts
+++ b/src/lib/types/index.ts
@@ -1,3 +1,5 @@
+import { SpawnOptions } from 'child_process';
+
 import { SimpleGitTask } from './tasks';
 import { SimpleGitProgressEvent } from './handlers';
 
@@ -85,6 +87,8 @@ export interface SimpleGitPluginConfig {
        */
       block: number;
    };
+
+   spawnOptions: Pick<SpawnOptions, 'uid' | 'gid'>;
 }
 
 /**
diff --git a/src/lib/utils/util.ts b/src/lib/utils/util.ts
index 2684f30..94c18b5 100644
--- a/src/lib/utils/util.ts
+++ b/src/lib/utils/util.ts
@@ -141,3 +141,10 @@ export function prefixedArray<T>(input: T[], prefix: T): T[] {
 export function bufferToString (input: Buffer | Buffer[]): string {
    return (Array.isArray(input) ? Buffer.concat(input) : input).toString('utf-8');
 }
+
+/**
+ * Get a new object from a source object with only the listed properties.
+ */
+export function pick (source: Record<string, any>, properties: string[]) {
+   return Object.assign({}, ...properties.map((property) => property in source ? {[property]: source[property]} : {}));
+}


Test patch:
diff --git a/test/unit/__fixtures__/expectations.ts b/test/unit/__fixtures__/expectations.ts
index 088aee1..58aff8c 100644
--- a/test/unit/__fixtures__/expectations.ts
+++ b/test/unit/__fixtures__/expectations.ts
@@ -35,3 +35,7 @@ export function assertChildProcessEnvironmentVariables(env: any) {
    expect(mockChildProcessModule.$mostRecent()).toHaveProperty('$env', env);
 }
 
+export function assertChildProcessSpawnOptions(options: any) {
+  expect(mockChildProcessModule.$mostRecent().$options).toMatchObject(options);
+}
+
diff --git a/test/unit/plugins.spec.ts b/test/unit/plugins.spec.ts
index cdef975..8dfb0b3 100644
--- a/test/unit/plugins.spec.ts
+++ b/test/unit/plugins.spec.ts
@@ -1,5 +1,6 @@
 import { SimpleGit } from '../../typings';
 import {
+   assertChildProcessSpawnOptions,
    assertExecutedCommands,
    assertExecutedCommandsContainsOnce,
    closeWithSuccess,
@@ -24,6 +25,14 @@ describe('plugins', () => {
       assertExecutedCommands('-c', 'a', '-c', 'bcd', 'foo');
    });
 
+   it('allows setting uid and gid', async () => {
+      git = newSimpleGit({spawnOptions: {uid: 1, gid: 2}});
+      git.init();
+
+      await closeWithSuccess();
+      assertChildProcessSpawnOptions({uid: 1, gid: 2});
+   });
+
    describe('progress', () => {
 
       it('emits progress events when counting objects', async () => {