@camaradesuk/git-worktree-tools 1.4.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -11
- package/dist/api/list.d.ts +10 -4
- package/dist/api/list.d.ts.map +1 -1
- package/dist/api/list.js +5 -1
- package/dist/api/list.js.map +1 -1
- package/dist/api/list.test.d.ts +5 -0
- package/dist/api/list.test.d.ts.map +1 -0
- package/dist/api/list.test.js +390 -0
- package/dist/api/list.test.js.map +1 -0
- package/dist/cli/cleanpr.js +35 -4
- package/dist/cli/cleanpr.js.map +1 -1
- package/dist/cli/cleanpr.test.js +254 -0
- package/dist/cli/cleanpr.test.js.map +1 -1
- package/dist/cli/lswt.js +54 -6
- package/dist/cli/lswt.js.map +1 -1
- package/dist/cli/lswt.test.js +207 -0
- package/dist/cli/lswt.test.js.map +1 -1
- package/dist/cli/newpr.js +94 -66
- package/dist/cli/newpr.js.map +1 -1
- package/dist/cli/newpr.test.js +10 -9
- package/dist/cli/newpr.test.js.map +1 -1
- package/dist/cli/wt/clean.d.ts +16 -0
- package/dist/cli/wt/clean.d.ts.map +1 -0
- package/dist/cli/wt/clean.js +64 -0
- package/dist/cli/wt/clean.js.map +1 -0
- package/dist/cli/wt/completion.d.ts +12 -0
- package/dist/cli/wt/completion.d.ts.map +1 -0
- package/dist/cli/wt/completion.js +246 -0
- package/dist/cli/wt/completion.js.map +1 -0
- package/dist/cli/wt/completion.test.d.ts +5 -0
- package/dist/cli/wt/completion.test.d.ts.map +1 -0
- package/dist/cli/wt/completion.test.js +173 -0
- package/dist/cli/wt/completion.test.js.map +1 -0
- package/dist/cli/wt/config.d.ts +13 -0
- package/dist/cli/wt/config.d.ts.map +1 -0
- package/dist/cli/wt/config.js +40 -0
- package/dist/cli/wt/config.js.map +1 -0
- package/dist/cli/wt/entry.test.d.ts +8 -0
- package/dist/cli/wt/entry.test.d.ts.map +1 -0
- package/dist/cli/wt/entry.test.js +198 -0
- package/dist/cli/wt/entry.test.js.map +1 -0
- package/dist/cli/wt/link.d.ts +13 -0
- package/dist/cli/wt/link.d.ts.map +1 -0
- package/dist/cli/wt/link.js +88 -0
- package/dist/cli/wt/link.js.map +1 -0
- package/dist/cli/wt/list.d.ts +16 -0
- package/dist/cli/wt/list.d.ts.map +1 -0
- package/dist/cli/wt/list.js +65 -0
- package/dist/cli/wt/list.js.map +1 -0
- package/dist/cli/wt/new.d.ts +18 -0
- package/dist/cli/wt/new.d.ts.map +1 -0
- package/dist/cli/wt/new.js +78 -0
- package/dist/cli/wt/new.js.map +1 -0
- package/dist/cli/wt/run-command.d.ts +31 -0
- package/dist/cli/wt/run-command.d.ts.map +1 -0
- package/dist/cli/wt/run-command.js +49 -0
- package/dist/cli/wt/run-command.js.map +1 -0
- package/dist/cli/wt/run-command.test.d.ts +5 -0
- package/dist/cli/wt/run-command.test.d.ts.map +1 -0
- package/dist/cli/wt/run-command.test.js +88 -0
- package/dist/cli/wt/run-command.test.js.map +1 -0
- package/dist/cli/wt/state.d.ts +13 -0
- package/dist/cli/wt/state.d.ts.map +1 -0
- package/dist/cli/wt/state.js +38 -0
- package/dist/cli/wt/state.js.map +1 -0
- package/dist/cli/wt/wt.test.d.ts +8 -0
- package/dist/cli/wt/wt.test.d.ts.map +1 -0
- package/dist/cli/wt/wt.test.js +378 -0
- package/dist/cli/wt/wt.test.js.map +1 -0
- package/dist/cli/wt.d.ts +25 -0
- package/dist/cli/wt.d.ts.map +1 -0
- package/dist/cli/wt.js +74 -0
- package/dist/cli/wt.js.map +1 -0
- package/dist/cli/wtconfig.js +4 -4
- package/dist/cli/wtconfig.js.map +1 -1
- package/dist/cli/wtlink.js +66 -9
- package/dist/cli/wtlink.js.map +1 -1
- package/dist/cli/wtlink.test.js +101 -0
- package/dist/cli/wtlink.test.js.map +1 -1
- package/dist/e2e/cli.e2e.test.js +156 -1
- package/dist/e2e/cli.e2e.test.js.map +1 -1
- package/dist/e2e/lswt/lswt.e2e.test.js +33 -0
- package/dist/e2e/lswt/lswt.e2e.test.js.map +1 -1
- package/dist/e2e/newpr-full-flow.e2e.test.d.ts +2 -0
- package/dist/e2e/newpr-full-flow.e2e.test.d.ts.map +1 -0
- package/dist/e2e/newpr-full-flow.e2e.test.js +279 -0
- package/dist/e2e/newpr-full-flow.e2e.test.js.map +1 -0
- package/dist/e2e/wtlink/wtlink.e2e.test.js +52 -0
- package/dist/e2e/wtlink/wtlink.e2e.test.js.map +1 -1
- package/dist/integration/lswt-remote-pr.integration.test.d.ts +2 -0
- package/dist/integration/lswt-remote-pr.integration.test.d.ts.map +1 -0
- package/dist/integration/lswt-remote-pr.integration.test.js +222 -0
- package/dist/integration/lswt-remote-pr.integration.test.js.map +1 -0
- package/dist/integration/newpr-branchfrom-head.integration.test.d.ts +2 -0
- package/dist/integration/newpr-branchfrom-head.integration.test.d.ts.map +1 -0
- package/dist/integration/newpr-branchfrom-head.integration.test.js +498 -0
- package/dist/integration/newpr-branchfrom-head.integration.test.js.map +1 -0
- package/dist/lib/git.d.ts +1 -0
- package/dist/lib/git.d.ts.map +1 -1
- package/dist/lib/git.js +17 -30
- package/dist/lib/git.js.map +1 -1
- package/dist/lib/git.test.js +154 -123
- package/dist/lib/git.test.js.map +1 -1
- package/dist/lib/github.d.ts +45 -0
- package/dist/lib/github.d.ts.map +1 -1
- package/dist/lib/github.js +172 -0
- package/dist/lib/github.js.map +1 -1
- package/dist/lib/github.test.js +127 -1
- package/dist/lib/github.test.js.map +1 -1
- package/dist/lib/json-output.d.ts +11 -1
- package/dist/lib/json-output.d.ts.map +1 -1
- package/dist/lib/json-output.js +42 -1
- package/dist/lib/json-output.js.map +1 -1
- package/dist/lib/json-output.test.js +2 -0
- package/dist/lib/json-output.test.js.map +1 -1
- package/dist/lib/lswt/action-executors.d.ts.map +1 -1
- package/dist/lib/lswt/action-executors.js +143 -35
- package/dist/lib/lswt/action-executors.js.map +1 -1
- package/dist/lib/lswt/action-executors.test.js +362 -0
- package/dist/lib/lswt/action-executors.test.js.map +1 -1
- package/dist/lib/lswt/actions.d.ts.map +1 -1
- package/dist/lib/lswt/actions.js +38 -0
- package/dist/lib/lswt/actions.js.map +1 -1
- package/dist/lib/lswt/actions.test.js +126 -0
- package/dist/lib/lswt/actions.test.js.map +1 -1
- package/dist/lib/lswt/environment.d.ts +4 -0
- package/dist/lib/lswt/environment.d.ts.map +1 -1
- package/dist/lib/lswt/environment.js +23 -0
- package/dist/lib/lswt/environment.js.map +1 -1
- package/dist/lib/lswt/environment.test.js +129 -2
- package/dist/lib/lswt/environment.test.js.map +1 -1
- package/dist/lib/lswt/formatters.d.ts +2 -1
- package/dist/lib/lswt/formatters.d.ts.map +1 -1
- package/dist/lib/lswt/formatters.js +27 -2
- package/dist/lib/lswt/formatters.js.map +1 -1
- package/dist/lib/lswt/formatters.test.js +66 -2
- package/dist/lib/lswt/formatters.test.js.map +1 -1
- package/dist/lib/lswt/fuzzy-search.d.ts +27 -0
- package/dist/lib/lswt/fuzzy-search.d.ts.map +1 -0
- package/dist/lib/lswt/fuzzy-search.js +130 -0
- package/dist/lib/lswt/fuzzy-search.js.map +1 -0
- package/dist/lib/lswt/fuzzy-search.test.d.ts +5 -0
- package/dist/lib/lswt/fuzzy-search.test.d.ts.map +1 -0
- package/dist/lib/lswt/fuzzy-search.test.js +207 -0
- package/dist/lib/lswt/fuzzy-search.test.js.map +1 -0
- package/dist/lib/lswt/index.d.ts +3 -1
- package/dist/lib/lswt/index.d.ts.map +1 -1
- package/dist/lib/lswt/index.js +3 -2
- package/dist/lib/lswt/index.js.map +1 -1
- package/dist/lib/lswt/interactive.d.ts +50 -4
- package/dist/lib/lswt/interactive.d.ts.map +1 -1
- package/dist/lib/lswt/interactive.js +458 -56
- package/dist/lib/lswt/interactive.js.map +1 -1
- package/dist/lib/lswt/interactive.test.js +454 -66
- package/dist/lib/lswt/interactive.test.js.map +1 -1
- package/dist/lib/lswt/types.d.ts +8 -2
- package/dist/lib/lswt/types.d.ts.map +1 -1
- package/dist/lib/lswt/worktree-info.d.ts +11 -0
- package/dist/lib/lswt/worktree-info.d.ts.map +1 -1
- package/dist/lib/lswt/worktree-info.js +48 -0
- package/dist/lib/lswt/worktree-info.js.map +1 -1
- package/dist/lib/lswt/worktree-info.test.js +169 -0
- package/dist/lib/lswt/worktree-info.test.js.map +1 -1
- package/dist/lib/newpr/action-deps.test.d.ts +5 -0
- package/dist/lib/newpr/action-deps.test.d.ts.map +1 -0
- package/dist/lib/newpr/action-deps.test.js +111 -0
- package/dist/lib/newpr/action-deps.test.js.map +1 -0
- package/dist/lib/newpr/args.d.ts.map +1 -1
- package/dist/lib/newpr/args.js +6 -2
- package/dist/lib/newpr/args.js.map +1 -1
- package/dist/lib/newpr/args.test.js +209 -1
- package/dist/lib/newpr/args.test.js.map +1 -1
- package/dist/lib/newpr/scenario-handler.d.ts.map +1 -1
- package/dist/lib/newpr/scenario-handler.js +14 -5
- package/dist/lib/newpr/scenario-handler.js.map +1 -1
- package/dist/lib/newpr/scenario-handler.test.js +6 -0
- package/dist/lib/newpr/scenario-handler.test.js.map +1 -1
- package/dist/lib/prompts.d.ts +4 -0
- package/dist/lib/prompts.d.ts.map +1 -1
- package/dist/lib/prompts.js +178 -1
- package/dist/lib/prompts.js.map +1 -1
- package/dist/lib/prompts.test.js +279 -0
- package/dist/lib/prompts.test.js.map +1 -1
- package/dist/lib/wtlink/link-configs.test.js +282 -2
- package/dist/lib/wtlink/link-configs.test.js.map +1 -1
- package/dist/lib/wtlink/main-menu.js +1 -0
- package/dist/lib/wtlink/main-menu.js.map +1 -1
- package/dist/lib/wtlink/main-menu.test.d.ts +5 -0
- package/dist/lib/wtlink/main-menu.test.d.ts.map +1 -0
- package/dist/lib/wtlink/main-menu.test.js +124 -0
- package/dist/lib/wtlink/main-menu.test.js.map +1 -0
- package/dist/lib/wtlink/manage-manifest.d.ts +5 -0
- package/dist/lib/wtlink/manage-manifest.d.ts.map +1 -1
- package/dist/lib/wtlink/manage-manifest.js +65 -2
- package/dist/lib/wtlink/manage-manifest.js.map +1 -1
- package/dist/lib/wtlink/manage-manifest.test.js +144 -2
- package/dist/lib/wtlink/manage-manifest.test.js.map +1 -1
- package/dist/mcp/server.test.js +49 -0
- package/dist/mcp/server.test.js.map +1 -1
- package/package.json +2 -1
package/dist/lib/git.test.js
CHANGED
|
@@ -1,179 +1,219 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { execSync } from 'child_process';
|
|
2
|
+
import { spawnSync, execSync } from 'child_process';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as git from './git.js';
|
|
5
5
|
// Mock child_process
|
|
6
6
|
vi.mock('child_process', () => ({
|
|
7
|
+
spawnSync: vi.fn(),
|
|
7
8
|
execSync: vi.fn(),
|
|
8
9
|
}));
|
|
10
|
+
const mockSpawnSync = vi.mocked(spawnSync);
|
|
9
11
|
const mockExecSync = vi.mocked(execSync);
|
|
12
|
+
/**
|
|
13
|
+
* Helper to create a successful spawnSync result
|
|
14
|
+
*/
|
|
15
|
+
function mockSpawnSuccess(stdout) {
|
|
16
|
+
return {
|
|
17
|
+
status: 0,
|
|
18
|
+
signal: null,
|
|
19
|
+
output: ['', stdout, ''],
|
|
20
|
+
pid: 123,
|
|
21
|
+
stdout,
|
|
22
|
+
stderr: '',
|
|
23
|
+
error: undefined,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Helper to create a failed spawnSync result
|
|
28
|
+
*/
|
|
29
|
+
function mockSpawnFailure(stderr) {
|
|
30
|
+
return {
|
|
31
|
+
status: 1,
|
|
32
|
+
signal: null,
|
|
33
|
+
output: ['', '', stderr],
|
|
34
|
+
pid: 123,
|
|
35
|
+
stdout: '',
|
|
36
|
+
stderr,
|
|
37
|
+
error: undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
10
40
|
describe('git', () => {
|
|
11
41
|
beforeEach(() => {
|
|
12
42
|
vi.clearAllMocks();
|
|
13
43
|
});
|
|
14
44
|
describe('exec', () => {
|
|
15
45
|
it('executes git command and returns output with trailing whitespace trimmed', () => {
|
|
16
|
-
|
|
46
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('output with trailing whitespace \n'));
|
|
17
47
|
const result = git.exec(['status']);
|
|
18
48
|
// Leading whitespace is preserved (important for git status), trailing is trimmed
|
|
19
49
|
expect(result).toBe('output with trailing whitespace');
|
|
20
|
-
expect(
|
|
50
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['status'], expect.any(Object));
|
|
21
51
|
});
|
|
22
52
|
it('preserves leading whitespace in output', () => {
|
|
23
53
|
// Leading spaces are significant in git status --porcelain output
|
|
24
|
-
|
|
54
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(' M file.txt\n'));
|
|
25
55
|
const result = git.exec(['status', '--porcelain']);
|
|
26
56
|
expect(result).toBe(' M file.txt');
|
|
27
57
|
});
|
|
28
58
|
it('passes cwd option correctly', () => {
|
|
29
|
-
|
|
59
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('output'));
|
|
30
60
|
git.exec(['status'], { cwd: '/some/path' });
|
|
31
|
-
expect(
|
|
61
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['status'], expect.objectContaining({ cwd: '/some/path' }));
|
|
32
62
|
});
|
|
33
63
|
it('throws error with stderr message on failure', () => {
|
|
34
|
-
|
|
35
|
-
error.stderr = Buffer.from('fatal: not a git repository');
|
|
36
|
-
mockExecSync.mockImplementation(() => {
|
|
37
|
-
throw error;
|
|
38
|
-
});
|
|
64
|
+
mockSpawnSync.mockReturnValue(mockSpawnFailure('fatal: not a git repository'));
|
|
39
65
|
expect(() => git.exec(['status'])).toThrow('Git command failed');
|
|
40
66
|
});
|
|
41
67
|
});
|
|
42
68
|
describe('execSafe', () => {
|
|
43
69
|
it('returns output on success', () => {
|
|
44
|
-
|
|
70
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('output'));
|
|
45
71
|
const result = git.execSafe(['status']);
|
|
46
72
|
expect(result).toBe('output');
|
|
47
73
|
});
|
|
48
74
|
it('returns null on failure', () => {
|
|
49
|
-
|
|
50
|
-
throw new Error('Command failed');
|
|
51
|
-
});
|
|
75
|
+
mockSpawnSync.mockReturnValue(mockSpawnFailure('Command failed'));
|
|
52
76
|
const result = git.execSafe(['status']);
|
|
53
77
|
expect(result).toBeNull();
|
|
54
78
|
});
|
|
55
79
|
});
|
|
56
80
|
describe('getRepoRoot', () => {
|
|
57
81
|
it('returns normalized path from git rev-parse', () => {
|
|
58
|
-
|
|
82
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('/home/user/repo\n'));
|
|
59
83
|
const result = git.getRepoRoot();
|
|
60
84
|
expect(result).toContain('repo');
|
|
61
|
-
expect(
|
|
85
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['rev-parse', '--show-toplevel'], expect.any(Object));
|
|
62
86
|
});
|
|
63
87
|
});
|
|
64
88
|
describe('getRepoName', () => {
|
|
65
89
|
it('extracts name from SSH remote URL', () => {
|
|
66
|
-
|
|
90
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('git@github.com:org/my-repo.git'));
|
|
67
91
|
const result = git.getRepoName('/repo');
|
|
68
92
|
expect(result).toBe('my-repo');
|
|
69
93
|
});
|
|
70
94
|
it('extracts name from HTTPS remote URL', () => {
|
|
71
|
-
|
|
95
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('https://github.com/org/my-repo.git'));
|
|
72
96
|
const result = git.getRepoName('/repo');
|
|
73
97
|
expect(result).toBe('my-repo');
|
|
74
98
|
});
|
|
75
99
|
it('extracts name from URL without .git suffix', () => {
|
|
76
|
-
|
|
100
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('https://github.com/org/my-repo'));
|
|
77
101
|
const result = git.getRepoName('/repo');
|
|
78
102
|
expect(result).toBe('my-repo');
|
|
79
103
|
});
|
|
80
104
|
it('falls back to directory name when no remote', () => {
|
|
81
|
-
|
|
82
|
-
throw new Error('No remote');
|
|
83
|
-
});
|
|
105
|
+
mockSpawnSync.mockReturnValue(mockSpawnFailure('No remote'));
|
|
84
106
|
const result = git.getRepoName('/home/user/my-project');
|
|
85
107
|
expect(result).toBe('my-project');
|
|
86
108
|
});
|
|
109
|
+
it('extracts name from Unix local path with .git suffix', () => {
|
|
110
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('/path/to/my-repo.git'));
|
|
111
|
+
const result = git.getRepoName('/repo');
|
|
112
|
+
expect(result).toBe('my-repo');
|
|
113
|
+
});
|
|
114
|
+
it('extracts name from Windows local path with .git suffix', () => {
|
|
115
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('C:\\Users\\test\\repos\\my-repo.git'));
|
|
116
|
+
const result = git.getRepoName('/repo');
|
|
117
|
+
expect(result).toBe('my-repo');
|
|
118
|
+
});
|
|
119
|
+
it('extracts name from Windows local path without .git suffix', () => {
|
|
120
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('C:\\Users\\test\\repos\\my-repo'));
|
|
121
|
+
const result = git.getRepoName('/repo');
|
|
122
|
+
expect(result).toBe('my-repo');
|
|
123
|
+
});
|
|
124
|
+
it('extracts name from Windows short path format', () => {
|
|
125
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\main-repo.git'));
|
|
126
|
+
const result = git.getRepoName('/repo');
|
|
127
|
+
expect(result).toBe('main-repo');
|
|
128
|
+
});
|
|
87
129
|
});
|
|
88
130
|
describe('getCurrentBranch', () => {
|
|
89
131
|
it('returns branch name', () => {
|
|
90
|
-
|
|
132
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('feature/my-branch'));
|
|
91
133
|
const result = git.getCurrentBranch();
|
|
92
134
|
expect(result).toBe('feature/my-branch');
|
|
93
135
|
});
|
|
94
136
|
it('returns null for detached HEAD', () => {
|
|
95
|
-
|
|
137
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('HEAD'));
|
|
96
138
|
const result = git.getCurrentBranch();
|
|
97
139
|
expect(result).toBeNull();
|
|
98
140
|
});
|
|
99
141
|
});
|
|
100
142
|
describe('isDetachedHead', () => {
|
|
101
143
|
it('returns true when in detached HEAD state', () => {
|
|
102
|
-
|
|
144
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('HEAD'));
|
|
103
145
|
expect(git.isDetachedHead()).toBe(true);
|
|
104
146
|
});
|
|
105
147
|
it('returns false when on a branch', () => {
|
|
106
|
-
|
|
148
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('main'));
|
|
107
149
|
expect(git.isDetachedHead()).toBe(false);
|
|
108
150
|
});
|
|
109
151
|
});
|
|
110
152
|
describe('getWorkingTreeStatus', () => {
|
|
111
153
|
it('returns clean for empty status', () => {
|
|
112
|
-
|
|
154
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
113
155
|
expect(git.getWorkingTreeStatus()).toBe('clean');
|
|
114
156
|
});
|
|
115
157
|
it('returns staged_only for staged changes', () => {
|
|
116
|
-
|
|
158
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('M file.txt'));
|
|
117
159
|
expect(git.getWorkingTreeStatus()).toBe('staged_only');
|
|
118
160
|
});
|
|
119
161
|
it('returns unstaged_only for unstaged changes', () => {
|
|
120
|
-
|
|
162
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(' M file.txt'));
|
|
121
163
|
expect(git.getWorkingTreeStatus()).toBe('unstaged_only');
|
|
122
164
|
});
|
|
123
165
|
it('returns unstaged_only for untracked files', () => {
|
|
124
|
-
|
|
166
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('?? newfile.txt'));
|
|
125
167
|
expect(git.getWorkingTreeStatus()).toBe('unstaged_only');
|
|
126
168
|
});
|
|
127
169
|
it('returns both for staged and unstaged changes', () => {
|
|
128
|
-
|
|
170
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('MM file.txt'));
|
|
129
171
|
expect(git.getWorkingTreeStatus()).toBe('both');
|
|
130
172
|
});
|
|
131
173
|
it('returns both for staged changes and untracked files', () => {
|
|
132
|
-
|
|
174
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('M staged.txt\n?? untracked.txt'));
|
|
133
175
|
expect(git.getWorkingTreeStatus()).toBe('both');
|
|
134
176
|
});
|
|
135
177
|
});
|
|
136
178
|
describe('getStagedFiles', () => {
|
|
137
179
|
it('returns list of staged files', () => {
|
|
138
|
-
|
|
180
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('file1.txt\nfile2.txt\n'));
|
|
139
181
|
const result = git.getStagedFiles();
|
|
140
182
|
expect(result).toEqual(['file1.txt', 'file2.txt']);
|
|
141
183
|
});
|
|
142
184
|
it('returns empty array when nothing staged', () => {
|
|
143
|
-
|
|
144
|
-
throw new Error('No staged files');
|
|
145
|
-
});
|
|
185
|
+
mockSpawnSync.mockReturnValue(mockSpawnFailure('No staged files'));
|
|
146
186
|
const result = git.getStagedFiles();
|
|
147
187
|
expect(result).toEqual([]);
|
|
148
188
|
});
|
|
149
189
|
});
|
|
150
190
|
describe('getUnstagedFiles', () => {
|
|
151
191
|
it('returns list of unstaged and untracked files', () => {
|
|
152
|
-
|
|
192
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(' M modified.txt\n?? untracked.txt'));
|
|
153
193
|
const result = git.getUnstagedFiles();
|
|
154
194
|
expect(result).toEqual(['modified.txt', 'untracked.txt']);
|
|
155
195
|
});
|
|
156
196
|
it('excludes staged-only files', () => {
|
|
157
|
-
|
|
197
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('M staged.txt\n M both.txt'));
|
|
158
198
|
const result = git.getUnstagedFiles();
|
|
159
199
|
expect(result).toEqual(['both.txt']);
|
|
160
200
|
});
|
|
161
201
|
it('returns empty array when working tree is clean', () => {
|
|
162
|
-
|
|
202
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
163
203
|
const result = git.getUnstagedFiles();
|
|
164
204
|
expect(result).toEqual([]);
|
|
165
205
|
});
|
|
166
206
|
});
|
|
167
207
|
describe('listWorktrees', () => {
|
|
168
208
|
it('parses worktree list porcelain output', () => {
|
|
169
|
-
|
|
209
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('worktree /home/user/repo\n' +
|
|
170
210
|
'HEAD abc123def456\n' +
|
|
171
211
|
'branch refs/heads/main\n' +
|
|
172
212
|
'\n' +
|
|
173
213
|
'worktree /home/user/repo.pr42\n' +
|
|
174
214
|
'HEAD def456abc123\n' +
|
|
175
215
|
'branch refs/heads/feature/test\n' +
|
|
176
|
-
'\n');
|
|
216
|
+
'\n'));
|
|
177
217
|
const result = git.listWorktrees();
|
|
178
218
|
expect(result).toHaveLength(2);
|
|
179
219
|
expect(result[0]).toEqual({
|
|
@@ -196,207 +236,203 @@ describe('git', () => {
|
|
|
196
236
|
});
|
|
197
237
|
});
|
|
198
238
|
it('handles bare repository', () => {
|
|
199
|
-
|
|
239
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('worktree /home/user/repo.git\n' + 'bare\n' + '\n'));
|
|
200
240
|
const result = git.listWorktrees();
|
|
201
241
|
expect(result[0].isBare).toBe(true);
|
|
202
242
|
expect(result[0].isMain).toBe(true);
|
|
203
243
|
});
|
|
204
244
|
it('handles locked and prunable worktrees', () => {
|
|
205
|
-
|
|
245
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('worktree /home/user/repo.pr1\n' +
|
|
206
246
|
'HEAD abc123\n' +
|
|
207
247
|
'branch refs/heads/test\n' +
|
|
208
248
|
'locked\n' +
|
|
209
249
|
'prunable\n' +
|
|
210
|
-
'\n');
|
|
250
|
+
'\n'));
|
|
211
251
|
const result = git.listWorktrees();
|
|
212
252
|
expect(result[0].isLocked).toBe(true);
|
|
213
253
|
expect(result[0].isPrunable).toBe(true);
|
|
214
254
|
});
|
|
215
255
|
it('handles detached HEAD in worktree', () => {
|
|
216
|
-
|
|
256
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('worktree /home/user/repo\n' + 'HEAD abc123\n' + 'detached\n' + '\n'));
|
|
217
257
|
const result = git.listWorktrees();
|
|
218
258
|
expect(result[0].branch).toBeNull();
|
|
219
259
|
});
|
|
220
260
|
});
|
|
221
261
|
describe('getCommitRelationship', () => {
|
|
222
262
|
it('returns same when HEAD equals base', () => {
|
|
223
|
-
|
|
224
|
-
.mockReturnValueOnce('abc123') // getHeadCommit
|
|
225
|
-
.mockReturnValueOnce('abc123'); // getRefCommit
|
|
263
|
+
mockSpawnSync
|
|
264
|
+
.mockReturnValueOnce(mockSpawnSuccess('abc123')) // getHeadCommit
|
|
265
|
+
.mockReturnValueOnce(mockSpawnSuccess('abc123')); // getRefCommit
|
|
226
266
|
const result = git.getCommitRelationship('main');
|
|
227
267
|
expect(result).toBe('same');
|
|
228
268
|
});
|
|
229
269
|
it('returns divergent when base branch does not exist', () => {
|
|
230
|
-
|
|
231
|
-
.mockReturnValueOnce('abc123') // getHeadCommit
|
|
232
|
-
.
|
|
233
|
-
throw new Error('unknown revision');
|
|
234
|
-
}); // getRefCommit fails
|
|
270
|
+
mockSpawnSync
|
|
271
|
+
.mockReturnValueOnce(mockSpawnSuccess('abc123')) // getHeadCommit
|
|
272
|
+
.mockReturnValueOnce(mockSpawnFailure('unknown revision')); // getRefCommit fails
|
|
235
273
|
const result = git.getCommitRelationship('main');
|
|
236
274
|
expect(result).toBe('divergent');
|
|
237
275
|
});
|
|
238
276
|
});
|
|
239
277
|
describe('getCommitsAhead', () => {
|
|
240
278
|
it('returns list of commits ahead of base', () => {
|
|
241
|
-
|
|
279
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('abc123 First commit\ndef456 Second commit'));
|
|
242
280
|
const result = git.getCommitsAhead('main');
|
|
243
281
|
expect(result).toEqual(['abc123 First commit', 'def456 Second commit']);
|
|
244
282
|
});
|
|
245
283
|
it('returns empty array when not ahead', () => {
|
|
246
|
-
|
|
247
|
-
throw new Error('No commits');
|
|
248
|
-
});
|
|
284
|
+
mockSpawnSync.mockReturnValue(mockSpawnFailure('No commits'));
|
|
249
285
|
const result = git.getCommitsAhead('main');
|
|
250
286
|
expect(result).toEqual([]);
|
|
251
287
|
});
|
|
252
288
|
});
|
|
253
289
|
describe('addWorktree', () => {
|
|
254
290
|
it('creates worktree with existing branch', () => {
|
|
255
|
-
|
|
291
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
256
292
|
git.addWorktree('/path/to/worktree', 'feature-branch');
|
|
257
|
-
expect(
|
|
293
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['worktree', 'add', '/path/to/worktree', 'feature-branch'], expect.any(Object));
|
|
258
294
|
});
|
|
259
295
|
it('creates worktree with new branch', () => {
|
|
260
|
-
|
|
296
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
261
297
|
git.addWorktree('/path/to/worktree', 'new-branch', { createBranch: true });
|
|
262
|
-
expect(
|
|
298
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['worktree', 'add', '-b', 'new-branch', '/path/to/worktree'], expect.any(Object));
|
|
263
299
|
});
|
|
264
300
|
it('creates worktree with new branch from start point', () => {
|
|
265
|
-
|
|
301
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
266
302
|
git.addWorktree('/path/to/worktree', 'new-branch', {
|
|
267
303
|
createBranch: true,
|
|
268
304
|
startPoint: 'origin/main',
|
|
269
305
|
});
|
|
270
|
-
expect(
|
|
306
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['worktree', 'add', '-b', 'new-branch', '/path/to/worktree', 'origin/main'], expect.any(Object));
|
|
271
307
|
});
|
|
272
308
|
});
|
|
273
309
|
describe('removeWorktree', () => {
|
|
274
310
|
it('removes worktree', () => {
|
|
275
|
-
|
|
311
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
276
312
|
git.removeWorktree('/path/to/worktree');
|
|
277
|
-
expect(
|
|
313
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['worktree', 'remove', '/path/to/worktree'], expect.any(Object));
|
|
278
314
|
});
|
|
279
315
|
it('force removes worktree', () => {
|
|
280
|
-
|
|
316
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
281
317
|
git.removeWorktree('/path/to/worktree', { force: true });
|
|
282
|
-
expect(
|
|
318
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['worktree', 'remove', '--force', '/path/to/worktree'], expect.any(Object));
|
|
283
319
|
});
|
|
284
320
|
});
|
|
285
321
|
describe('createBranch', () => {
|
|
286
322
|
it('creates branch from HEAD', () => {
|
|
287
|
-
|
|
323
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
288
324
|
git.createBranch('new-branch');
|
|
289
|
-
expect(
|
|
325
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['branch', 'new-branch'], expect.any(Object));
|
|
290
326
|
});
|
|
291
327
|
it('creates branch from start point', () => {
|
|
292
|
-
|
|
328
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
293
329
|
git.createBranch('new-branch', 'origin/main');
|
|
294
|
-
expect(
|
|
330
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['branch', 'new-branch', 'origin/main'], expect.any(Object));
|
|
295
331
|
});
|
|
296
332
|
});
|
|
297
333
|
describe('deleteBranch', () => {
|
|
298
334
|
it('deletes branch with -d flag', () => {
|
|
299
|
-
|
|
335
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
300
336
|
git.deleteBranch('old-branch');
|
|
301
|
-
expect(
|
|
337
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['branch', '-d', 'old-branch'], expect.any(Object));
|
|
302
338
|
});
|
|
303
339
|
it('force deletes branch with -D flag', () => {
|
|
304
|
-
|
|
340
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
305
341
|
git.deleteBranch('old-branch', { force: true });
|
|
306
|
-
expect(
|
|
342
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['branch', '-D', 'old-branch'], expect.any(Object));
|
|
307
343
|
});
|
|
308
344
|
});
|
|
309
345
|
describe('commit', () => {
|
|
310
346
|
it('creates commit with message', () => {
|
|
311
|
-
|
|
312
|
-
.mockReturnValueOnce('') // commit
|
|
313
|
-
.mockReturnValueOnce('abc123'); // getHeadCommit
|
|
347
|
+
mockSpawnSync
|
|
348
|
+
.mockReturnValueOnce(mockSpawnSuccess('')) // commit
|
|
349
|
+
.mockReturnValueOnce(mockSpawnSuccess('abc123')); // getHeadCommit
|
|
314
350
|
const result = git.commit({ message: 'Test commit' });
|
|
315
|
-
expect(
|
|
351
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['commit', '-m', 'Test commit'], expect.any(Object));
|
|
316
352
|
expect(result).toBe('abc123');
|
|
317
353
|
});
|
|
318
354
|
it('creates commit with all flag', () => {
|
|
319
|
-
|
|
355
|
+
mockSpawnSync
|
|
356
|
+
.mockReturnValueOnce(mockSpawnSuccess(''))
|
|
357
|
+
.mockReturnValueOnce(mockSpawnSuccess('abc123'));
|
|
320
358
|
git.commit({ message: 'Test commit', all: true });
|
|
321
|
-
expect(
|
|
359
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['commit', '-a', '-m', 'Test commit'], expect.any(Object));
|
|
322
360
|
});
|
|
323
361
|
it('creates empty commit when allowed', () => {
|
|
324
|
-
|
|
362
|
+
mockSpawnSync
|
|
363
|
+
.mockReturnValueOnce(mockSpawnSuccess(''))
|
|
364
|
+
.mockReturnValueOnce(mockSpawnSuccess('abc123'));
|
|
325
365
|
git.commit({ message: 'Empty commit', allowEmpty: true });
|
|
326
|
-
expect(
|
|
366
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['commit', '--allow-empty', '-m', 'Empty commit'], expect.any(Object));
|
|
327
367
|
});
|
|
328
368
|
});
|
|
329
369
|
describe('push', () => {
|
|
330
370
|
it('pushes to remote', () => {
|
|
331
|
-
|
|
371
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
332
372
|
git.push();
|
|
333
|
-
expect(
|
|
373
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['push'], expect.any(Object));
|
|
334
374
|
});
|
|
335
375
|
it('pushes with upstream flag', () => {
|
|
336
|
-
|
|
376
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
337
377
|
git.push({ setUpstream: true, remote: 'origin', branch: 'feature' });
|
|
338
|
-
expect(
|
|
378
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['push', '-u', 'origin', 'feature'], expect.any(Object));
|
|
339
379
|
});
|
|
340
380
|
it('force pushes', () => {
|
|
341
|
-
|
|
381
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
342
382
|
git.push({ force: true });
|
|
343
|
-
expect(
|
|
383
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['push', '--force'], expect.any(Object));
|
|
344
384
|
});
|
|
345
385
|
});
|
|
346
386
|
describe('stash', () => {
|
|
347
387
|
it('creates stash and returns reference', () => {
|
|
348
|
-
|
|
388
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('Saved working directory'));
|
|
349
389
|
const result = git.stash();
|
|
350
390
|
expect(result).toBe('stash@{0}');
|
|
351
391
|
});
|
|
352
392
|
it('returns null when nothing to stash', () => {
|
|
353
|
-
|
|
393
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('No local changes to save'));
|
|
354
394
|
const result = git.stash();
|
|
355
395
|
expect(result).toBeNull();
|
|
356
396
|
});
|
|
357
397
|
it('stashes with keep-index flag', () => {
|
|
358
|
-
|
|
398
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('Saved'));
|
|
359
399
|
git.stash({ keepIndex: true });
|
|
360
|
-
expect(
|
|
400
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['stash', 'push', '--keep-index'], expect.any(Object));
|
|
361
401
|
});
|
|
362
402
|
it('stashes with message', () => {
|
|
363
|
-
|
|
403
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('Saved'));
|
|
364
404
|
git.stash({ message: 'WIP: feature' });
|
|
365
|
-
expect(
|
|
405
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['stash', 'push', '-m', 'WIP: feature'], expect.any(Object));
|
|
366
406
|
});
|
|
367
407
|
it('stashes untracked files', () => {
|
|
368
|
-
|
|
408
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('Saved'));
|
|
369
409
|
git.stash({ includeUntracked: true });
|
|
370
|
-
expect(
|
|
410
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['stash', 'push', '--include-untracked'], expect.any(Object));
|
|
371
411
|
});
|
|
372
412
|
});
|
|
373
413
|
describe('branchExists', () => {
|
|
374
414
|
it('returns true when branch exists', () => {
|
|
375
|
-
|
|
415
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('abc123'));
|
|
376
416
|
expect(git.branchExists('main')).toBe(true);
|
|
377
417
|
});
|
|
378
418
|
it('returns false when branch does not exist', () => {
|
|
379
|
-
|
|
380
|
-
throw new Error('unknown revision');
|
|
381
|
-
});
|
|
419
|
+
mockSpawnSync.mockReturnValue(mockSpawnFailure('unknown revision'));
|
|
382
420
|
expect(git.branchExists('nonexistent')).toBe(false);
|
|
383
421
|
});
|
|
384
422
|
});
|
|
385
423
|
describe('remoteBranchExists', () => {
|
|
386
424
|
it('returns true when remote branch exists', () => {
|
|
387
|
-
|
|
425
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('abc123'));
|
|
388
426
|
expect(git.remoteBranchExists('main')).toBe(true);
|
|
389
427
|
});
|
|
390
428
|
it('returns false when remote branch does not exist', () => {
|
|
391
|
-
|
|
392
|
-
throw new Error('unknown revision');
|
|
393
|
-
});
|
|
429
|
+
mockSpawnSync.mockReturnValue(mockSpawnFailure('unknown revision'));
|
|
394
430
|
expect(git.remoteBranchExists('nonexistent')).toBe(false);
|
|
395
431
|
});
|
|
396
432
|
it('checks specific remote', () => {
|
|
397
|
-
|
|
433
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('abc123'));
|
|
398
434
|
git.remoteBranchExists('main', 'upstream');
|
|
399
|
-
expect(
|
|
435
|
+
expect(mockSpawnSync).toHaveBeenCalledWith('git', ['rev-parse', '--verify', 'refs/remotes/upstream/main'], expect.any(Object));
|
|
400
436
|
});
|
|
401
437
|
});
|
|
402
438
|
describe('getMainWorktreeRoot', () => {
|
|
@@ -404,43 +440,38 @@ describe('git', () => {
|
|
|
404
440
|
// Use path.join to create platform-appropriate paths
|
|
405
441
|
const repoPath = path.join('/home', 'user', 'repo');
|
|
406
442
|
const gitDir = path.join(repoPath, '.git');
|
|
407
|
-
|
|
443
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(gitDir));
|
|
408
444
|
const result = git.getMainWorktreeRoot(repoPath);
|
|
409
445
|
expect(result).toBe(path.resolve(repoPath));
|
|
410
446
|
});
|
|
411
447
|
it('returns main worktree root when in linked worktree', () => {
|
|
412
448
|
const mainRepo = path.join('/home', 'user', 'main-repo');
|
|
413
449
|
const worktreeGitDir = path.join(mainRepo, '.git', 'worktrees', 'feature-branch');
|
|
414
|
-
|
|
450
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(worktreeGitDir));
|
|
415
451
|
const result = git.getMainWorktreeRoot(path.join('/home', 'user', 'main-repo.pr42'));
|
|
416
452
|
expect(result).toBe(path.resolve(mainRepo));
|
|
417
453
|
});
|
|
418
454
|
it('falls back to getRepoRoot when commonDir is null', () => {
|
|
419
455
|
const fallbackPath = path.join('/fallback', 'root');
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
// getRepoRoot calls rev-parse --show-toplevel
|
|
425
|
-
return fallbackPath;
|
|
426
|
-
});
|
|
456
|
+
// First call (git-common-dir) fails, second call (show-toplevel) succeeds
|
|
457
|
+
mockSpawnSync
|
|
458
|
+
.mockReturnValueOnce(mockSpawnFailure('failed'))
|
|
459
|
+
.mockReturnValueOnce(mockSpawnSuccess(fallbackPath));
|
|
427
460
|
const result = git.getMainWorktreeRoot();
|
|
428
|
-
expect(result).
|
|
461
|
+
expect(result).toContain('fallback');
|
|
429
462
|
});
|
|
430
463
|
});
|
|
431
464
|
describe('isGitIgnored', () => {
|
|
432
465
|
it('returns true when file is ignored', () => {
|
|
433
|
-
|
|
466
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess('node_modules/'));
|
|
434
467
|
expect(git.isGitIgnored('node_modules/')).toBe(true);
|
|
435
468
|
});
|
|
436
469
|
it('returns false when file is not ignored', () => {
|
|
437
|
-
|
|
438
|
-
throw new Error('no output for unignored files');
|
|
439
|
-
});
|
|
470
|
+
mockSpawnSync.mockReturnValue(mockSpawnFailure('no output for unignored files'));
|
|
440
471
|
expect(git.isGitIgnored('src/index.ts')).toBe(false);
|
|
441
472
|
});
|
|
442
473
|
it('returns false when check-ignore returns empty', () => {
|
|
443
|
-
|
|
474
|
+
mockSpawnSync.mockReturnValue(mockSpawnSuccess(''));
|
|
444
475
|
expect(git.isGitIgnored('src/index.ts')).toBe(false);
|
|
445
476
|
});
|
|
446
477
|
});
|