Skip to content

Commit

Permalink
feat: git.cwd can now be configured to affect just the chain rather…
Browse files Browse the repository at this point in the history
… than root instance.

Adds a new interface for `git.cwd` to supply the `directory` as an object containing `{ path: string, root?: boolean }` where omitting the `root` property or setting it to `false` will change the working directory only for the chain, whereas setting `root` to `true` (or supplying `directory` as a string - for backward compatibility purposes) will change the working directory of the main instance.
  • Loading branch information
steveukx committed May 13, 2021
1 parent eda7120 commit 4110662
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 41 deletions.
47 changes: 47 additions & 0 deletions examples/git-change-working-directory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## Changing Working Directory

To change the directory the `git` commands are run in you can either configure the `simple-git` instance
when it is created by using the `baseDir` property:

```typescript
import { join } from 'path';
import simpleGit from 'simple-git';

const git = simpleGit({ baseDir: join(__dirname, 'repos') });
```

Or explicitly set the working directory at some later time, for example after cloning a repo:

```typescript
import { join } from 'path';
import simpleGit, { SimpleGit } from 'simple-git';

const remote = `https://github.com/steveukx/git-js.git`;
const target = join(__dirname, 'repos', 'git-js');

// repo is now a `SimpleGit` instance operating on the `target` directory
// having cloned the remote repo then switched into the cloned directory
const repo: SimpleGit = await simpleGit().clone(remote, target).cwd({ path: target });
```

In the example above we're using the command chaining feature of `simple-git` where many commands
are treated as an atomic operation. To rewrite this using separate `async/await` steps would be:

```typescript
import { join } from 'path';
import simpleGit, { SimpleGit } from 'simple-git';

const remote = `https://github.com/steveukx/git-js.git`;
const target = join(__dirname, 'repos', 'git-js');

// create a `SimpleGit` instance
const git: SimpleGit = simpleGit();

// use that instance to do the clone
await git.clone(remote, target);

// then set the working directory of the root instance - you want all future
// tasks run through `git` to be from the new directory, rather than just tasks
// chained off this task
await git.cwd({ path: target, root: true });
```
6 changes: 5 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@ For type details of the response for each of the tasks, please see the [TypeScri
| `.commit(message, handlerFn)` | commits changes in the current working directory with the supplied message where the message can be either a single string or array of strings to be passed as separate arguments (the `git` command line interface converts these to be separated by double line breaks) |
| `.commit(message, [fileA, ...], options, handlerFn)` | commits changes on the named files with the supplied message, when supplied, the optional options object can contain any other parameters to pass to the commit command, setting the value of the property to be a string will add `name=value` to the command string, setting any other type of value will result in just the key from the object being passed (ie: just `name`), an example of setting the author is below |
| `.customBinary(gitPath)` | sets the command to use to reference git, allows for using a git binary not available on the path environment variable |
| `.cwd(workingDirectory)` | Sets the current working directory for all commands after this step in the chain |
| `.diff(options, handlerFn)` | get the diff of the current repo compared to the last commit with a set of options supplied as a string |
| `.diff(handlerFn)` | get the diff for all file in the current repo compared to the last commit |
| `.diffSummary(handlerFn)` | gets a summary of the diff for files in the repo, uses the `git diff --stat` format to calculate changes. Handler is called with a nullable error object and an instance of the [DiffSummary](src/lib/responses/DiffSummary.js) |
Expand Down Expand Up @@ -330,6 +329,11 @@ For type details of the response for each of the tasks, please see the [TypeScri
- `.submoduleInit([options]` Initialises sub modules, the optional [options](#how-to-specify-options) argument can be used to pass extra options to the `git submodule init` command.
- `.submoduleUpdate(subModuleName, [options])` Updates sub modules, can be called with a sub module name and [options](#how-to-specify-options), just the options or with no arguments

## changing the working directory [examples](examples/git-change-working-directory.md)

- `.cwd(workingDirectory)` Sets the working directory for all future commands - note, this will change the working for the root instance, any chain created from the root will also be changed.
- `.cwd({ path, root = false })` Sets the working directory for all future commands either in the current chain of commands (where `root` is omitted or set to `false`) or in the main instance (where `root` is `true`).

# How to Specify Options

Where the task accepts custom options (eg: `pull` or `commit`), these can be supplied as an object, the keys of which
Expand Down
21 changes: 1 addition & 20 deletions src/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ const {SimpleGitApi} = require('./lib/simple-git-api');

const {Scheduler} = require('./lib/runners/scheduler');
const {GitLogger} = require('./lib/git-logger');
const {adhocExecTask, configurationErrorTask} = require('./lib/tasks/task');
const {configurationErrorTask} = require('./lib/tasks/task');
const {
NOOP,
asArray,
filterArray,
filterPrimitives,
filterString,
filterStringOrStringArray,
filterType,
folderExists,
getTrailingOptions,
trailingFunctionArgument,
trailingOptionsArgument
Expand Down Expand Up @@ -89,23 +87,6 @@ Git.prototype.env = function (name, value) {
return this;
};

/**
* Sets the working directory of the subsequent commands.
*/
Git.prototype.cwd = function (workingDirectory) {
const task = (typeof workingDirectory !== 'string')
? configurationErrorTask('Git.cwd: workingDirectory must be supplied as a string')
: adhocExecTask(() => {
if (!folderExists(workingDirectory)) {
throw new Error(`Git.cwd: cannot change to non-directory "${ workingDirectory }"`);
}

return (this._executor.cwd = workingDirectory);
});

return this._runTask(task, trailingFunctionArgument(arguments) || NOOP);
};

/**
* Sets a handler function to be called whenever a new child process is created, the handler function will be called
* with the name of the command being run and the stdout & stderr streams used by the ChildProcess.
Expand Down
9 changes: 7 additions & 2 deletions src/lib/runners/git-executor-chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ export class GitExecutorChain implements SimpleGitExecutor {

private _chain: Promise<any> = Promise.resolve();
private _queue = new TasksPendingQueue();
private _cwd: string | undefined;

public get binary() {
return this._executor.binary;
}

public get cwd() {
return this._executor.cwd;
return this._cwd || this._executor.cwd;
}

public set cwd(cwd: string) {
this._cwd = cwd;
}

public get env() {
Expand Down Expand Up @@ -93,7 +98,7 @@ export class GitExecutorChain implements SimpleGitExecutor {

private async attemptEmptyTask(task: EmptyTask, logger: OutputLogger) {
logger(`empty task bypassing child process to call to task's parser`);
return task.parser();
return task.parser(this);
}

private handleTaskData<R>(
Expand Down
20 changes: 19 additions & 1 deletion src/lib/simple-git-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { PushResult, SimpleGit, SimpleGitBase, TaskOptions } from '../../typings';
import { taskCallback } from './task-callback';
import { changeWorkingDirectoryTask } from './tasks/change-working-directory';
import { pushTask } from './tasks/push';
import { straightThroughStringTask } from './tasks/task';
import { configurationErrorTask, straightThroughStringTask } from './tasks/task';
import { SimpleGitExecutor, SimpleGitTask, SimpleGitTaskCallback } from './types';
import { asArray, filterString, filterType, getTrailingOptions, trailingFunctionArgument } from './utils';

Expand Down Expand Up @@ -34,6 +35,23 @@ export class SimpleGitApi implements SimpleGitBase {
);
}

cwd(directory: string | { path: string, root?: boolean }) {
const next = trailingFunctionArgument(arguments);

if (typeof directory === 'string') {
return this._runTask(changeWorkingDirectoryTask(directory, this._executor), next);
}

if (typeof directory?.path === 'string') {
return this._runTask(changeWorkingDirectoryTask(directory.path, directory.root && this._executor || undefined), next);
}

return this._runTask(
configurationErrorTask('Git.cwd: workingDirectory must be supplied as a string'),
next
);
}

push(remote?: string, branch?: string, options?: TaskOptions, callback?: SimpleGitTaskCallback<PushResult>): SimpleGit & Promise<PushResult>;
push(options?: TaskOptions, callback?: SimpleGitTaskCallback<PushResult>): SimpleGit & Promise<PushResult>;
push(callback?: SimpleGitTaskCallback<PushResult>): SimpleGit & Promise<PushResult>;
Expand Down
13 changes: 13 additions & 0 deletions src/lib/tasks/change-working-directory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { folderExists } from '../utils';
import { SimpleGitExecutor } from '../types';
import { adhocExecTask } from './task';

export function changeWorkingDirectoryTask (directory: string, root?: SimpleGitExecutor) {
return adhocExecTask((instance: SimpleGitExecutor) => {
if (!folderExists(directory)) {
throw new Error(`Git.cwd: cannot change to non-directory "${ directory }"`);
}

return ((root || instance).cwd = directory);
});
}
18 changes: 10 additions & 8 deletions src/lib/tasks/task.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { TaskConfigurationError } from '../errors/task-configuration-error';
import { BufferTask, EmptyTaskParser, SimpleGitTask, SimpleGitTaskConfiguration, StringTask } from '../types';
import { BufferTask, EmptyTaskParser, SimpleGitTask, StringTask } from '../types';

export const EMPTY_COMMANDS: [] = [];

export type EmptyTask<RESPONSE = void> = SimpleGitTaskConfiguration<RESPONSE, 'utf-8', string> & {
export type EmptyTask = {
commands: typeof EMPTY_COMMANDS;
parser: EmptyTaskParser<RESPONSE>;
format: 'empty',
parser: EmptyTaskParser;
onError?: undefined;
};


export function adhocExecTask<R>(parser: () => R): StringTask<R> {
export function adhocExecTask(parser: EmptyTaskParser): EmptyTask {
return {
commands: EMPTY_COMMANDS,
format: 'utf-8',
format: 'empty',
parser,
}
};
}

export function configurationErrorTask(error: Error | string): EmptyTask {
return {
commands: EMPTY_COMMANDS,
format: 'utf-8',
format: 'empty',
parser() {
throw typeof error === 'string' ? new TaskConfigurationError(error) : error;
}
Expand Down Expand Up @@ -52,5 +54,5 @@ export function isBufferTask<R>(task: SimpleGitTask<R>): task is BufferTask<R> {
}

export function isEmptyTask<R>(task: SimpleGitTask<R>): task is EmptyTask {
return !task.commands.length;
return task.format === 'empty' || !task.commands.length;
}
6 changes: 3 additions & 3 deletions src/lib/types/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GitExecutorResult } from './index';
import { GitExecutorResult, SimpleGitExecutor } from './index';
import { EmptyTask } from '../tasks/task';

export type TaskResponseFormat = Buffer | string;
Expand All @@ -7,8 +7,8 @@ export interface TaskParser<INPUT extends TaskResponseFormat, RESPONSE> {
(stdOut: INPUT, stdErr: INPUT): RESPONSE;
}

export interface EmptyTaskParser<RESPONSE> {
(): RESPONSE;
export interface EmptyTaskParser {
(executor: SimpleGitExecutor): void;
}

export interface SimpleGitTaskConfiguration<RESPONSE, FORMAT, INPUT extends TaskResponseFormat> {
Expand Down
2 changes: 1 addition & 1 deletion test/__fixtures__/create-test-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const io = {
return done(path);
}

mkdir(path, (err) => err ? fail(err) : done(path));
mkdir(path, {recursive: true}, (err) => err ? fail(err) : done(path));
});
},
mkdtemp (): Promise<string> {
Expand Down
43 changes: 43 additions & 0 deletions test/integration/change-directory.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { promiseError, promiseResult } from '@kwsites/promise-result';
import { assertGitError, createTestContext, newSimpleGit, SimpleGitTestContext, wait } from '../__fixtures__';
import { SimpleGit } from '../../typings';

describe('change-directory', () => {

Expand All @@ -13,6 +14,44 @@ describe('change-directory', () => {
badDir = await context.path('good', 'bad');
});

it('cwd with path config starts new chain by default', async () => {
await context.dir('foo', 'bar');
await newSimpleGit(context.root).init();

// root chain with a configured working directory
const root = newSimpleGit(await context.path('good'));

// other chains with their own working directories
const foo = root.cwd({ path: await context.path('foo') });
const bar = root.cwd({ path: await context.path('foo', 'bar') });

const offsets = await Promise.all([
showPrefix(foo),
showPrefix(bar),
showPrefix(root),
]);

expect(offsets).toEqual(['foo/', 'foo/bar/', 'good/']);
});

it('cwd with path config can act on root instance', async () => {
await context.dir('foo', 'bar');
await newSimpleGit(context.root).init();

// root chain with a configured working directory
const root = newSimpleGit(await context.path('good'));

// other chains with their own working directories
const foo = root.cwd({ path: await context.path('foo'), root: true });

const offsets = await Promise.all([
showPrefix(foo),
showPrefix(root),
]);

expect(offsets).toEqual(['foo/', 'foo/']);
});

it('switches into new directory - happy path promise', async () => {
const result = await promiseResult(newSimpleGit(context.root).cwd(goodDir));
expect(result).toEqual(expect.objectContaining({
Expand Down Expand Up @@ -42,4 +81,8 @@ describe('change-directory', () => {
expect(spies[2]).not.toHaveBeenCalled();

});

function showPrefix (git: SimpleGit) {
return git.raw('rev-parse', '--show-prefix').then(s => String(s).trim());
}
})
10 changes: 5 additions & 5 deletions typings/simple-git.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export interface SimpleGitBase {
*/
add(files: string | string[], callback?: types.SimpleGitTaskCallback<string>): Response<string>;

/**
* Sets the working directory of the subsequent commands.
*/
cwd(directory: { path: string, root?: boolean }, callback?: types.SimpleGitTaskCallback<string>): Response<string>;
cwd<path extends string>(directory: path, callback?: types.SimpleGitTaskCallback<path>): Response<path>;

/**
* Pushes the current committed changes to a remote, optionally specify the names of the remote and branch to use
Expand Down Expand Up @@ -221,11 +226,6 @@ export interface SimpleGit extends SimpleGitBase {
*/
customBinary(command: string): this;

/**
* Sets the working directory of the subsequent commands.
*/
cwd<path extends string>(workingDirectory: path, callback?: types.SimpleGitTaskCallback<path>): Response<path>;

/**
* Delete one local branch. Supply the branchName as a string to return a
* single `BranchDeletionSummary` instances.
Expand Down

0 comments on commit 4110662

Please sign in to comment.