Skip to content

Commit 91eb7fb

Browse files
authored
fix: support parsing empty responses
fix: support parsing empty responses - parsers should treat `undefined` in either `stdout` or `stderr` the same as an empty string fix: detect fatal exceptions in `git pull` - In the case that a `git pull` fails with a recognised fatal error (eg: a `--ff-only` pull cannot be fast-forwarded), the exception thrown will be a `GitResponseError<PullFailedResult>` with summary details of the exception rather than a standard process terminated exception Closes #713
1 parent 08ac626 commit 91eb7fb

File tree

9 files changed

+162
-7
lines changed

9 files changed

+162
-7
lines changed

simple-git/src/lib/parsers/parse-pull.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { PullDetail, PullResult, RemoteMessages } from '../../../typings';
2-
import { PullSummary } from '../responses/PullSummary';
1+
import { PullDetail, PullFailedResult, PullResult, RemoteMessages } from '../../../typings';
2+
import { PullFailedSummary, PullSummary } from '../responses/PullSummary';
33
import { TaskParser } from '../types';
44
import { append, LineParser, parseStringResponse } from '../utils';
55
import { parseRemoteMessages } from './parse-remote-messages';
@@ -35,6 +35,17 @@ const parsers: LineParser<PullResult>[] = [
3535
}),
3636
];
3737

38+
const errorParsers: LineParser<PullFailedResult>[] = [
39+
new LineParser(/^from\s(.+)$/i, (result, [remote]) => void (result.remote = remote)),
40+
new LineParser(/^fatal:\s(.+)$/, (result, [message]) => void (result.message = message)),
41+
new LineParser(/([a-z0-9]+)\.\.([a-z0-9]+)\s+(\S+)\s+->\s+(\S+)$/, (result, [hashLocal, hashRemote, branchLocal, branchRemote]) => {
42+
result.branch.local = branchLocal;
43+
result.hash.local = hashLocal;
44+
result.branch.remote = branchRemote;
45+
result.hash.remote = hashRemote;
46+
}),
47+
];
48+
3849
export const parsePullDetail: TaskParser<string, PullDetail> = (stdOut, stdErr) => {
3950
return parseStringResponse(new PullSummary(), parsers, stdOut, stdErr);
4051
}
@@ -46,3 +57,9 @@ export const parsePullResult: TaskParser<string, PullResult> = (stdOut, stdErr)
4657
parseRemoteMessages<RemoteMessages>(stdOut, stdErr),
4758
);
4859
}
60+
61+
export function parsePullErrorResult(stdOut: string, stdErr: string) {
62+
const pullError = parseStringResponse(new PullFailedSummary(), errorParsers, stdOut, stdErr);
63+
64+
return pullError.message && pullError;
65+
}

simple-git/src/lib/responses/PullSummary.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PullDetailFileChanges, PullDetailSummary, PullResult } from '../../../typings';
1+
import { PullDetailFileChanges, PullDetailSummary, PullFailedResult, PullResult } from '../../../typings';
22

33
export class PullSummary implements PullResult {
44
public remoteMessages = {
@@ -16,4 +16,20 @@ export class PullSummary implements PullResult {
1616
};
1717
}
1818

19+
export class PullFailedSummary implements PullFailedResult {
20+
remote = '';
21+
hash = {
22+
local: '',
23+
remote: '',
24+
};
25+
branch = {
26+
local: '',
27+
remote: '',
28+
};
29+
message = '';
30+
31+
toString() {
32+
return this.message;
33+
}
34+
}
1935

simple-git/src/lib/tasks/pull.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { PullResult } from '../../../typings';
2-
import { parsePullResult } from '../parsers/parse-pull';
2+
import { GitResponseError } from '../errors/git-response-error';
3+
import { parsePullErrorResult, parsePullResult } from '../parsers/parse-pull';
34
import { Maybe, StringTask } from '../types';
5+
import { bufferToString } from '../utils';
46

57
export function pullTask(remote: Maybe<string>, branch: Maybe<string>, customArgs: string[]): StringTask<PullResult> {
68
const commands: string[] = ['pull', ...customArgs];
@@ -13,6 +15,14 @@ export function pullTask(remote: Maybe<string>, branch: Maybe<string>, customArg
1315
format: 'utf-8',
1416
parser(stdOut, stdErr): PullResult {
1517
return parsePullResult(stdOut, stdErr);
18+
},
19+
onError(result, _error, _done, fail) {
20+
const pullError = parsePullErrorResult(bufferToString(result.stdOut), bufferToString(result.stdErr));
21+
if (pullError) {
22+
return fail(new GitResponseError(pullError));
23+
}
24+
25+
fail(_error);
1626
}
1727
}
1828
}

simple-git/src/lib/utils/util.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ function isArrayLike(input: any): input is ArrayLike {
5555
return !!(input && typeof input.length === 'number');
5656
}
5757

58-
export function toLinesWithContent(input: string, trimmed = true, separator = '\n'): string[] {
58+
export function toLinesWithContent(input = '', trimmed = true, separator = '\n'): string[] {
5959
return input.split(separator)
6060
.reduce((output, line) => {
6161
const lineContent = trimmed ? line.trim() : line;

simple-git/test/__fixtures__/setup-init.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { SimpleGitTestContext } from './create-test-context';
44
export const GIT_USER_NAME = 'Simple Git Tests';
55
export const GIT_USER_EMAIL = 'tests@simple-git.dev';
66

7-
export async function setUpInit ({git}: SimpleGitTestContext) {
7+
export async function setUpInit({git}: Pick<SimpleGitTestContext, 'git'>) {
88
await git.raw('-c', 'init.defaultbranch=master', 'init');
99
await configureGitCommitter(git);
1010
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { promiseError } from '@kwsites/promise-result';
2+
import { GitResponseError, PullFailedResult } from '../../typings';
3+
import { createTestContext, like, newSimpleGit, setUpInit, SimpleGitTestContext } from '../__fixtures__';
4+
5+
describe('pull --ff-only', () => {
6+
let context: SimpleGitTestContext;
7+
8+
beforeEach(async () => context = await createTestContext());
9+
beforeEach(async () => {
10+
const upstream = await context.dir('upstream');
11+
const local = context.path('local');
12+
await context.file(['upstream', 'file']);
13+
14+
await givenRemote(upstream);
15+
await givenLocal(upstream, local);
16+
});
17+
18+
async function givenLocal(upstream: string, local: string) {
19+
await newSimpleGit(context.root).clone(upstream, local);
20+
await setUpInit({git: newSimpleGit(local)});
21+
}
22+
23+
async function givenRemote(upstream: string) {
24+
const git = newSimpleGit(upstream);
25+
await setUpInit({git});
26+
await git.add('.');
27+
await git.commit('first');
28+
}
29+
30+
async function givenRemoteFileChanged() {
31+
await context.file(['upstream', 'file'], 'new remote file content');
32+
await newSimpleGit(context.path('upstream')).add('.').commit('remote updated');
33+
}
34+
35+
async function givenLocalFileChanged() {
36+
await context.file(['local', 'file'], 'new local file content');
37+
await newSimpleGit(context.path('local')).add('.').commit('local updated');
38+
}
39+
40+
it('allows fast-forward when there are no changes local or remote', async () => {
41+
const git = newSimpleGit(context.path('local'));
42+
const result = await git.pull(['--ff-only']);
43+
44+
expect(result.files).toEqual([]);
45+
});
46+
47+
it('allows fast-forward when there are some remote but no local changes', async () => {
48+
await givenRemoteFileChanged();
49+
50+
const git = newSimpleGit(context.path('local'));
51+
const result = await git.pull(['--ff-only']);
52+
53+
expect(result.files).toEqual(['file']);
54+
});
55+
56+
it('allows fast-forward when there are no remote but some local changes', async () => {
57+
await givenLocalFileChanged();
58+
59+
const git = newSimpleGit(context.path('local'));
60+
const result = await git.pull(['--ff-only']);
61+
62+
expect(result.files).toEqual([]);
63+
});
64+
65+
it('fails fast-forward when there are both remote and local changes', async () => {
66+
await givenLocalFileChanged();
67+
await givenRemoteFileChanged();
68+
69+
const git = newSimpleGit(context.path('local'));
70+
const err = await promiseError<GitResponseError<PullFailedResult>>(git.pull(['--ff-only']));
71+
72+
expect(err?.git.message).toMatch('Not possible to fast-forward, aborting');
73+
expect(err?.git).toEqual(like({
74+
remote: context.path('upstream'),
75+
hash: {
76+
local: expect.any(String),
77+
remote: expect.any(String),
78+
},
79+
branch: {
80+
local: expect.any(String),
81+
remote: expect.any(String),
82+
},
83+
message: String(err?.git),
84+
}))
85+
});
86+
87+
});

simple-git/test/unit/utils.spec.ts

+7
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ describe('utils', () => {
6060

6161
describe('content', () => {
6262

63+
it('caters for empty values', () => {
64+
expect(toLinesWithContent()).toEqual([]);
65+
expect(toLinesWithContent(undefined, false)).toEqual([]);
66+
expect(toLinesWithContent('')).toEqual([]);
67+
expect(toLinesWithContent('', false)).toEqual([]);
68+
});
69+
6370
it('filters lines with content', () => {
6471
expect(toLinesWithContent(' \n content \n\n')).toEqual(['content']);
6572
expect(toLinesWithContent(' \n content \n\n', false)).toEqual([' ', ' content ']);

simple-git/typings/response.d.ts

+17
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,23 @@ export interface PullDetail {
256256
export interface PullResult extends PullDetail, RemoteMessageResult {
257257
}
258258

259+
/**
260+
* Wrapped with the `GitResponseError` as the exception thrown from a `git.pull` task
261+
* to provide additional detail as to what failed.
262+
*/
263+
export interface PullFailedResult {
264+
remote: string,
265+
hash: {
266+
local: string;
267+
remote: string;
268+
},
269+
branch: {
270+
local: string;
271+
remote: string;
272+
},
273+
message: string;
274+
}
275+
259276
/**
260277
* Represents file name changes in a StatusResult
261278
*/

simple-git/typings/simple-git.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,8 @@ export interface SimpleGit extends SimpleGitBase {
442442
mv(from: string | string[], to: string, callback?: types.SimpleGitTaskCallback<resp.MoveSummary>): Response<resp.MoveSummary>;
443443

444444
/**
445-
* Fetch from and integrate with another repository or a local branch.
445+
* Fetch from and integrate with another repository or a local branch. In the case that the `git pull` fails with a
446+
* recognised fatal error, the exception thrown by this function will be a `GitResponseError<PullFailedResult>`.
446447
*/
447448
pull(remote?: string, branch?: string, options?: types.TaskOptions, callback?: types.SimpleGitTaskCallback<resp.PullResult>): Response<resp.PullResult>;
448449

0 commit comments

Comments
 (0)