Repository: steveukx/git-js
Pull request #637 (2021-06-12 09:55:50)

Pr/636


Patch:
diff --git a/docs/PLUGIN-SPAWN-OPTIONS.md b/docs/PLUGIN-SPAWN-OPTIONS.md
new file mode 100644
index 0000000..fa1420e
--- /dev/null
+++ b/docs/PLUGIN-SPAWN-OPTIONS.md
@@ -0,0 +1,18 @@
+## Process Owner User / Group
+
+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: { gid: 20 } });
+
+// any command executed will belong to system group 20
+await git.pull();
+```
+
+```typescript
+const git: SimpleGit = simpleGit('/some/path', { spawnOptions: { uid: 1000 } });
+
+// any command executed will belong to system user 1000
+await git.pull();
+```
diff --git a/readme.md b/readme.md
index d6255bb..7c3b46e 100644
--- a/readme.md
+++ b/readme.md
@@ -85,6 +85,9 @@ await git.pull();
 - [Progress Events](./docs/PLUGIN-PROGRESS-EVENTS.md)
   Receive progress events as `git` works through long-running processes.
 
+- [Spawned Process Ownership](./docs/PLUGIN-SPAWN-OPTIONS.md)
+  Configure the system `uid` / `gid` to use for spawned `git` processes.
+
 - [Timeout](./docs/PLUGIN-TIMEOUT.md)
   Automatically kill the wrapped `git` process after a rolling timeout.
 
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..743c90f
--- /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..4ebb18e 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,25 @@ describe('plugins', () => {
       assertExecutedCommands('-c', 'a', '-c', 'bcd', 'foo');
    });
 
+   describe('spawnOptions', () => {
+      it('allows setting uid and gid', async () => {
+         git = newSimpleGit({spawnOptions: {uid: 1, gid: 2}});
+         git.init();
+
+         await closeWithSuccess();
+         assertChildProcessSpawnOptions({uid: 1, gid: 2});
+      });
+
+      it('sets process ids along with environment variables', async () => {
+         git = newSimpleGit({spawnOptions: {gid: 2}});
+         git.env({hello: 'world'});
+         git.raw('rev-parse', '--show-toplevel');
+
+         await closeWithSuccess();
+         assertChildProcessSpawnOptions({gid: 2, env: {hello: 'world'}});
+      });
+   });
+
    describe('progress', () => {
 
       it('emits progress events when counting objects', async () => {