@ilya-lesikov/pi-pi 0.1.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/3p/pi-ask-user/index.ts +1835 -0
- package/3p/pi-ask-user/package.json +50 -0
- package/3p/pi-ask-user/single-select-layout.ts +203 -0
- package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
- package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
- package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
- package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
- package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
- package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
- package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
- package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
- package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
- package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
- package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
- package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
- package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
- package/3p/pi-lsp/package.json +54 -0
- package/3p/pi-plannotator/AGENTS.md +583 -0
- package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
- package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
- package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
- package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
- package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
- package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
- package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
- package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
- package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
- package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
- package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
- package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
- package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
- package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
- package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
- package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
- package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
- package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
- package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
- package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
- package/3p/pi-plannotator/package.json +53 -0
- package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
- package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
- package/3p/pi-plannotator/packages/ai/context.ts +250 -0
- package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
- package/3p/pi-plannotator/packages/ai/index.ts +105 -0
- package/3p/pi-plannotator/packages/ai/package.json +21 -0
- package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
- package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
- package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
- package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
- package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
- package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
- package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/ai/types.ts +379 -0
- package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
- package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
- package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
- package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
- package/3p/pi-plannotator/packages/server/browser.ts +245 -0
- package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
- package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
- package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
- package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
- package/3p/pi-plannotator/packages/server/config.ts +8 -0
- package/3p/pi-plannotator/packages/server/draft.ts +1 -0
- package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
- package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
- package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
- package/3p/pi-plannotator/packages/server/git.ts +141 -0
- package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
- package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
- package/3p/pi-plannotator/packages/server/ide.ts +43 -0
- package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
- package/3p/pi-plannotator/packages/server/image.ts +66 -0
- package/3p/pi-plannotator/packages/server/index.ts +648 -0
- package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
- package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
- package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
- package/3p/pi-plannotator/packages/server/jj.ts +86 -0
- package/3p/pi-plannotator/packages/server/p4.ts +417 -0
- package/3p/pi-plannotator/packages/server/package.json +40 -0
- package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
- package/3p/pi-plannotator/packages/server/pr.ts +146 -0
- package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
- package/3p/pi-plannotator/packages/server/project.ts +45 -0
- package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
- package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
- package/3p/pi-plannotator/packages/server/remote.ts +74 -0
- package/3p/pi-plannotator/packages/server/repo.ts +80 -0
- package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
- package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
- package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
- package/3p/pi-plannotator/packages/server/review.ts +1318 -0
- package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
- package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
- package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
- package/3p/pi-plannotator/packages/server/storage.ts +17 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
- package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
- package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
- package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
- package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
- package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
- package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
- package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
- package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
- package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
- package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
- package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
- package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
- package/3p/pi-plannotator/packages/shared/config.ts +262 -0
- package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
- package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
- package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
- package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
- package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
- package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
- package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
- package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
- package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
- package/3p/pi-plannotator/packages/shared/package.json +55 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
- package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
- package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
- package/3p/pi-plannotator/packages/shared/project.ts +71 -0
- package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
- package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
- package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
- package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
- package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
- package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
- package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
- package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
- package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
- package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
- package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
- package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
- package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
- package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/shared/types.ts +29 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
- package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
- package/3p/pi-subagents/package.json +50 -0
- package/3p/pi-subagents/src/agent-manager.ts +413 -0
- package/3p/pi-subagents/src/agent-runner.ts +502 -0
- package/3p/pi-subagents/src/agent-types.ts +248 -0
- package/3p/pi-subagents/src/context.ts +58 -0
- package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
- package/3p/pi-subagents/src/custom-agents.ts +137 -0
- package/3p/pi-subagents/src/default-agents.ts +144 -0
- package/3p/pi-subagents/src/env.ts +33 -0
- package/3p/pi-subagents/src/group-join.ts +141 -0
- package/3p/pi-subagents/src/index.ts +1811 -0
- package/3p/pi-subagents/src/invocation-config.ts +40 -0
- package/3p/pi-subagents/src/memory.ts +165 -0
- package/3p/pi-subagents/src/model-resolver.ts +81 -0
- package/3p/pi-subagents/src/output-file.ts +77 -0
- package/3p/pi-subagents/src/prompts.ts +85 -0
- package/3p/pi-subagents/src/skill-loader.ts +79 -0
- package/3p/pi-subagents/src/types.ts +111 -0
- package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
- package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
- package/3p/pi-subagents/src/worktree.ts +162 -0
- package/3p/pi-tasks/package.json +52 -0
- package/3p/pi-tasks/src/auto-clear.ts +91 -0
- package/3p/pi-tasks/src/index.ts +1150 -0
- package/3p/pi-tasks/src/process-tracker.ts +140 -0
- package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
- package/3p/pi-tasks/src/task-store.ts +324 -0
- package/3p/pi-tasks/src/tasks-config.ts +27 -0
- package/3p/pi-tasks/src/types.ts +40 -0
- package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
- package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
- package/AGENTS.md +28 -0
- package/LICENSE +201 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
- package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
- package/extensions/orchestrator/agents/explore.ts +33 -0
- package/extensions/orchestrator/agents/librarian.ts +43 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
- package/extensions/orchestrator/agents/planner.ts +61 -0
- package/extensions/orchestrator/agents/registry.ts +156 -0
- package/extensions/orchestrator/agents/task.ts +50 -0
- package/extensions/orchestrator/agents/tool-routing.ts +84 -0
- package/extensions/orchestrator/ast-search.ts +85 -0
- package/extensions/orchestrator/cbm.ts +330 -0
- package/extensions/orchestrator/command-handlers.test.ts +163 -0
- package/extensions/orchestrator/command-handlers.ts +116 -0
- package/extensions/orchestrator/commands.test.ts +81 -0
- package/extensions/orchestrator/commands.ts +75 -0
- package/extensions/orchestrator/config.test.ts +221 -0
- package/extensions/orchestrator/config.ts +230 -0
- package/extensions/orchestrator/context.test.ts +293 -0
- package/extensions/orchestrator/context.ts +206 -0
- package/extensions/orchestrator/event-handlers.test.ts +190 -0
- package/extensions/orchestrator/event-handlers.ts +1416 -0
- package/extensions/orchestrator/exa.ts +104 -0
- package/extensions/orchestrator/flant-infra.ts +486 -0
- package/extensions/orchestrator/index.ts +80 -0
- package/extensions/orchestrator/integration.test.ts +1214 -0
- package/extensions/orchestrator/orchestrator.test.ts +252 -0
- package/extensions/orchestrator/orchestrator.ts +538 -0
- package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
- package/extensions/orchestrator/phases/brainstorm.ts +273 -0
- package/extensions/orchestrator/phases/implementation.ts +40 -0
- package/extensions/orchestrator/phases/machine.test.ts +293 -0
- package/extensions/orchestrator/phases/machine.ts +209 -0
- package/extensions/orchestrator/phases/planning.ts +255 -0
- package/extensions/orchestrator/phases/review.ts +193 -0
- package/extensions/orchestrator/plannotator.ts +56 -0
- package/extensions/orchestrator/pp-menu.ts +866 -0
- package/extensions/orchestrator/state.test.ts +343 -0
- package/extensions/orchestrator/state.ts +237 -0
- package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
- package/extensions/orchestrator/validate-artifacts.ts +272 -0
- package/extensions/orchestrator/vendor.d.ts +26 -0
- package/package.json +73 -0
- package/scripts/postinstall.sh +18 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DiffResult,
|
|
3
|
+
type DiffType,
|
|
4
|
+
type GitContext,
|
|
5
|
+
type GitDiffOptions,
|
|
6
|
+
type ReviewGitRuntime,
|
|
7
|
+
detectRemoteDefaultBranch,
|
|
8
|
+
getFileContentsForDiff as getGitFileContentsForDiff,
|
|
9
|
+
getGitContext,
|
|
10
|
+
gitAddFile,
|
|
11
|
+
gitResetFile,
|
|
12
|
+
parseWorktreeDiffType,
|
|
13
|
+
runGitDiff,
|
|
14
|
+
} from "./review-core";
|
|
15
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
type ReviewJjRuntime,
|
|
18
|
+
detectJjWorkspace,
|
|
19
|
+
getJjContext,
|
|
20
|
+
getJjFileContentsForDiff,
|
|
21
|
+
runJjDiff,
|
|
22
|
+
} from "./jj-core";
|
|
23
|
+
|
|
24
|
+
export type {
|
|
25
|
+
DiffOption,
|
|
26
|
+
DiffResult,
|
|
27
|
+
DiffType,
|
|
28
|
+
GitContext,
|
|
29
|
+
GitDiffOptions,
|
|
30
|
+
WorktreeInfo,
|
|
31
|
+
} from "./review-core";
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
JJ_TRUNK_REVSET,
|
|
35
|
+
jjCompareTargetRevset,
|
|
36
|
+
jjLineBaseRevset,
|
|
37
|
+
parseRemoteBookmark,
|
|
38
|
+
parseWorktreeDiffType,
|
|
39
|
+
validateFilePath,
|
|
40
|
+
} from "./review-core";
|
|
41
|
+
|
|
42
|
+
export interface VcsProvider {
|
|
43
|
+
readonly id: string;
|
|
44
|
+
detect(cwd?: string): Promise<boolean>;
|
|
45
|
+
getRoot?(cwd?: string): Promise<string | null>;
|
|
46
|
+
ownsDiffType(diffType: string): boolean;
|
|
47
|
+
canStageFiles?(diffType: string): boolean;
|
|
48
|
+
getContext(cwd?: string): Promise<GitContext>;
|
|
49
|
+
runDiff(diffType: DiffType, defaultBranch: string, cwd?: string, options?: GitDiffOptions): Promise<DiffResult>;
|
|
50
|
+
getFileContents(
|
|
51
|
+
diffType: DiffType,
|
|
52
|
+
defaultBranch: string,
|
|
53
|
+
filePath: string,
|
|
54
|
+
oldPath?: string,
|
|
55
|
+
cwd?: string,
|
|
56
|
+
): Promise<{ oldContent: string | null; newContent: string | null }>;
|
|
57
|
+
stageFile?(filePath: string, cwd?: string): Promise<void>;
|
|
58
|
+
unstageFile?(filePath: string, cwd?: string): Promise<void>;
|
|
59
|
+
resolveCwd?(diffType: string, fallbackCwd?: string): string | undefined;
|
|
60
|
+
detectRemoteDefaultCompareTarget?(cwd?: string): Promise<string | null>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type VcsSelection = "auto" | "git" | "jj" | "p4";
|
|
64
|
+
|
|
65
|
+
export interface VcsApi {
|
|
66
|
+
detectVcs(cwd?: string): Promise<VcsProvider>;
|
|
67
|
+
detectManagedVcs(cwd?: string, vcsType?: VcsSelection): Promise<VcsProvider | null>;
|
|
68
|
+
getVcsContext(cwd?: string, vcsType?: VcsSelection): Promise<GitContext>;
|
|
69
|
+
detectRemoteDefaultCompareTarget(cwd?: string, vcsType?: VcsSelection): Promise<string | null>;
|
|
70
|
+
prepareLocalReviewDiff(options: PrepareLocalReviewDiffOptions): Promise<PreparedLocalReviewDiff>;
|
|
71
|
+
runVcsDiff(
|
|
72
|
+
diffType: DiffType,
|
|
73
|
+
defaultBranch?: string,
|
|
74
|
+
cwd?: string,
|
|
75
|
+
options?: GitDiffOptions,
|
|
76
|
+
): Promise<DiffResult>;
|
|
77
|
+
getVcsFileContentsForDiff(
|
|
78
|
+
diffType: DiffType,
|
|
79
|
+
defaultBranch: string,
|
|
80
|
+
filePath: string,
|
|
81
|
+
oldPath?: string,
|
|
82
|
+
cwd?: string,
|
|
83
|
+
): Promise<{ oldContent: string | null; newContent: string | null }>;
|
|
84
|
+
canStageFiles(diffType: string, cwd?: string): Promise<boolean>;
|
|
85
|
+
stageFile(diffType: string, filePath: string, cwd?: string): Promise<void>;
|
|
86
|
+
unstageFile(diffType: string, filePath: string, cwd?: string): Promise<void>;
|
|
87
|
+
resolveVcsCwd(diffType: string, fallbackCwd?: string): string | undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface PrepareLocalReviewDiffOptions {
|
|
91
|
+
cwd?: string;
|
|
92
|
+
vcsType?: VcsSelection;
|
|
93
|
+
requestedDiffType?: DiffType;
|
|
94
|
+
requestedBase?: string;
|
|
95
|
+
configuredDiffType: DiffType;
|
|
96
|
+
hideWhitespace?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface PreparedLocalReviewDiff {
|
|
100
|
+
gitContext: GitContext;
|
|
101
|
+
diffType: DiffType;
|
|
102
|
+
base: string;
|
|
103
|
+
rawPatch: string;
|
|
104
|
+
gitRef: string;
|
|
105
|
+
error?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const GIT_DIFF_TYPES = new Set(["uncommitted", "staged", "unstaged", "last-commit", "branch", "merge-base", "all"]);
|
|
109
|
+
const JJ_DIFF_TYPES = new Set(["jj-current", "jj-last", "jj-line", "jj-evolog", "jj-all"]);
|
|
110
|
+
|
|
111
|
+
function selectNearestProvider(
|
|
112
|
+
candidates: Array<{ provider: VcsProvider; root: string | null; order: number }>,
|
|
113
|
+
cwd?: string,
|
|
114
|
+
): VcsProvider | null {
|
|
115
|
+
if (candidates.length === 0) return null;
|
|
116
|
+
|
|
117
|
+
const effectiveCwd = resolve(cwd ?? process.cwd());
|
|
118
|
+
const ranked = candidates
|
|
119
|
+
.map((candidate) => ({
|
|
120
|
+
...candidate,
|
|
121
|
+
rootDepth: candidate.root ? vcsRootDepth(candidate.root) : -1,
|
|
122
|
+
containsCwd: candidate.root ? isSameOrAncestor(candidate.root, effectiveCwd) : false,
|
|
123
|
+
}))
|
|
124
|
+
.sort((a, b) => {
|
|
125
|
+
if (a.containsCwd !== b.containsCwd) return a.containsCwd ? -1 : 1;
|
|
126
|
+
if (a.rootDepth !== b.rootDepth) return b.rootDepth - a.rootDepth;
|
|
127
|
+
return a.order - b.order;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return ranked[0]?.provider ?? null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isSameOrAncestor(root: string, child: string): boolean {
|
|
134
|
+
const relativePath = relative(resolve(root), child);
|
|
135
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function vcsRootDepth(root: string): number {
|
|
139
|
+
return resolve(root).split(/[\\/]+/).filter(Boolean).length;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createGitProvider(runtime: ReviewGitRuntime): VcsProvider {
|
|
143
|
+
return {
|
|
144
|
+
id: "git",
|
|
145
|
+
|
|
146
|
+
async detect(cwd?: string): Promise<boolean> {
|
|
147
|
+
try {
|
|
148
|
+
const result = await runtime.runGit(["rev-parse", "--is-inside-work-tree"], { cwd });
|
|
149
|
+
return result.exitCode === 0;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
async getRoot(cwd?: string): Promise<string | null> {
|
|
156
|
+
const result = await runtime.runGit(["rev-parse", "--show-toplevel"], { cwd });
|
|
157
|
+
return result.exitCode === 0 ? result.stdout.trim() || null : null;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
ownsDiffType(diffType: string): boolean {
|
|
161
|
+
return GIT_DIFF_TYPES.has(diffType) || diffType.startsWith("worktree:");
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
canStageFiles(diffType: string): boolean {
|
|
165
|
+
const effectiveDiffType = parseWorktreeDiffType(diffType)?.subType ?? diffType;
|
|
166
|
+
return effectiveDiffType === "uncommitted" || effectiveDiffType === "unstaged";
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
getContext(cwd?: string): Promise<GitContext> {
|
|
170
|
+
return getGitContext(runtime, cwd);
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
runDiff(diffType: DiffType, defaultBranch: string, cwd?: string, options?: GitDiffOptions): Promise<DiffResult> {
|
|
174
|
+
return runGitDiff(runtime, diffType, defaultBranch, cwd, options);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
getFileContents(diffType, defaultBranch, filePath, oldPath?, cwd?) {
|
|
178
|
+
return getGitFileContentsForDiff(runtime, diffType, defaultBranch, filePath, oldPath, cwd);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
stageFile(filePath: string, cwd?: string): Promise<void> {
|
|
182
|
+
return gitAddFile(runtime, filePath, cwd);
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
unstageFile(filePath: string, cwd?: string): Promise<void> {
|
|
186
|
+
return gitResetFile(runtime, filePath, cwd);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
detectRemoteDefaultCompareTarget(cwd?: string): Promise<string | null> {
|
|
190
|
+
return detectRemoteDefaultBranch(runtime, cwd);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
resolveCwd(diffType: string, fallbackCwd?: string): string | undefined {
|
|
194
|
+
const parsed = parseWorktreeDiffType(diffType);
|
|
195
|
+
return parsed?.path ?? fallbackCwd;
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function createJjProvider(runtime: ReviewJjRuntime): VcsProvider {
|
|
201
|
+
return {
|
|
202
|
+
id: "jj",
|
|
203
|
+
|
|
204
|
+
async detect(cwd?: string): Promise<boolean> {
|
|
205
|
+
return (await detectJjWorkspace(runtime, cwd)) !== null;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
getRoot(cwd?: string): Promise<string | null> {
|
|
209
|
+
return detectJjWorkspace(runtime, cwd);
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
ownsDiffType(diffType: string): boolean {
|
|
213
|
+
return JJ_DIFF_TYPES.has(diffType);
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
getContext(cwd?: string): Promise<GitContext> {
|
|
217
|
+
return getJjContext(runtime, cwd);
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
runDiff(diffType: DiffType, defaultBranch: string, cwd?: string, options?: GitDiffOptions): Promise<DiffResult> {
|
|
221
|
+
return runJjDiff(runtime, diffType, defaultBranch, cwd, options);
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
getFileContents(diffType, defaultBranch, filePath, oldPath?, cwd?) {
|
|
225
|
+
return getJjFileContentsForDiff(runtime, diffType, defaultBranch, filePath, oldPath, cwd);
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function createVcsApi(providers: readonly VcsProvider[]): VcsApi {
|
|
231
|
+
const providerList = [...providers];
|
|
232
|
+
const defaultProvider = providerList.find((provider) => provider.id === "git") ?? providerList[0];
|
|
233
|
+
const vcsCache = new Map<string, VcsProvider>();
|
|
234
|
+
const managedVcsCache = new Map<string, VcsProvider>();
|
|
235
|
+
|
|
236
|
+
if (!defaultProvider) {
|
|
237
|
+
throw new Error("createVcsApi requires at least one provider");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function collectDetectedProviders(cwd?: string): Promise<Array<{ provider: VcsProvider; root: string | null; order: number }>> {
|
|
241
|
+
const candidates: Array<{ provider: VcsProvider; root: string | null; order: number }> = [];
|
|
242
|
+
for (let index = 0; index < providerList.length; index++) {
|
|
243
|
+
const provider = providerList[index];
|
|
244
|
+
let root: string | null = null;
|
|
245
|
+
let detected = false;
|
|
246
|
+
try {
|
|
247
|
+
if (provider.getRoot) {
|
|
248
|
+
root = await provider.getRoot(cwd);
|
|
249
|
+
detected = root !== null;
|
|
250
|
+
} else {
|
|
251
|
+
detected = await provider.detect(cwd);
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (detected) {
|
|
257
|
+
candidates.push({ provider, root, order: index });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return candidates;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function detectManagedVcs(cwd?: string, vcsType?: VcsSelection): Promise<VcsProvider | null> {
|
|
264
|
+
const key = `${vcsType ?? "auto"}:${cwd ?? process.cwd()}`;
|
|
265
|
+
const cached = managedVcsCache.get(key);
|
|
266
|
+
if (cached) return cached;
|
|
267
|
+
|
|
268
|
+
if (vcsType && vcsType !== "auto") {
|
|
269
|
+
const provider = getProviderById(vcsType);
|
|
270
|
+
let detected = false;
|
|
271
|
+
try {
|
|
272
|
+
detected = provider ? await provider.detect(cwd) : false;
|
|
273
|
+
} catch {
|
|
274
|
+
detected = false;
|
|
275
|
+
}
|
|
276
|
+
const result = detected ? provider : null;
|
|
277
|
+
if (result) managedVcsCache.set(key, result);
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const candidates = await collectDetectedProviders(cwd);
|
|
282
|
+
const detected = selectNearestProvider(candidates, cwd);
|
|
283
|
+
if (detected) managedVcsCache.set(key, detected);
|
|
284
|
+
return detected;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function detectVcs(cwd?: string): Promise<VcsProvider> {
|
|
288
|
+
const key = cwd ?? process.cwd();
|
|
289
|
+
const cached = vcsCache.get(key);
|
|
290
|
+
if (cached) return cached;
|
|
291
|
+
|
|
292
|
+
const detected = (await detectManagedVcs(cwd)) ?? defaultProvider;
|
|
293
|
+
vcsCache.set(key, detected);
|
|
294
|
+
return detected;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getProviderForDiffType(diffType: string): VcsProvider | null {
|
|
298
|
+
for (const provider of providerList) {
|
|
299
|
+
if (provider.ownsDiffType(diffType)) {
|
|
300
|
+
return provider;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function getProviderById(id: Exclude<VcsSelection, "auto">): VcsProvider | null {
|
|
307
|
+
return providerList.find((provider) => provider.id === id) ?? null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function formatVcsName(id: Exclude<VcsSelection, "auto">): string {
|
|
311
|
+
switch (id) {
|
|
312
|
+
case "git":
|
|
313
|
+
return "Git";
|
|
314
|
+
case "jj":
|
|
315
|
+
return "JJ";
|
|
316
|
+
case "p4":
|
|
317
|
+
return "P4";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function getProviderForSelection(
|
|
322
|
+
vcsType: VcsSelection | undefined,
|
|
323
|
+
cwd?: string,
|
|
324
|
+
): Promise<VcsProvider> {
|
|
325
|
+
if (!vcsType || vcsType === "auto") {
|
|
326
|
+
return detectVcs(cwd);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const provider = getProviderById(vcsType);
|
|
330
|
+
const vcsName = formatVcsName(vcsType);
|
|
331
|
+
if (!provider) {
|
|
332
|
+
throw new Error(`${vcsName} support is not available in this runtime.`);
|
|
333
|
+
}
|
|
334
|
+
if (!(await provider.detect(cwd))) {
|
|
335
|
+
throw new Error(`${vcsName} workspace not found.`);
|
|
336
|
+
}
|
|
337
|
+
return provider;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function getProviderForOperation(diffType: string, cwd?: string): Promise<VcsProvider> {
|
|
341
|
+
return getProviderForDiffType(diffType) ?? detectVcs(cwd);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function getContextWithProvider(
|
|
345
|
+
cwd?: string,
|
|
346
|
+
vcsType?: VcsSelection,
|
|
347
|
+
): Promise<{ provider: VcsProvider; gitContext: GitContext }> {
|
|
348
|
+
const provider = await getProviderForSelection(vcsType, cwd);
|
|
349
|
+
return { provider, gitContext: await provider.getContext(cwd) };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function resolveRequestedDiffType(
|
|
353
|
+
provider: VcsProvider,
|
|
354
|
+
gitContext: GitContext,
|
|
355
|
+
requestedDiffType: DiffType | undefined,
|
|
356
|
+
configuredDiffType: DiffType,
|
|
357
|
+
): DiffType {
|
|
358
|
+
if (requestedDiffType && provider.ownsDiffType(requestedDiffType)) {
|
|
359
|
+
return requestedDiffType;
|
|
360
|
+
}
|
|
361
|
+
return resolveInitialDiffType(gitContext, configuredDiffType);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function resolveInitialBase(
|
|
365
|
+
gitContext: GitContext,
|
|
366
|
+
diffType: DiffType,
|
|
367
|
+
requestedBase: string | undefined,
|
|
368
|
+
ownsRequestedDiffType: boolean,
|
|
369
|
+
): string {
|
|
370
|
+
if (gitContext.vcsType === "jj") {
|
|
371
|
+
if (diffType === "jj-line" && ownsRequestedDiffType && requestedBase) {
|
|
372
|
+
return requestedBase;
|
|
373
|
+
}
|
|
374
|
+
return gitContext.defaultBranch;
|
|
375
|
+
}
|
|
376
|
+
return requestedBase ?? gitContext.defaultBranch;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
detectVcs,
|
|
381
|
+
detectManagedVcs,
|
|
382
|
+
|
|
383
|
+
async getVcsContext(cwd?: string, vcsType?: VcsSelection): Promise<GitContext> {
|
|
384
|
+
return (await getContextWithProvider(cwd, vcsType)).gitContext;
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
async detectRemoteDefaultCompareTarget(cwd?: string, vcsType?: VcsSelection): Promise<string | null> {
|
|
388
|
+
const provider = await getProviderForSelection(vcsType, cwd);
|
|
389
|
+
return provider.detectRemoteDefaultCompareTarget?.(cwd) ?? null;
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
async prepareLocalReviewDiff(options: PrepareLocalReviewDiffOptions): Promise<PreparedLocalReviewDiff> {
|
|
393
|
+
const { provider, gitContext } = await getContextWithProvider(options.cwd, options.vcsType);
|
|
394
|
+
const ownsRequestedDiffType = options.requestedDiffType !== undefined
|
|
395
|
+
&& provider.ownsDiffType(options.requestedDiffType);
|
|
396
|
+
const diffType = resolveRequestedDiffType(
|
|
397
|
+
provider,
|
|
398
|
+
gitContext,
|
|
399
|
+
options.requestedDiffType,
|
|
400
|
+
options.configuredDiffType,
|
|
401
|
+
);
|
|
402
|
+
const base = resolveInitialBase(gitContext, diffType, options.requestedBase, ownsRequestedDiffType);
|
|
403
|
+
const result = await provider.runDiff(diffType, base, gitContext.cwd ?? options.cwd, {
|
|
404
|
+
hideWhitespace: options.hideWhitespace,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
gitContext,
|
|
409
|
+
diffType,
|
|
410
|
+
base,
|
|
411
|
+
rawPatch: result.patch,
|
|
412
|
+
gitRef: result.label,
|
|
413
|
+
error: result.error,
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
async runVcsDiff(
|
|
418
|
+
diffType: DiffType,
|
|
419
|
+
defaultBranch: string = "main",
|
|
420
|
+
cwd?: string,
|
|
421
|
+
options?: GitDiffOptions,
|
|
422
|
+
): Promise<DiffResult> {
|
|
423
|
+
const provider = await getProviderForOperation(diffType, cwd);
|
|
424
|
+
return provider.runDiff(diffType, defaultBranch, cwd, options);
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
async getVcsFileContentsForDiff(
|
|
428
|
+
diffType: DiffType,
|
|
429
|
+
defaultBranch: string,
|
|
430
|
+
filePath: string,
|
|
431
|
+
oldPath?: string,
|
|
432
|
+
cwd?: string,
|
|
433
|
+
): Promise<{ oldContent: string | null; newContent: string | null }> {
|
|
434
|
+
const provider = await getProviderForOperation(diffType, cwd);
|
|
435
|
+
return provider.getFileContents(diffType, defaultBranch, filePath, oldPath, cwd);
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
async canStageFiles(diffType: string, cwd?: string): Promise<boolean> {
|
|
439
|
+
const provider = await getProviderForOperation(diffType, cwd);
|
|
440
|
+
return provider.stageFile !== undefined && (provider.canStageFiles?.(diffType) ?? false);
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
async stageFile(diffType: string, filePath: string, cwd?: string): Promise<void> {
|
|
444
|
+
const provider = await getProviderForOperation(diffType, cwd);
|
|
445
|
+
if (!provider.stageFile || !(provider.canStageFiles?.(diffType) ?? false)) {
|
|
446
|
+
throw new Error(`Staging not available for ${provider.id}`);
|
|
447
|
+
}
|
|
448
|
+
return provider.stageFile(filePath, cwd);
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
async unstageFile(diffType: string, filePath: string, cwd?: string): Promise<void> {
|
|
452
|
+
const provider = await getProviderForOperation(diffType, cwd);
|
|
453
|
+
if (!provider.unstageFile || !(provider.canStageFiles?.(diffType) ?? false)) {
|
|
454
|
+
throw new Error(`Unstaging not available for ${provider.id}`);
|
|
455
|
+
}
|
|
456
|
+
return provider.unstageFile(filePath, cwd);
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
resolveVcsCwd(diffType: string, fallbackCwd?: string): string | undefined {
|
|
460
|
+
const provider = getProviderForDiffType(diffType);
|
|
461
|
+
return provider?.resolveCwd?.(diffType, fallbackCwd) ?? fallbackCwd;
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function resolveInitialDiffType(
|
|
467
|
+
gitContext: GitContext,
|
|
468
|
+
configuredDiffType: DiffType,
|
|
469
|
+
): DiffType {
|
|
470
|
+
if (gitContext.vcsType === "p4") {
|
|
471
|
+
return "p4-default";
|
|
472
|
+
}
|
|
473
|
+
if (gitContext.vcsType === "jj") {
|
|
474
|
+
return "jj-current";
|
|
475
|
+
}
|
|
476
|
+
if (gitContext.diffOptions.some((option) => option.id === configuredDiffType)) {
|
|
477
|
+
return configuredDiffType;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const fallback = gitContext.diffOptions[0]?.id;
|
|
481
|
+
return fallback ? fallback as DiffType : configuredDiffType;
|
|
482
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ReviewGitRuntime } from "./review-core";
|
|
3
|
+
import type { PRMetadata } from "./pr-types";
|
|
4
|
+
import { createWorktreePool } from "./worktree-pool";
|
|
5
|
+
|
|
6
|
+
function fakeRuntime(): { runtime: ReviewGitRuntime; commands: string[][] } {
|
|
7
|
+
const commands: string[][] = [];
|
|
8
|
+
const runtime: ReviewGitRuntime = {
|
|
9
|
+
async runGit(args) {
|
|
10
|
+
commands.push(args);
|
|
11
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
12
|
+
},
|
|
13
|
+
async readTextFile() { return null; },
|
|
14
|
+
};
|
|
15
|
+
return { runtime, commands };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeMetadata(number: number, baseBranch = "main"): PRMetadata {
|
|
19
|
+
return {
|
|
20
|
+
platform: "github",
|
|
21
|
+
host: "github.com",
|
|
22
|
+
owner: "acme",
|
|
23
|
+
repo: "widgets",
|
|
24
|
+
number,
|
|
25
|
+
title: `PR #${number}`,
|
|
26
|
+
author: "alice",
|
|
27
|
+
baseBranch,
|
|
28
|
+
headBranch: `feature/pr-${number}`,
|
|
29
|
+
baseSha: "abc123def456abc123def456abc123def456abc1",
|
|
30
|
+
headSha: "def456abc123def456abc123def456abc123def4",
|
|
31
|
+
url: `https://github.com/acme/widgets/pull/${number}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("worktree-pool", () => {
|
|
36
|
+
test("resolve returns undefined for unknown PR", () => {
|
|
37
|
+
const pool = createWorktreePool({ sessionDir: "/tmp/session", repoDir: "/repo", isSameRepo: true });
|
|
38
|
+
expect(pool.resolve("https://github.com/acme/widgets/pull/99")).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("resolve returns path for ready entry", () => {
|
|
42
|
+
const initial = { path: "/tmp/session/pool/pr-3", prUrl: "https://github.com/acme/widgets/pull/3", number: 3, ready: true };
|
|
43
|
+
const pool = createWorktreePool({ sessionDir: "/tmp/session", repoDir: "/repo", isSameRepo: true }, initial);
|
|
44
|
+
expect(pool.resolve("https://github.com/acme/widgets/pull/3")).toBe("/tmp/session/pool/pr-3");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("has returns true for existing entry", () => {
|
|
48
|
+
const initial = { path: "/tmp/session/pool/pr-3", prUrl: "https://github.com/acme/widgets/pull/3", number: 3, ready: true };
|
|
49
|
+
const pool = createWorktreePool({ sessionDir: "/tmp/session", repoDir: "/repo", isSameRepo: true }, initial);
|
|
50
|
+
expect(pool.has("https://github.com/acme/widgets/pull/3")).toBe(true);
|
|
51
|
+
expect(pool.has("https://github.com/acme/widgets/pull/99")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("ensure creates worktree on first call", async () => {
|
|
55
|
+
const { runtime, commands } = fakeRuntime();
|
|
56
|
+
const pool = createWorktreePool({ sessionDir: "/tmp/session", repoDir: "/repo", isSameRepo: true });
|
|
57
|
+
|
|
58
|
+
const entry = await pool.ensure(runtime, makeMetadata(5));
|
|
59
|
+
|
|
60
|
+
expect(entry.path).toBe("/tmp/session/pool/pr-5");
|
|
61
|
+
expect(entry.prUrl).toBe("https://github.com/acme/widgets/pull/5");
|
|
62
|
+
expect(entry.number).toBe(5);
|
|
63
|
+
expect(entry.ready).toBe(true);
|
|
64
|
+
|
|
65
|
+
// Verify fetch order: baseBranch → baseSha → PR head → worktree add
|
|
66
|
+
expect(commands[0]).toEqual(["fetch", "origin", "--", "main"]);
|
|
67
|
+
expect(commands[1][0]).toBe("cat-file"); // ensureObjectAvailable check
|
|
68
|
+
expect(commands[2]).toEqual(["fetch", "origin", "--", "refs/pull/5/head"]);
|
|
69
|
+
expect(commands[3]).toEqual(["worktree", "add", "--detach", "/tmp/session/pool/pr-5", "FETCH_HEAD"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("ensure returns cached entry on second call", async () => {
|
|
73
|
+
const { runtime, commands } = fakeRuntime();
|
|
74
|
+
const pool = createWorktreePool({ sessionDir: "/tmp/session", repoDir: "/repo", isSameRepo: true });
|
|
75
|
+
|
|
76
|
+
await pool.ensure(runtime, makeMetadata(5));
|
|
77
|
+
const commandCountAfterFirst = commands.length;
|
|
78
|
+
|
|
79
|
+
const second = await pool.ensure(runtime, makeMetadata(5));
|
|
80
|
+
expect(second.path).toBe("/tmp/session/pool/pr-5");
|
|
81
|
+
expect(commands.length).toBe(commandCountAfterFirst); // No new git commands
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("ensure creates separate entries for different PRs", async () => {
|
|
85
|
+
const { runtime } = fakeRuntime();
|
|
86
|
+
const pool = createWorktreePool({ sessionDir: "/tmp/session", repoDir: "/repo", isSameRepo: true });
|
|
87
|
+
|
|
88
|
+
const a = await pool.ensure(runtime, makeMetadata(3));
|
|
89
|
+
const b = await pool.ensure(runtime, makeMetadata(4, "feature/pr-3"));
|
|
90
|
+
|
|
91
|
+
expect(a.path).toBe("/tmp/session/pool/pr-3");
|
|
92
|
+
expect(b.path).toBe("/tmp/session/pool/pr-4");
|
|
93
|
+
expect(pool.resolve(a.prUrl)).toBe(a.path);
|
|
94
|
+
expect(pool.resolve(b.prUrl)).toBe(b.path);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("cross-repo pool returns matching entry", async () => {
|
|
98
|
+
const { runtime } = fakeRuntime();
|
|
99
|
+
const initial = { path: "/tmp/session/pool/pr-3", prUrl: "https://github.com/acme/widgets/pull/3", number: 3, ready: true };
|
|
100
|
+
const pool = createWorktreePool({ sessionDir: "/tmp/session", repoDir: "/repo", isSameRepo: false }, initial);
|
|
101
|
+
|
|
102
|
+
const entry = await pool.ensure(runtime, makeMetadata(3));
|
|
103
|
+
expect(entry.path).toBe("/tmp/session/pool/pr-3");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("cross-repo pool rejects different PR", async () => {
|
|
107
|
+
const { runtime } = fakeRuntime();
|
|
108
|
+
const initial = { path: "/tmp/session/pool/pr-3", prUrl: "https://github.com/acme/widgets/pull/3", number: 3, ready: true };
|
|
109
|
+
const pool = createWorktreePool({ sessionDir: "/tmp/session", repoDir: "/repo", isSameRepo: false }, initial);
|
|
110
|
+
|
|
111
|
+
await expect(pool.ensure(runtime, makeMetadata(5))).rejects.toThrow("Cross-repo pool cannot create worktrees for other PRs");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("cross-repo pool throws when empty", async () => {
|
|
115
|
+
const { runtime } = fakeRuntime();
|
|
116
|
+
const pool = createWorktreePool({ sessionDir: "/tmp/session", repoDir: "/repo", isSameRepo: false });
|
|
117
|
+
|
|
118
|
+
await expect(pool.ensure(runtime, makeMetadata(5))).rejects.toThrow("Cross-repo pool cannot create worktrees for other PRs");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("cleanup removes all entries", async () => {
|
|
122
|
+
const { runtime, commands } = fakeRuntime();
|
|
123
|
+
const pool = createWorktreePool({ sessionDir: "/tmp/session", repoDir: "/repo", isSameRepo: true });
|
|
124
|
+
|
|
125
|
+
await pool.ensure(runtime, makeMetadata(3));
|
|
126
|
+
await pool.ensure(runtime, makeMetadata(4, "feature/pr-3"));
|
|
127
|
+
commands.length = 0; // Reset
|
|
128
|
+
|
|
129
|
+
await pool.cleanup(runtime);
|
|
130
|
+
|
|
131
|
+
expect(pool.has("https://github.com/acme/widgets/pull/3")).toBe(false);
|
|
132
|
+
expect(pool.has("https://github.com/acme/widgets/pull/4")).toBe(false);
|
|
133
|
+
// Should have called worktree remove for both
|
|
134
|
+
const removeCommands = commands.filter(c => c[0] === "worktree" && c[1] === "remove");
|
|
135
|
+
expect(removeCommands.length).toBe(2);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("GitLab MR uses correct ref format", async () => {
|
|
139
|
+
const { runtime, commands } = fakeRuntime();
|
|
140
|
+
const pool = createWorktreePool({ sessionDir: "/tmp/session", repoDir: "/repo", isSameRepo: true });
|
|
141
|
+
|
|
142
|
+
const glMetadata: PRMetadata = {
|
|
143
|
+
platform: "gitlab",
|
|
144
|
+
host: "gitlab.com",
|
|
145
|
+
projectPath: "group/project",
|
|
146
|
+
iid: 42,
|
|
147
|
+
title: "MR !42",
|
|
148
|
+
author: "bob",
|
|
149
|
+
baseBranch: "main",
|
|
150
|
+
headBranch: "feature/fix",
|
|
151
|
+
baseSha: "abc123def456abc123def456abc123def456abc1",
|
|
152
|
+
headSha: "def456abc123def456abc123def456abc123def4",
|
|
153
|
+
url: "https://gitlab.com/group/project/-/merge_requests/42",
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const entry = await pool.ensure(runtime, glMetadata);
|
|
157
|
+
expect(entry.path).toBe("/tmp/session/pool/pr-42");
|
|
158
|
+
|
|
159
|
+
const fetchPRHead = commands.find(c => c[0] === "fetch" && c[2] === "--" && c[3]?.includes("merge-requests"));
|
|
160
|
+
expect(fetchPRHead?.[3]).toBe("refs/merge-requests/42/head");
|
|
161
|
+
});
|
|
162
|
+
});
|