@akiojin/gwt 2.9.1 → 2.10.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 +6 -0
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +10 -1
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/types.d.ts +1 -1
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +43 -23
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +67 -67
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +107 -130
- package/src/cli/ui/__tests__/components/App.test.tsx +102 -71
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +57 -30
- package/src/cli/ui/components/App.tsx +16 -3
- package/src/cli/ui/types.ts +1 -1
- package/src/index.test.ts +1 -1
- package/src/worktree.ts +54 -37
|
@@ -13,18 +13,44 @@ import {
|
|
|
13
13
|
} from "vitest";
|
|
14
14
|
import { render } from "@testing-library/react";
|
|
15
15
|
import React from "react";
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
16
|
+
import * as BranchListScreenModule from "../../components/screens/BranchListScreen.js";
|
|
17
|
+
import type { BranchListScreenProps } from "../../components/screens/BranchListScreen.js";
|
|
18
18
|
import { Window } from "happy-dom";
|
|
19
19
|
import type { BranchInfo, BranchItem, Statistics } from "../../types.js";
|
|
20
|
-
import * as useGitDataModule from "../../hooks/useGitData.js";
|
|
21
20
|
|
|
22
21
|
const mockRefresh = vi.fn();
|
|
23
|
-
const
|
|
24
|
-
const
|
|
22
|
+
const branchListProps: BranchListScreenProps[] = [];
|
|
23
|
+
const useGitDataMock = vi.fn();
|
|
24
|
+
let App: typeof import("../../components/App.js").App;
|
|
25
|
+
|
|
26
|
+
vi.mock("../../hooks/useGitData.js", () => ({
|
|
27
|
+
useGitData: (...args: any[]) => useGitDataMock(...args),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("../../components/screens/BranchListScreen.js", () => ({
|
|
31
|
+
BranchListScreen: (props: BranchListScreenProps) => {
|
|
32
|
+
branchListProps.push(props);
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
<div>BranchList</div>
|
|
36
|
+
{props.error && <div>Error: {props.error.message}</div>}
|
|
37
|
+
<div>
|
|
38
|
+
Local:{props.stats?.localCount ?? 0} Remote:
|
|
39
|
+
{props.stats?.remoteCount ?? 0} Worktrees:
|
|
40
|
+
{props.stats?.worktreeCount ?? 0}
|
|
41
|
+
</div>
|
|
42
|
+
<ul>
|
|
43
|
+
{props.branches.map((b) => (
|
|
44
|
+
<li key={b.name}>{b.name}</li>
|
|
45
|
+
))}
|
|
46
|
+
</ul>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
25
51
|
|
|
26
52
|
describe("Edge Cases Integration Tests", () => {
|
|
27
|
-
beforeEach(() => {
|
|
53
|
+
beforeEach(async () => {
|
|
28
54
|
// Setup happy-dom
|
|
29
55
|
const window = new Window();
|
|
30
56
|
globalThis.window = window as any;
|
|
@@ -32,8 +58,9 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
32
58
|
|
|
33
59
|
// Reset mocks
|
|
34
60
|
vi.clearAllMocks();
|
|
35
|
-
|
|
36
|
-
|
|
61
|
+
useGitDataMock.mockReset();
|
|
62
|
+
branchListProps.length = 0;
|
|
63
|
+
App = (await import("../../components/App.js")).App;
|
|
37
64
|
});
|
|
38
65
|
|
|
39
66
|
/**
|
|
@@ -62,7 +89,7 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
62
89
|
|
|
63
90
|
const onSelect = vi.fn();
|
|
64
91
|
const { container } = render(
|
|
65
|
-
<BranchListScreen
|
|
92
|
+
<BranchListScreenModule.BranchListScreen
|
|
66
93
|
branches={mockBranches}
|
|
67
94
|
stats={mockStats}
|
|
68
95
|
onSelect={onSelect}
|
|
@@ -93,16 +120,16 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
93
120
|
};
|
|
94
121
|
|
|
95
122
|
const onSelect = vi.fn();
|
|
96
|
-
const {
|
|
97
|
-
<BranchListScreen
|
|
123
|
+
const { container } = render(
|
|
124
|
+
<BranchListScreenModule.BranchListScreen
|
|
98
125
|
branches={mockBranches}
|
|
99
126
|
stats={mockStats}
|
|
100
127
|
onSelect={onSelect}
|
|
101
128
|
/>,
|
|
102
129
|
);
|
|
103
130
|
|
|
104
|
-
|
|
105
|
-
expect(
|
|
131
|
+
expect(container).toBeDefined();
|
|
132
|
+
expect(branchListProps.at(-1)?.branches).toHaveLength(1);
|
|
106
133
|
|
|
107
134
|
process.stdout.rows = originalRows;
|
|
108
135
|
});
|
|
@@ -139,15 +166,17 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
139
166
|
|
|
140
167
|
const onSelect = vi.fn();
|
|
141
168
|
const { container } = render(
|
|
142
|
-
<BranchListScreen
|
|
169
|
+
<BranchListScreenModule.BranchListScreen
|
|
143
170
|
branches={mockBranches}
|
|
144
171
|
stats={mockStats}
|
|
145
172
|
onSelect={onSelect}
|
|
146
173
|
/>,
|
|
147
174
|
);
|
|
148
175
|
|
|
149
|
-
|
|
150
|
-
expect(
|
|
176
|
+
expect(container.textContent).toContain(longBranchName);
|
|
177
|
+
expect(
|
|
178
|
+
branchListProps.at(-1)?.branches?.some((b) => b.name === longBranchName),
|
|
179
|
+
).toBe(true);
|
|
151
180
|
});
|
|
152
181
|
|
|
153
182
|
it("[T092] should handle branch names with special characters", () => {
|
|
@@ -177,7 +206,7 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
177
206
|
|
|
178
207
|
const onSelect = vi.fn();
|
|
179
208
|
const { container } = render(
|
|
180
|
-
<BranchListScreen
|
|
209
|
+
<BranchListScreenModule.BranchListScreen
|
|
181
210
|
branches={mockBranches}
|
|
182
211
|
stats={mockStats}
|
|
183
212
|
onSelect={onSelect}
|
|
@@ -196,7 +225,7 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
196
225
|
it("[T093] should catch errors in App component", async () => {
|
|
197
226
|
// Mock useGitData to throw an error after initial render
|
|
198
227
|
let callCount = 0;
|
|
199
|
-
|
|
228
|
+
useGitDataMock.mockImplementation(() => {
|
|
200
229
|
callCount++;
|
|
201
230
|
if (callCount > 1) {
|
|
202
231
|
throw new Error("Simulated error");
|
|
@@ -220,7 +249,7 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
220
249
|
|
|
221
250
|
it("[T093] should display error message when data loading fails", () => {
|
|
222
251
|
const testError = new Error("Test error: Failed to load Git data");
|
|
223
|
-
|
|
252
|
+
useGitDataMock.mockReturnValue({
|
|
224
253
|
branches: [],
|
|
225
254
|
worktrees: [],
|
|
226
255
|
loading: false,
|
|
@@ -232,13 +261,15 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
232
261
|
const onExit = vi.fn();
|
|
233
262
|
const { getByText } = render(<App onExit={onExit} />);
|
|
234
263
|
|
|
235
|
-
|
|
264
|
+
expect(branchListProps).not.toHaveLength(0);
|
|
265
|
+
expect(branchListProps.at(-1)?.error).toBe(testError);
|
|
266
|
+
// Error should be displayed via stubbed screen
|
|
236
267
|
expect(getByText(/Error:/i)).toBeDefined();
|
|
237
268
|
expect(getByText(/Failed to load Git data/i)).toBeDefined();
|
|
238
269
|
});
|
|
239
270
|
|
|
240
271
|
it("[T093] should handle empty branches list gracefully", () => {
|
|
241
|
-
|
|
272
|
+
useGitDataMock.mockReturnValue({
|
|
242
273
|
branches: [],
|
|
243
274
|
worktrees: [],
|
|
244
275
|
loading: false,
|
|
@@ -265,7 +296,7 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
265
296
|
isCurrent: false,
|
|
266
297
|
}));
|
|
267
298
|
|
|
268
|
-
|
|
299
|
+
useGitDataMock.mockReturnValue({
|
|
269
300
|
branches: mockBranches,
|
|
270
301
|
worktrees: Array.from({ length: 30 }, (_, i) => ({
|
|
271
302
|
branch: `feature/branch-${i}`,
|
|
@@ -305,7 +336,7 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
305
336
|
|
|
306
337
|
const onSelect = vi.fn();
|
|
307
338
|
const { container, rerender } = render(
|
|
308
|
-
<BranchListScreen
|
|
339
|
+
<BranchListScreenModule.BranchListScreen
|
|
309
340
|
branches={mockBranches}
|
|
310
341
|
stats={mockStats}
|
|
311
342
|
onSelect={onSelect}
|
|
@@ -319,7 +350,7 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
319
350
|
|
|
320
351
|
// Re-render
|
|
321
352
|
rerender(
|
|
322
|
-
<BranchListScreen
|
|
353
|
+
<BranchListScreenModule.BranchListScreen
|
|
323
354
|
branches={mockBranches}
|
|
324
355
|
stats={mockStats}
|
|
325
356
|
onSelect={onSelect}
|
|
@@ -332,11 +363,7 @@ describe("Edge Cases Integration Tests", () => {
|
|
|
332
363
|
});
|
|
333
364
|
|
|
334
365
|
afterEach(() => {
|
|
335
|
-
|
|
336
|
-
|
|
366
|
+
useGitDataMock.mockReset();
|
|
367
|
+
branchListProps.length = 0;
|
|
337
368
|
});
|
|
338
369
|
});
|
|
339
|
-
|
|
340
|
-
afterAll(() => {
|
|
341
|
-
useGitDataSpy.mockRestore();
|
|
342
|
-
});
|
|
@@ -31,7 +31,11 @@ import type {
|
|
|
31
31
|
InferenceLevel,
|
|
32
32
|
SelectedBranchState,
|
|
33
33
|
} from "../types.js";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
getRepositoryRoot,
|
|
36
|
+
deleteBranch,
|
|
37
|
+
deleteRemoteBranch,
|
|
38
|
+
} from "../../../git.js";
|
|
35
39
|
import {
|
|
36
40
|
createWorktree,
|
|
37
41
|
generateWorktreePath,
|
|
@@ -728,6 +732,16 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
728
732
|
}
|
|
729
733
|
|
|
730
734
|
await deleteBranch(target.branch, true);
|
|
735
|
+
|
|
736
|
+
// マージ済みの場合のみリモートブランチも削除
|
|
737
|
+
if (target.hasRemoteBranch && target.reasons?.includes("merged-pr")) {
|
|
738
|
+
try {
|
|
739
|
+
await deleteRemoteBranch(target.branch);
|
|
740
|
+
} catch {
|
|
741
|
+
// リモート削除失敗はログのみ、処理は続行
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
731
745
|
succeededBranches.push(target.branch);
|
|
732
746
|
setCleanupIndicators((prev) => ({
|
|
733
747
|
...prev,
|
|
@@ -803,8 +817,7 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
803
817
|
// All selections complete - exit with result
|
|
804
818
|
if (selectedBranch && selectedTool) {
|
|
805
819
|
const defaultModel = getDefaultModelOption(selectedTool);
|
|
806
|
-
const resolvedModel =
|
|
807
|
-
selectedModel?.model ?? defaultModel?.id ?? null;
|
|
820
|
+
const resolvedModel = selectedModel?.model ?? defaultModel?.id ?? null;
|
|
808
821
|
const resolvedInference =
|
|
809
822
|
selectedModel?.inferenceLevel ??
|
|
810
823
|
getDefaultInferenceForModel(defaultModel ?? undefined);
|
package/src/cli/ui/types.ts
CHANGED
|
@@ -132,7 +132,7 @@ export interface WorktreeWithPR {
|
|
|
132
132
|
pullRequest: PullRequest | null;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
export type CleanupReason = "merged-pr" | "no-diff-with-base";
|
|
135
|
+
export type CleanupReason = "merged-pr" | "no-diff-with-base" | "remote-synced";
|
|
136
136
|
|
|
137
137
|
export interface CleanupTarget {
|
|
138
138
|
worktreePath: string | null; // null for local branch only cleanup
|
package/src/index.test.ts
CHANGED
|
@@ -38,7 +38,7 @@ describe("showVersion via CLI args", () => {
|
|
|
38
38
|
process.argv = ["node", "index.js", "--version"];
|
|
39
39
|
|
|
40
40
|
// getPackageVersion()をモック
|
|
41
|
-
const mockVersion = "
|
|
41
|
+
const mockVersion = "2.6.1";
|
|
42
42
|
vi.spyOn(utils, "getPackageVersion").mockResolvedValue(mockVersion);
|
|
43
43
|
|
|
44
44
|
// Act: main()を呼び出す
|
package/src/worktree.ts
CHANGED
|
@@ -100,11 +100,9 @@ async function listWorktrees(): Promise<WorktreeInfo[]> {
|
|
|
100
100
|
try {
|
|
101
101
|
const { getRepositoryRoot } = await import("./git.js");
|
|
102
102
|
const repoRoot = await getRepositoryRoot();
|
|
103
|
-
const { stdout } = await execa(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{ cwd: repoRoot },
|
|
107
|
-
);
|
|
103
|
+
const { stdout } = await execa("git", ["worktree", "list", "--porcelain"], {
|
|
104
|
+
cwd: repoRoot,
|
|
105
|
+
});
|
|
108
106
|
const worktrees: WorktreeInfo[] = [];
|
|
109
107
|
const lines = stdout.split("\n");
|
|
110
108
|
|
|
@@ -486,16 +484,14 @@ async function getOrphanedLocalBranches({
|
|
|
486
484
|
reasons.push("merged-pr");
|
|
487
485
|
}
|
|
488
486
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
);
|
|
487
|
+
const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
|
|
488
|
+
localBranch.name,
|
|
489
|
+
baseBranch,
|
|
490
|
+
repoRoot,
|
|
491
|
+
);
|
|
495
492
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
493
|
+
if (!hasUniqueCommits) {
|
|
494
|
+
reasons.push("no-diff-with-base");
|
|
499
495
|
}
|
|
500
496
|
|
|
501
497
|
if (process.env.DEBUG_CLEANUP) {
|
|
@@ -506,14 +502,18 @@ async function getOrphanedLocalBranches({
|
|
|
506
502
|
);
|
|
507
503
|
}
|
|
508
504
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
505
|
+
let hasRemoteBranch = false;
|
|
506
|
+
try {
|
|
507
|
+
hasRemoteBranch = await checkRemoteBranchExists(localBranch.name);
|
|
508
|
+
} catch {
|
|
509
|
+
hasRemoteBranch = false;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!hasUnpushed && hasRemoteBranch && hasUniqueCommits) {
|
|
513
|
+
reasons.push("remote-synced");
|
|
514
|
+
}
|
|
516
515
|
|
|
516
|
+
if (reasons.length > 0) {
|
|
517
517
|
cleanupTargets.push({
|
|
518
518
|
worktreePath: null, // worktreeは存在しない
|
|
519
519
|
branch: localBranch.name,
|
|
@@ -627,13 +627,19 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
|
627
627
|
|
|
628
628
|
const cleanupReasons: CleanupReason[] = [];
|
|
629
629
|
|
|
630
|
-
if (mergedPR) {
|
|
631
|
-
cleanupReasons.push("merged-pr");
|
|
632
|
-
}
|
|
633
|
-
|
|
634
630
|
// worktreeパスの存在を確認
|
|
635
631
|
const fs = await import("node:fs");
|
|
636
|
-
|
|
632
|
+
// Some test environments mock node:fs without existsSync on the module root.
|
|
633
|
+
const existsSync =
|
|
634
|
+
typeof fs.existsSync === "function"
|
|
635
|
+
? fs.existsSync
|
|
636
|
+
: typeof (fs as { default?: { existsSync?: unknown } }).default
|
|
637
|
+
?.existsSync === "function"
|
|
638
|
+
? (fs as { default: { existsSync: (p: string) => boolean } }).default
|
|
639
|
+
.existsSync
|
|
640
|
+
: null;
|
|
641
|
+
|
|
642
|
+
const isAccessible = existsSync ? existsSync(worktree.worktreePath) : false;
|
|
637
643
|
|
|
638
644
|
let hasUncommitted = false;
|
|
639
645
|
let hasUnpushed = false;
|
|
@@ -657,16 +663,29 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
|
657
663
|
}
|
|
658
664
|
}
|
|
659
665
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
+
let hasRemoteBranch = false;
|
|
667
|
+
try {
|
|
668
|
+
hasRemoteBranch = await checkRemoteBranchExists(worktree.branch);
|
|
669
|
+
} catch {
|
|
670
|
+
hasRemoteBranch = false;
|
|
671
|
+
}
|
|
666
672
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
673
|
+
if (mergedPR) {
|
|
674
|
+
cleanupReasons.push("merged-pr");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
|
|
678
|
+
worktree.branch,
|
|
679
|
+
baseBranch,
|
|
680
|
+
repoRoot,
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
// 差分がない場合はベース同等としてクリーンアップ候補
|
|
684
|
+
if (!hasUniqueCommits) {
|
|
685
|
+
cleanupReasons.push("no-diff-with-base");
|
|
686
|
+
} else if (!hasUncommitted && !hasUnpushed && hasRemoteBranch) {
|
|
687
|
+
// 未マージでも、ローカルに未コミット/未プッシュがなくリモートが最新ならローカルのみクリーンアップ許可
|
|
688
|
+
cleanupReasons.push("remote-synced");
|
|
670
689
|
}
|
|
671
690
|
|
|
672
691
|
if (process.env.DEBUG_CLEANUP) {
|
|
@@ -681,8 +700,6 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
|
|
|
681
700
|
continue;
|
|
682
701
|
}
|
|
683
702
|
|
|
684
|
-
const hasRemoteBranch = await checkRemoteBranchExists(worktree.branch);
|
|
685
|
-
|
|
686
703
|
const target: CleanupTarget = {
|
|
687
704
|
worktreePath: worktree.worktreePath,
|
|
688
705
|
branch: worktree.branch,
|