Skip to content

Commit

Permalink
feat: StatusResult returned by git.status() should include `detac…
Browse files Browse the repository at this point in the history
…hed` state of the working copy. (#695)
  • Loading branch information
steveukx committed Dec 1, 2021
1 parent 617b87e commit f464ebe
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 57 deletions.
58 changes: 18 additions & 40 deletions 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;
}
Expand Down Expand Up @@ -75,15 +51,15 @@ 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];
}

function conflicts(indexX: PorcelainFileStatus, ...indexY: PorcelainFileStatus[]) {
return indexY.map(y => parser(indexX, y, (result, file) => append(result.conflicted, file)));
}

const parsers: Map<string, (result: StatusSummary, file: string) => unknown> = new Map([
const parsers: Map<string, StatusLineParser> = 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)),
Expand Down Expand Up @@ -134,6 +110,8 @@ const parsers: Map<string, (result: StatusSummary, file: string) => unknown> = n

regexResult = onEmptyBranchReg.exec(line);
result.current = regexResult && regexResult[1] || result.current;

result.detached = /\(no branch\)/.test(line);
}]
]);

Expand Down
25 changes: 24 additions & 1 deletion 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;
Expand Down Expand Up @@ -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,
}))
})

});
34 changes: 18 additions & 16 deletions test/unit/status.spec.ts
Expand Up @@ -191,7 +191,7 @@ describe('status', () => {
## No commits yet on master
`);

expect(statusSummary).toEqual(expect.objectContaining({
expect(statusSummary).toEqual(like({
current: `master`
}))
});
Expand All @@ -204,21 +204,21 @@ 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'}]
}));
});

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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -254,22 +256,22 @@ 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'],
}));
});

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,
}));
Expand All @@ -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: [],
Expand All @@ -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'],
}));
Expand All @@ -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'],
}));
Expand Down Expand Up @@ -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'},
Expand All @@ -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'],
}));
});
Expand All @@ -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']
}));
});
Expand Down
27 changes: 27 additions & 0 deletions typings/response.d.ts
Expand Up @@ -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.
*/
Expand Down

0 comments on commit f464ebe

Please sign in to comment.