ryco-cli 0.1.30 → 0.1.31
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/dist/{AcpAdapter-DjP0IHpH.cjs → AcpAdapter-ByUp9-wk.cjs} +71 -51
- package/dist/{AcpAdapter-CKWzfB3Q.mjs → AcpAdapter-CY_ANTKu.mjs} +73 -53
- package/dist/{AcpNativeLogging-Do0LktDb.mjs → AcpNativeLogging-C7PC6imv.mjs} +30 -4
- package/dist/{AcpNativeLogging-5gTPK9Fq.cjs → AcpNativeLogging-OQeAya9d.cjs} +33 -1
- package/dist/{AcpRegistryDriver-BblCFfev.cjs → AcpRegistryDriver-CD9GN7rI.cjs} +3 -3
- package/dist/{AcpRegistryDriver-BzohT-PC.mjs → AcpRegistryDriver-DMZ5VBWJ.mjs} +4 -4
- package/dist/AzureDevOpsSourceControlProvider-5giVYoNi.mjs +2053 -0
- package/dist/AzureDevOpsSourceControlProvider-BKMXEUaN.cjs +2055 -0
- package/dist/{BitbucketSourceControlProvider-D5UVAc9z.mjs → BitbucketSourceControlProvider-BVUBsEUF.mjs} +63 -23
- package/dist/{BitbucketSourceControlProvider-Bvl_q1o2.cjs → BitbucketSourceControlProvider-BWnnrbch.cjs} +62 -22
- package/dist/{BunPTY-Dcl1Lp62.mjs → BunPTY-CMbDQ1Xb.mjs} +1 -1
- package/dist/{ClaudeDriver-DZkz0QPr.cjs → ClaudeDriver-DCrqouOR.cjs} +1373 -191
- package/dist/{ClaudeDriver-BuQ_azDw.mjs → ClaudeDriver-DVOss8cC.mjs} +1375 -193
- package/dist/{CodexDriver-6Ph2cjAW.cjs → CodexDriver-CBQPHJqU.cjs} +258 -28
- package/dist/{CodexDriver-Bi49RIL4.mjs → CodexDriver-DJoTky7Z.mjs} +261 -31
- package/dist/{CodexTextGeneration-C5TIspJA.mjs → CodexTextGeneration-BSgNgfZv.mjs} +6 -6
- package/dist/{CodexTextGeneration-sXPR9eld.cjs → CodexTextGeneration-C9MDQmZm.cjs} +7 -7
- package/dist/{CopilotDriver-Bcdpz6U_.mjs → CopilotDriver-CUkdPXcH.mjs} +78 -20
- package/dist/{CopilotDriver-D6P0xgRX.cjs → CopilotDriver-DbaMDH16.cjs} +76 -18
- package/dist/{CursorDriver-CRnDGwEv.mjs → CursorDriver-B-ZilXJB.mjs} +114 -97
- package/dist/{CursorDriver-DMJ7sGSx.cjs → CursorDriver-DKQqPbKH.cjs} +114 -97
- package/dist/{ForgejoSourceControlProvider-D4jGdaJX.cjs → ForgejoSourceControlProvider-CgpjFECj.cjs} +94 -60
- package/dist/{ForgejoSourceControlProvider-Dl_Fk3RY.mjs → ForgejoSourceControlProvider-DemMYUye.mjs} +95 -61
- package/dist/{GitHubSourceControlProvider-QMe-zymI.cjs → GitHubSourceControlProvider-C4un6hOK.cjs} +218 -119
- package/dist/{GitHubSourceControlProvider-DnwKf0ap.mjs → GitHubSourceControlProvider-CRSDYcjc.mjs} +206 -108
- package/dist/GitLabSourceControlProvider-D8W1ZLPh.cjs +2260 -0
- package/dist/GitLabSourceControlProvider-DpWkJ0M3.mjs +2258 -0
- package/dist/{GrokDriver-jy8JIvYY.cjs → GrokDriver-BQ6G_7ZZ.cjs} +6 -7
- package/dist/{GrokDriver-Bvq1jP3S.mjs → GrokDriver-Buvas9-q.mjs} +5 -6
- package/dist/{Metrics-CnmjpbWk.mjs → Metrics-C3vLS9rz.mjs} +123 -13
- package/dist/{Metrics-C0LSU8s7.cjs → Metrics-DjRPLE98.cjs} +170 -12
- package/dist/{NodePTY-D7tFWuQe.mjs → NodePTY-DgnuBruq.mjs} +1 -1
- package/dist/{OpenCodeDriver-DhyVaBMT.mjs → OpenCodeDriver-BWEZD841.mjs} +183 -49
- package/dist/{OpenCodeDriver-Bu5D19tl.cjs → OpenCodeDriver-qUiX0RLH.cjs} +183 -49
- package/dist/{SideQuestionIsolation-B-aE6aBz.mjs → SideQuestionIsolation-B1T-DsR0.mjs} +1 -1
- package/dist/{SideQuestionIsolation-CtJ4wX83.cjs → SideQuestionIsolation-BPC-Bxsz.cjs} +1 -1
- package/dist/bin.cjs +37021 -10253
- package/dist/bin.mjs +36892 -10178
- package/dist/client/assets/AccountSettings-B88Q_w9Y.js +1 -0
- package/dist/client/assets/AiFocusSettings-Bb0TrCPn.js +1 -0
- package/dist/client/assets/{AnimatedHeight-UXn4-nUd.js → AnimatedHeight-epXOsgdm.js} +1 -1
- package/dist/client/assets/AppearanceSettings-XS9WRVPU.js +1 -0
- package/dist/client/assets/AutomationsDialog-bZckh4Y3.js +1 -0
- package/dist/client/assets/AutomationsSummary-wPFZCF49.js +1 -0
- package/dist/client/assets/BranchToolbar.logic-DjzLKbbB.js +3 -0
- package/dist/client/assets/BrowserPanel-CdBLnnfz.js +1 -0
- package/dist/client/assets/ChatRightPanel-BCINd0vP.js +34 -0
- package/dist/client/assets/ChatThreadRouteView-BFZ0Si-l.js +1 -0
- package/dist/client/assets/ColorSwatchPicker-CGyo99o0.js +1 -0
- package/dist/client/assets/CommandPalette.logic-BIOMtnEQ.js +1 -0
- package/dist/client/assets/CommandPaletteDialog-Db4LH--z.js +2 -0
- package/dist/client/assets/CompositeRoot-DtnwMJKj.js +1 -0
- package/dist/client/assets/ComputerUseSettings-CkqCqDsU.js +1 -0
- package/dist/client/assets/ConnectionsSettings-B9mxExdG.js +7 -0
- package/dist/client/assets/CreatePullRequestDialog-CJ7pNCue.js +1 -0
- package/dist/client/assets/CreatePullRequestDialogHost-n4ZjdSr4.js +1 -0
- package/dist/client/assets/DiagnosticsSettings-f9rFG9re.js +2 -0
- package/dist/client/assets/DiagnosticsSettings.logic-Byf-Bu4S.js +1 -0
- package/dist/client/assets/DialogHandle-C0c_rK3I.js +1 -0
- package/dist/client/assets/{DialogPopup-NfwDyR5c.js → DialogPopup-BKW1PEEa.js} +1 -1
- package/dist/client/assets/DialogRoot-BIubqrv0.js +1 -0
- package/dist/client/assets/DialogTitle-BAX_FLoG.js +1 -0
- package/dist/client/assets/{DialogViewport-B6w_ROOE.js → DialogViewport-C2iOUx0O.js} +1 -1
- package/dist/client/assets/DiffPanel-BkqDt_SS.js +25 -0
- package/dist/client/assets/DisclosureRegion-B8556yU0.js +1 -0
- package/dist/client/assets/DraftChatThreadRouteView-BKgRKJ2w.js +1 -0
- package/dist/client/assets/{ExternalIdentityWeb.logic-BlKX8J9h.js → ExternalIdentityWeb.logic-Cu6suNrN.js} +1 -1
- package/dist/client/assets/GlassSurface-DLYYzRdP.js +1 -0
- package/dist/client/assets/HostedConnectionControls-CoVqtnrK.js +1 -0
- package/dist/client/assets/HostedHubRoot-CM2CwPUr.js +2 -0
- package/dist/client/assets/{HostedNativeAuthorizationRoute-j7oSlakZ.js → HostedNativeAuthorizationRoute-Bizmqrh_.js} +2 -2
- package/dist/client/assets/HostedNodeRenameDialog-SXWMsTO0.js +1 -0
- package/dist/client/assets/HostedRelayTrustNotice-BfTyrBi5.js +1 -0
- package/dist/client/assets/InboxStatusGlyph-BPTqzp7l.js +1 -0
- package/dist/client/assets/IntegrationsSettingsPanel-DAPvkV4i.js +1 -0
- package/dist/client/assets/KeybindingsSettings-B8g-YI2O.js +1 -0
- package/dist/client/assets/LazyBrowserPanel-Ds9cdOC_.js +2 -0
- package/dist/client/assets/McpServersSettings-Dw6G3ff3.js +6 -0
- package/dist/client/assets/MobileSheet-DfgXNWbd.js +1 -0
- package/dist/client/assets/ModelTuningDial-DQBAbuNL.js +1 -0
- package/dist/client/assets/NewWorktreeDialog-CDZAJmK6.js +4 -0
- package/dist/client/assets/NodeSecuritySettings-3MELA9ru.js +1 -0
- package/dist/client/assets/NullStore-gT_6_xZE.js +1 -0
- package/dist/client/assets/OnboardingDialog-CAAtNnJf.js +1 -0
- package/dist/client/assets/OpinionatedPluginsSettings-AiDB0Vl4.js +1 -0
- package/dist/client/assets/{PdfAttachmentPreview-C8-hT8KQ.js → PdfAttachmentPreview-BFNW2wjN.js} +2 -2
- package/dist/client/assets/{PhoneGetAppInterstitial-Zh0zpBgT.js → PhoneGetAppInterstitial-D8fwZ8E9.js} +1 -1
- package/dist/client/assets/PhoneHome-YHgHljA0.js +1 -0
- package/dist/client/assets/PhoneSettingsSurface-BVxJppvK.js +2 -0
- package/dist/client/assets/PreviewPanel-BYcEnkv9.js +643 -0
- package/dist/client/assets/ProjectScriptDialog-K34K2Kzm.js +1 -0
- package/dist/client/assets/ProjectsPage-DXnMYLjb.js +1 -0
- package/dist/client/assets/PromoteChatDialog-CAY7_wJ-.js +1 -0
- package/dist/client/assets/ProviderLimitWindow-CK04BSkM.js +1 -0
- package/dist/client/assets/ProviderUpdateLaunchNotification.logic-BMBrDDOC.js +1 -0
- package/dist/client/assets/ProvidersSettingsPanel-C85PX9Es.js +1 -0
- package/dist/client/assets/PullRequestsPage-C0rgfGwB.js +1 -0
- package/dist/client/assets/{QuitShortcutFeedback-Qs9XsuZ4.js → QuitShortcutFeedback-DSkKqETs.js} +1 -1
- package/dist/client/assets/RedactedSensitiveText-DQ0YveQd.js +1 -0
- package/dist/client/assets/RollingText-BzvpP5fa.js +1 -0
- package/dist/client/assets/RootAppShell-BYQ-eGNT.js +6 -0
- package/dist/client/assets/{Separator-B6Esv57Y.js → Separator-DpiW19Vw.js} +1 -1
- package/dist/client/assets/SettingsPage-BCFN7Ju2.js +2 -0
- package/dist/client/assets/SettingsPanels-CP6z2b1x.js +3 -0
- package/dist/client/assets/SettingsPanels.editor-D8U-TWWG.js +1 -0
- package/dist/client/assets/SettingsSelect-DEhNaz3E.js +1 -0
- package/dist/client/assets/Sidebar.logic-D_ODfSpl.js +1 -0
- package/dist/client/assets/SimulatorPanel-DCT6fZ0X.js +1 -0
- package/dist/client/assets/SourceControlSettings-DxUtvPMK.js +1 -0
- package/dist/client/assets/StatisticsPage-C9SrwtM4.js +1 -0
- package/dist/client/assets/StatisticsPanel-BMYLETs0.js +1 -0
- package/dist/client/assets/ThreadLinkList-BN5z5EpS.js +169 -0
- package/dist/client/assets/ThreadTerminalDrawer-CgIFOlR-.js +18 -0
- package/dist/client/assets/ThreadWorkspacePanel-DvXoqQPF.js +3 -0
- package/dist/client/assets/ToolbarRootContext-8FsV9Cpj.js +1 -0
- package/dist/client/assets/TraitsPicker-DcrpJahy.js +1 -0
- package/dist/client/assets/Virtualizer-CwXeUPla.js +1345 -0
- package/dist/client/assets/WorkspacePullRequestPanel-3mlcJn9G.js +1 -0
- package/dist/client/assets/WorkspaceReview-UGbCgXeK.js +2 -0
- package/dist/client/assets/{abnfDiagram-N423BO3Z-ByEe3mC0.js → abnfDiagram-N423BO3Z-DVEM6Scc.js} +1 -1
- package/dist/client/assets/{activity-CwTUF6lv.js → activity-DVCuAejY.js} +1 -1
- package/dist/client/assets/{alert-DQEkrtUJ.js → alert-FV7ZIit_.js} +1 -1
- package/dist/client/assets/alert-dialog-CgXR9tED.js +1 -0
- package/dist/client/assets/app-window-3xtwqZgI.js +1 -0
- package/dist/client/assets/appChrome-COCz-T7w.js +1 -0
- package/dist/client/assets/{appearancePreferences-nFx4MHoI.js → appearancePreferences-NnsvXwBe.js} +1 -1
- package/dist/client/assets/architecture-7GRP2DOG-DXXb_0_g.js +1 -0
- package/dist/client/assets/{architectureDiagram-T3A2C74G-Bd-yxaTr.js → architectureDiagram-T3A2C74G-B6pcKiCj.js} +1 -1
- package/dist/client/assets/{arrow-left-DUOpzEbP.js → arrow-left-Blu3LSE2.js} +1 -1
- package/dist/client/assets/{arrow-right-HOYJnPKw.js → arrow-right-CO3CH0ne.js} +1 -1
- package/dist/client/assets/arrow-up-B69gV2Bq.js +1 -0
- package/dist/client/assets/{arrow-up-right-DHnMKuVA.js → arrow-up-right-BbPGFOLA.js} +1 -1
- package/dist/client/assets/authorization-Ob6YwWiI.js +1 -0
- package/dist/client/assets/autocomplete-RVpjsrDv.js +1 -0
- package/dist/client/assets/ban-BIm2PVkE.js +1 -0
- package/dist/client/assets/{blockDiagram-VBNYF7ZC-CPDBonMC.js → blockDiagram-VBNYF7ZC-WNerGp9V.js} +1 -1
- package/dist/client/assets/{bot-DxjPA7Bw.js → bot-C9nWtaWq.js} +1 -1
- package/dist/client/assets/browserState-jt47YsqD.js +1 -0
- package/dist/client/assets/{c4Diagram-5PPSVZJV-BM6pAiqT.js → c4Diagram-5PPSVZJV-wp1ioOoM.js} +1 -1
- package/dist/client/assets/camera-C13VoW3c.js +1 -0
- package/dist/client/assets/capabilities-CfOhq009.js +1 -0
- package/dist/client/assets/channel-56r2qL2v.js +1 -0
- package/dist/client/assets/{chart-column-Cg7d41tb.js → chart-column-DRv0YtL_.js} +1 -1
- package/dist/client/assets/checkbox-CIIIY9FA.js +1 -0
- package/dist/client/assets/checks-Dw022Bvy.js +1 -0
- package/dist/client/assets/{chunk-2GRJ4B5K-DVKozJ41.js → chunk-2GRJ4B5K-DRl3ILpc.js} +1 -1
- package/dist/client/assets/{chunk-4I5QYGJK-BRoMwPkW.js → chunk-4I5QYGJK-CYTRl4bo.js} +1 -1
- package/dist/client/assets/{chunk-5RXB4S5H-DFMKyoqP.js → chunk-5RXB4S5H-VCNwRsOa.js} +1 -1
- package/dist/client/assets/{chunk-6Q2QTUOP-Ba6UENe9.js → chunk-6Q2QTUOP-DnSeKGPG.js} +1 -1
- package/dist/client/assets/{chunk-7Z6QIM7H-B87gYLWH.js → chunk-7Z6QIM7H-Cjilh9TF.js} +1 -1
- package/dist/client/assets/{chunk-GF5L2VYU-DZ840GRQ.js → chunk-GF5L2VYU-CGR7AW0t.js} +1 -1
- package/dist/client/assets/{chunk-I66GZJ75-BO6p4Aqg.js → chunk-I66GZJ75-CXSn8dDu.js} +1 -1
- package/dist/client/assets/{chunk-JQJVKLGR-5dTfVinz.js → chunk-JQJVKLGR-Dro-75lU.js} +1 -1
- package/dist/client/assets/{chunk-KBJHAD2P-BeWfBZdP.js → chunk-KBJHAD2P-BDqN2kVf.js} +1 -1
- package/dist/client/assets/{chunk-NSK5VX7P-DuRkdn5k.js → chunk-NSK5VX7P-DGo5UczH.js} +1 -1
- package/dist/client/assets/{chunk-QR6OTTB3-Duz4Y_uE.js → chunk-QR6OTTB3-RWuyWnBI.js} +1 -1
- package/dist/client/assets/{chunk-UBXNYLIW-D-z8URgN.js → chunk-UBXNYLIW-BPRzeB0w.js} +1 -1
- package/dist/client/assets/{chunk-W5SLKNZC-AGjFjjoJ.js → chunk-W5SLKNZC-DYWocTFi.js} +1 -1
- package/dist/client/assets/{chunk-WRU74C26-BgY3UiXB.js → chunk-WRU74C26-ChbB1jY3.js} +1 -1
- package/dist/client/assets/circle-stop-D8B4hRpY.js +1 -0
- package/dist/client/assets/circle-x-CNLfrTqR.js +1 -0
- package/dist/client/assets/classDiagram-JCYQIIEL-DGvmFev3.js +1 -0
- package/dist/client/assets/classDiagram-v2-OCEON4UE-DGvmFev3.js +1 -0
- package/dist/client/assets/{clipboard-copy-BWf3JuTE.js → clipboard-copy-DGi8FKNO.js} +1 -1
- package/dist/client/assets/clock-3-DmJ4_XPI.js +1 -0
- package/dist/client/assets/clock-DZIuPtNG.js +1 -0
- package/dist/client/assets/color-picker-BdBXje-a.js +1 -0
- package/dist/client/assets/columns-2-CWlMOClK.js +1 -0
- package/dist/client/assets/combobox-Dtjtf7AB.js +1 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-BuOAGqlK.js → cynefin-OW5HDTMX-TuShvMuM.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-MW4NZA55-DqV0Aokp.js → cynefinDiagram-MW4NZA55-B0Edt2Em.js} +1 -1
- package/dist/client/assets/{dagre-VZM6K2ZE-D_GFquDW.js → dagre-VZM6K2ZE-D-eiuqHx.js} +1 -1
- package/dist/client/assets/{data-list-BJCq3CVP.js → data-list-BAx6jBY1.js} +1 -1
- package/dist/client/assets/desktopUpdateAtoms-BjrLa3tl.js +1 -0
- package/dist/client/assets/desktopWorkspace-C2wXVBhc.js +1 -0
- package/dist/client/assets/{deviceName-C2N8ZRX3.js → deviceName-B4youG0v.js} +1 -1
- package/dist/client/assets/{diagram-7IWD3JNH-B1wlsNg2.js → diagram-7IWD3JNH-DgM6lMlR.js} +1 -1
- package/dist/client/assets/{diagram-B4RE2ZJO-D-CYoym7.js → diagram-B4RE2ZJO-CyHNDjzV.js} +1 -1
- package/dist/client/assets/{diagram-LBJQPF4R-DqJ9NCqt.js → diagram-LBJQPF4R-DDzuLIN5.js} +1 -1
- package/dist/client/assets/{diagram-Q27KOJAE-3yp1D2u4.js → diagram-Q27KOJAE-BaBvK9QB.js} +1 -1
- package/dist/client/assets/{diagram-UB23O5K3-Byp25nGC.js → diagram-UB23O5K3-CzZ2ewuZ.js} +1 -1
- package/dist/client/assets/dialogWords-Cg0YkJVg.js +1 -0
- package/dist/client/assets/diffRouteSearch-D2MKN_ah.js +1 -0
- package/dist/client/assets/disclosureMotion-cbMJn5-z.js +1 -0
- package/dist/client/assets/{download-DsYNYJeu.js → download-TvtXS4hE.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-BXEA7PRR-B_IM1l6o.js → ebnfDiagram-BXEA7PRR-CL1vdOKi.js} +1 -1
- package/dist/client/assets/editorPreferences-DqKyA-2Z.js +1 -0
- package/dist/client/assets/ellipsis-CDK1dNFZ.js +1 -0
- package/dist/client/assets/ellipsis-vertical-CwzEUTwV.js +1 -0
- package/dist/client/assets/{empty-BbJHvskr.js → empty-BovKoxbn.js} +1 -1
- package/dist/client/assets/{erDiagram-JOGREHBK--OtcrCRJ.js → erDiagram-JOGREHBK-DmQR2if6.js} +1 -1
- package/dist/client/assets/eventmodeling-NTZA5JFV-D272GIcY.js +1 -0
- package/dist/client/assets/external-link-CJeVJZ7s.js +1 -0
- package/dist/client/assets/eye-BUmrXqzg.js +1 -0
- package/dist/client/assets/file-diff-BWc3mYsz.js +1 -0
- package/dist/client/assets/flowDiagram-UKHOOZJN-DLyS38Mm.js +1 -0
- package/dist/client/assets/folder-git-2-CPxg5wKP.js +1 -0
- package/dist/client/assets/folder-open-DcMymvp1.js +1 -0
- package/dist/client/assets/formatCount-B4KLI651.js +1 -0
- package/dist/client/assets/{ganttDiagram-PKOTCBZU-w0seBi3Q.js → ganttDiagram-PKOTCBZU-1q2nUL92.js} +1 -1
- package/dist/client/assets/git-pull-request-BFPwpm3c.js +1 -0
- package/dist/client/assets/{gitGraph-4MIJSDKK-BOJ0lcSW.js → gitGraph-4MIJSDKK-COtwmv2S.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-DS77QQ5N-BMfOCCYn.js → gitGraphDiagram-DS77QQ5N-ChsvaxX4.js} +1 -1
- package/dist/client/assets/{group-tVMKGp7Q.js → group-DWWn0dTx.js} +1 -1
- package/dist/client/assets/hammer-BXo-EU88.js +1 -0
- package/dist/client/assets/hostedConnectionCoordinator-BkAE-RhA.js +1 -0
- package/dist/client/assets/{index-BqZvnGNr.js → index-DARIYokI.js} +2 -2
- package/dist/client/assets/{info-A6RAGUB7-acjBTQ5o.js → info-A6RAGUB7-CI2qZ9wW.js} +1 -1
- package/dist/client/assets/{infoDiagram-6WML65LV-DB01Nc2m.js → infoDiagram-6WML65LV-BPmgcTPH.js} +1 -1
- package/dist/client/assets/{input-BDWSVClC.js → input-CCie4mm1.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-WSZJBQD7-KhZTvm-J.js → ishikawaDiagram-WSZJBQD7-LyS9p3Yg.js} +1 -1
- package/dist/client/assets/{journeyDiagram-NVQOT4AX-C3JCYk-2.js → journeyDiagram-NVQOT4AX-BYaAmN65.js} +1 -1
- package/dist/client/assets/{kanban-definition-27J2QSJJ-Dbo0C1s6.js → kanban-definition-27J2QSJJ-D-pwzqFt.js} +1 -1
- package/dist/client/assets/{key-round-CETZqe3h.js → key-round-BlrUTfxB.js} +1 -1
- package/dist/client/assets/keyboard-DlBdq6sz.js +1 -0
- package/dist/client/assets/label-ChtVI9lp.js +1 -0
- package/dist/client/assets/layers-BB-ruGvd.js +1 -0
- package/dist/client/assets/lifecycleSuggestionPolicy-0ERe_O53.js +1 -0
- package/dist/client/assets/list-checks-BkM_ZRsj.js +1 -0
- package/dist/client/assets/loader-3vQnfqIT.js +1 -0
- package/dist/client/assets/{log-out-D5-aSBAz.js → log-out-CKq9YiLY.js} +1 -1
- package/dist/client/assets/{logging-B_qXVrk6.js → logging-CCg76rgn.js} +1 -1
- package/dist/client/assets/main-Ch0bwS-M.js +9 -0
- package/dist/client/assets/main-Di0isH9D.css +2 -0
- package/dist/client/assets/{mermaid-parser.core-B6Q-3x_q.js → mermaid-parser.core-RK9ah5RM.js} +2 -2
- package/dist/client/assets/{mermaidEngine-DksLcesB.js → mermaidEngine-Dr8UsPhH.js} +3 -3
- package/dist/client/assets/message-circle-dashed-BrjwqS7-.js +1 -0
- package/dist/client/assets/messageQueueDrain-B7R7Qmq6.js +1 -0
- package/dist/client/assets/messages-square-cDTkDJv_.js +1 -0
- package/dist/client/assets/{mindmap-definition-FAOFIHXS-LaMzRxqw.js → mindmap-definition-FAOFIHXS-DVudWasx.js} +1 -1
- package/dist/client/assets/{packet-AYTQ26CC-BHhg59Si.js → packet-AYTQ26CC-DnkTsVhD.js} +1 -1
- package/dist/client/assets/panels-top-left-B7jZiLth.js +1 -0
- package/dist/client/assets/path-text-3Rfu6lt8.js +1 -0
- package/dist/client/assets/{pegDiagram-VL7TDLO6-s7l7UD9d.js → pegDiagram-VL7TDLO6-Do5PIp8B.js} +1 -1
- package/dist/client/assets/pencil-Bzg-nc6d.js +1 -0
- package/dist/client/assets/{pie-WAS4IAKB-DygX31Z9.js → pie-WAS4IAKB-B_nNQOzn.js} +1 -1
- package/dist/client/assets/{pieDiagram-7S7Q4E2Y-bb6I9TtZ.js → pieDiagram-7S7Q4E2Y-D_IG4xwA.js} +1 -1
- package/dist/client/assets/{play-DUvOxTpL.js → play-DpXXQ1qB.js} +1 -1
- package/dist/client/assets/popover-OWEv6cQz.js +48 -0
- package/dist/client/assets/presentationTier-DPVAxTK4.js +1 -0
- package/dist/client/assets/previewRouteSearch-BjZdTuSf.js +1 -0
- package/dist/client/assets/primary-Cf-xJExj.js +4 -0
- package/dist/client/assets/primitives-CxTBN7D4.js +1 -0
- package/dist/client/assets/projectCheckouts.logic-DUT4AxXr.js +1 -0
- package/dist/client/assets/projectPaths-B8aVmOZX.js +1 -0
- package/dist/client/assets/projectPreferences-CzWBItFj.js +1 -0
- package/dist/client/assets/providerCapabilities-B2YOPsLN.js +1 -0
- package/dist/client/assets/providerDriverMeta-BHHL9uw0.js +1 -0
- package/dist/client/assets/providerModels-v9PYbzRE.js +1 -0
- package/dist/client/assets/providerStatus-CDPaqb0S.js +1 -0
- package/dist/client/assets/pullRequestsModel.logic-Dv_om_C4.js +1 -0
- package/dist/client/assets/{qr-code-dbkTo07n.js → qr-code-BlbiBIwH.js} +1 -1
- package/dist/client/assets/{qr-code-D4CtBq2F.js → qr-code-DywzVgQC.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-CIZ2JOQS-Cj1Y68ZF.js → quadrantDiagram-CIZ2JOQS-DY6fJyrM.js} +1 -1
- package/dist/client/assets/{radar-RG4KPBEZ-FHM3ftPp.js → radar-RG4KPBEZ-D9689I5m.js} +1 -1
- package/dist/client/assets/radio-group-DVwD6Cuy.js +1 -0
- package/dist/client/assets/{railroad-74A4TZTK-CPMn18Cm.js → railroad-74A4TZTK-DVzWbQnl.js} +1 -1
- package/dist/client/assets/railroad-abnf-HS5TGJTU-C_2P6umA.js +1 -0
- package/dist/client/assets/railroad-ebnf-LZEXJU2U-aGlR3R3z.js +1 -0
- package/dist/client/assets/railroad-peg-WCYAUIDC-DCB9oE8y.js +1 -0
- package/dist/client/assets/{railroadDiagram-AXF67PYL-DyV3SuDj.js → railroadDiagram-AXF67PYL-D1oJ6Jtl.js} +1 -1
- package/dist/client/assets/readCache-B7cDPz2O.js +1 -0
- package/dist/client/assets/{refresh-cw-CgDa2Ok_.js → refresh-cw-CRUTivd6.js} +1 -1
- package/dist/client/assets/{requirementDiagram-LRYGKXZP-_xEAGWaH.js → requirementDiagram-LRYGKXZP-7RM8iREp.js} +1 -1
- package/dist/client/assets/rotate-ccw-BE2ZCwL6.js +1 -0
- package/dist/client/assets/roving-radio-group-NMPWrDqe.js +1 -0
- package/dist/client/assets/rows-2-DDS4dGTX.js +1 -0
- package/dist/client/assets/runtime-DjjjAcTE.js +18 -0
- package/dist/client/assets/safePolygon-C1AzMRS4.js +1 -0
- package/dist/client/assets/{sankeyDiagram-W5VNT64P-Clf6oTLi.js → sankeyDiagram-W5VNT64P-C1tKAPGN.js} +1 -1
- package/dist/client/assets/{save-C8h0AMdl.js → save-OZdkPs_y.js} +1 -1
- package/dist/client/assets/scroll-area-80P7vbjw.js +1 -0
- package/dist/client/assets/{scrollEdges-BXYnwntN.js → scrollEdges-C0Oc-5qi.js} +1 -1
- package/dist/client/assets/select-Cb9FUPHD.js +1 -0
- package/dist/client/assets/separator-CO8bC7Wc.js +1 -0
- package/dist/client/assets/{sequenceDiagram-SI44F4Z6-CcYtQLSe.js → sequenceDiagram-SI44F4Z6-DGVpb7jL.js} +1 -1
- package/dist/client/assets/settings-2-PrRDu6Lc.js +1 -0
- package/dist/client/assets/settingsLayout-CRFNCk_D.js +1 -0
- package/dist/client/assets/sheet-C5DW7qVO.js +1 -0
- package/dist/client/assets/{shield-DPC4oHTi.js → shield-CBHYqlkR.js} +1 -1
- package/dist/client/assets/{shield-alert-CwH36qZb.js → shield-alert-Dc2cGwyR.js} +1 -1
- package/dist/client/assets/{shield-check-CGD7sEeF.js → shield-check-_wl7R4Js.js} +1 -1
- package/dist/client/assets/sidebar-0BBaFNm3.js +1 -0
- package/dist/client/assets/sidebarProjectFolders-BjWzbt8C.js +3 -0
- package/dist/client/assets/sidebarProjectGrouping-nQRStWrZ.js +1 -0
- package/dist/client/assets/sidebarStatusText-BBKuNies.js +1 -0
- package/dist/client/assets/sidebarUndo-Dfq1lvyF.js +1 -0
- package/dist/client/assets/{skeleton-rS3IgBob.js → skeleton-DfUe6VV_.js} +1 -1
- package/dist/client/assets/sliders-horizontal-BGXkwvKv.js +1 -0
- package/dist/client/assets/sliding-tabs-BnDSjRU3.js +39 -0
- package/dist/client/assets/{smartphone-DgVJfbv3.js → smartphone-OEAQOTdv.js} +1 -1
- package/dist/client/assets/spinner-lHRJWsym.js +1 -0
- package/dist/client/assets/square-B05NKv3e.js +1 -0
- package/dist/client/assets/{star-Cqay-Cae.js → star-C8mHmfZu.js} +1 -1
- package/dist/client/assets/{stateDiagram-OKZ733FA-DEKURpRP.js → stateDiagram-OKZ733FA-CCOAegCG.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-UEYNNEHI-D60qqBed.js +1 -0
- package/dist/client/assets/storeSelectors-BichCKDC.js +1 -0
- package/dist/client/assets/surfaceMorph-BTRijmlU.js +1 -0
- package/dist/client/assets/{swimlanes-SLNWSIFB-DrR6sbi6.js → swimlanes-SLNWSIFB-CFsLYT4h.js} +1 -1
- package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-D0SaJgSn.js +8 -0
- package/dist/client/assets/switch-oQc7Ifbl.js +1 -0
- package/dist/client/assets/text-wrap-DSRdSfDm.js +1 -0
- package/dist/client/assets/textarea-VyiIZ0wQ.js +1 -0
- package/dist/client/assets/threadPriorityRefreshRuntime-H656C1J_.js +1 -0
- package/dist/client/assets/ticker-CPutfhZ4.js +1 -0
- package/dist/client/assets/{timeline-definition-Z64GVDOM-bQJvmJx5.js → timeline-definition-Z64GVDOM-B4agmO7R.js} +1 -1
- package/dist/client/assets/toggle-DFqtCPt-.js +1 -0
- package/dist/client/assets/toggle-group-C5ICmqkI.js +1 -0
- package/dist/client/assets/{tokenModePresentation-BkS-mry_.js → tokenModePresentation-umDRm4k1.js} +1 -1
- package/dist/client/assets/tooltip-DbqhseXd.js +1 -0
- package/dist/client/assets/{treeView-Q6P3EWNA-D4Ex5QTp.js → treeView-Q6P3EWNA-C_S3udFB.js} +1 -1
- package/dist/client/assets/{treemap-WGGIJYW6-C4B6Y6qM.js → treemap-WGGIJYW6-B-kUSAkE.js} +1 -1
- package/dist/client/assets/undo-2-lyluTGMW.js +1 -0
- package/dist/client/assets/{unplug-B1dczi-t.js → unplug-D6iu-qlh.js} +1 -1
- package/dist/client/assets/usage-ClF-eUmy.js +1 -0
- package/dist/client/assets/useActiveIndicator-R5NWu8Fg.js +1 -0
- package/dist/client/assets/useAppSidebarCollapsed-BbMxfio5.js +1 -0
- package/dist/client/assets/useAppearancePreference-Ix13pMKy.js +1 -0
- package/dist/client/assets/useAtlassian-BYDCc9lv.js +0 -0
- package/dist/client/assets/useCheckoutResolution-CR-oOp5-.js +1 -0
- package/dist/client/assets/useDesktopRelaunchGuard-Jmh615iZ.js +1 -0
- package/dist/client/assets/useDiffFileNavigation-DeuzdD5E.js +15 -0
- package/dist/client/assets/useDiffLayout-DhcXwhBL.js +1 -0
- package/dist/client/assets/useEnvironmentSettingsTarget-DM6BRDT7.js +1 -0
- package/dist/client/assets/useFocus-CV0KIZwg.js +1 -0
- package/dist/client/assets/useGit-CUcs7h5_.js +1 -0
- package/dist/client/assets/useGitBranchSearch-CeKFtnCL.js +1 -0
- package/dist/client/assets/useHostedDraftExecutionTarget-DvQU5UOq.js +1 -0
- package/dist/client/assets/useLabelableId-CEgCRi6q.js +1 -0
- package/dist/client/assets/useLandingFlash-BfOOlFtW.js +1 -0
- package/dist/client/assets/useLogicalProjectSnapshots-CRNZke74.js +1 -0
- package/dist/client/assets/useNavigate-sQZthdyT.js +1 -0
- package/dist/client/assets/usePageLeadingInsetClass-Bf4nUvx4.js +1 -0
- package/dist/client/assets/usePhoneThreadActions-VaQ-mZex.js +1 -0
- package/dist/client/assets/usePopupHandleStore-DimKo2Yy.js +1 -0
- package/dist/client/assets/usePopupViewport-YeqX_UM3.js +1 -0
- package/dist/client/assets/usePresentationTier-B9kCQ4YH.js +1 -0
- package/dist/client/assets/useProject-Kt74Z12J.js +1 -0
- package/dist/client/assets/useProviderOptionsUpdater-CTzNh2B7.js +2 -0
- package/dist/client/assets/usePullRequestFilesViewed-D5Snmzys.js +1 -0
- package/dist/client/assets/usePullRequestsModel-DJL5HHbP.js +40 -0
- package/dist/client/assets/useScrollLock-CLZ31xsK.js +1 -0
- package/dist/client/assets/useSidebarThreadActions-n6EkLATf.js +6 -0
- package/dist/client/assets/{useStatistics-B76_r68I.js → useStatistics-DD0mYS4J.js} +14 -14
- package/dist/client/assets/{useTheme-B441xg4F.js → useTheme-Bdy-BDs-.js} +1 -1
- package/dist/client/assets/useValueChanged-DoumqMnV.js +1 -0
- package/dist/client/assets/{user-round-CLy1Qy1Y.js → user-round-BPDXr8Gq.js} +1 -1
- package/dist/client/assets/{vennDiagram-T6HMQDX7-8iiroXim.js → vennDiagram-T6HMQDX7-B1UEn_j_.js} +1 -1
- package/dist/client/assets/{vscode-icons-CJBVQtq5.js → vscode-icons-Ccy-gzFN.js} +1 -1
- package/dist/client/assets/{wardley-WFR3VGLG-DpyNUbTc.js → wardley-WFR3VGLG-gPdyDc-7.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-T6FBY63Y-CI7is3QO.js → wardleyDiagram-T6FBY63Y-2aDcwOA4.js} +1 -1
- package/dist/client/assets/workspaceLifecycle-OJ1aKksw.js +3 -0
- package/dist/client/assets/workspaceMetadataProjection-CyEyJhUa.js +1 -0
- package/dist/client/assets/workspaceRouteSearch-CCavUdbH.js +1 -0
- package/dist/client/assets/{wrench-7pbbxqrZ.js → wrench-CPUCA_rn.js} +1 -1
- package/dist/client/assets/wsTransport-C88seBPA.js +15 -0
- package/dist/client/assets/{xychartDiagram-ELKLHX3M-BTEX3m2y.js → xychartDiagram-ELKLHX3M-C1_7Q-1i.js} +1 -1
- package/dist/client/favicon.svg +1 -1
- package/dist/client/index.html +1 -1
- package/dist/collectUint8StreamText-BsLSgI8i.mjs +710 -0
- package/dist/collectUint8StreamText-D5olwfpM.cjs +757 -0
- package/dist/{deviceHostWorker-L8C9wy38.mjs → deviceHostWorker-B-Dn-6An.mjs} +1 -1
- package/dist/{makeManagedServerProvider-CF776SAO.cjs → makeManagedServerProvider-B5tP7tvg.cjs} +1 -1
- package/dist/{makeManagedServerProvider-B90UKjnv.mjs → makeManagedServerProvider-B65ipCxM.mjs} +2 -2
- package/dist/resource-monitor/linux-arm64/ryco-resource-monitor +0 -0
- package/dist/resource-monitor/ryco-resource-monitor +0 -0
- package/dist/resource-monitor/ryco-resource-monitor.exe +0 -0
- package/dist/resource-monitor/win32-arm64/ryco-resource-monitor.exe +0 -0
- package/dist/resource-monitor/win32-x64/ryco-resource-monitor.exe +0 -0
- package/dist/{runtimeSession-V3RIy8Ao.mjs → runtimeSession-D_BZ-wRH.mjs} +1 -1
- package/dist/{src-CARHuYkT.cjs → src-FmeCeBJ9.cjs} +2291 -72
- package/dist/{src-CtsgPPhb.mjs → src-VTj60bUm.mjs} +1974 -61
- package/package.json +1 -1
- package/dist/AzureDevOpsSourceControlProvider-CKSvfuDS.cjs +0 -190
- package/dist/AzureDevOpsSourceControlProvider-DRWSXTml.mjs +0 -188
- package/dist/GitLabSourceControlProvider-Cj07OTBD.mjs +0 -186
- package/dist/GitLabSourceControlProvider-CvvN5pHt.cjs +0 -188
- package/dist/client/assets/AccountSettings-v6-r6gYl.js +0 -1
- package/dist/client/assets/AiFocusSettings-BVdLON0q.js +0 -1
- package/dist/client/assets/AppearanceSettings-DRChsQnN.js +0 -1
- package/dist/client/assets/BranchToolbar.logic-YN3KjVr9.js +0 -3
- package/dist/client/assets/BrowserPanel-Dxw3stFP.js +0 -1
- package/dist/client/assets/ChatRightPanel-CygpcJiF.js +0 -201
- package/dist/client/assets/ChatThreadRouteView-DYLZFHu0.js +0 -1
- package/dist/client/assets/CommandPaletteDialog-BRvBzP1R.js +0 -1
- package/dist/client/assets/CompositeRoot-BeEGGWwn.js +0 -1
- package/dist/client/assets/ComputerUseSettings-Cq8Pi0ZU.js +0 -1
- package/dist/client/assets/ConnectionsSettings-RSYtexxd.js +0 -7
- package/dist/client/assets/ContextHandoffEndpointLabel-CeGDLNA4.js +0 -4
- package/dist/client/assets/DiagnosticsSettings-CDa6MVx4.js +0 -2
- package/dist/client/assets/DialogHandle-BE7egDQQ.js +0 -1
- package/dist/client/assets/DialogRoot-C4mtzigX.js +0 -1
- package/dist/client/assets/DialogTitle-B_Q4DykI.js +0 -1
- package/dist/client/assets/DialogTrigger-BSqd4Crj.js +0 -1
- package/dist/client/assets/DraftChatThreadRouteView-JW_bXVrM.js +0 -1
- package/dist/client/assets/GlassSurface-DDG4eTlj.js +0 -1
- package/dist/client/assets/HostedConnectionControls-CscV6hm1.js +0 -1
- package/dist/client/assets/HostedHubRoot-gp7-ClBP.js +0 -2
- package/dist/client/assets/HostedNodeRenameDialog-D1DSiTpx.js +0 -1
- package/dist/client/assets/HostedRelayTrustNotice-B9jp4SRh.js +0 -1
- package/dist/client/assets/IntegrationsSettingsPanel-DH7SfWtV.js +0 -2
- package/dist/client/assets/KeybindingsSettings-DkgYYSHS.js +0 -7
- package/dist/client/assets/LazyBrowserPanel-D4e76vc_.js +0 -2
- package/dist/client/assets/McpServersSettings-Dc1kPa-X.js +0 -6
- package/dist/client/assets/MobileSheet-lT--mLEz.js +0 -1
- package/dist/client/assets/NodeSecuritySettings-DcfSx4KT.js +0 -1
- package/dist/client/assets/NullStore-8d2s-Re3.js +0 -1
- package/dist/client/assets/OnboardingDialog-CnLjwM6Z.js +0 -1
- package/dist/client/assets/OpinionatedPluginsSettings-BTJyS3rN.js +0 -1
- package/dist/client/assets/PhoneHome-CIMkuDJF.js +0 -1
- package/dist/client/assets/PhoneSettingsSurface-CfZQxJNI.js +0 -2
- package/dist/client/assets/ProvidersSettingsPanel-DAVSiwE1.js +0 -1
- package/dist/client/assets/RedactedSensitiveText-BHGbQJwB.js +0 -1
- package/dist/client/assets/RootAppShell-T056pByH.js +0 -7
- package/dist/client/assets/SettingsDialog-BERU2Qg7.js +0 -2
- package/dist/client/assets/SettingsPanels-De_XZOQF.js +0 -3
- package/dist/client/assets/SettingsPanels.editor-Dkva9gX_.js +0 -2
- package/dist/client/assets/SourceControlSettings-B4HS17cB.js +0 -1
- package/dist/client/assets/StatisticsPage-Bj2ftFdH.js +0 -1
- package/dist/client/assets/StatisticsPanel-Dw7GBBmd.js +0 -1
- package/dist/client/assets/StatisticsSettingsLink-_XL3ABoI.js +0 -1
- package/dist/client/assets/ThreadTerminalDrawer-CPPzQSXN.js +0 -18
- package/dist/client/assets/ThreadWorkspacePanel-DWKU7Omm.js +0 -2073
- package/dist/client/assets/appChrome-TkWLAGxk.js +0 -1
- package/dist/client/assets/architecture-7GRP2DOG-HS59zfrk.js +0 -1
- package/dist/client/assets/authorization-D8dOcEYq.js +0 -1
- package/dist/client/assets/badge-2AZ3HfVS.js +0 -1
- package/dist/client/assets/capabilities-CsHjxjab.js +0 -1
- package/dist/client/assets/channel-Dn3_cOkm.js +0 -1
- package/dist/client/assets/chevron-right-Dsw16yGn.js +0 -1
- package/dist/client/assets/classDiagram-JCYQIIEL-ZQvrS8No.js +0 -1
- package/dist/client/assets/classDiagram-v2-OCEON4UE-ZQvrS8No.js +0 -1
- package/dist/client/assets/compiler-runtime-C4g8XhC5.js +0 -1
- package/dist/client/assets/desktopWorkspace-_iM6B9Kf.js +0 -1
- package/dist/client/assets/editorPreferences-D8q1ItMW.js +0 -1
- package/dist/client/assets/eventmodeling-NTZA5JFV-Dj71kuPV.js +0 -1
- package/dist/client/assets/flowDiagram-UKHOOZJN-BaAiwuvL.js +0 -1
- package/dist/client/assets/hostedConnectionCoordinator-BuRWCvUG.js +0 -1
- package/dist/client/assets/hubRoutes-jcMPIFy3.js +0 -1
- package/dist/client/assets/kbd-Dxm5Elqj.js +0 -1
- package/dist/client/assets/layers-Dqez1Uq8.js +0 -1
- package/dist/client/assets/liquidGlass-CA2o_Cq0.js +0 -1
- package/dist/client/assets/liquidGlass.worker-icvLJKHf.js +0 -1
- package/dist/client/assets/main-BqSANuAs.css +0 -2
- package/dist/client/assets/main-DAbEANjY.js +0 -9
- package/dist/client/assets/motion-BdscFixQ.js +0 -1
- package/dist/client/assets/palette-92ylWqeC.js +0 -1
- package/dist/client/assets/perfInstrumentation-BT27w4tn.js +0 -1
- package/dist/client/assets/presentationTier-xlIjcFab.js +0 -1
- package/dist/client/assets/primary-DZjgHvyf.js +0 -5
- package/dist/client/assets/providerDriverMeta-T2YbXzuS.js +0 -1
- package/dist/client/assets/providerStatus-DBGvvkyF.js +0 -1
- package/dist/client/assets/radio-group-CRnHYWa8.js +0 -1
- package/dist/client/assets/railroad-abnf-HS5TGJTU-DxU7Mt1k.js +0 -1
- package/dist/client/assets/railroad-ebnf-LZEXJU2U-BYAPVjRd.js +0 -1
- package/dist/client/assets/railroad-peg-WCYAUIDC-BOqgR_X7.js +0 -1
- package/dist/client/assets/runtime-BwHA1GUS.js +0 -18
- package/dist/client/assets/scroll-area-7n2X_RLQ.js +0 -1
- package/dist/client/assets/separator-CL1qjMqJ.js +0 -1
- package/dist/client/assets/settingsLayout-DxsFwhF0.js +0 -1
- package/dist/client/assets/sheet-CGiaw8-i.js +0 -1
- package/dist/client/assets/sidebar-Cc6ANbOP.js +0 -1
- package/dist/client/assets/sidebarProjectFolders-DO2Uv84F.js +0 -4
- package/dist/client/assets/sidebarStatusText-B4AtFXZo.js +0 -1
- package/dist/client/assets/sidebarUndo-gdSrfa4O.js +0 -1
- package/dist/client/assets/square-Cr9X8U6y.js +0 -1
- package/dist/client/assets/src-C3Y_imaX.js +0 -14
- package/dist/client/assets/stateDiagram-v2-UEYNNEHI-CWtHILqh.js +0 -1
- package/dist/client/assets/storeSelectors-CTDWheMy.js +0 -1
- package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-DuaPce9O.js +0 -8
- package/dist/client/assets/switch-DnpJB2xv.js +0 -48
- package/dist/client/assets/toggle-D10FsFoE.js +0 -1
- package/dist/client/assets/toggle-group-BhzbQPEQ.js +0 -1
- package/dist/client/assets/tooltip-BlweKBxc.js +0 -1
- package/dist/client/assets/usage-BBwaxMif.js +0 -1
- package/dist/client/assets/useAppSidebarCollapsed-CFy476E3.js +0 -1
- package/dist/client/assets/useAppearancePreference-Dlh9P8iu.js +0 -1
- package/dist/client/assets/useBaseUiId-DKOjzmu3.js +0 -1
- package/dist/client/assets/useLabelableId-DQbY2GG1.js +0 -1
- package/dist/client/assets/useNavigate-B8oXtF30.js +0 -1
- package/dist/client/assets/usePhoneThreadActions-6DCLiyJ5.js +0 -1
- package/dist/client/assets/usePopupViewport-CNWcP7Od.js +0 -1
- package/dist/client/assets/usePresentationTier-hxB7Yv_Q.js +0 -1
- package/dist/client/assets/useScrollLock-BbKeKZdj.js +0 -1
- package/dist/client/assets/useTriggerFocusGuards-BC4Wb3GI.js +0 -1
- package/dist/client/assets/useValueChanged-BkTbyZdO.js +0 -1
- package/dist/client/assets/workspaceMetadataProjection-aqZEXDrQ.js +0 -1
- package/dist/collectUint8StreamText-BAUF-vzz.mjs +0 -37
- package/dist/collectUint8StreamText-vagpNUMJ.cjs +0 -42
- package/dist/git-9xWsJYIH.mjs +0 -132
- package/dist/git-xIkXFy_l.cjs +0 -218
- package/dist/sourceControl-B0D1Op29.cjs +0 -253
- package/dist/sourceControl-BiEBxN01.mjs +0 -224
|
@@ -0,0 +1,2258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Ds as SourceControlProviderError, Gr as ChangeRequestReviewThread, Kr as ChangeRequestTimelineItem, Ts as SOURCE_CONTROL_WORKFLOW_LOG_MAX_BYTES, Ur as CHANGE_REQUEST_FILE_CONTENTS_MAX_BYTES, ks as truncateSourceControlDetailContent } from "./src-VTj60bUm.mjs";
|
|
4
|
+
import { $r as withUnsupportedChangeRequestOptionGuards, Ar as isGitSha, Cr as CURRENT_GITLAB_PROJECT, Dr as gitLabWrite, Er as gitLabProjectEndpoint, Mr as parseGitLabMergeRequestReference, Nr as withGitLabQuery, Or as isGitLabDiscussionId, Qr as sourceControlRefFromInput, Sr as parseGitLabTimestamp, Tr as gitLabMergeRequestEndpoint, Xr as SourceControlProvider, Zr as sourceBranch, _r as gitLabMergeCapabilities, br as gitLabReviewDecision, cr as GitLabApprovalStateSchema, dr as GitLabReviewerSchema, fr as GitLabUserRefSchema, gr as gitLabDraftTitle, hr as gitLabAutoMerge, jr as isLastGitLabPage, kr as isGitLabNumericId, lr as GitLabMergeRequestDetailSchema, mr as gitLabAccessLevel, or as GitLabCli, pr as decodeGitLabMergeRequestListJson, rr as gitlabDiscovery, sr as GitLabCliError, ur as GitLabProjectSchema, vr as gitLabMergeRequestFacts, wr as gitLabGet, xr as gitLabReviewerStates, yr as gitLabMergeStateStatus } from "./bin.mjs";
|
|
5
|
+
import { DateTime, Effect, Exit, Layer, Option, Result, Schema, SchemaIssue } from "effect";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
//#region src/sourceControl/gitLabMergeRequestDiffs.ts
|
|
8
|
+
/**
|
|
9
|
+
* GitLab file diffs (merge request `/diffs`, commit `/diff`) and the line
|
|
10
|
+
* arithmetic GitLab positions need. Pure and side-effect free.
|
|
11
|
+
*
|
|
12
|
+
* Docs:
|
|
13
|
+
* - https://docs.gitlab.com/api/merge_requests/#list-merge-request-diffs
|
|
14
|
+
* - https://docs.gitlab.com/api/commits/#retrieve-commit-diff
|
|
15
|
+
* - https://docs.gitlab.com/api/discussions/#create-a-new-thread-in-the-merge-request-diff
|
|
16
|
+
* (unchanged lines need both `old_line` and `new_line`; line codes are
|
|
17
|
+
* `<sha1(path)>_<old>_<new>`)
|
|
18
|
+
*/
|
|
19
|
+
const GitLabDiffEntrySchema = Schema.Struct({
|
|
20
|
+
old_path: Schema.String,
|
|
21
|
+
new_path: Schema.String,
|
|
22
|
+
a_mode: Schema.optional(Schema.NullOr(Schema.String)),
|
|
23
|
+
b_mode: Schema.optional(Schema.NullOr(Schema.String)),
|
|
24
|
+
diff: Schema.optional(Schema.NullOr(Schema.String)),
|
|
25
|
+
new_file: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
26
|
+
renamed_file: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
27
|
+
deleted_file: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
28
|
+
collapsed: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
29
|
+
too_large: Schema.optional(Schema.NullOr(Schema.Boolean))
|
|
30
|
+
});
|
|
31
|
+
/** GitLab's `DiffFile#file_path`: the new path, or the old one when there is none. */
|
|
32
|
+
function gitLabDiffFilePath(entry) {
|
|
33
|
+
return entry.new_path || entry.old_path;
|
|
34
|
+
}
|
|
35
|
+
function findGitLabDiffEntry(entries, path) {
|
|
36
|
+
return entries.find((entry) => entry.new_path === path) ?? entries.find((entry) => entry.old_path === path);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Rebuild a git-style unified diff from GitLab's per-file JSON. GitLab sends
|
|
40
|
+
* only the hunks, so the `diff --git` / mode / rename / `---` `+++` headers are
|
|
41
|
+
* restored here; a file without hunk text (binary, too large, pure rename or
|
|
42
|
+
* mode change) keeps only its extended headers, as `git diff` prints them.
|
|
43
|
+
*/
|
|
44
|
+
function buildGitLabUnifiedDiff(entries) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const oldPath = entry.old_path || entry.new_path;
|
|
48
|
+
const newPath = entry.new_path || entry.old_path;
|
|
49
|
+
out.push(`diff --git a/${oldPath} b/${newPath}`);
|
|
50
|
+
if (entry.new_file === true) out.push(`new file mode ${entry.b_mode || "100644"}`);
|
|
51
|
+
else if (entry.deleted_file === true) out.push(`deleted file mode ${entry.a_mode || "100644"}`);
|
|
52
|
+
else if (entry.a_mode && entry.b_mode && entry.a_mode !== entry.b_mode) out.push(`old mode ${entry.a_mode}`, `new mode ${entry.b_mode}`);
|
|
53
|
+
if (entry.renamed_file === true && oldPath !== newPath) out.push(`rename from ${oldPath}`, `rename to ${newPath}`);
|
|
54
|
+
const body = entry.diff ?? "";
|
|
55
|
+
if (body.trim().length === 0) continue;
|
|
56
|
+
out.push(entry.new_file === true ? "--- /dev/null" : `--- a/${oldPath}`);
|
|
57
|
+
out.push(entry.deleted_file === true ? "+++ /dev/null" : `+++ b/${newPath}`);
|
|
58
|
+
out.push(body.endsWith("\n") ? body.slice(0, -1) : body);
|
|
59
|
+
}
|
|
60
|
+
return out.length === 0 ? "" : `${out.join("\n")}\n`;
|
|
61
|
+
}
|
|
62
|
+
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/u;
|
|
63
|
+
function parseGitLabDiffHunks(diff) {
|
|
64
|
+
const hunks = [];
|
|
65
|
+
const rows = diff.split("\n");
|
|
66
|
+
if (rows.at(-1) === "") rows.pop();
|
|
67
|
+
let current = null;
|
|
68
|
+
const flush = () => {
|
|
69
|
+
if (current) {
|
|
70
|
+
const { oldPos: _oldPos, newPos: _newPos, ...hunk } = current;
|
|
71
|
+
hunks.push(hunk);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
const header = HUNK_HEADER.exec(row);
|
|
76
|
+
if (header) {
|
|
77
|
+
flush();
|
|
78
|
+
const oldStart = Number(header[1]);
|
|
79
|
+
const newStart = Number(header[3]);
|
|
80
|
+
current = {
|
|
81
|
+
header: row,
|
|
82
|
+
oldStart,
|
|
83
|
+
oldCount: header[2] === void 0 ? 1 : Number(header[2]),
|
|
84
|
+
newStart,
|
|
85
|
+
newCount: header[4] === void 0 ? 1 : Number(header[4]),
|
|
86
|
+
rawLines: [],
|
|
87
|
+
lines: [],
|
|
88
|
+
oldPos: oldStart,
|
|
89
|
+
newPos: newStart
|
|
90
|
+
};
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!current) continue;
|
|
94
|
+
const rawIndex = current.rawLines.length;
|
|
95
|
+
current.rawLines.push(row);
|
|
96
|
+
const marker = row[0];
|
|
97
|
+
if (marker === "\\") continue;
|
|
98
|
+
if (marker === "+") {
|
|
99
|
+
current.lines.push({
|
|
100
|
+
kind: "added",
|
|
101
|
+
oldPos: current.oldPos,
|
|
102
|
+
newPos: current.newPos,
|
|
103
|
+
rawIndex
|
|
104
|
+
});
|
|
105
|
+
current.newPos += 1;
|
|
106
|
+
} else if (marker === "-") {
|
|
107
|
+
current.lines.push({
|
|
108
|
+
kind: "removed",
|
|
109
|
+
oldPos: current.oldPos,
|
|
110
|
+
newPos: current.newPos,
|
|
111
|
+
rawIndex
|
|
112
|
+
});
|
|
113
|
+
current.oldPos += 1;
|
|
114
|
+
} else {
|
|
115
|
+
current.lines.push({
|
|
116
|
+
kind: "context",
|
|
117
|
+
oldPos: current.oldPos,
|
|
118
|
+
newPos: current.newPos,
|
|
119
|
+
rawIndex
|
|
120
|
+
});
|
|
121
|
+
current.oldPos += 1;
|
|
122
|
+
current.newPos += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
flush();
|
|
126
|
+
return hunks;
|
|
127
|
+
}
|
|
128
|
+
function lineMatches(line, side, number) {
|
|
129
|
+
return side === "left" ? line.kind !== "added" && line.oldPos === number : line.kind !== "removed" && line.newPos === number;
|
|
130
|
+
}
|
|
131
|
+
function gitLabLineCode(filePath, oldPos, newPos) {
|
|
132
|
+
return `${createHash("sha1").update(filePath).digest("hex")}_${oldPos}_${newPos}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Anchor `line` on `side` of a file's diff. Lines inside a hunk take their
|
|
136
|
+
* hunk position; unchanged lines between hunks (expanded context) are
|
|
137
|
+
* shifted by the hunks before them. Null when the line is not in the file.
|
|
138
|
+
*/
|
|
139
|
+
function resolveGitLabLineAnchor(entry, side, line) {
|
|
140
|
+
const filePath = gitLabDiffFilePath(entry);
|
|
141
|
+
const hunks = parseGitLabDiffHunks(entry.diff ?? "");
|
|
142
|
+
let delta = 0;
|
|
143
|
+
for (const hunk of hunks) {
|
|
144
|
+
const start = side === "left" ? hunk.oldStart : hunk.newStart;
|
|
145
|
+
const count = side === "left" ? hunk.oldCount : hunk.newCount;
|
|
146
|
+
const first = count === 0 ? start + 1 : start;
|
|
147
|
+
if (line < first) break;
|
|
148
|
+
if (line < first + count) {
|
|
149
|
+
const found = hunk.lines.find((candidate) => lineMatches(candidate, side, line));
|
|
150
|
+
return found ? {
|
|
151
|
+
kind: found.kind,
|
|
152
|
+
oldLine: found.kind === "added" ? null : found.oldPos,
|
|
153
|
+
newLine: found.kind === "removed" ? null : found.newPos,
|
|
154
|
+
lineCode: gitLabLineCode(filePath, found.oldPos, found.newPos)
|
|
155
|
+
} : null;
|
|
156
|
+
}
|
|
157
|
+
delta += hunk.newCount - hunk.oldCount;
|
|
158
|
+
}
|
|
159
|
+
if (entry.new_file === true || entry.deleted_file === true) return null;
|
|
160
|
+
const oldLine = side === "left" ? line : line - delta;
|
|
161
|
+
const newLine = side === "left" ? line + delta : line;
|
|
162
|
+
if (oldLine < 1 || newLine < 1) return null;
|
|
163
|
+
return {
|
|
164
|
+
kind: "context",
|
|
165
|
+
oldLine,
|
|
166
|
+
newLine,
|
|
167
|
+
lineCode: gitLabLineCode(filePath, oldLine, newLine)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The hunk excerpt a thread was written against (as GitHub's `diffHunk`):
|
|
172
|
+
* the hunk header and its lines up to and including the commented line.
|
|
173
|
+
*/
|
|
174
|
+
function gitLabDiffHunkExcerpt(entry, side, line) {
|
|
175
|
+
for (const hunk of parseGitLabDiffHunks(entry.diff ?? "")) {
|
|
176
|
+
const found = hunk.lines.find((candidate) => lineMatches(candidate, side, line));
|
|
177
|
+
if (found) return [hunk.header, ...hunk.rawLines.slice(0, found.rawIndex + 1)].join("\n");
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function rangePoint(anchor) {
|
|
182
|
+
return {
|
|
183
|
+
line_code: anchor.lineCode,
|
|
184
|
+
type: anchor.kind === "added" ? "new" : "old",
|
|
185
|
+
...anchor.oldLine !== null ? { old_line: anchor.oldLine } : {},
|
|
186
|
+
...anchor.newLine !== null ? { new_line: anchor.newLine } : {}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* `POST .../draft_notes` body for one review comment, anchored to the diff
|
|
191
|
+
* the reviewer saw (`diffRefs` must be the head they reviewed).
|
|
192
|
+
* Docs: https://docs.gitlab.com/api/draft_notes/#create-a-draft-note
|
|
193
|
+
*/
|
|
194
|
+
function buildGitLabDraftNoteBody(input) {
|
|
195
|
+
const { comment, diffRefs } = input;
|
|
196
|
+
const entry = findGitLabDiffEntry(input.entries, comment.path);
|
|
197
|
+
if (!entry) return Result.fail(`${comment.path} is not part of this merge request's diff.`);
|
|
198
|
+
const shas = {
|
|
199
|
+
base_sha: diffRefs.base_sha,
|
|
200
|
+
start_sha: diffRefs.start_sha,
|
|
201
|
+
head_sha: diffRefs.head_sha,
|
|
202
|
+
old_path: entry.old_path,
|
|
203
|
+
new_path: entry.new_path
|
|
204
|
+
};
|
|
205
|
+
if (comment.subjectType === "file") return Result.succeed({
|
|
206
|
+
note: comment.body,
|
|
207
|
+
position: {
|
|
208
|
+
position_type: "file",
|
|
209
|
+
...shas
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
if (comment.line === void 0) return Result.fail(`The line comment on ${comment.path} is missing its line number.`);
|
|
213
|
+
const side = comment.side ?? "right";
|
|
214
|
+
const anchor = resolveGitLabLineAnchor(entry, side, comment.line);
|
|
215
|
+
if (!anchor) return Result.fail(`Line ${comment.line} of ${comment.path} is not part of this merge request's diff.`);
|
|
216
|
+
const startSide = comment.startSide ?? side;
|
|
217
|
+
const isRange = comment.startLine !== void 0 && (comment.startLine !== comment.line || startSide !== side);
|
|
218
|
+
let lineRange;
|
|
219
|
+
if (isRange && comment.startLine !== void 0) {
|
|
220
|
+
const start = resolveGitLabLineAnchor(entry, startSide, comment.startLine);
|
|
221
|
+
if (!start) return Result.fail(`Line ${comment.startLine} of ${comment.path} is not part of this merge request's diff.`);
|
|
222
|
+
lineRange = {
|
|
223
|
+
start: rangePoint(start),
|
|
224
|
+
end: rangePoint(anchor)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return Result.succeed({
|
|
228
|
+
note: comment.body,
|
|
229
|
+
position: {
|
|
230
|
+
position_type: "text",
|
|
231
|
+
...shas,
|
|
232
|
+
...anchor.oldLine !== null ? { old_line: anchor.oldLine } : {},
|
|
233
|
+
...anchor.newLine !== null ? { new_line: anchor.newLine } : {},
|
|
234
|
+
...lineRange ? { line_range: lineRange } : {}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Thread openers by one author this close together came from one published review. */
|
|
239
|
+
const REVIEW_BATCH_WINDOW_MS = 6e4;
|
|
240
|
+
const GitLabLineRangePointSchema = Schema.Struct({
|
|
241
|
+
line_code: Schema.optional(Schema.NullOr(Schema.String)),
|
|
242
|
+
type: Schema.optional(Schema.NullOr(Schema.String)),
|
|
243
|
+
old_line: Schema.optional(Schema.NullOr(Schema.Number)),
|
|
244
|
+
new_line: Schema.optional(Schema.NullOr(Schema.Number))
|
|
245
|
+
});
|
|
246
|
+
const GitLabPositionSchema = Schema.Struct({
|
|
247
|
+
base_sha: Schema.optional(Schema.NullOr(Schema.String)),
|
|
248
|
+
start_sha: Schema.optional(Schema.NullOr(Schema.String)),
|
|
249
|
+
head_sha: Schema.optional(Schema.NullOr(Schema.String)),
|
|
250
|
+
old_path: Schema.optional(Schema.NullOr(Schema.String)),
|
|
251
|
+
new_path: Schema.optional(Schema.NullOr(Schema.String)),
|
|
252
|
+
position_type: Schema.optional(Schema.NullOr(Schema.String)),
|
|
253
|
+
old_line: Schema.optional(Schema.NullOr(Schema.Number)),
|
|
254
|
+
new_line: Schema.optional(Schema.NullOr(Schema.Number)),
|
|
255
|
+
line_range: Schema.optional(Schema.NullOr(Schema.Struct({
|
|
256
|
+
start: Schema.optional(Schema.NullOr(GitLabLineRangePointSchema)),
|
|
257
|
+
end: Schema.optional(Schema.NullOr(GitLabLineRangePointSchema))
|
|
258
|
+
})))
|
|
259
|
+
});
|
|
260
|
+
const GitLabNoteSchema = Schema.Struct({
|
|
261
|
+
id: Schema.Number,
|
|
262
|
+
type: Schema.optional(Schema.NullOr(Schema.String)),
|
|
263
|
+
body: Schema.String,
|
|
264
|
+
author: Schema.optional(Schema.NullOr(GitLabUserRefSchema)),
|
|
265
|
+
created_at: Schema.String,
|
|
266
|
+
updated_at: Schema.optional(Schema.NullOr(Schema.String)),
|
|
267
|
+
system: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
268
|
+
position: Schema.optional(Schema.NullOr(GitLabPositionSchema)),
|
|
269
|
+
resolvable: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
270
|
+
resolved: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
271
|
+
resolved_by: Schema.optional(Schema.NullOr(GitLabUserRefSchema)),
|
|
272
|
+
internal: Schema.optional(Schema.NullOr(Schema.Boolean))
|
|
273
|
+
});
|
|
274
|
+
const GitLabDiscussionSchema = Schema.Struct({
|
|
275
|
+
id: Schema.String,
|
|
276
|
+
individual_note: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
277
|
+
resolvable: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
278
|
+
resolved: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
279
|
+
notes: Schema.Array(GitLabNoteSchema)
|
|
280
|
+
});
|
|
281
|
+
const GitLabStateEventSchema = Schema.Struct({
|
|
282
|
+
id: Schema.Number,
|
|
283
|
+
user: Schema.optional(Schema.NullOr(GitLabUserRefSchema)),
|
|
284
|
+
created_at: Schema.String,
|
|
285
|
+
state: Schema.String
|
|
286
|
+
});
|
|
287
|
+
const GitLabLabelEventSchema = Schema.Struct({
|
|
288
|
+
id: Schema.Number,
|
|
289
|
+
user: Schema.optional(Schema.NullOr(GitLabUserRefSchema)),
|
|
290
|
+
created_at: Schema.String,
|
|
291
|
+
action: Schema.String,
|
|
292
|
+
label: Schema.optional(Schema.NullOr(Schema.Struct({
|
|
293
|
+
name: Schema.String,
|
|
294
|
+
color: Schema.optional(Schema.NullOr(Schema.String)),
|
|
295
|
+
description: Schema.optional(Schema.NullOr(Schema.String))
|
|
296
|
+
})))
|
|
297
|
+
});
|
|
298
|
+
const GitLabCommitSchema = Schema.Struct({
|
|
299
|
+
id: Schema.String,
|
|
300
|
+
short_id: Schema.optional(Schema.NullOr(Schema.String)),
|
|
301
|
+
title: Schema.optional(Schema.NullOr(Schema.String)),
|
|
302
|
+
message: Schema.optional(Schema.NullOr(Schema.String)),
|
|
303
|
+
author_name: Schema.optional(Schema.NullOr(Schema.String)),
|
|
304
|
+
authored_date: Schema.optional(Schema.NullOr(Schema.String)),
|
|
305
|
+
committed_date: Schema.optional(Schema.NullOr(Schema.String)),
|
|
306
|
+
created_at: Schema.optional(Schema.NullOr(Schema.String))
|
|
307
|
+
});
|
|
308
|
+
const GitLabVersionSchema = Schema.Struct({
|
|
309
|
+
id: Schema.Number,
|
|
310
|
+
head_commit_sha: Schema.String,
|
|
311
|
+
base_commit_sha: Schema.optional(Schema.NullOr(Schema.String)),
|
|
312
|
+
start_commit_sha: Schema.optional(Schema.NullOr(Schema.String)),
|
|
313
|
+
created_at: Schema.String
|
|
314
|
+
});
|
|
315
|
+
const GitLabDraftNoteSchema = Schema.Struct({
|
|
316
|
+
id: Schema.Number,
|
|
317
|
+
author_id: Schema.optional(Schema.NullOr(Schema.Number)),
|
|
318
|
+
note: Schema.optional(Schema.NullOr(Schema.String))
|
|
319
|
+
});
|
|
320
|
+
const GitLabCurrentUserSchema = Schema.Struct({
|
|
321
|
+
id: Schema.Number,
|
|
322
|
+
username: Schema.String,
|
|
323
|
+
avatar_url: Schema.optional(Schema.NullOr(Schema.String)),
|
|
324
|
+
bot: Schema.optional(Schema.NullOr(Schema.Boolean))
|
|
325
|
+
});
|
|
326
|
+
const isTimelineItem = Schema.is(ChangeRequestTimelineItem);
|
|
327
|
+
const isReviewThread = Schema.is(ChangeRequestReviewThread);
|
|
328
|
+
function trimmed$1(value) {
|
|
329
|
+
const text = value?.trim() ?? "";
|
|
330
|
+
return text.length > 0 ? text : null;
|
|
331
|
+
}
|
|
332
|
+
function positiveInt(value) {
|
|
333
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
334
|
+
}
|
|
335
|
+
function gitLabActor(user) {
|
|
336
|
+
const login = trimmed$1(user?.username);
|
|
337
|
+
if (!login) return null;
|
|
338
|
+
const avatarUrl = trimmed$1(user?.avatar_url);
|
|
339
|
+
return {
|
|
340
|
+
login,
|
|
341
|
+
...avatarUrl ? { avatarUrl } : {},
|
|
342
|
+
...user?.bot === true ? { isBot: true } : {}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const HTML_ENTITIES = {
|
|
346
|
+
amp: "&",
|
|
347
|
+
lt: "<",
|
|
348
|
+
gt: ">",
|
|
349
|
+
quot: "\"",
|
|
350
|
+
apos: "'",
|
|
351
|
+
nbsp: " "
|
|
352
|
+
};
|
|
353
|
+
function decodeHtmlEntities(text) {
|
|
354
|
+
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/giu, (match, entity) => {
|
|
355
|
+
if (entity.startsWith("#x") || entity.startsWith("#X")) return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
|
|
356
|
+
if (entity.startsWith("#")) return String.fromCodePoint(Number(entity.slice(1)));
|
|
357
|
+
return HTML_ENTITIES[entity.toLowerCase()] ?? match;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/** System notes are Markdown or (newer GitLab) HTML; compare them as plain text. */
|
|
361
|
+
function gitLabSystemNoteText(body) {
|
|
362
|
+
return decodeHtmlEntities(body.replace(/<[^>]*>/gu, "")).replace(/\*\*/gu, "").replace(/\s+/gu, " ").trim();
|
|
363
|
+
}
|
|
364
|
+
/** Strip GitLab's inline-diff markers (`{-old-}`, `{+new+}`) from a title. */
|
|
365
|
+
function stripInlineDiffMarkers(text) {
|
|
366
|
+
return text.replace(/\{[-+]|[-+]\}/gu, "");
|
|
367
|
+
}
|
|
368
|
+
/** The two titles of a "changed title from … to …" note, HTML or Markdown form. */
|
|
369
|
+
function parseGitLabTitleChange(body) {
|
|
370
|
+
const html = [...body.matchAll(/<code[^>]*class="idiff"[^>]*>([\s\S]*?)<\/code>/giu)];
|
|
371
|
+
if (html.length >= 2 && html[0]?.[1] !== void 0 && html[1]?.[1] !== void 0) return {
|
|
372
|
+
previousTitle: decodeHtmlEntities(html[0][1].replace(/<[^>]*>/gu, "")).trim(),
|
|
373
|
+
currentTitle: decodeHtmlEntities(html[1][1].replace(/<[^>]*>/gu, "")).trim()
|
|
374
|
+
};
|
|
375
|
+
const markdown = /changed title from \*\*([\s\S]*)\*\* to \*\*([\s\S]*)\*\*\s*$/u.exec(body.trim());
|
|
376
|
+
if (markdown?.[1] !== void 0 && markdown[2] !== void 0) return {
|
|
377
|
+
previousTitle: stripInlineDiffMarkers(markdown[1]).trim(),
|
|
378
|
+
currentTitle: stripInlineDiffMarkers(markdown[2]).trim()
|
|
379
|
+
};
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
/** `@user` mentions in one clause of a system note. */
|
|
383
|
+
function mentionedUsers(text) {
|
|
384
|
+
return [...text.matchAll(/@([\w.-]+)/gu)].flatMap((match) => match[1] ? [match[1].replace(/\.+$/u, "")] : []);
|
|
385
|
+
}
|
|
386
|
+
/** Split "requested review from @a and removed review request for @b" into its clauses. */
|
|
387
|
+
function clauses(text, phrases) {
|
|
388
|
+
const lower = text.toLowerCase();
|
|
389
|
+
const found = phrases.flatMap((phrase) => {
|
|
390
|
+
const index = lower.indexOf(phrase);
|
|
391
|
+
return index >= 0 ? [{
|
|
392
|
+
phrase,
|
|
393
|
+
index
|
|
394
|
+
}] : [];
|
|
395
|
+
}).toSorted((left, right) => left.index - right.index);
|
|
396
|
+
return found.map((entry, position) => {
|
|
397
|
+
const end = found[position + 1]?.index ?? text.length;
|
|
398
|
+
return {
|
|
399
|
+
phrase: entry.phrase,
|
|
400
|
+
users: mentionedUsers(text.slice(entry.index + entry.phrase.length, end))
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function projectWebUrl(mergeRequestUrl) {
|
|
405
|
+
const index = mergeRequestUrl.indexOf("/-/merge_requests/");
|
|
406
|
+
return index > 0 ? mergeRequestUrl.slice(0, index) : null;
|
|
407
|
+
}
|
|
408
|
+
function crossReference(text, mergeRequestUrl) {
|
|
409
|
+
const match = /^mentioned in (merge request|issue) ([\w./-]*?)([!#])(\d+)$/u.exec(text);
|
|
410
|
+
const number = match?.[4] ? positiveInt(Number(match[4])) : null;
|
|
411
|
+
const projectUrl = projectWebUrl(mergeRequestUrl);
|
|
412
|
+
if (!match || number === null || !projectUrl) return null;
|
|
413
|
+
const kind = match[1] === "merge request" ? "change-request" : "issue";
|
|
414
|
+
const repository = trimmed$1(match[2]);
|
|
415
|
+
return {
|
|
416
|
+
kind,
|
|
417
|
+
number,
|
|
418
|
+
url: `${repository ? `${new URL(projectUrl).origin}/${repository}` : projectUrl}/-/${kind === "change-request" ? "merge_requests" : "issues"}/${number}`,
|
|
419
|
+
...repository ? { repository } : {}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* One system note as timeline items. GitLab writes system notes as prose, so
|
|
424
|
+
* only its documented, stable phrasings are recognised; anything else is
|
|
425
|
+
* dropped rather than guessed at. State and label changes come from their
|
|
426
|
+
* resource-event APIs, commits from the commit list, so their notes drop too.
|
|
427
|
+
*/
|
|
428
|
+
function gitLabSystemNoteItems(note, context) {
|
|
429
|
+
const createdAt = parseGitLabTimestamp(note.created_at);
|
|
430
|
+
if (!createdAt) return [];
|
|
431
|
+
const actor = gitLabActor(note.author);
|
|
432
|
+
const base = {
|
|
433
|
+
createdAt,
|
|
434
|
+
...actor ? { actor } : {}
|
|
435
|
+
};
|
|
436
|
+
const id = `note:${note.id}`;
|
|
437
|
+
const text = gitLabSystemNoteText(note.body);
|
|
438
|
+
const lower = text.toLowerCase();
|
|
439
|
+
if (lower.startsWith("changed title from")) {
|
|
440
|
+
const titles = parseGitLabTitleChange(note.body);
|
|
441
|
+
return titles ? [{
|
|
442
|
+
...base,
|
|
443
|
+
id,
|
|
444
|
+
kind: "renamed",
|
|
445
|
+
...titles
|
|
446
|
+
}] : [];
|
|
447
|
+
}
|
|
448
|
+
if (lower === "marked this merge request as draft" || lower === "marked as a work in progress" || lower === "marked this merge request as a draft") return [{
|
|
449
|
+
...base,
|
|
450
|
+
id,
|
|
451
|
+
kind: "converted-to-draft"
|
|
452
|
+
}];
|
|
453
|
+
if (lower === "marked this merge request as ready" || lower === "unmarked as a work in progress") return [{
|
|
454
|
+
...base,
|
|
455
|
+
id,
|
|
456
|
+
kind: "ready-for-review"
|
|
457
|
+
}];
|
|
458
|
+
if (lower.startsWith("requested review from") || lower.startsWith("removed review request for")) return clauses(text, ["requested review from", "removed review request for"]).flatMap((clause) => clause.users.map((reviewer) => ({
|
|
459
|
+
...base,
|
|
460
|
+
id: `${id}:${clause.phrase === "requested review from" ? "review-requested" : "review-request-removed"}:${reviewer}`,
|
|
461
|
+
kind: clause.phrase === "requested review from" ? "review-requested" : "review-request-removed",
|
|
462
|
+
reviewer,
|
|
463
|
+
reviewerKind: "user"
|
|
464
|
+
})));
|
|
465
|
+
if (lower.startsWith("assigned to") || lower.startsWith("unassigned")) return clauses(text, ["assigned to", "unassigned"]).flatMap((clause) => clause.users.map((assignee) => ({
|
|
466
|
+
...base,
|
|
467
|
+
id: `${id}:${clause.phrase === "assigned to" ? "assigned" : "unassigned"}:${assignee}`,
|
|
468
|
+
kind: clause.phrase === "assigned to" ? "assigned" : "unassigned",
|
|
469
|
+
assignee
|
|
470
|
+
})));
|
|
471
|
+
if (lower === "approved this merge request") return [{
|
|
472
|
+
...base,
|
|
473
|
+
id,
|
|
474
|
+
kind: "review",
|
|
475
|
+
state: "approved",
|
|
476
|
+
body: "",
|
|
477
|
+
threadIds: []
|
|
478
|
+
}];
|
|
479
|
+
if (lower === "requested changes") return [{
|
|
480
|
+
...base,
|
|
481
|
+
id,
|
|
482
|
+
kind: "review",
|
|
483
|
+
state: "changes_requested",
|
|
484
|
+
body: "",
|
|
485
|
+
threadIds: []
|
|
486
|
+
}];
|
|
487
|
+
if (lower === "unapproved this merge request" || lower.startsWith("revoked approval for this merge request")) return actor ? [{
|
|
488
|
+
...base,
|
|
489
|
+
id,
|
|
490
|
+
kind: "review-dismissed",
|
|
491
|
+
reviewAuthor: actor.login
|
|
492
|
+
}] : [];
|
|
493
|
+
if (lower.startsWith("reset approvals from")) {
|
|
494
|
+
const dismissed = [];
|
|
495
|
+
for (const reviewAuthor of mentionedUsers(text)) dismissed.push({
|
|
496
|
+
...base,
|
|
497
|
+
id: `${id}:review-dismissed:${reviewAuthor}`,
|
|
498
|
+
kind: "review-dismissed",
|
|
499
|
+
reviewAuthor,
|
|
500
|
+
message: "Approvals reset by a push to the branch."
|
|
501
|
+
});
|
|
502
|
+
return dismissed;
|
|
503
|
+
}
|
|
504
|
+
if (/^enabled (?:an )?automatic (?:merge|add to merge train)/u.test(lower)) return [{
|
|
505
|
+
...base,
|
|
506
|
+
id,
|
|
507
|
+
kind: "auto-merge-enabled"
|
|
508
|
+
}];
|
|
509
|
+
if (/^(?:canceled|cancelled|aborted) (?:the )?automatic (?:merge|add to merge train)/u.test(lower)) return [{
|
|
510
|
+
...base,
|
|
511
|
+
id,
|
|
512
|
+
kind: "auto-merge-disabled"
|
|
513
|
+
}];
|
|
514
|
+
const target = /^changed target branch from `?(.+?)`? to `?(.+?)`?$/u.exec(text);
|
|
515
|
+
if (target?.[1] && target[2]) return [{
|
|
516
|
+
...base,
|
|
517
|
+
id,
|
|
518
|
+
kind: "base-ref-changed",
|
|
519
|
+
previousRefName: target[1],
|
|
520
|
+
currentRefName: target[2]
|
|
521
|
+
}];
|
|
522
|
+
const reference = crossReference(text, context.mergeRequestUrl);
|
|
523
|
+
if (reference) return [{
|
|
524
|
+
...base,
|
|
525
|
+
id,
|
|
526
|
+
kind: "cross-referenced",
|
|
527
|
+
source: {
|
|
528
|
+
...reference,
|
|
529
|
+
title: ""
|
|
530
|
+
}
|
|
531
|
+
}];
|
|
532
|
+
return [];
|
|
533
|
+
}
|
|
534
|
+
/** Commits a push note ("added 2 commits … <li>abc123 - …") credits to its author. */
|
|
535
|
+
function gitLabPushedCommitPrefixes(note) {
|
|
536
|
+
const text = gitLabSystemNoteText(note.body);
|
|
537
|
+
if (!/^added \d+ (?:new )?commits?/iu.test(text)) return [];
|
|
538
|
+
return [...note.body.matchAll(/<li>\s*([0-9a-f]{7,40})\s+-/giu)].flatMap((match) => match[1] ? [match[1].toLowerCase()] : []);
|
|
539
|
+
}
|
|
540
|
+
function noteComment(note, context) {
|
|
541
|
+
const createdAt = parseGitLabTimestamp(note.created_at);
|
|
542
|
+
if (!createdAt) return null;
|
|
543
|
+
const updatedAt = parseGitLabTimestamp(note.updated_at);
|
|
544
|
+
const own = context.viewerId !== null && note.author?.id === context.viewerId;
|
|
545
|
+
return {
|
|
546
|
+
id: String(note.id),
|
|
547
|
+
author: gitLabActor(note.author) ?? { login: "ghost" },
|
|
548
|
+
body: note.body,
|
|
549
|
+
createdAt,
|
|
550
|
+
...updatedAt && DateTime.toEpochMillis(updatedAt) !== DateTime.toEpochMillis(createdAt) ? { updatedAt } : {},
|
|
551
|
+
url: `${context.mergeRequestUrl}#note_${note.id}`,
|
|
552
|
+
state: "submitted",
|
|
553
|
+
viewerCanUpdate: own,
|
|
554
|
+
viewerCanDelete: own
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function lineOf(point) {
|
|
558
|
+
const newLine = positiveInt(point.new_line);
|
|
559
|
+
if (newLine !== null) return {
|
|
560
|
+
side: "right",
|
|
561
|
+
line: newLine
|
|
562
|
+
};
|
|
563
|
+
const oldLine = positiveInt(point.old_line);
|
|
564
|
+
return oldLine !== null ? {
|
|
565
|
+
side: "left",
|
|
566
|
+
line: oldLine
|
|
567
|
+
} : null;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* A discussion with a diff position as a review thread. Positions follow the
|
|
571
|
+
* merge request across pushes: GitLab moves a note's `position` to the new
|
|
572
|
+
* head while its line still exists, so a position still on an older head no
|
|
573
|
+
* longer maps onto the diff and the thread is outdated.
|
|
574
|
+
*/
|
|
575
|
+
function gitLabReviewThread(discussion, context) {
|
|
576
|
+
const notes = discussion.notes.filter((note) => note.system !== true);
|
|
577
|
+
const opener = notes[0];
|
|
578
|
+
const position = opener?.position;
|
|
579
|
+
if (!opener || !position) return null;
|
|
580
|
+
const positionType = position.position_type?.trim().toLowerCase() ?? "text";
|
|
581
|
+
if (positionType !== "text" && positionType !== "file") return null;
|
|
582
|
+
const path = trimmed$1(position.new_path) ?? trimmed$1(position.old_path);
|
|
583
|
+
const openerCreatedAt = parseGitLabTimestamp(opener.created_at);
|
|
584
|
+
if (!path || !openerCreatedAt) return null;
|
|
585
|
+
const positionHead = trimmed$1(position.head_sha);
|
|
586
|
+
const isOutdated = positionHead !== null && context.headSha !== null && positionHead !== context.headSha;
|
|
587
|
+
const anchor = positionType === "text" ? lineOf(position) : null;
|
|
588
|
+
if (positionType === "text" && !anchor) return null;
|
|
589
|
+
const side = anchor?.side ?? "right";
|
|
590
|
+
const rangeStart = position.line_range?.start ? lineOf(position.line_range.start) : null;
|
|
591
|
+
const start = anchor && rangeStart && (rangeStart.line !== anchor.line || rangeStart.side !== anchor.side) ? rangeStart : null;
|
|
592
|
+
const entry = !isOutdated && anchor && context.diffEntries ? findGitLabDiffEntry(context.diffEntries, path) : void 0;
|
|
593
|
+
const diffHunk = entry && anchor ? gitLabDiffHunkExcerpt(entry, anchor.side, anchor.line) : null;
|
|
594
|
+
const resolvable = notes.filter((note) => note.resolvable === true);
|
|
595
|
+
const isResolvable = resolvable.length > 0 || discussion.resolvable === true;
|
|
596
|
+
const isResolved = resolvable.length > 0 ? resolvable.every((note) => note.resolved === true) : discussion.resolved === true;
|
|
597
|
+
const resolvedBy = isResolved ? resolvable.map((note) => trimmed$1(note.resolved_by?.username)).findLast(Boolean) ?? null : null;
|
|
598
|
+
const comments = notes.slice(0, 100).flatMap((note) => {
|
|
599
|
+
const comment = noteComment(note, context);
|
|
600
|
+
return comment ? [comment] : [];
|
|
601
|
+
});
|
|
602
|
+
const thread = {
|
|
603
|
+
id: discussion.id,
|
|
604
|
+
path,
|
|
605
|
+
subjectType: positionType === "file" ? "file" : "line",
|
|
606
|
+
side,
|
|
607
|
+
...start ? { startSide: start.side } : {},
|
|
608
|
+
line: anchor && !isOutdated ? anchor.line : null,
|
|
609
|
+
...start ? { startLine: isOutdated ? null : start.line } : {},
|
|
610
|
+
...anchor ? { originalLine: anchor.line } : {},
|
|
611
|
+
...start ? { originalStartLine: start.line } : {},
|
|
612
|
+
...positionHead ? { originalCommitOid: positionHead } : {},
|
|
613
|
+
...diffHunk ? { diffHunk } : {},
|
|
614
|
+
isResolved,
|
|
615
|
+
isOutdated,
|
|
616
|
+
...resolvedBy ? { resolvedBy } : {},
|
|
617
|
+
viewerCanReply: context.viewerCanReply,
|
|
618
|
+
viewerCanResolve: isResolvable && !isResolved && context.viewerCanResolve,
|
|
619
|
+
viewerCanUnresolve: isResolvable && isResolved && context.viewerCanResolve,
|
|
620
|
+
comments,
|
|
621
|
+
totalComments: Math.max(notes.length, comments.length)
|
|
622
|
+
};
|
|
623
|
+
if (!isReviewThread(thread)) return null;
|
|
624
|
+
return {
|
|
625
|
+
thread,
|
|
626
|
+
openerAuthor: trimmed$1(opener.author?.username),
|
|
627
|
+
openerCreatedAt
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
function gitLabViewerCapabilities(input) {
|
|
631
|
+
const { user, facts } = input;
|
|
632
|
+
const login = trimmed$1(user?.username);
|
|
633
|
+
if (!user || !login) return null;
|
|
634
|
+
const access = gitLabAccessLevel(input.project);
|
|
635
|
+
const isAuthor = facts.authorId !== null ? facts.authorId === user.id : facts.authorUsername === login;
|
|
636
|
+
const canUpdate = isAuthor || access >= 30;
|
|
637
|
+
const canMerge = facts.canMerge === true;
|
|
638
|
+
return {
|
|
639
|
+
login,
|
|
640
|
+
isAuthor,
|
|
641
|
+
canUpdate,
|
|
642
|
+
canMerge,
|
|
643
|
+
canReview: !facts.discussionLocked || access > 0,
|
|
644
|
+
canUpdateBranch: canUpdate,
|
|
645
|
+
canEnableAutoMerge: canMerge,
|
|
646
|
+
canDisableAutoMerge: canMerge || facts.autoMergeBy !== null && facts.autoMergeBy === login
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function epoch(value) {
|
|
650
|
+
return DateTime.toEpochMillis(value);
|
|
651
|
+
}
|
|
652
|
+
function commitItems(input, pushers) {
|
|
653
|
+
return input.commits.items.flatMap((commit) => {
|
|
654
|
+
const createdAt = parseGitLabTimestamp(commit.committed_date) ?? parseGitLabTimestamp(commit.created_at) ?? parseGitLabTimestamp(commit.authored_date);
|
|
655
|
+
const oid = trimmed$1(commit.id);
|
|
656
|
+
if (!createdAt || !oid) return [];
|
|
657
|
+
const [headline = "", ...rest] = (commit.message ?? commit.title ?? "").split("\n");
|
|
658
|
+
const body = rest.join("\n").trim();
|
|
659
|
+
const actor = [...pushers.entries()].find(([prefix]) => oid.toLowerCase().startsWith(prefix))?.[1];
|
|
660
|
+
return [{
|
|
661
|
+
id: `commit:${oid}`,
|
|
662
|
+
kind: "commit",
|
|
663
|
+
createdAt,
|
|
664
|
+
...actor ? { actor } : {},
|
|
665
|
+
oid,
|
|
666
|
+
shortOid: trimmed$1(commit.short_id) ?? oid.slice(0, 8),
|
|
667
|
+
messageHeadline: trimmed$1(commit.title) ?? headline.trim(),
|
|
668
|
+
...body ? { messageBody: body } : {}
|
|
669
|
+
}];
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* A push that rewrote history: the previous version's head is no longer one
|
|
674
|
+
* of the merge request's commits. Undecidable when the commit list is cut.
|
|
675
|
+
*/
|
|
676
|
+
function forcePushItems(input) {
|
|
677
|
+
if (input.commits.truncated) return [];
|
|
678
|
+
const known = new Set(input.commits.items.map((commit) => commit.id.toLowerCase()));
|
|
679
|
+
const versions = input.versions.flatMap((version) => {
|
|
680
|
+
const createdAt = parseGitLabTimestamp(version.created_at);
|
|
681
|
+
return createdAt ? [{
|
|
682
|
+
version,
|
|
683
|
+
createdAt
|
|
684
|
+
}] : [];
|
|
685
|
+
}).toSorted((left, right) => epoch(left.createdAt) - epoch(right.createdAt));
|
|
686
|
+
const items = [];
|
|
687
|
+
for (let index = 1; index < versions.length; index += 1) {
|
|
688
|
+
const previous = versions[index - 1];
|
|
689
|
+
const current = versions[index];
|
|
690
|
+
if (!previous || !current) continue;
|
|
691
|
+
const before = previous.version.head_commit_sha.trim();
|
|
692
|
+
const after = current.version.head_commit_sha.trim();
|
|
693
|
+
if (!before || !after || before === after || known.has(before.toLowerCase())) continue;
|
|
694
|
+
items.push({
|
|
695
|
+
id: `version:${current.version.id}`,
|
|
696
|
+
kind: "force-pushed",
|
|
697
|
+
createdAt: current.createdAt,
|
|
698
|
+
beforeOid: before,
|
|
699
|
+
afterOid: after
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
return items;
|
|
703
|
+
}
|
|
704
|
+
function eventItems(input) {
|
|
705
|
+
const items = [];
|
|
706
|
+
for (const event of input.stateEvents) {
|
|
707
|
+
const createdAt = parseGitLabTimestamp(event.created_at);
|
|
708
|
+
if (!createdAt) continue;
|
|
709
|
+
const actor = gitLabActor(event.user);
|
|
710
|
+
const base = {
|
|
711
|
+
id: `state:${event.id}`,
|
|
712
|
+
createdAt,
|
|
713
|
+
...actor ? { actor } : {}
|
|
714
|
+
};
|
|
715
|
+
switch (event.state.trim().toLowerCase()) {
|
|
716
|
+
case "merged":
|
|
717
|
+
items.push({
|
|
718
|
+
...base,
|
|
719
|
+
kind: "merged",
|
|
720
|
+
...input.facts.mergeCommitSha ? { commitOid: input.facts.mergeCommitSha } : {},
|
|
721
|
+
baseRefName: input.facts.targetBranch
|
|
722
|
+
});
|
|
723
|
+
break;
|
|
724
|
+
case "closed":
|
|
725
|
+
items.push({
|
|
726
|
+
...base,
|
|
727
|
+
kind: "closed"
|
|
728
|
+
});
|
|
729
|
+
break;
|
|
730
|
+
case "reopened": items.push({
|
|
731
|
+
...base,
|
|
732
|
+
kind: "reopened"
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
for (const event of input.labelEvents) {
|
|
737
|
+
const createdAt = parseGitLabTimestamp(event.created_at);
|
|
738
|
+
const name = trimmed$1(event.label?.name);
|
|
739
|
+
const action = event.action.trim().toLowerCase();
|
|
740
|
+
if (!createdAt || !name || action !== "add" && action !== "remove") continue;
|
|
741
|
+
const actor = gitLabActor(event.user);
|
|
742
|
+
const color = trimmed$1(event.label?.color)?.replace(/^#/u, "").toLowerCase();
|
|
743
|
+
const description = trimmed$1(event.label?.description);
|
|
744
|
+
items.push({
|
|
745
|
+
id: `label:${event.id}`,
|
|
746
|
+
kind: action === "add" ? "labeled" : "unlabeled",
|
|
747
|
+
createdAt,
|
|
748
|
+
...actor ? { actor } : {},
|
|
749
|
+
label: {
|
|
750
|
+
name,
|
|
751
|
+
...color && /^[0-9a-f]{3,8}$/u.test(color) ? { color } : {},
|
|
752
|
+
...description ? { description } : {}
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
return items;
|
|
757
|
+
}
|
|
758
|
+
/** Threads published together (one author, openers within a minute) form one review. */
|
|
759
|
+
function reviewItems(entries) {
|
|
760
|
+
const sorted = entries.toSorted((left, right) => epoch(left.openerCreatedAt) - epoch(right.openerCreatedAt));
|
|
761
|
+
const items = [];
|
|
762
|
+
let batch = [];
|
|
763
|
+
const flush = () => {
|
|
764
|
+
const first = batch[0];
|
|
765
|
+
if (!first) return;
|
|
766
|
+
const actor = first.thread.comments[0]?.author;
|
|
767
|
+
items.push({
|
|
768
|
+
id: `review:${first.thread.id}`,
|
|
769
|
+
kind: "review",
|
|
770
|
+
createdAt: first.openerCreatedAt,
|
|
771
|
+
...actor ? { actor } : {},
|
|
772
|
+
state: "commented",
|
|
773
|
+
body: "",
|
|
774
|
+
threadIds: batch.map((entry) => entry.thread.id)
|
|
775
|
+
});
|
|
776
|
+
batch = [];
|
|
777
|
+
};
|
|
778
|
+
for (const entry of sorted) {
|
|
779
|
+
const last = batch.at(-1);
|
|
780
|
+
if (last && (last.openerAuthor !== entry.openerAuthor || epoch(entry.openerCreatedAt) - epoch(last.openerCreatedAt) > REVIEW_BATCH_WINDOW_MS)) flush();
|
|
781
|
+
batch.push(entry);
|
|
782
|
+
}
|
|
783
|
+
flush();
|
|
784
|
+
return items;
|
|
785
|
+
}
|
|
786
|
+
function assembleGitLabChangeRequestActivity(input) {
|
|
787
|
+
const { facts } = input;
|
|
788
|
+
const viewer = gitLabViewerCapabilities(input);
|
|
789
|
+
const access = gitLabAccessLevel(input.project);
|
|
790
|
+
const threadContext = {
|
|
791
|
+
mergeRequestUrl: facts.webUrl,
|
|
792
|
+
headSha: facts.headSha,
|
|
793
|
+
viewerId: input.user?.id ?? null,
|
|
794
|
+
viewerCanReply: viewer !== null && viewer.canReview,
|
|
795
|
+
viewerCanResolve: viewer !== null && (viewer.isAuthor || access >= 30),
|
|
796
|
+
diffEntries: input.diffEntries
|
|
797
|
+
};
|
|
798
|
+
const threadEntries = input.discussions.items.flatMap((discussion) => {
|
|
799
|
+
const entry = gitLabReviewThread(discussion, threadContext);
|
|
800
|
+
return entry ? [entry] : [];
|
|
801
|
+
});
|
|
802
|
+
const threadIds = new Set(threadEntries.map((entry) => entry.thread.id));
|
|
803
|
+
const pushers = /* @__PURE__ */ new Map();
|
|
804
|
+
const timeline = [];
|
|
805
|
+
const seen = /* @__PURE__ */ new Set();
|
|
806
|
+
const push = (item) => {
|
|
807
|
+
if (seen.has(item.id) || !isTimelineItem(item)) return;
|
|
808
|
+
seen.add(item.id);
|
|
809
|
+
timeline.push(item);
|
|
810
|
+
};
|
|
811
|
+
const notes = input.notes.items.toSorted((left, right) => Date.parse(left.created_at) - Date.parse(right.created_at) || left.id - right.id);
|
|
812
|
+
for (const note of notes) {
|
|
813
|
+
if (note.system === true) {
|
|
814
|
+
const actor = gitLabActor(note.author);
|
|
815
|
+
if (actor) for (const prefix of gitLabPushedCommitPrefixes(note)) pushers.set(prefix, actor);
|
|
816
|
+
for (const item of gitLabSystemNoteItems(note, {
|
|
817
|
+
mergeRequestUrl: facts.webUrl,
|
|
818
|
+
targetBranch: facts.targetBranch
|
|
819
|
+
})) push(item);
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if (note.position || note.type === "DiffNote") continue;
|
|
823
|
+
const comment = noteComment(note, {
|
|
824
|
+
...threadContext,
|
|
825
|
+
diffEntries: null
|
|
826
|
+
});
|
|
827
|
+
if (!comment) continue;
|
|
828
|
+
push({
|
|
829
|
+
id: `note:${note.id}`,
|
|
830
|
+
kind: "comment",
|
|
831
|
+
createdAt: comment.createdAt,
|
|
832
|
+
actor: comment.author,
|
|
833
|
+
body: comment.body,
|
|
834
|
+
...comment.updatedAt ? { updatedAt: comment.updatedAt } : {},
|
|
835
|
+
...comment.url ? { url: comment.url } : {},
|
|
836
|
+
viewerCanUpdate: comment.viewerCanUpdate === true,
|
|
837
|
+
viewerCanDelete: comment.viewerCanDelete === true
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
const cutoff = input.notes.truncated && notes[0] ? Date.parse(notes[0].created_at) : null;
|
|
841
|
+
for (const item of [
|
|
842
|
+
...eventItems(input),
|
|
843
|
+
...commitItems(input, pushers),
|
|
844
|
+
...forcePushItems(input),
|
|
845
|
+
...reviewItems(threadEntries.filter((entry) => threadIds.has(entry.thread.id)))
|
|
846
|
+
]) {
|
|
847
|
+
if (cutoff !== null && epoch(item.createdAt) < cutoff) continue;
|
|
848
|
+
push(item);
|
|
849
|
+
}
|
|
850
|
+
timeline.sort((left, right) => epoch(left.createdAt) - epoch(right.createdAt));
|
|
851
|
+
return {
|
|
852
|
+
provider: "gitlab",
|
|
853
|
+
number: facts.iid,
|
|
854
|
+
headSha: facts.headSha,
|
|
855
|
+
viewer,
|
|
856
|
+
timeline,
|
|
857
|
+
timelineTruncated: input.notes.truncated || input.commits.truncated,
|
|
858
|
+
reviewThreads: threadEntries.map((entry) => entry.thread),
|
|
859
|
+
reviewThreadsTruncated: input.discussions.truncated,
|
|
860
|
+
pendingReview: input.draftNotes && input.draftNotes.length > 0 ? {
|
|
861
|
+
id: "draft-notes",
|
|
862
|
+
commentsCount: input.draftNotes.length
|
|
863
|
+
} : null
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
//#endregion
|
|
867
|
+
//#region src/sourceControl/gitLabPipelines.ts
|
|
868
|
+
/**
|
|
869
|
+
* GitLab CI as the page's checks: pipelines are workflow runs, jobs are jobs
|
|
870
|
+
* (GitLab jobs have no steps), commit statuses are the check rollup. Pure.
|
|
871
|
+
*
|
|
872
|
+
* Docs:
|
|
873
|
+
* - https://docs.gitlab.com/api/merge_requests/#list-merge-request-pipelines
|
|
874
|
+
* - https://docs.gitlab.com/api/pipelines/#list-project-pipelines
|
|
875
|
+
* - https://docs.gitlab.com/api/jobs/#list-all-jobs-by-pipeline
|
|
876
|
+
* - https://docs.gitlab.com/api/jobs/#retrieve-a-log-file-for-a-job
|
|
877
|
+
* - https://docs.gitlab.com/api/commits/#list-commit-statuses
|
|
878
|
+
*/
|
|
879
|
+
const GitLabPipelineSchema = Schema.Struct({
|
|
880
|
+
id: Schema.Number,
|
|
881
|
+
iid: Schema.optional(Schema.NullOr(Schema.Number)),
|
|
882
|
+
sha: Schema.String,
|
|
883
|
+
ref: Schema.optional(Schema.NullOr(Schema.String)),
|
|
884
|
+
status: Schema.String,
|
|
885
|
+
source: Schema.optional(Schema.NullOr(Schema.String)),
|
|
886
|
+
name: Schema.optional(Schema.NullOr(Schema.String)),
|
|
887
|
+
web_url: Schema.optional(Schema.NullOr(Schema.String)),
|
|
888
|
+
created_at: Schema.optional(Schema.NullOr(Schema.String)),
|
|
889
|
+
updated_at: Schema.optional(Schema.NullOr(Schema.String)),
|
|
890
|
+
started_at: Schema.optional(Schema.NullOr(Schema.String)),
|
|
891
|
+
finished_at: Schema.optional(Schema.NullOr(Schema.String)),
|
|
892
|
+
duration: Schema.optional(Schema.NullOr(Schema.Number)),
|
|
893
|
+
user: Schema.optional(Schema.NullOr(GitLabUserRefSchema))
|
|
894
|
+
});
|
|
895
|
+
const GitLabJobSchema = Schema.Struct({
|
|
896
|
+
id: Schema.Number,
|
|
897
|
+
name: Schema.String,
|
|
898
|
+
stage: Schema.optional(Schema.NullOr(Schema.String)),
|
|
899
|
+
status: Schema.String,
|
|
900
|
+
allow_failure: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
901
|
+
started_at: Schema.optional(Schema.NullOr(Schema.String)),
|
|
902
|
+
finished_at: Schema.optional(Schema.NullOr(Schema.String)),
|
|
903
|
+
duration: Schema.optional(Schema.NullOr(Schema.Number)),
|
|
904
|
+
web_url: Schema.optional(Schema.NullOr(Schema.String))
|
|
905
|
+
});
|
|
906
|
+
const GitLabCommitStatusSchema = Schema.Struct({
|
|
907
|
+
id: Schema.Number,
|
|
908
|
+
/** GitLab 17.9+. */
|
|
909
|
+
pipeline_id: Schema.optional(Schema.NullOr(Schema.Number)),
|
|
910
|
+
name: Schema.String,
|
|
911
|
+
status: Schema.String,
|
|
912
|
+
target_url: Schema.optional(Schema.NullOr(Schema.String)),
|
|
913
|
+
allow_failure: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
914
|
+
created_at: Schema.optional(Schema.NullOr(Schema.String)),
|
|
915
|
+
started_at: Schema.optional(Schema.NullOr(Schema.String)),
|
|
916
|
+
finished_at: Schema.optional(Schema.NullOr(Schema.String))
|
|
917
|
+
});
|
|
918
|
+
function trimmed(value) {
|
|
919
|
+
const text = value?.trim() ?? "";
|
|
920
|
+
return text.length > 0 ? text : null;
|
|
921
|
+
}
|
|
922
|
+
function optionString(value) {
|
|
923
|
+
const text = trimmed(value);
|
|
924
|
+
return text ? Option.some(text) : Option.none();
|
|
925
|
+
}
|
|
926
|
+
function optionTime(value) {
|
|
927
|
+
const parsed = parseGitLabTimestamp(value);
|
|
928
|
+
return parsed ? Option.some(parsed) : Option.none();
|
|
929
|
+
}
|
|
930
|
+
function durationMs(seconds) {
|
|
931
|
+
return typeof seconds === "number" && Number.isFinite(seconds) && seconds >= 0 ? Option.some(Math.round(seconds * 1e3)) : Option.none();
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* GitLab pipeline/job/status values as the GitHub-style status + conclusion
|
|
935
|
+
* the page classifies. A failure GitLab tolerates (`allow_failure`) is
|
|
936
|
+
* neutral, an optional manual job is skipped, a blocking one waits.
|
|
937
|
+
*/
|
|
938
|
+
function gitLabCheckState(status, allowFailure = false) {
|
|
939
|
+
switch (status.trim().toLowerCase()) {
|
|
940
|
+
case "success": return {
|
|
941
|
+
status: "completed",
|
|
942
|
+
conclusion: "success"
|
|
943
|
+
};
|
|
944
|
+
case "failed": return {
|
|
945
|
+
status: "completed",
|
|
946
|
+
conclusion: allowFailure ? "neutral" : "failure"
|
|
947
|
+
};
|
|
948
|
+
case "canceled":
|
|
949
|
+
case "cancelled": return {
|
|
950
|
+
status: "completed",
|
|
951
|
+
conclusion: "cancelled"
|
|
952
|
+
};
|
|
953
|
+
case "skipped": return {
|
|
954
|
+
status: "completed",
|
|
955
|
+
conclusion: "skipped"
|
|
956
|
+
};
|
|
957
|
+
case "manual": return allowFailure ? {
|
|
958
|
+
status: "completed",
|
|
959
|
+
conclusion: "skipped"
|
|
960
|
+
} : {
|
|
961
|
+
status: "waiting",
|
|
962
|
+
conclusion: null
|
|
963
|
+
};
|
|
964
|
+
case "running":
|
|
965
|
+
case "canceling": return {
|
|
966
|
+
status: "in_progress",
|
|
967
|
+
conclusion: null
|
|
968
|
+
};
|
|
969
|
+
case "created":
|
|
970
|
+
case "pending":
|
|
971
|
+
case "preparing":
|
|
972
|
+
case "scheduled":
|
|
973
|
+
case "waiting_for_resource":
|
|
974
|
+
case "waiting_for_callback": return {
|
|
975
|
+
status: "queued",
|
|
976
|
+
conclusion: null
|
|
977
|
+
};
|
|
978
|
+
default: return {
|
|
979
|
+
status: status.trim().toLowerCase() || "unknown",
|
|
980
|
+
conclusion: null
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* A pipeline's name on the page: its `name` (`workflow:name`), else
|
|
986
|
+
* `Pipeline #<iid>`. The runs list and the check rollup both use it, so the
|
|
987
|
+
* rollup's jobs group under their pipeline.
|
|
988
|
+
*/
|
|
989
|
+
function gitLabPipelineRunName(pipeline) {
|
|
990
|
+
return trimmed(pipeline.name) ?? `Pipeline #${pipeline.iid ?? pipeline.id}`;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* @param sourceHeadOid The merge request head the pipeline verifies when it
|
|
994
|
+
* ran on a merged-results or merge-train commit (see `sourceHeadOid`).
|
|
995
|
+
*/
|
|
996
|
+
function gitLabWorkflowRun(pipeline, sourceHeadOid) {
|
|
997
|
+
const sha = trimmed(pipeline.sha);
|
|
998
|
+
const url = trimmed(pipeline.web_url);
|
|
999
|
+
if (!sha || !url) return null;
|
|
1000
|
+
const state = gitLabCheckState(pipeline.status);
|
|
1001
|
+
const event = trimmed(pipeline.source);
|
|
1002
|
+
const source = trimmed(sourceHeadOid);
|
|
1003
|
+
return {
|
|
1004
|
+
provider: "gitlab",
|
|
1005
|
+
runId: String(pipeline.id),
|
|
1006
|
+
workflowName: gitLabPipelineRunName(pipeline),
|
|
1007
|
+
branch: optionString(pipeline.ref),
|
|
1008
|
+
...event ? { event } : {},
|
|
1009
|
+
commit: {
|
|
1010
|
+
oid: sha,
|
|
1011
|
+
shortOid: sha.slice(0, 8)
|
|
1012
|
+
},
|
|
1013
|
+
...source && source !== sha ? { sourceHeadOid: source } : {},
|
|
1014
|
+
actor: optionString(pipeline.user?.username),
|
|
1015
|
+
status: state.status,
|
|
1016
|
+
conclusion: optionString(state.conclusion),
|
|
1017
|
+
startedAt: optionTime(pipeline.started_at ?? pipeline.created_at),
|
|
1018
|
+
updatedAt: optionTime(pipeline.updated_at ?? pipeline.finished_at),
|
|
1019
|
+
durationMs: durationMs(pipeline.duration),
|
|
1020
|
+
url
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* The merge request's pipelines for its current head: those on the head
|
|
1025
|
+
* commit plus the head pipeline itself, whose SHA is the merged-results or
|
|
1026
|
+
* merge-train commit rather than the head.
|
|
1027
|
+
*/
|
|
1028
|
+
function selectGitLabHeadPipelines(pipelines, head) {
|
|
1029
|
+
return pipelines.filter((pipeline) => head.headSha !== null && pipeline.sha === head.headSha || head.headPipelineId !== null && pipeline.id === head.headPipelineId);
|
|
1030
|
+
}
|
|
1031
|
+
function gitLabWorkflowJob(job) {
|
|
1032
|
+
const state = gitLabCheckState(job.status, job.allow_failure === true);
|
|
1033
|
+
const stage = trimmed(job.stage);
|
|
1034
|
+
const name = trimmed(job.name) ?? `Job ${job.id}`;
|
|
1035
|
+
return {
|
|
1036
|
+
jobId: String(job.id),
|
|
1037
|
+
name: stage ? `${stage} / ${name}` : name,
|
|
1038
|
+
status: state.status,
|
|
1039
|
+
conclusion: optionString(state.conclusion),
|
|
1040
|
+
startedAt: optionTime(job.started_at),
|
|
1041
|
+
completedAt: optionTime(job.finished_at),
|
|
1042
|
+
durationMs: durationMs(job.duration),
|
|
1043
|
+
url: optionString(job.web_url),
|
|
1044
|
+
steps: []
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* The newest status per name (retries replace earlier attempts). GitLab lists
|
|
1049
|
+
* every CI job as a commit status too (a job is a `CommitStatus` row with the
|
|
1050
|
+
* same id), so the head pipeline's jobs become check runs of that pipeline,
|
|
1051
|
+
* named and linked as its jobs list names them; only what is left (external
|
|
1052
|
+
* statuses, trigger jobs, other pipelines) stays a status context. Without the
|
|
1053
|
+
* head pipeline's jobs, every status is a status context.
|
|
1054
|
+
*/
|
|
1055
|
+
function gitLabCheckRollup(statuses, headPipeline) {
|
|
1056
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1057
|
+
for (const status of statuses) {
|
|
1058
|
+
const name = trimmed(status.name);
|
|
1059
|
+
if (!name) continue;
|
|
1060
|
+
const existing = latest.get(name);
|
|
1061
|
+
if (!existing || existing.id < status.id) latest.set(name, status);
|
|
1062
|
+
}
|
|
1063
|
+
const jobsById = new Map((headPipeline?.jobs ?? []).map((job) => [job.id, job]));
|
|
1064
|
+
return [...latest.values()].toSorted((left, right) => left.id - right.id).map((status) => {
|
|
1065
|
+
const state = gitLabCheckState(status.status, status.allow_failure === true);
|
|
1066
|
+
const job = jobsById.get(status.id);
|
|
1067
|
+
const statusUrl = optionString(status.target_url);
|
|
1068
|
+
const startedAt = optionTime(status.started_at ?? status.created_at);
|
|
1069
|
+
const completedAt = optionTime(status.finished_at);
|
|
1070
|
+
if (job && headPipeline) {
|
|
1071
|
+
const mapped = gitLabWorkflowJob(job);
|
|
1072
|
+
return {
|
|
1073
|
+
kind: "check-run",
|
|
1074
|
+
name: mapped.name,
|
|
1075
|
+
workflowName: headPipeline.runName,
|
|
1076
|
+
status: Option.some(state.status),
|
|
1077
|
+
conclusion: optionString(state.conclusion),
|
|
1078
|
+
url: Option.orElse(mapped.url, () => statusUrl),
|
|
1079
|
+
startedAt,
|
|
1080
|
+
completedAt
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
return {
|
|
1084
|
+
kind: "status-context",
|
|
1085
|
+
name: status.name.trim(),
|
|
1086
|
+
status: Option.some(state.status),
|
|
1087
|
+
conclusion: optionString(state.conclusion),
|
|
1088
|
+
url: statusUrl,
|
|
1089
|
+
startedAt,
|
|
1090
|
+
completedAt
|
|
1091
|
+
};
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* A list row's pipeline as a one-item rollup: the newest pipeline on the
|
|
1096
|
+
* merge request's refs (`refs/merge-requests/<iid>/…`) or its branch head.
|
|
1097
|
+
*/
|
|
1098
|
+
function gitLabListRowRollup(pipelines, row) {
|
|
1099
|
+
const mergeRequestRef = `refs/merge-requests/${row.iid}/`;
|
|
1100
|
+
const pipeline = pipelines.filter((candidate) => candidate.ref?.startsWith(mergeRequestRef) === true || candidate.ref === row.sourceBranch && row.headSha !== null && candidate.sha === row.headSha).toSorted((left, right) => right.id - left.id)[0];
|
|
1101
|
+
if (!pipeline) return null;
|
|
1102
|
+
const state = gitLabCheckState(pipeline.status);
|
|
1103
|
+
return [{
|
|
1104
|
+
kind: "unknown",
|
|
1105
|
+
name: trimmed(pipeline.name) ?? "Pipeline",
|
|
1106
|
+
status: Option.some(state.status),
|
|
1107
|
+
conclusion: optionString(state.conclusion),
|
|
1108
|
+
url: optionString(pipeline.web_url),
|
|
1109
|
+
startedAt: optionTime(pipeline.started_at ?? pipeline.created_at),
|
|
1110
|
+
completedAt: optionTime(pipeline.finished_at)
|
|
1111
|
+
}];
|
|
1112
|
+
}
|
|
1113
|
+
const SECTION_MARKER = /section_(?:start|end):\d+:[^\r\n\u001b]*(?:\r?\u001b\[0K)?/gu;
|
|
1114
|
+
const ANSI_ESCAPE = /\u001b\[[0-9;?]*[A-Za-z]/gu;
|
|
1115
|
+
/**
|
|
1116
|
+
* Plain text, newest output kept: the end of a log is where a job failed.
|
|
1117
|
+
* `startCut`: the raw log is already only its tail, so its first line may
|
|
1118
|
+
* begin inside a section marker or escape and is dropped.
|
|
1119
|
+
*/
|
|
1120
|
+
function gitLabJobLogTail(raw, options) {
|
|
1121
|
+
const startCut = options?.startCut === true;
|
|
1122
|
+
const newline = startCut ? raw.indexOf("\n") : -1;
|
|
1123
|
+
const plain = (startCut ? newline >= 0 ? raw.slice(newline + 1) : "" : raw).replace(SECTION_MARKER, "").replace(ANSI_ESCAPE, "").replace(/\r\n/gu, "\n");
|
|
1124
|
+
const bytes = Buffer.from(plain, "utf8");
|
|
1125
|
+
if (bytes.byteLength <= 204800) return {
|
|
1126
|
+
log: plain,
|
|
1127
|
+
truncated: startCut
|
|
1128
|
+
};
|
|
1129
|
+
let tail = bytes.subarray(bytes.byteLength - SOURCE_CONTROL_WORKFLOW_LOG_MAX_BYTES);
|
|
1130
|
+
let offset = 0;
|
|
1131
|
+
while (offset < tail.length && ((tail[offset] ?? 0) & 192) === 128) offset += 1;
|
|
1132
|
+
tail = tail.subarray(offset);
|
|
1133
|
+
return {
|
|
1134
|
+
log: tail.toString("utf8"),
|
|
1135
|
+
truncated: true
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
//#endregion
|
|
1139
|
+
//#region src/sourceControl/gitLabMergeRequestPage.ts
|
|
1140
|
+
/** List pages can carry long bodies; the process default (1 MB) is too small. */
|
|
1141
|
+
const LIST_PAGE_MAX_BYTES = 16777216;
|
|
1142
|
+
const DIFF_PAGE_MAX_BYTES = 33554432;
|
|
1143
|
+
const DIFF_TIMEOUT_MS = 6e4;
|
|
1144
|
+
/** Files read for a whole merge request diff (100 per page). */
|
|
1145
|
+
const DIFF_MAX_PAGES = 30;
|
|
1146
|
+
/** Files read to anchor review comments / excerpt thread hunks. */
|
|
1147
|
+
const ACTIVITY_DIFF_MAX_PAGES = 3;
|
|
1148
|
+
const COMMIT_PAGES = 3;
|
|
1149
|
+
/**
|
|
1150
|
+
* Raw bytes kept from the end of a job log: room for the shown tail
|
|
1151
|
+
* (`SOURCE_CONTROL_WORKFLOW_LOG_MAX_BYTES`) once colours and section markers
|
|
1152
|
+
* are stripped. Longer logs stream through and only their end is kept.
|
|
1153
|
+
*/
|
|
1154
|
+
const JOB_LOG_TAIL_BYTES = 8 * SOURCE_CONTROL_WORKFLOW_LOG_MAX_BYTES;
|
|
1155
|
+
/** First GitLab release whose `bulk_publish` records `reviewer_state` (verify). */
|
|
1156
|
+
const REQUEST_CHANGES_MIN_VERSION = {
|
|
1157
|
+
major: 19,
|
|
1158
|
+
minor: 2
|
|
1159
|
+
};
|
|
1160
|
+
function fail(operation, detail, status) {
|
|
1161
|
+
return new GitLabCliError({
|
|
1162
|
+
operation,
|
|
1163
|
+
detail,
|
|
1164
|
+
...status !== void 0 ? { status } : {}
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
function gitLabStaleHeadError(operation) {
|
|
1168
|
+
return fail(operation, "The merge request's head changed since it was loaded. Refresh and review the new commits, then try again.", 409);
|
|
1169
|
+
}
|
|
1170
|
+
function isStatus(status) {
|
|
1171
|
+
return (error) => error.status === status;
|
|
1172
|
+
}
|
|
1173
|
+
function requireGitLabMergeRequestRef(operation, reference) {
|
|
1174
|
+
const ref = parseGitLabMergeRequestReference(reference);
|
|
1175
|
+
return ref ? Effect.succeed(ref) : Effect.fail(fail(operation, `"${reference}" is not a merge request number or merge request URL.`));
|
|
1176
|
+
}
|
|
1177
|
+
function decodeJson(operation, schema, raw) {
|
|
1178
|
+
return Schema.decodeEffect(Schema.fromJsonString(schema))(raw.trim()).pipe(Effect.mapError((error) => fail(operation, `GitLab returned an unexpected response: ${SchemaIssue.makeFormatterDefault()(error.issue)}`)));
|
|
1179
|
+
}
|
|
1180
|
+
const decodeUnknownArray = Schema.fromJsonString(Schema.Array(Schema.Unknown));
|
|
1181
|
+
/** A JSON array, keeping the entries `item` decodes (one odd entry must not hide the rest). */
|
|
1182
|
+
function decodeList(operation, item, raw) {
|
|
1183
|
+
const decodeItem = Schema.decodeUnknownExit(item);
|
|
1184
|
+
return Schema.decodeEffect(decodeUnknownArray)(raw.trim() || "[]").pipe(Effect.mapError((error) => fail(operation, `GitLab returned an unexpected list: ${SchemaIssue.makeFormatterDefault()(error.issue)}`)), Effect.map((entries) => entries.flatMap((entry) => {
|
|
1185
|
+
const decoded = decodeItem(entry);
|
|
1186
|
+
return Exit.isSuccess(decoded) ? [decoded.value] : [];
|
|
1187
|
+
})));
|
|
1188
|
+
}
|
|
1189
|
+
function getJson(call, operation, request, schema) {
|
|
1190
|
+
return call(operation, request, { maxOutputBytes: LIST_PAGE_MAX_BYTES }).pipe(Effect.flatMap((output) => decodeJson(operation, schema, output.stdout)));
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Read up to `maxPages` pages of a list endpoint. A short page ends the
|
|
1194
|
+
* list; a full last page means more exist (`truncated`).
|
|
1195
|
+
*/
|
|
1196
|
+
function getGitLabPages(call, operation, input) {
|
|
1197
|
+
const perPage = input.perPage ?? 100;
|
|
1198
|
+
return Effect.gen(function* () {
|
|
1199
|
+
const items = [];
|
|
1200
|
+
for (let page = 1; page <= input.maxPages; page += 1) {
|
|
1201
|
+
const raw = (yield* call(operation, gitLabGet(input.ref, withGitLabQuery(input.endpoint, {
|
|
1202
|
+
per_page: perPage,
|
|
1203
|
+
page
|
|
1204
|
+
})), {
|
|
1205
|
+
maxOutputBytes: input.maxOutputBytes ?? LIST_PAGE_MAX_BYTES,
|
|
1206
|
+
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {}
|
|
1207
|
+
})).stdout.trim() || "[]";
|
|
1208
|
+
const count = yield* Schema.decodeEffect(decodeUnknownArray)(raw).pipe(Effect.map((entries) => entries.length), Effect.mapError(() => fail(operation, "GitLab returned an unexpected list.")));
|
|
1209
|
+
items.push(...yield* decodeList(operation, input.item, raw));
|
|
1210
|
+
if (isLastGitLabPage(count, perPage)) return {
|
|
1211
|
+
items,
|
|
1212
|
+
truncated: false
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
return {
|
|
1216
|
+
items,
|
|
1217
|
+
truncated: true
|
|
1218
|
+
};
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
function getGitLabMergeRequestFacts(call, operation, ref) {
|
|
1222
|
+
return getJson(call, operation, gitLabGet(ref, gitLabMergeRequestEndpoint(ref)), GitLabMergeRequestDetailSchema).pipe(Effect.map(gitLabMergeRequestFacts));
|
|
1223
|
+
}
|
|
1224
|
+
function getCurrentUser(call, operation, ref) {
|
|
1225
|
+
return getJson(call, operation, gitLabGet(ref, "user"), GitLabCurrentUserSchema);
|
|
1226
|
+
}
|
|
1227
|
+
function getProject(call, operation, ref) {
|
|
1228
|
+
return getJson(call, operation, gitLabGet(ref, gitLabProjectEndpoint(ref)), GitLabProjectSchema);
|
|
1229
|
+
}
|
|
1230
|
+
/** A best-effort read: a failure is logged and reads as `fallback`. */
|
|
1231
|
+
function optional(effect, fallback) {
|
|
1232
|
+
return effect.pipe(Effect.catch((error) => Effect.logWarning("GitLab read failed; continuing without it.", {
|
|
1233
|
+
operation: error.operation,
|
|
1234
|
+
detail: error.detail
|
|
1235
|
+
}).pipe(Effect.as(fallback))));
|
|
1236
|
+
}
|
|
1237
|
+
function getDiffEntries(call, operation, ref, maxPages) {
|
|
1238
|
+
return getGitLabPages(call, operation, {
|
|
1239
|
+
ref,
|
|
1240
|
+
endpoint: gitLabMergeRequestEndpoint(ref, "/diffs"),
|
|
1241
|
+
item: GitLabDiffEntrySchema,
|
|
1242
|
+
maxPages,
|
|
1243
|
+
maxOutputBytes: DIFF_PAGE_MAX_BYTES,
|
|
1244
|
+
timeoutMs: DIFF_TIMEOUT_MS
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
function hasActiveLineThread(discussions, headSha) {
|
|
1248
|
+
return discussions.some((discussion) => {
|
|
1249
|
+
const position = discussion.notes.find((note) => note.system !== true)?.position;
|
|
1250
|
+
return position?.position_type !== "file" && position !== void 0 && position !== null && (headSha === null || position.head_sha === headSha);
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
function fetchGitLabChangeRequestActivity(call, reference) {
|
|
1254
|
+
const operation = "getChangeRequestActivity";
|
|
1255
|
+
return Effect.gen(function* () {
|
|
1256
|
+
const ref = yield* requireGitLabMergeRequestRef(operation, reference);
|
|
1257
|
+
const pages = (suffix, item, maxPages) => getGitLabPages(call, operation, {
|
|
1258
|
+
ref,
|
|
1259
|
+
endpoint: gitLabMergeRequestEndpoint(ref, suffix),
|
|
1260
|
+
item,
|
|
1261
|
+
maxPages
|
|
1262
|
+
});
|
|
1263
|
+
const read = yield* Effect.all({
|
|
1264
|
+
facts: getGitLabMergeRequestFacts(call, operation, ref),
|
|
1265
|
+
user: optional(getCurrentUser(call, operation, ref), null),
|
|
1266
|
+
project: optional(getProject(call, operation, ref), null),
|
|
1267
|
+
notes: pages(withGitLabQuery("/notes", {
|
|
1268
|
+
sort: "desc",
|
|
1269
|
+
order_by: "created_at"
|
|
1270
|
+
}), GitLabNoteSchema, 3),
|
|
1271
|
+
discussions: pages("/discussions", GitLabDiscussionSchema, 3),
|
|
1272
|
+
stateEvents: optional(pages("/resource_state_events", GitLabStateEventSchema, 1), {
|
|
1273
|
+
items: [],
|
|
1274
|
+
truncated: false
|
|
1275
|
+
}),
|
|
1276
|
+
labelEvents: optional(pages("/resource_label_events", GitLabLabelEventSchema, 1), {
|
|
1277
|
+
items: [],
|
|
1278
|
+
truncated: false
|
|
1279
|
+
}),
|
|
1280
|
+
commits: optional(pages("/commits", GitLabCommitSchema, 1), {
|
|
1281
|
+
items: [],
|
|
1282
|
+
truncated: false
|
|
1283
|
+
}),
|
|
1284
|
+
versions: optional(pages("/versions", GitLabVersionSchema, 1), {
|
|
1285
|
+
items: [],
|
|
1286
|
+
truncated: false
|
|
1287
|
+
}),
|
|
1288
|
+
draftNotes: optional(pages("/draft_notes", GitLabDraftNoteSchema, 1).pipe(Effect.map((page) => page.items)), null)
|
|
1289
|
+
}, { concurrency: 6 });
|
|
1290
|
+
const diffEntries = hasActiveLineThread(read.discussions.items, read.facts.headSha) ? yield* optional(getDiffEntries(call, operation, ref, ACTIVITY_DIFF_MAX_PAGES).pipe(Effect.map((page) => page.items)), null) : null;
|
|
1291
|
+
return assembleGitLabChangeRequestActivity({
|
|
1292
|
+
facts: read.facts,
|
|
1293
|
+
user: read.user,
|
|
1294
|
+
project: read.project,
|
|
1295
|
+
notes: read.notes,
|
|
1296
|
+
discussions: read.discussions,
|
|
1297
|
+
stateEvents: read.stateEvents.items,
|
|
1298
|
+
labelEvents: read.labelEvents.items,
|
|
1299
|
+
commits: read.commits,
|
|
1300
|
+
versions: read.versions.items,
|
|
1301
|
+
draftNotes: read.draftNotes,
|
|
1302
|
+
diffEntries
|
|
1303
|
+
});
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
/** One discussion re-read as a review thread (after a reply). */
|
|
1307
|
+
function fetchGitLabReviewThread(call, operation, ref, discussionId) {
|
|
1308
|
+
return Effect.gen(function* () {
|
|
1309
|
+
const read = yield* Effect.all({
|
|
1310
|
+
discussion: getJson(call, operation, gitLabGet(ref, gitLabMergeRequestEndpoint(ref, `/discussions/${discussionId}`)), GitLabDiscussionSchema),
|
|
1311
|
+
facts: getGitLabMergeRequestFacts(call, operation, ref),
|
|
1312
|
+
user: optional(getCurrentUser(call, operation, ref), null),
|
|
1313
|
+
project: optional(getProject(call, operation, ref), null)
|
|
1314
|
+
}, { concurrency: 4 });
|
|
1315
|
+
const diffEntries = hasActiveLineThread([read.discussion], read.facts.headSha) ? yield* optional(getDiffEntries(call, operation, ref, ACTIVITY_DIFF_MAX_PAGES).pipe(Effect.map((page) => page.items)), null) : null;
|
|
1316
|
+
const viewer = gitLabViewerCapabilities(read);
|
|
1317
|
+
const entry = gitLabReviewThread(read.discussion, {
|
|
1318
|
+
mergeRequestUrl: read.facts.webUrl,
|
|
1319
|
+
headSha: read.facts.headSha,
|
|
1320
|
+
viewerId: read.user?.id ?? null,
|
|
1321
|
+
viewerCanReply: viewer !== null && viewer.canReview,
|
|
1322
|
+
viewerCanResolve: viewer !== null && (viewer.isAuthor || gitLabAccessLevel(read.project) >= 30),
|
|
1323
|
+
diffEntries
|
|
1324
|
+
});
|
|
1325
|
+
if (!entry) return yield* fail(operation, "This discussion is not a review thread on the diff.");
|
|
1326
|
+
return entry.thread;
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
/** The merge request's commit SHAs (newest first), bounded. */
|
|
1330
|
+
function listCommitShas(call, operation, ref) {
|
|
1331
|
+
return getGitLabPages(call, operation, {
|
|
1332
|
+
ref,
|
|
1333
|
+
endpoint: gitLabMergeRequestEndpoint(ref, "/commits"),
|
|
1334
|
+
item: GitLabCommitSchema,
|
|
1335
|
+
maxPages: COMMIT_PAGES
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* The whole merge request diff (merge base → head, as GitLab shows it) or
|
|
1340
|
+
* one of its commits against its parent, as a unified diff. With
|
|
1341
|
+
* `expectedHeadSha`, a head that moves before or during the read fails.
|
|
1342
|
+
*/
|
|
1343
|
+
function fetchGitLabChangeRequestDiff(call, input) {
|
|
1344
|
+
const operation = "getChangeRequestDiff";
|
|
1345
|
+
return Effect.gen(function* () {
|
|
1346
|
+
const ref = yield* requireGitLabMergeRequestRef(operation, input.reference);
|
|
1347
|
+
const verifyHead = () => getGitLabMergeRequestFacts(call, operation, ref).pipe(Effect.flatMap((facts) => facts.headSha === input.expectedHeadSha ? Effect.void : Effect.fail(gitLabStaleHeadError(operation))));
|
|
1348
|
+
if (input.expectedHeadSha) yield* verifyHead();
|
|
1349
|
+
const commitSha = input.commitSha?.trim();
|
|
1350
|
+
let entries;
|
|
1351
|
+
if (commitSha) {
|
|
1352
|
+
const commits = yield* listCommitShas(call, operation, ref);
|
|
1353
|
+
const wanted = commitSha.toLowerCase();
|
|
1354
|
+
const matches = commits.items.filter((commit) => wanted.length >= 7 ? commit.id.toLowerCase().startsWith(wanted) : commit.id.toLowerCase() === wanted);
|
|
1355
|
+
const commit = matches.length === 1 ? matches[0] : void 0;
|
|
1356
|
+
if (!commit || !isGitSha(commit.id)) return yield* fail(operation, `Commit ${commitSha} is not part of merge request !${ref.iid}.`);
|
|
1357
|
+
entries = yield* getGitLabPages(call, operation, {
|
|
1358
|
+
ref,
|
|
1359
|
+
endpoint: gitLabProjectEndpoint(ref, `/repository/commits/${commit.id}/diff`),
|
|
1360
|
+
item: GitLabDiffEntrySchema,
|
|
1361
|
+
maxPages: DIFF_MAX_PAGES,
|
|
1362
|
+
maxOutputBytes: DIFF_PAGE_MAX_BYTES,
|
|
1363
|
+
timeoutMs: DIFF_TIMEOUT_MS
|
|
1364
|
+
});
|
|
1365
|
+
} else entries = yield* getDiffEntries(call, operation, ref, DIFF_MAX_PAGES);
|
|
1366
|
+
if (entries.truncated) return yield* fail(operation, "This diff has too many files to display.");
|
|
1367
|
+
if (input.expectedHeadSha) yield* verifyHead();
|
|
1368
|
+
return buildGitLabUnifiedDiff(entries.items);
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
function isRepositoryFilePath(path) {
|
|
1372
|
+
return path.length > 0 && !path.startsWith("/") && !path.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
|
|
1373
|
+
}
|
|
1374
|
+
function fetchFileAtRevision(call, operation, ref, path, revision) {
|
|
1375
|
+
return call(operation, gitLabGet(ref, withGitLabQuery(gitLabProjectEndpoint(ref, `/repository/files/${encodeURIComponent(path)}/raw`), { ref: revision })), {
|
|
1376
|
+
maxOutputBytes: CHANGE_REQUEST_FILE_CONTENTS_MAX_BYTES,
|
|
1377
|
+
truncateOutputAtMaxBytes: true
|
|
1378
|
+
}).pipe(Effect.map((output) => output.stdout.includes("\0") ? {
|
|
1379
|
+
contents: null,
|
|
1380
|
+
truncated: false
|
|
1381
|
+
} : {
|
|
1382
|
+
contents: output.stdout,
|
|
1383
|
+
truncated: output.stdoutTruncated
|
|
1384
|
+
}), Effect.catchIf(isStatus(404), () => Effect.succeed({
|
|
1385
|
+
contents: null,
|
|
1386
|
+
truncated: false
|
|
1387
|
+
})));
|
|
1388
|
+
}
|
|
1389
|
+
function fetchGitLabFileContents(call, input) {
|
|
1390
|
+
const operation = "getChangeRequestFileContents";
|
|
1391
|
+
return Effect.gen(function* () {
|
|
1392
|
+
for (const path of [input.path, input.previousPath]) if (path !== void 0 && !isRepositoryFilePath(path)) return yield* fail(operation, `Invalid repository file path: ${path}`);
|
|
1393
|
+
for (const sha of [input.headSha, input.baseSha]) if (sha !== void 0 && !isGitSha(sha)) return yield* fail(operation, `Invalid commit SHA: ${sha}`);
|
|
1394
|
+
const ref = yield* requireGitLabMergeRequestRef(operation, input.reference);
|
|
1395
|
+
let baseSha = input.baseSha;
|
|
1396
|
+
if (!baseSha) {
|
|
1397
|
+
const facts = yield* getGitLabMergeRequestFacts(call, operation, ref);
|
|
1398
|
+
baseSha = facts.diffRefs && facts.diffRefs.head_sha === input.headSha ? facts.diffRefs.base_sha : (yield* getJson(call, operation, gitLabGet(ref, withGitLabQuery(gitLabProjectEndpoint(ref, "/repository/merge_base"), { refs: [facts.targetBranch, input.headSha] })), GitLabCommitSchema)).id;
|
|
1399
|
+
}
|
|
1400
|
+
const [oldSide, newSide] = yield* Effect.all([fetchFileAtRevision(call, operation, ref, input.previousPath ?? input.path, baseSha), fetchFileAtRevision(call, operation, ref, input.path, input.headSha)], { concurrency: 2 });
|
|
1401
|
+
return {
|
|
1402
|
+
path: input.path,
|
|
1403
|
+
oldContents: oldSide.contents,
|
|
1404
|
+
newContents: newSide.contents,
|
|
1405
|
+
truncated: oldSide.truncated || newSide.truncated
|
|
1406
|
+
};
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
function parseGitLabVersion(version) {
|
|
1410
|
+
const match = /^(\d+)\.(\d+)/u.exec(version.trim());
|
|
1411
|
+
return match?.[1] && match[2] ? {
|
|
1412
|
+
major: Number(match[1]),
|
|
1413
|
+
minor: Number(match[2])
|
|
1414
|
+
} : null;
|
|
1415
|
+
}
|
|
1416
|
+
function gitLabSupportsReviewerState(version) {
|
|
1417
|
+
const parsed = parseGitLabVersion(version);
|
|
1418
|
+
if (!parsed) return false;
|
|
1419
|
+
return parsed.major > REQUEST_CHANGES_MIN_VERSION.major || parsed.major === REQUEST_CHANGES_MIN_VERSION.major && parsed.minor >= REQUEST_CHANGES_MIN_VERSION.minor;
|
|
1420
|
+
}
|
|
1421
|
+
const GitLabVersionInfoSchema = Schema.Struct({ version: Schema.String });
|
|
1422
|
+
const GitLabCreatedIdSchema = Schema.Struct({ id: Schema.Number });
|
|
1423
|
+
/**
|
|
1424
|
+
* A review is GitLab's pending-draft flow: each comment (and the summary) is
|
|
1425
|
+
* created as a draft note on the reviewed head, then `bulk_publish` publishes
|
|
1426
|
+
* them together with any drafts the viewer started on the web. An approval
|
|
1427
|
+
* carries `sha`, so GitLab refuses it (409) when the head moved.
|
|
1428
|
+
*/
|
|
1429
|
+
function submitGitLabReview(call, input) {
|
|
1430
|
+
const operation = "submitChangeRequestReview";
|
|
1431
|
+
return Effect.gen(function* () {
|
|
1432
|
+
const ref = yield* requireGitLabMergeRequestRef(operation, input.reference);
|
|
1433
|
+
const facts = yield* getGitLabMergeRequestFacts(call, operation, ref);
|
|
1434
|
+
if (facts.headSha !== input.expectedHeadSha) return yield* gitLabStaleHeadError(operation);
|
|
1435
|
+
if (input.event === "request_changes") {
|
|
1436
|
+
const version = yield* getJson(call, operation, gitLabGet(ref, "version"), GitLabVersionInfoSchema);
|
|
1437
|
+
if (!gitLabSupportsReviewerState(version.version)) return yield* fail(operation, `This GitLab instance (${version.version}) cannot record "request changes" through its API; it needs GitLab ${REQUEST_CHANGES_MIN_VERSION.major}.${REQUEST_CHANGES_MIN_VERSION.minor} or later.`);
|
|
1438
|
+
}
|
|
1439
|
+
const bodies = [];
|
|
1440
|
+
if (input.comments.length > 0) {
|
|
1441
|
+
const diffRefs = facts.diffRefs;
|
|
1442
|
+
if (!diffRefs) return yield* fail(operation, "GitLab has not computed this merge request's diff yet.");
|
|
1443
|
+
const entries = yield* getDiffEntries(call, operation, ref, DIFF_MAX_PAGES);
|
|
1444
|
+
for (const comment of input.comments) {
|
|
1445
|
+
const built = buildGitLabDraftNoteBody({
|
|
1446
|
+
comment,
|
|
1447
|
+
diffRefs,
|
|
1448
|
+
entries: entries.items
|
|
1449
|
+
});
|
|
1450
|
+
if (Result.isFailure(built)) return yield* fail(operation, built.failure);
|
|
1451
|
+
bodies.push(built.success);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if ((input.body?.trim() ?? "").length > 0) bodies.push({ note: input.body });
|
|
1455
|
+
const existing = yield* getGitLabPages(call, operation, {
|
|
1456
|
+
ref,
|
|
1457
|
+
endpoint: gitLabMergeRequestEndpoint(ref, "/draft_notes"),
|
|
1458
|
+
item: GitLabDraftNoteSchema,
|
|
1459
|
+
maxPages: 1
|
|
1460
|
+
}).pipe(Effect.map((page) => page.items.length));
|
|
1461
|
+
if (input.event === "comment" && bodies.length === 0 && existing === 0) return yield* fail(operation, "Add a comment before submitting a review.");
|
|
1462
|
+
const created = [];
|
|
1463
|
+
const discardCreated = Effect.suspend(() => Effect.forEach([...created], (id) => call(operation, gitLabWrite(ref, "DELETE", gitLabMergeRequestEndpoint(ref, `/draft_notes/${id}`))).pipe(Effect.ignore), { discard: true }));
|
|
1464
|
+
yield* Effect.forEach(bodies, (body) => call(operation, gitLabWrite(ref, "POST", gitLabMergeRequestEndpoint(ref, "/draft_notes"), body)).pipe(Effect.flatMap((output) => decodeJson(operation, GitLabCreatedIdSchema, output.stdout)), Effect.tap((draft) => Effect.sync(() => created.push(draft.id)))), { discard: true }).pipe(Effect.tapError(() => discardCreated));
|
|
1465
|
+
if (input.event === "approve") yield* call(operation, gitLabWrite(ref, "POST", gitLabMergeRequestEndpoint(ref, "/approve"), { sha: input.expectedHeadSha })).pipe(Effect.mapError((error) => error.status === 409 ? gitLabStaleHeadError(operation) : error.status === 401 || error.status === 403 ? fail(operation, "GitLab did not accept your approval: you are not an eligible approver for this merge request.", error.status) : error), Effect.tapError(() => discardCreated));
|
|
1466
|
+
if (created.length + existing > 0 || input.event === "request_changes") yield* call(operation, gitLabWrite(ref, "POST", gitLabMergeRequestEndpoint(ref, "/draft_notes/bulk_publish"), input.event === "request_changes" ? { reviewer_state: "requested_changes" } : void 0)).pipe(Effect.mapError((error) => input.event === "approve" ? fail(operation, `Approved, but publishing the review comments failed; they are kept as pending drafts. ${error.detail}`, error.status) : error));
|
|
1467
|
+
if (input.event === "request_changes") yield* verifyGitLabChangesRequested(call, operation, ref);
|
|
1468
|
+
const lastId = created.at(-1);
|
|
1469
|
+
return {
|
|
1470
|
+
reviewId: lastId !== void 0 ? `draft-note:${lastId}` : input.event === "approve" ? `approval:${input.expectedHeadSha}` : "draft-notes",
|
|
1471
|
+
state: input.event === "approve" ? "approved" : input.event === "request_changes" ? "changes_requested" : "commented",
|
|
1472
|
+
url: facts.webUrl
|
|
1473
|
+
};
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* `bulk_publish` answers 204 even when GitLab's `UpdateReviewerStateService`
|
|
1478
|
+
* refused the state: the viewer cannot update the merge request (a Reporter,
|
|
1479
|
+
* an external contributor), or is not a reviewer and cannot be added (Free
|
|
1480
|
+
* allows one reviewer). The reviewers are re-read so a refused change request
|
|
1481
|
+
* is reported instead of claimed. When that read fails the outcome is unknown
|
|
1482
|
+
* and the submission stands; the next activity read shows what GitLab kept.
|
|
1483
|
+
*/
|
|
1484
|
+
function verifyGitLabChangesRequested(call, operation, ref) {
|
|
1485
|
+
return Effect.gen(function* () {
|
|
1486
|
+
const read = yield* Effect.all({
|
|
1487
|
+
viewer: getCurrentUser(call, operation, ref),
|
|
1488
|
+
reviewers: getJson(call, operation, gitLabGet(ref, gitLabMergeRequestEndpoint(ref, "/reviewers")), Schema.Array(GitLabReviewerSchema))
|
|
1489
|
+
}, { concurrency: 2 }).pipe(Effect.option);
|
|
1490
|
+
if (Option.isNone(read)) return;
|
|
1491
|
+
const { viewer, reviewers } = read.value;
|
|
1492
|
+
if (reviewers.find((reviewer) => reviewer.user.id === viewer.id || reviewer.user.username?.toLowerCase() === viewer.username.toLowerCase())?.state?.trim().toLowerCase() === "requested_changes") return;
|
|
1493
|
+
return yield* fail(operation, "Your review was published, but GitLab did not record your change request: you need to be a reviewer of this merge request with permission to update it.");
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
function replyToGitLabThread(call, input) {
|
|
1497
|
+
const operation = "replyToReviewThread";
|
|
1498
|
+
return Effect.gen(function* () {
|
|
1499
|
+
const ref = yield* requireGitLabMergeRequestRef(operation, input.reference);
|
|
1500
|
+
if (!isGitLabDiscussionId(input.threadId)) return yield* fail(operation, `Unknown review thread: ${input.threadId}`);
|
|
1501
|
+
yield* call(operation, gitLabWrite(ref, "POST", gitLabMergeRequestEndpoint(ref, `/discussions/${input.threadId}/notes`), { body: input.body }));
|
|
1502
|
+
return yield* fetchGitLabReviewThread(call, operation, ref, input.threadId);
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
function setGitLabThreadResolved(call, input) {
|
|
1506
|
+
const operation = "setReviewThreadResolved";
|
|
1507
|
+
return Effect.gen(function* () {
|
|
1508
|
+
const ref = yield* requireGitLabMergeRequestRef(operation, input.reference);
|
|
1509
|
+
if (!isGitLabDiscussionId(input.threadId)) return yield* fail(operation, `Unknown review thread: ${input.threadId}`);
|
|
1510
|
+
const output = yield* call(operation, gitLabWrite(ref, "PUT", withGitLabQuery(gitLabMergeRequestEndpoint(ref, `/discussions/${input.threadId}`), { resolved: input.resolved })));
|
|
1511
|
+
const discussion = yield* decodeJson(operation, GitLabDiscussionSchema, output.stdout);
|
|
1512
|
+
const resolvable = discussion.notes.filter((note) => note.resolvable === true);
|
|
1513
|
+
const isResolved = resolvable.length > 0 ? resolvable.every((note) => note.resolved === true) : discussion.resolved === true;
|
|
1514
|
+
const resolvedBy = isResolved ? resolvable.map((note) => note.resolved_by?.username?.trim()).findLast(Boolean) : void 0;
|
|
1515
|
+
return {
|
|
1516
|
+
threadId: discussion.id,
|
|
1517
|
+
isResolved,
|
|
1518
|
+
...resolvedBy ? { resolvedBy } : {}
|
|
1519
|
+
};
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
/** Top-level notes, diff notes, and review summaries are all notes on GitLab. */
|
|
1523
|
+
function updateGitLabComment(call, input) {
|
|
1524
|
+
const operation = "updateChangeRequestComment";
|
|
1525
|
+
return Effect.gen(function* () {
|
|
1526
|
+
const ref = yield* requireGitLabMergeRequestRef(operation, input.reference);
|
|
1527
|
+
if (!isGitLabNumericId(input.commentId)) return yield* fail(operation, `Unknown comment: ${input.commentId}`);
|
|
1528
|
+
const endpoint = gitLabMergeRequestEndpoint(ref, `/notes/${input.commentId}`);
|
|
1529
|
+
if (input.action === "delete") {
|
|
1530
|
+
yield* call(operation, gitLabWrite(ref, "DELETE", endpoint));
|
|
1531
|
+
return {
|
|
1532
|
+
commentId: input.commentId,
|
|
1533
|
+
deleted: true
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
yield* call(operation, gitLabWrite(ref, "PUT", endpoint, { body: input.body }));
|
|
1537
|
+
return {
|
|
1538
|
+
commentId: input.commentId,
|
|
1539
|
+
deleted: false
|
|
1540
|
+
};
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
function addGitLabComment(call, input) {
|
|
1544
|
+
const operation = "addChangeRequestComment";
|
|
1545
|
+
return Effect.gen(function* () {
|
|
1546
|
+
const ref = yield* requireGitLabMergeRequestRef(operation, input.reference);
|
|
1547
|
+
yield* call(operation, gitLabWrite(ref, "POST", gitLabMergeRequestEndpoint(ref, "/notes"), { body: input.body }));
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
const GitLabUserLookupSchema = Schema.Struct({
|
|
1551
|
+
id: Schema.Number,
|
|
1552
|
+
username: Schema.String
|
|
1553
|
+
});
|
|
1554
|
+
function resolveUserIds(call, operation, ref, logins) {
|
|
1555
|
+
return Effect.forEach(logins, (login) => getJson(call, operation, gitLabGet(ref, withGitLabQuery("users", { username: login })), Schema.Array(GitLabUserLookupSchema)).pipe(Effect.flatMap((users) => {
|
|
1556
|
+
const match = users.find((user) => user.username.toLowerCase() === login.trim().toLowerCase());
|
|
1557
|
+
return match ? Effect.succeed(match.id) : Effect.fail(fail(operation, `No GitLab user named @${login}.`));
|
|
1558
|
+
})), { concurrency: 4 });
|
|
1559
|
+
}
|
|
1560
|
+
/** The full id list GitLab's update takes: current minus removed plus added. */
|
|
1561
|
+
function nextGitLabUserIds(input) {
|
|
1562
|
+
const removed = new Set(input.removeLogins.map((login) => login.trim().toLowerCase()));
|
|
1563
|
+
const ids = input.current.flatMap((user) => typeof user.id === "number" && !removed.has(user.username?.trim().toLowerCase() ?? "") ? [user.id] : []);
|
|
1564
|
+
for (const id of input.addIds) if (!ids.includes(id)) ids.push(id);
|
|
1565
|
+
return ids.length > 0 ? ids : [0];
|
|
1566
|
+
}
|
|
1567
|
+
const GitLabCancelAutoMergeSchema = Schema.Struct({
|
|
1568
|
+
status: Schema.optional(Schema.NullOr(Schema.String)),
|
|
1569
|
+
message: Schema.optional(Schema.NullOr(Schema.String))
|
|
1570
|
+
});
|
|
1571
|
+
function deleteSourceBranch(call, operation, ref, facts) {
|
|
1572
|
+
if (facts.isCrossRepository) return Effect.fail(fail(operation, "The source branch lives in a fork, which Ryco does not delete."));
|
|
1573
|
+
return call(operation, gitLabWrite(ref, "DELETE", gitLabProjectEndpoint(ref, `/repository/branches/${encodeURIComponent(facts.sourceBranch)}`))).pipe(Effect.asVoid);
|
|
1574
|
+
}
|
|
1575
|
+
/** Apply one lifecycle action; the provider re-reads the detail afterwards. */
|
|
1576
|
+
function updateGitLabMergeRequest(call, input) {
|
|
1577
|
+
const operation = "updateChangeRequest";
|
|
1578
|
+
return Effect.gen(function* () {
|
|
1579
|
+
const ref = yield* requireGitLabMergeRequestRef(operation, input.reference);
|
|
1580
|
+
const endpoint = gitLabMergeRequestEndpoint(ref);
|
|
1581
|
+
const update = (body) => call(operation, gitLabWrite(ref, "PUT", endpoint, body)).pipe(Effect.asVoid);
|
|
1582
|
+
const action = input.action;
|
|
1583
|
+
switch (action.kind) {
|
|
1584
|
+
case "edit": {
|
|
1585
|
+
const body = {
|
|
1586
|
+
...action.title !== void 0 ? { title: action.title } : {},
|
|
1587
|
+
...action.body !== void 0 ? { description: action.body } : {},
|
|
1588
|
+
...action.baseRefName !== void 0 ? { target_branch: action.baseRefName } : {}
|
|
1589
|
+
};
|
|
1590
|
+
if (Object.keys(body).length === 0) return yield* fail(operation, "Nothing to update.");
|
|
1591
|
+
return yield* update(body);
|
|
1592
|
+
}
|
|
1593
|
+
case "set-draft": {
|
|
1594
|
+
const facts = yield* getGitLabMergeRequestFacts(call, operation, ref);
|
|
1595
|
+
if (facts.isDraft === action.draft) return;
|
|
1596
|
+
return yield* update({ title: gitLabDraftTitle(facts.title, action.draft) });
|
|
1597
|
+
}
|
|
1598
|
+
case "close":
|
|
1599
|
+
yield* update({ state_event: "close" });
|
|
1600
|
+
if (action.deleteBranch === true) {
|
|
1601
|
+
const facts = yield* getGitLabMergeRequestFacts(call, operation, ref);
|
|
1602
|
+
yield* deleteSourceBranch(call, operation, ref, facts);
|
|
1603
|
+
}
|
|
1604
|
+
return;
|
|
1605
|
+
case "reopen": return yield* update({ state_event: "reopen" });
|
|
1606
|
+
case "labels": {
|
|
1607
|
+
const body = {
|
|
1608
|
+
...action.add.length > 0 ? { add_labels: action.add.join(",") } : {},
|
|
1609
|
+
...action.remove.length > 0 ? { remove_labels: action.remove.join(",") } : {}
|
|
1610
|
+
};
|
|
1611
|
+
if (Object.keys(body).length === 0) return;
|
|
1612
|
+
return yield* update(body);
|
|
1613
|
+
}
|
|
1614
|
+
case "reviewers":
|
|
1615
|
+
case "assignees": {
|
|
1616
|
+
if (action.add.length === 0 && action.remove.length === 0) return;
|
|
1617
|
+
const facts = yield* getGitLabMergeRequestFacts(call, operation, ref);
|
|
1618
|
+
const addIds = yield* resolveUserIds(call, operation, ref, action.add);
|
|
1619
|
+
const ids = nextGitLabUserIds({
|
|
1620
|
+
current: action.kind === "reviewers" ? facts.reviewers : facts.assignees,
|
|
1621
|
+
removeLogins: action.remove,
|
|
1622
|
+
addIds
|
|
1623
|
+
});
|
|
1624
|
+
return yield* update(action.kind === "reviewers" ? { reviewer_ids: ids } : { assignee_ids: ids });
|
|
1625
|
+
}
|
|
1626
|
+
case "update-branch":
|
|
1627
|
+
if (action.method !== "rebase") return yield* fail(operation, "GitLab updates a merge request's branch by rebasing it; merging the target branch in is not available.");
|
|
1628
|
+
if ((yield* getGitLabMergeRequestFacts(call, operation, ref)).headSha !== action.expectedHeadSha) return yield* gitLabStaleHeadError(operation);
|
|
1629
|
+
yield* call(operation, gitLabWrite(ref, "PUT", gitLabMergeRequestEndpoint(ref, "/rebase")));
|
|
1630
|
+
return;
|
|
1631
|
+
case "auto-merge": {
|
|
1632
|
+
if (!action.enabled) {
|
|
1633
|
+
const output = yield* call(operation, gitLabWrite(ref, "POST", gitLabMergeRequestEndpoint(ref, "/cancel_merge_when_pipeline_succeeds")));
|
|
1634
|
+
const result = yield* decodeJson(operation, GitLabCancelAutoMergeSchema, output.stdout);
|
|
1635
|
+
if (result.status?.trim().toLowerCase() === "error") return yield* fail(operation, result.message?.trim() || "GitLab could not cancel the automatic merge.");
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const [facts, project] = yield* Effect.all([getGitLabMergeRequestFacts(call, operation, ref), getProject(call, operation, ref)], { concurrency: 2 });
|
|
1639
|
+
if (action.expectedHeadSha && facts.headSha !== action.expectedHeadSha) return yield* gitLabStaleHeadError(operation);
|
|
1640
|
+
if (action.mergeMethod && !gitLabMergeCapabilities(project)[action.mergeMethod]) return yield* fail(operation, `The ${action.mergeMethod} merge method is not available for this project.`);
|
|
1641
|
+
const sha = action.expectedHeadSha ?? facts.headSha;
|
|
1642
|
+
yield* call(operation, gitLabWrite(ref, "PUT", gitLabMergeRequestEndpoint(ref, "/merge"), {
|
|
1643
|
+
auto_merge: true,
|
|
1644
|
+
merge_when_pipeline_succeeds: true,
|
|
1645
|
+
...sha ? { sha } : {},
|
|
1646
|
+
...action.mergeMethod ? { squash: action.mergeMethod === "squash" } : {}
|
|
1647
|
+
})).pipe(Effect.mapError((error) => mergeError(operation, error)));
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
case "delete-branch": {
|
|
1651
|
+
const facts = yield* getGitLabMergeRequestFacts(call, operation, ref);
|
|
1652
|
+
return yield* deleteSourceBranch(call, operation, ref, facts);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
function mergeError(operation, error) {
|
|
1658
|
+
switch (error.status) {
|
|
1659
|
+
case 409: return gitLabStaleHeadError(operation);
|
|
1660
|
+
case 401: return fail(operation, "You do not have permission to merge this merge request.", 401);
|
|
1661
|
+
case 405: return fail(operation, "GitLab cannot merge this merge request right now (HTTP 405). Check its pipeline, approvals, open threads, conflicts and draft status.", 405);
|
|
1662
|
+
case 422: return fail(operation, `GitLab could not merge the branch (HTTP 422). ${error.detail}`, 422);
|
|
1663
|
+
default: return error;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
const GitLabMergeResultSchema = Schema.Struct({ state: Schema.optional(Schema.NullOr(Schema.String)) });
|
|
1667
|
+
/**
|
|
1668
|
+
* `PUT .../merge` with `sha` (GitLab refuses a moved head with 409). The
|
|
1669
|
+
* merge method is the project's (merge commit or fast-forward), so only it
|
|
1670
|
+
* and squash are accepted. A merge request GitLab queues (merge train,
|
|
1671
|
+
* pending pipeline) reports `enqueued`.
|
|
1672
|
+
*/
|
|
1673
|
+
function mergeGitLabMergeRequest(call, input) {
|
|
1674
|
+
const operation = "mergeChangeRequest";
|
|
1675
|
+
return Effect.gen(function* () {
|
|
1676
|
+
const ref = yield* requireGitLabMergeRequestRef(operation, input.reference);
|
|
1677
|
+
const [facts, project] = yield* Effect.all([getGitLabMergeRequestFacts(call, operation, ref), getProject(call, operation, ref)], { concurrency: 2 });
|
|
1678
|
+
if (input.expectedHeadSha && facts.headSha !== input.expectedHeadSha) return yield* gitLabStaleHeadError(operation);
|
|
1679
|
+
if (!gitLabMergeCapabilities(project)[input.mergeMethod]) return yield* fail(operation, `The ${input.mergeMethod} merge method is not available for this project.`);
|
|
1680
|
+
const output = yield* call(operation, gitLabWrite(ref, "PUT", gitLabMergeRequestEndpoint(ref, "/merge"), {
|
|
1681
|
+
squash: input.mergeMethod === "squash",
|
|
1682
|
+
...input.expectedHeadSha ? { sha: input.expectedHeadSha } : {},
|
|
1683
|
+
...input.deleteBranch !== void 0 ? { should_remove_source_branch: input.deleteBranch } : {}
|
|
1684
|
+
})).pipe(Effect.mapError((error) => mergeError(operation, error)));
|
|
1685
|
+
return { outcome: (yield* decodeJson(operation, GitLabMergeResultSchema, output.stdout)).state?.trim().toLowerCase() === "merged" ? "merged" : "enqueued" };
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
function toDetailCommit(commit) {
|
|
1689
|
+
const oid = commit.id.trim();
|
|
1690
|
+
if (!oid) return null;
|
|
1691
|
+
const author = commit.author_name?.trim();
|
|
1692
|
+
const committedDate = commit.committed_date?.trim();
|
|
1693
|
+
return {
|
|
1694
|
+
oid,
|
|
1695
|
+
shortOid: commit.short_id?.trim() || oid.slice(0, 8),
|
|
1696
|
+
messageHeadline: commit.title?.trim() ?? (commit.message ?? "").split("\n")[0]?.trim() ?? "",
|
|
1697
|
+
...committedDate ? { committedDate } : {},
|
|
1698
|
+
...author ? { author } : {}
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
const GitLabCommitParentsSchema = Schema.Struct({
|
|
1702
|
+
id: Schema.String,
|
|
1703
|
+
parent_ids: Schema.optional(Schema.NullOr(Schema.Array(Schema.String)))
|
|
1704
|
+
});
|
|
1705
|
+
/**
|
|
1706
|
+
* `head_pipeline` when it verifies the current head: on the head commit, or
|
|
1707
|
+
* on a merged-results or merge-train commit that has the head as a parent.
|
|
1708
|
+
* Any other SHA is an earlier head's pipeline (GitLab keeps it after a push
|
|
1709
|
+
* that started none), which would show stale checks, so it is dropped; so is
|
|
1710
|
+
* one whose merge commit cannot be read.
|
|
1711
|
+
*/
|
|
1712
|
+
function resolveGitLabHeadPipeline(call, operation, ref, facts) {
|
|
1713
|
+
const pipeline = facts.headPipeline;
|
|
1714
|
+
const head = facts.headSha?.toLowerCase() ?? null;
|
|
1715
|
+
const sha = pipeline?.sha?.toLowerCase() ?? null;
|
|
1716
|
+
if (!pipeline || !sha || !head || !isGitSha(sha)) return Effect.succeed(null);
|
|
1717
|
+
const resolved = {
|
|
1718
|
+
id: pipeline.id,
|
|
1719
|
+
sha,
|
|
1720
|
+
runName: gitLabPipelineRunName(pipeline)
|
|
1721
|
+
};
|
|
1722
|
+
if (sha === head) return Effect.succeed(resolved);
|
|
1723
|
+
return optional(getJson(call, operation, gitLabGet(ref, gitLabProjectEndpoint(ref, `/repository/commits/${sha}`)), GitLabCommitParentsSchema), null).pipe(Effect.map((commit) => commit?.parent_ids?.some((parent) => parent.toLowerCase() === head) === true ? resolved : null));
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Readiness facts for the detail: reviewers' states and approvals, the merge
|
|
1727
|
+
* state, the project's merge methods, head pipeline statuses and commits.
|
|
1728
|
+
* Each read is optional; a failed one leaves its facts out (never guessed).
|
|
1729
|
+
*/
|
|
1730
|
+
function fetchGitLabDetailReadiness(call, facts, reference) {
|
|
1731
|
+
const operation = "getChangeRequestDetail";
|
|
1732
|
+
return Effect.gen(function* () {
|
|
1733
|
+
const ref = yield* requireGitLabMergeRequestRef(operation, reference);
|
|
1734
|
+
const headPipeline = yield* resolveGitLabHeadPipeline(call, operation, ref, facts);
|
|
1735
|
+
const statusSha = headPipeline?.sha ?? facts.headSha;
|
|
1736
|
+
const read = yield* Effect.all({
|
|
1737
|
+
reviewers: optional(getJson(call, operation, gitLabGet(ref, gitLabMergeRequestEndpoint(ref, "/reviewers")), Schema.Array(GitLabReviewerSchema)), null),
|
|
1738
|
+
approvals: optional(getJson(call, operation, gitLabGet(ref, gitLabMergeRequestEndpoint(ref, "/approvals")), GitLabApprovalStateSchema), null),
|
|
1739
|
+
project: optional(getProject(call, operation, ref), null),
|
|
1740
|
+
statuses: statusSha && isGitSha(statusSha) ? optional(getGitLabPages(call, operation, {
|
|
1741
|
+
ref,
|
|
1742
|
+
endpoint: gitLabProjectEndpoint(ref, `/repository/commits/${statusSha}/statuses`),
|
|
1743
|
+
item: GitLabCommitStatusSchema,
|
|
1744
|
+
maxPages: 1
|
|
1745
|
+
}).pipe(Effect.map((page) => page.items)), null) : Effect.succeed(null),
|
|
1746
|
+
headJobs: headPipeline ? optional(getGitLabPages(call, operation, {
|
|
1747
|
+
ref,
|
|
1748
|
+
endpoint: gitLabProjectEndpoint(ref, `/pipelines/${headPipeline.id}/jobs`),
|
|
1749
|
+
item: GitLabJobSchema,
|
|
1750
|
+
maxPages: 1
|
|
1751
|
+
}).pipe(Effect.map((page) => page.items)), null) : Effect.succeed(null),
|
|
1752
|
+
commits: optional(getGitLabPages(call, operation, {
|
|
1753
|
+
ref,
|
|
1754
|
+
endpoint: gitLabMergeRequestEndpoint(ref, "/commits"),
|
|
1755
|
+
item: GitLabCommitSchema,
|
|
1756
|
+
maxPages: 1
|
|
1757
|
+
}).pipe(Effect.map((page) => page.items)), null)
|
|
1758
|
+
}, { concurrency: 6 });
|
|
1759
|
+
const reviewerStates = read.reviewers !== null ? gitLabReviewerStates({
|
|
1760
|
+
reviewers: read.reviewers,
|
|
1761
|
+
approvals: read.approvals
|
|
1762
|
+
}) : null;
|
|
1763
|
+
const reviewDecision = reviewerStates !== null && read.approvals !== null ? gitLabReviewDecision({
|
|
1764
|
+
reviewerStates,
|
|
1765
|
+
approvals: read.approvals
|
|
1766
|
+
}) : void 0;
|
|
1767
|
+
const mergeStateStatus = facts.isDraft ? "draft" : gitLabMergeStateStatus(facts.detailedMergeStatus);
|
|
1768
|
+
const reviewers = facts.reviewers.flatMap((user) => user.username?.trim() ? [user.username.trim()] : []);
|
|
1769
|
+
const commits = read.commits?.flatMap((commit) => {
|
|
1770
|
+
const mapped = toDetailCommit(commit);
|
|
1771
|
+
return mapped ? [mapped] : [];
|
|
1772
|
+
}).toReversed();
|
|
1773
|
+
const checkRollup = read.statuses !== null ? gitLabCheckRollup(read.statuses, headPipeline && read.headJobs ? {
|
|
1774
|
+
runName: headPipeline.runName,
|
|
1775
|
+
jobs: read.headJobs
|
|
1776
|
+
} : null) : null;
|
|
1777
|
+
return {
|
|
1778
|
+
...facts.headSha ? { headSha: facts.headSha } : {},
|
|
1779
|
+
...reviewerStates !== null ? { reviewerStates } : {},
|
|
1780
|
+
...reviewDecision !== void 0 ? { reviewDecision } : {},
|
|
1781
|
+
...mergeStateStatus ? { mergeStateStatus } : {},
|
|
1782
|
+
...read.project !== null ? { mergeCapabilities: gitLabMergeCapabilities(read.project) } : {},
|
|
1783
|
+
...facts.state === "open" && read.project !== null ? { autoMerge: gitLabAutoMerge({
|
|
1784
|
+
facts,
|
|
1785
|
+
project: read.project
|
|
1786
|
+
}) } : {},
|
|
1787
|
+
...facts.forceRemoveSourceBranch !== null ? { deleteBranchOnMerge: facts.forceRemoveSourceBranch } : {},
|
|
1788
|
+
...checkRollup !== null ? { checkRollup } : {},
|
|
1789
|
+
...commits && commits.length > 0 ? { commits } : {},
|
|
1790
|
+
...reviewers.length > 0 ? { reviewers } : {},
|
|
1791
|
+
...facts.closedAt && facts.state === "closed" ? { closedAt: facts.closedAt } : {},
|
|
1792
|
+
...facts.mergedAt ? { mergedAt: facts.mergedAt } : {},
|
|
1793
|
+
...facts.mergedBy && facts.state === "merged" ? { mergedBy: facts.mergedBy } : {},
|
|
1794
|
+
...facts.changesCount !== null ? { changedFiles: facts.changesCount } : {}
|
|
1795
|
+
};
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
const STATE_QUERY = {
|
|
1799
|
+
open: "opened",
|
|
1800
|
+
closed: "closed",
|
|
1801
|
+
merged: "merged",
|
|
1802
|
+
all: "all"
|
|
1803
|
+
};
|
|
1804
|
+
/**
|
|
1805
|
+
* A server-side filtered list: involvement relative to the authenticated
|
|
1806
|
+
* user (`author_username`, `reviewer_username`, `assignee_id`), free-text
|
|
1807
|
+
* `search`, state, and source branch. GitLab has no "mentioned" or
|
|
1808
|
+
* "involved" filter, so those fail instead of returning everything.
|
|
1809
|
+
*/
|
|
1810
|
+
function listGitLabMergeRequestsFiltered(call, input) {
|
|
1811
|
+
const operation = "listChangeRequests";
|
|
1812
|
+
return Effect.gen(function* () {
|
|
1813
|
+
let involvementQuery = {};
|
|
1814
|
+
if (input.involvement !== void 0) {
|
|
1815
|
+
if (input.involvement === "mentioned" || input.involvement === "involved") return yield* fail(operation, `GitLab cannot filter merge requests by "${input.involvement}"; use authored, review requested, or assigned.`);
|
|
1816
|
+
const user = yield* getCurrentUser(call, operation, null);
|
|
1817
|
+
involvementQuery = input.involvement === "authored" ? { author_username: user.username } : input.involvement === "review-requested" ? { reviewer_username: user.username } : { assignee_id: user.id };
|
|
1818
|
+
}
|
|
1819
|
+
const query = input.query?.trim();
|
|
1820
|
+
const output = yield* call(operation, gitLabGet(null, withGitLabQuery(gitLabProjectEndpoint(CURRENT_GITLAB_PROJECT, "/merge_requests"), {
|
|
1821
|
+
state: STATE_QUERY[input.state],
|
|
1822
|
+
...involvementQuery,
|
|
1823
|
+
...query ? { search: query } : {},
|
|
1824
|
+
...input.sourceBranch ? { source_branch: input.sourceBranch } : {},
|
|
1825
|
+
order_by: "updated_at",
|
|
1826
|
+
sort: "desc",
|
|
1827
|
+
per_page: Math.min(Math.max(input.limit ?? 20, 1), 100)
|
|
1828
|
+
})), { maxOutputBytes: LIST_PAGE_MAX_BYTES });
|
|
1829
|
+
const decoded = decodeGitLabMergeRequestListJson(output.stdout.trim() || "[]");
|
|
1830
|
+
return Result.isSuccess(decoded) ? decoded.success : yield* fail(operation, "GitLab returned an unexpected merge request list.");
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
/** One rollup per list row from the project's recent pipelines (one request for the page). */
|
|
1834
|
+
function enrichGitLabListRowsWithPipelines(call, rows) {
|
|
1835
|
+
if (!rows.some((row) => row.state === "open")) return Effect.succeed(rows);
|
|
1836
|
+
return optional(getGitLabPages(call, "listChangeRequests", {
|
|
1837
|
+
ref: null,
|
|
1838
|
+
endpoint: gitLabProjectEndpoint(CURRENT_GITLAB_PROJECT, "/pipelines"),
|
|
1839
|
+
item: GitLabPipelineSchema,
|
|
1840
|
+
maxPages: 1
|
|
1841
|
+
}).pipe(Effect.map((page) => page.items)), []).pipe(Effect.map((pipelines) => rows.map((row) => {
|
|
1842
|
+
if (row.state !== "open" || row.checkRollup) return row;
|
|
1843
|
+
const checkRollup = gitLabListRowRollup(pipelines, {
|
|
1844
|
+
iid: row.number,
|
|
1845
|
+
sourceBranch: row.headRefName,
|
|
1846
|
+
headSha: row.headSha ?? null
|
|
1847
|
+
});
|
|
1848
|
+
return checkRollup ? {
|
|
1849
|
+
...row,
|
|
1850
|
+
checkRollup
|
|
1851
|
+
} : row;
|
|
1852
|
+
})));
|
|
1853
|
+
}
|
|
1854
|
+
const GitLabLabelSchema = Schema.Struct({
|
|
1855
|
+
name: Schema.String,
|
|
1856
|
+
color: Schema.optional(Schema.NullOr(Schema.String)),
|
|
1857
|
+
description: Schema.optional(Schema.NullOr(Schema.String))
|
|
1858
|
+
});
|
|
1859
|
+
const GitLabMemberSchema = Schema.Struct({
|
|
1860
|
+
username: Schema.String,
|
|
1861
|
+
name: Schema.optional(Schema.NullOr(Schema.String)),
|
|
1862
|
+
avatar_url: Schema.optional(Schema.NullOr(Schema.String)),
|
|
1863
|
+
state: Schema.optional(Schema.NullOr(Schema.String))
|
|
1864
|
+
});
|
|
1865
|
+
function listGitLabLabels(call) {
|
|
1866
|
+
return getGitLabPages(call, "listLabels", {
|
|
1867
|
+
ref: null,
|
|
1868
|
+
endpoint: gitLabProjectEndpoint(CURRENT_GITLAB_PROJECT, "/labels"),
|
|
1869
|
+
item: GitLabLabelSchema,
|
|
1870
|
+
maxPages: 3
|
|
1871
|
+
}).pipe(Effect.map((page) => page.items.flatMap((label) => {
|
|
1872
|
+
const name = label.name.trim();
|
|
1873
|
+
if (!name) return [];
|
|
1874
|
+
const color = label.color?.trim().replace(/^#/u, "").toLowerCase();
|
|
1875
|
+
const description = label.description?.trim();
|
|
1876
|
+
return [{
|
|
1877
|
+
name,
|
|
1878
|
+
...color && /^[0-9a-f]{3,8}$/u.test(color) ? { color } : {},
|
|
1879
|
+
...description ? { description } : {}
|
|
1880
|
+
}];
|
|
1881
|
+
})));
|
|
1882
|
+
}
|
|
1883
|
+
/** Active members, inherited ones included (reviewer and assignee candidates). */
|
|
1884
|
+
function listGitLabAssignees(call) {
|
|
1885
|
+
return getGitLabPages(call, "listAssignees", {
|
|
1886
|
+
ref: null,
|
|
1887
|
+
endpoint: gitLabProjectEndpoint(CURRENT_GITLAB_PROJECT, "/members/all"),
|
|
1888
|
+
item: GitLabMemberSchema,
|
|
1889
|
+
maxPages: 3
|
|
1890
|
+
}).pipe(Effect.map((page) => page.items.flatMap((member) => {
|
|
1891
|
+
const login = member.username.trim();
|
|
1892
|
+
if (!login || member.state && member.state !== "active") return [];
|
|
1893
|
+
const displayName = member.name?.trim();
|
|
1894
|
+
const avatarUrl = member.avatar_url?.trim();
|
|
1895
|
+
return [{
|
|
1896
|
+
login,
|
|
1897
|
+
...displayName ? { displayName } : {},
|
|
1898
|
+
...avatarUrl ? { avatarUrl } : {}
|
|
1899
|
+
}];
|
|
1900
|
+
})));
|
|
1901
|
+
}
|
|
1902
|
+
function requireNumericId(operation, label, value) {
|
|
1903
|
+
return isGitLabNumericId(value) ? Effect.succeed(value) : Effect.fail(fail(operation, `Unknown ${label}: ${value}`));
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Pipelines for a merge request's head (`pullRequestNumber`), a commit, or a
|
|
1907
|
+
* branch's newest commit, newest first.
|
|
1908
|
+
*/
|
|
1909
|
+
function listGitLabWorkflowRuns(call, input) {
|
|
1910
|
+
const operation = "listWorkflowRuns";
|
|
1911
|
+
return Effect.gen(function* () {
|
|
1912
|
+
const perPage = Math.min(Math.max(input.limit ?? 50, 1), 100);
|
|
1913
|
+
const project = CURRENT_GITLAB_PROJECT;
|
|
1914
|
+
let pipelines;
|
|
1915
|
+
let sourceHeadOf = () => null;
|
|
1916
|
+
let headSha = input.commitSha?.trim() || null;
|
|
1917
|
+
if (headSha !== null && !isGitSha(headSha)) return yield* fail(operation, `Invalid commit SHA: ${headSha}`);
|
|
1918
|
+
if (input.pullRequestNumber !== void 0) {
|
|
1919
|
+
const ref = {
|
|
1920
|
+
...project,
|
|
1921
|
+
iid: input.pullRequestNumber
|
|
1922
|
+
};
|
|
1923
|
+
const facts = yield* getGitLabMergeRequestFacts(call, operation, ref);
|
|
1924
|
+
headSha = headSha ?? facts.headSha;
|
|
1925
|
+
const { page, headPipeline } = yield* Effect.all({
|
|
1926
|
+
page: getGitLabPages(call, operation, {
|
|
1927
|
+
ref,
|
|
1928
|
+
endpoint: gitLabMergeRequestEndpoint(ref, "/pipelines"),
|
|
1929
|
+
item: GitLabPipelineSchema,
|
|
1930
|
+
maxPages: 1,
|
|
1931
|
+
perPage
|
|
1932
|
+
}),
|
|
1933
|
+
headPipeline: input.commitSha ? Effect.succeed(null) : resolveGitLabHeadPipeline(call, operation, ref, facts)
|
|
1934
|
+
}, { concurrency: 2 });
|
|
1935
|
+
pipelines = selectGitLabHeadPipelines(page.items, {
|
|
1936
|
+
headSha,
|
|
1937
|
+
headPipelineId: headPipeline?.id ?? null
|
|
1938
|
+
});
|
|
1939
|
+
sourceHeadOf = (pipeline) => pipeline.id === headPipeline?.id ? facts.headSha : null;
|
|
1940
|
+
} else if (headSha !== null) pipelines = (yield* getGitLabPages(call, operation, {
|
|
1941
|
+
ref: null,
|
|
1942
|
+
endpoint: withGitLabQuery(gitLabProjectEndpoint(project, "/pipelines"), { sha: headSha }),
|
|
1943
|
+
item: GitLabPipelineSchema,
|
|
1944
|
+
maxPages: 1,
|
|
1945
|
+
perPage
|
|
1946
|
+
})).items;
|
|
1947
|
+
else if (input.branch?.trim()) {
|
|
1948
|
+
const page = yield* getGitLabPages(call, operation, {
|
|
1949
|
+
ref: null,
|
|
1950
|
+
endpoint: withGitLabQuery(gitLabProjectEndpoint(project, "/pipelines"), { ref: input.branch.trim() }),
|
|
1951
|
+
item: GitLabPipelineSchema,
|
|
1952
|
+
maxPages: 1,
|
|
1953
|
+
perPage
|
|
1954
|
+
});
|
|
1955
|
+
headSha = page.items[0]?.sha ?? null;
|
|
1956
|
+
pipelines = page.items.filter((pipeline) => pipeline.sha === headSha);
|
|
1957
|
+
} else pipelines = [];
|
|
1958
|
+
return {
|
|
1959
|
+
provider: "gitlab",
|
|
1960
|
+
repository: Option.none(),
|
|
1961
|
+
pullRequestNumber: input.pullRequestNumber !== void 0 ? Option.some(input.pullRequestNumber) : Option.none(),
|
|
1962
|
+
headSha: headSha ? Option.some(headSha) : Option.none(),
|
|
1963
|
+
runs: pipelines.flatMap((pipeline) => {
|
|
1964
|
+
const run = gitLabWorkflowRun(pipeline, sourceHeadOf(pipeline));
|
|
1965
|
+
return run ? [run] : [];
|
|
1966
|
+
})
|
|
1967
|
+
};
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
function listGitLabPipelineJobs(call, runId) {
|
|
1971
|
+
const operation = "getWorkflowRunJobs";
|
|
1972
|
+
return Effect.gen(function* () {
|
|
1973
|
+
yield* requireNumericId(operation, "pipeline", runId);
|
|
1974
|
+
return {
|
|
1975
|
+
provider: "gitlab",
|
|
1976
|
+
runId,
|
|
1977
|
+
jobs: (yield* getGitLabPages(call, operation, {
|
|
1978
|
+
ref: null,
|
|
1979
|
+
endpoint: gitLabProjectEndpoint(CURRENT_GITLAB_PROJECT, `/pipelines/${runId}/jobs`),
|
|
1980
|
+
item: GitLabJobSchema,
|
|
1981
|
+
maxPages: 3
|
|
1982
|
+
})).items.toSorted((left, right) => left.id - right.id).map(gitLabWorkflowJob)
|
|
1983
|
+
};
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
function getGitLabJobLog(call, input) {
|
|
1987
|
+
const operation = "getWorkflowJobLog";
|
|
1988
|
+
return Effect.gen(function* () {
|
|
1989
|
+
yield* requireNumericId(operation, "job", input.jobId);
|
|
1990
|
+
const output = yield* call(operation, gitLabGet(null, gitLabProjectEndpoint(CURRENT_GITLAB_PROJECT, `/jobs/${input.jobId}/trace`)), {
|
|
1991
|
+
maxOutputBytes: JOB_LOG_TAIL_BYTES,
|
|
1992
|
+
keepOutputTail: true,
|
|
1993
|
+
timeoutMs: 6e4
|
|
1994
|
+
});
|
|
1995
|
+
const tail = gitLabJobLogTail(output.stdout, { startCut: output.stdoutTruncated });
|
|
1996
|
+
return {
|
|
1997
|
+
provider: "gitlab",
|
|
1998
|
+
runId: input.runId,
|
|
1999
|
+
jobId: input.jobId,
|
|
2000
|
+
log: tail.log,
|
|
2001
|
+
truncated: tail.truncated || output.stdoutTruncated
|
|
2002
|
+
};
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
/** `failed-jobs` retries a pipeline's failed and canceled jobs; `job` retries one. */
|
|
2006
|
+
function rerunGitLabPipeline(call, input) {
|
|
2007
|
+
const operation = "rerunWorkflow";
|
|
2008
|
+
return Effect.gen(function* () {
|
|
2009
|
+
if (input.target === "job") {
|
|
2010
|
+
yield* requireNumericId(operation, "job", input.jobId);
|
|
2011
|
+
yield* call(operation, gitLabWrite(CURRENT_GITLAB_PROJECT, "POST", gitLabProjectEndpoint(CURRENT_GITLAB_PROJECT, `/jobs/${input.jobId}/retry`)));
|
|
2012
|
+
return {
|
|
2013
|
+
provider: "gitlab",
|
|
2014
|
+
runId: input.runId,
|
|
2015
|
+
target: "job",
|
|
2016
|
+
jobId: input.jobId
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
yield* requireNumericId(operation, "pipeline", input.runId);
|
|
2020
|
+
yield* call(operation, gitLabWrite(CURRENT_GITLAB_PROJECT, "POST", gitLabProjectEndpoint(CURRENT_GITLAB_PROJECT, `/pipelines/${input.runId}/retry`)));
|
|
2021
|
+
return {
|
|
2022
|
+
provider: "gitlab",
|
|
2023
|
+
runId: input.runId,
|
|
2024
|
+
target: "failed-jobs"
|
|
2025
|
+
};
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
//#endregion
|
|
2029
|
+
//#region src/sourceControl/GitLabSourceControlProvider.ts
|
|
2030
|
+
function providerError(operation, cause) {
|
|
2031
|
+
return new SourceControlProviderError({
|
|
2032
|
+
provider: "gitlab",
|
|
2033
|
+
operation,
|
|
2034
|
+
detail: cause.detail,
|
|
2035
|
+
cause
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
function toChangeRequest(summary) {
|
|
2039
|
+
return {
|
|
2040
|
+
provider: "gitlab",
|
|
2041
|
+
number: summary.number,
|
|
2042
|
+
title: summary.title,
|
|
2043
|
+
url: summary.url,
|
|
2044
|
+
baseRefName: summary.baseRefName,
|
|
2045
|
+
headRefName: summary.headRefName,
|
|
2046
|
+
state: summary.state ?? "open",
|
|
2047
|
+
updatedAt: summary.updatedAt ?? Option.none(),
|
|
2048
|
+
...summary.isCrossRepository !== void 0 ? { isCrossRepository: summary.isCrossRepository } : {},
|
|
2049
|
+
...summary.headRepositoryNameWithOwner !== void 0 ? { headRepositoryNameWithOwner: summary.headRepositoryNameWithOwner } : {},
|
|
2050
|
+
...summary.headRepositoryOwnerLogin !== void 0 ? { headRepositoryOwnerLogin: summary.headRepositoryOwnerLogin } : {},
|
|
2051
|
+
...summary.isDraft !== void 0 ? { isDraft: summary.isDraft } : {},
|
|
2052
|
+
...summary.author ? { author: summary.author } : {},
|
|
2053
|
+
...summary.assignees && summary.assignees.length > 0 ? { assignees: summary.assignees } : {},
|
|
2054
|
+
...summary.labels && summary.labels.length > 0 ? { labels: summary.labels } : {},
|
|
2055
|
+
...summary.commentsCount !== void 0 ? { commentsCount: summary.commentsCount } : {},
|
|
2056
|
+
...summary.headSha ? { headSha: summary.headSha } : {},
|
|
2057
|
+
...summary.mergeability ? { mergeability: summary.mergeability } : {},
|
|
2058
|
+
...summary.mergeStateStatus ? { mergeStateStatus: summary.mergeStateStatus } : {},
|
|
2059
|
+
...summary.reviewDecision ? { reviewDecision: summary.reviewDecision } : {},
|
|
2060
|
+
...summary.createdAt ? { createdAt: summary.createdAt } : {}
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
function toIssueSummary(raw) {
|
|
2064
|
+
return {
|
|
2065
|
+
provider: "gitlab",
|
|
2066
|
+
number: raw.number,
|
|
2067
|
+
title: raw.title,
|
|
2068
|
+
url: raw.url,
|
|
2069
|
+
state: raw.state,
|
|
2070
|
+
...raw.author ? { author: raw.author } : {},
|
|
2071
|
+
updatedAt: raw.updatedAt.pipe(Option.map((s) => DateTime.fromDateUnsafe(new Date(s)))),
|
|
2072
|
+
labels: raw.labels.map((name) => ({ name }))
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
function toIssueDetail(raw, options) {
|
|
2076
|
+
const content = options.fullContent ? {
|
|
2077
|
+
body: raw.body,
|
|
2078
|
+
comments: raw.comments,
|
|
2079
|
+
truncated: false
|
|
2080
|
+
} : truncateSourceControlDetailContent({
|
|
2081
|
+
body: raw.body,
|
|
2082
|
+
comments: raw.comments
|
|
2083
|
+
});
|
|
2084
|
+
return {
|
|
2085
|
+
...toIssueSummary(raw),
|
|
2086
|
+
body: content.body,
|
|
2087
|
+
comments: content.comments.map((c) => ({
|
|
2088
|
+
author: c.author,
|
|
2089
|
+
body: c.body,
|
|
2090
|
+
createdAt: DateTime.fromDateUnsafe(new Date(c.createdAt))
|
|
2091
|
+
})),
|
|
2092
|
+
truncated: content.truncated
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
function toChangeRequestDetail(raw, options) {
|
|
2096
|
+
const content = options.fullContent ? {
|
|
2097
|
+
body: raw.body,
|
|
2098
|
+
comments: raw.comments,
|
|
2099
|
+
truncated: false
|
|
2100
|
+
} : truncateSourceControlDetailContent({
|
|
2101
|
+
body: raw.body,
|
|
2102
|
+
comments: raw.comments
|
|
2103
|
+
});
|
|
2104
|
+
return {
|
|
2105
|
+
...toChangeRequest(raw),
|
|
2106
|
+
body: content.body,
|
|
2107
|
+
comments: content.comments.map((c) => ({
|
|
2108
|
+
author: c.author,
|
|
2109
|
+
body: c.body,
|
|
2110
|
+
createdAt: DateTime.fromDateUnsafe(new Date(c.createdAt))
|
|
2111
|
+
})),
|
|
2112
|
+
truncated: content.truncated
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
const make = Effect.fn("makeGitLabSourceControlProvider")(function* () {
|
|
2116
|
+
const gitlab = yield* GitLabCli;
|
|
2117
|
+
/** The page runner for one checkout: `glab api` resolves `:fullpath` from `cwd`. */
|
|
2118
|
+
const callIn = (cwd) => (operation, request, options) => gitlab.api({
|
|
2119
|
+
cwd,
|
|
2120
|
+
operation,
|
|
2121
|
+
request,
|
|
2122
|
+
...options
|
|
2123
|
+
});
|
|
2124
|
+
const run = (operation, effect) => effect.pipe(Effect.mapError((error) => providerError(operation, error)));
|
|
2125
|
+
const getChangeRequestDetail = (input) => Effect.gen(function* () {
|
|
2126
|
+
const raw = yield* gitlab.getMergeRequestDetail({
|
|
2127
|
+
cwd: input.cwd,
|
|
2128
|
+
reference: input.reference
|
|
2129
|
+
});
|
|
2130
|
+
const detail = toChangeRequestDetail(raw, { fullContent: input.fullContent ?? false });
|
|
2131
|
+
if (!raw.facts) return detail;
|
|
2132
|
+
const readiness = yield* fetchGitLabDetailReadiness(callIn(input.cwd), raw.facts, String(raw.number));
|
|
2133
|
+
return {
|
|
2134
|
+
...detail,
|
|
2135
|
+
...readiness
|
|
2136
|
+
};
|
|
2137
|
+
}).pipe(Effect.mapError((error) => providerError("getChangeRequestDetail", error)));
|
|
2138
|
+
const freshDetail = (input) => getChangeRequestDetail({
|
|
2139
|
+
cwd: input.cwd,
|
|
2140
|
+
reference: input.reference,
|
|
2141
|
+
fullContent: true
|
|
2142
|
+
});
|
|
2143
|
+
const provider = SourceControlProvider.of({
|
|
2144
|
+
kind: "gitlab",
|
|
2145
|
+
listChangeRequests: (input) => {
|
|
2146
|
+
const source = sourceControlRefFromInput(input);
|
|
2147
|
+
const query = input.query?.trim() ?? "";
|
|
2148
|
+
const call = callIn(input.cwd);
|
|
2149
|
+
return (input.involvement !== void 0 || query.length > 0 ? listGitLabMergeRequestsFiltered(call, {
|
|
2150
|
+
state: input.state,
|
|
2151
|
+
involvement: input.involvement,
|
|
2152
|
+
query,
|
|
2153
|
+
sourceBranch: sourceBranch(input) || void 0,
|
|
2154
|
+
limit: input.limit
|
|
2155
|
+
}) : gitlab.listMergeRequests({
|
|
2156
|
+
cwd: input.cwd,
|
|
2157
|
+
headSelector: input.headSelector,
|
|
2158
|
+
...source ? { source } : {},
|
|
2159
|
+
state: input.state,
|
|
2160
|
+
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
2161
|
+
})).pipe(Effect.map((items) => items.map(toChangeRequest)), Effect.flatMap((rows) => input.headSelector.trim().length === 0 && input.includeStackSummary !== false ? enrichGitLabListRowsWithPipelines(call, rows) : Effect.succeed(rows)), Effect.mapError((error) => providerError("listChangeRequests", error)));
|
|
2162
|
+
},
|
|
2163
|
+
getChangeRequest: (input) => gitlab.getMergeRequest(input).pipe(Effect.map(toChangeRequest), Effect.mapError((error) => providerError("getChangeRequest", error))),
|
|
2164
|
+
createChangeRequest: (input) => {
|
|
2165
|
+
const source = sourceControlRefFromInput(input);
|
|
2166
|
+
return gitlab.createMergeRequest({
|
|
2167
|
+
cwd: input.cwd,
|
|
2168
|
+
baseBranch: input.baseRefName,
|
|
2169
|
+
headSelector: input.headSelector,
|
|
2170
|
+
...source ? { source } : {},
|
|
2171
|
+
...input.target ? { target: input.target } : {},
|
|
2172
|
+
title: input.title,
|
|
2173
|
+
bodyFile: input.bodyFile,
|
|
2174
|
+
...input.draft === true ? { draft: true } : {}
|
|
2175
|
+
}).pipe(Effect.mapError((error) => providerError("createChangeRequest", error)));
|
|
2176
|
+
},
|
|
2177
|
+
getRepositoryCloneUrls: (input) => gitlab.getRepositoryCloneUrls(input).pipe(Effect.mapError((error) => providerError("getRepositoryCloneUrls", error))),
|
|
2178
|
+
createRepository: (input) => gitlab.createRepository(input).pipe(Effect.mapError((error) => providerError("createRepository", error))),
|
|
2179
|
+
getDefaultBranch: (input) => gitlab.getDefaultBranch(input).pipe(Effect.mapError((error) => providerError("getDefaultBranch", error))),
|
|
2180
|
+
checkoutChangeRequest: (input) => gitlab.checkoutMergeRequest(input).pipe(Effect.mapError((error) => providerError("checkoutChangeRequest", error))),
|
|
2181
|
+
listIssues: (input) => gitlab.listIssues({
|
|
2182
|
+
cwd: input.cwd,
|
|
2183
|
+
state: input.state,
|
|
2184
|
+
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
2185
|
+
}).pipe(Effect.map((items) => items.map(toIssueSummary)), Effect.mapError((error) => providerError("listIssues", error))),
|
|
2186
|
+
getIssue: (input) => gitlab.getIssue({
|
|
2187
|
+
cwd: input.cwd,
|
|
2188
|
+
reference: input.reference
|
|
2189
|
+
}).pipe(Effect.map((raw) => toIssueDetail(raw, { fullContent: input.fullContent ?? false })), Effect.mapError((error) => providerError("getIssue", error))),
|
|
2190
|
+
addIssueComment: () => Effect.fail(new SourceControlProviderError({
|
|
2191
|
+
provider: "gitlab",
|
|
2192
|
+
operation: "addIssueComment",
|
|
2193
|
+
detail: "Not implemented for gitlab"
|
|
2194
|
+
})),
|
|
2195
|
+
addIssueCommentReaction: () => Effect.fail(new SourceControlProviderError({
|
|
2196
|
+
provider: "gitlab",
|
|
2197
|
+
operation: "addIssueCommentReaction",
|
|
2198
|
+
detail: "Not implemented for gitlab"
|
|
2199
|
+
})),
|
|
2200
|
+
searchIssues: (input) => gitlab.searchIssues({
|
|
2201
|
+
cwd: input.cwd,
|
|
2202
|
+
query: input.query,
|
|
2203
|
+
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
2204
|
+
}).pipe(Effect.map((items) => items.map(toIssueSummary)), Effect.mapError((error) => providerError("searchIssues", error))),
|
|
2205
|
+
searchChangeRequests: (input) => gitlab.searchMergeRequests({
|
|
2206
|
+
cwd: input.cwd,
|
|
2207
|
+
query: input.query,
|
|
2208
|
+
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
2209
|
+
}).pipe(Effect.map((items) => items.map(toChangeRequest)), Effect.mapError((error) => providerError("searchChangeRequests", error))),
|
|
2210
|
+
getChangeRequestDetail,
|
|
2211
|
+
addChangeRequestComment: (input) => run("addChangeRequestComment", addGitLabComment(callIn(input.cwd), input)).pipe(Effect.andThen(() => freshDetail(input))),
|
|
2212
|
+
addChangeRequestCommentReaction: () => Effect.fail(new SourceControlProviderError({
|
|
2213
|
+
provider: "gitlab",
|
|
2214
|
+
operation: "addChangeRequestCommentReaction",
|
|
2215
|
+
detail: "GitLab does not support comment reactions."
|
|
2216
|
+
})),
|
|
2217
|
+
getChangeRequestDiff: (input) => run("getChangeRequestDiff", fetchGitLabChangeRequestDiff(callIn(input.cwd), input)),
|
|
2218
|
+
mergeChangeRequest: (input) => run("mergeChangeRequest", mergeGitLabMergeRequest(callIn(input.cwd), input)),
|
|
2219
|
+
getChangeRequestActivity: (input) => run("getChangeRequestActivity", fetchGitLabChangeRequestActivity(callIn(input.cwd), input.reference)),
|
|
2220
|
+
getChangeRequestFileContents: ({ context: _context, ...input }) => run("getChangeRequestFileContents", fetchGitLabFileContents(callIn(input.cwd), input)),
|
|
2221
|
+
submitChangeRequestReview: ({ context: _context, ...input }) => run("submitChangeRequestReview", submitGitLabReview(callIn(input.cwd), input)),
|
|
2222
|
+
replyToReviewThread: (input) => run("replyToReviewThread", replyToGitLabThread(callIn(input.cwd), input)).pipe(Effect.map((thread) => ({ thread }))),
|
|
2223
|
+
setReviewThreadResolved: (input) => run("setReviewThreadResolved", setGitLabThreadResolved(callIn(input.cwd), input)),
|
|
2224
|
+
updateChangeRequestComment: ({ context: _context, ...input }) => run("updateChangeRequestComment", updateGitLabComment(callIn(input.cwd), input)),
|
|
2225
|
+
updateChangeRequest: (input) => run("updateChangeRequest", updateGitLabMergeRequest(callIn(input.cwd), input)).pipe(Effect.andThen(() => freshDetail(input)), Effect.map((detail) => ({ detail }))),
|
|
2226
|
+
createIssue: () => Effect.fail(new SourceControlProviderError({
|
|
2227
|
+
provider: "gitlab",
|
|
2228
|
+
operation: "createIssue",
|
|
2229
|
+
detail: "Not implemented in Phase 1"
|
|
2230
|
+
})),
|
|
2231
|
+
listLabels: (input) => run("listLabels", listGitLabLabels(callIn(input.cwd))),
|
|
2232
|
+
listAssignees: (input) => run("listAssignees", listGitLabAssignees(callIn(input.cwd))),
|
|
2233
|
+
getPullRequestState: (input) => gitlab.getMergeRequest({
|
|
2234
|
+
cwd: input.cwd,
|
|
2235
|
+
reference: String(input.number)
|
|
2236
|
+
}).pipe(Effect.map((summary) => {
|
|
2237
|
+
const state = summary.state ?? "open";
|
|
2238
|
+
return {
|
|
2239
|
+
state,
|
|
2240
|
+
isDraft: summary.isDraft ?? false,
|
|
2241
|
+
terminalAt: state === "merged" ? summary.mergedAt ?? summary.closedAt ?? null : state === "closed" ? summary.closedAt ?? null : null
|
|
2242
|
+
};
|
|
2243
|
+
}), Effect.mapError((error) => providerError("getPullRequestState", error))),
|
|
2244
|
+
getIssueState: () => Effect.fail(new SourceControlProviderError({
|
|
2245
|
+
provider: "gitlab",
|
|
2246
|
+
operation: "getIssueState",
|
|
2247
|
+
detail: "Not implemented for gitlab"
|
|
2248
|
+
})),
|
|
2249
|
+
listWorkflowRuns: (input) => run("listWorkflowRuns", listGitLabWorkflowRuns(callIn(input.cwd), input)),
|
|
2250
|
+
getWorkflowRunJobs: (input) => run("getWorkflowRunJobs", listGitLabPipelineJobs(callIn(input.cwd), input.runId)),
|
|
2251
|
+
getWorkflowJobLog: (input) => run("getWorkflowJobLog", getGitLabJobLog(callIn(input.cwd), input)),
|
|
2252
|
+
rerunWorkflow: ({ context: _context, ...input }) => run("rerunWorkflow", rerunGitLabPipeline(callIn(input.cwd), input))
|
|
2253
|
+
});
|
|
2254
|
+
return withUnsupportedChangeRequestOptionGuards(provider);
|
|
2255
|
+
});
|
|
2256
|
+
const layer = Layer.effect(SourceControlProvider, make());
|
|
2257
|
+
//#endregion
|
|
2258
|
+
export { gitlabDiscovery as discovery, layer, make };
|