farming-code 2.2.6
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/LICENSE +21 -0
- package/README.md +287 -0
- package/README.zh_cn.md +329 -0
- package/SECURITY.md +29 -0
- package/THIRD_PARTY_NOTICES.md +106 -0
- package/backend/acp/client-services.js +312 -0
- package/backend/acp/patch-decisions.js +137 -0
- package/backend/acp/permission-security.js +108 -0
- package/backend/acp-runtime.js +1144 -0
- package/backend/acp-session-state.js +448 -0
- package/backend/acp-transcript.js +169 -0
- package/backend/agent-activity.js +186 -0
- package/backend/agent-env.js +250 -0
- package/backend/agent-json-stream.js +268 -0
- package/backend/agent-manager.js +4259 -0
- package/backend/agent-memory-report.js +583 -0
- package/backend/agent-order.js +162 -0
- package/backend/agent-provider-session.js +287 -0
- package/backend/agent-session-history.js +888 -0
- package/backend/app-server-api.js +917 -0
- package/backend/async-cache.js +94 -0
- package/backend/auth.js +178 -0
- package/backend/claude-settings.js +121 -0
- package/backend/cli-agents.js +484 -0
- package/backend/codex-app-server-home.js +39 -0
- package/backend/codex-app-server-runtime.js +901 -0
- package/backend/codex-context-window.js +189 -0
- package/backend/codex-models.js +229 -0
- package/backend/codex-rollout-follower.js +243 -0
- package/backend/codex-session-history.js +553 -0
- package/backend/codex-transcript-sanitizer.js +103 -0
- package/backend/codex-transcript.js +2341 -0
- package/backend/command-runner-child.js +67 -0
- package/backend/config-manager.js +815 -0
- package/backend/control-api.js +151 -0
- package/backend/data/chinese-poetic-words.json +8223 -0
- package/backend/data/poetic-word-sources/china.json +49209 -0
- package/backend/data/poetic-word-sources/india.json +9763 -0
- package/backend/data/poetic-word-sources/japan.json +8455 -0
- package/backend/executable-discovery.js +207 -0
- package/backend/farming-app-cli.js +790 -0
- package/backend/farming-cli.js +400 -0
- package/backend/farming-session-store.js +373 -0
- package/backend/haiku-token.js +772 -0
- package/backend/index-html.js +49 -0
- package/backend/input-parts.js +54 -0
- package/backend/input-routing.js +24 -0
- package/backend/json-cli-runtime.js +161 -0
- package/backend/local-session-engine.js +620 -0
- package/backend/main-agent-skills.js +292 -0
- package/backend/main-page-session.js +119 -0
- package/backend/native-pty-host-client.js +516 -0
- package/backend/native-pty-host-path.js +22 -0
- package/backend/native-pty-host.js +816 -0
- package/backend/native-session-engine.js +196 -0
- package/backend/network.js +23 -0
- package/backend/npm-update-helper.js +196 -0
- package/backend/packaged-node-pty.js +79 -0
- package/backend/provider-session-id.js +47 -0
- package/backend/qr-share-tickets.js +100 -0
- package/backend/review-diff-router.js +180 -0
- package/backend/review-diff-service.js +1270 -0
- package/backend/review-session-router.js +74 -0
- package/backend/review-session-service.js +432 -0
- package/backend/review-session-store.js +144 -0
- package/backend/review-state-router.js +116 -0
- package/backend/review-state-store.js +287 -0
- package/backend/run-history-store.js +70 -0
- package/backend/server.js +2446 -0
- package/backend/session-engine-bridge.js +110 -0
- package/backend/session-engine-router.js +66 -0
- package/backend/session-engine.js +39 -0
- package/backend/shell-busy-integration.js +472 -0
- package/backend/slash-command-discovery.js +296 -0
- package/backend/storage-layout.js +101 -0
- package/backend/system-monitor.js +77 -0
- package/backend/terminal-runtime-cleanup.js +80 -0
- package/backend/terminal-screen-state.js +240 -0
- package/backend/terminal-screen-worker-pool.js +165 -0
- package/backend/terminal-screen-worker-thread.js +161 -0
- package/backend/terminal-screen-worker.js +255 -0
- package/backend/terminal-status.js +356 -0
- package/backend/theme-manager.js +145 -0
- package/backend/update-service.js +1183 -0
- package/backend/usage-forecast.js +86 -0
- package/backend/usage-monitor.js +1357 -0
- package/backend/workspace-discovery.js +311 -0
- package/backend/workspace-file-router.js +431 -0
- package/backend/workspace-file-service.js +2765 -0
- package/bin/farming +5 -0
- package/config/farming.deploy.env.example +13 -0
- package/config/farming.install.env.example +17 -0
- package/dist/assets/App-BrP-ENHg.css +1 -0
- package/dist/assets/App-Dza5yEDe.js +124 -0
- package/dist/assets/FileEditorMarkdownPreview-DRDNQ8mX.css +1 -0
- package/dist/assets/FileEditorMarkdownPreview-elKWc8Im.js +404 -0
- package/dist/assets/FileEditorPane-BQLocXOp.js +5 -0
- package/dist/assets/IconGlyphs-RkMAdBXF.js +1 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/ProjectFilesSection-lS1azbgj.js +12 -0
- package/dist/assets/ReviewPage-DgnmgU2T.js +3 -0
- package/dist/assets/abap-Cc5D5h5i.js +1 -0
- package/dist/assets/abnfDiagram-VRR7QNED-BgawzDQT.js +1 -0
- package/dist/assets/apex-BKopwLmx.js +1 -0
- package/dist/assets/api-D1lyBYIQ.js +1 -0
- package/dist/assets/arc-CWoVlxJw.js +1 -0
- package/dist/assets/architecture-TIHT7OUA-CbJBFh3w.js +1 -0
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DUShwPg0.js +36 -0
- package/dist/assets/array-HeX70jSN.js +1 -0
- package/dist/assets/azcli-DI-C34WA.js +1 -0
- package/dist/assets/bat-Cbp6u42z.js +1 -0
- package/dist/assets/bicep-4QA1jTCM.js +2 -0
- package/dist/assets/blockDiagram-677ZJIJ3-CeGZviXR.js +132 -0
- package/dist/assets/c4Diagram-LMCZKHZV-CLhYht6S.js +10 -0
- package/dist/assets/cameligo-Dda_adPs.js +1 -0
- package/dist/assets/channel-Uw90AO0b.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-d5WWh8jo.js +1 -0
- package/dist/assets/chunk-32BRIVSS-LFYpT-Cu.js +1 -0
- package/dist/assets/chunk-52WLFC77-BH6jDw9J.js +10 -0
- package/dist/assets/chunk-5FCAYU7R-C7AmIhYv.js +1 -0
- package/dist/assets/chunk-5HE753X5-C8cXaouP.js +1 -0
- package/dist/assets/chunk-5JV3BV7I-C1PtimcA.js +1 -0
- package/dist/assets/chunk-5TONJI2A-_81zUME8.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-Dlw7AXWp.js +15 -0
- package/dist/assets/chunk-7BUUIJ7U-ZBRhjLOh.js +1 -0
- package/dist/assets/chunk-BIQX33UG-CWmzxeAw.js +1 -0
- package/dist/assets/chunk-C7G6YPKG-B_mIUlb7.js +1 -0
- package/dist/assets/chunk-CQNSW5MT-CMayolwI.js +1 -0
- package/dist/assets/chunk-CYSBUYHQ-lSPbrL3Z.js +1 -0
- package/dist/assets/chunk-DECur_0Z.js +1 -0
- package/dist/assets/chunk-EMLP6XTP-D9-jtCnM.js +1 -0
- package/dist/assets/chunk-EX3LRPZG-C63hVuAJ.js +231 -0
- package/dist/assets/chunk-FWX5IMBZ-CboX-WF_.js +2 -0
- package/dist/assets/chunk-HOUHSVGY-BOgSd6Un.js +1 -0
- package/dist/assets/chunk-ICXQ74PX-C0K-pgZt.js +2 -0
- package/dist/assets/chunk-JG7HCLWE-Cib606u_.js +2 -0
- package/dist/assets/chunk-JWPE2WC7-DSyBRaRG.js +1 -0
- package/dist/assets/chunk-KEIR6QF5-CNOHik0T.js +161 -0
- package/dist/assets/chunk-MOJQB5TN-CVMfwN_7.js +88 -0
- package/dist/assets/chunk-MOZMSUNE-D6JeZqmE.js +1 -0
- package/dist/assets/chunk-OGEWGWER-DLT3pxyw.js +1 -0
- package/dist/assets/chunk-OSBZ3O6U-C0W8KSeg.js +1 -0
- package/dist/assets/chunk-PUDLZKDR-Cd079yIQ.js +156 -0
- package/dist/assets/chunk-Q4XR5HBZ-D0jHa6SO.js +70 -0
- package/dist/assets/chunk-QBLGF6JB-p4omp7uO.js +1 -0
- package/dist/assets/chunk-R7FJI6CG-CZ_p9hSr.js +1 -0
- package/dist/assets/chunk-RYQCIY6F-CZA9pvFX.js +1 -0
- package/dist/assets/chunk-U6XO7XAA-CcXKsWxp.js +2 -0
- package/dist/assets/chunk-V7JOEXUC-CVDtR75H.js +206 -0
- package/dist/assets/chunk-VAUOI2AC-B-W2_9Ot.js +1 -0
- package/dist/assets/chunk-VR4S4FIN-FD3kWh32.js +1 -0
- package/dist/assets/chunk-WYO6CB5R-C2bGaS5r.js +127 -0
- package/dist/assets/chunk-XXDRQBXY-BBXwjhUx.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-Di0sYzi1.js +1 -0
- package/dist/assets/chunk-YOTPTUD7-BXHmEk89.js +1 -0
- package/dist/assets/chunk-ZGVPDNZ5-ymgts0zo.js +62 -0
- package/dist/assets/chunk-ZIRB5QZD-DncwlQQc.js +32 -0
- package/dist/assets/classDiagram-OUVF2IWQ-DKW_dtku.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-C8Fi7-yv.js +1 -0
- package/dist/assets/clojure-CvY8A83L.js +1 -0
- package/dist/assets/code-dark-Cjb1QGhu.css +1 -0
- package/dist/assets/code-mobile-3C2Rr-CN.css +1 -0
- package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/assets/coffee-4LX2Sv8A.js +1 -0
- package/dist/assets/core-ZlAPicox.js +3 -0
- package/dist/assets/cose-bilkent-JH36ORCC-BNIAz0nN.js +1 -0
- package/dist/assets/cpp-D4AzSV28.js +1 -0
- package/dist/assets/csharp-C4nWN4Yd.js +1 -0
- package/dist/assets/csp-CiEaaS5Z.js +1 -0
- package/dist/assets/css-DXxBRsbk.js +3 -0
- package/dist/assets/css.worker-DyDu5ynT.js +89 -0
- package/dist/assets/cssMode-DDWxTPm_.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-DgXGHlRt.js +1 -0
- package/dist/assets/cynefinDiagram-TSTJHNR4-CwpobXIl.js +62 -0
- package/dist/assets/cypher-DOMzqyFA.js +1 -0
- package/dist/assets/cytoscape.esm-Cl8iC3D7.js +321 -0
- package/dist/assets/dagre-DXlcHgr3.js +1 -0
- package/dist/assets/dagre-VKFMJZFB-Dkhlfjna.js +4 -0
- package/dist/assets/dart-BWS7MXt_.js +1 -0
- package/dist/assets/defaultLocale-GJwWH1Jr.js +1 -0
- package/dist/assets/diagram-FQU43EPY-hA8dicp6.js +3 -0
- package/dist/assets/diagram-G47NLZAW-BTY6JTzO.js +24 -0
- package/dist/assets/diagram-NH7WQ7WH-xkaHHX9M.js +24 -0
- package/dist/assets/diagram-OA4YK3LP-gk_fg9UU.js +30 -0
- package/dist/assets/diagram-WEI45ONY-BvC8xd6T.js +41 -0
- package/dist/assets/dist-ClAL-Tv5.js +1 -0
- package/dist/assets/dockerfile-Baf4U3xT.js +1 -0
- package/dist/assets/ebnfDiagram-CCIWWBDH-CR7RaNat.js +1 -0
- package/dist/assets/ecl-CT7udlkQ.js +1 -0
- package/dist/assets/editor-CtbFZlk0.css +1 -0
- package/dist/assets/editor.api2-B9h6iFPK.js +872 -0
- package/dist/assets/editor.worker-Dpt_xPrs.js +26 -0
- package/dist/assets/elixir-CIbAZw5l.js +1 -0
- package/dist/assets/erDiagram-Q63AITRT-_M-JFPbj.js +85 -0
- package/dist/assets/eventmodeling-45OFAUF4-DXYTQ2nt.js +1 -0
- package/dist/assets/file-icons-EFUGSSwf.js +1 -0
- package/dist/assets/flow9-Dqiw45WV.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DAiS5Y7G.js +1 -0
- package/dist/assets/freemarker2-QBsTERH-.js +3 -0
- package/dist/assets/fsharp-E8E2L9jU.js +1 -0
- package/dist/assets/ganttDiagram-NO4QXBWP-Bl8P1yf_.js +292 -0
- package/dist/assets/gitGraph-TEB2WS4Q-CzTBjEvV.js +1 -0
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DoHa5ysz.js +106 -0
- package/dist/assets/go-CouVSN4D.js +1 -0
- package/dist/assets/graphlib-CbSvZcZU.js +1 -0
- package/dist/assets/graphql-a6h6h5-t.js +1 -0
- package/dist/assets/handlebars-CuoxPo5A.js +1 -0
- package/dist/assets/hcl-vso3e6N-.js +1 -0
- package/dist/assets/html-gX5PEu0Q.js +1 -0
- package/dist/assets/html.worker-IWNXhnIC.js +502 -0
- package/dist/assets/htmlMode-DIWUbUxd.js +1 -0
- package/dist/assets/index-BpHPeJf0.js +10 -0
- package/dist/assets/info-DKCQHKI2-DznP7j3s.js +1 -0
- package/dist/assets/infoDiagram-FWYZ7A6U-DSFcSuUs.js +2 -0
- package/dist/assets/ini-SwZZkotR.js +1 -0
- package/dist/assets/init-BPLPMQ3Y.js +1 -0
- package/dist/assets/ishikawaDiagram-FXEZZL3T-DdkqullI.js +70 -0
- package/dist/assets/java-CtRLC4_3.js +1 -0
- package/dist/assets/javascript-BYKGIUGs.js +1 -0
- package/dist/assets/journeyDiagram-5HDEW3XC-B4hRnQJr.js +139 -0
- package/dist/assets/json.worker-c4h5s80L.js +58 -0
- package/dist/assets/jsonMode-AFmQQP6u.js +7 -0
- package/dist/assets/jsx-runtime-BthL93pI.js +1 -0
- package/dist/assets/julia-CuJWFKQH.js +1 -0
- package/dist/assets/kanban-definition-HUTT4EX6-tm2FZB2E.js +89 -0
- package/dist/assets/katex-C8DPyI6s.js +257 -0
- package/dist/assets/kotlin-B_kBhsre.js +1 -0
- package/dist/assets/less-CtrHIfz3.js +2 -0
- package/dist/assets/lexon-yVt1pJLu.js +1 -0
- package/dist/assets/line-CmsjK4kn.js +1 -0
- package/dist/assets/linear-Dld3_ERu.js +1 -0
- package/dist/assets/liquid-DBIl-f0z.js +1 -0
- package/dist/assets/lspLanguageFeatures-DK6tqicV.js +4 -0
- package/dist/assets/lua-prOwgJ4U.js +1 -0
- package/dist/assets/m3-DhrdTQ84.js +1 -0
- package/dist/assets/main-DTJwH51M.css +1 -0
- package/dist/assets/map-LZWWoeWa.js +1 -0
- package/dist/assets/markdown-DoI5HwSM.js +1 -0
- package/dist/assets/mdx-DS-bVtZN.js +1 -0
- package/dist/assets/mermaid-parser.core-BQtepoLd.js +4 -0
- package/dist/assets/mermaid.core-C7UT_EEL.js +11 -0
- package/dist/assets/mindmap-definition-LN4V7U3C-DplfjGms.js +96 -0
- package/dist/assets/mips-Dye5jsBU.js +1 -0
- package/dist/assets/monaco.contribution-CyLosfZI.js +2 -0
- package/dist/assets/msdax-TkNOyIL8.js +1 -0
- package/dist/assets/mysql-CEc-RKZF.js +1 -0
- package/dist/assets/objective-c-CqqCsQz0.js +1 -0
- package/dist/assets/ordinal-BLrOss5K.js +1 -0
- package/dist/assets/packet-7NZHBO7P-BDEPOPha.js +1 -0
- package/dist/assets/pascal-DO_pj8yg.js +1 -0
- package/dist/assets/pascaligo-1cbdZdF3.js +1 -0
- package/dist/assets/path-YdFzr2W6.js +1 -0
- package/dist/assets/pegDiagram-2B236MQR-lu3SdkQs.js +1 -0
- package/dist/assets/perl-B_N3PYfC.js +1 -0
- package/dist/assets/pgsql-9iYk1FQ7.js +1 -0
- package/dist/assets/php-rO2sgoUM.js +1 -0
- package/dist/assets/pie-RZYD4A2V-DWIrwPJQ.js +1 -0
- package/dist/assets/pieDiagram-ENE6RG2P-3fHbmDf9.js +39 -0
- package/dist/assets/pla-Cbgv60kq.js +1 -0
- package/dist/assets/postiats-y6xrhy_M.js +1 -0
- package/dist/assets/powerquery-BaD8gWNE.js +1 -0
- package/dist/assets/powershell-BMhG4JVe.js +1 -0
- package/dist/assets/preload-helper-mJowh_hw.js +1 -0
- package/dist/assets/protobuf-CdOP5kN3.js +2 -0
- package/dist/assets/pug-Ck60hNF9.js +1 -0
- package/dist/assets/python-C93q3xXz.js +1 -0
- package/dist/assets/qoder-C9LmmOSf.svg +1 -0
- package/dist/assets/qoder-Cf9gl0Y5.svg +1 -0
- package/dist/assets/qoder-gHCinseV.svg +1 -0
- package/dist/assets/qrcode-DV-_rjU8.js +3 -0
- package/dist/assets/qsharp-BcaKFfP-.js +1 -0
- package/dist/assets/quadrantDiagram-ABIIQ3AL-a5qoWz-X.js +7 -0
- package/dist/assets/r-rFNes0iK.js +1 -0
- package/dist/assets/radar-I7S5WNFK-B7OaD8vp.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BXW2nuCA.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-Buw9QG2r.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-BjSi4sWJ.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6-DN1wIVhS.js +1 -0
- package/dist/assets/railroadDiagram-RFXS5EU6-D2TOuXW9.js +1 -0
- package/dist/assets/razor-CBifNGXO.js +1 -0
- package/dist/assets/react-dom-gtU6X-Hj.js +1 -0
- package/dist/assets/redis-DkpQ3K6G.js +1 -0
- package/dist/assets/redshift-Di9N5TI3.js +1 -0
- package/dist/assets/requirementDiagram-TGXJPOKE-LQBWpUvd.js +84 -0
- package/dist/assets/restructuredtext-By5ZzLt6.js +1 -0
- package/dist/assets/review-BcnwQtJv.css +1 -0
- package/dist/assets/rough.esm-c4PR5shF.js +1 -0
- package/dist/assets/ruby-fF5zGiLp.js +1 -0
- package/dist/assets/rust-C5b3AYY6.js +1 -0
- package/dist/assets/sankeyDiagram-HTMAVEWB-B1ohYoyK.js +40 -0
- package/dist/assets/sb-CD07UNRs.js +1 -0
- package/dist/assets/scala-jtn1ip9G.js +1 -0
- package/dist/assets/scheme-C4vqMkn_.js +1 -0
- package/dist/assets/scss-I4Glxjbz.js +3 -0
- package/dist/assets/sequenceDiagram-DBY2YBRQ-C3vM-hMq.js +162 -0
- package/dist/assets/shell-DubUcuns.js +1 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-daTB6OcF.js +1 -0
- package/dist/assets/solidity-CNy2KSl2.js +1 -0
- package/dist/assets/sophia-D9_URt84.js +1 -0
- package/dist/assets/sparql-DviXpm4C.js +1 -0
- package/dist/assets/sql-vDRbAZgK.js +1 -0
- package/dist/assets/src-WBW1QxJt.js +1 -0
- package/dist/assets/st-CnpnRqA7.js +1 -0
- package/dist/assets/stateDiagram-2N3HPSRC-BvFhOryI.js +1 -0
- package/dist/assets/stateDiagram-v2-6OUMAXLB-WMrz4PNo.js +1 -0
- package/dist/assets/swift-D_g8Ganz.js +1 -0
- package/dist/assets/swimlanes-5IMT3BWC-DKF_b-2s.js +2 -0
- package/dist/assets/swimlanesDiagram-G3AALYLV-BGswC9oC.js +8 -0
- package/dist/assets/systemverilog-OuZa5h1M.js +1 -0
- package/dist/assets/tcl-DBm9ucsV.js +1 -0
- package/dist/assets/timeline-definition-FHXFAJF6-vAs11ux_.js +120 -0
- package/dist/assets/tokens-BwenNn5Q.css +1 -0
- package/dist/assets/treeView-QDETBFTQ-BVfICaZQ.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-oWHPqo17.js +1 -0
- package/dist/assets/ts.worker-BwM5Ha3L.js +67719 -0
- package/dist/assets/tsMode-DGZTlTj8.js +11 -0
- package/dist/assets/twig-BRzEqX5Y.js +1 -0
- package/dist/assets/typescript-KJKfYO7K.js +1 -0
- package/dist/assets/typespec-D4ZgGeX8.js +1 -0
- package/dist/assets/useWorkspaceMenuKeyboard-CneKAZUJ.js +1 -0
- package/dist/assets/vb-DcP4s6WS.js +1 -0
- package/dist/assets/vennDiagram-L72KCM5P-BybTX_9B.js +34 -0
- package/dist/assets/wardley-OPB4EBWU-Dg8t5y3s.js +1 -0
- package/dist/assets/wardleyDiagram-EHGQE667-CicUmMBR.js +78 -0
- package/dist/assets/wgsl-CnRJ_QUf.js +298 -0
- package/dist/assets/workers-BSQRIkqa.js +1 -0
- package/dist/assets/workspace-view-state-CTyDzk2D.js +1 -0
- package/dist/assets/xml-C8FAcN6j.js +1 -0
- package/dist/assets/xychartDiagram-FW5EYKEG-BhWDyCX-.js +7 -0
- package/dist/assets/yaml-BQ4NrRhG.js +1 -0
- package/dist/assets/zsh-CLpveKlF.svg +1 -0
- package/dist/assets/zsh-FxSpMPbz.svg +1 -0
- package/dist/farming-2/app-icon-v2-180.png +0 -0
- package/dist/farming-2/app-icon-v2-192.png +0 -0
- package/dist/farming-2/app-icon-v2-512.png +0 -0
- package/dist/farming-2/app-icon-v2-maskable-512.png +0 -0
- package/dist/farming-2/app-icon-v2-master.png +0 -0
- package/dist/farming-2/favicon-v2-16.png +0 -0
- package/dist/farming-2/favicon-v2-32.png +0 -0
- package/dist/farming-2/favicon-v2-48.png +0 -0
- package/dist/farming-2/favicon-v2-64.png +0 -0
- package/dist/farming-2/favicon-v2.ico +0 -0
- package/dist/farming-2/favicon.ico +0 -0
- package/dist/farming-2/github-social-preview.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v1-bee-garden.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v2.png +0 -0
- package/dist/farming-2/images/avatar-watercolor-v3.png +0 -0
- package/dist/farming-2/legacy-icons-v1/favicon.ico +1 -0
- package/dist/farming-2/legacy-icons-v1/logo-16.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-180.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-192.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-32.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-48.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-512.png +0 -0
- package/dist/farming-2/legacy-icons-v1/logo-64.png +0 -0
- package/dist/farming-2/logo-16.png +0 -0
- package/dist/farming-2/logo-180.png +0 -0
- package/dist/farming-2/logo-192.png +0 -0
- package/dist/farming-2/logo-32.png +0 -0
- package/dist/farming-2/logo-48.png +0 -0
- package/dist/farming-2/logo-512.png +0 -0
- package/dist/farming-2/logo-64.png +0 -0
- package/dist/farming-2/site.webmanifest +27 -0
- package/dist/farming-2/style_capsules/watercolor-pastoral.json +94 -0
- package/dist/index.html +27 -0
- package/frontend/ghostty-loader.js +24 -0
- package/frontend/runtime-paths.js +28 -0
- package/frontend/session-bridge.js +82 -0
- package/frontend/session-modal-bridge.js +331 -0
- package/frontend/skin-bridge.js +25 -0
- package/frontend/skins/crt/app.js +6940 -0
- package/frontend/skins/crt/assets/branding/farming-crt-icon.svg +11 -0
- package/frontend/skins/crt/assets/branding/farming-crt-logo-v1.png +0 -0
- package/frontend/skins/crt/assets/effects/phosphor-noise.svg +11 -0
- package/frontend/skins/crt/assets/fonts/departure-mono/DepartureMonoNerdFontMono-Regular.otf +0 -0
- package/frontend/skins/crt/assets/fonts/departure-mono/LICENSE +93 -0
- package/frontend/skins/crt/effects.js +8 -0
- package/frontend/skins/crt/index.html +1712 -0
- package/frontend/skins/crt/styles/billing.css +1003 -0
- package/frontend/skins/crt/styles/effects.css +171 -0
- package/frontend/skins/crt/styles/history.css +215 -0
- package/frontend/skins/crt/styles/monochrome-green.css +509 -0
- package/frontend/skins/crt/styles/search.css +271 -0
- package/frontend/terminal-bridge.js +192 -0
- package/frontend/theme-loader.js +84 -0
- package/frontend/themes/terminal/theme.json +24 -0
- package/frontend/vendor/ghostty-web/LICENSE +21 -0
- package/frontend/vendor/ghostty-web/__vite-browser-external-2447137e.js +4 -0
- package/frontend/vendor/ghostty-web/ghostty-vt.wasm +0 -0
- package/frontend/vendor/ghostty-web/ghostty-web.js +2963 -0
- package/frontend/vendor/ghostty-web/ghostty-web.umd.cjs +13 -0
- package/frontend/vendor/ghostty-web/index.d.ts +1842 -0
- package/index.html +23 -0
- package/package.json +144 -0
- package/scripts/sync-ghostty-vendor.js +66 -0
|
@@ -0,0 +1,1270 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { WorkspaceFileError, parseUnifiedDiffRows } = require('./workspace-file-service');
|
|
5
|
+
|
|
6
|
+
const MAX_REVIEW_FILES = 200;
|
|
7
|
+
const MAX_WORKING_COPY_SCAN_FILES = 2000;
|
|
8
|
+
const MAX_UNTRACKED_LINES = 500;
|
|
9
|
+
const MAX_REVIEW_CONTEXT_RANGE_LINES = 10000;
|
|
10
|
+
const DIFF_CONCURRENCY = 4;
|
|
11
|
+
|
|
12
|
+
function reviewKind(gitStatus) {
|
|
13
|
+
if (gitStatus === 'added' || gitStatus === 'untracked') return 'added';
|
|
14
|
+
if (gitStatus === 'copied') return 'copied';
|
|
15
|
+
if (gitStatus === 'deleted') return 'deleted';
|
|
16
|
+
if (gitStatus === 'renamed') return 'renamed';
|
|
17
|
+
if (gitStatus === 'rewritten') return 'rewritten';
|
|
18
|
+
return 'modified';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function reviewStatus(kind) {
|
|
22
|
+
if (kind === 'added') return 'A';
|
|
23
|
+
if (kind === 'copied') return 'C';
|
|
24
|
+
if (kind === 'deleted') return 'D';
|
|
25
|
+
if (kind === 'renamed') return 'R';
|
|
26
|
+
if (kind === 'rewritten') return 'W';
|
|
27
|
+
if (kind === 'unmodified') return 'U';
|
|
28
|
+
if (kind === 'reverted') return 'X';
|
|
29
|
+
return 'M';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function countRows(hunks) {
|
|
33
|
+
return hunks.reduce((total, hunk) => hunk.rows.reduce((count, row) => ({
|
|
34
|
+
added: count.added + (row.kind === 'added' || row.kind === 'changed' ? 1 : 0),
|
|
35
|
+
removed: count.removed + (row.kind === 'deleted' || row.kind === 'changed' ? 1 : 0),
|
|
36
|
+
}), total), { added: 0, removed: 0 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function metadataOnlyOption(value) {
|
|
40
|
+
return value === true || value === '1' || value === 'true';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeIgnoreWhitespace(value) {
|
|
44
|
+
if (value === 'ALL' || value === 'IGNORE_ALL') return 'ALL';
|
|
45
|
+
if (value === 'TRAILING' || value === 'IGNORE_TRAILING') return 'TRAILING';
|
|
46
|
+
if (value === 'LEADING_AND_TRAILING' || value === 'IGNORE_LEADING_AND_TRAILING') return 'LEADING_AND_TRAILING';
|
|
47
|
+
return 'NONE';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeDiffContext(value) {
|
|
51
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
52
|
+
const context = Number(value);
|
|
53
|
+
if (!Number.isInteger(context) || context < 0) return undefined;
|
|
54
|
+
return Math.min(context, 10000);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeReviewLimit(value) {
|
|
58
|
+
const limit = Number(value);
|
|
59
|
+
if (!Number.isInteger(limit) || limit <= 0) return MAX_REVIEW_FILES;
|
|
60
|
+
return Math.min(MAX_REVIEW_FILES, limit);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeContextRangeInteger(value, name, { allowZero = false } = {}) {
|
|
64
|
+
const number = Number(value);
|
|
65
|
+
const minimum = allowZero ? 0 : 1;
|
|
66
|
+
if (!Number.isInteger(number) || number < minimum) {
|
|
67
|
+
throw new WorkspaceFileError(`${name} must be an integer greater than or equal to ${minimum}`, 400);
|
|
68
|
+
}
|
|
69
|
+
return number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function textLines(content) {
|
|
73
|
+
if (content === undefined || content === null || content === '') return [];
|
|
74
|
+
const lines = String(content).split('\n');
|
|
75
|
+
if (lines.at(-1) === '') lines.pop();
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function reviewTextFileMeta(name, lines) {
|
|
80
|
+
return {
|
|
81
|
+
contentType: 'text/plain',
|
|
82
|
+
lines: lines.length,
|
|
83
|
+
name,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function contextRowsFromSources(sources, options = {}) {
|
|
88
|
+
const oldStart = normalizeContextRangeInteger(options.oldStart, 'oldStart');
|
|
89
|
+
const newStart = normalizeContextRangeInteger(options.newStart, 'newStart');
|
|
90
|
+
const requestedLines = normalizeContextRangeInteger(options.lines, 'lines');
|
|
91
|
+
if (requestedLines > MAX_REVIEW_CONTEXT_RANGE_LINES) {
|
|
92
|
+
throw new WorkspaceFileError(`lines must not exceed ${MAX_REVIEW_CONTEXT_RANGE_LINES}`, 400);
|
|
93
|
+
}
|
|
94
|
+
const left = sources.leftLines.slice(oldStart - 1, oldStart - 1 + requestedLines);
|
|
95
|
+
const right = sources.rightLines.slice(newStart - 1, newStart - 1 + requestedLines);
|
|
96
|
+
if (left.length !== requestedLines || right.length !== requestedLines) {
|
|
97
|
+
throw new WorkspaceFileError('review context range is outside the file', 416);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
leftLines: sources.leftLines.length,
|
|
101
|
+
rightLines: sources.rightLines.length,
|
|
102
|
+
rows: Array.from({ length: requestedLines }, (_, index) => ({
|
|
103
|
+
kind: 'context',
|
|
104
|
+
left: { line: oldStart + index, text: left[index] },
|
|
105
|
+
right: { line: newStart + index, text: right[index] },
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeWorkingCopyScope(value) {
|
|
111
|
+
return value === 'tracked' || value === 'untracked' ? value : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeModifiedWithinDays(value) {
|
|
115
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
116
|
+
const days = Number(value);
|
|
117
|
+
return Number.isInteger(days) && days >= 1 && days <= 3650 ? days : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function filterWorkingCopyChangeItems(root, items, options = {}, now = Date.now()) {
|
|
121
|
+
const scope = normalizeWorkingCopyScope(options.scope);
|
|
122
|
+
let filtered = items;
|
|
123
|
+
if (scope === 'tracked') {
|
|
124
|
+
filtered = items.filter(change => change.gitStatus !== 'untracked');
|
|
125
|
+
} else if (scope === 'untracked') {
|
|
126
|
+
filtered = items.filter(change => change.gitStatus === 'untracked');
|
|
127
|
+
const modifiedWithinDays = normalizeModifiedWithinDays(options.modifiedWithinDays);
|
|
128
|
+
if (modifiedWithinDays !== undefined) {
|
|
129
|
+
const modifiedSince = now - modifiedWithinDays * 24 * 60 * 60 * 1000;
|
|
130
|
+
filtered = filtered.filter(change => {
|
|
131
|
+
if (!isSafeReviewPath(change.path)) return false;
|
|
132
|
+
try {
|
|
133
|
+
return fs.statSync(path.join(root, change.path)).mtimeMs >= modifiedSince;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return filtered;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function gitWhitespaceArgs(value) {
|
|
144
|
+
const normalized = normalizeIgnoreWhitespace(value);
|
|
145
|
+
if (normalized === 'ALL') return ['--ignore-all-space'];
|
|
146
|
+
if (normalized === 'TRAILING') return ['--ignore-space-at-eol'];
|
|
147
|
+
if (normalized === 'LEADING_AND_TRAILING') return ['--ignore-space-change'];
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function gitContextArgs(value) {
|
|
152
|
+
const context = normalizeDiffContext(value);
|
|
153
|
+
return context === undefined ? [] : [`--unified=${context}`];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function diffContentOptions(options = {}) {
|
|
157
|
+
const ignoreWhitespace = normalizeIgnoreWhitespace(options.ignoreWhitespace);
|
|
158
|
+
const context = normalizeDiffContext(options.context);
|
|
159
|
+
return {
|
|
160
|
+
...(ignoreWhitespace !== 'NONE' ? { ignoreWhitespace } : {}),
|
|
161
|
+
...(context !== undefined ? { context } : {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function hasOptions(value) {
|
|
166
|
+
return Object.keys(value).length > 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function reviewFileIdentity(file) {
|
|
170
|
+
return {
|
|
171
|
+
added: file.added,
|
|
172
|
+
binary: file.binary === true,
|
|
173
|
+
kind: file.kind,
|
|
174
|
+
newMode: file.newMode || '',
|
|
175
|
+
newSha: file.newSha || '',
|
|
176
|
+
oldMode: file.oldMode || '',
|
|
177
|
+
oldSha: file.oldSha || '',
|
|
178
|
+
path: file.path,
|
|
179
|
+
previousPath: file.previousPath || '',
|
|
180
|
+
removed: file.removed,
|
|
181
|
+
size: Number.isInteger(file.size) ? file.size : null,
|
|
182
|
+
sizeDelta: Number.isInteger(file.sizeDelta) ? file.sizeDelta : null,
|
|
183
|
+
status: file.status || reviewStatus(file.kind),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function metadataFile(change, options = {}) {
|
|
188
|
+
const kind = change.kind || reviewKind(change.gitStatus);
|
|
189
|
+
return {
|
|
190
|
+
added: Number.isInteger(options.added) && options.added >= 0 ? options.added : 0,
|
|
191
|
+
...(options.binary === true ? { binary: true } : {}),
|
|
192
|
+
diff: { hunks: [], ...(options.truncated === true ? { truncated: true } : {}) },
|
|
193
|
+
diffLoaded: false,
|
|
194
|
+
...(options.diffTooExpensive === true ? { diffTooExpensive: true } : {}),
|
|
195
|
+
kind,
|
|
196
|
+
...(typeof options.newMode === 'string' ? { newMode: options.newMode } : {}),
|
|
197
|
+
...(typeof options.newSha === 'string' ? { newSha: options.newSha } : {}),
|
|
198
|
+
...(typeof options.oldMode === 'string' ? { oldMode: options.oldMode } : {}),
|
|
199
|
+
...(typeof options.oldSha === 'string' ? { oldSha: options.oldSha } : {}),
|
|
200
|
+
path: change.path,
|
|
201
|
+
...(change.previousPath ? { previousPath: change.previousPath } : {}),
|
|
202
|
+
removed: Number.isInteger(options.removed) && options.removed >= 0 ? options.removed : 0,
|
|
203
|
+
...(Number.isInteger(options.size) ? { size: options.size } : {}),
|
|
204
|
+
...(Number.isInteger(options.sizeDelta) ? { sizeDelta: options.sizeDelta } : {}),
|
|
205
|
+
status: change.status || reviewStatus(kind),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function assertUniqueReviewPaths(files) {
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
if (seen.has(file.path)) throw new WorkspaceFileError('review snapshot contains duplicate file paths', 500);
|
|
213
|
+
seen.add(file.path);
|
|
214
|
+
}
|
|
215
|
+
return files;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function untrackedLines(content) {
|
|
219
|
+
const lines = String(content || '').split('\n');
|
|
220
|
+
if (lines.at(-1) === '') lines.pop();
|
|
221
|
+
return lines;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function untrackedHunks(content) {
|
|
225
|
+
const lines = untrackedLines(content);
|
|
226
|
+
const visibleLines = lines.slice(0, MAX_UNTRACKED_LINES);
|
|
227
|
+
return [{
|
|
228
|
+
header: `@@ -0,0 +1,${visibleLines.length} @@`,
|
|
229
|
+
oldStart: 0,
|
|
230
|
+
oldLines: 0,
|
|
231
|
+
newStart: 1,
|
|
232
|
+
newLines: visibleLines.length,
|
|
233
|
+
rows: visibleLines.map((text, index) => ({ kind: 'added', right: { line: index + 1, text } })),
|
|
234
|
+
}];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function untrackedPatch(filePath, content) {
|
|
238
|
+
const lines = untrackedLines(content);
|
|
239
|
+
const visibleLines = lines.slice(0, MAX_UNTRACKED_LINES);
|
|
240
|
+
const suffix = visibleLines.length ? `${visibleLines.map(line => `+${line}`).join('\n')}\n` : '';
|
|
241
|
+
return [
|
|
242
|
+
`diff --git a/${filePath} b/${filePath}`,
|
|
243
|
+
'new file mode 100644',
|
|
244
|
+
'--- /dev/null',
|
|
245
|
+
`+++ b/${filePath}`,
|
|
246
|
+
`@@ -0,0 +1,${visibleLines.length} @@`,
|
|
247
|
+
suffix,
|
|
248
|
+
].join('\n');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function untrackedContentTooLarge(content) {
|
|
252
|
+
return untrackedLines(content).length > MAX_UNTRACKED_LINES;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function mapWithConcurrency(values, mapper) {
|
|
256
|
+
const result = new Array(values.length);
|
|
257
|
+
let next = 0;
|
|
258
|
+
await Promise.all(Array.from({ length: Math.min(DIFF_CONCURRENCY, values.length) }, async () => {
|
|
259
|
+
while (next < values.length) {
|
|
260
|
+
const index = next++;
|
|
261
|
+
result[index] = await mapper(values[index]);
|
|
262
|
+
}
|
|
263
|
+
}));
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function stableHash(value) {
|
|
268
|
+
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function reviewRootIdentity(root) {
|
|
272
|
+
try {
|
|
273
|
+
return fs.realpathSync.native(root);
|
|
274
|
+
} catch {
|
|
275
|
+
return root;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function workingCopyReviewId(root, options = {}) {
|
|
280
|
+
const scope = normalizeWorkingCopyScope(options.scope);
|
|
281
|
+
if (!scope) return `working-copy-${stableHash(reviewRootIdentity(root)).slice(0, 24)}`;
|
|
282
|
+
const modifiedWithinDays = scope === 'untracked' ? normalizeModifiedWithinDays(options.modifiedWithinDays) : undefined;
|
|
283
|
+
const identity = [reviewRootIdentity(root), scope, modifiedWithinDays || 'all'].join('\n');
|
|
284
|
+
return `working-copy-${stableHash(identity).slice(0, 24)}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function workingCopyPatchset(files) {
|
|
288
|
+
return `Working copy ${stableHash(JSON.stringify(files.map(reviewFileIdentity))).slice(0, 12)}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function gitRangeReviewId(root, base, head) {
|
|
292
|
+
return `git-range-${stableHash(`${reviewRootIdentity(root)}\n${base}\n${head}`).slice(0, 24)}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function parseComparisonCommits(value) {
|
|
296
|
+
return String(value || '').split('\x1e').map(record => record.trim()).filter(Boolean).map(record => {
|
|
297
|
+
const [id = '', parents = '', shortId = '', ...subjectParts] = record.split('\x1f');
|
|
298
|
+
const parent = parents.trim().split(/\s+/).filter(Boolean)[0];
|
|
299
|
+
const subject = subjectParts.join('\x1f').trim();
|
|
300
|
+
if (!/^[a-f0-9]{40,64}$/i.test(id) || !/^[a-f0-9]{40,64}$/i.test(parent || '')) return null;
|
|
301
|
+
return {
|
|
302
|
+
base: parent,
|
|
303
|
+
head: id,
|
|
304
|
+
id: `commit:${id}`,
|
|
305
|
+
label: `${shortId || id.slice(0, 8)} ${subject || 'Commit'}`.trim(),
|
|
306
|
+
};
|
|
307
|
+
}).filter(Boolean);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function parseComparisonBranches(value, currentBranch) {
|
|
311
|
+
return String(value || '').split('\n').map(line => line.trim()).filter(Boolean).map(line => {
|
|
312
|
+
const [name = '', id = ''] = line.split('\0');
|
|
313
|
+
if (!name || name === currentBranch || name.endsWith('/HEAD') || !/^[a-f0-9]{40,64}$/i.test(id)) return null;
|
|
314
|
+
return { id, name };
|
|
315
|
+
}).filter(Boolean);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isSafeGitRevision(value) {
|
|
319
|
+
return typeof value === 'string'
|
|
320
|
+
&& value.length > 0
|
|
321
|
+
&& value.length <= 200
|
|
322
|
+
&& !value.startsWith('-')
|
|
323
|
+
&& !/[\\\0\r\n\t ]/.test(value);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isReviewHead(value) {
|
|
327
|
+
return value === 'now' || isSafeGitRevision(value);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function gitRangeRevisionArgs(base, head) {
|
|
331
|
+
return head === 'now' ? [base] : [base, head];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isSafeReviewPath(value) {
|
|
335
|
+
return typeof value === 'string'
|
|
336
|
+
&& value.length > 0
|
|
337
|
+
&& value.length <= 4096
|
|
338
|
+
&& !value.includes('\0')
|
|
339
|
+
&& !value.startsWith('/')
|
|
340
|
+
&& !value.startsWith('\\')
|
|
341
|
+
&& value.split(/[\\/]/).every(segment => segment && segment !== '.' && segment !== '..');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function nulFields(stdout) {
|
|
345
|
+
const fields = String(stdout || '').split('\0');
|
|
346
|
+
if (fields.at(-1) === '') fields.pop();
|
|
347
|
+
return fields;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function changeFromStatus(status, path, previousPath) {
|
|
351
|
+
if (status.startsWith('R')) return { kind: 'renamed', path, previousPath, status: 'R' };
|
|
352
|
+
if (status.startsWith('C')) return { kind: 'copied', path, previousPath, status: 'C' };
|
|
353
|
+
if (status === 'A') return { kind: 'added', path, status: 'A' };
|
|
354
|
+
if (status === 'D') return { kind: 'deleted', path, status: 'D' };
|
|
355
|
+
if (status === 'W') return { kind: 'rewritten', path, status: 'W' };
|
|
356
|
+
return { kind: 'modified', path, status: 'M' };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function parseNameStatus(stdout) {
|
|
360
|
+
if (String(stdout || '').includes('\0')) {
|
|
361
|
+
const fields = nulFields(stdout);
|
|
362
|
+
const changes = [];
|
|
363
|
+
for (let index = 0; index < fields.length;) {
|
|
364
|
+
const status = fields[index++] || '';
|
|
365
|
+
if (status.startsWith('R') || status.startsWith('C')) {
|
|
366
|
+
const previousPath = fields[index++];
|
|
367
|
+
const filePath = fields[index++];
|
|
368
|
+
if (filePath) changes.push(changeFromStatus(status, filePath, previousPath));
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const filePath = fields[index++];
|
|
372
|
+
if (filePath) changes.push(changeFromStatus(status, filePath));
|
|
373
|
+
}
|
|
374
|
+
return changes;
|
|
375
|
+
}
|
|
376
|
+
return String(stdout || '').split('\n').filter(Boolean).map(line => {
|
|
377
|
+
const fields = line.split('\t');
|
|
378
|
+
const status = fields[0] || '';
|
|
379
|
+
return changeFromStatus(status, status.startsWith('R') || status.startsWith('C') ? fields[2] : fields[1], fields[1]);
|
|
380
|
+
}).filter(file => file.path);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function normalizedNumstatPath(pathField) {
|
|
384
|
+
const value = String(pathField || '');
|
|
385
|
+
const braceRename = value.match(/^(.*)\{.* => (.*)\}(.*)$/);
|
|
386
|
+
if (braceRename) return `${braceRename[1]}${braceRename[2]}${braceRename[3]}`;
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function numstatPathForChanges(pathField, changes = []) {
|
|
391
|
+
const value = String(pathField || '');
|
|
392
|
+
if (!value) return '';
|
|
393
|
+
if (changes.some(change => change.path === value)) return value;
|
|
394
|
+
|
|
395
|
+
const bracePath = normalizedNumstatPath(value);
|
|
396
|
+
const renamed = changes.find(change => {
|
|
397
|
+
if (!change.previousPath) return false;
|
|
398
|
+
if (bracePath && change.path === bracePath) return true;
|
|
399
|
+
if (value === `${change.previousPath} => ${change.path}`) return true;
|
|
400
|
+
return false;
|
|
401
|
+
});
|
|
402
|
+
if (renamed) return renamed.path;
|
|
403
|
+
return value;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function splitNumstatEntry(value) {
|
|
407
|
+
const text = String(value || '');
|
|
408
|
+
const first = text.indexOf('\t');
|
|
409
|
+
const second = first === -1 ? -1 : text.indexOf('\t', first + 1);
|
|
410
|
+
if (first === -1 || second === -1) return ['', '', ''];
|
|
411
|
+
return [text.slice(0, first), text.slice(first + 1, second), text.slice(second + 1)];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function parseNumstat(stdout, changes = []) {
|
|
415
|
+
const stats = new Map();
|
|
416
|
+
if (String(stdout || '').includes('\0')) {
|
|
417
|
+
const fields = nulFields(stdout);
|
|
418
|
+
for (let index = 0; index < fields.length;) {
|
|
419
|
+
const counts = fields[index++] || '';
|
|
420
|
+
const [addedText, removedText, inlinePath] = splitNumstatEntry(counts);
|
|
421
|
+
const renameLike = inlinePath === '';
|
|
422
|
+
const pathField = renameLike ? `${fields[index++] || ''} => ${fields[index++] || ''}` : inlinePath;
|
|
423
|
+
const added = Number(addedText);
|
|
424
|
+
const removed = Number(removedText);
|
|
425
|
+
const path = numstatPathForChanges(pathField, changes);
|
|
426
|
+
if (!path) continue;
|
|
427
|
+
stats.set(path, {
|
|
428
|
+
added: Number.isInteger(added) && added >= 0 ? added : 0,
|
|
429
|
+
binary: addedText === '-' || removedText === '-',
|
|
430
|
+
removed: Number.isInteger(removed) && removed >= 0 ? removed : 0,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return stats;
|
|
434
|
+
}
|
|
435
|
+
for (const line of String(stdout || '').split('\n')) {
|
|
436
|
+
if (!line.trim()) continue;
|
|
437
|
+
const [addedText, removedText, pathText] = splitNumstatEntry(line);
|
|
438
|
+
if (!pathText) continue;
|
|
439
|
+
const added = Number(addedText);
|
|
440
|
+
const removed = Number(removedText);
|
|
441
|
+
const path = numstatPathForChanges(pathText, changes);
|
|
442
|
+
if (!path) continue;
|
|
443
|
+
stats.set(path, {
|
|
444
|
+
added: Number.isInteger(added) && added >= 0 ? added : 0,
|
|
445
|
+
binary: addedText === '-' || removedText === '-',
|
|
446
|
+
removed: Number.isInteger(removed) && removed >= 0 ? removed : 0,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return stats;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function parseRawDiffMetadata(stdout) {
|
|
453
|
+
const metadata = new Map();
|
|
454
|
+
if (String(stdout || '').includes('\0')) {
|
|
455
|
+
const fields = nulFields(stdout);
|
|
456
|
+
for (let index = 0; index < fields.length;) {
|
|
457
|
+
const header = fields[index++] || '';
|
|
458
|
+
const match = header.match(/^:([0-7]{6}) ([0-7]{6}) ([0-9a-f]+) ([0-9a-f]+) ([A-Z][0-9]*)$/i);
|
|
459
|
+
if (!match) continue;
|
|
460
|
+
const status = match[5] || '';
|
|
461
|
+
const oldPath = fields[index++];
|
|
462
|
+
const filePath = (status.startsWith('R') || status.startsWith('C')) ? fields[index++] : oldPath;
|
|
463
|
+
if (!filePath) continue;
|
|
464
|
+
const fileMetadata = {};
|
|
465
|
+
if (match[1] !== '000000') fileMetadata.oldMode = match[1];
|
|
466
|
+
if (match[2] !== '000000') fileMetadata.newMode = match[2];
|
|
467
|
+
if (!isZeroObjectId(match[3])) fileMetadata.oldSha = match[3];
|
|
468
|
+
if (!isZeroObjectId(match[4])) fileMetadata.newSha = match[4];
|
|
469
|
+
metadata.set(filePath, fileMetadata);
|
|
470
|
+
}
|
|
471
|
+
return metadata;
|
|
472
|
+
}
|
|
473
|
+
for (const line of String(stdout || '').split('\n')) {
|
|
474
|
+
if (!line.trim()) continue;
|
|
475
|
+
const fields = line.split('\t');
|
|
476
|
+
const header = fields[0] || '';
|
|
477
|
+
const match = header.match(/^:([0-7]{6}) ([0-7]{6}) ([0-9a-f]+) ([0-9a-f]+) ([A-Z][0-9]*)$/i);
|
|
478
|
+
if (!match) continue;
|
|
479
|
+
const status = match[5] || '';
|
|
480
|
+
const path = (status.startsWith('R') || status.startsWith('C')) ? fields[2] : fields[1];
|
|
481
|
+
if (!path) continue;
|
|
482
|
+
const fileMetadata = {};
|
|
483
|
+
if (match[1] !== '000000') fileMetadata.oldMode = match[1];
|
|
484
|
+
if (match[2] !== '000000') fileMetadata.newMode = match[2];
|
|
485
|
+
if (!isZeroObjectId(match[3])) fileMetadata.oldSha = match[3];
|
|
486
|
+
if (!isZeroObjectId(match[4])) fileMetadata.newSha = match[4];
|
|
487
|
+
metadata.set(path, fileMetadata);
|
|
488
|
+
}
|
|
489
|
+
return metadata;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function patchDiffHeader(patch) {
|
|
493
|
+
const lines = String(patch || '').split('\n');
|
|
494
|
+
const header = [];
|
|
495
|
+
for (const line of lines) {
|
|
496
|
+
if (line.startsWith('@@ ')) break;
|
|
497
|
+
if (!line) continue;
|
|
498
|
+
header.push(line);
|
|
499
|
+
if (line === 'GIT binary patch') break;
|
|
500
|
+
}
|
|
501
|
+
return header;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function isZeroObjectId(value) {
|
|
505
|
+
return /^0+$/.test(String(value || ''));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function patchMetadata(patchOrHeader) {
|
|
509
|
+
const header = Array.isArray(patchOrHeader) ? patchOrHeader : patchDiffHeader(patchOrHeader);
|
|
510
|
+
const metadata = {};
|
|
511
|
+
for (const line of header) {
|
|
512
|
+
const index = line.match(/^index ([0-9a-f]+)\.\.([0-9a-f]+)(?: ([0-7]{6}))?$/i);
|
|
513
|
+
if (index) {
|
|
514
|
+
if (!isZeroObjectId(index[1])) metadata.oldSha = index[1];
|
|
515
|
+
if (!isZeroObjectId(index[2])) metadata.newSha = index[2];
|
|
516
|
+
if (index[3]) {
|
|
517
|
+
metadata.oldMode = metadata.oldMode || index[3];
|
|
518
|
+
metadata.newMode = metadata.newMode || index[3];
|
|
519
|
+
}
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const newFileMode = line.match(/^new file mode ([0-7]{6})$/);
|
|
523
|
+
if (newFileMode) {
|
|
524
|
+
metadata.newMode = newFileMode[1];
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const deletedFileMode = line.match(/^deleted file mode ([0-7]{6})$/);
|
|
528
|
+
if (deletedFileMode) {
|
|
529
|
+
metadata.oldMode = deletedFileMode[1];
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const oldMode = line.match(/^old mode ([0-7]{6})$/);
|
|
533
|
+
if (oldMode) {
|
|
534
|
+
metadata.oldMode = oldMode[1];
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const newMode = line.match(/^new mode ([0-7]{6})$/);
|
|
538
|
+
if (newMode) metadata.newMode = newMode[1];
|
|
539
|
+
}
|
|
540
|
+
return metadata;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function fileFromPatch(change, patch) {
|
|
544
|
+
const hunks = parseUnifiedDiffRows(patch);
|
|
545
|
+
const totals = countRows(hunks);
|
|
546
|
+
const binary = /(^|\n)(Binary files? |GIT binary patch(?:\n|$))/.test(String(patch || ''));
|
|
547
|
+
const diffHeader = patchDiffHeader(patch);
|
|
548
|
+
const metadata = patchMetadata(diffHeader);
|
|
549
|
+
return {
|
|
550
|
+
added: totals.added,
|
|
551
|
+
...(binary ? { binary: true } : {}),
|
|
552
|
+
diff: {
|
|
553
|
+
...(diffHeader.length ? { diffHeader } : {}),
|
|
554
|
+
hunks,
|
|
555
|
+
},
|
|
556
|
+
kind: change.kind,
|
|
557
|
+
...metadata,
|
|
558
|
+
path: change.path,
|
|
559
|
+
...(change.previousPath ? { previousPath: change.previousPath } : {}),
|
|
560
|
+
removed: totals.removed,
|
|
561
|
+
status: change.status || reviewStatus(change.kind),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function fileWithStats(file, stat) {
|
|
566
|
+
if (!stat) return file;
|
|
567
|
+
return {
|
|
568
|
+
...file,
|
|
569
|
+
added: stat.added,
|
|
570
|
+
...(stat.binary === true ? { binary: true } : {}),
|
|
571
|
+
removed: stat.removed,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function gitDiffPathArgs(change) {
|
|
576
|
+
return change.previousPath ? [change.previousPath, change.path] : [change.path];
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function gitDiffPathspecArgs(changes = []) {
|
|
580
|
+
const seen = new Set();
|
|
581
|
+
const paths = [];
|
|
582
|
+
for (const change of changes) {
|
|
583
|
+
for (const filePath of gitDiffPathArgs(change)) {
|
|
584
|
+
if (!filePath || seen.has(filePath)) continue;
|
|
585
|
+
seen.add(filePath);
|
|
586
|
+
paths.push(filePath);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return paths;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
class ReviewDiffService {
|
|
593
|
+
constructor(agentManager, fileService) {
|
|
594
|
+
this.agentManager = agentManager;
|
|
595
|
+
this.fileService = fileService;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
resolveWorkspace(agentId, requestedRoot) {
|
|
599
|
+
if (requestedRoot !== undefined) {
|
|
600
|
+
if (typeof requestedRoot !== 'string' || !requestedRoot.trim() || (typeof agentId === 'string' && agentId.trim())) {
|
|
601
|
+
throw new WorkspaceFileError('exactly one review workspace target is required', 400);
|
|
602
|
+
}
|
|
603
|
+
let root;
|
|
604
|
+
try {
|
|
605
|
+
root = fs.realpathSync.native(path.resolve(requestedRoot));
|
|
606
|
+
} catch {
|
|
607
|
+
throw new WorkspaceFileError('review workspace does not exist', 404);
|
|
608
|
+
}
|
|
609
|
+
if (!fs.statSync(root).isDirectory()) throw new WorkspaceFileError('review workspace must be a directory', 400);
|
|
610
|
+
return root;
|
|
611
|
+
}
|
|
612
|
+
if (typeof agentId !== 'string' || !agentId.trim()) {
|
|
613
|
+
throw new WorkspaceFileError('review workspace target is required', 400);
|
|
614
|
+
}
|
|
615
|
+
const root = this.agentManager?.getAgentWorkspaceRoot?.(agentId);
|
|
616
|
+
if (!root) throw new WorkspaceFileError('agent not found', 404);
|
|
617
|
+
return root;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async git(root, args, options = {}) {
|
|
621
|
+
try {
|
|
622
|
+
return await this.fileService.execFile(this.fileService.gitPath, ['-C', root, ...args], {
|
|
623
|
+
cwd: root,
|
|
624
|
+
timeout: this.fileService.diffTimeoutMs,
|
|
625
|
+
maxBuffer: this.fileService.diffMaxBuffer,
|
|
626
|
+
...options,
|
|
627
|
+
});
|
|
628
|
+
} catch (error) {
|
|
629
|
+
if (error?.code === 'ETIMEDOUT') throw new WorkspaceFileError('git comparison source request timed out', 504);
|
|
630
|
+
throw new WorkspaceFileError(error?.stderr || error?.message || 'git comparison sources could not be loaded', 500);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async getComparisonSources(agentId, options = {}) {
|
|
635
|
+
const root = this.resolveWorkspace(agentId, options.root);
|
|
636
|
+
const [{ stdout: headOutput }, { stdout: indexOutput }, branchResult, logResult, statusResult] = await Promise.all([
|
|
637
|
+
this.git(root, ['rev-parse', '--verify', 'HEAD']),
|
|
638
|
+
this.git(root, ['write-tree']),
|
|
639
|
+
this.git(root, ['symbolic-ref', '--quiet', '--short', 'HEAD']).catch(() => ({ stdout: '' })),
|
|
640
|
+
this.git(root, ['log', '-n', '12', '--format=%H%x1f%P%x1f%h%x1f%s%x1e', 'HEAD']),
|
|
641
|
+
Promise.all([
|
|
642
|
+
this.git(root, ['diff', '--name-only', '-z', '--']),
|
|
643
|
+
this.git(root, ['diff', '--cached', '--name-only', '-z', '--']),
|
|
644
|
+
this.git(root, ['ls-files', '--others', '--exclude-standard', '-z']),
|
|
645
|
+
]),
|
|
646
|
+
]);
|
|
647
|
+
const head = String(headOutput || '').trim();
|
|
648
|
+
const indexTree = String(indexOutput || '').trim();
|
|
649
|
+
if (!/^[a-f0-9]{40,64}$/i.test(head) || !/^[a-f0-9]{40,64}$/i.test(indexTree)) {
|
|
650
|
+
throw new WorkspaceFileError('git comparison sources are invalid', 500);
|
|
651
|
+
}
|
|
652
|
+
const currentBranch = String(branchResult.stdout || '').trim();
|
|
653
|
+
const [unstagedResult, stagedResult, untrackedResult] = statusResult;
|
|
654
|
+
const refsResult = await this.git(root, [
|
|
655
|
+
'for-each-ref',
|
|
656
|
+
'--count=20',
|
|
657
|
+
'--sort=-committerdate',
|
|
658
|
+
'--format=%(refname:short)%00%(objectname)',
|
|
659
|
+
'refs/heads',
|
|
660
|
+
'refs/remotes',
|
|
661
|
+
]);
|
|
662
|
+
const branchRefs = parseComparisonBranches(refsResult.stdout, currentBranch);
|
|
663
|
+
const branches = [];
|
|
664
|
+
for (const branch of branchRefs) {
|
|
665
|
+
try {
|
|
666
|
+
const { stdout } = await this.git(root, ['merge-base', 'HEAD', branch.id]);
|
|
667
|
+
const base = String(stdout || '').trim();
|
|
668
|
+
if (!/^[a-f0-9]{40,64}$/i.test(base) || base === head) continue;
|
|
669
|
+
branches.push({
|
|
670
|
+
base,
|
|
671
|
+
head,
|
|
672
|
+
id: `branch:${branch.name}`,
|
|
673
|
+
label: branch.name,
|
|
674
|
+
});
|
|
675
|
+
} catch {
|
|
676
|
+
// Unrelated branch histories are not useful comparison sources.
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
branches,
|
|
681
|
+
commits: parseComparisonCommits(logResult.stdout),
|
|
682
|
+
currentBranch: currentBranch || 'Detached HEAD',
|
|
683
|
+
root,
|
|
684
|
+
staged: {
|
|
685
|
+
available: Boolean(String(stagedResult.stdout || '')),
|
|
686
|
+
base: head,
|
|
687
|
+
head: indexTree,
|
|
688
|
+
id: 'staged',
|
|
689
|
+
label: 'Staged',
|
|
690
|
+
},
|
|
691
|
+
unstaged: {
|
|
692
|
+
available: Boolean(String(unstagedResult.stdout || '') || String(untrackedResult.stdout || '')),
|
|
693
|
+
base: indexTree,
|
|
694
|
+
head: 'now',
|
|
695
|
+
id: 'unstaged',
|
|
696
|
+
label: 'Unstaged',
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async getWorkingCopyChanges(root, options, limit) {
|
|
702
|
+
const scope = normalizeWorkingCopyScope(options.scope);
|
|
703
|
+
const scanLimit = scope ? MAX_WORKING_COPY_SCAN_FILES : limit;
|
|
704
|
+
const changes = await this.fileService.changes(root, { limit: scanLimit });
|
|
705
|
+
const items = filterWorkingCopyChangeItems(root, assertUniqueReviewPaths(changes.items), options);
|
|
706
|
+
return {
|
|
707
|
+
items: items.slice(0, limit),
|
|
708
|
+
truncated: (
|
|
709
|
+
changes.truncated === true
|
|
710
|
+
&& (scope !== 'tracked' || changes.items.at(-1)?.gitStatus !== 'untracked')
|
|
711
|
+
) || items.length > limit,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async getGitRangeChanges(root, base, head) {
|
|
716
|
+
try {
|
|
717
|
+
const { stdout } = await this.fileService.execFile(this.fileService.gitPath, [
|
|
718
|
+
'-C',
|
|
719
|
+
root,
|
|
720
|
+
'diff',
|
|
721
|
+
'--name-status',
|
|
722
|
+
'-z',
|
|
723
|
+
'--find-renames',
|
|
724
|
+
...gitRangeRevisionArgs(base, head),
|
|
725
|
+
'--',
|
|
726
|
+
], { cwd: root, timeout: this.fileService.diffTimeoutMs, maxBuffer: this.fileService.diffMaxBuffer });
|
|
727
|
+
const changes = parseNameStatus(stdout);
|
|
728
|
+
if (head !== 'now') return changes;
|
|
729
|
+
const untracked = await this.fileService.execFile(this.fileService.gitPath, [
|
|
730
|
+
'-C',
|
|
731
|
+
root,
|
|
732
|
+
'ls-files',
|
|
733
|
+
'--others',
|
|
734
|
+
'--exclude-standard',
|
|
735
|
+
'-z',
|
|
736
|
+
], { cwd: root, timeout: this.fileService.diffTimeoutMs, maxBuffer: this.fileService.diffMaxBuffer });
|
|
737
|
+
return [...changes, ...nulFields(untracked.stdout).map(path => ({ gitStatus: 'untracked', kind: 'added', path, status: 'A' }))];
|
|
738
|
+
} catch (error) {
|
|
739
|
+
if (error?.code === 'ETIMEDOUT') throw new WorkspaceFileError('git diff timed out', 504);
|
|
740
|
+
throw new WorkspaceFileError(error?.stderr || error?.message || 'git diff failed', 500);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async getCommitSummary(root, revision) {
|
|
745
|
+
if (!isSafeGitRevision(revision) || revision === 'now') return null;
|
|
746
|
+
try {
|
|
747
|
+
const { stdout } = await this.fileService.execFile(this.fileService.gitPath, [
|
|
748
|
+
'-C',
|
|
749
|
+
root,
|
|
750
|
+
'show',
|
|
751
|
+
'-s',
|
|
752
|
+
'--format=%H%x1f%an%x1f%ae%x1f%aI%x1f%B',
|
|
753
|
+
revision,
|
|
754
|
+
], { cwd: root, timeout: this.fileService.diffTimeoutMs, maxBuffer: this.fileService.diffMaxBuffer });
|
|
755
|
+
const [id, authorName, authorEmail, authoredAt, ...messageParts] = String(stdout || '').split('\x1f');
|
|
756
|
+
const message = messageParts.join('\x1f').trim();
|
|
757
|
+
if (!/^[a-f0-9]{40,64}$/i.test(id || '') || !message) return null;
|
|
758
|
+
return { authoredAt: String(authoredAt || '').trim(), authorEmail: String(authorEmail || '').trim(), authorName: String(authorName || '').trim(), id, message };
|
|
759
|
+
} catch {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async getComparison(root, base, head) {
|
|
765
|
+
const [baseCommit, headCommit] = await Promise.all([
|
|
766
|
+
this.getCommitSummary(root, base),
|
|
767
|
+
head === 'now' ? Promise.resolve(null) : this.getCommitSummary(root, head),
|
|
768
|
+
]);
|
|
769
|
+
if (!baseCommit && !headCommit) return null;
|
|
770
|
+
return {
|
|
771
|
+
...(baseCommit ? { base: baseCommit } : {}),
|
|
772
|
+
...(headCommit ? { head: headCommit } : {}),
|
|
773
|
+
workingTree: head === 'now',
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async readGitTextFile(root, revision, filePath) {
|
|
778
|
+
try {
|
|
779
|
+
const { stdout } = await this.fileService.execFile(this.fileService.gitPath, [
|
|
780
|
+
'-C',
|
|
781
|
+
root,
|
|
782
|
+
'show',
|
|
783
|
+
`${revision}:${filePath}`,
|
|
784
|
+
], { cwd: root, encoding: 'buffer', timeout: this.fileService.diffTimeoutMs, maxBuffer: this.fileService.diffMaxBuffer });
|
|
785
|
+
const buffer = Buffer.isBuffer(stdout) ? stdout : Buffer.from(String(stdout || ''));
|
|
786
|
+
if (buffer.includes(0)) throw new WorkspaceFileError('binary files do not have expandable text context', 415);
|
|
787
|
+
return textLines(buffer.toString('utf8'));
|
|
788
|
+
} catch (error) {
|
|
789
|
+
if (error instanceof WorkspaceFileError) throw error;
|
|
790
|
+
if (error?.code === 'ETIMEDOUT') throw new WorkspaceFileError('git show timed out', 504);
|
|
791
|
+
throw new WorkspaceFileError(error?.stderr || error?.message || 'git file content could not be loaded', 500);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async readWorkingTreeTextFile(root, filePath) {
|
|
796
|
+
try {
|
|
797
|
+
const resolved = await this.fileService.resolvePath(root, filePath);
|
|
798
|
+
const buffer = await fs.promises.readFile(resolved.target);
|
|
799
|
+
if (buffer.includes(0)) throw new WorkspaceFileError('binary files do not have expandable text context', 415);
|
|
800
|
+
return textLines(buffer.toString('utf8'));
|
|
801
|
+
} catch (error) {
|
|
802
|
+
if (error instanceof WorkspaceFileError) throw error;
|
|
803
|
+
throw new WorkspaceFileError(error?.message || 'working tree file content could not be loaded', 500);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async getWorkingCopyTextSources(root, change, source) {
|
|
808
|
+
const kind = reviewKind(change.gitStatus);
|
|
809
|
+
const leftLines = kind === 'added' ? [] : textLines(source.originalContent);
|
|
810
|
+
const rightLines = kind === 'deleted' ? [] : textLines(source.modifiedContent);
|
|
811
|
+
return {
|
|
812
|
+
leftLines,
|
|
813
|
+
leftName: change.previousPath || change.path,
|
|
814
|
+
rightLines,
|
|
815
|
+
rightName: change.path,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async getGitRangeTextSources(root, base, head, change) {
|
|
820
|
+
const kind = change.kind || reviewKind(change.gitStatus);
|
|
821
|
+
const leftName = change.previousPath || change.path;
|
|
822
|
+
const [leftLines, rightLines] = await Promise.all([
|
|
823
|
+
kind === 'added' ? Promise.resolve([]) : this.readGitTextFile(root, base, leftName),
|
|
824
|
+
kind === 'deleted'
|
|
825
|
+
? Promise.resolve([])
|
|
826
|
+
: head === 'now'
|
|
827
|
+
? this.readWorkingTreeTextFile(root, change.path)
|
|
828
|
+
: this.readGitTextFile(root, head, change.path),
|
|
829
|
+
]);
|
|
830
|
+
return { leftLines, leftName, rightLines, rightName: change.path };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async getWorkingCopyFileContext(agentId, filePath, options = {}) {
|
|
834
|
+
const root = this.resolveWorkspace(agentId, options.root);
|
|
835
|
+
if (!isSafeReviewPath(filePath)) throw new WorkspaceFileError('file path is required', 400);
|
|
836
|
+
const changes = await this.getWorkingCopyChanges(root, options, MAX_REVIEW_FILES);
|
|
837
|
+
const change = changes.items.find(item => item.path === filePath);
|
|
838
|
+
if (!change) throw new WorkspaceFileError('review file not found', 404);
|
|
839
|
+
if (change.gitStatus === 'untracked') throw new WorkspaceFileError('untracked files do not have common diff context', 400);
|
|
840
|
+
const source = await this.fileService.diff(root, change.path);
|
|
841
|
+
if (source.binary === true) throw new WorkspaceFileError('binary files do not have expandable text context', 415);
|
|
842
|
+
return contextRowsFromSources(await this.getWorkingCopyTextSources(root, change, source), options);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async getGitRangeFileContext(agentId, options = {}) {
|
|
846
|
+
const root = this.resolveWorkspace(agentId, options.root);
|
|
847
|
+
const base = String(options.base || '').trim();
|
|
848
|
+
const head = String(options.head || '').trim();
|
|
849
|
+
const filePath = typeof options.path === 'string' ? options.path : '';
|
|
850
|
+
if (!isSafeGitRevision(base) || !isReviewHead(head)) {
|
|
851
|
+
throw new WorkspaceFileError('base and head revisions are required', 400);
|
|
852
|
+
}
|
|
853
|
+
if (!isSafeReviewPath(filePath)) throw new WorkspaceFileError('file path is required', 400);
|
|
854
|
+
const changes = assertUniqueReviewPaths(await this.getGitRangeChanges(root, base, head));
|
|
855
|
+
const change = changes.find(item => item.path === filePath);
|
|
856
|
+
if (!change) throw new WorkspaceFileError('review file not found', 404);
|
|
857
|
+
if (change.gitStatus === 'untracked') throw new WorkspaceFileError('untracked files do not have common diff context', 400);
|
|
858
|
+
return contextRowsFromSources(await this.getGitRangeTextSources(root, base, head, change), options);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async getGitRangeUntrackedFile(root, change, metadataOnly) {
|
|
862
|
+
const source = await this.fileService.diff(root, change.path);
|
|
863
|
+
const hunks = untrackedHunks(source.modifiedContent);
|
|
864
|
+
const totals = countRows(hunks);
|
|
865
|
+
const truncated = source.truncated === true || untrackedContentTooLarge(source.modifiedContent);
|
|
866
|
+
const metadata = patchMetadata(patchDiffHeader(untrackedPatch(change.path, source.modifiedContent)));
|
|
867
|
+
if (metadataOnly) {
|
|
868
|
+
return metadataFile(change, {
|
|
869
|
+
added: totals.added,
|
|
870
|
+
binary: source.binary === true,
|
|
871
|
+
diffTooExpensive: truncated,
|
|
872
|
+
...metadata,
|
|
873
|
+
removed: totals.removed,
|
|
874
|
+
size: source.size,
|
|
875
|
+
sizeDelta: source.sizeDelta,
|
|
876
|
+
truncated,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
added: totals.added,
|
|
881
|
+
...(source.binary === true ? { binary: true } : {}),
|
|
882
|
+
diff: {
|
|
883
|
+
diffHeader: patchDiffHeader(untrackedPatch(change.path, source.modifiedContent)),
|
|
884
|
+
hunks,
|
|
885
|
+
truncated,
|
|
886
|
+
},
|
|
887
|
+
...(truncated ? { diffTooExpensive: true } : {}),
|
|
888
|
+
kind: 'added',
|
|
889
|
+
...metadata,
|
|
890
|
+
path: change.path,
|
|
891
|
+
removed: totals.removed,
|
|
892
|
+
...(Number.isInteger(source.size) ? { size: source.size } : {}),
|
|
893
|
+
...(Number.isInteger(source.sizeDelta) ? { sizeDelta: source.sizeDelta } : {}),
|
|
894
|
+
status: 'A',
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async getWorkingCopy(agentId, options = {}) {
|
|
899
|
+
const root = this.resolveWorkspace(agentId, options.root);
|
|
900
|
+
const limit = normalizeReviewLimit(options.limit);
|
|
901
|
+
const metadataOnly = metadataOnlyOption(options.metadataOnly);
|
|
902
|
+
const contentOptions = diffContentOptions(options);
|
|
903
|
+
const changes = await this.getWorkingCopyChanges(root, options, limit);
|
|
904
|
+
const changeItems = changes.items;
|
|
905
|
+
const files = await mapWithConcurrency(changeItems, async change => {
|
|
906
|
+
const source = await this.fileService.diff(root, change.path);
|
|
907
|
+
const diffSource = !metadataOnly && hasOptions(contentOptions)
|
|
908
|
+
? await this.fileService.diff(root, change.path, contentOptions)
|
|
909
|
+
: source;
|
|
910
|
+
const isUntracked = change.gitStatus === 'untracked';
|
|
911
|
+
const metadataHunks = isUntracked ? untrackedHunks(source.modifiedContent) : parseUnifiedDiffRows(source.patch);
|
|
912
|
+
const hunks = isUntracked ? metadataHunks : parseUnifiedDiffRows(diffSource.patch);
|
|
913
|
+
const totals = countRows(metadataHunks);
|
|
914
|
+
const kind = reviewKind(change.gitStatus);
|
|
915
|
+
const diffHeader = patchDiffHeader(isUntracked ? untrackedPatch(change.path, source.modifiedContent) : (diffSource.patch || source.patch));
|
|
916
|
+
const metadata = patchMetadata(diffHeader);
|
|
917
|
+
const untrackedTooLarge = isUntracked && untrackedContentTooLarge(source.modifiedContent);
|
|
918
|
+
if (metadataOnly) {
|
|
919
|
+
return metadataFile({ ...change, kind }, {
|
|
920
|
+
added: totals.added,
|
|
921
|
+
binary: source.binary === true,
|
|
922
|
+
diffTooExpensive: source.truncated === true || untrackedTooLarge,
|
|
923
|
+
...metadata,
|
|
924
|
+
removed: totals.removed,
|
|
925
|
+
size: source.size,
|
|
926
|
+
sizeDelta: source.sizeDelta,
|
|
927
|
+
truncated: source.truncated === true || untrackedTooLarge,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
added: totals.added,
|
|
932
|
+
...(source.binary === true ? { binary: true } : {}),
|
|
933
|
+
diff: {
|
|
934
|
+
...(diffHeader.length ? { diffHeader } : {}),
|
|
935
|
+
hunks,
|
|
936
|
+
truncated: source.truncated === true || diffSource.truncated === true || untrackedTooLarge,
|
|
937
|
+
},
|
|
938
|
+
...(source.truncated === true || diffSource.truncated === true || untrackedTooLarge ? { diffTooExpensive: true } : {}),
|
|
939
|
+
kind,
|
|
940
|
+
...metadata,
|
|
941
|
+
path: change.path,
|
|
942
|
+
...(change.previousPath ? { previousPath: change.previousPath } : {}),
|
|
943
|
+
removed: totals.removed,
|
|
944
|
+
...(Number.isInteger(source.size) ? { size: source.size } : {}),
|
|
945
|
+
...(Number.isInteger(source.sizeDelta) ? { sizeDelta: source.sizeDelta } : {}),
|
|
946
|
+
status: reviewStatus(kind),
|
|
947
|
+
};
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
const comparison = await this.getComparison(root, 'HEAD', 'now');
|
|
951
|
+
return {
|
|
952
|
+
basePatchset: 'HEAD',
|
|
953
|
+
...(comparison ? { comparison } : {}),
|
|
954
|
+
files,
|
|
955
|
+
isGitRepo: true,
|
|
956
|
+
patchset: workingCopyPatchset(files),
|
|
957
|
+
reviewId: workingCopyReviewId(root, options),
|
|
958
|
+
root,
|
|
959
|
+
source: 'working-copy',
|
|
960
|
+
truncated: changes.truncated,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async getWorkingCopyFile(agentId, filePath, options = {}) {
|
|
965
|
+
const root = this.resolveWorkspace(agentId, options.root);
|
|
966
|
+
const contentOptions = diffContentOptions(options);
|
|
967
|
+
if (!isSafeReviewPath(filePath)) throw new WorkspaceFileError('file path is required', 400);
|
|
968
|
+
const changes = await this.getWorkingCopyChanges(root, options, MAX_REVIEW_FILES);
|
|
969
|
+
const changeItems = changes.items;
|
|
970
|
+
const change = changeItems.find(item => item.path === filePath);
|
|
971
|
+
if (!change) throw new WorkspaceFileError('review file not found', 404);
|
|
972
|
+
const source = await this.fileService.diff(root, change.path);
|
|
973
|
+
const diffSource = hasOptions(contentOptions)
|
|
974
|
+
? await this.fileService.diff(root, change.path, contentOptions)
|
|
975
|
+
: source;
|
|
976
|
+
const isUntracked = change.gitStatus === 'untracked';
|
|
977
|
+
const metadataHunks = isUntracked ? untrackedHunks(source.modifiedContent) : parseUnifiedDiffRows(source.patch);
|
|
978
|
+
const hunks = isUntracked ? metadataHunks : parseUnifiedDiffRows(diffSource.patch);
|
|
979
|
+
const totals = countRows(metadataHunks);
|
|
980
|
+
const kind = reviewKind(change.gitStatus);
|
|
981
|
+
const diffHeader = patchDiffHeader(isUntracked ? untrackedPatch(change.path, source.modifiedContent) : (diffSource.patch || source.patch));
|
|
982
|
+
const metadata = patchMetadata(diffHeader);
|
|
983
|
+
const untrackedTooLarge = isUntracked && untrackedContentTooLarge(source.modifiedContent);
|
|
984
|
+
const textSources = options.fileMeta === true
|
|
985
|
+
&& !isUntracked
|
|
986
|
+
&& source.binary !== true
|
|
987
|
+
&& typeof source.originalContent === 'string'
|
|
988
|
+
&& typeof source.modifiedContent === 'string'
|
|
989
|
+
? await this.getWorkingCopyTextSources(root, change, source)
|
|
990
|
+
: null;
|
|
991
|
+
return {
|
|
992
|
+
added: totals.added,
|
|
993
|
+
...(source.binary === true ? { binary: true } : {}),
|
|
994
|
+
diff: {
|
|
995
|
+
...(diffHeader.length ? { diffHeader } : {}),
|
|
996
|
+
hunks,
|
|
997
|
+
...(textSources ? { leftMeta: reviewTextFileMeta(textSources.leftName, textSources.leftLines) } : {}),
|
|
998
|
+
...(textSources ? { rightMeta: reviewTextFileMeta(textSources.rightName, textSources.rightLines) } : {}),
|
|
999
|
+
truncated: source.truncated === true || diffSource.truncated === true || untrackedTooLarge,
|
|
1000
|
+
},
|
|
1001
|
+
...(source.truncated === true || diffSource.truncated === true || untrackedTooLarge ? { diffTooExpensive: true } : {}),
|
|
1002
|
+
kind,
|
|
1003
|
+
...metadata,
|
|
1004
|
+
path: change.path,
|
|
1005
|
+
...(change.previousPath ? { previousPath: change.previousPath } : {}),
|
|
1006
|
+
removed: totals.removed,
|
|
1007
|
+
...(Number.isInteger(source.size) ? { size: source.size } : {}),
|
|
1008
|
+
...(Number.isInteger(source.sizeDelta) ? { sizeDelta: source.sizeDelta } : {}),
|
|
1009
|
+
status: reviewStatus(kind),
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async getWorkingCopyPatch(agentId, options = {}) {
|
|
1014
|
+
const root = this.resolveWorkspace(agentId, options.root);
|
|
1015
|
+
const limit = normalizeReviewLimit(options.limit);
|
|
1016
|
+
const contentOptions = diffContentOptions(options);
|
|
1017
|
+
const changes = await this.getWorkingCopyChanges(root, options, limit);
|
|
1018
|
+
const changeItems = changes.items;
|
|
1019
|
+
const patchResults = await mapWithConcurrency(changeItems, async change => {
|
|
1020
|
+
const source = await this.fileService.diff(root, change.path, contentOptions);
|
|
1021
|
+
const untrackedTooLarge = change.gitStatus === 'untracked' && untrackedContentTooLarge(source.modifiedContent);
|
|
1022
|
+
return {
|
|
1023
|
+
patch: change.gitStatus === 'untracked'
|
|
1024
|
+
? untrackedPatch(change.path, source.modifiedContent)
|
|
1025
|
+
: String(source.patch || ''),
|
|
1026
|
+
truncated: source.truncated === true || untrackedTooLarge,
|
|
1027
|
+
};
|
|
1028
|
+
});
|
|
1029
|
+
return {
|
|
1030
|
+
patch: patchResults.map(result => result.patch).filter(Boolean).join('\n'),
|
|
1031
|
+
truncated: changes.truncated || patchResults.some(result => result.truncated),
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
async getGitRange(agentId, options = {}) {
|
|
1036
|
+
const root = this.resolveWorkspace(agentId, options.root);
|
|
1037
|
+
const base = String(options.base || '').trim();
|
|
1038
|
+
const head = String(options.head || '').trim();
|
|
1039
|
+
if (!isSafeGitRevision(base) || !isReviewHead(head)) {
|
|
1040
|
+
throw new WorkspaceFileError('base and head revisions are required', 400);
|
|
1041
|
+
}
|
|
1042
|
+
const limit = normalizeReviewLimit(options.limit);
|
|
1043
|
+
const ignoreWhitespace = normalizeIgnoreWhitespace(options.ignoreWhitespace);
|
|
1044
|
+
const context = normalizeDiffContext(options.context);
|
|
1045
|
+
const changes = await this.getGitRangeChanges(root, base, head);
|
|
1046
|
+
const comparisonPromise = this.getComparison(root, base, head);
|
|
1047
|
+
|
|
1048
|
+
const selected = assertUniqueReviewPaths(changes.slice(0, limit));
|
|
1049
|
+
const lineStats = ignoreWhitespace !== 'NONE' && !metadataOnlyOption(options.metadataOnly)
|
|
1050
|
+
? await this.getGitRangeNumstat(root, base, head, ignoreWhitespace, selected)
|
|
1051
|
+
: null;
|
|
1052
|
+
if (metadataOnlyOption(options.metadataOnly)) {
|
|
1053
|
+
const stats = await this.getGitRangeNumstat(root, base, head, 'NONE', selected);
|
|
1054
|
+
const rawMetadata = await this.getGitRangeRawMetadata(root, base, head, selected);
|
|
1055
|
+
const files = await mapWithConcurrency(selected, async change => {
|
|
1056
|
+
if (change.gitStatus === 'untracked') return this.getGitRangeUntrackedFile(root, change, true);
|
|
1057
|
+
return metadataFile(change, {
|
|
1058
|
+
...stats.get(change.path),
|
|
1059
|
+
...rawMetadata.get(change.path),
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
const comparison = await comparisonPromise;
|
|
1063
|
+
return {
|
|
1064
|
+
basePatchset: base,
|
|
1065
|
+
...(comparison ? { comparison } : {}),
|
|
1066
|
+
files,
|
|
1067
|
+
isGitRepo: true,
|
|
1068
|
+
patchset: head,
|
|
1069
|
+
reviewId: options.reviewId || gitRangeReviewId(root, base, head),
|
|
1070
|
+
root,
|
|
1071
|
+
source: 'git-range',
|
|
1072
|
+
truncated: changes.length > limit,
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const files = await mapWithConcurrency(selected, async change => {
|
|
1077
|
+
if (change.gitStatus === 'untracked') return this.getGitRangeUntrackedFile(root, change, false);
|
|
1078
|
+
try {
|
|
1079
|
+
const { stdout } = await this.fileService.execFile(this.fileService.gitPath, [
|
|
1080
|
+
'-C',
|
|
1081
|
+
root,
|
|
1082
|
+
'diff',
|
|
1083
|
+
'--find-renames',
|
|
1084
|
+
...gitWhitespaceArgs(ignoreWhitespace),
|
|
1085
|
+
...gitContextArgs(context),
|
|
1086
|
+
...gitRangeRevisionArgs(base, head),
|
|
1087
|
+
'--',
|
|
1088
|
+
...gitDiffPathArgs(change),
|
|
1089
|
+
], { cwd: root, timeout: this.fileService.diffTimeoutMs, maxBuffer: this.fileService.diffMaxBuffer });
|
|
1090
|
+
const file = fileFromPatch(change, stdout);
|
|
1091
|
+
const stat = lineStats?.get(change.path);
|
|
1092
|
+
return fileWithStats(file, stat);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
if (error?.code === 'ETIMEDOUT') throw new WorkspaceFileError('git diff timed out', 504);
|
|
1095
|
+
throw new WorkspaceFileError(error?.stderr || error?.message || 'git diff failed', 500);
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
const comparison = await comparisonPromise;
|
|
1100
|
+
return {
|
|
1101
|
+
basePatchset: base,
|
|
1102
|
+
...(comparison ? { comparison } : {}),
|
|
1103
|
+
files,
|
|
1104
|
+
isGitRepo: true,
|
|
1105
|
+
patchset: head,
|
|
1106
|
+
reviewId: options.reviewId || gitRangeReviewId(root, base, head),
|
|
1107
|
+
root,
|
|
1108
|
+
source: 'git-range',
|
|
1109
|
+
truncated: changes.length > limit,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
async getGitRangeFile(agentId, options = {}) {
|
|
1114
|
+
const root = this.resolveWorkspace(agentId, options.root);
|
|
1115
|
+
const base = String(options.base || '').trim();
|
|
1116
|
+
const head = String(options.head || '').trim();
|
|
1117
|
+
const path = typeof options.path === 'string' ? options.path : '';
|
|
1118
|
+
const ignoreWhitespace = normalizeIgnoreWhitespace(options.ignoreWhitespace);
|
|
1119
|
+
const context = normalizeDiffContext(options.context);
|
|
1120
|
+
if (!isSafeGitRevision(base) || !isReviewHead(head)) {
|
|
1121
|
+
throw new WorkspaceFileError('base and head revisions are required', 400);
|
|
1122
|
+
}
|
|
1123
|
+
if (!isSafeReviewPath(path)) throw new WorkspaceFileError('file path is required', 400);
|
|
1124
|
+
const changes = await this.getGitRangeChanges(root, base, head);
|
|
1125
|
+
const uniqueChanges = assertUniqueReviewPaths(changes);
|
|
1126
|
+
const change = uniqueChanges.find(item => item.path === path);
|
|
1127
|
+
if (!change) throw new WorkspaceFileError('review file not found', 404);
|
|
1128
|
+
if (change.gitStatus === 'untracked') return this.getGitRangeUntrackedFile(root, change, false);
|
|
1129
|
+
const stats = ignoreWhitespace !== 'NONE'
|
|
1130
|
+
? await this.getGitRangeNumstat(root, base, head, ignoreWhitespace, [change])
|
|
1131
|
+
: null;
|
|
1132
|
+
try {
|
|
1133
|
+
const { stdout } = await this.fileService.execFile(this.fileService.gitPath, [
|
|
1134
|
+
'-C',
|
|
1135
|
+
root,
|
|
1136
|
+
'diff',
|
|
1137
|
+
'--find-renames',
|
|
1138
|
+
...gitWhitespaceArgs(ignoreWhitespace),
|
|
1139
|
+
...gitContextArgs(context),
|
|
1140
|
+
...gitRangeRevisionArgs(base, head),
|
|
1141
|
+
'--',
|
|
1142
|
+
...gitDiffPathArgs(change),
|
|
1143
|
+
], { cwd: root, timeout: this.fileService.diffTimeoutMs, maxBuffer: this.fileService.diffMaxBuffer });
|
|
1144
|
+
const file = fileFromPatch(change, stdout);
|
|
1145
|
+
if (options.fileMeta === true && file.binary !== true) {
|
|
1146
|
+
const textSources = await this.getGitRangeTextSources(root, base, head, change);
|
|
1147
|
+
file.diff = {
|
|
1148
|
+
...file.diff,
|
|
1149
|
+
leftMeta: reviewTextFileMeta(textSources.leftName, textSources.leftLines),
|
|
1150
|
+
rightMeta: reviewTextFileMeta(textSources.rightName, textSources.rightLines),
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
const stat = stats?.get(change.path);
|
|
1154
|
+
return fileWithStats(file, stat);
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
if (error?.code === 'ETIMEDOUT') throw new WorkspaceFileError('git diff timed out', 504);
|
|
1157
|
+
throw new WorkspaceFileError(error?.stderr || error?.message || 'git diff failed', 500);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
async getGitRangePatch(agentId, options = {}) {
|
|
1162
|
+
const root = this.resolveWorkspace(agentId, options.root);
|
|
1163
|
+
const base = String(options.base || '').trim();
|
|
1164
|
+
const head = String(options.head || '').trim();
|
|
1165
|
+
if (!isSafeGitRevision(base) || !isReviewHead(head)) {
|
|
1166
|
+
throw new WorkspaceFileError('base and head revisions are required', 400);
|
|
1167
|
+
}
|
|
1168
|
+
const limit = normalizeReviewLimit(options.limit);
|
|
1169
|
+
const ignoreWhitespace = normalizeIgnoreWhitespace(options.ignoreWhitespace);
|
|
1170
|
+
const context = normalizeDiffContext(options.context);
|
|
1171
|
+
const changes = await this.getGitRangeChanges(root, base, head);
|
|
1172
|
+
|
|
1173
|
+
const selected = assertUniqueReviewPaths(changes.slice(0, limit));
|
|
1174
|
+
const patches = await mapWithConcurrency(selected, async change => {
|
|
1175
|
+
if (change.gitStatus === 'untracked') {
|
|
1176
|
+
const source = await this.fileService.diff(root, change.path);
|
|
1177
|
+
return untrackedPatch(change.path, source.modifiedContent);
|
|
1178
|
+
}
|
|
1179
|
+
try {
|
|
1180
|
+
const { stdout } = await this.fileService.execFile(this.fileService.gitPath, [
|
|
1181
|
+
'-C',
|
|
1182
|
+
root,
|
|
1183
|
+
'diff',
|
|
1184
|
+
'--find-renames',
|
|
1185
|
+
...gitWhitespaceArgs(ignoreWhitespace),
|
|
1186
|
+
...gitContextArgs(context),
|
|
1187
|
+
...gitRangeRevisionArgs(base, head),
|
|
1188
|
+
'--',
|
|
1189
|
+
...gitDiffPathArgs(change),
|
|
1190
|
+
], { cwd: root, timeout: this.fileService.diffTimeoutMs, maxBuffer: this.fileService.diffMaxBuffer });
|
|
1191
|
+
return stdout;
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
if (error?.code === 'ETIMEDOUT') throw new WorkspaceFileError('git diff timed out', 504);
|
|
1194
|
+
throw new WorkspaceFileError(error?.stderr || error?.message || 'git diff failed', 500);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
return {
|
|
1198
|
+
patch: patches.filter(Boolean).join('\n'),
|
|
1199
|
+
truncated: changes.length > limit,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
async getGitRangeNumstat(root, base, head, ignoreWhitespace = 'NONE', changes = []) {
|
|
1204
|
+
try {
|
|
1205
|
+
const { stdout } = await this.fileService.execFile(this.fileService.gitPath, [
|
|
1206
|
+
'-C',
|
|
1207
|
+
root,
|
|
1208
|
+
'diff',
|
|
1209
|
+
'--numstat',
|
|
1210
|
+
'-z',
|
|
1211
|
+
'--find-renames',
|
|
1212
|
+
...gitWhitespaceArgs(ignoreWhitespace),
|
|
1213
|
+
...gitRangeRevisionArgs(base, head),
|
|
1214
|
+
'--',
|
|
1215
|
+
...gitDiffPathspecArgs(changes),
|
|
1216
|
+
], { cwd: root, timeout: this.fileService.diffTimeoutMs, maxBuffer: this.fileService.diffMaxBuffer });
|
|
1217
|
+
return parseNumstat(stdout, changes);
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
if (error?.code === 'ETIMEDOUT') throw new WorkspaceFileError('git diff timed out', 504);
|
|
1220
|
+
throw new WorkspaceFileError(error?.stderr || error?.message || 'git diff failed', 500);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
async getGitRangeRawMetadata(root, base, head, changes = []) {
|
|
1225
|
+
try {
|
|
1226
|
+
const { stdout } = await this.fileService.execFile(this.fileService.gitPath, [
|
|
1227
|
+
'-C',
|
|
1228
|
+
root,
|
|
1229
|
+
'diff',
|
|
1230
|
+
'--raw',
|
|
1231
|
+
'-z',
|
|
1232
|
+
'--find-renames',
|
|
1233
|
+
...gitRangeRevisionArgs(base, head),
|
|
1234
|
+
'--',
|
|
1235
|
+
...gitDiffPathspecArgs(changes),
|
|
1236
|
+
], { cwd: root, timeout: this.fileService.diffTimeoutMs, maxBuffer: this.fileService.diffMaxBuffer });
|
|
1237
|
+
return parseRawDiffMetadata(stdout);
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
if (error?.code === 'ETIMEDOUT') throw new WorkspaceFileError('git diff timed out', 504);
|
|
1240
|
+
throw new WorkspaceFileError(error?.stderr || error?.message || 'git diff failed', 500);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
module.exports = {
|
|
1246
|
+
ReviewDiffService,
|
|
1247
|
+
filterWorkingCopyChangeItems,
|
|
1248
|
+
untrackedPatch,
|
|
1249
|
+
untrackedHunks,
|
|
1250
|
+
fileFromPatch,
|
|
1251
|
+
gitRangeReviewId,
|
|
1252
|
+
gitDiffPathspecArgs,
|
|
1253
|
+
gitWhitespaceArgs,
|
|
1254
|
+
gitContextArgs,
|
|
1255
|
+
metadataFile,
|
|
1256
|
+
normalizeDiffContext,
|
|
1257
|
+
normalizeIgnoreWhitespace,
|
|
1258
|
+
normalizeReviewLimit,
|
|
1259
|
+
normalizeModifiedWithinDays,
|
|
1260
|
+
normalizeWorkingCopyScope,
|
|
1261
|
+
patchDiffHeader,
|
|
1262
|
+
patchMetadata,
|
|
1263
|
+
parseComparisonBranches,
|
|
1264
|
+
parseComparisonCommits,
|
|
1265
|
+
parseNameStatus,
|
|
1266
|
+
parseNumstat,
|
|
1267
|
+
parseRawDiffMetadata,
|
|
1268
|
+
workingCopyPatchset,
|
|
1269
|
+
workingCopyReviewId,
|
|
1270
|
+
};
|