Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/support-env-in-constructor-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"simple-git": patch
---

Support `env` in the `simpleGit` builder options - supply environment variables for the spawned `git` child processes when the instance is created, using the same interface as `.env()`. Previously the `env` property was silently ignored (see #1189). Guarded keys supplied this way reject the tasks they are used with unless named in `allowEnvironment`, matching the `.env()` behaviour.
11 changes: 11 additions & 0 deletions simple-git/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,17 @@ simpleGit({ unsafe: { allowUnsafeSshCommand: true }, allowEnvironment: ['GIT_SSH
.catch((err) => {});
```

The initial environment can also be supplied in the `simpleGit` builder options, using the same interface as
passing an object to `.env()`:

```javascript
simpleGit({
unsafe: { allowUnsafeSshCommand: true },
allowEnvironment: ['GIT_SSH_COMMAND'],
env: { ...process.env, GIT_SSH_COMMAND },
}).status();
```

Note - when passing environment variables into the child process, these will replace the standard `process.env`
variables, the example above creates a new object based on `process.env` but with the `GIT_SSH_COMMAND` property added.

Expand Down
1 change: 1 addition & 0 deletions simple-git/src/git.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function Git(options, plugins) {
plugins
);

this._executor.env = options.env;
this._trimmed = options.trimmed;
}

Expand Down
9 changes: 9 additions & 0 deletions simple-git/src/lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ export interface SimpleGitOptions extends Partial<SimpleGitPluginConfig> {
* Base directory for all tasks run through this `simple-git` instance
*/
baseDir: string;
/**
* Initial environment variables for the spawned child processes, used in
* the same way as supplying an object to `.env()` after the instance is
* created - when omitted the ambient environment is inherited. Guarded
* keys (every `GIT_`-prefixed key plus known-vulnerable non-prefixed keys
* such as `EDITOR` / `PAGER`) supplied here reject the tasks they are used
* with unless named in `allowEnvironment`.
*/
env: GitExecutorEnv;
/**
* Limit for the number of child processes that will be spawned concurrently from a `simple-git` instance
*/
Expand Down
1 change: 1 addition & 0 deletions simple-git/src/lib/utils/simple-git-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { SimpleGitOptions } from '../types';

const defaultOptions: Omit<SimpleGitOptions, 'baseDir'> = {
binary: 'git',
env: undefined,
maxConcurrentProcesses: 5,
config: [],
trimmed: false,
Expand Down
40 changes: 40 additions & 0 deletions simple-git/test/integration/constructor-env.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
createTestContext,
newSimpleGit,
setUpInit,
type SimpleGitTestContext,
} from '@simple-git/test-utils';
import { beforeEach, describe, expect, it } from 'vitest';

describe('constructor env', () => {
let context: SimpleGitTestContext;

beforeEach(async () => (context = await createTestContext()));
beforeEach(async () => {
await setUpInit(context);
await context.git.raw(['config', 'user.name', 'Fallback']);
await context.git.raw(['config', 'user.email', 'fallback@test.local']);
});

it('passes allow-listed env supplied in the constructor options to the git child process', async () => {
const git = newSimpleGit(context.root, {
env: { GIT_AUTHOR_NAME: 'Env Test', GIT_AUTHOR_EMAIL: 'env@test.local' },
allowEnvironment: ['GIT_AUTHOR_NAME', 'GIT_AUTHOR_EMAIL'],
});

await git.raw(['commit', '--allow-empty', '-m', 'x']);
const author = await git.raw(['log', '-1', '--format=%an <%ae>']);

expect(author.trim()).toBe('Env Test <env@test.local>');
});

it('rejects guarded keys from the constructor env when they are not allow-listed', async () => {
const git = newSimpleGit(context.root, {
env: { GIT_AUTHOR_NAME: 'Env Test', GIT_AUTHOR_EMAIL: 'env@test.local' },
});

await expect(git.raw(['commit', '--allow-empty', '-m', 'x'])).rejects.toThrow(
'Use of "GIT_AUTHOR_NAME" is blocked by the environment guard'
);
});
});
51 changes: 51 additions & 0 deletions simple-git/test/unit/plugins/plugins.allow-environment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,55 @@ describe('envFilter', () => {
await closeWithSuccess('ok');
expect(await recovered).toBe('ok');
});

it('supplies the initial child process environment from constructor options', async () => {
// regression test for https://github.com/steveukx/git-js/issues/1189 -
// `env` given to the `simpleGit` builder was silently dropped, so the
// child process fell back to the ambient environment instead
const env = await spawnedEnv(newSimpleGit({ env: { NOT_GUARDED: 'ok' } }));

expect(env.NOT_GUARDED).toBe('ok');
});

it('rejects guarded keys supplied via constructor env when not allow-listed', async () => {
const git = newSimpleGit({ env: { GIT_TERMINAL_PROMPT: '0' } });
const queue = git.raw('status');

const error = await promiseError(queue);
assertGitError(error, 'GIT_TERMINAL_PROMPT', GitPluginError);
assertGitError(error, 'allowEnvironment');
assertNoExecutedTasks();
});

it('allows GIT_SSH_COMMAND from constructor env when allow-listed', async () => {
// the scenario reported in #1189 - supplying GIT_SSH_COMMAND when the
// instance is created rather than through `.env()`
const env = await spawnedEnv(
newSimpleGit({
allowEnvironment: ['GIT_SSH_COMMAND'],
unsafe: { allowUnsafeSshCommand: true },
env: { GIT_SSH_COMMAND: 'ssh -i /home/user/.ssh/id_gitlab' },
})
);

expect(env.GIT_SSH_COMMAND).toBe('ssh -i /home/user/.ssh/id_gitlab');
});

it('merges .env(name, value) into the constructor env', async () => {
const env = await spawnedEnv(
newSimpleGit({ env: { FROM_CTOR: 'a' } }).env('FROM_METHOD', 'b')
);

expect(env.FROM_CTOR).toBe('a');
expect(env.FROM_METHOD).toBe('b');
});

it('replaces the constructor env when .env() is given a full object', async () => {
const env = await spawnedEnv(
newSimpleGit({ env: { FROM_CTOR: 'a' } }).env({ FROM_METHOD: 'b' })
);

expect(env).not.toHaveProperty('FROM_CTOR');
expect(env.FROM_METHOD).toBe('b');
});
});