From f464ebe567c6c5cd4e99fd7e6300d9efdd4cbb1b Mon Sep 17 00:00:00 2001 From: Steve King Date: Wed, 1 Dec 2021 20:10:43 +0000 Subject: [PATCH] feat: `StatusResult` returned by `git.status()` should include `detached` state of the working copy. (#695) --- src/lib/responses/StatusSummary.ts | 58 ++++++++++-------------------- test/integration/status.spec.ts | 25 ++++++++++++- test/unit/status.spec.ts | 34 +++++++++--------- typings/response.d.ts | 27 ++++++++++++++ 4 files changed, 87 insertions(+), 57 deletions(-) diff --git a/src/lib/responses/StatusSummary.ts b/src/lib/responses/StatusSummary.ts index f0bf1f57..4f062349 100644 --- a/src/lib/responses/StatusSummary.ts +++ b/src/lib/responses/StatusSummary.ts @@ -1,48 +1,24 @@ -import { FileStatusResult, StatusResult, StatusResultRenamed } from '../../../typings'; +import { StatusResult } from '../../../typings'; import { append } from '../utils'; import { FileStatusSummary } from './FileStatusSummary'; -/** - * The StatusSummary is returned as a response to getting `git().status()` - */ +type StatusLineParser = (result: StatusResult, file: string) => void; + export class StatusSummary implements StatusResult { - public not_added: string[] = []; - public conflicted: string[] = []; - public created: string[] = []; - public deleted: string[] = []; - public modified: string[] = []; - public renamed: StatusResultRenamed[] = []; - - /** - * All files represented as an array of objects containing the `path` and status in `index` and - * in the `working_dir`. - */ - public files: FileStatusResult[] = []; - public staged: string[] = []; - - /** - * Number of commits ahead of the tracked branch - */ + public not_added = []; + public conflicted = []; + public created = []; + public deleted = []; + public modified = []; + public renamed = []; + public files = []; + public staged = []; public ahead = 0; - - /** - *Number of commits behind the tracked branch - */ public behind = 0; + public current = null; + public tracking = null; + public detached = false; - /** - * Name of the current branch - */ - public current: string | null = null; - - /** - * Name of the branch being tracked - */ - public tracking: string | null = null; - - /** - * Gets whether this StatusSummary represents a clean working branch. - */ public isClean(): boolean { return !this.files.length; } @@ -75,7 +51,7 @@ function renamedFile(line: string) { }; } -function parser(indexX: PorcelainFileStatus, indexY: PorcelainFileStatus, handler: (result: StatusSummary, file: string) => void): [string, (result: StatusSummary, file: string) => unknown] { +function parser(indexX: PorcelainFileStatus, indexY: PorcelainFileStatus, handler: StatusLineParser): [string, StatusLineParser] { return [`${indexX}${indexY}`, handler]; } @@ -83,7 +59,7 @@ function conflicts(indexX: PorcelainFileStatus, ...indexY: PorcelainFileStatus[] return indexY.map(y => parser(indexX, y, (result, file) => append(result.conflicted, file))); } -const parsers: Map unknown> = new Map([ +const parsers: Map = new Map([ parser(PorcelainFileStatus.NONE, PorcelainFileStatus.ADDED, (result, file) => append(result.created, file)), parser(PorcelainFileStatus.NONE, PorcelainFileStatus.DELETED, (result, file) => append(result.deleted, file)), parser(PorcelainFileStatus.NONE, PorcelainFileStatus.MODIFIED, (result, file) => append(result.modified, file)), @@ -134,6 +110,8 @@ const parsers: Map unknown> = n regexResult = onEmptyBranchReg.exec(line); result.current = regexResult && regexResult[1] || result.current; + + result.detached = /\(no branch\)/.test(line); }] ]); diff --git a/test/integration/status.spec.ts b/test/integration/status.spec.ts index 9f49d33e..4f5f24c5 100644 --- a/test/integration/status.spec.ts +++ b/test/integration/status.spec.ts @@ -1,4 +1,11 @@ -import { createTestContext, newSimpleGit, setUpFilesAdded, setUpInit, SimpleGitTestContext } from '../__fixtures__'; +import { + createTestContext, + like, + newSimpleGit, + setUpFilesAdded, + setUpInit, + SimpleGitTestContext +} from '../__fixtures__'; describe('status', () => { let context: SimpleGitTestContext; @@ -39,4 +46,20 @@ describe('status', () => { expect(status.not_added).toEqual(['dirty-dir/dirty']); }); + it('detached head', async () => { + const git = newSimpleGit(context.root); + expect(await git.status()).toEqual(like({ + detached: false, + current: expect.any(String), + })); + + await git.raw('tag', 'v1'); + await git.raw('checkout', 'v1'); + + expect(await git.status()).toEqual(like({ + current: 'HEAD', + detached: true, + })) + }) + }); diff --git a/test/unit/status.spec.ts b/test/unit/status.spec.ts index 37f75354..68bb0e2c 100644 --- a/test/unit/status.spec.ts +++ b/test/unit/status.spec.ts @@ -191,7 +191,7 @@ describe('status', () => { ## No commits yet on master `); - expect(statusSummary).toEqual(expect.objectContaining({ + expect(statusSummary).toEqual(like({ current: `master` })) }); @@ -204,7 +204,7 @@ A src/b.txt R src/a.txt -> src/c.txt `); - expect(statusSummary).toEqual(expect.objectContaining({ + expect(statusSummary).toEqual(like({ created: ['src/b.txt'], modified: ['other.txt'], renamed: [{from: 'src/a.txt', to: 'src/c.txt'}] @@ -212,13 +212,13 @@ R src/a.txt -> src/c.txt }); it('Handles renamed', () => { - expect(parseStatusSummary(' R src/file.js -> src/another-file.js')).toEqual(expect.objectContaining({ + expect(parseStatusSummary(' R src/file.js -> src/another-file.js')).toEqual(like({ renamed: [{from: 'src/file.js', to: 'src/another-file.js'}], })); }); it('parses status - current, tracking and ahead', () => { - expect(parseStatusSummary('## master...origin/master [ahead 3]')).toEqual(expect.objectContaining({ + expect(parseStatusSummary('## master...origin/master [ahead 3]')).toEqual(like({ current: 'master', tracking: 'origin/master', ahead: 3, @@ -227,7 +227,8 @@ R src/a.txt -> src/c.txt }); it('parses status - current, tracking and behind', () => { - expect(parseStatusSummary('## master...origin/master [behind 2]')).toEqual(expect.objectContaining({ + expect(parseStatusSummary('## master...origin/master [behind 2]')).toEqual(like({ + detached: false, current: 'master', tracking: 'origin/master', ahead: 0, @@ -236,7 +237,7 @@ R src/a.txt -> src/c.txt }); it('parses status - current, tracking', () => { - expect(parseStatusSummary('## release/0.34.0...origin/release/0.34.0')).toEqual(expect.objectContaining({ + expect(parseStatusSummary('## release/0.34.0...origin/release/0.34.0')).toEqual(like({ current: 'release/0.34.0', tracking: 'origin/release/0.34.0', ahead: 0, @@ -245,7 +246,8 @@ R src/a.txt -> src/c.txt }); it('parses status - HEAD no branch', () => { - expect(parseStatusSummary('## HEAD (no branch)')).toEqual(expect.objectContaining({ + expect(parseStatusSummary('## HEAD (no branch)')).toEqual(like({ + detached: true, current: 'HEAD', tracking: null, ahead: 0, @@ -254,7 +256,7 @@ R src/a.txt -> src/c.txt }); it('parses status - with untracked', () => { - expect(parseStatusSummary('?? Not tracked File\nUU Conflicted\n D Removed')).toEqual(expect.objectContaining({ + expect(parseStatusSummary('?? Not tracked File\nUU Conflicted\n D Removed')).toEqual(like({ not_added: ['Not tracked File'], conflicted: ['Conflicted'], deleted: ['Removed'], @@ -262,14 +264,14 @@ R src/a.txt -> src/c.txt }); it('parses status - modified, added and added-changed', () => { - expect(parseStatusSummary(' M Modified\n A Added\nAM Changed')).toEqual(expect.objectContaining({ + expect(parseStatusSummary(' M Modified\n A Added\nAM Changed')).toEqual(like({ modified: ['Modified', 'Changed'], created: ['Added', 'Changed'], })); }); it('parses status', () => { - expect(parseStatusSummary(statusResponse('this_branch').stdOut)).toEqual(expect.objectContaining({ + expect(parseStatusSummary(statusResponse('this_branch').stdOut)).toEqual(like({ current: 'this_branch', tracking: null, })); @@ -283,7 +285,7 @@ R src/a.txt -> src/c.txt const statusSummary = parseStatusSummary('\n'); expect(statusSummary.isClean()).toBe(true); - expect(statusSummary).toEqual(expect.objectContaining({ + expect(statusSummary).toEqual(like({ created: [], deleted: [], modified: [], @@ -300,7 +302,7 @@ R src/a.txt -> src/c.txt A ccc ?? ddd `); - expect(statusSummary).toEqual(expect.objectContaining({ + expect(statusSummary).toEqual(like({ staged: ['bbb', 'ccc'], modified: ['aaa', 'bbb'], })); @@ -313,7 +315,7 @@ R src/a.txt -> src/c.txt M modified M staged `); - expect(statusSummary).toEqual(expect.objectContaining({ + expect(statusSummary).toEqual(like({ staged: ['staged-modified', 'staged'], modified: ['staged-modified', 'modified', 'staged'], })); @@ -344,7 +346,7 @@ R src/a.txt -> src/c.txt MM src/git_ind_wd.js M src/git_ind.js `); - expect(statusSummary).toEqual(expect.objectContaining({ + expect(statusSummary).toEqual(like({ files: [ {path: 'src/git_wd.js', index: ' ', working_dir: 'M'}, {path: 'src/git_ind_wd.js', index: 'M', working_dir: 'M'}, @@ -354,7 +356,7 @@ M src/git_ind.js }); it('Report conflict when both sides have added the same file', () => { - expect(parseStatusSummary(`## master\nAA filename`)).toEqual(expect.objectContaining({ + expect(parseStatusSummary(`## master\nAA filename`)).toEqual(like({ conflicted: ['filename'], })); }); @@ -370,7 +372,7 @@ M src/git_ind.js AA test-foo.js `); - expect(statusSummary).toEqual(expect.objectContaining({ + expect(statusSummary).toEqual(like({ conflicted: ['package.json', 'src/git.js', 'src/index.js', 'src/newfile.js', 'test.js', 'test', 'test-foo.js'] })); }); diff --git a/typings/response.d.ts b/typings/response.d.ts index 27c2efbc..d115eeed 100644 --- a/typings/response.d.ts +++ b/typings/response.d.ts @@ -294,12 +294,39 @@ export interface StatusResult { modified: string[]; renamed: StatusResultRenamed[]; staged: string[]; + + /** + * All files represented as an array of objects containing the `path` and status in `index` and + * in the `working_dir`. + */ files: FileStatusResult[]; + + /** + * Number of commits ahead of the tracked branch + */ ahead: number; + + /** + *Number of commits behind the tracked branch + */ behind: number; + + /** + * Name of the current branch + */ current: string | null; + + /** + * Name of the branch being tracked + */ tracking: string | null; + /** + * Detached status of the working copy, for more detail of what the working branch + * is detached from use `git.branch()` + */ + detached: boolean; + /** * Gets whether this represents a clean working branch. */