diff --git a/.changeset/silver-impalas-love.md b/.changeset/silver-impalas-love.md new file mode 100644 index 00000000..5b274c07 --- /dev/null +++ b/.changeset/silver-impalas-love.md @@ -0,0 +1,5 @@ +--- +'simple-git': minor +--- + +add status to DiffResult when using --name-status diff --git a/simple-git/src/lib/api.ts b/simple-git/src/lib/api.ts index a73e26d3..1ea784ee 100644 --- a/simple-git/src/lib/api.ts +++ b/simple-git/src/lib/api.ts @@ -7,12 +7,14 @@ import { TaskConfigurationError } from './errors/task-configuration-error'; import { CheckRepoActions } from './tasks/check-is-repo'; import { CleanOptions } from './tasks/clean'; import { GitConfigScope } from './tasks/config'; +import { DiffNameStatus } from './tasks/diff-name-status'; import { grepQueryBuilder } from './tasks/grep'; import { ResetMode } from './tasks/reset'; export { CheckRepoActions, CleanOptions, + DiffNameStatus, GitConfigScope, GitConstructError, GitError, diff --git a/simple-git/src/lib/parsers/parse-diff-summary.ts b/simple-git/src/lib/parsers/parse-diff-summary.ts index 5a9be692..45d1bb27 100644 --- a/simple-git/src/lib/parsers/parse-diff-summary.ts +++ b/simple-git/src/lib/parsers/parse-diff-summary.ts @@ -1,7 +1,8 @@ import { DiffResult } from '../../../typings'; import { LogFormat } from '../args/log-format'; import { DiffSummary } from '../responses/DiffSummary'; -import { asNumber, LineParser, parseStringResponse } from '../utils'; +import { isDiffNameStatus } from '../tasks/diff-name-status'; +import { asNumber, LineParser, orVoid, parseStringResponse } from '../utils'; const statParser = [ new LineParser( @@ -86,16 +87,20 @@ const nameOnlyParser = [ ]; const nameStatusParser = [ - new LineParser(/([ACDMRTUXB])\s*(.+)$/, (result, [_status, file]) => { - result.changed++; - result.files.push({ - file, - changes: 0, - insertions: 0, - deletions: 0, - binary: false, - }); - }), + new LineParser( + /([ACDMRTUXB])([0-9]{0,3})\t(.[^\t]*)(\t(.[^\t]*))?$/, + (result, [status, _similarity, from, _to, to]) => { + result.changed++; + result.files.push({ + file: to ?? from, + changes: 0, + status: orVoid(isDiffNameStatus(status) && status), + insertions: 0, + deletions: 0, + binary: false, + }); + } + ), ]; const diffSummaryParsers: Record[]> = { diff --git a/simple-git/src/lib/tasks/diff-name-status.ts b/simple-git/src/lib/tasks/diff-name-status.ts new file mode 100644 index 00000000..faed3b3e --- /dev/null +++ b/simple-git/src/lib/tasks/diff-name-status.ts @@ -0,0 +1,17 @@ +export enum DiffNameStatus { + ADDED = 'A', + COPIED = 'C', + DELETED = 'D', + MODIFIED = 'M', + RENAMED = 'R', + CHANGED = 'T', + UNMERGED = 'U', + UNKNOWN = 'X', + BROKEN = 'B', +} + +const diffNameStatus = new Set(Object.values(DiffNameStatus)); + +export function isDiffNameStatus(input: string): input is DiffNameStatus { + return diffNameStatus.has(input as DiffNameStatus); +} diff --git a/simple-git/src/lib/utils/util.ts b/simple-git/src/lib/utils/util.ts index 58b462e9..2cb8c841 100644 --- a/simple-git/src/lib/utils/util.ts +++ b/simple-git/src/lib/utils/util.ts @@ -157,3 +157,10 @@ export function pick(source: Record, properties: string[]) { export function delay(duration = 0): Promise { return new Promise((done) => setTimeout(done, duration)); } + +export function orVoid(input: T | false) { + if (input === false) { + return undefined; + } + return input; +} diff --git a/simple-git/test/integration/log-name-status.spec.ts b/simple-git/test/integration/log-name-status.spec.ts new file mode 100644 index 00000000..8b8f410f --- /dev/null +++ b/simple-git/test/integration/log-name-status.spec.ts @@ -0,0 +1,50 @@ +import { + createTestContext, + like, + newSimpleGit, + setUpFilesAdded, + setUpInit, + SimpleGitTestContext, +} from '@simple-git/test-utils'; + +import { DiffNameStatus, DiffResultTextFile } from '../..'; + +describe('log-name-status', function () { + let context: SimpleGitTestContext; + const steps = ['mv a b', 'commit -m two']; + + beforeEach(async () => { + context = await createTestContext(); + await setUpInit(context); + await setUpFilesAdded(context, ['a'], '.', 'one'); + for (const step of steps) { + await context.git.raw(step.split(' ')); + } + }); + + it('detects files moved with --name-status', async () => { + const actual = await newSimpleGit(context.root).log(['--name-status']); + + expect(actual.all).toEqual([ + mockListLogLine('two', { b: DiffNameStatus.RENAMED }), + mockListLogLine('one', { a: DiffNameStatus.ADDED }), + ]); + }); +}); + +function mockListLogLine(message: string, changes: Record) { + const files: DiffResultTextFile[] = Object.entries(changes).map(([file, status]) => { + return { + binary: false, + changes: 0, + deletions: 0, + file, + insertions: 0, + status, + }; + }); + return like({ + message, + diff: like({ changed: files.length, deletions: 0, insertions: 0, files }), + }); +} diff --git a/simple-git/test/unit/diff.spec.ts b/simple-git/test/unit/diff.spec.ts index b6279125..b56cd081 100644 --- a/simple-git/test/unit/diff.spec.ts +++ b/simple-git/test/unit/diff.spec.ts @@ -314,12 +314,12 @@ describe('diff', () => { it('diffSummary with --name-status', async () => { const task = git.diffSummary(['--name-status']); - await closeWithSuccess(`M ${file}`); + await closeWithSuccess(`M\t${file}\nR100\tfrom\tto`); assertExecutedCommands('diff', '--name-status'); expect(await task).toEqual( like({ - changed: 1, + changed: 2, deletions: 0, insertions: 0, files: [ @@ -328,6 +328,15 @@ describe('diff', () => { changes: 0, insertions: 0, deletions: 0, + status: 'M', + binary: false, + }, + { + file: 'to', + changes: 0, + insertions: 0, + deletions: 0, + status: 'R', binary: false, }, ], diff --git a/simple-git/test/unit/utils.spec.ts b/simple-git/test/unit/utils.spec.ts index 74e40cf4..f57f35b4 100644 --- a/simple-git/test/unit/utils.spec.ts +++ b/simple-git/test/unit/utils.spec.ts @@ -11,10 +11,24 @@ import { including, last, NOOP, + orVoid, toLinesWithContent, } from '../../src/lib/utils'; describe('utils', () => { + describe('orVoid', () => { + it.each([[null], [true], [''], ['non empty string'], [[]], [{}], [0], [1]])( + 'passes through %s', + (item) => { + expect(orVoid(item)).toBe(item); + } + ); + + it.each([[false], [undefined]])('removes %s', (item) => { + expect(orVoid(item)).toBe(undefined); + }); + }); + describe('array edges', () => { it.each<[string, any, string | number | undefined, string | undefined]>([ ['string array', ['abc', 'def'], 'abc', 'def'], diff --git a/simple-git/typings/response.d.ts b/simple-git/typings/response.d.ts index 638f602c..04f825e7 100644 --- a/simple-git/typings/response.d.ts +++ b/simple-git/typings/response.d.ts @@ -1,4 +1,5 @@ -import { DefaultLogFields } from '../src/lib/tasks/log'; +import type { DiffNameStatus } from '../src/lib/tasks/diff-name-status'; +import type { DefaultLogFields } from '../src/lib/tasks/log'; export interface BranchSummaryBranch { current: boolean; @@ -141,6 +142,9 @@ export interface DiffResultTextFile { insertions: number; deletions: number; binary: false; + + /** `--name-status` argument needed */ + status?: DiffNameStatus; } export interface DiffResultBinaryFile { @@ -148,6 +152,9 @@ export interface DiffResultBinaryFile { before: number; after: number; binary: true; + + /** `--name-status` argument needed */ + status?: string; } export interface DiffResult { diff --git a/simple-git/typings/types.d.ts b/simple-git/typings/types.d.ts index f294c82d..8e5457d6 100644 --- a/simple-git/typings/types.d.ts +++ b/simple-git/typings/types.d.ts @@ -16,6 +16,7 @@ export { CheckRepoActions } from '../src/lib/tasks/check-is-repo'; export { CleanOptions, CleanMode } from '../src/lib/tasks/clean'; export type { CloneOptions } from '../src/lib/tasks/clone'; export { GitConfigScope } from '../src/lib/tasks/config'; +export { DiffNameStatus } from '../src/lib/tasks/diff-name-status'; export { GitGrepQuery, grepQueryBuilder } from '../src/lib/tasks/grep'; export { ResetOptions, ResetMode } from '../src/lib/tasks/reset'; export type { VersionResult } from '../src/lib/tasks/version';