@einja/dev-cli 0.1.7 → 0.1.8
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 +2 -3
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +16 -9
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/task-loop/index.d.ts +18 -0
- package/dist/commands/task-loop/index.d.ts.map +1 -0
- package/dist/commands/task-loop/index.js +333 -0
- package/dist/commands/task-loop/index.js.map +1 -0
- package/dist/commands/task-loop/lib/args-parser.d.ts +15 -0
- package/dist/commands/task-loop/lib/args-parser.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/args-parser.js +79 -0
- package/dist/commands/task-loop/lib/args-parser.js.map +1 -0
- package/dist/commands/task-loop/lib/branch-manager.d.ts +66 -0
- package/dist/commands/task-loop/lib/branch-manager.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/branch-manager.js +697 -0
- package/dist/commands/task-loop/lib/branch-manager.js.map +1 -0
- package/dist/commands/task-loop/lib/conflict-handler.d.ts +8 -0
- package/dist/commands/task-loop/lib/conflict-handler.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/conflict-handler.js +68 -0
- package/dist/commands/task-loop/lib/conflict-handler.js.map +1 -0
- package/dist/commands/task-loop/lib/dependency-resolver.d.ts +45 -0
- package/dist/commands/task-loop/lib/dependency-resolver.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/dependency-resolver.js +184 -0
- package/dist/commands/task-loop/lib/dependency-resolver.js.map +1 -0
- package/dist/commands/task-loop/lib/gh-setup.d.ts +15 -0
- package/dist/commands/task-loop/lib/gh-setup.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/gh-setup.js +196 -0
- package/dist/commands/task-loop/lib/gh-setup.js.map +1 -0
- package/dist/commands/task-loop/lib/github-client.d.ts +27 -0
- package/dist/commands/task-loop/lib/github-client.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/github-client.js +110 -0
- package/dist/commands/task-loop/lib/github-client.js.map +1 -0
- package/dist/commands/task-loop/lib/issue-parser.d.ts +27 -0
- package/dist/commands/task-loop/lib/issue-parser.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/issue-parser.js +254 -0
- package/dist/commands/task-loop/lib/issue-parser.js.map +1 -0
- package/dist/commands/task-loop/lib/project-selector.d.ts +15 -0
- package/dist/commands/task-loop/lib/project-selector.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/project-selector.js +187 -0
- package/dist/commands/task-loop/lib/project-selector.js.map +1 -0
- package/dist/commands/task-loop/lib/task-number-utils.d.ts +56 -0
- package/dist/commands/task-loop/lib/task-number-utils.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/task-number-utils.js +114 -0
- package/dist/commands/task-loop/lib/task-number-utils.js.map +1 -0
- package/dist/commands/task-loop/lib/task-state-manager.d.ts +59 -0
- package/dist/commands/task-loop/lib/task-state-manager.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/task-state-manager.js +184 -0
- package/dist/commands/task-loop/lib/task-state-manager.js.map +1 -0
- package/dist/commands/task-loop/lib/types.d.ts +93 -0
- package/dist/commands/task-loop/lib/types.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/types.js +5 -0
- package/dist/commands/task-loop/lib/types.js.map +1 -0
- package/dist/commands/task-loop/lib/vibe-kanban-client.d.ts +82 -0
- package/dist/commands/task-loop/lib/vibe-kanban-client.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/vibe-kanban-client.js +280 -0
- package/dist/commands/task-loop/lib/vibe-kanban-client.js.map +1 -0
- package/dist/commands/task-loop/lib/vibe-kanban-rest-client.d.ts +42 -0
- package/dist/commands/task-loop/lib/vibe-kanban-rest-client.d.ts.map +1 -0
- package/dist/commands/task-loop/lib/vibe-kanban-rest-client.js +122 -0
- package/dist/commands/task-loop/lib/vibe-kanban-rest-client.js.map +1 -0
- package/dist/lib/sync/batch-processor.test.js +1 -1
- package/dist/lib/sync/batch-processor.test.js.map +1 -1
- package/dist/lib/sync/diff-engine.js +1 -1
- package/dist/lib/sync/diff-engine.js.map +1 -1
- package/dist/lib/sync/hash-cache.d.ts +10 -4
- package/dist/lib/sync/hash-cache.d.ts.map +1 -1
- package/dist/lib/sync/hash-cache.js +28 -9
- package/dist/lib/sync/hash-cache.js.map +1 -1
- package/dist/lib/sync/hash-cache.test.js +69 -38
- package/dist/lib/sync/hash-cache.test.js.map +1 -1
- package/dist/lib/sync/metadata-manager.d.ts.map +1 -1
- package/dist/lib/sync/metadata-manager.js +3 -4
- package/dist/lib/sync/metadata-manager.js.map +1 -1
- package/dist/lib/sync/performance.test.js +4 -5
- package/dist/lib/sync/performance.test.js.map +1 -1
- package/package.json +1 -1
- package/scaffolds/CLAUDE.md.template +81 -13
- package/dist/commands/task-loop.d.ts +0 -11
- package/dist/commands/task-loop.d.ts.map +0 -1
- package/dist/commands/task-loop.js +0 -81
- package/dist/commands/task-loop.js.map +0 -1
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git ブランチ操作
|
|
3
|
+
*/
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { resolveConflictWithClaude } from "./conflict-handler.js";
|
|
8
|
+
/**
|
|
9
|
+
* gh CLI が認証済みか確認
|
|
10
|
+
*/
|
|
11
|
+
function isGhCliAuthenticated() {
|
|
12
|
+
try {
|
|
13
|
+
execSync("gh auth status", { stdio: "ignore" });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch (_a) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* GitHub API を使用してリモートでブランチをマージ
|
|
22
|
+
* ローカルの状態に依存しない
|
|
23
|
+
*/
|
|
24
|
+
function mergeWithGitHubApi(baseBranch, headBranch, commitMessage) {
|
|
25
|
+
try {
|
|
26
|
+
execSync(`gh api repos/:owner/:repo/merges -f base="${baseBranch}" -f head="${headBranch}" -f commit_message="${commitMessage}"`, { encoding: "utf-8", stdio: "pipe" });
|
|
27
|
+
return { success: true };
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const errorStr = String(error);
|
|
31
|
+
// 409 Conflict = マージコンフリクト
|
|
32
|
+
if (errorStr.includes("409") || errorStr.includes("Merge conflict")) {
|
|
33
|
+
return { success: false, conflicted: true, error: "マージコンフリクトが発生しました" };
|
|
34
|
+
}
|
|
35
|
+
// 204 No Content または "already" = Already up to date
|
|
36
|
+
if (errorStr.includes("204") || errorStr.includes("already")) {
|
|
37
|
+
return { success: true, alreadyUpToDate: true };
|
|
38
|
+
}
|
|
39
|
+
return { success: false, error: errorStr };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* git worktree を使用してマージ(フォールバック)
|
|
44
|
+
* --detach で既存 worktree と競合しない
|
|
45
|
+
* コンフリクト時は worktree を削除せず、パスを返す
|
|
46
|
+
*/
|
|
47
|
+
function mergeWithWorktree(baseBranch, headBranch, commitMessage) {
|
|
48
|
+
const tempDir = path.join(os.tmpdir(), `merge-work-${Date.now()}`);
|
|
49
|
+
try {
|
|
50
|
+
// detached HEAD で worktree 作成(既存 worktree と競合しない)
|
|
51
|
+
execSync(`git worktree add --detach "${tempDir}" origin/${baseBranch}`, {
|
|
52
|
+
stdio: "pipe",
|
|
53
|
+
});
|
|
54
|
+
// マージ実行
|
|
55
|
+
try {
|
|
56
|
+
execSync(`git -C "${tempDir}" merge origin/${headBranch} -m "${commitMessage}"`, {
|
|
57
|
+
stdio: "pipe",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (mergeError) {
|
|
61
|
+
// マージコンフリクト - worktree を保持してパスを返す
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
conflicted: true,
|
|
65
|
+
error: "マージコンフリクトが発生しました",
|
|
66
|
+
worktreePath: tempDir
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// プッシュ(ブランチ名を明示)
|
|
70
|
+
execSync(`git -C "${tempDir}" push origin HEAD:${baseBranch}`, { stdio: "pipe" });
|
|
71
|
+
// 成功時のみクリーンアップ
|
|
72
|
+
try {
|
|
73
|
+
execSync(`git worktree remove "${tempDir}" --force`, { stdio: "ignore" });
|
|
74
|
+
}
|
|
75
|
+
catch (_a) {
|
|
76
|
+
// クリーンアップ失敗は無視
|
|
77
|
+
}
|
|
78
|
+
return { success: true };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// エラー時はクリーンアップ
|
|
82
|
+
try {
|
|
83
|
+
execSync(`git worktree remove "${tempDir}" --force`, { stdio: "ignore" });
|
|
84
|
+
}
|
|
85
|
+
catch (_b) {
|
|
86
|
+
// クリーンアップ失敗は無視
|
|
87
|
+
}
|
|
88
|
+
return { success: false, error: String(error) };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 統合マージ関数(gh CLI → worktree のフォールバック)
|
|
93
|
+
* ローカルの状態に依存せずにリモートブランチをマージ
|
|
94
|
+
*
|
|
95
|
+
* GitHub APIで409(コンフリクト)が返った場合、worktreeで再試行する。
|
|
96
|
+
* GitHub APIは保守的な判定をするため、ローカルのgit merge(ort戦略)なら
|
|
97
|
+
* 自動解決できるケースがある。
|
|
98
|
+
*/
|
|
99
|
+
function mergeRemoteBranches(baseBranch, headBranch, commitMessage) {
|
|
100
|
+
// gh CLI が認証済みなら GitHub API を使用
|
|
101
|
+
if (isGhCliAuthenticated()) {
|
|
102
|
+
console.log(" 🔧 GitHub API でマージを実行");
|
|
103
|
+
const apiResult = mergeWithGitHubApi(baseBranch, headBranch, commitMessage);
|
|
104
|
+
// API成功 or コンフリクト以外のエラー → そのまま返す
|
|
105
|
+
if (apiResult.success || !apiResult.conflicted) {
|
|
106
|
+
return apiResult;
|
|
107
|
+
}
|
|
108
|
+
// 409(コンフリクト)の場合のみ worktree で再試行
|
|
109
|
+
// GitHub APIは保守的なため、ローカルのort戦略なら解決できる場合がある
|
|
110
|
+
console.log(" ⚠️ GitHub API で 409 Conflict、worktree で再試行...");
|
|
111
|
+
return mergeWithWorktree(baseBranch, headBranch, commitMessage);
|
|
112
|
+
}
|
|
113
|
+
// gh CLI 未認証: git worktree を使用
|
|
114
|
+
console.log(" 🔧 git worktree でマージを実行");
|
|
115
|
+
return mergeWithWorktree(baseBranch, headBranch, commitMessage);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* リモートの最新情報を取得
|
|
119
|
+
*/
|
|
120
|
+
export function fetchRemote() {
|
|
121
|
+
execSync("git fetch origin", { stdio: "inherit" });
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* リモートのデフォルトブランチを取得
|
|
125
|
+
*/
|
|
126
|
+
export function getDefaultBranch() {
|
|
127
|
+
try {
|
|
128
|
+
const result = execSync("git remote show origin | grep 'HEAD branch' | awk '{print $NF}'", {
|
|
129
|
+
encoding: "utf-8",
|
|
130
|
+
});
|
|
131
|
+
return result.trim();
|
|
132
|
+
}
|
|
133
|
+
catch (_a) {
|
|
134
|
+
return "main";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* ブランチが存在するか確認(ローカルまたはリモート)
|
|
139
|
+
*/
|
|
140
|
+
export function branchExists(branchName) {
|
|
141
|
+
try {
|
|
142
|
+
// ローカルブランチの確認
|
|
143
|
+
execSync(`git rev-parse --verify ${branchName}`, { stdio: "ignore" });
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch (_a) {
|
|
147
|
+
try {
|
|
148
|
+
// リモートブランチの確認
|
|
149
|
+
execSync(`git rev-parse --verify origin/${branchName}`, {
|
|
150
|
+
stdio: "ignore",
|
|
151
|
+
});
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch (_b) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 現在のブランチ名を取得
|
|
161
|
+
*/
|
|
162
|
+
export function getCurrentBranch() {
|
|
163
|
+
const result = execSync("git branch --show-current", { encoding: "utf-8" });
|
|
164
|
+
return result.trim();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* ブランチをチェックアウト(存在しない場合は作成)
|
|
168
|
+
*/
|
|
169
|
+
export function checkoutBranch(branchName, baseBranch) {
|
|
170
|
+
if (branchExists(branchName)) {
|
|
171
|
+
execSync(`git checkout ${branchName}`, { stdio: "inherit" });
|
|
172
|
+
}
|
|
173
|
+
else if (baseBranch) {
|
|
174
|
+
execSync(`git checkout -b ${branchName} origin/${baseBranch}`, {
|
|
175
|
+
stdio: "inherit",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
execSync(`git checkout -b ${branchName}`, { stdio: "inherit" });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Issue ブランチを作成または取得
|
|
184
|
+
* @returns ブランチ名
|
|
185
|
+
*/
|
|
186
|
+
export function ensureIssueBranch(issueNumber, baseBranch) {
|
|
187
|
+
const branchName = `issue/${issueNumber}`;
|
|
188
|
+
fetchRemote();
|
|
189
|
+
if (branchExists(branchName)) {
|
|
190
|
+
console.log(`📌 既存の Issue ブランチを使用: ${branchName}`);
|
|
191
|
+
checkoutBranch(branchName);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(`🌿 Issue ブランチを作成: ${branchName} (from ${baseBranch})`);
|
|
195
|
+
checkoutBranch(branchName, baseBranch);
|
|
196
|
+
// リモートにプッシュ
|
|
197
|
+
execSync(`git push -u origin ${branchName}`, { stdio: "inherit" });
|
|
198
|
+
}
|
|
199
|
+
return branchName;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Issue ブランチを作成または確認(チェックアウトなし)
|
|
203
|
+
* 複数 Issue の同時実行をサポートするため、現在のブランチを変更しない
|
|
204
|
+
* @returns ブランチ名
|
|
205
|
+
*/
|
|
206
|
+
export function ensureIssueBranchWithoutCheckout(issueNumber, baseBranch) {
|
|
207
|
+
const branchName = `issue/${issueNumber}`;
|
|
208
|
+
fetchRemote();
|
|
209
|
+
if (branchExists(branchName)) {
|
|
210
|
+
console.log(`📌 既存の Issue ブランチを使用: ${branchName}`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
console.log(`🌿 Issue ブランチを作成: ${branchName} (from ${baseBranch})`);
|
|
214
|
+
// チェックアウトせずにブランチを作成
|
|
215
|
+
execSync(`git branch ${branchName} origin/${baseBranch}`, {
|
|
216
|
+
stdio: "inherit",
|
|
217
|
+
});
|
|
218
|
+
// リモートにプッシュ
|
|
219
|
+
execSync(`git push -u origin ${branchName}`, { stdio: "inherit" });
|
|
220
|
+
}
|
|
221
|
+
return branchName;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Phase ブランチを作成または確認(チェックアウトなし)
|
|
225
|
+
* 複数 Issue の同時実行をサポートするため、現在のブランチを変更しない
|
|
226
|
+
* @returns ブランチ名
|
|
227
|
+
*/
|
|
228
|
+
export function ensurePhaseBranchWithoutCheckout(issueNumber, phaseNumber, issueBranch) {
|
|
229
|
+
const branchName = `issue/${issueNumber}-phase${phaseNumber}`;
|
|
230
|
+
if (branchExists(branchName)) {
|
|
231
|
+
console.log(` 📌 既存の Phase ブランチを使用: ${branchName}`);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
console.log(` 🌿 Phase ブランチを作成: ${branchName} (from ${issueBranch})`);
|
|
235
|
+
// チェックアウトせずにブランチを作成(ローカルの Issue ブランチから)
|
|
236
|
+
execSync(`git branch ${branchName} ${issueBranch}`, {
|
|
237
|
+
stdio: "inherit",
|
|
238
|
+
});
|
|
239
|
+
// リモートにプッシュ
|
|
240
|
+
execSync(`git push -u origin ${branchName}`, { stdio: "inherit" });
|
|
241
|
+
}
|
|
242
|
+
return branchName;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Phase ブランチ名を取得
|
|
246
|
+
*/
|
|
247
|
+
export function getPhaseBranchNameNew(issueNumber, phaseNumber) {
|
|
248
|
+
return `issue/${issueNumber}-phase${phaseNumber}`;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Phase ブランチを作成または取得
|
|
252
|
+
* @returns ブランチ名
|
|
253
|
+
*/
|
|
254
|
+
export function ensurePhaseBranch(issueNumber, phaseNumber, issueBranch) {
|
|
255
|
+
const branchName = `issue/${issueNumber}-phase${phaseNumber}`;
|
|
256
|
+
if (branchExists(branchName)) {
|
|
257
|
+
console.log(`📌 既存の Phase ブランチを使用: ${branchName}`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.log(`🌿 Phase ブランチを作成: ${branchName} (from ${issueBranch})`);
|
|
261
|
+
// Issue ブランチから Phase ブランチを作成
|
|
262
|
+
const currentBranch = getCurrentBranch();
|
|
263
|
+
if (currentBranch !== issueBranch) {
|
|
264
|
+
checkoutBranch(issueBranch);
|
|
265
|
+
}
|
|
266
|
+
execSync(`git checkout -b ${branchName}`, { stdio: "inherit" });
|
|
267
|
+
execSync(`git push -u origin ${branchName}`, { stdio: "inherit" });
|
|
268
|
+
}
|
|
269
|
+
return branchName;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* タスクグループの Phase ブランチ名を取得
|
|
273
|
+
*/
|
|
274
|
+
export function getPhaseBranchName(issueNumber, phaseNumber) {
|
|
275
|
+
return `issue/${issueNumber}-phase${phaseNumber}`;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Phase ブランチを同期(存在しなければ作成、存在すれば最新化)
|
|
279
|
+
* タスク着手時に呼び出され、リモートの最新状態を反映する
|
|
280
|
+
* また、作成元の Issue ブランチの変更も取り込む
|
|
281
|
+
*/
|
|
282
|
+
export async function syncPhaseBranch(issueNumber, phaseNumber, issueBranch, issueBranchBase) {
|
|
283
|
+
const branchName = `issue/${issueNumber}-phase${phaseNumber}`;
|
|
284
|
+
// リモートの最新を取得
|
|
285
|
+
fetchRemote();
|
|
286
|
+
// Issue ブランチをリモートの最新に同期し、ベースブランチの変更も取り込む
|
|
287
|
+
await syncIssueBranch(issueBranch, issueBranchBase);
|
|
288
|
+
// リモートにブランチが存在するか確認
|
|
289
|
+
let remoteExists = false;
|
|
290
|
+
try {
|
|
291
|
+
execSync(`git rev-parse --verify origin/${branchName}`, {
|
|
292
|
+
stdio: "ignore",
|
|
293
|
+
});
|
|
294
|
+
remoteExists = true;
|
|
295
|
+
}
|
|
296
|
+
catch (_a) {
|
|
297
|
+
remoteExists = false;
|
|
298
|
+
}
|
|
299
|
+
if (remoteExists) {
|
|
300
|
+
// リモートに存在する場合: リモートと同期
|
|
301
|
+
console.log(` 🔄 Phase ブランチを同期: ${branchName}`);
|
|
302
|
+
// リモートの最新を取得
|
|
303
|
+
execSync(`git fetch origin ${branchName}`, { stdio: "inherit" });
|
|
304
|
+
// ローカルブランチが存在するか確認
|
|
305
|
+
let localExists = false;
|
|
306
|
+
try {
|
|
307
|
+
execSync(`git rev-parse --verify ${branchName}`, { stdio: "ignore" });
|
|
308
|
+
localExists = true;
|
|
309
|
+
}
|
|
310
|
+
catch (_b) {
|
|
311
|
+
localExists = false;
|
|
312
|
+
}
|
|
313
|
+
if (!localExists) {
|
|
314
|
+
// ローカルにない場合は作成
|
|
315
|
+
execSync(`git branch ${branchName} origin/${branchName}`, {
|
|
316
|
+
stdio: "inherit",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
// ローカルにある場合はリモートとマージ(pull相当)
|
|
321
|
+
mergeRemoteIntoLocal(branchName);
|
|
322
|
+
}
|
|
323
|
+
// Issue ブランチの変更を Phase ブランチに取り込む
|
|
324
|
+
await mergeIssueBranchIntoPhase(branchName, issueBranch);
|
|
325
|
+
}
|
|
326
|
+
else if (branchExists(branchName)) {
|
|
327
|
+
// ローカルのみに存在する場合: Issue ブランチの変更を取り込んでプッシュ
|
|
328
|
+
console.log(` 📤 Phase ブランチをプッシュ: ${branchName}`);
|
|
329
|
+
await mergeIssueBranchIntoPhase(branchName, issueBranch);
|
|
330
|
+
execSync(`git push -u origin ${branchName}`, { stdio: "inherit" });
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// どこにも存在しない場合: Issue ブランチの最新から新規作成
|
|
334
|
+
console.log(` 🌿 Phase ブランチを作成: ${branchName}`);
|
|
335
|
+
execSync(`git branch ${branchName} ${issueBranch}`, {
|
|
336
|
+
stdio: "inherit",
|
|
337
|
+
});
|
|
338
|
+
execSync(`git push -u origin ${branchName}`, { stdio: "inherit" });
|
|
339
|
+
}
|
|
340
|
+
return branchName;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* リモートブランチの変更をローカルブランチにマージ(pull相当)
|
|
344
|
+
* ローカルとリモートが異なる場合のみマージを実行
|
|
345
|
+
* 現在のブランチの場合は直接マージ、それ以外は worktree または fast-forward で処理
|
|
346
|
+
*/
|
|
347
|
+
function mergeRemoteIntoLocal(branchName) {
|
|
348
|
+
// ローカルとリモートが同じか確認
|
|
349
|
+
const localCommit = execSync(`git rev-parse ${branchName}`, {
|
|
350
|
+
encoding: "utf-8",
|
|
351
|
+
}).trim();
|
|
352
|
+
const remoteCommit = execSync(`git rev-parse origin/${branchName}`, {
|
|
353
|
+
encoding: "utf-8",
|
|
354
|
+
}).trim();
|
|
355
|
+
if (localCommit === remoteCommit) {
|
|
356
|
+
console.log(` ✅ リモートと同期済み: ${branchName}`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// 現在のブランチかどうか確認
|
|
360
|
+
const currentBranch = getCurrentBranch();
|
|
361
|
+
const isCurrentBranch = currentBranch === branchName;
|
|
362
|
+
// ローカルがリモートの祖先か確認 (fast-forward 可能)
|
|
363
|
+
try {
|
|
364
|
+
execSync(`git merge-base --is-ancestor ${localCommit} ${remoteCommit}`, { stdio: "ignore" });
|
|
365
|
+
// fast-forward 可能
|
|
366
|
+
if (isCurrentBranch) {
|
|
367
|
+
// 現在のブランチの場合は直接マージ
|
|
368
|
+
execSync(`git merge origin/${branchName} --ff-only`, { stdio: "inherit" });
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
// 別のブランチの場合は branch -f で更新
|
|
372
|
+
execSync(`git branch -f ${branchName} origin/${branchName}`, { stdio: "inherit" });
|
|
373
|
+
}
|
|
374
|
+
console.log(` ✅ リモートの変更を取り込み (fast-forward): ${branchName}`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
catch (_a) {
|
|
378
|
+
// fast-forward 不可
|
|
379
|
+
}
|
|
380
|
+
// リモートがローカルの祖先か確認 (プッシュが必要)
|
|
381
|
+
try {
|
|
382
|
+
execSync(`git merge-base --is-ancestor ${remoteCommit} ${localCommit}`, { stdio: "ignore" });
|
|
383
|
+
// プッシュが必要
|
|
384
|
+
execSync(`git push origin ${branchName}`, { stdio: "inherit" });
|
|
385
|
+
console.log(` ✅ ローカルの変更をプッシュ: ${branchName}`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
catch (_b) {
|
|
389
|
+
// プッシュだけでは不十分、マージが必要
|
|
390
|
+
}
|
|
391
|
+
// 両方が進んでいる場合: マージが必要
|
|
392
|
+
console.log(` 🔀 リモートの変更をマージ: ${branchName}`);
|
|
393
|
+
if (isCurrentBranch) {
|
|
394
|
+
// 現在のブランチの場合は直接マージ
|
|
395
|
+
try {
|
|
396
|
+
execSync(`git merge origin/${branchName} --no-edit`, { stdio: "pipe" });
|
|
397
|
+
}
|
|
398
|
+
catch (_c) {
|
|
399
|
+
execSync(`git merge --abort`, { stdio: "ignore" });
|
|
400
|
+
throw new Error(`ブランチ ${branchName} のリモート同期でコンフリクトが発生しました。手動で解決してください。`);
|
|
401
|
+
}
|
|
402
|
+
// マージ結果をプッシュ
|
|
403
|
+
execSync(`git push origin ${branchName}`, { stdio: "pipe" });
|
|
404
|
+
console.log(` ✅ リモートの変更をマージ: ${branchName}`);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
// 別のブランチの場合は worktree でマージ
|
|
408
|
+
const tempDir = path.join(os.tmpdir(), `merge-sync-${Date.now()}`);
|
|
409
|
+
try {
|
|
410
|
+
// ローカルブランチを一時的に worktree にチェックアウト
|
|
411
|
+
execSync(`git worktree add "${tempDir}" ${branchName}`, { stdio: "pipe" });
|
|
412
|
+
// リモートの変更をマージ
|
|
413
|
+
try {
|
|
414
|
+
execSync(`git -C "${tempDir}" merge origin/${branchName} --no-edit`, { stdio: "pipe" });
|
|
415
|
+
}
|
|
416
|
+
catch (_d) {
|
|
417
|
+
execSync(`git -C "${tempDir}" merge --abort`, { stdio: "ignore" });
|
|
418
|
+
throw new Error(`ブランチ ${branchName} のリモート同期でコンフリクトが発生しました。手動で解決してください。`);
|
|
419
|
+
}
|
|
420
|
+
// マージ結果をプッシュ
|
|
421
|
+
execSync(`git -C "${tempDir}" push origin ${branchName}`, { stdio: "pipe" });
|
|
422
|
+
console.log(` ✅ リモートの変更をマージ: ${branchName}`);
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
// クリーンアップ
|
|
426
|
+
try {
|
|
427
|
+
execSync(`git worktree remove "${tempDir}" --force`, { stdio: "ignore" });
|
|
428
|
+
}
|
|
429
|
+
catch (_e) {
|
|
430
|
+
// クリーンアップ失敗は無視
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Issue ブランチをリモートの最新に同期し、ベースブランチの変更も取り込む
|
|
437
|
+
*/
|
|
438
|
+
async function syncIssueBranch(issueBranch, issueBranchBase) {
|
|
439
|
+
// リモートに Issue ブランチが存在するか確認
|
|
440
|
+
let remoteExists = false;
|
|
441
|
+
try {
|
|
442
|
+
execSync(`git rev-parse --verify origin/${issueBranch}`, {
|
|
443
|
+
stdio: "ignore",
|
|
444
|
+
});
|
|
445
|
+
remoteExists = true;
|
|
446
|
+
}
|
|
447
|
+
catch (_a) {
|
|
448
|
+
remoteExists = false;
|
|
449
|
+
}
|
|
450
|
+
if (!remoteExists) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
console.log(` 🔄 Issue ブランチを同期: ${issueBranch}`);
|
|
454
|
+
// リモートの最新を取得
|
|
455
|
+
execSync(`git fetch origin ${issueBranch}`, { stdio: "inherit" });
|
|
456
|
+
// ローカルブランチが存在するか確認
|
|
457
|
+
let localExists = false;
|
|
458
|
+
try {
|
|
459
|
+
execSync(`git rev-parse --verify ${issueBranch}`, { stdio: "ignore" });
|
|
460
|
+
localExists = true;
|
|
461
|
+
}
|
|
462
|
+
catch (_b) {
|
|
463
|
+
localExists = false;
|
|
464
|
+
}
|
|
465
|
+
if (!localExists) {
|
|
466
|
+
// ローカルにない場合は作成
|
|
467
|
+
execSync(`git branch ${issueBranch} origin/${issueBranch}`, {
|
|
468
|
+
stdio: "inherit",
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
// ローカルにある場合はリモートとマージ(pull相当)
|
|
473
|
+
mergeRemoteIntoLocal(issueBranch);
|
|
474
|
+
}
|
|
475
|
+
// ベースブランチの変更を Issue ブランチに取り込む
|
|
476
|
+
await mergeBaseBranchIntoIssue(issueBranch, issueBranchBase);
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* ベースブランチの変更を Issue ブランチに取り込む(マージ)
|
|
480
|
+
* checkout を使用せず、GitHub API または worktree でリモートマージを実行
|
|
481
|
+
* コンフリクトが発生した場合は Claude Code でインタラクティブに解決
|
|
482
|
+
*/
|
|
483
|
+
async function mergeBaseBranchIntoIssue(issueBranch, issueBranchBase) {
|
|
484
|
+
// Issue ブランチがベースブランチの変更を既に含んでいるか確認
|
|
485
|
+
const remoteBaseBranch = `origin/${issueBranchBase}`;
|
|
486
|
+
const remoteIssueBranch = `origin/${issueBranch}`;
|
|
487
|
+
// リモートブランチ同士で比較(ローカルの状態に依存しない)
|
|
488
|
+
const mergeBase = execSync(`git merge-base ${remoteIssueBranch} ${remoteBaseBranch}`, {
|
|
489
|
+
encoding: "utf-8",
|
|
490
|
+
}).trim();
|
|
491
|
+
const baseHead = execSync(`git rev-parse ${remoteBaseBranch}`, {
|
|
492
|
+
encoding: "utf-8",
|
|
493
|
+
}).trim();
|
|
494
|
+
// ベースブランチの HEAD が merge-base と同じなら、取り込み済み
|
|
495
|
+
if (mergeBase === baseHead) {
|
|
496
|
+
console.log(" ✅ ベースブランチの変更は既に取り込み済み");
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
// Issue ブランチにベースブランチの変更をマージ(リモートで実行)
|
|
500
|
+
console.log(` 🔀 ベースブランチの変更を取り込み: ${issueBranchBase} → ${issueBranch}`);
|
|
501
|
+
const result = mergeRemoteBranches(issueBranch, issueBranchBase, `Merge ${issueBranchBase} into ${issueBranch}`);
|
|
502
|
+
if (!result.success) {
|
|
503
|
+
if (result.conflicted && result.worktreePath) {
|
|
504
|
+
// Claude Code でコンフリクト解消
|
|
505
|
+
const resolved = await resolveConflictWithClaude({
|
|
506
|
+
targetBranch: issueBranch,
|
|
507
|
+
sourceBranch: issueBranchBase,
|
|
508
|
+
operationType: "base"
|
|
509
|
+
}, result.worktreePath);
|
|
510
|
+
if (!resolved) {
|
|
511
|
+
// コンフリクト解消失敗 - worktree をクリーンアップ
|
|
512
|
+
try {
|
|
513
|
+
execSync(`git worktree remove "${result.worktreePath}" --force`, { stdio: "ignore" });
|
|
514
|
+
}
|
|
515
|
+
catch (_a) {
|
|
516
|
+
// クリーンアップ失敗は無視
|
|
517
|
+
}
|
|
518
|
+
throw new Error(`Issue ブランチ ${issueBranch} へのベースブランチ ${issueBranchBase} のマージでコンフリクトを解消できませんでした。`);
|
|
519
|
+
}
|
|
520
|
+
// コンフリクト解消成功 - プッシュしてクリーンアップ
|
|
521
|
+
try {
|
|
522
|
+
execSync(`git -C "${result.worktreePath}" push origin HEAD:${issueBranch}`, { stdio: "pipe" });
|
|
523
|
+
console.log(" ✅ コンフリクト解消後のマージを完了しました");
|
|
524
|
+
}
|
|
525
|
+
finally {
|
|
526
|
+
execSync(`git worktree remove "${result.worktreePath}" --force`, { stdio: "ignore" });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
throw new Error(`マージに失敗しました: ${result.error}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
console.log(" ✅ ベースブランチのマージ完了");
|
|
535
|
+
}
|
|
536
|
+
// ローカルブランチをリモートに合わせて更新
|
|
537
|
+
try {
|
|
538
|
+
execSync(`git fetch origin ${issueBranch}`, { stdio: "pipe" });
|
|
539
|
+
execSync(`git branch -f ${issueBranch} origin/${issueBranch}`, { stdio: "pipe" });
|
|
540
|
+
}
|
|
541
|
+
catch (_b) {
|
|
542
|
+
// ローカルブランチの更新失敗は警告のみ(リモートは更新済み)
|
|
543
|
+
console.log(` ⚠️ ローカルブランチの更新をスキップ: ${issueBranch}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Phase ブランチを Issue ブランチにマージ(フェーズ完了時)
|
|
548
|
+
* checkout を使用せず、GitHub API または worktree でリモートマージを実行
|
|
549
|
+
* コンフリクトが発生した場合は Claude Code でインタラクティブに解決
|
|
550
|
+
*/
|
|
551
|
+
export async function mergePhaseBranchIntoIssue(issueNumber, phaseNumber, issueBranch) {
|
|
552
|
+
const phaseBranch = `issue/${issueNumber}-phase${phaseNumber}`;
|
|
553
|
+
// リモートの最新を取得
|
|
554
|
+
fetchRemote();
|
|
555
|
+
// リモートに Phase ブランチが存在するか確認
|
|
556
|
+
let remoteExists = false;
|
|
557
|
+
try {
|
|
558
|
+
execSync(`git rev-parse --verify origin/${phaseBranch}`, {
|
|
559
|
+
stdio: "ignore",
|
|
560
|
+
});
|
|
561
|
+
remoteExists = true;
|
|
562
|
+
}
|
|
563
|
+
catch (_a) {
|
|
564
|
+
remoteExists = false;
|
|
565
|
+
}
|
|
566
|
+
if (!remoteExists) {
|
|
567
|
+
console.log(` ⏭️ Phase ブランチがリモートに存在しません: ${phaseBranch}`);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
// Issue ブランチが Phase ブランチの変更を既に含んでいるか確認
|
|
571
|
+
const mergeBase = execSync(`git merge-base origin/${issueBranch} origin/${phaseBranch}`, {
|
|
572
|
+
encoding: "utf-8",
|
|
573
|
+
}).trim();
|
|
574
|
+
const phaseHead = execSync(`git rev-parse origin/${phaseBranch}`, {
|
|
575
|
+
encoding: "utf-8",
|
|
576
|
+
}).trim();
|
|
577
|
+
// Phase ブランチの HEAD が merge-base と同じなら、取り込み済み
|
|
578
|
+
if (mergeBase === phaseHead) {
|
|
579
|
+
console.log(` ✅ Phase ${phaseNumber} の変更は既に Issue ブランチに取り込み済み`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// Issue ブランチに Phase ブランチの変更をマージ(リモートで実行)
|
|
583
|
+
console.log(` 🔀 Phase ブランチを Issue ブランチにマージ: ${phaseBranch} → ${issueBranch}`);
|
|
584
|
+
const result = mergeRemoteBranches(issueBranch, phaseBranch, `Merge phase${phaseNumber} into ${issueBranch}`);
|
|
585
|
+
if (!result.success) {
|
|
586
|
+
if (result.conflicted && result.worktreePath) {
|
|
587
|
+
// Claude Code でコンフリクト解消
|
|
588
|
+
const resolved = await resolveConflictWithClaude({
|
|
589
|
+
targetBranch: issueBranch,
|
|
590
|
+
sourceBranch: phaseBranch,
|
|
591
|
+
operationType: "phase"
|
|
592
|
+
}, result.worktreePath);
|
|
593
|
+
if (!resolved) {
|
|
594
|
+
// コンフリクト解消失敗 - worktree をクリーンアップ
|
|
595
|
+
try {
|
|
596
|
+
execSync(`git worktree remove "${result.worktreePath}" --force`, { stdio: "ignore" });
|
|
597
|
+
}
|
|
598
|
+
catch (_b) {
|
|
599
|
+
// クリーンアップ失敗は無視
|
|
600
|
+
}
|
|
601
|
+
throw new Error(`Issue ブランチ ${issueBranch} への Phase ブランチ ${phaseBranch} のマージでコンフリクトを解消できませんでした。`);
|
|
602
|
+
}
|
|
603
|
+
// コンフリクト解消成功 - プッシュしてクリーンアップ
|
|
604
|
+
try {
|
|
605
|
+
execSync(`git -C "${result.worktreePath}" push origin HEAD:${issueBranch}`, { stdio: "pipe" });
|
|
606
|
+
console.log(` ✅ Phase ${phaseNumber} コンフリクト解消後のマージを完了しました`);
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
execSync(`git worktree remove "${result.worktreePath}" --force`, { stdio: "ignore" });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
throw new Error(`マージに失敗しました: ${result.error}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
console.log(` ✅ Phase ${phaseNumber} マージ完了`);
|
|
618
|
+
}
|
|
619
|
+
// ローカルブランチをリモートに合わせて更新
|
|
620
|
+
try {
|
|
621
|
+
execSync(`git fetch origin ${issueBranch}`, { stdio: "pipe" });
|
|
622
|
+
execSync(`git branch -f ${issueBranch} origin/${issueBranch}`, { stdio: "pipe" });
|
|
623
|
+
}
|
|
624
|
+
catch (_c) {
|
|
625
|
+
// ローカルブランチの更新失敗は警告のみ(リモートは更新済み)
|
|
626
|
+
console.log(` ⚠️ ローカルブランチの更新をスキップ: ${issueBranch}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Issue ブランチの変更を Phase ブランチに取り込む(マージ)
|
|
631
|
+
* checkout を使用せず、GitHub API または worktree でリモートマージを実行
|
|
632
|
+
* コンフリクトが発生した場合は Claude Code でインタラクティブに解決
|
|
633
|
+
*/
|
|
634
|
+
async function mergeIssueBranchIntoPhase(phaseBranch, issueBranch) {
|
|
635
|
+
// Phase ブランチが Issue ブランチの変更を既に含んでいるか確認
|
|
636
|
+
// リモートブランチ同士で比較(ローカルの状態に依存しない)
|
|
637
|
+
const remotePhaseBranch = `origin/${phaseBranch}`;
|
|
638
|
+
const remoteIssueBranch = `origin/${issueBranch}`;
|
|
639
|
+
const mergeBase = execSync(`git merge-base ${remotePhaseBranch} ${remoteIssueBranch}`, {
|
|
640
|
+
encoding: "utf-8",
|
|
641
|
+
}).trim();
|
|
642
|
+
const issueHead = execSync(`git rev-parse ${remoteIssueBranch}`, {
|
|
643
|
+
encoding: "utf-8",
|
|
644
|
+
}).trim();
|
|
645
|
+
// Issue ブランチの HEAD が merge-base と同じなら、取り込み済み
|
|
646
|
+
if (mergeBase === issueHead) {
|
|
647
|
+
console.log(" ✅ Issue ブランチの変更は既に取り込み済み");
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
// Phase ブランチに Issue ブランチの変更をマージ(リモートで実行)
|
|
651
|
+
console.log(` 🔀 Issue ブランチの変更を取り込み: ${issueBranch} → ${phaseBranch}`);
|
|
652
|
+
const result = mergeRemoteBranches(phaseBranch, issueBranch, `Merge ${issueBranch} into ${phaseBranch}`);
|
|
653
|
+
if (!result.success) {
|
|
654
|
+
if (result.conflicted && result.worktreePath) {
|
|
655
|
+
// Claude Code でコンフリクト解消
|
|
656
|
+
const resolved = await resolveConflictWithClaude({
|
|
657
|
+
targetBranch: phaseBranch,
|
|
658
|
+
sourceBranch: issueBranch,
|
|
659
|
+
operationType: "phase"
|
|
660
|
+
}, result.worktreePath);
|
|
661
|
+
if (!resolved) {
|
|
662
|
+
// コンフリクト解消失敗 - worktree をクリーンアップ
|
|
663
|
+
try {
|
|
664
|
+
execSync(`git worktree remove "${result.worktreePath}" --force`, { stdio: "ignore" });
|
|
665
|
+
}
|
|
666
|
+
catch (_a) {
|
|
667
|
+
// クリーンアップ失敗は無視
|
|
668
|
+
}
|
|
669
|
+
throw new Error(`Phase ブランチ ${phaseBranch} への Issue ブランチ ${issueBranch} のマージでコンフリクトを解消できませんでした。`);
|
|
670
|
+
}
|
|
671
|
+
// コンフリクト解消成功 - プッシュしてクリーンアップ
|
|
672
|
+
try {
|
|
673
|
+
execSync(`git -C "${result.worktreePath}" push origin HEAD:${phaseBranch}`, { stdio: "pipe" });
|
|
674
|
+
console.log(" ✅ コンフリクト解消後のマージを完了しました");
|
|
675
|
+
}
|
|
676
|
+
finally {
|
|
677
|
+
execSync(`git worktree remove "${result.worktreePath}" --force`, { stdio: "ignore" });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
throw new Error(`マージに失敗しました: ${result.error}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
console.log(" ✅ マージ完了");
|
|
686
|
+
}
|
|
687
|
+
// ローカルブランチをリモートに合わせて更新
|
|
688
|
+
try {
|
|
689
|
+
execSync(`git fetch origin ${phaseBranch}`, { stdio: "pipe" });
|
|
690
|
+
execSync(`git branch -f ${phaseBranch} origin/${phaseBranch}`, { stdio: "pipe" });
|
|
691
|
+
}
|
|
692
|
+
catch (_b) {
|
|
693
|
+
// ローカルブランチの更新失敗は警告のみ(リモートは更新済み)
|
|
694
|
+
console.log(` ⚠️ ローカルブランチの更新をスキップ: ${phaseBranch}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
//# sourceMappingURL=branch-manager.js.map
|