claude-teammate 0.1.37 → 0.1.38
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/package.json +1 -1
- package/src/claude.js +127 -0
- package/src/commands/worker.js +94 -1
- package/src/github.js +81 -1
- package/src/repo.js +60 -0
- package/src/runtime.js +2 -1
package/package.json
CHANGED
package/src/claude.js
CHANGED
|
@@ -139,6 +139,34 @@ const GITHUB_PR_COMMENT_REVIEW_SCHEMA = {
|
|
|
139
139
|
required: ["decision", "comment_body", "epic_facts", "epic_guardrails"]
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
+
const GITHUB_PR_REVIEW_SCHEMA = {
|
|
143
|
+
type: "object",
|
|
144
|
+
additionalProperties: false,
|
|
145
|
+
properties: {
|
|
146
|
+
summary: {
|
|
147
|
+
type: "string"
|
|
148
|
+
},
|
|
149
|
+
suggestions: {
|
|
150
|
+
type: "array",
|
|
151
|
+
items: {
|
|
152
|
+
type: "object",
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
properties: {
|
|
155
|
+
file: { type: "string" },
|
|
156
|
+
line: { type: "number" },
|
|
157
|
+
side: { type: "string" },
|
|
158
|
+
body: { type: "string" },
|
|
159
|
+
classification: { type: "string", enum: ["Minor", "Major"] },
|
|
160
|
+
committable: { type: "boolean" },
|
|
161
|
+
suggestion: { type: "string" }
|
|
162
|
+
},
|
|
163
|
+
required: ["file", "line", "side", "body", "classification", "committable"]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
required: ["summary", "suggestions"]
|
|
168
|
+
};
|
|
169
|
+
|
|
142
170
|
const EPIC_MEMORY_CLEANUP_SCHEMA = {
|
|
143
171
|
type: "object",
|
|
144
172
|
additionalProperties: false,
|
|
@@ -502,6 +530,50 @@ export async function runClaudePullRequestCommentReview(input) {
|
|
|
502
530
|
return validateGitHubPullRequestCommentReviewResult(parseClaudeOutput(stdout));
|
|
503
531
|
}
|
|
504
532
|
|
|
533
|
+
export async function runClaudePrReview(input) {
|
|
534
|
+
if (!input.repoPath) {
|
|
535
|
+
throw new Error("PR review requires a local repository path.");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const repoPaths = [input.repoPath];
|
|
539
|
+
await Promise.all(repoPaths.map((repoPath) => clearLocalPlaywrightMcpConfig(repoPath)));
|
|
540
|
+
|
|
541
|
+
const args = [
|
|
542
|
+
"--print",
|
|
543
|
+
"--model",
|
|
544
|
+
input.model || DEFAULT_MODEL,
|
|
545
|
+
"--permission-mode",
|
|
546
|
+
"default",
|
|
547
|
+
"--strict-mcp-config",
|
|
548
|
+
"--tools",
|
|
549
|
+
"Read,Grep,Glob",
|
|
550
|
+
"--effort",
|
|
551
|
+
"medium",
|
|
552
|
+
"--output-format",
|
|
553
|
+
"json",
|
|
554
|
+
"--json-schema",
|
|
555
|
+
JSON.stringify(GITHUB_PR_REVIEW_SCHEMA),
|
|
556
|
+
"--append-system-prompt",
|
|
557
|
+
buildGitHubPRReviewSystemPrompt(),
|
|
558
|
+
buildGitHubPRReviewUserPrompt(input)
|
|
559
|
+
];
|
|
560
|
+
|
|
561
|
+
let stdout;
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
({ stdout } = await runClaudeCommand("claude", args, {
|
|
565
|
+
cwd: input.repoPath,
|
|
566
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
567
|
+
timeout: input.timeoutMs || DEFAULT_TIMEOUT_MS,
|
|
568
|
+
onSpawn: input.onSpawn
|
|
569
|
+
}));
|
|
570
|
+
} catch (error) {
|
|
571
|
+
throw new Error(formatClaudeInvocationError(error, input.timeoutMs || DEFAULT_TIMEOUT_MS));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return validateGitHubPrReviewResult(parseClaudeOutput(stdout));
|
|
575
|
+
}
|
|
576
|
+
|
|
505
577
|
export function parseClaudeOutput(output) {
|
|
506
578
|
const trimmed = output.trim();
|
|
507
579
|
if (!trimmed) {
|
|
@@ -522,6 +594,10 @@ export function parseClaudeOutput(output) {
|
|
|
522
594
|
return direct;
|
|
523
595
|
}
|
|
524
596
|
|
|
597
|
+
if ("summary" in direct && "suggestions" in direct) {
|
|
598
|
+
return direct;
|
|
599
|
+
}
|
|
600
|
+
|
|
525
601
|
if ("result" in direct && typeof direct.result === "string") {
|
|
526
602
|
return JSON.parse(direct.result);
|
|
527
603
|
}
|
|
@@ -634,6 +710,27 @@ function validateGitHubPullRequestCommentReviewResult(result) {
|
|
|
634
710
|
};
|
|
635
711
|
}
|
|
636
712
|
|
|
713
|
+
function validateGitHubPrReviewResult(result) {
|
|
714
|
+
if (!result || typeof result !== "object") {
|
|
715
|
+
throw new Error("Claude CLI returned an invalid GitHub PR review payload.");
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
summary: String(result.summary ?? "").trim(),
|
|
720
|
+
suggestions: Array.isArray(result.suggestions)
|
|
721
|
+
? result.suggestions.map((s) => ({
|
|
722
|
+
file: String(s.file ?? "").trim(),
|
|
723
|
+
line: Number(s.line) || 1,
|
|
724
|
+
side: String(s.side ?? "RIGHT").trim(),
|
|
725
|
+
body: String(s.body ?? "").trim(),
|
|
726
|
+
classification: ["Minor", "Major"].includes(s.classification) ? s.classification : "Minor",
|
|
727
|
+
committable: Boolean(s.committable),
|
|
728
|
+
suggestion: s.suggestion != null ? String(s.suggestion).trim() : undefined
|
|
729
|
+
}))
|
|
730
|
+
: []
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
637
734
|
function validateEpicMemoryCleanupResult(result) {
|
|
638
735
|
if (!result || typeof result !== "object") {
|
|
639
736
|
throw new Error("Claude CLI returned an invalid epic memory cleanup payload.");
|
|
@@ -931,6 +1028,20 @@ function buildGitHubPRCommentReviewSystemPrompt() {
|
|
|
931
1028
|
].join(" ");
|
|
932
1029
|
}
|
|
933
1030
|
|
|
1031
|
+
function buildGitHubPRReviewSystemPrompt() {
|
|
1032
|
+
return [
|
|
1033
|
+
"You are reviewing a GitHub pull request as a code reviewer.",
|
|
1034
|
+
"The primary input is the unified diff provided inline.",
|
|
1035
|
+
"You may also use Read, Grep, and Glob to read related code when the diff is ambiguous.",
|
|
1036
|
+
"Do not edit files, do not run mutating commands, and do not implement anything.",
|
|
1037
|
+
"Skip trivial style-only issues unless they introduce a real risk.",
|
|
1038
|
+
"Classify each suggestion as Minor (non-blocking) or Major (should be addressed before merge).",
|
|
1039
|
+
"Where possible, produce a committable suggestion with the corrected code.",
|
|
1040
|
+
"Return a summary paragraph describing what the PR does, followed by a summary of all suggestions.",
|
|
1041
|
+
"Return only structured output matching the provided schema."
|
|
1042
|
+
].join(" ");
|
|
1043
|
+
}
|
|
1044
|
+
|
|
934
1045
|
function buildEpicMemoryCleanupSystemPrompt() {
|
|
935
1046
|
return [
|
|
936
1047
|
"You are updating epic memory for an autonomous engineering workflow.",
|
|
@@ -1082,6 +1193,22 @@ Instructions:
|
|
|
1082
1193
|
- If blocked, return result=stuck with that summary explaining why it did not work.`;
|
|
1083
1194
|
}
|
|
1084
1195
|
|
|
1196
|
+
function buildGitHubPRReviewUserPrompt(input) {
|
|
1197
|
+
return `Review this GitHub pull request diff.
|
|
1198
|
+
|
|
1199
|
+
Pull request number: ${input.pr?.number ?? ""}
|
|
1200
|
+
Pull request title:
|
|
1201
|
+
${input.pr?.title ?? ""}
|
|
1202
|
+
|
|
1203
|
+
Repository path:
|
|
1204
|
+
${input.repoPath}
|
|
1205
|
+
|
|
1206
|
+
Unified diff:
|
|
1207
|
+
${input.diff || "(empty)"}
|
|
1208
|
+
|
|
1209
|
+
Review the diff carefully. Read related code files with Read, Grep, or Glob only when the diff alone is insufficient to judge correctness. Return a summary describing the PR and its suggestions.`;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1085
1212
|
function buildEpicMemoryCleanupUserPrompt(input) {
|
|
1086
1213
|
return `Update this epic memory facts and guardrails and clean it up.
|
|
1087
1214
|
|
package/src/commands/worker.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
runClaudeEpicMemoryCleanup,
|
|
9
9
|
runClaudeEpicMemorySummarize,
|
|
10
10
|
runClaudeImplementation,
|
|
11
|
+
runClaudePrReview,
|
|
11
12
|
runClaudePullRequestCommentReview,
|
|
12
13
|
runClaudeGitHubIssueReview
|
|
13
14
|
} from "../claude.js";
|
|
@@ -26,8 +27,10 @@ import {
|
|
|
26
27
|
saveIssueMemory
|
|
27
28
|
} from "../memory.js";
|
|
28
29
|
import {
|
|
30
|
+
checkoutPullRequestBranch,
|
|
29
31
|
commitAndPushRepoChanges,
|
|
30
32
|
ensureBranchFromDefault,
|
|
33
|
+
ensureReviewRepo,
|
|
31
34
|
hasUsableGitCheckout,
|
|
32
35
|
listMissingRepoPaths,
|
|
33
36
|
parseGitHubRepoUrl,
|
|
@@ -116,7 +119,12 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
116
119
|
draftPrCount: 0,
|
|
117
120
|
draftPrs: [],
|
|
118
121
|
prCommentReview: buildPrSubtaskState(previousState.prCommentReview),
|
|
119
|
-
prImplementation: buildPrSubtaskState(previousState.prImplementation)
|
|
122
|
+
prImplementation: buildPrSubtaskState(previousState.prImplementation),
|
|
123
|
+
lastReviewPollAt: null,
|
|
124
|
+
lastReviewSuccessAt: null,
|
|
125
|
+
lastReviewError: null,
|
|
126
|
+
reviewPrCount: 0,
|
|
127
|
+
reviewPrs: []
|
|
120
128
|
};
|
|
121
129
|
|
|
122
130
|
const updatePrSubtaskState = async (key, updates) => {
|
|
@@ -136,6 +144,7 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
136
144
|
let jiraPolling = false;
|
|
137
145
|
let githubPolling = false;
|
|
138
146
|
let prPolling = false;
|
|
147
|
+
let reviewPolling = false;
|
|
139
148
|
|
|
140
149
|
const shutdown = async (signal) => {
|
|
141
150
|
if (stopping) {
|
|
@@ -344,9 +353,90 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
344
353
|
}
|
|
345
354
|
};
|
|
346
355
|
|
|
356
|
+
const pollReviewPrs = async () => {
|
|
357
|
+
if (reviewPolling || stopping) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
reviewPolling = true;
|
|
362
|
+
state.lastReviewPollAt = new Date().toISOString();
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const repos = await listKnownRepos(projectRoot);
|
|
366
|
+
const processedPrs = [];
|
|
367
|
+
let reviewedPrCount = 0;
|
|
368
|
+
|
|
369
|
+
for (const repo of repos) {
|
|
370
|
+
const prs = await github.listPrsNeedingReview(repo.url, githubBotUser.login);
|
|
371
|
+
reviewedPrCount += prs.length;
|
|
372
|
+
|
|
373
|
+
for (const pr of prs) {
|
|
374
|
+
try {
|
|
375
|
+
const repoPath = await ensureReviewRepo(repo.url, runtimePaths.reviewReposDir);
|
|
376
|
+
const prDetail = await github.fetchPullRequest(repo.url, pr.number);
|
|
377
|
+
await checkoutPullRequestBranch(repoPath, prDetail.headRef, prDetail.headRepoCloneUrl);
|
|
378
|
+
|
|
379
|
+
const diff = await github.fetchPullRequestDiff(repo.url, pr.number);
|
|
380
|
+
const result = await runClaudePrReview({
|
|
381
|
+
diff,
|
|
382
|
+
repoPath,
|
|
383
|
+
pr: prDetail,
|
|
384
|
+
timeoutMs: parseOptionalInt(values.CLAUDE_TIMEOUT_MS)
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
await github.createPullRequestReview(
|
|
388
|
+
repo.url,
|
|
389
|
+
pr.number,
|
|
390
|
+
result.summary,
|
|
391
|
+
result.suggestions,
|
|
392
|
+
"COMMENT"
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
await github.addLabelsToPullRequest(repo.url, pr.number, ["AI-reviewed"]);
|
|
396
|
+
|
|
397
|
+
processedPrs.push({
|
|
398
|
+
repoUrl: repo.url,
|
|
399
|
+
pullRequestNumber: String(pr.number),
|
|
400
|
+
pullRequestUrl: prDetail.url,
|
|
401
|
+
suggestionsCount: result.suggestions.length
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
await logger.info("PR review submitted", {
|
|
405
|
+
repo: repo.url,
|
|
406
|
+
pr: pr.number,
|
|
407
|
+
suggestions: result.suggestions.length
|
|
408
|
+
});
|
|
409
|
+
} catch (error) {
|
|
410
|
+
await logger.error("PR review failed", {
|
|
411
|
+
repo: repo.url,
|
|
412
|
+
pr: pr.number,
|
|
413
|
+
error
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
state.lastReviewSuccessAt = new Date().toISOString();
|
|
420
|
+
state.lastReviewError = null;
|
|
421
|
+
state.reviewPrCount = reviewedPrCount;
|
|
422
|
+
state.reviewPrs = processedPrs.slice(0, 20);
|
|
423
|
+
await writeState(runtimePaths.stateFile, state);
|
|
424
|
+
await logger.info("PR review poll complete", {
|
|
425
|
+
reviewed: reviewedPrCount
|
|
426
|
+
});
|
|
427
|
+
} catch (error) {
|
|
428
|
+
state.lastReviewError = error instanceof Error ? error.message : String(error);
|
|
429
|
+
await writeState(runtimePaths.stateFile, state);
|
|
430
|
+
await logger.error("PR review poll failed", { error });
|
|
431
|
+
} finally {
|
|
432
|
+
reviewPolling = false;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
347
436
|
await pollJira();
|
|
348
437
|
await pollGitHub();
|
|
349
438
|
await pollDraftPrs();
|
|
439
|
+
await pollReviewPrs();
|
|
350
440
|
setInterval(() => {
|
|
351
441
|
void pollJira();
|
|
352
442
|
}, POLL_INTERVAL_MS);
|
|
@@ -356,6 +446,9 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
356
446
|
setInterval(() => {
|
|
357
447
|
void pollDraftPrs();
|
|
358
448
|
}, POLL_INTERVAL_MS);
|
|
449
|
+
setInterval(() => {
|
|
450
|
+
void pollReviewPrs();
|
|
451
|
+
}, POLL_INTERVAL_MS);
|
|
359
452
|
}
|
|
360
453
|
|
|
361
454
|
async function processJiraIssue({ issue, jira, github, botUser, config, projectRoot, runtimePaths, logger }) {
|
package/src/github.js
CHANGED
|
@@ -290,6 +290,85 @@ export function createGitHubClient(config) {
|
|
|
290
290
|
return payload.default_branch;
|
|
291
291
|
},
|
|
292
292
|
|
|
293
|
+
async listPrsNeedingReview(repoUrl, botLogin) {
|
|
294
|
+
const repo = parseGitHubRepoUrl(repoUrl);
|
|
295
|
+
const query = `repo:${repo.owner}/${repo.name}+is:pr+is:open+review-requested:${botLogin}+-label:AI-reviewed`;
|
|
296
|
+
const url = new URL("https://api.github.com/search/issues");
|
|
297
|
+
url.searchParams.set("q", query);
|
|
298
|
+
url.searchParams.set("per_page", "10");
|
|
299
|
+
|
|
300
|
+
const payload = await requestGitHub(url, config, { method: "GET" }, repo);
|
|
301
|
+
return Array.isArray(payload.items)
|
|
302
|
+
? payload.items.map((item) => ({
|
|
303
|
+
number: item.number,
|
|
304
|
+
title: item.title ?? ""
|
|
305
|
+
}))
|
|
306
|
+
: [];
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
async addLabelsToPullRequest(repoUrl, prNumber, labels) {
|
|
310
|
+
const repo = parseGitHubRepoUrl(repoUrl);
|
|
311
|
+
return requestGitHub(
|
|
312
|
+
`https://api.github.com/repos/${repo.owner}/${repo.name}/issues/${prNumber}/labels`,
|
|
313
|
+
config,
|
|
314
|
+
{
|
|
315
|
+
method: "POST",
|
|
316
|
+
body: JSON.stringify({ labels })
|
|
317
|
+
},
|
|
318
|
+
repo
|
|
319
|
+
);
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
async createPullRequestReview(repoUrl, prNumber, body, suggestions, event) {
|
|
323
|
+
const repo = parseGitHubRepoUrl(repoUrl);
|
|
324
|
+
const comments = Array.isArray(suggestions)
|
|
325
|
+
? suggestions.map((s) => {
|
|
326
|
+
let commentBody = String(s.body || "").trim();
|
|
327
|
+
if (s.committable && s.suggestion) {
|
|
328
|
+
commentBody = `${commentBody}\n\n\`\`\`suggestion\n${s.suggestion}\n\`\`\``.trim();
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
path: String(s.file || ""),
|
|
332
|
+
line: Number(s.line) || 1,
|
|
333
|
+
side: String(s.side || "RIGHT").toUpperCase(),
|
|
334
|
+
body: commentBody
|
|
335
|
+
};
|
|
336
|
+
})
|
|
337
|
+
: [];
|
|
338
|
+
|
|
339
|
+
return requestGitHub(
|
|
340
|
+
`https://api.github.com/repos/${repo.owner}/${repo.name}/pulls/${prNumber}/reviews`,
|
|
341
|
+
config,
|
|
342
|
+
{
|
|
343
|
+
method: "POST",
|
|
344
|
+
body: JSON.stringify({ body: String(body || "").trim(), event: event || "COMMENT", comments })
|
|
345
|
+
},
|
|
346
|
+
repo
|
|
347
|
+
);
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
async fetchPullRequestDiff(repoUrl, prNumber) {
|
|
351
|
+
const repo = parseGitHubRepoUrl(repoUrl);
|
|
352
|
+
const response = await fetch(
|
|
353
|
+
`https://api.github.com/repos/${repo.owner}/${repo.name}/pulls/${prNumber}`,
|
|
354
|
+
{
|
|
355
|
+
headers: {
|
|
356
|
+
Accept: "application/vnd.github.v3.diff",
|
|
357
|
+
Authorization: `Bearer ${config.GITHUB_PAT}`,
|
|
358
|
+
"User-Agent": "claude-teammate"
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
);
|
|
362
|
+
if (!response.ok) {
|
|
363
|
+
const errorBody = await response.text();
|
|
364
|
+
throw new Error(buildGitHubRequestError(response.status, repo, errorBody, {
|
|
365
|
+
method: "GET",
|
|
366
|
+
path: `/repos/${repo.owner}/${repo.name}/pulls/${prNumber}`
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
return response.text();
|
|
370
|
+
},
|
|
371
|
+
|
|
293
372
|
async createPullRequest(repoUrl, pullRequest) {
|
|
294
373
|
const repo = parseGitHubRepoUrl(repoUrl);
|
|
295
374
|
const payload = await requestGitHub(
|
|
@@ -347,7 +426,8 @@ function mapGitHubPullRequestSummary(payload) {
|
|
|
347
426
|
id: payload.user?.id ?? null
|
|
348
427
|
},
|
|
349
428
|
headRef: payload.head?.ref ?? "",
|
|
350
|
-
baseRef: payload.base?.ref ?? ""
|
|
429
|
+
baseRef: payload.base?.ref ?? "",
|
|
430
|
+
headRepoCloneUrl: payload.head?.repo?.clone_url ?? null
|
|
351
431
|
};
|
|
352
432
|
}
|
|
353
433
|
|
package/src/repo.js
CHANGED
|
@@ -54,6 +54,66 @@ export async function validateOrEnsureLocalRepo(repoUrl, reposDir, localPath = "
|
|
|
54
54
|
return ensureLocalRepo(repoUrl, reposDir);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export async function ensureReviewRepo(repoUrl, reviewReposDir) {
|
|
58
|
+
const repo = parseGitHubRepoUrl(repoUrl);
|
|
59
|
+
const checkoutPath = path.join(reviewReposDir, repo.owner, repo.name);
|
|
60
|
+
|
|
61
|
+
if (await isUsableGitCheckout(checkoutPath)) {
|
|
62
|
+
await execGit(checkoutPath, ["checkout", "--", "."]);
|
|
63
|
+
await execGit(checkoutPath, ["clean", "-fd"]);
|
|
64
|
+
return checkoutPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await mkdir(path.dirname(checkoutPath), { recursive: true });
|
|
68
|
+
await execFileAsync("git", ["clone", repo.cloneUrl, checkoutPath], {
|
|
69
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
70
|
+
env: buildGitEnv(repo.cloneUrl)
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return checkoutPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function checkoutPullRequestBranch(repoPath, branchName, headRepoCloneUrl = null) {
|
|
77
|
+
const originUrl = await execGitOutput(repoPath, ["remote", "get-url", "origin"]);
|
|
78
|
+
const normalizedOrigin = originUrl.trim().replace(/\.git$/u, "").toLowerCase();
|
|
79
|
+
const normalizedHead = headRepoCloneUrl
|
|
80
|
+
? headRepoCloneUrl.trim().replace(/\.git$/u, "").toLowerCase()
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
const isFork = normalizedHead && normalizedHead !== normalizedOrigin;
|
|
84
|
+
|
|
85
|
+
if (isFork) {
|
|
86
|
+
const remoteName = "pr-head";
|
|
87
|
+
let remoteExists = false;
|
|
88
|
+
try {
|
|
89
|
+
await execGitOutput(repoPath, ["remote", "get-url", remoteName]);
|
|
90
|
+
remoteExists = true;
|
|
91
|
+
} catch {
|
|
92
|
+
remoteExists = false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (remoteExists) {
|
|
96
|
+
await execGit(repoPath, ["remote", "set-url", remoteName, headRepoCloneUrl]);
|
|
97
|
+
} else {
|
|
98
|
+
await execFileAsync("git", ["remote", "add", remoteName, headRepoCloneUrl], {
|
|
99
|
+
cwd: repoPath,
|
|
100
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
101
|
+
env: buildGitEnv(headRepoCloneUrl)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await execFileAsync("git", ["fetch", remoteName], {
|
|
106
|
+
cwd: repoPath,
|
|
107
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
108
|
+
env: buildGitEnv(headRepoCloneUrl)
|
|
109
|
+
});
|
|
110
|
+
await execGit(repoPath, ["checkout", "-B", branchName, `${remoteName}/${branchName}`]);
|
|
111
|
+
} else {
|
|
112
|
+
await execGit(repoPath, ["fetch", "origin"]);
|
|
113
|
+
await execGit(repoPath, ["checkout", "-B", branchName, `origin/${branchName}`]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
57
117
|
export async function ensureBranchFromDefault(repoPath, branchName, baseBranch) {
|
|
58
118
|
try {
|
|
59
119
|
await execGit(repoPath, ["fetch", "origin", baseBranch, "--prune"]);
|
package/src/runtime.js
CHANGED
|
@@ -11,7 +11,8 @@ export function getRuntimePaths(projectRoot) {
|
|
|
11
11
|
pidFile: path.join(runtimeDir, "worker.pid"),
|
|
12
12
|
stateFile: path.join(runtimeDir, "state.json"),
|
|
13
13
|
logFile: path.join(runtimeDir, "worker.log"),
|
|
14
|
-
reposDir: path.join(runtimeDir, "repos")
|
|
14
|
+
reposDir: path.join(runtimeDir, "repos"),
|
|
15
|
+
reviewReposDir: path.join(runtimeDir, "review-repos")
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
|