@akiojin/gwt 2.0.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.ja.md +323 -0
- package/README.md +347 -0
- package/bin/gwt.js +5 -0
- package/package.json +125 -0
- package/src/claude-history.ts +717 -0
- package/src/claude.ts +292 -0
- package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
- package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
- package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
- package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
- package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
- package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
- package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
- package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
- package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
- package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
- package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
- package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
- package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
- package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
- package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
- package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
- package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
- package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
- package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
- package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
- package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
- package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
- package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
- package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
- package/src/cli/ui/components/App.tsx +793 -0
- package/src/cli/ui/components/common/Confirm.tsx +40 -0
- package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
- package/src/cli/ui/components/common/Input.tsx +36 -0
- package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
- package/src/cli/ui/components/common/Select.tsx +216 -0
- package/src/cli/ui/components/parts/Footer.tsx +41 -0
- package/src/cli/ui/components/parts/Header.test.tsx +85 -0
- package/src/cli/ui/components/parts/Header.tsx +63 -0
- package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
- package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
- package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
- package/src/cli/ui/components/parts/Stats.tsx +67 -0
- package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
- package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
- package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
- package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
- package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
- package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
- package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
- package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
- package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
- package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
- package/src/cli/ui/hooks/useGitData.ts +157 -0
- package/src/cli/ui/hooks/useScreenState.ts +44 -0
- package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
- package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
- package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
- package/src/cli/ui/types.ts +295 -0
- package/src/cli/ui/utils/baseBranch.ts +34 -0
- package/src/cli/ui/utils/branchFormatter.ts +222 -0
- package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
- package/src/codex.ts +139 -0
- package/src/config/builtin-tools.ts +44 -0
- package/src/config/constants.ts +100 -0
- package/src/config/env-history.ts +45 -0
- package/src/config/index.ts +204 -0
- package/src/config/tools.ts +293 -0
- package/src/git.ts +1102 -0
- package/src/github.ts +158 -0
- package/src/index.test.ts +87 -0
- package/src/index.ts +684 -0
- package/src/index.ts.backup +1543 -0
- package/src/launcher.ts +142 -0
- package/src/repositories/git.repository.ts +129 -0
- package/src/repositories/github.repository.ts +83 -0
- package/src/repositories/worktree.repository.ts +69 -0
- package/src/services/BatchMergeService.ts +251 -0
- package/src/services/WorktreeOrchestrator.ts +115 -0
- package/src/services/__tests__/BatchMergeService.test.ts +518 -0
- package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
- package/src/services/dependency-installer.ts +199 -0
- package/src/services/git.service.ts +113 -0
- package/src/services/github.service.ts +61 -0
- package/src/services/worktree.service.ts +66 -0
- package/src/types/api.ts +241 -0
- package/src/types/tools.ts +235 -0
- package/src/utils/spinner.ts +54 -0
- package/src/utils/terminal.ts +272 -0
- package/src/utils.test.ts +43 -0
- package/src/utils.ts +60 -0
- package/src/web/client/index.html +12 -0
- package/src/web/client/src/components/BranchGraph.tsx +231 -0
- package/src/web/client/src/components/EnvEditor.tsx +145 -0
- package/src/web/client/src/components/Terminal.tsx +137 -0
- package/src/web/client/src/hooks/useBranches.ts +41 -0
- package/src/web/client/src/hooks/useConfig.ts +31 -0
- package/src/web/client/src/hooks/useSessions.ts +59 -0
- package/src/web/client/src/hooks/useWorktrees.ts +47 -0
- package/src/web/client/src/index.css +834 -0
- package/src/web/client/src/lib/api.ts +184 -0
- package/src/web/client/src/lib/websocket.ts +174 -0
- package/src/web/client/src/main.tsx +29 -0
- package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
- package/src/web/client/src/pages/BranchListPage.tsx +264 -0
- package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
- package/src/web/client/src/router.tsx +27 -0
- package/src/web/client/vite.config.ts +21 -0
- package/src/web/server/env/importer.ts +54 -0
- package/src/web/server/index.ts +74 -0
- package/src/web/server/pty/manager.ts +189 -0
- package/src/web/server/routes/branches.ts +126 -0
- package/src/web/server/routes/config.ts +220 -0
- package/src/web/server/routes/index.ts +37 -0
- package/src/web/server/routes/sessions.ts +130 -0
- package/src/web/server/routes/worktrees.ts +108 -0
- package/src/web/server/services/branches.ts +368 -0
- package/src/web/server/services/worktrees.ts +85 -0
- package/src/web/server/websocket/handler.ts +180 -0
- package/src/worktree.ts +703 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
formatBranchItem,
|
|
4
|
+
formatBranchItems,
|
|
5
|
+
} from "../../utils/branchFormatter.js";
|
|
6
|
+
import type { BranchInfo } from "../../types.js";
|
|
7
|
+
|
|
8
|
+
describe("branchFormatter", () => {
|
|
9
|
+
describe("formatBranchItem", () => {
|
|
10
|
+
it("should format a main branch", () => {
|
|
11
|
+
const branchInfo: BranchInfo = {
|
|
12
|
+
name: "main",
|
|
13
|
+
type: "local",
|
|
14
|
+
branchType: "main",
|
|
15
|
+
isCurrent: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const result = formatBranchItem(branchInfo);
|
|
19
|
+
|
|
20
|
+
expect(result.name).toBe("main");
|
|
21
|
+
expect(result.type).toBe("local");
|
|
22
|
+
expect(result.branchType).toBe("main");
|
|
23
|
+
expect(result.isCurrent).toBe(true);
|
|
24
|
+
expect(result.icons).toContain("⚡"); // main icon
|
|
25
|
+
expect(result.icons).toContain("⭐"); // current icon
|
|
26
|
+
expect(result.label).toContain("main");
|
|
27
|
+
expect(result.value).toBe("main");
|
|
28
|
+
expect(result.hasChanges).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should format a feature branch", () => {
|
|
32
|
+
const branchInfo: BranchInfo = {
|
|
33
|
+
name: "feature/new-ui",
|
|
34
|
+
type: "local",
|
|
35
|
+
branchType: "feature",
|
|
36
|
+
isCurrent: false,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = formatBranchItem(branchInfo);
|
|
40
|
+
|
|
41
|
+
expect(result.icons).toContain("✨"); // feature icon
|
|
42
|
+
expect(result.icons).not.toContain("⭐"); // not current
|
|
43
|
+
expect(result.label).toContain("feature/new-ui");
|
|
44
|
+
expect(result.value).toBe("feature/new-ui");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should format a hotfix branch", () => {
|
|
48
|
+
const branchInfo: BranchInfo = {
|
|
49
|
+
name: "hotfix/critical-bug",
|
|
50
|
+
type: "local",
|
|
51
|
+
branchType: "hotfix",
|
|
52
|
+
isCurrent: false,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const result = formatBranchItem(branchInfo);
|
|
56
|
+
|
|
57
|
+
expect(result.icons).toContain("🔥"); // hotfix icon
|
|
58
|
+
expect(result.label).toContain("hotfix/critical-bug");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should format a release branch", () => {
|
|
62
|
+
const branchInfo: BranchInfo = {
|
|
63
|
+
name: "release/v1.0.0",
|
|
64
|
+
type: "local",
|
|
65
|
+
branchType: "release",
|
|
66
|
+
isCurrent: false,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const result = formatBranchItem(branchInfo);
|
|
70
|
+
|
|
71
|
+
expect(result.icons).toContain("🚀"); // release icon
|
|
72
|
+
expect(result.label).toContain("release/v1.0.0");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should format a remote branch", () => {
|
|
76
|
+
const branchInfo: BranchInfo = {
|
|
77
|
+
name: "origin/main",
|
|
78
|
+
type: "remote",
|
|
79
|
+
branchType: "main",
|
|
80
|
+
isCurrent: false,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const result = formatBranchItem(branchInfo);
|
|
84
|
+
|
|
85
|
+
expect(result.icons).toContain("⚡"); // main icon
|
|
86
|
+
expect(result.icons).toContain("☁"); // remote icon
|
|
87
|
+
expect(result.type).toBe("remote");
|
|
88
|
+
expect(result.label).toContain("origin/main");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should align branch names regardless of remote icon presence", () => {
|
|
92
|
+
const localBranch: BranchInfo = {
|
|
93
|
+
name: "feature/foo",
|
|
94
|
+
type: "local",
|
|
95
|
+
branchType: "feature",
|
|
96
|
+
isCurrent: false,
|
|
97
|
+
};
|
|
98
|
+
const remoteBranch: BranchInfo = {
|
|
99
|
+
name: "origin/feature/foo",
|
|
100
|
+
type: "remote",
|
|
101
|
+
branchType: "feature",
|
|
102
|
+
isCurrent: false,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const localResult = formatBranchItem(localBranch);
|
|
106
|
+
const remoteResult = formatBranchItem(remoteBranch);
|
|
107
|
+
|
|
108
|
+
const localNameIndex = localResult.label.indexOf(localResult.name);
|
|
109
|
+
const remoteNameIndex = remoteResult.label.indexOf(remoteResult.name);
|
|
110
|
+
|
|
111
|
+
expect(localNameIndex).toBeGreaterThan(0);
|
|
112
|
+
expect(localNameIndex).toBe(remoteNameIndex);
|
|
113
|
+
expect(remoteResult.label).toMatch(/☁(?:️|︎)?\s+origin/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should include worktree status icon when provided", () => {
|
|
117
|
+
const branchInfo: BranchInfo = {
|
|
118
|
+
name: "feature/test",
|
|
119
|
+
type: "local",
|
|
120
|
+
branchType: "feature",
|
|
121
|
+
isCurrent: false,
|
|
122
|
+
worktree: {
|
|
123
|
+
path: "/path/to/worktree",
|
|
124
|
+
locked: false,
|
|
125
|
+
prunable: false,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const result = formatBranchItem(branchInfo);
|
|
130
|
+
|
|
131
|
+
expect(result.icons).toContain("🟢"); // active worktree icon
|
|
132
|
+
expect(result.worktreeStatus).toBe("active");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should mark branch with changes", () => {
|
|
136
|
+
const branchInfo: BranchInfo = {
|
|
137
|
+
name: "feature/wip",
|
|
138
|
+
type: "local",
|
|
139
|
+
branchType: "feature",
|
|
140
|
+
isCurrent: false,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const result = formatBranchItem(branchInfo, { hasChanges: true });
|
|
144
|
+
|
|
145
|
+
expect(result.icons).toContain("✏️"); // changes icon
|
|
146
|
+
expect(result.hasChanges).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should show unpushed commits icon", () => {
|
|
150
|
+
const branchInfo: BranchInfo = {
|
|
151
|
+
name: "feature/unpushed",
|
|
152
|
+
type: "local",
|
|
153
|
+
branchType: "feature",
|
|
154
|
+
isCurrent: false,
|
|
155
|
+
hasUnpushedCommits: true,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const result = formatBranchItem(branchInfo);
|
|
159
|
+
|
|
160
|
+
expect(result.icons).toContain("⬆️"); // unpushed icon
|
|
161
|
+
expect(result.label).toContain("⬆️");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should show open PR icon", () => {
|
|
165
|
+
const branchInfo: BranchInfo = {
|
|
166
|
+
name: "feature/pr-open",
|
|
167
|
+
type: "local",
|
|
168
|
+
branchType: "feature",
|
|
169
|
+
isCurrent: false,
|
|
170
|
+
openPR: { number: 123, title: "Test PR" },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const result = formatBranchItem(branchInfo);
|
|
174
|
+
|
|
175
|
+
expect(result.icons).toContain("🔀"); // open PR icon
|
|
176
|
+
expect(result.label).toContain("🔀");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should show merged PR icon", () => {
|
|
180
|
+
const branchInfo: BranchInfo = {
|
|
181
|
+
name: "feature/pr-merged",
|
|
182
|
+
type: "local",
|
|
183
|
+
branchType: "feature",
|
|
184
|
+
isCurrent: false,
|
|
185
|
+
mergedPR: { number: 456, mergedAt: "2025-10-31T00:00:00Z" },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const result = formatBranchItem(branchInfo);
|
|
189
|
+
|
|
190
|
+
expect(result.icons).toContain("✅"); // merged PR icon
|
|
191
|
+
expect(result.label).toContain("✅");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should show warning icon for inaccessible worktree", () => {
|
|
195
|
+
const branchInfo: BranchInfo = {
|
|
196
|
+
name: "feature/broken-worktree",
|
|
197
|
+
type: "local",
|
|
198
|
+
branchType: "feature",
|
|
199
|
+
isCurrent: false,
|
|
200
|
+
worktree: {
|
|
201
|
+
path: "/path/to/worktree",
|
|
202
|
+
locked: false,
|
|
203
|
+
prunable: false,
|
|
204
|
+
isAccessible: false,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const result = formatBranchItem(branchInfo);
|
|
209
|
+
|
|
210
|
+
expect(result.icons).toContain("🟠"); // inaccessible worktree icon
|
|
211
|
+
expect(result.icons).toContain("⚠️"); // warning icon
|
|
212
|
+
expect(result.worktreeStatus).toBe("inaccessible");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should prioritize hasChanges over unpushed commits", () => {
|
|
216
|
+
const branchInfo: BranchInfo = {
|
|
217
|
+
name: "feature/both",
|
|
218
|
+
type: "local",
|
|
219
|
+
branchType: "feature",
|
|
220
|
+
isCurrent: false,
|
|
221
|
+
hasUnpushedCommits: true,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const resultWithChanges = formatBranchItem(branchInfo, {
|
|
225
|
+
hasChanges: true,
|
|
226
|
+
});
|
|
227
|
+
expect(resultWithChanges.icons).toContain("✏️");
|
|
228
|
+
expect(resultWithChanges.icons).not.toContain("⬆️");
|
|
229
|
+
|
|
230
|
+
const resultWithoutChanges = formatBranchItem(branchInfo);
|
|
231
|
+
expect(resultWithoutChanges.icons).toContain("⬆️");
|
|
232
|
+
expect(resultWithoutChanges.icons).not.toContain("✏️");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should prioritize unpushed over open PR", () => {
|
|
236
|
+
const branchInfo: BranchInfo = {
|
|
237
|
+
name: "feature/unpushed-pr",
|
|
238
|
+
type: "local",
|
|
239
|
+
branchType: "feature",
|
|
240
|
+
isCurrent: false,
|
|
241
|
+
hasUnpushedCommits: true,
|
|
242
|
+
openPR: { number: 123, title: "Test PR" },
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const result = formatBranchItem(branchInfo);
|
|
246
|
+
|
|
247
|
+
expect(result.icons).toContain("⬆️");
|
|
248
|
+
expect(result.icons).not.toContain("🔀");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should prioritize open PR over merged PR", () => {
|
|
252
|
+
const branchInfo: BranchInfo = {
|
|
253
|
+
name: "feature/both-pr",
|
|
254
|
+
type: "local",
|
|
255
|
+
branchType: "feature",
|
|
256
|
+
isCurrent: false,
|
|
257
|
+
openPR: { number: 123, title: "Test PR" },
|
|
258
|
+
mergedPR: { number: 456, mergedAt: "2025-10-31T00:00:00Z" },
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const result = formatBranchItem(branchInfo);
|
|
262
|
+
|
|
263
|
+
expect(result.icons).toContain("🔀");
|
|
264
|
+
expect(result.icons).not.toContain("✅");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should prioritize merged PR over warning", () => {
|
|
268
|
+
const branchInfo: BranchInfo = {
|
|
269
|
+
name: "feature/merged-broken",
|
|
270
|
+
type: "local",
|
|
271
|
+
branchType: "feature",
|
|
272
|
+
isCurrent: false,
|
|
273
|
+
mergedPR: { number: 456, mergedAt: "2025-10-31T00:00:00Z" },
|
|
274
|
+
worktree: {
|
|
275
|
+
path: "/path/to/worktree",
|
|
276
|
+
locked: false,
|
|
277
|
+
prunable: false,
|
|
278
|
+
isAccessible: false,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const result = formatBranchItem(branchInfo);
|
|
283
|
+
|
|
284
|
+
expect(result.icons).toContain("✅");
|
|
285
|
+
expect(result.icons).not.toContain("⚠️");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should prioritize warning over current branch icon", () => {
|
|
289
|
+
const branchInfo: BranchInfo = {
|
|
290
|
+
name: "feature/current-broken",
|
|
291
|
+
type: "local",
|
|
292
|
+
branchType: "feature",
|
|
293
|
+
isCurrent: true,
|
|
294
|
+
worktree: {
|
|
295
|
+
path: "/path/to/worktree",
|
|
296
|
+
locked: false,
|
|
297
|
+
prunable: false,
|
|
298
|
+
isAccessible: false,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const result = formatBranchItem(branchInfo);
|
|
303
|
+
|
|
304
|
+
expect(result.icons).toContain("⚠️");
|
|
305
|
+
expect(result.icons).not.toContain("⭐");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should handle develop branch", () => {
|
|
309
|
+
const branchInfo: BranchInfo = {
|
|
310
|
+
name: "develop",
|
|
311
|
+
type: "local",
|
|
312
|
+
branchType: "develop",
|
|
313
|
+
isCurrent: false,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const result = formatBranchItem(branchInfo);
|
|
317
|
+
|
|
318
|
+
expect(result.icons).toContain("⚡"); // develop icon (same as main)
|
|
319
|
+
expect(result.label).toContain("develop");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should handle other branch type", () => {
|
|
323
|
+
const branchInfo: BranchInfo = {
|
|
324
|
+
name: "custom-branch",
|
|
325
|
+
type: "local",
|
|
326
|
+
branchType: "other",
|
|
327
|
+
isCurrent: false,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const result = formatBranchItem(branchInfo);
|
|
331
|
+
|
|
332
|
+
expect(result.icons).toContain("📌"); // other icon
|
|
333
|
+
expect(result.label).toContain("custom-branch");
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("formatBranchItems", () => {
|
|
338
|
+
it("should format multiple branches with sorting", () => {
|
|
339
|
+
const branches: BranchInfo[] = [
|
|
340
|
+
{
|
|
341
|
+
name: "main",
|
|
342
|
+
type: "local",
|
|
343
|
+
branchType: "main",
|
|
344
|
+
isCurrent: true,
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "feature/test",
|
|
348
|
+
type: "local",
|
|
349
|
+
branchType: "feature",
|
|
350
|
+
isCurrent: false,
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: "origin/main",
|
|
354
|
+
type: "remote",
|
|
355
|
+
branchType: "main",
|
|
356
|
+
isCurrent: false,
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
const results = formatBranchItems(branches);
|
|
361
|
+
|
|
362
|
+
expect(results).toHaveLength(3);
|
|
363
|
+
// Current branch (main) should be first
|
|
364
|
+
expect(results[0].name).toBe("main");
|
|
365
|
+
expect(results[0].isCurrent).toBe(true);
|
|
366
|
+
// origin/main is also main branch, so it comes second
|
|
367
|
+
expect(results[1].name).toBe("origin/main");
|
|
368
|
+
expect(results[1].type).toBe("remote");
|
|
369
|
+
// feature/test comes last
|
|
370
|
+
expect(results[2].name).toBe("feature/test");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should handle empty array", () => {
|
|
374
|
+
const results = formatBranchItems([]);
|
|
375
|
+
|
|
376
|
+
expect(results).toHaveLength(0);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("should sort branches alphabetically when no other priority applies", () => {
|
|
380
|
+
const branches: BranchInfo[] = [
|
|
381
|
+
{
|
|
382
|
+
name: "z-branch",
|
|
383
|
+
type: "local",
|
|
384
|
+
branchType: "feature",
|
|
385
|
+
isCurrent: false,
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
name: "a-branch",
|
|
389
|
+
type: "local",
|
|
390
|
+
branchType: "feature",
|
|
391
|
+
isCurrent: false,
|
|
392
|
+
},
|
|
393
|
+
];
|
|
394
|
+
|
|
395
|
+
const results = formatBranchItems(branches);
|
|
396
|
+
|
|
397
|
+
expect(results[0].name).toBe("a-branch");
|
|
398
|
+
expect(results[1].name).toBe("z-branch");
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("formatBranchItems - sorting", () => {
|
|
403
|
+
it("should prioritize current branch at the top", () => {
|
|
404
|
+
const branches: BranchInfo[] = [
|
|
405
|
+
{
|
|
406
|
+
name: "feature/a",
|
|
407
|
+
type: "local",
|
|
408
|
+
branchType: "feature",
|
|
409
|
+
isCurrent: false,
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: "feature/current",
|
|
413
|
+
type: "local",
|
|
414
|
+
branchType: "feature",
|
|
415
|
+
isCurrent: true,
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: "main",
|
|
419
|
+
type: "local",
|
|
420
|
+
branchType: "main",
|
|
421
|
+
isCurrent: false,
|
|
422
|
+
},
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
const results = formatBranchItems(branches);
|
|
426
|
+
|
|
427
|
+
expect(results[0].name).toBe("feature/current");
|
|
428
|
+
expect(results[0].isCurrent).toBe(true);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("should prioritize main branch as second (after current)", () => {
|
|
432
|
+
const branches: BranchInfo[] = [
|
|
433
|
+
{
|
|
434
|
+
name: "feature/test",
|
|
435
|
+
type: "local",
|
|
436
|
+
branchType: "feature",
|
|
437
|
+
isCurrent: false,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: "main",
|
|
441
|
+
type: "local",
|
|
442
|
+
branchType: "main",
|
|
443
|
+
isCurrent: false,
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "develop",
|
|
447
|
+
type: "local",
|
|
448
|
+
branchType: "develop",
|
|
449
|
+
isCurrent: false,
|
|
450
|
+
},
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
const results = formatBranchItems(branches);
|
|
454
|
+
|
|
455
|
+
expect(results[0].name).toBe("main");
|
|
456
|
+
expect(results[1].name).toBe("develop");
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("should prioritize develop branch after main (when main exists)", () => {
|
|
460
|
+
const branches: BranchInfo[] = [
|
|
461
|
+
{
|
|
462
|
+
name: "feature/test",
|
|
463
|
+
type: "local",
|
|
464
|
+
branchType: "feature",
|
|
465
|
+
isCurrent: false,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: "develop",
|
|
469
|
+
type: "local",
|
|
470
|
+
branchType: "develop",
|
|
471
|
+
isCurrent: false,
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: "main",
|
|
475
|
+
type: "local",
|
|
476
|
+
branchType: "main",
|
|
477
|
+
isCurrent: false,
|
|
478
|
+
},
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
const results = formatBranchItems(branches);
|
|
482
|
+
|
|
483
|
+
expect(results[0].name).toBe("main");
|
|
484
|
+
expect(results[1].name).toBe("develop");
|
|
485
|
+
expect(results[2].name).toBe("feature/test");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("should NOT prioritize develop branch when main does not exist", () => {
|
|
489
|
+
const branches: BranchInfo[] = [
|
|
490
|
+
{
|
|
491
|
+
name: "feature/a",
|
|
492
|
+
type: "local",
|
|
493
|
+
branchType: "feature",
|
|
494
|
+
isCurrent: false,
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: "develop",
|
|
498
|
+
type: "local",
|
|
499
|
+
branchType: "develop",
|
|
500
|
+
isCurrent: false,
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
name: "feature/z",
|
|
504
|
+
type: "local",
|
|
505
|
+
branchType: "feature",
|
|
506
|
+
isCurrent: false,
|
|
507
|
+
},
|
|
508
|
+
];
|
|
509
|
+
|
|
510
|
+
const results = formatBranchItems(branches);
|
|
511
|
+
|
|
512
|
+
// develop should be sorted alphabetically, not prioritized
|
|
513
|
+
expect(results[0].name).toBe("develop");
|
|
514
|
+
expect(results[1].name).toBe("feature/a");
|
|
515
|
+
expect(results[2].name).toBe("feature/z");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("should prioritize branches with worktree", () => {
|
|
519
|
+
const worktreeMap = new Map([
|
|
520
|
+
[
|
|
521
|
+
"feature/with-worktree",
|
|
522
|
+
{
|
|
523
|
+
path: "/path/to/worktree",
|
|
524
|
+
locked: false,
|
|
525
|
+
prunable: false,
|
|
526
|
+
isAccessible: true,
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
]);
|
|
530
|
+
|
|
531
|
+
const branches: BranchInfo[] = [
|
|
532
|
+
{
|
|
533
|
+
name: "feature/no-worktree",
|
|
534
|
+
type: "local",
|
|
535
|
+
branchType: "feature",
|
|
536
|
+
isCurrent: false,
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: "feature/with-worktree",
|
|
540
|
+
type: "local",
|
|
541
|
+
branchType: "feature",
|
|
542
|
+
isCurrent: false,
|
|
543
|
+
worktree: {
|
|
544
|
+
path: "/path/to/worktree",
|
|
545
|
+
locked: false,
|
|
546
|
+
prunable: false,
|
|
547
|
+
isAccessible: true,
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
];
|
|
551
|
+
|
|
552
|
+
const results = formatBranchItems(branches, worktreeMap);
|
|
553
|
+
|
|
554
|
+
expect(results[0].name).toBe("feature/with-worktree");
|
|
555
|
+
expect(results[1].name).toBe("feature/no-worktree");
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("should sort branches with worktree by latest commit timestamp", () => {
|
|
559
|
+
const worktreeMap = new Map([
|
|
560
|
+
[
|
|
561
|
+
"feature/recent",
|
|
562
|
+
{
|
|
563
|
+
path: "/path/to/recent",
|
|
564
|
+
locked: false,
|
|
565
|
+
prunable: false,
|
|
566
|
+
isAccessible: true,
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
[
|
|
570
|
+
"feature/older",
|
|
571
|
+
{
|
|
572
|
+
path: "/path/to/older",
|
|
573
|
+
locked: false,
|
|
574
|
+
prunable: false,
|
|
575
|
+
isAccessible: true,
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
]);
|
|
579
|
+
|
|
580
|
+
const branches: BranchInfo[] = [
|
|
581
|
+
{
|
|
582
|
+
name: "feature/older",
|
|
583
|
+
type: "local",
|
|
584
|
+
branchType: "feature",
|
|
585
|
+
isCurrent: false,
|
|
586
|
+
worktree: {
|
|
587
|
+
path: "/path/to/older",
|
|
588
|
+
locked: false,
|
|
589
|
+
prunable: false,
|
|
590
|
+
isAccessible: true,
|
|
591
|
+
},
|
|
592
|
+
latestCommitTimestamp: 1_700_000_000,
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
name: "feature/recent",
|
|
596
|
+
type: "local",
|
|
597
|
+
branchType: "feature",
|
|
598
|
+
isCurrent: false,
|
|
599
|
+
worktree: {
|
|
600
|
+
path: "/path/to/recent",
|
|
601
|
+
locked: false,
|
|
602
|
+
prunable: false,
|
|
603
|
+
isAccessible: true,
|
|
604
|
+
},
|
|
605
|
+
latestCommitTimestamp: 1_800_000_000,
|
|
606
|
+
},
|
|
607
|
+
];
|
|
608
|
+
|
|
609
|
+
const results = formatBranchItems(branches, worktreeMap);
|
|
610
|
+
|
|
611
|
+
expect(results[0].name).toBe("feature/recent");
|
|
612
|
+
expect(results[1].name).toBe("feature/older");
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("should prioritize local branches over remote branches", () => {
|
|
616
|
+
const branches: BranchInfo[] = [
|
|
617
|
+
{
|
|
618
|
+
name: "origin/feature/remote",
|
|
619
|
+
type: "remote",
|
|
620
|
+
branchType: "feature",
|
|
621
|
+
isCurrent: false,
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
name: "feature/local",
|
|
625
|
+
type: "local",
|
|
626
|
+
branchType: "feature",
|
|
627
|
+
isCurrent: false,
|
|
628
|
+
},
|
|
629
|
+
];
|
|
630
|
+
|
|
631
|
+
const results = formatBranchItems(branches);
|
|
632
|
+
|
|
633
|
+
expect(results[0].name).toBe("feature/local");
|
|
634
|
+
expect(results[0].type).toBe("local");
|
|
635
|
+
expect(results[1].name).toBe("origin/feature/remote");
|
|
636
|
+
expect(results[1].type).toBe("remote");
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("should sort by latest commit timestamp when worktree status matches", () => {
|
|
640
|
+
const branches: BranchInfo[] = [
|
|
641
|
+
{
|
|
642
|
+
name: "origin/feature/newer",
|
|
643
|
+
type: "remote",
|
|
644
|
+
branchType: "feature",
|
|
645
|
+
isCurrent: false,
|
|
646
|
+
latestCommitTimestamp: 1_900_000_000,
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
name: "feature/local-older",
|
|
650
|
+
type: "local",
|
|
651
|
+
branchType: "feature",
|
|
652
|
+
isCurrent: false,
|
|
653
|
+
latestCommitTimestamp: 1_800_000_000,
|
|
654
|
+
},
|
|
655
|
+
];
|
|
656
|
+
|
|
657
|
+
const results = formatBranchItems(branches);
|
|
658
|
+
|
|
659
|
+
expect(results[0].name).toBe("origin/feature/newer");
|
|
660
|
+
expect(results[1].name).toBe("feature/local-older");
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("should apply all sorting rules in correct priority order", () => {
|
|
664
|
+
const worktreeMap = new Map([
|
|
665
|
+
[
|
|
666
|
+
"feature/with-worktree",
|
|
667
|
+
{
|
|
668
|
+
path: "/path/to/worktree",
|
|
669
|
+
locked: false,
|
|
670
|
+
prunable: false,
|
|
671
|
+
isAccessible: true,
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
]);
|
|
675
|
+
|
|
676
|
+
const branches: BranchInfo[] = [
|
|
677
|
+
{
|
|
678
|
+
name: "origin/feature/z-remote",
|
|
679
|
+
type: "remote",
|
|
680
|
+
branchType: "feature",
|
|
681
|
+
isCurrent: false,
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
name: "feature/with-worktree",
|
|
685
|
+
type: "local",
|
|
686
|
+
branchType: "feature",
|
|
687
|
+
isCurrent: false,
|
|
688
|
+
worktree: {
|
|
689
|
+
path: "/path/to/worktree",
|
|
690
|
+
locked: false,
|
|
691
|
+
prunable: false,
|
|
692
|
+
isAccessible: true,
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
name: "feature/z-local-no-worktree",
|
|
697
|
+
type: "local",
|
|
698
|
+
branchType: "feature",
|
|
699
|
+
isCurrent: false,
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
name: "feature/a-local-no-worktree",
|
|
703
|
+
type: "local",
|
|
704
|
+
branchType: "feature",
|
|
705
|
+
isCurrent: false,
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
name: "develop",
|
|
709
|
+
type: "local",
|
|
710
|
+
branchType: "develop",
|
|
711
|
+
isCurrent: false,
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
name: "main",
|
|
715
|
+
type: "local",
|
|
716
|
+
branchType: "main",
|
|
717
|
+
isCurrent: false,
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
name: "feature/current",
|
|
721
|
+
type: "local",
|
|
722
|
+
branchType: "feature",
|
|
723
|
+
isCurrent: true,
|
|
724
|
+
},
|
|
725
|
+
];
|
|
726
|
+
|
|
727
|
+
const results = formatBranchItems(branches, worktreeMap);
|
|
728
|
+
|
|
729
|
+
// Expected order:
|
|
730
|
+
// 1. Current branch
|
|
731
|
+
// 2. main
|
|
732
|
+
// 3. develop (because main exists)
|
|
733
|
+
// 4. Branches with worktree
|
|
734
|
+
// 5. Local branches (alphabetically)
|
|
735
|
+
// 6. Remote branches
|
|
736
|
+
expect(results[0].name).toBe("feature/current");
|
|
737
|
+
expect(results[1].name).toBe("main");
|
|
738
|
+
expect(results[2].name).toBe("develop");
|
|
739
|
+
expect(results[3].name).toBe("feature/with-worktree");
|
|
740
|
+
expect(results[4].name).toBe("feature/a-local-no-worktree");
|
|
741
|
+
expect(results[5].name).toBe("feature/z-local-no-worktree");
|
|
742
|
+
expect(results[6].name).toBe("origin/feature/z-remote");
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("should handle release and hotfix branches without special priority", () => {
|
|
746
|
+
const branches: BranchInfo[] = [
|
|
747
|
+
{
|
|
748
|
+
name: "feature/test",
|
|
749
|
+
type: "local",
|
|
750
|
+
branchType: "feature",
|
|
751
|
+
isCurrent: false,
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
name: "hotfix/urgent",
|
|
755
|
+
type: "local",
|
|
756
|
+
branchType: "hotfix",
|
|
757
|
+
isCurrent: false,
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
name: "release/v1.0",
|
|
761
|
+
type: "local",
|
|
762
|
+
branchType: "release",
|
|
763
|
+
isCurrent: false,
|
|
764
|
+
},
|
|
765
|
+
];
|
|
766
|
+
|
|
767
|
+
const results = formatBranchItems(branches);
|
|
768
|
+
|
|
769
|
+
// Should be sorted alphabetically (no special priority)
|
|
770
|
+
expect(results[0].name).toBe("feature/test");
|
|
771
|
+
expect(results[1].name).toBe("hotfix/urgent");
|
|
772
|
+
expect(results[2].name).toBe("release/v1.0");
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
});
|