Skip to content

Commit 357b4de

Browse files
authored
Merge pull request #771 from steveukx/feat/status-with-nulls
Status Summary should use null terminators to allow files with spaces…
2 parents 94c2462 + ed412ef commit 357b4de

File tree

5 files changed

+77
-64
lines changed

5 files changed

+77
-64
lines changed

Diff for: .changeset/sixty-rivers-drop.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"simple-git": minor
3+
---
4+
5+
Use null separators in git.status to allow for non-ascii file names

Diff for: simple-git/src/lib/responses/StatusSummary.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { StatusResult } from '../../../typings';
2-
import { append } from '../utils';
2+
import { append, NULL } from '../utils';
33
import { FileStatusSummary } from './FileStatusSummary';
44

55
type StatusLineParser = (result: StatusResult, file: string) => void;
@@ -120,7 +120,7 @@ const parsers: Map<string, StatusLineParser> = new Map([
120120
]);
121121

122122
export const parseStatusSummary = function (text: string): StatusResult {
123-
const lines = text.trim().split('\n');
123+
const lines = text.trim().split(NULL);
124124
const status = new StatusSummary();
125125

126126
for (let i = 0, l = lines.length; i < l; i++) {

Diff for: simple-git/src/lib/tasks/status.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,21 @@ import { StatusResult } from '../../../typings';
22
import { parseStatusSummary } from '../responses/StatusSummary';
33
import { StringTask } from '../types';
44

5+
const ignoredOptions = ['--null', '-z'];
6+
57
export function statusTask(customArgs: string[]): StringTask<StatusResult> {
8+
const commands = [
9+
'status',
10+
'--porcelain',
11+
'-b',
12+
'-u',
13+
'--null',
14+
...customArgs.filter(arg => !ignoredOptions.includes(arg))
15+
];
16+
617
return {
718
format: 'utf-8',
8-
commands: ['status', '--porcelain', '-b', '-u', ...customArgs],
19+
commands,
920
parser(text: string) {
1021
return parseStatusSummary(text);
1122
}

Diff for: simple-git/test/unit/__fixtures__/responses/status.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createFixture } from '../create-fixture';
2+
import { NULL } from '../../../../src/lib/utils';
23

34
export function stagedRenamed(from = 'from.ext', to = 'to.ext', workingDir = ' ') {
45
return `R${workingDir} ${from} -> ${to}`;
@@ -30,6 +31,6 @@ export function statusResponse(branch = 'main', ...files: Array<string | (() =>
3031
...files.map(file => typeof file === 'function' ? file() : file),
3132
];
3233

33-
return createFixture(stdOut.join('\n'), '');
34+
return createFixture(stdOut.join(NULL), '');
3435

3536
}

Diff for: simple-git/test/unit/status.spec.ts

+56-60
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ import {
1717
} from './__fixtures__';
1818
import { SimpleGit, StatusResult } from '../../typings';
1919
import { parseStatusSummary, StatusSummary } from '../../src/lib/responses/StatusSummary';
20+
import { NULL } from '../../src/lib/utils';
2021

2122
describe('status', () => {
2223
let git: SimpleGit;
2324
let callback: jest.Mock;
24-
let statusCommands = (...extras: string[]) => ['status', '--porcelain', '-b', '-u', ...extras];
25+
let statusCommands = (...extras: string[]) => ['status', '--porcelain', '-b', '-u', '--null', ...extras];
2526

2627
beforeEach(() => callback = jest.fn());
2728

@@ -62,6 +63,13 @@ describe('status', () => {
6263

6364
beforeEach(() => git = newSimpleGit());
6465

66+
it('ignores explicit --null option', async () => {
67+
git.status(['-a', '--null', '-b', '-z', '-c']);
68+
await closeWithSuccess();
69+
70+
assertExecutedCommands(...statusCommands('-a', '-b', '-c'));
71+
});
72+
6573
it('throws errors to the rejection handler', async () => {
6674
const queue = git.status();
6775
await closeWithError('unknown');
@@ -145,6 +153,20 @@ describe('status', () => {
145153
}))
146154
});
147155

156+
it('Handles files with non ascii names', () => {
157+
expect(parseStatusSummary(statusResponse('main', stagedModified('😀 file.ext')).stdOut)).toEqual(like({
158+
current: 'main',
159+
modified: ['😀 file.ext'],
160+
}));
161+
});
162+
163+
it('Handles files with spaces in their names', () => {
164+
expect(parseStatusSummary(statusResponse('main', stagedModified('foo bar.ext')).stdOut)).toEqual(like({
165+
current: 'main',
166+
modified: ['foo bar.ext'],
167+
}));
168+
});
169+
148170
it('Handles ignored files', () => {
149171
expect(parseStatusSummary(statusResponse('main', stagedIgnored).stdOut)).toEqual(like({
150172
...empty,
@@ -196,22 +218,18 @@ describe('status', () => {
196218
});
197219

198220
it('Initial repo with no commits', () => {
199-
const statusSummary = parseStatusSummary(`
200-
## No commits yet on master
201-
`);
221+
const statusSummary = parseStatusSummary(`## No commits yet on master`);
202222

203223
expect(statusSummary).toEqual(like({
204224
current: `master`
205225
}))
206226
});
207227

208228
it('Complex status - renamed, new and un-tracked modifications', () => {
209-
const statusSummary = parseStatusSummary(`
210-
## master
211-
M other.txt
212-
A src/b.txt
213-
R src/a.txt -> src/c.txt
214-
`);
229+
const statusSummary = parseStatusSummary(statusResponse('master',
230+
' M other.txt',
231+
'A src/b.txt',
232+
'R src/a.txt -> src/c.txt').stdOut);
215233

216234
expect(statusSummary).toEqual(like({
217235
created: ['src/b.txt'],
@@ -264,20 +282,23 @@ R src/a.txt -> src/c.txt
264282
}));
265283
});
266284

267-
it('parses status - with untracked', () => {
268-
expect(parseStatusSummary('?? Not tracked File\nUU Conflicted\n D Removed')).toEqual(like({
269-
not_added: ['Not tracked File'],
270-
conflicted: ['Conflicted'],
271-
deleted: ['Removed'],
272-
}));
273-
});
274-
275-
it('parses status - modified, added and added-changed', () => {
276-
expect(parseStatusSummary(' M Modified\n A Added\nAM Changed')).toEqual(like({
277-
modified: ['Modified', 'Changed'],
278-
created: ['Added', 'Changed'],
279-
}));
280-
});
285+
it.each<[string, any]>([
286+
['?? Not tracked File', { not_added: ['Not tracked File'] }],
287+
['UU Conflicted', { conflicted: ['Conflicted'] }],
288+
[' D Removed', { deleted: ['Removed'] }],
289+
[' M Modified', { modified: ['Modified'] }],
290+
[' A Added', { created: ['Added'] }],
291+
['AM Changed', { created: ['Changed'], modified: ['Changed'] }],
292+
])('parses file status - %s', (file, result) => {
293+
expect(parseStatusSummary(statusResponse('branch', file).stdOut)).toEqual(like({
294+
modified: [],
295+
created: [],
296+
not_added: [],
297+
conflicted: [],
298+
deleted: [],
299+
...result,
300+
}))
301+
})
281302

282303
it('parses status', () => {
283304
expect(parseStatusSummary(statusResponse('this_branch').stdOut)).toEqual(like({
@@ -291,7 +312,7 @@ R src/a.txt -> src/c.txt
291312
});
292313

293314
it('allows isClean to be destructured', () => {
294-
const { isClean } = parseStatusSummary('\n');
315+
const {isClean} = parseStatusSummary('\n');
295316
expect(isClean()).toBe(true);
296317
});
297318

@@ -309,41 +330,23 @@ R src/a.txt -> src/c.txt
309330
});
310331

311332
it('staged modified files identified separately to other modified files', () => {
312-
const statusSummary = parseStatusSummary(`
313-
## master
314-
M aaa
315-
M bbb
316-
A ccc
317-
?? ddd
318-
`);
333+
const statusSummary = parseStatusSummary(`## master${NULL} M aaa${NULL}M bbb${NULL}A ccc${NULL}?? ddd`);
319334
expect(statusSummary).toEqual(like({
320335
staged: ['bbb', 'ccc'],
321336
modified: ['aaa', 'bbb'],
322337
}));
323338
});
324339

325340
it('staged modified file with modifications after staging', () => {
326-
const statusSummary = parseStatusSummary(`
327-
## master
328-
MM staged-modified
329-
M modified
330-
M staged
331-
`);
341+
const statusSummary = parseStatusSummary(`## master${NULL}MM staged-modified${NULL} M modified${NULL}M staged`);
332342
expect(statusSummary).toEqual(like({
333343
staged: ['staged-modified', 'staged'],
334344
modified: ['staged-modified', 'modified', 'staged'],
335345
}));
336346
});
337347

338348
it('modified status', () => {
339-
const statusSummary = parseStatusSummary(`
340-
M package.json
341-
M src/git.js
342-
AM src/index.js
343-
A src/newfile.js
344-
?? test
345-
UU test.js
346-
`);
349+
const statusSummary = parseStatusSummary(` M package.json${NULL}M src/git.js${NULL}AM src/index.js${NULL} A src/newfile.js${NULL}?? test${NULL}UU test.js`);
347350

348351
expect(statusSummary).toEqual(like({
349352
created: ['src/index.js', 'src/newfile.js'],
@@ -356,10 +359,8 @@ R src/a.txt -> src/c.txt
356359
});
357360

358361
it('index/wd status', () => {
359-
const statusSummary = parseStatusSummary(` M src/git_wd.js
360-
MM src/git_ind_wd.js
361-
M src/git_ind.js
362-
`);
362+
const statusSummary = parseStatusSummary(statusResponse('main',
363+
` M src/git_wd.js`, `MM src/git_ind_wd.js`, `M src/git_ind.js`).stdOut);
363364
expect(statusSummary).toEqual(like({
364365
files: [
365366
{path: 'src/git_wd.js', index: ' ', working_dir: 'M'},
@@ -370,21 +371,16 @@ M src/git_ind.js
370371
});
371372

372373
it('Report conflict when both sides have added the same file', () => {
373-
expect(parseStatusSummary(`## master\nAA filename`)).toEqual(like({
374+
expect(parseStatusSummary(statusResponse(`master`, `AA filename`).stdOut)).toEqual(like({
374375
conflicted: ['filename'],
375376
}));
376377
});
377378

378379
it('Report all types of merge conflict statuses', () => {
379-
const statusSummary = parseStatusSummary(`
380-
UU package.json
381-
DD src/git.js
382-
DU src/index.js
383-
UD src/newfile.js
384-
AU test.js
385-
UA test
386-
AA test-foo.js
387-
`);
380+
const statusSummary = parseStatusSummary(statusResponse('branch',
381+
'UU package.json', 'DD src/git.js', 'DU src/index.js',
382+
'UD src/newfile.js', 'AU test.js', 'UA test', 'AA test-foo.js'
383+
).stdOut);
388384

389385
expect(statusSummary).toEqual(like({
390386
conflicted: ['package.json', 'src/git.js', 'src/index.js', 'src/newfile.js', 'test.js', 'test', 'test-foo.js']

0 commit comments

Comments
 (0)