Repository: steveukx/git-js
Pull request #545 (2020-12-15 11:34:48)

Pr/2.28.0


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e20abf..723986b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,14 @@
 
 # Change History & Release Notes
 
+## 2.28.0 Add support for `git apply` & TypeScript Integration Tests
+
+- Adds support for `git.applyPatch` to apply patches generated in a `git diff` to the working index,
+  TypeScript consumers can make use of the `ApplyOptions` type definition to make use of strong types
+  for the supported options. Thanks to [@andreterron](https://github.com/andreterron) for the pull request.
+
+- Integration tests converted to TypeScript to ensure type safety across all tests.
+
 ## 2.27.0 Included staged delete/modify in StatusResult staged array  
 
 -  Update the `git.status` parser to account for staged deleted/modified files and staged files with subsequent
diff --git a/readme.md b/readme.md
index 3a1e971..b2d06aa 100644
--- a/readme.md
+++ b/readme.md
@@ -180,6 +180,11 @@ For type details of the response for each of the tasks, please see the [TypeScri
 | `.tags([options, ] handlerFn)` | list all tags, use the optional [options](#how-to-specify-options) object to set any options allows by the [git tag](https://git-scm.com/docs/git-tag) command. Tags will be sorted by semantic version number by default, for git versions 2.7 and above, use the `--sort` option to set a custom sort. |
 | `.show([options], handlerFn)` | Show various types of objects, for example the file content at a certain commit. `options` is the single value string or array of string commands you want to run |
 
+## git apply
+
+- `.applyPatch(patch, [options])` applies a single string patch (as generated by `git diff`), optionally configured with the supplied [options](#how-to-specify-options) to set any arguments supported by the [apply](https://git-scm.com/docs/git-apply) command. Returns the unmodified string response from `stdout` of the `git` binary.
+- `.applyPatch(patches, [options])` applies an array of string patches (as generated by `git diff`), optionally configured with the supplied [options](#how-to-specify-options) to set any arguments supported by the [apply](https://git-scm.com/docs/git-apply) command. Returns the unmodified string response from `stdout` of the `git` binary.
+
 ## git branch
 
 - `.branch([options])` uses the supplied [options](#how-to-specify-options) to run any arguments supported by the [branch](https://git-scm.com/docs/git-branch) command. Either returns a [BranchSummaryResult](src/lib/responses/BranchSummary.ts) instance when listing branches, or a [BranchSingleDeleteResult](typings/response.d.ts) type object when the options included `-d`, `-D` or `--delete` which cause it to delete a named branch rather than list existing branches.
@@ -573,7 +578,7 @@ catch (err) {
 }
 ```
 
-# Troubleshooting
+# Troubleshooting / FAQ
 
 ### Enable logging
 
diff --git a/src/git.js b/src/git.js
index 28dc882..85419f0 100644
--- a/src/git.js
+++ b/src/git.js
@@ -4,6 +4,7 @@ const {Scheduler} = require('./lib/runners/scheduler');
 const {GitLogger} = require('./lib/git-logger');
 const {adhocExecTask, configurationErrorTask} = require('./lib/tasks/task');
 const {NOOP, appendTaskOptions, 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');
 const {checkIsRepoTask} = require('./lib/tasks/check-is-repo');
@@ -795,6 +796,17 @@ Git.prototype.diffSummary = function () {
    );
 };
 
+Git.prototype.applyPatch = function(patches) {
+   const task = !filterStringOrStringArray(patches)
+      ? configurationErrorTask(`git.applyPatch requires one or more string patches as the first argument`)
+      : applyPatchTask(asArray(patches), getTrailingOptions([].slice.call(arguments, 1)));
+
+   return this._runTask(
+      task,
+      trailingFunctionArgument(arguments),
+   );
+}
+
 Git.prototype.revparse = function () {
    const commands = ['rev-parse', ...getTrailingOptions(arguments, true)];
    return this._runTask(
diff --git a/src/lib/runners/promise-wrapped.ts b/src/lib/runners/promise-wrapped.ts
index 31d96b2..1284259 100644
--- a/src/lib/runners/promise-wrapped.ts
+++ b/src/lib/runners/promise-wrapped.ts
@@ -12,6 +12,7 @@ const functionNamesPromiseApi = [
    'addConfig',
    'addRemote',
    'addTag',
+   'applyPatch',
    'binaryCatFile',
    'branch',
    'branchLocal',
diff --git a/src/lib/tasks/apply-patch.ts b/src/lib/tasks/apply-patch.ts
new file mode 100644
index 0000000..3ac5425
--- /dev/null
+++ b/src/lib/tasks/apply-patch.ts
@@ -0,0 +1,37 @@
+import { straightThroughStringTask } from './task';
+import { OptionFlags, Options, StringTask } from '../types';
+
+export type ApplyOptions = Options &
+   OptionFlags<
+      | '--stat'
+      | '--numstat'
+      | '--summary'
+      | '--check'
+      | '--index'
+      | '--intent-to-add'
+      | '--3way'
+      | '--apply'
+      | '--no-add'
+      | '-R'
+      | '--reverse'
+      | '--allow-binary-replacement'
+      | '--binary'
+      | '--reject'
+      | '-z'
+      | '--inaccurate-eof'
+      | '--recount'
+      | '--cached'
+      | '--ignore-space-change'
+      | '--ignore-whitespace'
+      | '--verbose'
+      | '--unsafe-paths'> &
+   OptionFlags<
+      '--whitespace',
+      'nowarn' | 'warn' | 'fix' | 'error' | 'error-all'> &
+   OptionFlags<'--build-fake-ancestor' | '--exclude' | '--include' | '--directory',
+      string> &
+   OptionFlags<'-p' | '-C', number>;
+
+export function applyPatchTask(patches: string[], customArgs: string[]): StringTask<string> {
+   return straightThroughStringTask(['apply', ...customArgs, ...patches]);
+}
diff --git a/typings/simple-git.d.ts b/typings/simple-git.d.ts
index e6c729a..4989554 100644
--- a/typings/simple-git.d.ts
+++ b/typings/simple-git.d.ts
@@ -30,6 +30,12 @@ export interface SimpleGit {
    addConfig(key: string, value: string, append?: boolean, callback?: types.SimpleGitTaskCallback<string>): Response<string>;
    addConfig(key: string, value: string, callback?: types.SimpleGitTaskCallback<string>): Response<string>;
 
+   /**
+    * Applies a patch to the repo
+    */
+   applyPatch(patches: string | string[], options: types.TaskOptions<types.ApplyOptions>, callback?: types.SimpleGitTaskCallback<string>): Response<string>;
+   applyPatch(patches: string | string[], callback?: types.SimpleGitTaskCallback<string>): Response<string>;
+
    /**
     * Configuration values visible to git in the current working directory
     */
diff --git a/typings/types.d.ts b/typings/types.d.ts
index d27331b..ac90481 100644
--- a/typings/types.d.ts
+++ b/typings/types.d.ts
@@ -6,4 +6,5 @@ export { outputHandler, Options, SimpleGitOptions, TaskOptions, SimpleGitTaskCal
 export { CheckRepoActions } from '../src/lib/tasks/check-is-repo';
 export { CleanOptions, CleanMode } from '../src/lib/tasks/clean';
 export { CloneOptions } from '../src/lib/tasks/clone';
+export { ApplyOptions } from '../src/lib/tasks/apply-patch';
 export { ResetOptions, ResetMode } from '../src/lib/tasks/reset';


Test patch:
diff --git a/test/unit/apply-patch.spec.ts b/test/unit/apply-patch.spec.ts
new file mode 100644
index 0000000..dd4bfe9
--- /dev/null
+++ b/test/unit/apply-patch.spec.ts
@@ -0,0 +1,96 @@
+import { SimpleGit, TaskOptions } from 'typings';
+import { assertExecutedCommands, assertGitError, closeWithSuccess, newSimpleGit, newSimpleGitP } from './__fixtures__';
+import { promiseError, promiseResult } from '@kwsites/promise-result';
+
+describe('applyPatch', () => {
+
+   describe('commands', () => {
+      let git: SimpleGit;
+
+      const applyPatchTests: [keyof SimpleGit, string, Array<string | TaskOptions>, string[]][] = [
+         ['applyPatch', 'with one file', ['./diff'], ['apply', './diff']],
+         ['applyPatch', 'with multiple files', [['./diff1', './diff2']], ['apply', './diff1', './diff2']],
+         ['applyPatch', 'with options array', ['./diff', ['--stat']], ['apply', '--stat', './diff']],
+         ['applyPatch', 'with options object', ['./diff', {'-p': 2}], ['apply', '-p=2', './diff']],
+      ];
+
+      beforeEach(() => git = newSimpleGit());
+
+      it.each(applyPatchTests)('callbacks - %s %s', async (api, name, applyPatchArgs, executedCommands)=> {
+         const callback = jest.fn();
+         const queue = (git[api] as any)(...applyPatchArgs, callback);
+         await closeWithSuccess(name);
+
+         expect(await queue).toBe(name);
+         expect(callback).toHaveBeenCalledWith(null, name);
+         assertExecutedCommands(...executedCommands);
+      });
+
+      it.each(applyPatchTests)(`promises - %s %s`, async (api, name, applyPatchArgs, executedCommands) => {
+         const queue = (git[api] as any)(...applyPatchArgs);
+         await closeWithSuccess(name);
+
+         expect(await queue).toBe(name);
+         assertExecutedCommands(...executedCommands);
+      });
+
+      it.each(applyPatchTests)(`(legacy) promise usage - %s %s`, async (_api, name, applyPatchArgs, executedCommands) => {
+         const queue = newSimpleGitP().applyPatch(...applyPatchArgs);
+         await closeWithSuccess(name);
+
+         expect(await queue).toBe(name);
+         assertExecutedCommands(...executedCommands);
+      });
+   });
+
+   describe('usage', () => {
+      let callback: jest.Mock;
+
+      const tests: Array<[string, RegExp | null, 'Y' | 'N', (git: SimpleGit) => Promise<string>]> = [
+         ['patch   - no-opt     - no-callback  ', null, 'N', (git) => git.applyPatch('foo')],
+         ['patch   - array-opt  - no-callback  ', null, 'N', (git) => git.applyPatch('foo', ['--opt'])],
+         ['patch   - object-opt - no-callback  ', null, 'N', (git) => git.applyPatch('foo', {'--opt': null})],
+         ['patch   - no-opt     - with-callback', null, 'Y', (git) => git.applyPatch('foo', callback)],
+         ['patch   - array-opt  - with-callback', null, 'Y', (git) => git.applyPatch('foo', ['--opt'], callback)],
+         ['patch   - object-opt - with-callback', null, 'Y', (git) => git.applyPatch('foo', {'--opt': null}, callback)],
+         ['patches - no-opt     - no-callback  ', null, 'N', (git) => git.applyPatch(['foo', 'bar'])],
+         ['patches - array-opt  - no-callback  ', null, 'N', (git) => git.applyPatch(['foo', 'bar'], ['--opt'])],
+         ['patches - object-opt - no-callback  ', null, 'N', (git) => git.applyPatch(['foo', 'bar'], {'--opt': null})],
+         ['patches - no-opt     - with-callback', null, 'Y', (git) => git.applyPatch(['foo', 'bar'], callback)],
+         ['patches - array-opt  - with-callback', null, 'Y', (git) => git.applyPatch(['foo', 'bar'], ['--opt'], callback)],
+         ['patches - object-opt - with-callback', null, 'Y', (git) => git.applyPatch(['foo', 'bar'], {'--opt': null}, callback)],
+
+         ['error: no patches', /string patches/, 'N', (git) => git.applyPatch({'--opt': null} as any)],
+      ];
+
+      const noCallbackTests = tests.filter(t => t[2] === 'N');
+
+      beforeEach(() => callback = jest.fn());
+
+      it.each(tests)(`git.applyPatch %s`, async (name, error, withCallback, task) => {
+         const result = task(newSimpleGit());
+
+         if (error) {
+            return assertGitError(await promiseError(result), error);
+         }
+
+         await closeWithSuccess(name);
+         expect(await result).toBe(name);
+
+         if (withCallback === 'Y') {
+            expect(callback).toHaveBeenCalledWith(null, name);
+         }
+      });
+
+      it.each(noCallbackTests)(`gitP.applyPatch %s`, async (name, error, _withCallback, task) => {
+         const result = promiseResult(task(newSimpleGitP()));
+
+         if (error) {
+            return assertGitError((await result).result, error);
+         }
+
+         await closeWithSuccess(name);
+         expect((await result).result).toBe(name);
+      });
+   });
+});