@hienlh/ppm 0.9.85 → 0.9.87
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/260415-0932-git-graph-stash-rebase-conflicts/reports/code-reviewer-260415-1020-stash-rebase-conflicts.md +288 -0
- package/260415-0932-git-graph-stash-rebase-conflicts/reports/tester-260415-1020-build-check.md +117 -0
- package/260415-1150-ext-silent-failure-debugging/reports/code-reviewer-260415-1159-ext-error-reporting-review.md +205 -0
- package/260415-1150-ext-silent-failure-debugging/reports/docs-manager-260415-1206-ext-error-reporting.md +99 -0
- package/260415-1150-ext-silent-failure-debugging/reports/tester-260415-1159-extension-error-reporting.md +174 -0
- package/CHANGELOG.md +24 -0
- package/dist/web/assets/{_basePickBy-D-bUmjma.js → _basePickBy-Bj0dI1ei.js} +1 -1
- package/dist/web/assets/{_baseUniq-BnXXIfRB.js → _baseUniq-CyzdZeQH.js} +1 -1
- package/dist/web/assets/ai-settings-section-Bo9lCaTd.js +1 -0
- package/dist/web/assets/{api-settings-Qi2xRiHa.js → api-settings-CUxg9RE5.js} +1 -1
- package/dist/web/assets/{arc-DB9vXGzd.js → arc-CxgHJ7Z4.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DDFO_NKq.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-BBV25747.js → architectureDiagram-2XIMDMQ5-D16OotsC.js} +1 -1
- package/dist/web/assets/arrow-up-I9-21gkR.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-BOTnY2Lq.js → blockDiagram-WCTKOSBZ-Ct57Wtfk.js} +1 -1
- package/dist/web/assets/{c4Diagram-IC4MRINW-D7QAUdHD.js → c4Diagram-IC4MRINW-BIymcNsg.js} +1 -1
- package/dist/web/assets/channel-wumTB1if.js +1 -0
- package/dist/web/assets/chat-tab-R4gKsnxD.js +10 -0
- package/dist/web/assets/chevron-right-DY_wImxB.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-BnOVw77D.js → chunk-4BX2VUAB-CENmY7Kw.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-BftA8DxR.js → chunk-55IACEB6-DhZGI1l3.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-B0vnP8v3.js → chunk-7E7YKBS2-DZcnC7Ow.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-Czlaj26D.js → chunk-7R4GIKGN-y8bfHEy-.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-DpEbDtMo.js → chunk-C72U2L5F-BHPkfQj2.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-BWXe6lkx.js → chunk-EGIJ26TM-nant2LXl.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DspqhPfk.js → chunk-FMBD7UC4-Bog4cpN-.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-D6HHRbYk.js → chunk-GEFDOKGD-86LFbsAC.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-Re-5eSlQ.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-C45i5G_3.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BC8wnMwf.js → chunk-JSJVCQXG-23eG9mgt.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-D3VDtyvX.js → chunk-KX2RTZJC-CHj8TnTB.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-Z-NBw_HS.js → chunk-KYZI473N-gqRLpJ4w.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL--RGkEh__.js → chunk-L3YUKLVL-DnSMmNFC.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-2B76t_Kx.js → chunk-MX3YWQON-B6g1ZH9X.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-BekY3tEi.js → chunk-NQ4KR5QH-DX32345Y.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-2CJLfx_1.js → chunk-O4XLMI2P-Vp_V4P-b.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-sug_L09P.js → chunk-OZEHJAEY-lKq2SWjA.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-_fwPRLQy.js → chunk-PQ6SQG4A-Bik13fTV.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-BUaTFJVQ.js → chunk-PU5JKC2W-DD95Rx35.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-N3VXx1VH.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-C37xW0vj.js → chunk-R5LLSJPH-dRhXRnrb.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-CCkt_MT6.js → chunk-WL4C6EOR-B1iIvLOG.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Dz2LBq7Y.js → chunk-XIRO2GV7-DZBoNl1_.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-DenTbBuj.js → chunk-XPW4576I-CgLyyW03.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-Dbp1nUSQ.js → chunk-XZSTWKYB-DjV8xl5A.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-3OTKowjE.js → chunk-YBOYWFTD-D_ILLe6_.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-mr-Cb1me.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-BKe8_uda.js +1 -0
- package/dist/web/assets/clone--z5KLAuR.js +1 -0
- package/dist/web/assets/code-editor-Br0vzTOy.js +8 -0
- package/dist/web/assets/columns-2-IeETSfON.js +1 -0
- package/dist/web/assets/conflict-editor-BPgCjnNz.js +19 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-MbmGZnt0.js → cose-bilkent-S5V4N54A-BGNPFv3x.js} +1 -1
- package/dist/web/assets/{csv-preview-uZ_7b8I7.js → csv-preview-BZRICDP0.js} +2 -2
- package/dist/web/assets/{dagre-CPhI6v-K.js → dagre-CkhlMHnx.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-CmSE-oNj.js → dagre-KLK3FWXG-Cnp996VG.js} +1 -1
- package/dist/web/assets/database-CgTomMxt.js +1 -0
- package/dist/web/assets/{database-viewer-5xljX0JI.js → database-viewer-DaUoQ-oR.js} +2 -2
- package/dist/web/assets/{diagram-E7M64L7V-B5XG3ZT7.js → diagram-E7M64L7V-BZF0tSOr.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-BsP248aX.js → diagram-IFDJBPK2-nUcO8sN8.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-Cna3408N.js → diagram-P4PSJMXO-CW0eCkwC.js} +1 -1
- package/dist/web/assets/diff-viewer-BzvK3gAE.js +4 -0
- package/dist/web/assets/dist-CM0oD8tQ.js +1 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-B7SgktiR.js → erDiagram-INFDFZHY-DSkriYZ9.js} +1 -1
- package/dist/web/assets/extension-webview-CGepEw-b.js +3 -0
- package/dist/web/assets/{flowDiagram-PKNHOUZH-FOYZZ1OB.js → flowDiagram-PKNHOUZH-CFYAfZBx.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-CnHVYh9v.js → ganttDiagram-A5KZAMGK-KSn4XAU4.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-OkvBPi6H.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-0G9XxZay.js → gitGraphDiagram-K3NZZRJ6-BMgjjVys.js} +1 -1
- package/dist/web/assets/{graphlib-CNiBwlg_.js → graphlib-BWe1iK_s.js} +1 -1
- package/dist/web/assets/index-CKsEzQ4f.js +26 -0
- package/dist/web/assets/index-Chf0otez.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-BDU2_bYD.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-Diq4Cyc3.js +2 -0
- package/dist/web/assets/input-BHj0veau.js +45 -0
- package/dist/web/assets/{isEmpty-CcCb5n2-.js → isEmpty-BfLnxq-B.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-D4QCzh5J.js → ishikawaDiagram-PHBUUO56-CiVEvp8o.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CnHYNfKW.js → journeyDiagram-4ABVD52K-CG_v5Aho.js} +1 -1
- package/dist/web/assets/jsx-runtime-BRW_vwa9.js +1 -0
- package/dist/web/assets/{kanban-definition-K7BYSVSG-Bh_g3EVu.js → kanban-definition-K7BYSVSG-miB0-_Zq.js} +1 -1
- package/dist/web/assets/keybindings-store-D5zgHod8.js +1 -0
- package/dist/web/assets/{line-6d3eBADm.js → line-CSuSrJ9J.js} +1 -1
- package/dist/web/assets/{linear-cA_2lQy7.js → linear-DFN_MPsw.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CZ07F7T6.js → markdown-renderer-DSYnGywb.js} +6 -6
- package/dist/web/assets/{mermaid-parser.core-C3kd7JXM.js → mermaid-parser.core-CFdP1Z5_.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-CYiUwhr_.js → mindmap-definition-YRQLILUH-pYPWwASE.js} +1 -1
- package/dist/web/assets/{ordinal-XHK5vIzZ.js → ordinal-DpFn432U.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-BwpIpYB3.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BPgAfmes.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-D0S7jeZA.js → pieDiagram-SKSYHLDU-Dovdlvhu.js} +1 -1
- package/dist/web/assets/plus-DQGIb4mQ.js +1 -0
- package/dist/web/assets/port-forwarding-tab-vmqDKmk2.js +1 -0
- package/dist/web/assets/{postgres-viewer-RldlAO_m.js → postgres-viewer-0lIAosrr.js} +3 -3
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-0hNP63hW.js → quadrantDiagram-337W2JSQ-TXe6cU_F.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-TqxBkWx-.js +1 -0
- package/dist/web/assets/refresh-cw-Clk8fdUD.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BVnmqFbL.js → requirementDiagram-Z7DCOOCP-CuiiuGS9.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DVkYdCJb.js → sankeyDiagram-WA2Y5GQK-BbRmhv0t.js} +1 -1
- package/dist/web/assets/scroll-area-BpXCNme3.js +1 -0
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-B80s7sOg.js → sequenceDiagram-2WXFIKYE-B2D8IQDb.js} +1 -1
- package/dist/web/assets/settings-tab-CMnv1fce.js +1 -0
- package/dist/web/assets/{sql-query-editor-CjZ7Z6XL.js → sql-query-editor-Bc2hAwqT.js} +1 -1
- package/dist/web/assets/sqlite-viewer-B60MS2Dy.js +1 -0
- package/dist/web/assets/square-vBdqj0bF.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-BPLXgXRR.js → stateDiagram-RAJIS63D-ylr4HxPu.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D6zvxf3M.js +1 -0
- package/dist/web/assets/table-Bi27fEaN.js +1 -0
- package/dist/web/assets/{terminal-tab-DjzD8GLn.js → terminal-tab-CCJoLstH.js} +2 -2
- package/dist/web/assets/text-wrap-D_OmSzhp.js +1 -0
- package/dist/web/assets/{timeline-definition-YZTLITO2-fa_51u1X.js → timeline-definition-YZTLITO2-pMv1grvM.js} +1 -1
- package/dist/web/assets/trash-2-CNuB-htI.js +1 -0
- package/dist/web/assets/treemap-KZPCXAKY-Kck06FKU.js +1 -0
- package/dist/web/assets/{use-monaco-theme-D9XFxQuU.js → use-monaco-theme-BJK48EmK.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-kX4jJn6W.js → vennDiagram-LZ73GAT5-C-rkIUbo.js} +1 -1
- package/dist/web/assets/x-Dw3TjeY_.js +1 -0
- package/dist/web/assets/{xychartDiagram-JWTSCODW-Bzm5lZBs.js → xychartDiagram-JWTSCODW-CtpjAakO.js} +1 -1
- package/dist/web/index.html +18 -22
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +169 -13
- package/docs/extension-development-guide.md +98 -1
- package/docs/journals/260414-1400-ext-git-graph-port-complete.md +147 -0
- package/docs/journals/260414-1452-git-graph-faithful-port.md +144 -0
- package/docs/journals/260414-1810-git-graph-ui-improvements-complete.md +261 -0
- package/docs/journals/260414-2001-bundled-extensions.md +219 -0
- package/docs/project-changelog.md +123 -21
- package/docs/project-roadmap.md +4 -2
- package/docs/system-architecture.md +77 -6
- package/package.json +1 -1
- package/packages/ext-git-graph/package.json +30 -0
- package/packages/ext-git-graph/src/extension-integration.test.ts +230 -0
- package/packages/ext-git-graph/src/extension-parsers.test.ts +193 -0
- package/packages/ext-git-graph/src/extension.ts +921 -0
- package/packages/ext-git-graph/src/git-log-parser.test.ts +271 -0
- package/packages/ext-git-graph/src/git-log-parser.ts +38 -0
- package/packages/ext-git-graph/src/types.ts +192 -0
- package/packages/ext-git-graph/src/webview-html.test.ts +142 -0
- package/packages/ext-git-graph/src/webview-html.ts +2417 -0
- package/packages/vscode-compat/src/index.ts +4 -0
- package/packages/vscode-compat/src/process.ts +25 -0
- package/packages/vscode-compat/src/window.ts +10 -0
- package/src/cli/commands/ext-cmd.ts +3 -1
- package/src/server/ws/extensions.ts +34 -4
- package/src/services/contribution-registry.ts +14 -1
- package/src/services/extension-host-worker.ts +12 -3
- package/src/services/extension-manifest.ts +18 -1
- package/src/services/extension-rpc-handlers.ts +68 -2
- package/src/services/extension.service.ts +63 -9
- package/src/types/extension-messages.ts +3 -1
- package/src/types/extension.ts +8 -0
- package/src/web/components/editor/code-editor.tsx +16 -4
- package/src/web/components/editor/conflict-editor.tsx +368 -0
- package/src/web/components/extensions/extension-webview.tsx +153 -12
- package/src/web/components/layout/command-palette.tsx +41 -17
- package/src/web/components/layout/editor-panel.tsx +16 -4
- package/src/web/components/layout/mobile-nav.tsx +6 -5
- package/src/web/components/layout/tab-bar.tsx +3 -3
- package/src/web/components/layout/tab-content.tsx +17 -5
- package/src/web/components/settings/keyboard-shortcuts-section.tsx +46 -1
- package/src/web/hooks/use-extension-ws.ts +30 -4
- package/src/web/hooks/use-global-keybindings.ts +24 -2
- package/src/web/hooks/use-url-sync.ts +8 -3
- package/src/web/stores/extension-store.ts +8 -0
- package/src/web/stores/keybindings-store.ts +2 -3
- package/src/web/stores/panel-store.ts +2 -2
- package/src/web/stores/panel-utils.ts +6 -2
- package/src/web/stores/tab-store.ts +3 -2
- package/dist/web/assets/ai-settings-section-D6d-RmR6.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-DpVzOETR.js +0 -1
- package/dist/web/assets/arrow-up-BigIMx-e.js +0 -1
- package/dist/web/assets/channel-Cgy1thYT.js +0 -1
- package/dist/web/assets/chat-tab-DXBb9Y3U.js +0 -10
- package/dist/web/assets/check-ePA3ZvK4.js +0 -1
- package/dist/web/assets/chevron-down-EQA06nR-.js +0 -1
- package/dist/web/assets/chevron-right-CXzzT44u.js +0 -1
- package/dist/web/assets/chunk-GLR3WWYH-CxUl1sdz.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-DN7ebS2Y.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-C4La7oLj.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-C3IyfqG-.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Dcvhz2pb.js +0 -1
- package/dist/web/assets/clone--C7Tby8z.js +0 -1
- package/dist/web/assets/code-editor-Cr7JrBKC.js +0 -8
- package/dist/web/assets/columns-2-BZ9uqssV.js +0 -1
- package/dist/web/assets/createLucideIcon-PuMiQgHl.js +0 -1
- package/dist/web/assets/database-D1ToEV9d.js +0 -1
- package/dist/web/assets/diff-viewer-BBr6e_gb.js +0 -4
- package/dist/web/assets/dist-KUoHa6tg.js +0 -1
- package/dist/web/assets/extension-webview-B0klBip8.js +0 -3
- package/dist/web/assets/eye-CNcBU6Tx.js +0 -1
- package/dist/web/assets/git-graph-CDiwGa0g.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-DcPyMEIJ.js +0 -1
- package/dist/web/assets/index-CkaCzNgO.css +0 -2
- package/dist/web/assets/index-Ic5uTu20.js +0 -26
- package/dist/web/assets/info-3K5VOQVL-Dw4O15cw.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-DFhmsucr.js +0 -2
- package/dist/web/assets/input-CcbTF6ih.js +0 -45
- package/dist/web/assets/jsx-runtime-R_NjdZtX.js +0 -1
- package/dist/web/assets/keybindings-store-CxE6BlG2.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-o3LmdL8H.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BjNP0M3B.js +0 -1
- package/dist/web/assets/plus-Iso5r9vD.js +0 -1
- package/dist/web/assets/port-forwarding-tab-BPuSc6pI.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-gDgOiaME.js +0 -1
- package/dist/web/assets/refresh-cw-BgQzFNaG.js +0 -1
- package/dist/web/assets/scroll-area-i4EZlOl_.js +0 -1
- package/dist/web/assets/settings-tab-BzSSN2BQ.js +0 -1
- package/dist/web/assets/sqlite-viewer-CoyZOM_Y.js +0 -1
- package/dist/web/assets/square-pfn_LYYy.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DksQJ7es.js +0 -1
- package/dist/web/assets/table-CHv2x_qg.js +0 -1
- package/dist/web/assets/tag-Bb_UFXt0.js +0 -1
- package/dist/web/assets/text-wrap-D8BbQYTx.js +0 -1
- package/dist/web/assets/trash-2-DYCa06CV.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-DwFqAvnj.js +0 -1
- package/dist/web/assets/x-BXecj-16.js +0 -1
- package/src/web/components/git/git-graph-branch-label.tsx +0 -124
- package/src/web/components/git/git-graph-constants.ts +0 -185
- package/src/web/components/git/git-graph-detail.tsx +0 -107
- package/src/web/components/git/git-graph-dialog.tsx +0 -72
- package/src/web/components/git/git-graph-row.tsx +0 -167
- package/src/web/components/git/git-graph-settings-dialog.tsx +0 -104
- package/src/web/components/git/git-graph-svg.tsx +0 -54
- package/src/web/components/git/git-graph-toolbar.tsx +0 -195
- package/src/web/components/git/git-graph.tsx +0 -193
- package/src/web/components/git/use-column-resize.ts +0 -33
- package/src/web/components/git/use-git-graph.ts +0 -201
- /package/dist/web/assets/{api-client-wQbeUyeh.js → api-client-BvxmRZUi.js} +0 -0
- /package/dist/web/assets/{array-X0JlPOfd.js → array-BFDiaBgf.js} +0 -0
- /package/dist/web/assets/{csv-parser-CElqio6o.js → csv-parser-i7fjqP2H.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-BfIOPvwt.js → cytoscape.esm-C8i2jUzT.js} +0 -0
- /package/dist/web/assets/{defaultLocale-B6RGN4id.js → defaultLocale-ZeknFqNe.js} +0 -0
- /package/dist/web/assets/{dist-CK1enexV.js → dist-DZmJeHOA.js} +0 -0
- /package/dist/web/assets/{init-BmUWJJHz.js → init-0VJVrkRJ.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-BrCM-iA1.js → isArrayLikeObject-ClzWCpcm.js} +0 -0
- /package/dist/web/assets/{katex-xQS_6bNb.js → katex-DR0kdMDv.js} +0 -0
- /package/dist/web/assets/{lib-CfWBrYll.js → lib-DSLzfeW0.js} +0 -0
- /package/dist/web/assets/{math-CpLFzrfV.js → math-CRc16Nj6.js} +0 -0
- /package/dist/web/assets/{path-CoPyR7c2.js → path-INs8XTPH.js} +0 -0
- /package/dist/web/assets/{preload-helper-CH6UZRzu.js → preload-helper-mr3rCizq.js} +0 -0
- /package/dist/web/assets/{react-j5zqhEum.js → react-0tkk-ztn.js} +0 -0
- /package/dist/web/assets/{rough.esm-D5NinLFK.js → rough.esm-eLccZ4OJ.js} +0 -0
- /package/dist/web/assets/{sql-completion-provider-D0xutVaK.js → sql-completion-provider-B8uUWWej.js} +0 -0
- /package/dist/web/assets/{src-j04igtQ5.js → src-CqyWLlNZ.js} +0 -0
- /package/dist/web/assets/{utils-CSCvNZxE.js → utils-DX8jb5qv.js} +0 -0
|
@@ -0,0 +1,2417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate the complete webview HTML for the git graph panel.
|
|
3
|
+
* All JS + CSS is inlined since webview runs in an iframe sandbox.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function getWebviewHtml(): string {
|
|
7
|
+
return `<!DOCTYPE html>
|
|
8
|
+
<html>
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="utf-8">
|
|
11
|
+
<style>
|
|
12
|
+
${getStyles()}
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div id="app">
|
|
17
|
+
<header id="toolbar">
|
|
18
|
+
<div class="toolbar-left">
|
|
19
|
+
<div id="branch-selector" class="branch-dropdown">
|
|
20
|
+
<button id="branch-trigger" class="branch-trigger">All Branches</button>
|
|
21
|
+
<div id="branch-dropdown-menu" class="branch-dropdown-menu hidden">
|
|
22
|
+
<input id="branch-filter" type="text" placeholder="Filter branches..." class="branch-filter-input" />
|
|
23
|
+
<div id="branch-list" class="branch-list"></div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<button id="btn-refresh" title="Refresh"></button>
|
|
27
|
+
<button id="btn-fetch" title="Fetch from remotes"></button>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="toolbar-right">
|
|
30
|
+
<div class="stash-dropdown">
|
|
31
|
+
<button id="btn-stash" title="Stashes"></button>
|
|
32
|
+
<div id="stash-popover" class="stash-popover hidden">
|
|
33
|
+
<div class="stash-popover-header"><span>Stashes</span></div>
|
|
34
|
+
<div id="stash-list" class="stash-list"></div>
|
|
35
|
+
<div class="stash-popover-footer">
|
|
36
|
+
<button id="stash-save" class="btn-sm">+ Stash Changes</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="worktree-dropdown">
|
|
41
|
+
<button id="btn-worktree" title="Worktrees"></button>
|
|
42
|
+
<div id="worktree-popover" class="worktree-popover hidden">
|
|
43
|
+
<div class="worktree-popover-header">
|
|
44
|
+
<span>Worktrees</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div id="worktree-list" class="worktree-list"></div>
|
|
47
|
+
<div class="worktree-popover-footer">
|
|
48
|
+
<button id="wt-add" class="btn-sm">+ Add Worktree</button>
|
|
49
|
+
<button id="wt-prune" class="btn-sm secondary" title="Remove stale worktree entries">Prune</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<button id="btn-find" title="Find (Ctrl+F)"></button>
|
|
54
|
+
<button id="btn-settings" title="Settings"></button>
|
|
55
|
+
</div>
|
|
56
|
+
</header>
|
|
57
|
+
<div id="find-bar" class="find-bar hidden">
|
|
58
|
+
<input id="find-input" type="text" placeholder="Search commits..." />
|
|
59
|
+
<span id="find-count"></span>
|
|
60
|
+
<button id="find-prev" title="Previous">↑</button>
|
|
61
|
+
<button id="find-next" title="Next">↓</button>
|
|
62
|
+
<button id="find-close" title="Close">×</button>
|
|
63
|
+
</div>
|
|
64
|
+
<div id="graph-container">
|
|
65
|
+
<div id="graph-header" class="commit-row header-row">
|
|
66
|
+
<div class="col-graph">Graph<div class="graph-resize-handle" id="graph-resize-handle"></div></div>
|
|
67
|
+
<div class="col-message">Message</div>
|
|
68
|
+
<div class="col-author">Author</div>
|
|
69
|
+
<div class="col-date">Date</div>
|
|
70
|
+
<div class="col-hash">Hash</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div id="commit-list-wrapper">
|
|
73
|
+
<div id="graph-svg-container"></div>
|
|
74
|
+
<div id="commit-list"></div>
|
|
75
|
+
</div>
|
|
76
|
+
<div id="loading" class="loading hidden">Loading...</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div id="detail-panel" class="detail-panel hidden"></div>
|
|
79
|
+
<div id="settings-panel" class="settings-panel">
|
|
80
|
+
<div class="settings-header">
|
|
81
|
+
<h3>Git Graph Settings</h3>
|
|
82
|
+
<button id="settings-close" title="Close">×</button>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="settings-body">
|
|
85
|
+
<details class="settings-section" open>
|
|
86
|
+
<summary>General</summary>
|
|
87
|
+
<div class="setting-row"><label>Max Commits</label><input type="number" id="s-maxCommits" min="10" max="10000" step="50"></div>
|
|
88
|
+
<div class="setting-row"><label>Show Tags</label><input type="checkbox" id="s-showTags"></div>
|
|
89
|
+
<div class="setting-row"><label>Show Stashes</label><input type="checkbox" id="s-showStashes"></div>
|
|
90
|
+
<div class="setting-row"><label>Show Remote Branches</label><input type="checkbox" id="s-showRemoteBranches"></div>
|
|
91
|
+
<div class="setting-row"><label>Graph Style</label><select id="s-graphStyle"><option value="rounded">Rounded</option><option value="angular">Angular</option></select></div>
|
|
92
|
+
<div class="setting-row"><label>First Parent Only</label><input type="checkbox" id="s-firstParentOnly"></div>
|
|
93
|
+
<div class="setting-row"><label>Date Format</label><select id="s-dateFormat"><option value="relative">Relative</option><option value="absolute">Absolute</option><option value="iso">ISO</option></select></div>
|
|
94
|
+
<div class="setting-row"><label>Commit Ordering</label><select id="s-commitOrdering"><option value="topo">Topological</option><option value="date">Date</option><option value="author-date">Author Date</option></select></div>
|
|
95
|
+
<div class="setting-row"><label>Auto Fetch Interval</label><select id="s-autoFetchInterval"><option value="0">Disabled</option><option value="10">10 seconds</option><option value="30">30 seconds</option><option value="60">1 minute</option><option value="120">2 minutes</option><option value="300">5 minutes</option></select></div>
|
|
96
|
+
</details>
|
|
97
|
+
<details class="settings-section" open>
|
|
98
|
+
<summary>User Details</summary>
|
|
99
|
+
<div class="setting-row"><label>Name</label><input type="text" id="s-userName" placeholder="user.name"></div>
|
|
100
|
+
<div class="setting-row"><label>Email</label><input type="text" id="s-userEmail" placeholder="user.email"></div>
|
|
101
|
+
<div class="setting-row" style="justify-content:flex-end"><button id="s-saveUser" class="btn-sm">Save User Details</button></div>
|
|
102
|
+
</details>
|
|
103
|
+
<details class="settings-section" open>
|
|
104
|
+
<summary>Remotes</summary>
|
|
105
|
+
<div id="s-remotes-list"></div>
|
|
106
|
+
<div class="add-remote-form">
|
|
107
|
+
<input type="text" id="s-newRemoteName" placeholder="Remote name">
|
|
108
|
+
<input type="text" id="s-newRemoteUrl" placeholder="Remote URL">
|
|
109
|
+
<button id="s-addRemote" class="btn-sm">Add Remote</button>
|
|
110
|
+
</div>
|
|
111
|
+
</details>
|
|
112
|
+
<details class="settings-section">
|
|
113
|
+
<summary>Issue Linking</summary>
|
|
114
|
+
<p style="font-size:11px;color:var(--subtext);margin-bottom:6px">Turn issue references in commit messages into clickable links.</p>
|
|
115
|
+
<div id="issue-rules-list"></div>
|
|
116
|
+
<button id="add-issue-rule" class="btn-sm" style="margin-top:6px">+ Add Rule</button>
|
|
117
|
+
</details>
|
|
118
|
+
<details class="settings-section">
|
|
119
|
+
<summary>Pull Request Creation</summary>
|
|
120
|
+
<div class="setting-row"><label>Provider</label><select id="pr-provider"><option value="">Disabled</option><option value="github">GitHub</option><option value="gitlab">GitLab</option><option value="bitbucket">Bitbucket</option><option value="custom">Custom</option></select></div>
|
|
121
|
+
<div id="pr-config" class="hidden">
|
|
122
|
+
<div class="setting-row"><label>Owner</label><input type="text" id="pr-owner" placeholder="owner or org"></div>
|
|
123
|
+
<div class="setting-row"><label>Repo</label><input type="text" id="pr-repo" placeholder="repository name"></div>
|
|
124
|
+
<div class="setting-row"><label>Target Branch</label><input type="text" id="pr-target" placeholder="main"></div>
|
|
125
|
+
<div class="setting-row"><label>URL Template</label><input type="text" id="pr-url-template" placeholder="https://..."></div>
|
|
126
|
+
<p style="font-size:10px;color:var(--subtext);margin:2px 0 4px">Variables: \${owner}, \${repo}, \${sourceBranch}, \${targetBranch}</p>
|
|
127
|
+
<div class="setting-row" style="justify-content:flex-end"><button id="pr-save" class="btn-sm">Save PR Config</button></div>
|
|
128
|
+
</div>
|
|
129
|
+
</details>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div id="status-bar">
|
|
133
|
+
<span id="status-text">Loading repository...</span>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div id="context-menu" class="context-menu hidden"></div>
|
|
137
|
+
<script>
|
|
138
|
+
${getScript()}
|
|
139
|
+
</script>
|
|
140
|
+
</body>
|
|
141
|
+
</html>`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getStyles(): string {
|
|
145
|
+
return `
|
|
146
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
147
|
+
:root {
|
|
148
|
+
--bg: #ffffff; --surface: #f4f4f5; --text: #09090b; --subtext: #71717a; --subtle: #a1a1aa;
|
|
149
|
+
--border: #e4e4e7; --border2: #d4d4d8; --blue: #3b82f6; --red: #ef4444; --green: #22c55e;
|
|
150
|
+
--yellow: #eab308; --purple: #8b5cf6; --orange: #f97316;
|
|
151
|
+
--surface-hover: #f4f4f5; --selected: #eff6ff;
|
|
152
|
+
}
|
|
153
|
+
@media (prefers-color-scheme: dark) {
|
|
154
|
+
:root {
|
|
155
|
+
--bg: #09090b; --surface: #18181b; --text: #fafafa; --subtext: #a1a1aa; --subtle: #52525b;
|
|
156
|
+
--border: #27272a; --border2: #3f3f46; --selected: #1e293b; --surface-hover: #27272a;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); font-size: 13px; overflow: hidden; height: 100vh; display: flex; flex-direction: column; }
|
|
160
|
+
#app { display: flex; flex-direction: column; height: 100vh; }
|
|
161
|
+
|
|
162
|
+
/* Toolbar */
|
|
163
|
+
#toolbar { display: flex; justify-content: space-between; align-items: center; padding: 6px 12px; border-bottom: 1px solid var(--border); background: var(--surface); flex-shrink: 0; }
|
|
164
|
+
.toolbar-left, .toolbar-right { display: flex; align-items: center; gap: 6px; }
|
|
165
|
+
select { background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 8px; font-size: 12px; }
|
|
166
|
+
button { background: transparent; color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; min-width: 28px; min-height: 28px; transition: background 0.15s, border-color 0.15s; }
|
|
167
|
+
button:hover { background: var(--surface-hover); border-color: var(--border2); }
|
|
168
|
+
button:active { background: var(--surface); }
|
|
169
|
+
.btn-fetching { opacity: 0.6; pointer-events: none; }
|
|
170
|
+
|
|
171
|
+
/* Branch dropdown */
|
|
172
|
+
.branch-dropdown { position: relative; }
|
|
173
|
+
.branch-trigger { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 4px 24px 4px 8px; font-size: 12px; cursor: pointer; min-width: 140px; text-align: left; position: relative; }
|
|
174
|
+
.branch-trigger::after { content: '\\25BC'; font-size: 8px; position: absolute; right: 8px; top: 50%; transform: translateY(-50%); color: var(--subtext); }
|
|
175
|
+
.branch-dropdown-menu { position: absolute; top: 100%; left: 0; z-index: 60; background: var(--surface); border: 1px solid var(--border2); border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 220px; max-height: 300px; display: flex; flex-direction: column; margin-top: 2px; }
|
|
176
|
+
.branch-filter-input { padding: 6px 8px; border: none; border-bottom: 1px solid var(--border); background: var(--bg); color: var(--text); font-size: 12px; outline: none; border-radius: 6px 6px 0 0; }
|
|
177
|
+
.branch-list { overflow-y: auto; max-height: 250px; }
|
|
178
|
+
.branch-option { padding: 6px 10px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 6px; }
|
|
179
|
+
.branch-option:hover { background: var(--surface-hover); }
|
|
180
|
+
.branch-option.selected { background: var(--selected); font-weight: 600; }
|
|
181
|
+
|
|
182
|
+
/* Worktree popover */
|
|
183
|
+
.worktree-dropdown { position: relative; }
|
|
184
|
+
#btn-worktree { display: flex; align-items: center; gap: 4px; font-size: 12px; padding: 4px 8px; }
|
|
185
|
+
#btn-worktree .wt-count { background: var(--accent, #58a6ff); color: #fff; font-size: 10px; border-radius: 8px; padding: 0 5px; min-width: 16px; text-align: center; line-height: 16px; }
|
|
186
|
+
.worktree-popover { position: absolute; top: 100%; right: 0; z-index: 60; background: var(--surface); border: 1px solid var(--border2); border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.2); min-width: 300px; max-width: 400px; margin-top: 4px; display: flex; flex-direction: column; }
|
|
187
|
+
.worktree-popover-header { padding: 8px 12px; font-size: 12px; font-weight: 600; border-bottom: 1px solid var(--border); }
|
|
188
|
+
.worktree-list { overflow-y: auto; max-height: 240px; }
|
|
189
|
+
.wt-item { padding: 8px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
|
190
|
+
.wt-item:last-child { border-bottom: none; }
|
|
191
|
+
.wt-item-info { flex: 1; min-width: 0; }
|
|
192
|
+
.wt-item-branch { font-weight: 600; display: flex; align-items: center; gap: 4px; }
|
|
193
|
+
.wt-item-path { font-size: 10px; color: var(--subtext); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
194
|
+
.wt-badge { font-size: 9px; padding: 1px 4px; border-radius: 4px; background: var(--border); color: var(--subtext); }
|
|
195
|
+
.wt-badge-current { background: var(--accent, #58a6ff); color: #fff; }
|
|
196
|
+
.wt-item-active { background: var(--selected); }
|
|
197
|
+
.wt-badge-locked { background: #d29922; color: #fff; }
|
|
198
|
+
.wt-item-actions { display: flex; gap: 4px; flex-shrink: 0; }
|
|
199
|
+
.wt-item-actions button { min-width: 24px; min-height: 24px; padding: 2px 6px; font-size: 10px; }
|
|
200
|
+
.worktree-popover-footer { padding: 8px 12px; border-top: 1px solid var(--border); display: flex; gap: 6px; }
|
|
201
|
+
.worktree-popover-footer .btn-sm { flex: 1; }
|
|
202
|
+
.wt-empty { padding: 16px; text-align: center; font-size: 11px; color: var(--subtext); }
|
|
203
|
+
@media (max-width: 768px) { .branch-option { padding: 10px 12px; min-height: 44px; } }
|
|
204
|
+
|
|
205
|
+
/* Stash popover */
|
|
206
|
+
.stash-dropdown { position: relative; }
|
|
207
|
+
#btn-stash { display: flex; align-items: center; gap: 4px; font-size: 12px; padding: 4px 8px; }
|
|
208
|
+
#btn-stash .stash-count { background: var(--accent, #58a6ff); color: #fff; font-size: 10px; border-radius: 8px; padding: 0 5px; min-width: 16px; text-align: center; line-height: 16px; }
|
|
209
|
+
.stash-popover { position: absolute; top: 100%; right: 0; z-index: 60; background: var(--surface); border: 1px solid var(--border2); border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.2); min-width: 300px; max-width: 400px; margin-top: 4px; display: flex; flex-direction: column; }
|
|
210
|
+
.stash-popover-header { padding: 8px 12px; font-size: 12px; font-weight: 600; border-bottom: 1px solid var(--border); }
|
|
211
|
+
.stash-list { overflow-y: auto; max-height: 240px; }
|
|
212
|
+
.stash-item { padding: 8px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
|
213
|
+
.stash-item:last-child { border-bottom: none; }
|
|
214
|
+
.stash-item-info { flex: 1; min-width: 0; }
|
|
215
|
+
.stash-item-ref { font-weight: 600; font-size: 11px; color: var(--subtext); }
|
|
216
|
+
.stash-item-msg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
217
|
+
.stash-item-actions { display: flex; gap: 4px; flex-shrink: 0; }
|
|
218
|
+
.stash-item-actions button { min-width: 24px; min-height: 24px; padding: 2px 6px; font-size: 10px; }
|
|
219
|
+
.stash-empty { padding: 16px; text-align: center; font-size: 11px; color: var(--subtext); }
|
|
220
|
+
.stash-popover-footer { padding: 8px 12px; border-top: 1px solid var(--border); display: flex; gap: 6px; }
|
|
221
|
+
.stash-popover-footer .btn-sm { flex: 1; }
|
|
222
|
+
|
|
223
|
+
/* Merge/rebase banner */
|
|
224
|
+
.merge-banner { padding: 8px 12px; background: rgba(133, 77, 14, 0.12); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 12px; flex-shrink: 0; }
|
|
225
|
+
.merge-banner .banner-icon { color: #eab308; }
|
|
226
|
+
.merge-banner .banner-text { flex: 1; }
|
|
227
|
+
.merge-banner .banner-actions { display: flex; gap: 4px; }
|
|
228
|
+
.merge-banner .banner-actions button { font-size: 11px; padding: 2px 8px; }
|
|
229
|
+
.merge-banner .btn-continue { background: var(--green); color: #fff; border-color: transparent; }
|
|
230
|
+
.merge-banner .btn-abort { background: var(--red); color: #fff; border-color: transparent; }
|
|
231
|
+
|
|
232
|
+
/* Conflict section */
|
|
233
|
+
.conflict-header { padding: 4px 0; font-size: 12px; font-weight: 600; color: var(--red); display: flex; align-items: center; gap: 4px; }
|
|
234
|
+
.file-status-U { color: var(--red); font-weight: bold; }
|
|
235
|
+
|
|
236
|
+
/* Find bar */
|
|
237
|
+
.find-bar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-bottom: 1px solid var(--border); background: var(--surface); }
|
|
238
|
+
.find-bar input { flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 8px; font-size: 12px; }
|
|
239
|
+
.find-bar input:focus { outline: none; border-color: var(--blue); }
|
|
240
|
+
#find-count { font-size: 11px; color: var(--subtext); min-width: 60px; }
|
|
241
|
+
.hidden { display: none !important; }
|
|
242
|
+
|
|
243
|
+
/* Graph container */
|
|
244
|
+
#graph-container { flex: 1; overflow-y: auto; overflow-x: hidden; }
|
|
245
|
+
.commit-row { display: flex; align-items: center; cursor: pointer; min-height: 28px; padding: 0 8px; }
|
|
246
|
+
.commit-row:hover { background: var(--surface-hover); }
|
|
247
|
+
.commit-row.selected { background: var(--selected); }
|
|
248
|
+
.commit-row.header-row { background: var(--surface); cursor: default; font-weight: 600; font-size: 11px; color: var(--subtext); text-transform: uppercase; letter-spacing: 0.5px; position: sticky; top: 0; z-index: 2; border-bottom: 1px solid var(--border); }
|
|
249
|
+
.commit-row.search-match { background: rgba(234, 179, 8, 0.15); }
|
|
250
|
+
.commit-row.virtual { opacity: 0.85; font-style: italic; }
|
|
251
|
+
.commit-row.virtual .col-message { color: var(--subtext); }
|
|
252
|
+
.file-clickable { cursor: pointer; border-radius: 3px; padding: 2px 4px; margin: 0 -4px; }
|
|
253
|
+
.file-clickable:hover { background: var(--surface-hover); }
|
|
254
|
+
.col-graph { width: var(--graph-col-w, 120px); min-width: var(--graph-col-w, 80px); overflow: hidden; flex-shrink: 0; position: relative; }
|
|
255
|
+
.graph-resize-handle { position: absolute; right: 0; top: 0; bottom: 0; width: 6px; cursor: col-resize; z-index: 3; background: transparent; }
|
|
256
|
+
.graph-resize-handle:hover, .graph-resize-handle.dragging { background: var(--blue); opacity: 0.5; }
|
|
257
|
+
.col-message { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 8px; }
|
|
258
|
+
.col-author { width: 120px; min-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--subtext); font-size: 12px; }
|
|
259
|
+
.col-date { width: 100px; min-width: 100px; color: var(--subtext); font-size: 12px; }
|
|
260
|
+
.col-hash { width: 70px; min-width: 70px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; color: var(--subtle); }
|
|
261
|
+
|
|
262
|
+
/* Ref badges */
|
|
263
|
+
.ref-badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-right: 4px; vertical-align: middle; }
|
|
264
|
+
.ref-head { background: var(--green); color: #fff; }
|
|
265
|
+
.ref-local { background: var(--blue); color: #fff; }
|
|
266
|
+
.ref-remote { background: var(--purple); color: #fff; }
|
|
267
|
+
.ref-tag { background: var(--yellow); color: #000; }
|
|
268
|
+
|
|
269
|
+
/* SVG graph — single SVG overlay */
|
|
270
|
+
#commit-list-wrapper { position: relative; }
|
|
271
|
+
#graph-svg-container { position: absolute; top: 0; left: 8px; z-index: 1; pointer-events: none; }
|
|
272
|
+
#graph-svg-container circle { pointer-events: auto; cursor: pointer; }
|
|
273
|
+
#graph-svg-container .line { stroke-width: 2; fill: none; }
|
|
274
|
+
#graph-svg-container .graphCurrent { fill: var(--bg); stroke-width: 2; }
|
|
275
|
+
#graph-svg-container .graphStashOuter { fill: none; stroke: #808080; stroke-width: 1.5; }
|
|
276
|
+
#graph-svg-container .graphStashInner { fill: #808080; }
|
|
277
|
+
.commit-row.graph-hover { background: var(--surface-hover); }
|
|
278
|
+
|
|
279
|
+
/* Detail panel */
|
|
280
|
+
.detail-panel { border-top: 1px solid var(--border2); background: var(--surface); max-height: 40vh; overflow-y: auto; padding: 12px 16px; flex-shrink: 0; }
|
|
281
|
+
.detail-panel h3 { font-size: 14px; margin-bottom: 8px; }
|
|
282
|
+
.detail-field { margin-bottom: 4px; font-size: 12px; }
|
|
283
|
+
.detail-field .label { color: var(--subtext); display: inline-block; width: 80px; }
|
|
284
|
+
.detail-message { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 8px; margin: 8px 0; font-size: 12px; white-space: pre-wrap; font-family: 'SF Mono', 'Fira Code', monospace; }
|
|
285
|
+
.file-list { margin-top: 8px; }
|
|
286
|
+
.file-item { display: flex; align-items: center; gap: 6px; padding: 2px 0; font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace; }
|
|
287
|
+
.file-status { display: inline-block; width: 16px; text-align: center; font-weight: 700; font-size: 11px; }
|
|
288
|
+
.file-status-A { color: var(--green); }
|
|
289
|
+
.file-status-M { color: var(--yellow); }
|
|
290
|
+
.file-status-D { color: var(--red); }
|
|
291
|
+
.file-status-R { color: var(--blue); }
|
|
292
|
+
.file-stat { color: var(--subtext); font-size: 11px; margin-left: auto; }
|
|
293
|
+
.file-stat .add { color: var(--green); }
|
|
294
|
+
.file-stat .del { color: var(--red); }
|
|
295
|
+
|
|
296
|
+
/* File view toggle */
|
|
297
|
+
.file-view-toggle { display: flex; gap: 2px; margin-bottom: 6px; }
|
|
298
|
+
.toggle-btn { padding: 2px 6px; font-size: 12px; min-width: 28px; min-height: 28px; border: 1px solid var(--border); border-radius: 4px; background: transparent; cursor: pointer; }
|
|
299
|
+
.toggle-btn.active { background: var(--surface-hover); border-color: var(--border2); }
|
|
300
|
+
.tree-dir { display: flex; align-items: center; gap: 4px; padding: 2px 0; font-size: 12px; color: var(--subtext); }
|
|
301
|
+
.tree-dir-name { font-weight: 500; color: var(--text); }
|
|
302
|
+
.tree-dir-count { font-size: 11px; color: var(--subtle); }
|
|
303
|
+
|
|
304
|
+
/* File actions */
|
|
305
|
+
.file-actions { display: flex; gap: 2px; margin-left: auto; flex-shrink: 0; }
|
|
306
|
+
.file-action-btn { min-width: 24px; min-height: 24px; padding: 0 4px; border: none; background: transparent; cursor: pointer; border-radius: 4px; font-size: 12px; color: var(--subtext); display: flex; align-items: center; justify-content: center; }
|
|
307
|
+
.file-action-btn:hover { background: var(--surface-hover); color: var(--text); }
|
|
308
|
+
.file-action-btn[data-action="discard"]:hover { color: var(--red); }
|
|
309
|
+
.section-actions { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
|
|
310
|
+
.commit-section { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px; }
|
|
311
|
+
.commit-section textarea { width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 6px; padding: 8px; font-size: 12px; font-family: inherit; resize: vertical; min-height: 60px; }
|
|
312
|
+
.commit-section textarea:focus { outline: none; border-color: var(--blue); }
|
|
313
|
+
.commit-actions { display: flex; justify-content: flex-end; margin-top: 6px; gap: 6px; }
|
|
314
|
+
.btn-commit { background: var(--green); color: #fff; border-color: transparent; padding: 4px 16px; font-weight: 600; }
|
|
315
|
+
.btn-commit:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
316
|
+
.btn-commit:hover:not(:disabled) { opacity: 0.9; }
|
|
317
|
+
@media (max-width: 768px) { .file-action-btn { min-width: 36px; min-height: 36px; } }
|
|
318
|
+
|
|
319
|
+
/* Status bar */
|
|
320
|
+
#status-bar { display: flex; align-items: center; padding: 4px 12px; border-top: 1px solid var(--border); background: var(--surface); font-size: 11px; color: var(--subtext); flex-shrink: 0; }
|
|
321
|
+
|
|
322
|
+
/* Context menu */
|
|
323
|
+
.context-menu { position: fixed; background: var(--surface); border: 1px solid var(--border2); border-radius: 6px; padding: 4px 0; min-width: 180px; z-index: 100; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
|
324
|
+
.ctx-item { padding: 6px 12px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 8px; }
|
|
325
|
+
.ctx-item:hover { background: var(--surface-hover); }
|
|
326
|
+
.ctx-item.destructive { color: var(--red); }
|
|
327
|
+
.ctx-separator { border-top: 1px solid var(--border); margin: 4px 0; }
|
|
328
|
+
|
|
329
|
+
/* Loading */
|
|
330
|
+
.loading { text-align: center; padding: 16px; color: var(--subtext); }
|
|
331
|
+
|
|
332
|
+
/* Settings panel */
|
|
333
|
+
.settings-panel { position: absolute; right: 0; top: 0; bottom: 0; width: 340px; background: var(--surface); border-left: 1px solid var(--border2); z-index: 50; overflow-y: auto; transform: translateX(100%); transition: transform 0.2s ease; display: flex; flex-direction: column; }
|
|
334
|
+
.settings-panel.open { transform: translateX(0); }
|
|
335
|
+
.settings-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
|
336
|
+
.settings-header h3 { font-size: 14px; font-weight: 600; }
|
|
337
|
+
.settings-body { flex: 1; overflow-y: auto; padding: 8px 0; }
|
|
338
|
+
.settings-section { border-bottom: 1px solid var(--border); padding: 8px 14px; }
|
|
339
|
+
.settings-section summary { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--subtext); cursor: pointer; padding: 4px 0; user-select: none; }
|
|
340
|
+
.settings-section[open] summary { margin-bottom: 6px; }
|
|
341
|
+
.setting-row { display: flex; align-items: center; justify-content: space-between; padding: 5px 0; font-size: 12px; gap: 8px; }
|
|
342
|
+
.setting-row label { flex: 1; min-width: 0; }
|
|
343
|
+
.setting-row input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--blue); flex-shrink: 0; }
|
|
344
|
+
.setting-row input[type="number"] { width: 72px; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 3px 6px; font-size: 12px; }
|
|
345
|
+
.setting-row select { background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 3px 6px; font-size: 12px; min-width: 100px; }
|
|
346
|
+
.setting-row input[type="text"] { flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 3px 6px; font-size: 12px; }
|
|
347
|
+
.btn-sm { font-size: 11px; padding: 3px 10px; border-radius: 6px; min-width: 0; min-height: 0; }
|
|
348
|
+
.remote-item { padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 12px; }
|
|
349
|
+
.remote-item:last-child { border-bottom: none; }
|
|
350
|
+
.remote-item .remote-name { font-weight: 600; margin-bottom: 2px; }
|
|
351
|
+
.remote-item .remote-url { color: var(--subtext); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; word-break: break-all; }
|
|
352
|
+
.remote-actions { display: flex; gap: 4px; margin-top: 4px; }
|
|
353
|
+
.add-remote-form { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
|
|
354
|
+
.add-remote-form input { background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 6px; font-size: 12px; }
|
|
355
|
+
.issue-rule-row { display: flex; gap: 4px; align-items: center; margin-bottom: 4px; }
|
|
356
|
+
.issue-rule-row input { flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 3px 6px; font-size: 11px; font-family: 'SF Mono', 'Fira Code', monospace; }
|
|
357
|
+
.issue-rule-row input.rule-error { border-color: var(--red); }
|
|
358
|
+
.issue-rule-row .rule-remove { min-width: 24px; min-height: 24px; padding: 0; font-size: 14px; color: var(--red); border: none; }
|
|
359
|
+
@media (max-width: 768px) { .settings-panel { width: 100%; } }
|
|
360
|
+
|
|
361
|
+
/* Dialog overlay */
|
|
362
|
+
.dialog-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 200; display: flex; align-items: center; justify-content: center; }
|
|
363
|
+
.dialog { background: var(--surface); border: 1px solid var(--border2); border-radius: 8px; padding: 16px; min-width: 300px; max-width: 400px; box-shadow: 0 8px 24px rgba(0,0,0,0.2); }
|
|
364
|
+
.dialog h3 { font-size: 14px; margin-bottom: 12px; }
|
|
365
|
+
.dialog p { font-size: 12px; color: var(--subtext); margin-bottom: 12px; }
|
|
366
|
+
.dialog p.warning { color: var(--red); font-weight: 600; }
|
|
367
|
+
.dialog input, .dialog select { width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 6px 8px; font-size: 12px; margin-bottom: 12px; }
|
|
368
|
+
.dialog input:focus, .dialog select:focus { outline: none; border-color: var(--blue); }
|
|
369
|
+
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
370
|
+
.dialog-actions button { min-width: 64px; }
|
|
371
|
+
.dialog-actions .btn-primary { background: var(--blue); color: #fff; border-color: transparent; }
|
|
372
|
+
.dialog-actions .btn-danger { background: var(--red); color: #fff; border-color: transparent; }
|
|
373
|
+
|
|
374
|
+
/* Links in commit messages */
|
|
375
|
+
.commit-link { color: var(--blue); text-decoration: none; cursor: pointer; }
|
|
376
|
+
.commit-link:hover { text-decoration: underline; }
|
|
377
|
+
|
|
378
|
+
/* Toast notifications */
|
|
379
|
+
.toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); padding: 8px 16px; border-radius: 6px; font-size: 12px; z-index: 300; animation: toast-in 0.3s ease; max-width: 80%; pointer-events: none; }
|
|
380
|
+
.toast-error { background: var(--red); color: #fff; }
|
|
381
|
+
.toast-success { background: var(--green); color: #fff; }
|
|
382
|
+
.toast-info { background: var(--blue); color: #fff; }
|
|
383
|
+
@keyframes toast-in { from { opacity: 0; transform: translateX(-50%) translateY(10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
|
|
384
|
+
|
|
385
|
+
/* Touch targets for mobile */
|
|
386
|
+
@media (max-width: 768px) {
|
|
387
|
+
.commit-row { min-height: 44px; }
|
|
388
|
+
.ctx-item { padding: 10px 16px; min-height: 44px; }
|
|
389
|
+
button { min-width: 44px; min-height: 44px; }
|
|
390
|
+
.col-author, .col-hash { display: none; }
|
|
391
|
+
.col-date { width: 60px; min-width: 60px; }
|
|
392
|
+
}
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function getScript(): string {
|
|
397
|
+
return `
|
|
398
|
+
const vscode = acquireVsCodeApi();
|
|
399
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
400
|
+
const NULL_VERTEX_ID = -1;
|
|
401
|
+
const GRAPH_COLORS = ['#4ec9b0','#569cd6','#c586c0','#ce9178','#dcdcaa','#4fc1ff','#d7ba7d','#9cdcfe','#b5cea8','#d16969'];
|
|
402
|
+
const graphConfig = {
|
|
403
|
+
colours: GRAPH_COLORS,
|
|
404
|
+
grid: { x: 16, y: 28, offsetX: 8, offsetY: 14, expandY: 60 },
|
|
405
|
+
style: 'rounded'
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// --- State ---
|
|
409
|
+
const DEFAULT_SETTINGS = {
|
|
410
|
+
maxCommits: 300, showTags: true, showStashes: true, showRemoteBranches: true,
|
|
411
|
+
graphStyle: 'rounded', firstParentOnly: false, dateFormat: 'relative', commitOrdering: 'topo',
|
|
412
|
+
issueLinkingRules: [{ pattern: '#(\\\\d+)', url: '' }], prCreation: null,
|
|
413
|
+
autoFetchInterval: 0,
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const state = {
|
|
417
|
+
repo: '',
|
|
418
|
+
commits: [],
|
|
419
|
+
branches: [],
|
|
420
|
+
tags: [],
|
|
421
|
+
remotes: [],
|
|
422
|
+
stashes: [],
|
|
423
|
+
currentBranch: '',
|
|
424
|
+
head: '',
|
|
425
|
+
selectedCommit: null,
|
|
426
|
+
expandedCommit: null,
|
|
427
|
+
maxCommits: 300,
|
|
428
|
+
loading: false,
|
|
429
|
+
uncommitted: null,
|
|
430
|
+
searchMatches: [],
|
|
431
|
+
searchIndex: -1,
|
|
432
|
+
settings: { ...DEFAULT_SETTINGS },
|
|
433
|
+
userDetails: { name: '', email: '' },
|
|
434
|
+
graphColWidth: null,
|
|
435
|
+
fileViewMode: 'list',
|
|
436
|
+
worktrees: [],
|
|
437
|
+
mergeState: null,
|
|
438
|
+
_lastDetail: null,
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// --- SVG Icons ---
|
|
442
|
+
const ICONS = {
|
|
443
|
+
refresh: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>',
|
|
444
|
+
download: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
|
|
445
|
+
search: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
|
|
446
|
+
settings: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/></svg>',
|
|
447
|
+
folderOpen: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2l-1-5H4l-1 5a2 2 0 002 2z"/></svg>',
|
|
448
|
+
file: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>',
|
|
449
|
+
list: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>',
|
|
450
|
+
tree: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>',
|
|
451
|
+
plus: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
|
|
452
|
+
minus: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>',
|
|
453
|
+
x: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
|
|
454
|
+
fileOpen: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>',
|
|
455
|
+
gitBranch: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 01-9 9"/></svg>',
|
|
456
|
+
trash: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>',
|
|
457
|
+
archive: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>',
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// --- Toast notifications ---
|
|
461
|
+
function showToast(message, type) {
|
|
462
|
+
const existing = document.querySelector('.toast');
|
|
463
|
+
if (existing) existing.remove();
|
|
464
|
+
const el = document.createElement('div');
|
|
465
|
+
el.className = 'toast toast-' + (type || 'error');
|
|
466
|
+
el.textContent = message;
|
|
467
|
+
document.body.appendChild(el);
|
|
468
|
+
setTimeout(() => { if (el.parentNode) el.remove(); }, 4000);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// --- Init ---
|
|
472
|
+
document.getElementById('btn-refresh').innerHTML = ICONS.refresh;
|
|
473
|
+
document.getElementById('btn-fetch').innerHTML = ICONS.download;
|
|
474
|
+
document.getElementById('btn-find').innerHTML = ICONS.search;
|
|
475
|
+
document.getElementById('btn-settings').innerHTML = ICONS.settings;
|
|
476
|
+
document.getElementById('btn-worktree').innerHTML = ICONS.gitBranch + ' <span class="wt-count" style="display:none">0</span>';
|
|
477
|
+
document.getElementById('btn-stash').innerHTML = ICONS.archive + ' <span class="stash-count" style="display:none">0</span>';
|
|
478
|
+
vscode.postMessage({ command: 'ready' });
|
|
479
|
+
|
|
480
|
+
// --- Message handler ---
|
|
481
|
+
window.addEventListener('message', (event) => {
|
|
482
|
+
const msg = event.data;
|
|
483
|
+
switch (msg.command) {
|
|
484
|
+
case 'loadRepoInfo':
|
|
485
|
+
state.repo = msg.data.path;
|
|
486
|
+
state.branches = msg.data.branches;
|
|
487
|
+
state.tags = msg.data.tags;
|
|
488
|
+
state.remotes = msg.data.remotes;
|
|
489
|
+
state.stashes = msg.data.stashes;
|
|
490
|
+
state.head = msg.data.head;
|
|
491
|
+
state.currentBranch = msg.data.currentBranch;
|
|
492
|
+
renderBranchSelector();
|
|
493
|
+
updateStatus();
|
|
494
|
+
if (document.getElementById('settings-panel').classList.contains('open')) renderRemotesList();
|
|
495
|
+
break;
|
|
496
|
+
case 'loadCommits':
|
|
497
|
+
if (msg.append) {
|
|
498
|
+
state.commits = state.commits.concat(msg.data);
|
|
499
|
+
} else {
|
|
500
|
+
state.commits = msg.data;
|
|
501
|
+
}
|
|
502
|
+
renderCommitList();
|
|
503
|
+
updateStatus();
|
|
504
|
+
state.loading = false;
|
|
505
|
+
document.getElementById('loading').classList.add('hidden');
|
|
506
|
+
break;
|
|
507
|
+
case 'commitDetails':
|
|
508
|
+
renderDetailPanel(msg.data);
|
|
509
|
+
break;
|
|
510
|
+
case 'refresh':
|
|
511
|
+
state.commits = msg.data;
|
|
512
|
+
if (msg.repoInfo) {
|
|
513
|
+
state.branches = msg.repoInfo.branches;
|
|
514
|
+
state.tags = msg.repoInfo.tags;
|
|
515
|
+
state.remotes = msg.repoInfo.remotes;
|
|
516
|
+
state.stashes = msg.repoInfo.stashes;
|
|
517
|
+
state.head = msg.repoInfo.head;
|
|
518
|
+
state.currentBranch = msg.repoInfo.currentBranch;
|
|
519
|
+
renderBranchSelector();
|
|
520
|
+
}
|
|
521
|
+
renderCommitList();
|
|
522
|
+
updateStatus();
|
|
523
|
+
break;
|
|
524
|
+
case 'loadUncommitted':
|
|
525
|
+
state.uncommitted = msg.data;
|
|
526
|
+
state.mergeState = msg.data?.mergeState || null;
|
|
527
|
+
renderCommitList();
|
|
528
|
+
renderMergeBanner();
|
|
529
|
+
if (state.selectedCommit === 'uncommitted') {
|
|
530
|
+
const u = msg.data;
|
|
531
|
+
if (!u || (u.staged.length === 0 && u.unstaged.length === 0 && (!u.conflicted || u.conflicted.length === 0))) {
|
|
532
|
+
state.selectedCommit = null;
|
|
533
|
+
state.expandedCommit = null;
|
|
534
|
+
document.getElementById('detail-panel').classList.add('hidden');
|
|
535
|
+
} else {
|
|
536
|
+
renderUncommittedDetail();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
case 'loadSettings':
|
|
541
|
+
state.settings = { ...DEFAULT_SETTINGS, ...msg.data };
|
|
542
|
+
state.maxCommits = state.settings.maxCommits;
|
|
543
|
+
applySettingsToUI();
|
|
544
|
+
break;
|
|
545
|
+
case 'loadUserDetails':
|
|
546
|
+
state.userDetails = msg.data;
|
|
547
|
+
document.getElementById('s-userName').value = msg.data.name;
|
|
548
|
+
document.getElementById('s-userEmail').value = msg.data.email;
|
|
549
|
+
break;
|
|
550
|
+
case 'loadOwnerRepo':
|
|
551
|
+
if (msg.data.owner) document.getElementById('pr-owner').value = msg.data.owner;
|
|
552
|
+
if (msg.data.repo) document.getElementById('pr-repo').value = msg.data.repo;
|
|
553
|
+
break;
|
|
554
|
+
case 'actionResult':
|
|
555
|
+
if (msg.action === 'fetch') {
|
|
556
|
+
fetchInProgress = false;
|
|
557
|
+
btnFetch.classList.remove('btn-fetching');
|
|
558
|
+
btnFetch.title = 'Fetch from remotes';
|
|
559
|
+
if (!msg.result.ok) {
|
|
560
|
+
document.getElementById('status-text').textContent = 'Fetch failed: ' + (msg.result.error || 'Unknown error');
|
|
561
|
+
}
|
|
562
|
+
} else if (!msg.result.ok && msg.action === 'createBranch' && msg.result.error && msg.result.error.includes('already exists')) {
|
|
563
|
+
// Extract branch name from error: "fatal: a branch named 'X' already exists"
|
|
564
|
+
const branchMatch = msg.result.error.match(/branch named '([^']+)'/);
|
|
565
|
+
const branchName = branchMatch ? branchMatch[1] : 'this branch';
|
|
566
|
+
showDialog({
|
|
567
|
+
title: 'Branch Already Exists',
|
|
568
|
+
message: 'A branch named <b>' + escHtml(branchName) + '</b> already exists, do you want to replace it with this new branch?',
|
|
569
|
+
rawMessage: true,
|
|
570
|
+
confirmLabel: 'Yes, replace the existing branch',
|
|
571
|
+
cancelLabel: 'No, choose another branch name',
|
|
572
|
+
onConfirm: () => gitAction('createBranch', { ...msg.args, force: true }),
|
|
573
|
+
});
|
|
574
|
+
} else if (!msg.result.ok) {
|
|
575
|
+
showToast('Git action failed: ' + (msg.result.error || 'Unknown error'), 'error');
|
|
576
|
+
}
|
|
577
|
+
// Refresh worktree list after worktree mutations
|
|
578
|
+
if (msg.result.ok && (msg.action === 'addWorktree' || msg.action === 'removeWorktree' || msg.action === 'pruneWorktrees')) {
|
|
579
|
+
vscode.postMessage({ command: 'requestWorktrees' });
|
|
580
|
+
}
|
|
581
|
+
// Refresh stash list after stash mutations
|
|
582
|
+
if (msg.result.ok && ['stashSave','stashPop','stashDrop','stashApply'].includes(msg.action)) {
|
|
583
|
+
vscode.postMessage({ command: 'requestStashes' });
|
|
584
|
+
}
|
|
585
|
+
break;
|
|
586
|
+
case 'loadWorktrees':
|
|
587
|
+
state.worktrees = msg.data || [];
|
|
588
|
+
renderWorktreeList();
|
|
589
|
+
break;
|
|
590
|
+
case 'loadStashes':
|
|
591
|
+
state.stashes = msg.data || [];
|
|
592
|
+
renderStashList();
|
|
593
|
+
break;
|
|
594
|
+
case 'error':
|
|
595
|
+
document.getElementById('status-text').textContent = 'Error: ' + msg.message;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// --- File click delegation (opens diff tab) ---
|
|
601
|
+
document.getElementById('detail-panel').addEventListener('click', (e) => {
|
|
602
|
+
// File-level action buttons (stage/unstage/discard/open)
|
|
603
|
+
const actionBtn = e.target.closest('.file-action-btn');
|
|
604
|
+
if (actionBtn) {
|
|
605
|
+
e.stopPropagation();
|
|
606
|
+
const action = actionBtn.dataset.action;
|
|
607
|
+
const file = actionBtn.dataset.file;
|
|
608
|
+
if (action === 'open') {
|
|
609
|
+
vscode.postMessage({ command: 'openFile', filePath: file });
|
|
610
|
+
} else if (action === 'open-conflict') {
|
|
611
|
+
vscode.postMessage({ command: 'openConflictFile', filePath: file });
|
|
612
|
+
} else if (action === 'stage') {
|
|
613
|
+
vscode.postMessage({ command: 'gitAction', action: 'stage', args: { files: [file] } });
|
|
614
|
+
} else if (action === 'unstage') {
|
|
615
|
+
vscode.postMessage({ command: 'gitAction', action: 'unstage', args: { files: [file] } });
|
|
616
|
+
} else if (action === 'discard') {
|
|
617
|
+
showDialog({
|
|
618
|
+
title: 'Discard Changes',
|
|
619
|
+
message: 'Discard changes to "' + file + '"? This cannot be undone.',
|
|
620
|
+
destructive: true,
|
|
621
|
+
confirmLabel: 'Discard',
|
|
622
|
+
onConfirm: () => vscode.postMessage({ command: 'gitAction', action: 'discard', args: { files: [file] } }),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
// Section-level actions (Stage All / Unstage All)
|
|
628
|
+
const sectionBtn = e.target.closest('.section-action-btn');
|
|
629
|
+
if (sectionBtn) {
|
|
630
|
+
e.stopPropagation();
|
|
631
|
+
const action = sectionBtn.dataset.action;
|
|
632
|
+
if (action === 'stage-all') {
|
|
633
|
+
const files = state.uncommitted.unstaged.map(f => f.path);
|
|
634
|
+
vscode.postMessage({ command: 'gitAction', action: 'stage', args: { files } });
|
|
635
|
+
} else if (action === 'unstage-all') {
|
|
636
|
+
const files = state.uncommitted.staged.map(f => f.path);
|
|
637
|
+
vscode.postMessage({ command: 'gitAction', action: 'unstage', args: { files } });
|
|
638
|
+
}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
// Toggle buttons (tree/list view)
|
|
642
|
+
const toggleBtn = e.target.closest('.toggle-btn[data-view]');
|
|
643
|
+
if (toggleBtn) {
|
|
644
|
+
state.fileViewMode = toggleBtn.dataset.view;
|
|
645
|
+
if (state.selectedCommit === 'uncommitted') {
|
|
646
|
+
renderUncommittedDetail();
|
|
647
|
+
} else if (state._lastDetail) {
|
|
648
|
+
renderDetailPanel(state._lastDetail);
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
// File click (opens diff)
|
|
653
|
+
const item = e.target.closest('.file-clickable');
|
|
654
|
+
if (!item) return;
|
|
655
|
+
const filePath = item.dataset.path;
|
|
656
|
+
const hash = item.dataset.hash;
|
|
657
|
+
const parentHash = item.dataset.parent || null;
|
|
658
|
+
if (filePath && hash) {
|
|
659
|
+
vscode.postMessage({ command: 'openDiff', filePath, hash, parentHash });
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// --- Branch dropdown ---
|
|
664
|
+
let selectedBranch = 'all';
|
|
665
|
+
const branchTrigger = document.getElementById('branch-trigger');
|
|
666
|
+
const branchMenu = document.getElementById('branch-dropdown-menu');
|
|
667
|
+
const branchFilterInput = document.getElementById('branch-filter');
|
|
668
|
+
const branchListEl = document.getElementById('branch-list');
|
|
669
|
+
|
|
670
|
+
branchTrigger.addEventListener('click', (e) => {
|
|
671
|
+
e.stopPropagation();
|
|
672
|
+
const wasHidden = branchMenu.classList.contains('hidden');
|
|
673
|
+
branchMenu.classList.toggle('hidden');
|
|
674
|
+
if (wasHidden) {
|
|
675
|
+
branchFilterInput.value = '';
|
|
676
|
+
renderBranchOptions('');
|
|
677
|
+
branchFilterInput.focus();
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
document.addEventListener('click', (e) => {
|
|
682
|
+
if (!e.target.closest('#branch-selector')) branchMenu.classList.add('hidden');
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
branchFilterInput.addEventListener('input', () => {
|
|
686
|
+
renderBranchOptions(branchFilterInput.value.toLowerCase());
|
|
687
|
+
});
|
|
688
|
+
branchFilterInput.addEventListener('click', (e) => e.stopPropagation());
|
|
689
|
+
|
|
690
|
+
function renderBranchOptions(filter) {
|
|
691
|
+
const options = [{ name: 'all', label: 'All Branches', current: false }];
|
|
692
|
+
state.branches.forEach(b => {
|
|
693
|
+
if (b.remote && !state.settings.showRemoteBranches) return;
|
|
694
|
+
options.push({ name: b.name, label: (b.current ? '* ' : '') + b.name, current: b.current });
|
|
695
|
+
});
|
|
696
|
+
const filtered = filter ? options.filter(o => o.label.toLowerCase().includes(filter)) : options;
|
|
697
|
+
branchListEl.innerHTML = filtered.map(o =>
|
|
698
|
+
'<div class="branch-option' + (o.name === selectedBranch ? ' selected' : '') + '" data-branch="' + escHtml(o.name) + '">' + escHtml(o.label) + '</div>'
|
|
699
|
+
).join('');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
branchListEl.addEventListener('click', (e) => {
|
|
703
|
+
const opt = e.target.closest('.branch-option');
|
|
704
|
+
if (!opt) return;
|
|
705
|
+
const branch = opt.dataset.branch;
|
|
706
|
+
selectedBranch = branch;
|
|
707
|
+
branchTrigger.textContent = branch === 'all' ? 'All Branches' : branch;
|
|
708
|
+
branchMenu.classList.add('hidden');
|
|
709
|
+
state.commits = [];
|
|
710
|
+
document.getElementById('commit-list').innerHTML = '';
|
|
711
|
+
vscode.postMessage({ command: 'requestCommits', branch, maxCommits: state.maxCommits });
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
function renderBranchSelector() {
|
|
715
|
+
const branchNames = state.branches.map(b => b.name);
|
|
716
|
+
if (selectedBranch !== 'all' && !branchNames.includes(selectedBranch)) selectedBranch = 'all';
|
|
717
|
+
branchTrigger.textContent = selectedBranch === 'all' ? 'All Branches' : selectedBranch;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// --- Refresh ---
|
|
721
|
+
document.getElementById('btn-refresh').addEventListener('click', () => {
|
|
722
|
+
state.commits = [];
|
|
723
|
+
document.getElementById('commit-list').innerHTML = '';
|
|
724
|
+
vscode.postMessage({ command: 'requestRepoInfo' });
|
|
725
|
+
vscode.postMessage({ command: 'requestCommits', maxCommits: state.maxCommits });
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// --- Fetch ---
|
|
729
|
+
const btnFetch = document.getElementById('btn-fetch');
|
|
730
|
+
let fetchInProgress = false;
|
|
731
|
+
let autoFetchTimer = null;
|
|
732
|
+
|
|
733
|
+
function doFetch() {
|
|
734
|
+
fetchInProgress = true;
|
|
735
|
+
btnFetch.classList.add('btn-fetching');
|
|
736
|
+
btnFetch.title = 'Fetching...';
|
|
737
|
+
vscode.postMessage({ command: 'gitAction', action: 'fetch', args: { prune: true } });
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
btnFetch.addEventListener('click', () => { if (!fetchInProgress) doFetch(); });
|
|
741
|
+
|
|
742
|
+
function startAutoFetch(intervalSec) {
|
|
743
|
+
stopAutoFetch();
|
|
744
|
+
if (!intervalSec || intervalSec <= 0) return;
|
|
745
|
+
const ms = Math.max(intervalSec, 10) * 1000;
|
|
746
|
+
autoFetchTimer = setInterval(() => { if (!fetchInProgress) doFetch(); }, ms);
|
|
747
|
+
}
|
|
748
|
+
function stopAutoFetch() {
|
|
749
|
+
if (autoFetchTimer) { clearInterval(autoFetchTimer); autoFetchTimer = null; }
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// --- Worktree popover ---
|
|
753
|
+
const wtPopover = document.getElementById('worktree-popover');
|
|
754
|
+
const btnWorktree = document.getElementById('btn-worktree');
|
|
755
|
+
|
|
756
|
+
btnWorktree.addEventListener('click', (e) => {
|
|
757
|
+
e.stopPropagation();
|
|
758
|
+
const wasHidden = wtPopover.classList.contains('hidden');
|
|
759
|
+
wtPopover.classList.toggle('hidden');
|
|
760
|
+
if (wasHidden) vscode.postMessage({ command: 'requestWorktrees' });
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
document.addEventListener('click', (e) => {
|
|
764
|
+
if (!e.target.closest('.worktree-dropdown')) wtPopover.classList.add('hidden');
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
function renderWorktreeList() {
|
|
768
|
+
const listEl = document.getElementById('worktree-list');
|
|
769
|
+
const countEl = btnWorktree.querySelector('.wt-count');
|
|
770
|
+
const wts = state.worktrees;
|
|
771
|
+
if (countEl) {
|
|
772
|
+
countEl.textContent = wts.length;
|
|
773
|
+
countEl.style.display = wts.length > 1 ? '' : 'none';
|
|
774
|
+
}
|
|
775
|
+
if (!wts.length) {
|
|
776
|
+
listEl.innerHTML = '<div class="wt-empty">No worktrees found</div>';
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
listEl.innerHTML = wts.map((wt, i) => {
|
|
780
|
+
const branchName = wt.branch || (wt.isDetached ? 'detached HEAD' : '(bare)');
|
|
781
|
+
const shortHash = wt.head ? wt.head.substring(0, 7) : '';
|
|
782
|
+
const isCurrent = wt.path === state.repo;
|
|
783
|
+
let badges = '';
|
|
784
|
+
if (isCurrent) badges += ' <span class="wt-badge wt-badge-current">current</span>';
|
|
785
|
+
if (wt.isMain && !isCurrent) badges += ' <span class="wt-badge">main</span>';
|
|
786
|
+
if (wt.locked) badges += ' <span class="wt-badge wt-badge-locked">locked</span>';
|
|
787
|
+
if (wt.prunable) badges += ' <span class="wt-badge">prunable</span>';
|
|
788
|
+
if (wt.isDetached) badges += ' <span class="wt-badge">detached</span>';
|
|
789
|
+
const actions = isCurrent ? ''
|
|
790
|
+
: '<button class="wt-open" data-idx="' + i + '" title="Open in PPM">' + ICONS.fileOpen + '</button>'
|
|
791
|
+
+ (wt.isMain ? '' : '<button class="wt-remove" data-idx="' + i + '" title="Remove worktree">' + ICONS.trash + '</button>');
|
|
792
|
+
return '<div class="wt-item' + (isCurrent ? ' wt-item-active' : '') + '">'
|
|
793
|
+
+ '<div class="wt-item-info">'
|
|
794
|
+
+ '<div class="wt-item-branch">' + ICONS.gitBranch + ' ' + escHtml(branchName) + badges + '</div>'
|
|
795
|
+
+ '<div class="wt-item-path" title="' + escHtml(wt.path) + '">' + escHtml(wt.path) + ' <span style="color:var(--subtle)">' + shortHash + '</span></div>'
|
|
796
|
+
+ '</div>'
|
|
797
|
+
+ (actions ? '<div class="wt-item-actions">' + actions + '</div>' : '')
|
|
798
|
+
+ '</div>';
|
|
799
|
+
}).join('');
|
|
800
|
+
|
|
801
|
+
// Bind action buttons
|
|
802
|
+
listEl.querySelectorAll('.wt-open').forEach(btn => {
|
|
803
|
+
btn.addEventListener('click', (e) => {
|
|
804
|
+
e.stopPropagation();
|
|
805
|
+
const wt = state.worktrees[parseInt(btn.dataset.idx)];
|
|
806
|
+
if (wt) vscode.postMessage({ command: 'openWorktree', path: wt.path });
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
listEl.querySelectorAll('.wt-remove').forEach(btn => {
|
|
810
|
+
btn.addEventListener('click', (e) => {
|
|
811
|
+
e.stopPropagation();
|
|
812
|
+
const wt = state.worktrees[parseInt(btn.dataset.idx)];
|
|
813
|
+
if (!wt) return;
|
|
814
|
+
showDialog({
|
|
815
|
+
title: 'Remove Worktree',
|
|
816
|
+
message: 'Remove worktree at "' + wt.path + '"?',
|
|
817
|
+
destructive: true,
|
|
818
|
+
confirmLabel: 'Remove',
|
|
819
|
+
onConfirm: () => vscode.postMessage({ command: 'removeWorktree', path: wt.path }),
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
document.getElementById('wt-add').addEventListener('click', () => {
|
|
826
|
+
showCreateWorktreeDialog();
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
document.getElementById('wt-prune').addEventListener('click', () => {
|
|
830
|
+
showDialog({
|
|
831
|
+
title: 'Prune Worktrees',
|
|
832
|
+
message: 'Remove stale worktree entries (worktrees whose directories no longer exist)?',
|
|
833
|
+
confirmLabel: 'Prune',
|
|
834
|
+
onConfirm: () => vscode.postMessage({ command: 'pruneWorktrees' }),
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
function showCreateWorktreeDialog(startPoint) {
|
|
839
|
+
const overlay = document.createElement('div');
|
|
840
|
+
overlay.className = 'dialog-overlay';
|
|
841
|
+
const dialog = document.createElement('div');
|
|
842
|
+
dialog.className = 'dialog';
|
|
843
|
+
dialog.innerHTML = '<h3>Add Worktree</h3>'
|
|
844
|
+
+ '<p style="font-size:12px;margin-bottom:8px">Path for the new worktree directory:</p>'
|
|
845
|
+
+ '<input type="text" id="wt-dialog-path" placeholder="/path/to/worktree" style="width:100%;margin-bottom:8px" />'
|
|
846
|
+
+ '<p style="font-size:12px;margin-bottom:4px">Branch:</p>'
|
|
847
|
+
+ '<div style="display:flex;gap:8px;margin-bottom:6px">'
|
|
848
|
+
+ '<label style="font-size:11px;display:flex;align-items:center;gap:4px"><input type="radio" name="wt-branch-mode" value="existing" checked /> Existing branch</label>'
|
|
849
|
+
+ '<label style="font-size:11px;display:flex;align-items:center;gap:4px"><input type="radio" name="wt-branch-mode" value="new" /> New branch</label>'
|
|
850
|
+
+ '</div>'
|
|
851
|
+
+ '<input type="text" id="wt-dialog-branch" placeholder="Branch name" style="width:100%;margin-bottom:8px" />'
|
|
852
|
+
+ (startPoint ? '<input type="hidden" id="wt-dialog-start" value="' + escHtml(startPoint) + '" />' : '<input type="text" id="wt-dialog-start" placeholder="Start point (commit/branch, optional)" style="width:100%;margin-bottom:8px" />');
|
|
853
|
+
|
|
854
|
+
const actions = document.createElement('div');
|
|
855
|
+
actions.className = 'dialog-actions';
|
|
856
|
+
const cancelBtn = document.createElement('button');
|
|
857
|
+
cancelBtn.textContent = 'Cancel';
|
|
858
|
+
cancelBtn.className = 'secondary';
|
|
859
|
+
cancelBtn.addEventListener('click', () => overlay.remove());
|
|
860
|
+
const confirmBtn = document.createElement('button');
|
|
861
|
+
confirmBtn.textContent = 'Create';
|
|
862
|
+
confirmBtn.className = 'btn-primary';
|
|
863
|
+
confirmBtn.addEventListener('click', () => {
|
|
864
|
+
const path = dialog.querySelector('#wt-dialog-path').value.trim();
|
|
865
|
+
if (!path) { showToast('Path is required', 'error'); return; }
|
|
866
|
+
const branch = dialog.querySelector('#wt-dialog-branch').value.trim();
|
|
867
|
+
const mode = dialog.querySelector('input[name="wt-branch-mode"]:checked').value;
|
|
868
|
+
const sp = dialog.querySelector('#wt-dialog-start');
|
|
869
|
+
const startPt = sp ? sp.value.trim() : '';
|
|
870
|
+
const msg = { command: 'addWorktree', path };
|
|
871
|
+
if (mode === 'new' && branch) { msg.newBranch = branch; }
|
|
872
|
+
else if (branch) { msg.branch = branch; }
|
|
873
|
+
if (startPt) msg.startPoint = startPt;
|
|
874
|
+
vscode.postMessage(msg);
|
|
875
|
+
overlay.remove();
|
|
876
|
+
});
|
|
877
|
+
actions.appendChild(cancelBtn);
|
|
878
|
+
actions.appendChild(confirmBtn);
|
|
879
|
+
dialog.appendChild(actions);
|
|
880
|
+
overlay.appendChild(dialog);
|
|
881
|
+
document.body.appendChild(overlay);
|
|
882
|
+
setTimeout(() => dialog.querySelector('#wt-dialog-path').focus(), 50);
|
|
883
|
+
overlay.addEventListener('keydown', (e) => {
|
|
884
|
+
if (e.key === 'Escape') overlay.remove();
|
|
885
|
+
if (e.key === 'Enter') confirmBtn.click();
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// --- Merge/rebase banner ---
|
|
890
|
+
function renderMergeBanner() {
|
|
891
|
+
let banner = document.getElementById('merge-banner');
|
|
892
|
+
if (!state.mergeState) {
|
|
893
|
+
if (banner) banner.remove();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const ms = state.mergeState;
|
|
897
|
+
const typeLabel = ms.type === 'cherry-pick' ? 'Cherry-pick' : ms.type.charAt(0).toUpperCase() + ms.type.slice(1);
|
|
898
|
+
const progressText = ms.progress ? ' (' + ms.progress + ')' : '';
|
|
899
|
+
const msgText = ms.message ? ' — ' + escHtml(ms.message) : '';
|
|
900
|
+
|
|
901
|
+
let buttonsHtml = '';
|
|
902
|
+
if (ms.type === 'rebase') {
|
|
903
|
+
buttonsHtml = '<button class="btn-sm btn-continue" data-merge-action="rebaseContinue">Continue</button>'
|
|
904
|
+
+ '<button class="btn-sm" data-merge-action="rebaseSkip">Skip</button>'
|
|
905
|
+
+ '<button class="btn-sm btn-abort" data-merge-action="rebaseAbort">Abort</button>';
|
|
906
|
+
} else if (ms.type === 'merge') {
|
|
907
|
+
buttonsHtml = '<button class="btn-sm btn-abort" data-merge-action="mergeAbort">Abort</button>';
|
|
908
|
+
} else if (ms.type === 'cherry-pick') {
|
|
909
|
+
buttonsHtml = '<button class="btn-sm btn-continue" data-merge-action="cherryPickContinue">Continue</button>'
|
|
910
|
+
+ '<button class="btn-sm btn-abort" data-merge-action="cherryPickAbort">Abort</button>';
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (!banner) {
|
|
914
|
+
banner = document.createElement('div');
|
|
915
|
+
banner.id = 'merge-banner';
|
|
916
|
+
banner.className = 'merge-banner';
|
|
917
|
+
const toolbar = document.getElementById('toolbar');
|
|
918
|
+
toolbar.parentNode.insertBefore(banner, toolbar.nextSibling);
|
|
919
|
+
}
|
|
920
|
+
banner.innerHTML = '<span class="banner-icon">⚠</span>'
|
|
921
|
+
+ '<span class="banner-text"><strong>' + typeLabel + ' in progress' + progressText + '</strong>' + msgText + '</span>'
|
|
922
|
+
+ '<div class="banner-actions">' + buttonsHtml + '</div>';
|
|
923
|
+
|
|
924
|
+
banner.querySelectorAll('[data-merge-action]').forEach(btn => {
|
|
925
|
+
btn.addEventListener('click', () => {
|
|
926
|
+
const action = btn.dataset.mergeAction;
|
|
927
|
+
if (action.includes('Abort')) {
|
|
928
|
+
showDialog({
|
|
929
|
+
title: 'Abort ' + typeLabel,
|
|
930
|
+
message: 'Abort the current ' + ms.type + '? Any resolved conflicts will be lost.',
|
|
931
|
+
destructive: true,
|
|
932
|
+
confirmLabel: 'Abort',
|
|
933
|
+
onConfirm: () => gitAction(action, {}),
|
|
934
|
+
});
|
|
935
|
+
} else {
|
|
936
|
+
gitAction(action, {});
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// --- Stash popover ---
|
|
943
|
+
const btnStash = document.getElementById('btn-stash');
|
|
944
|
+
const stashPopover = document.getElementById('stash-popover');
|
|
945
|
+
|
|
946
|
+
btnStash.addEventListener('click', (e) => {
|
|
947
|
+
e.stopPropagation();
|
|
948
|
+
const wasHidden = stashPopover.classList.contains('hidden');
|
|
949
|
+
stashPopover.classList.toggle('hidden');
|
|
950
|
+
if (wasHidden) vscode.postMessage({ command: 'requestStashes' });
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
document.addEventListener('click', (e) => {
|
|
954
|
+
if (!e.target.closest('.stash-dropdown')) stashPopover.classList.add('hidden');
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
function renderStashList() {
|
|
958
|
+
const listEl = document.getElementById('stash-list');
|
|
959
|
+
const countEl = btnStash.querySelector('.stash-count');
|
|
960
|
+
const stashes = state.stashes;
|
|
961
|
+
if (countEl) {
|
|
962
|
+
countEl.textContent = stashes.length;
|
|
963
|
+
countEl.style.display = stashes.length > 0 ? '' : 'none';
|
|
964
|
+
}
|
|
965
|
+
if (!stashes.length) {
|
|
966
|
+
listEl.innerHTML = '<div class="stash-empty">No stashes</div>';
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
listEl.innerHTML = stashes.map((s, i) => {
|
|
970
|
+
const ref = 'stash@{' + s.index + '}';
|
|
971
|
+
return '<div class="stash-item">'
|
|
972
|
+
+ '<div class="stash-item-info">'
|
|
973
|
+
+ '<div class="stash-item-ref">' + escHtml(ref) + '</div>'
|
|
974
|
+
+ '<div class="stash-item-msg" title="' + escHtml(s.message) + '">' + escHtml(s.message) + '</div>'
|
|
975
|
+
+ '</div>'
|
|
976
|
+
+ '<div class="stash-item-actions">'
|
|
977
|
+
+ '<button class="stash-apply btn-sm" data-idx="' + i + '" title="Apply (keep stash)">Apply</button>'
|
|
978
|
+
+ '<button class="stash-pop btn-sm" data-idx="' + i + '" title="Pop (apply & remove)">Pop</button>'
|
|
979
|
+
+ '<button class="stash-drop btn-sm" data-idx="' + i + '" title="Drop (delete)">Drop</button>'
|
|
980
|
+
+ '</div>'
|
|
981
|
+
+ '</div>';
|
|
982
|
+
}).join('');
|
|
983
|
+
|
|
984
|
+
listEl.querySelectorAll('.stash-apply').forEach(btn => {
|
|
985
|
+
btn.addEventListener('click', (e) => {
|
|
986
|
+
e.stopPropagation();
|
|
987
|
+
const s = state.stashes[parseInt(btn.dataset.idx)];
|
|
988
|
+
if (s) gitAction('stashApply', { stashRef: 'stash@{' + s.index + '}' });
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
listEl.querySelectorAll('.stash-pop').forEach(btn => {
|
|
992
|
+
btn.addEventListener('click', (e) => {
|
|
993
|
+
e.stopPropagation();
|
|
994
|
+
const s = state.stashes[parseInt(btn.dataset.idx)];
|
|
995
|
+
if (s) gitAction('stashPop', { stashRef: 'stash@{' + s.index + '}' });
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
listEl.querySelectorAll('.stash-drop').forEach(btn => {
|
|
999
|
+
btn.addEventListener('click', (e) => {
|
|
1000
|
+
e.stopPropagation();
|
|
1001
|
+
const s = state.stashes[parseInt(btn.dataset.idx)];
|
|
1002
|
+
if (!s) return;
|
|
1003
|
+
showDialog({
|
|
1004
|
+
title: 'Drop Stash',
|
|
1005
|
+
message: 'Delete stash@{' + s.index + '}? This cannot be undone.',
|
|
1006
|
+
destructive: true,
|
|
1007
|
+
confirmLabel: 'Drop',
|
|
1008
|
+
onConfirm: () => gitAction('stashDrop', { stashRef: 'stash@{' + s.index + '}' }),
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
document.getElementById('stash-save').addEventListener('click', () => {
|
|
1015
|
+
showDialog({
|
|
1016
|
+
title: 'Stash Changes',
|
|
1017
|
+
input: { placeholder: 'Stash message (optional)' },
|
|
1018
|
+
confirmLabel: 'Stash',
|
|
1019
|
+
onConfirm: (msg) => gitAction('stashSave', msg ? { message: msg } : {}),
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
// --- Graph column resize ---
|
|
1024
|
+
{
|
|
1025
|
+
const resizeHandle = document.getElementById('graph-resize-handle');
|
|
1026
|
+
let resizing = false, startX = 0, startW = 0;
|
|
1027
|
+
resizeHandle.addEventListener('pointerdown', (e) => {
|
|
1028
|
+
e.preventDefault();
|
|
1029
|
+
resizing = true;
|
|
1030
|
+
startX = e.clientX;
|
|
1031
|
+
startW = document.querySelector('.col-graph').offsetWidth;
|
|
1032
|
+
resizeHandle.classList.add('dragging');
|
|
1033
|
+
resizeHandle.setPointerCapture(e.pointerId);
|
|
1034
|
+
});
|
|
1035
|
+
resizeHandle.addEventListener('pointermove', (e) => {
|
|
1036
|
+
if (!resizing) return;
|
|
1037
|
+
const newW = Math.max(40, Math.min(400, startW + e.clientX - startX));
|
|
1038
|
+
document.documentElement.style.setProperty('--graph-col-w', newW + 'px');
|
|
1039
|
+
});
|
|
1040
|
+
resizeHandle.addEventListener('pointerup', (e) => {
|
|
1041
|
+
if (!resizing) return;
|
|
1042
|
+
resizing = false;
|
|
1043
|
+
resizeHandle.classList.remove('dragging');
|
|
1044
|
+
const newW = Math.max(40, Math.min(400, startW + e.clientX - startX));
|
|
1045
|
+
state.graphColWidth = newW;
|
|
1046
|
+
document.documentElement.style.setProperty('--graph-col-w', newW + 'px');
|
|
1047
|
+
});
|
|
1048
|
+
resizeHandle.addEventListener('dblclick', () => {
|
|
1049
|
+
state.graphColWidth = null;
|
|
1050
|
+
graphRender(-1);
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// --- Graph rendering (faithful port of vscode-git-graph graph.ts) ---
|
|
1055
|
+
|
|
1056
|
+
class GBranch {
|
|
1057
|
+
constructor(colour) {
|
|
1058
|
+
this._colour = colour;
|
|
1059
|
+
this._end = 0;
|
|
1060
|
+
this._lines = [];
|
|
1061
|
+
this._numUncommitted = 0;
|
|
1062
|
+
}
|
|
1063
|
+
addLine(p1, p2, isCommitted, lockedFirst) {
|
|
1064
|
+
this._lines.push({ p1, p2, lockedFirst });
|
|
1065
|
+
if (isCommitted) {
|
|
1066
|
+
if (p2.x === 0 && p2.y < this._numUncommitted) this._numUncommitted = p2.y;
|
|
1067
|
+
} else {
|
|
1068
|
+
this._numUncommitted++;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
getColour() { return this._colour; }
|
|
1072
|
+
getEnd() { return this._end; }
|
|
1073
|
+
setEnd(end) { this._end = end; }
|
|
1074
|
+
|
|
1075
|
+
draw(svg, config, expandAt) {
|
|
1076
|
+
const colour = config.colours[this._colour % config.colours.length];
|
|
1077
|
+
const d = config.grid.y * (config.style === 'angular' ? 0.38 : 0.8);
|
|
1078
|
+
const pxLines = [];
|
|
1079
|
+
let curPath = '';
|
|
1080
|
+
|
|
1081
|
+
for (let i = 0; i < this._lines.length; i++) {
|
|
1082
|
+
const ln = this._lines[i];
|
|
1083
|
+
let x1 = ln.p1.x * config.grid.x + config.grid.offsetX;
|
|
1084
|
+
let y1 = ln.p1.y * config.grid.y + config.grid.offsetY;
|
|
1085
|
+
let x2 = ln.p2.x * config.grid.x + config.grid.offsetX;
|
|
1086
|
+
let y2 = ln.p2.y * config.grid.y + config.grid.offsetY;
|
|
1087
|
+
|
|
1088
|
+
if (expandAt > -1) {
|
|
1089
|
+
if (ln.p1.y > expandAt) {
|
|
1090
|
+
y1 += config.grid.expandY; y2 += config.grid.expandY;
|
|
1091
|
+
} else if (ln.p2.y > expandAt) {
|
|
1092
|
+
if (x1 === x2) {
|
|
1093
|
+
y2 += config.grid.expandY;
|
|
1094
|
+
} else if (ln.lockedFirst) {
|
|
1095
|
+
pxLines.push({ p1: { x: x1, y: y1 }, p2: { x: x2, y: y2 }, isC: i >= this._numUncommitted, lf: ln.lockedFirst });
|
|
1096
|
+
pxLines.push({ p1: { x: x2, y: y1 + config.grid.y }, p2: { x: x2, y: y2 + config.grid.expandY }, isC: i >= this._numUncommitted, lf: ln.lockedFirst });
|
|
1097
|
+
continue;
|
|
1098
|
+
} else {
|
|
1099
|
+
pxLines.push({ p1: { x: x1, y: y1 }, p2: { x: x1, y: y2 - config.grid.y + config.grid.expandY }, isC: i >= this._numUncommitted, lf: ln.lockedFirst });
|
|
1100
|
+
y1 += config.grid.expandY; y2 += config.grid.expandY;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
pxLines.push({ p1: { x: x1, y: y1 }, p2: { x: x2, y: y2 }, isC: i >= this._numUncommitted, lf: ln.lockedFirst });
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Simplify consecutive vertical segments
|
|
1108
|
+
let si = 0;
|
|
1109
|
+
while (si < pxLines.length - 1) {
|
|
1110
|
+
const a = pxLines[si], b = pxLines[si + 1];
|
|
1111
|
+
if (a.p1.x === a.p2.x && a.p2.x === b.p1.x && b.p1.x === b.p2.x && a.p2.y === b.p1.y && a.isC === b.isC) {
|
|
1112
|
+
a.p2.y = b.p2.y;
|
|
1113
|
+
pxLines.splice(si + 1, 1);
|
|
1114
|
+
} else { si++; }
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Build SVG paths
|
|
1118
|
+
for (let i = 0; i < pxLines.length; i++) {
|
|
1119
|
+
const pl = pxLines[i];
|
|
1120
|
+
const x1 = pl.p1.x, y1 = pl.p1.y, x2 = pl.p2.x, y2 = pl.p2.y;
|
|
1121
|
+
|
|
1122
|
+
if (curPath !== '' && i > 0 && pl.isC !== pxLines[i - 1].isC) {
|
|
1123
|
+
GBranch._drawPath(svg, curPath, pxLines[i - 1].isC, colour);
|
|
1124
|
+
curPath = '';
|
|
1125
|
+
}
|
|
1126
|
+
if (curPath === '' || (i > 0 && (x1 !== pxLines[i - 1].p2.x || y1 !== pxLines[i - 1].p2.y))) {
|
|
1127
|
+
curPath += 'M' + x1.toFixed(0) + ',' + y1.toFixed(1);
|
|
1128
|
+
}
|
|
1129
|
+
if (x1 === x2) {
|
|
1130
|
+
curPath += 'L' + x2.toFixed(0) + ',' + y2.toFixed(1);
|
|
1131
|
+
} else if (config.style === 'angular') {
|
|
1132
|
+
curPath += 'L' + (pl.lf ? (x2.toFixed(0) + ',' + (y2 - d).toFixed(1)) : (x1.toFixed(0) + ',' + (y1 + d).toFixed(1))) + 'L' + x2.toFixed(0) + ',' + y2.toFixed(1);
|
|
1133
|
+
} else {
|
|
1134
|
+
curPath += 'C' + x1.toFixed(0) + ',' + (y1 + d).toFixed(1) + ' ' + x2.toFixed(0) + ',' + (y2 - d).toFixed(1) + ' ' + x2.toFixed(0) + ',' + y2.toFixed(1);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (curPath !== '') GBranch._drawPath(svg, curPath, pxLines[pxLines.length - 1].isC, colour);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
static _drawPath(svg, path, isCommitted, colour) {
|
|
1141
|
+
const line = document.createElementNS(SVG_NS, 'path');
|
|
1142
|
+
line.setAttribute('class', 'line');
|
|
1143
|
+
line.setAttribute('d', path);
|
|
1144
|
+
line.setAttribute('stroke', isCommitted ? colour : '#808080');
|
|
1145
|
+
if (!isCommitted) line.setAttribute('stroke-dasharray', '2');
|
|
1146
|
+
svg.appendChild(line);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
class GVertex {
|
|
1151
|
+
constructor(id, isStash) {
|
|
1152
|
+
this.id = id;
|
|
1153
|
+
this.isStash = isStash;
|
|
1154
|
+
this._x = 0;
|
|
1155
|
+
this._children = [];
|
|
1156
|
+
this._parents = [];
|
|
1157
|
+
this._nextParent = 0;
|
|
1158
|
+
this._onBranch = null;
|
|
1159
|
+
this._isCommitted = true;
|
|
1160
|
+
this._isCurrent = false;
|
|
1161
|
+
this._nextX = 0;
|
|
1162
|
+
this._connections = [];
|
|
1163
|
+
}
|
|
1164
|
+
addChild(v) { this._children.push(v); }
|
|
1165
|
+
getChildren() { return this._children; }
|
|
1166
|
+
addParent(v) { this._parents.push(v); }
|
|
1167
|
+
getParents() { return this._parents; }
|
|
1168
|
+
hasParents() { return this._parents.length > 0; }
|
|
1169
|
+
getNextParent() { return this._nextParent < this._parents.length ? this._parents[this._nextParent] : null; }
|
|
1170
|
+
registerParentProcessed() { this._nextParent++; }
|
|
1171
|
+
isMerge() { return this._parents.length > 1; }
|
|
1172
|
+
|
|
1173
|
+
addToBranch(branch, x) { if (this._onBranch === null) { this._onBranch = branch; this._x = x; } }
|
|
1174
|
+
isNotOnBranch() { return this._onBranch === null; }
|
|
1175
|
+
isOnThisBranch(branch) { return this._onBranch === branch; }
|
|
1176
|
+
getBranch() { return this._onBranch; }
|
|
1177
|
+
|
|
1178
|
+
getPoint() { return { x: this._x, y: this.id }; }
|
|
1179
|
+
getNextPoint() { return { x: this._nextX, y: this.id }; }
|
|
1180
|
+
|
|
1181
|
+
getPointConnectingTo(vertex, onBranch) {
|
|
1182
|
+
for (let i = 0; i < this._connections.length; i++) {
|
|
1183
|
+
if (this._connections[i].connectsTo === vertex && this._connections[i].onBranch === onBranch) return { x: i, y: this.id };
|
|
1184
|
+
}
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
registerUnavailablePoint(x, connectsTo, onBranch) {
|
|
1188
|
+
if (x === this._nextX) { this._nextX = x + 1; this._connections[x] = { connectsTo, onBranch }; }
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
getColour() { return this._onBranch !== null ? this._onBranch.getColour() : 0; }
|
|
1192
|
+
getIsCommitted() { return this._isCommitted; }
|
|
1193
|
+
setNotCommitted() { this._isCommitted = false; }
|
|
1194
|
+
setCurrent() { this._isCurrent = true; }
|
|
1195
|
+
|
|
1196
|
+
draw(svg, config, expandOffset, overListener, outListener) {
|
|
1197
|
+
if (this._onBranch === null) return;
|
|
1198
|
+
const colour = this._isCommitted ? config.colours[this._onBranch.getColour() % config.colours.length] : '#808080';
|
|
1199
|
+
const cx = (this._x * config.grid.x + config.grid.offsetX).toString();
|
|
1200
|
+
const cy = (this.id * config.grid.y + config.grid.offsetY + (expandOffset ? config.grid.expandY : 0)).toString();
|
|
1201
|
+
|
|
1202
|
+
const circle = document.createElementNS(SVG_NS, 'circle');
|
|
1203
|
+
circle.dataset.id = this.id.toString();
|
|
1204
|
+
circle.setAttribute('cx', cx);
|
|
1205
|
+
circle.setAttribute('cy', cy);
|
|
1206
|
+
circle.setAttribute('r', '4');
|
|
1207
|
+
if (this._isCurrent) {
|
|
1208
|
+
circle.setAttribute('class', 'graphCurrent');
|
|
1209
|
+
circle.setAttribute('stroke', colour);
|
|
1210
|
+
} else {
|
|
1211
|
+
circle.setAttribute('fill', colour);
|
|
1212
|
+
}
|
|
1213
|
+
svg.appendChild(circle);
|
|
1214
|
+
|
|
1215
|
+
if (this.isStash && !this._isCurrent) {
|
|
1216
|
+
circle.setAttribute('r', '4.5');
|
|
1217
|
+
circle.setAttribute('class', 'graphStashOuter');
|
|
1218
|
+
const inner = document.createElementNS(SVG_NS, 'circle');
|
|
1219
|
+
inner.setAttribute('cx', cx);
|
|
1220
|
+
inner.setAttribute('cy', cy);
|
|
1221
|
+
inner.setAttribute('r', '2');
|
|
1222
|
+
inner.setAttribute('class', 'graphStashInner');
|
|
1223
|
+
svg.appendChild(inner);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
circle.addEventListener('mouseover', overListener);
|
|
1227
|
+
circle.addEventListener('mouseout', outListener);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// --- Graph layout state ---
|
|
1232
|
+
let gVertices = [], gBranches = [], gAvailColours = [], gCommitLookup = {};
|
|
1233
|
+
|
|
1234
|
+
function graphLoadCommits(commits) {
|
|
1235
|
+
gVertices = []; gBranches = []; gAvailColours = [];
|
|
1236
|
+
if (commits.length === 0) return;
|
|
1237
|
+
|
|
1238
|
+
const stashHashes = new Set(state.stashes.map(s => s.hash));
|
|
1239
|
+
const nullVertex = new GVertex(NULL_VERTEX_ID, false);
|
|
1240
|
+
const lookup = {};
|
|
1241
|
+
for (let i = 0; i < commits.length; i++) {
|
|
1242
|
+
lookup[commits[i].hash] = i;
|
|
1243
|
+
gVertices.push(new GVertex(i, stashHashes.has(commits[i].hash)));
|
|
1244
|
+
}
|
|
1245
|
+
gCommitLookup = lookup;
|
|
1246
|
+
|
|
1247
|
+
for (let i = 0; i < commits.length; i++) {
|
|
1248
|
+
for (let j = 0; j < commits[i].parents.length; j++) {
|
|
1249
|
+
const ph = commits[i].parents[j];
|
|
1250
|
+
if (typeof lookup[ph] === 'number') {
|
|
1251
|
+
gVertices[i].addParent(gVertices[lookup[ph]]);
|
|
1252
|
+
gVertices[lookup[ph]].addChild(gVertices[i]);
|
|
1253
|
+
} else {
|
|
1254
|
+
gVertices[i].addParent(nullVertex);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (state.head && typeof lookup[state.head] === 'number') {
|
|
1260
|
+
gVertices[lookup[state.head]].setCurrent();
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
let i = 0;
|
|
1264
|
+
while (i < gVertices.length) {
|
|
1265
|
+
if (gVertices[i].getNextParent() !== null || gVertices[i].isNotOnBranch()) {
|
|
1266
|
+
graphDeterminePath(i);
|
|
1267
|
+
} else { i++; }
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function graphDeterminePath(startAt) {
|
|
1272
|
+
let i = startAt;
|
|
1273
|
+
let vertex = gVertices[i], parentVertex = gVertices[i].getNextParent(), curVertex;
|
|
1274
|
+
let lastPoint = vertex.isNotOnBranch() ? vertex.getNextPoint() : vertex.getPoint(), curPoint;
|
|
1275
|
+
|
|
1276
|
+
if (parentVertex !== null && parentVertex.id !== NULL_VERTEX_ID && vertex.isMerge() && !vertex.isNotOnBranch() && !parentVertex.isNotOnBranch()) {
|
|
1277
|
+
let foundPtp = false, pBranch = parentVertex.getBranch();
|
|
1278
|
+
for (i = startAt + 1; i < gVertices.length; i++) {
|
|
1279
|
+
curVertex = gVertices[i];
|
|
1280
|
+
curPoint = curVertex.getPointConnectingTo(parentVertex, pBranch);
|
|
1281
|
+
if (curPoint !== null) { foundPtp = true; } else { curPoint = curVertex.getNextPoint(); }
|
|
1282
|
+
pBranch.addLine(lastPoint, curPoint, vertex.getIsCommitted(), !foundPtp && curVertex !== parentVertex ? lastPoint.x < curPoint.x : true);
|
|
1283
|
+
curVertex.registerUnavailablePoint(curPoint.x, parentVertex, pBranch);
|
|
1284
|
+
lastPoint = curPoint;
|
|
1285
|
+
if (foundPtp) { vertex.registerParentProcessed(); break; }
|
|
1286
|
+
}
|
|
1287
|
+
} else {
|
|
1288
|
+
const branch = new GBranch(graphGetAvailableColour(startAt));
|
|
1289
|
+
vertex.addToBranch(branch, lastPoint.x);
|
|
1290
|
+
vertex.registerUnavailablePoint(lastPoint.x, vertex, branch);
|
|
1291
|
+
for (i = startAt + 1; i < gVertices.length; i++) {
|
|
1292
|
+
curVertex = gVertices[i];
|
|
1293
|
+
curPoint = parentVertex === curVertex && !parentVertex.isNotOnBranch() ? curVertex.getPoint() : curVertex.getNextPoint();
|
|
1294
|
+
branch.addLine(lastPoint, curPoint, vertex.getIsCommitted(), lastPoint.x < curPoint.x);
|
|
1295
|
+
curVertex.registerUnavailablePoint(curPoint.x, parentVertex, branch);
|
|
1296
|
+
lastPoint = curPoint;
|
|
1297
|
+
if (parentVertex === curVertex) {
|
|
1298
|
+
vertex.registerParentProcessed();
|
|
1299
|
+
const onBranch = !parentVertex.isNotOnBranch();
|
|
1300
|
+
parentVertex.addToBranch(branch, curPoint.x);
|
|
1301
|
+
vertex = parentVertex;
|
|
1302
|
+
parentVertex = vertex.getNextParent();
|
|
1303
|
+
if (parentVertex === null || onBranch) break;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (i === gVertices.length && parentVertex !== null && parentVertex.id === NULL_VERTEX_ID) {
|
|
1307
|
+
vertex.registerParentProcessed();
|
|
1308
|
+
}
|
|
1309
|
+
branch.setEnd(i);
|
|
1310
|
+
gBranches.push(branch);
|
|
1311
|
+
gAvailColours[branch.getColour()] = i;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function graphGetAvailableColour(startAt) {
|
|
1316
|
+
for (let i = 0; i < gAvailColours.length; i++) {
|
|
1317
|
+
if (startAt > gAvailColours[i]) return i;
|
|
1318
|
+
}
|
|
1319
|
+
gAvailColours.push(0);
|
|
1320
|
+
return gAvailColours.length - 1;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function graphRender(expandIdx) {
|
|
1324
|
+
const container = document.getElementById('graph-svg-container');
|
|
1325
|
+
container.innerHTML = '';
|
|
1326
|
+
if (gVertices.length === 0) { if (state.graphColWidth === null) document.documentElement.style.setProperty('--graph-col-w', '40px'); return; }
|
|
1327
|
+
|
|
1328
|
+
// Detect mobile: match CSS breakpoint where row height changes to 44px
|
|
1329
|
+
const isMobile = window.matchMedia('(max-width: 768px)').matches;
|
|
1330
|
+
const cfg = isMobile
|
|
1331
|
+
? { ...graphConfig, grid: { ...graphConfig.grid, y: 44, offsetY: 22 } }
|
|
1332
|
+
: graphConfig;
|
|
1333
|
+
|
|
1334
|
+
const svg = document.createElementNS(SVG_NS, 'svg');
|
|
1335
|
+
const group = document.createElementNS(SVG_NS, 'g');
|
|
1336
|
+
|
|
1337
|
+
for (let i = 0; i < gBranches.length; i++) gBranches[i].draw(group, cfg, expandIdx);
|
|
1338
|
+
|
|
1339
|
+
const overL = (e) => graphVertexOver(e), outL = (e) => graphVertexOut(e);
|
|
1340
|
+
for (let i = 0; i < gVertices.length; i++) {
|
|
1341
|
+
gVertices[i].draw(group, cfg, expandIdx > -1 && i > expandIdx, overL, outL);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
svg.appendChild(group);
|
|
1345
|
+
|
|
1346
|
+
let maxX = 0;
|
|
1347
|
+
for (let i = 0; i < gVertices.length; i++) {
|
|
1348
|
+
const p = gVertices[i].getNextPoint();
|
|
1349
|
+
if (p.x > maxX) maxX = p.x;
|
|
1350
|
+
}
|
|
1351
|
+
const w = 2 * cfg.grid.offsetX + Math.max(maxX - 1, 0) * cfg.grid.x;
|
|
1352
|
+
const h = gVertices.length * cfg.grid.y + cfg.grid.offsetY - cfg.grid.y / 2 + (expandIdx > -1 ? cfg.grid.expandY : 0);
|
|
1353
|
+
|
|
1354
|
+
const gw = Math.max(w, 40);
|
|
1355
|
+
svg.setAttribute('width', gw.toString());
|
|
1356
|
+
svg.setAttribute('height', h.toString());
|
|
1357
|
+
container.appendChild(svg);
|
|
1358
|
+
if (state.graphColWidth === null) document.documentElement.style.setProperty('--graph-col-w', gw + 'px');
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function graphVertexOver(e) {
|
|
1362
|
+
if (!e.target || !e.target.dataset || !e.target.dataset.id) return;
|
|
1363
|
+
const id = parseInt(e.target.dataset.id);
|
|
1364
|
+
if (id >= 0 && id < state.commits.length) {
|
|
1365
|
+
const rows = document.querySelectorAll('.commit-row:not(.header-row)');
|
|
1366
|
+
if (rows[id]) rows[id].classList.add('graph-hover');
|
|
1367
|
+
e.target.setAttribute('r', e.target.classList.contains('graphStashOuter') ? '5.5' : '5');
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
function graphVertexOut(e) {
|
|
1371
|
+
if (!e.target || !e.target.dataset || !e.target.dataset.id) return;
|
|
1372
|
+
const id = parseInt(e.target.dataset.id);
|
|
1373
|
+
if (id >= 0) {
|
|
1374
|
+
const rows = document.querySelectorAll('.commit-row:not(.header-row)');
|
|
1375
|
+
if (rows[id]) rows[id].classList.remove('graph-hover');
|
|
1376
|
+
e.target.setAttribute('r', e.target.classList.contains('graphStashOuter') ? '4.5' : '4');
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// --- Commit list ---
|
|
1381
|
+
function getDisplayCommits() {
|
|
1382
|
+
const u = state.uncommitted;
|
|
1383
|
+
const totalFiles = u ? (u.staged.length + u.unstaged.length + (u.conflicted ? u.conflicted.length : 0)) : 0;
|
|
1384
|
+
if (!u || totalFiles === 0) return state.commits;
|
|
1385
|
+
const virtualCommit = {
|
|
1386
|
+
hash: 'uncommitted',
|
|
1387
|
+
parents: state.head ? [state.head] : [],
|
|
1388
|
+
author: '',
|
|
1389
|
+
authorEmail: '',
|
|
1390
|
+
authorDate: Math.floor(Date.now() / 1000),
|
|
1391
|
+
committer: '',
|
|
1392
|
+
committerEmail: '',
|
|
1393
|
+
commitDate: Math.floor(Date.now() / 1000),
|
|
1394
|
+
refs: [],
|
|
1395
|
+
message: 'Uncommitted Changes (' + totalFiles + ' files)',
|
|
1396
|
+
};
|
|
1397
|
+
return [virtualCommit, ...state.commits];
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function renderCommitList() {
|
|
1401
|
+
const container = document.getElementById('commit-list');
|
|
1402
|
+
container.innerHTML = '';
|
|
1403
|
+
|
|
1404
|
+
const displayCommits = getDisplayCommits();
|
|
1405
|
+
graphLoadCommits(displayCommits);
|
|
1406
|
+
|
|
1407
|
+
// Mark uncommitted vertex for dashed lines
|
|
1408
|
+
if (displayCommits.length > 0 && displayCommits[0].hash === 'uncommitted') {
|
|
1409
|
+
if (gVertices.length > 0) gVertices[0].setNotCommitted();
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
displayCommits.forEach((commit, idx) => {
|
|
1413
|
+
const isVirtual = commit.hash === 'uncommitted';
|
|
1414
|
+
const row = document.createElement('div');
|
|
1415
|
+
row.className = 'commit-row' + (isVirtual ? ' virtual' : '');
|
|
1416
|
+
row.dataset.hash = commit.hash;
|
|
1417
|
+
|
|
1418
|
+
// Graph spacer column (SVG overlays this area)
|
|
1419
|
+
const graphCol = document.createElement('div');
|
|
1420
|
+
graphCol.className = 'col-graph';
|
|
1421
|
+
|
|
1422
|
+
// Message column with ref badges
|
|
1423
|
+
const msgCol = document.createElement('div');
|
|
1424
|
+
msgCol.className = 'col-message';
|
|
1425
|
+
let badges = '';
|
|
1426
|
+
if (commit.refs) {
|
|
1427
|
+
commit.refs.forEach(ref => {
|
|
1428
|
+
if (ref.type === 'tag' && !state.settings.showTags) return;
|
|
1429
|
+
if (ref.type === 'remote' && !state.settings.showRemoteBranches) return;
|
|
1430
|
+
badges += '<span class="ref-badge ref-' + ref.type + '">' + escHtml(ref.name) + '</span>';
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
msgCol.innerHTML = badges + formatCommitMessage(commit.message);
|
|
1434
|
+
|
|
1435
|
+
// Attach context menu and double-click to ref badges
|
|
1436
|
+
msgCol.querySelectorAll('.ref-badge').forEach(badge => {
|
|
1437
|
+
const refName = badge.textContent;
|
|
1438
|
+
const refType = badge.className.includes('ref-head') ? 'head'
|
|
1439
|
+
: badge.className.includes('ref-remote') ? 'remote'
|
|
1440
|
+
: badge.className.includes('ref-tag') ? 'tag' : 'local';
|
|
1441
|
+
badge.style.cursor = 'pointer';
|
|
1442
|
+
badge.addEventListener('dblclick', (e) => {
|
|
1443
|
+
e.stopPropagation();
|
|
1444
|
+
gitAction('checkout', { target: refName });
|
|
1445
|
+
});
|
|
1446
|
+
badge.addEventListener('contextmenu', (e) => {
|
|
1447
|
+
e.preventDefault();
|
|
1448
|
+
e.stopPropagation();
|
|
1449
|
+
showBranchContextMenu(e.clientX, e.clientY, refName, refType, commit);
|
|
1450
|
+
});
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
const authorCol = document.createElement('div');
|
|
1454
|
+
authorCol.className = 'col-author';
|
|
1455
|
+
authorCol.textContent = isVirtual ? '' : commit.author;
|
|
1456
|
+
|
|
1457
|
+
const dateCol = document.createElement('div');
|
|
1458
|
+
dateCol.className = 'col-date';
|
|
1459
|
+
dateCol.textContent = isVirtual ? 'now' : formatDate(commit.commitDate);
|
|
1460
|
+
|
|
1461
|
+
const hashCol = document.createElement('div');
|
|
1462
|
+
hashCol.className = 'col-hash';
|
|
1463
|
+
hashCol.textContent = isVirtual ? '...' : commit.hash.substring(0, 7);
|
|
1464
|
+
|
|
1465
|
+
row.appendChild(graphCol);
|
|
1466
|
+
row.appendChild(msgCol);
|
|
1467
|
+
row.appendChild(authorCol);
|
|
1468
|
+
row.appendChild(dateCol);
|
|
1469
|
+
row.appendChild(hashCol);
|
|
1470
|
+
|
|
1471
|
+
row.addEventListener('click', () => selectCommit(commit.hash));
|
|
1472
|
+
if (isVirtual) {
|
|
1473
|
+
row.addEventListener('contextmenu', (e) => {
|
|
1474
|
+
e.preventDefault();
|
|
1475
|
+
showUncommittedContextMenu(e.clientX, e.clientY);
|
|
1476
|
+
});
|
|
1477
|
+
setupLongPress(row, (x, y) => showUncommittedContextMenu(x, y));
|
|
1478
|
+
} else {
|
|
1479
|
+
row.addEventListener('contextmenu', (e) => {
|
|
1480
|
+
e.preventDefault();
|
|
1481
|
+
showCommitContextMenu(e.clientX, e.clientY, commit);
|
|
1482
|
+
});
|
|
1483
|
+
setupLongPress(row, (x, y) => showCommitContextMenu(x, y, commit));
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
container.appendChild(row);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
graphRender(-1);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function selectCommit(hash) {
|
|
1493
|
+
// Deselect previous
|
|
1494
|
+
document.querySelectorAll('.commit-row.selected').forEach(el => el.classList.remove('selected'));
|
|
1495
|
+
|
|
1496
|
+
if (state.selectedCommit === hash) {
|
|
1497
|
+
state.selectedCommit = null;
|
|
1498
|
+
state.expandedCommit = null;
|
|
1499
|
+
document.getElementById('detail-panel').classList.add('hidden');
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
state.selectedCommit = hash;
|
|
1504
|
+
state.expandedCommit = hash;
|
|
1505
|
+
const row = document.querySelector('[data-hash="' + CSS.escape(hash) + '"]');
|
|
1506
|
+
if (row) row.classList.add('selected');
|
|
1507
|
+
|
|
1508
|
+
if (hash === 'uncommitted') {
|
|
1509
|
+
renderUncommittedDetail();
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
vscode.postMessage({ command: 'requestCommitDetails', hash });
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// --- File tree helpers ---
|
|
1516
|
+
function buildFileTree(files) {
|
|
1517
|
+
const root = { name: '', children: {}, files: [] };
|
|
1518
|
+
for (const f of files) {
|
|
1519
|
+
const parts = f.path.split('/');
|
|
1520
|
+
let node = root;
|
|
1521
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1522
|
+
if (!node.children[parts[i]]) node.children[parts[i]] = { name: parts[i], children: {}, files: [] };
|
|
1523
|
+
node = node.children[parts[i]];
|
|
1524
|
+
}
|
|
1525
|
+
node.files.push({ ...f, fileName: parts[parts.length - 1] });
|
|
1526
|
+
}
|
|
1527
|
+
return root;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function countFiles(node) {
|
|
1531
|
+
let count = node.files.length;
|
|
1532
|
+
for (const child of Object.values(node.children)) count += countFiles(child);
|
|
1533
|
+
return count;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function renderFileTree(node, depth, hash, parentHash, section) {
|
|
1537
|
+
let html = '';
|
|
1538
|
+
const dirs = Object.keys(node.children).sort();
|
|
1539
|
+
for (const dir of dirs) {
|
|
1540
|
+
const child = node.children[dir];
|
|
1541
|
+
html += '<div class="tree-dir" style="padding-left:' + (depth * 16) + 'px">';
|
|
1542
|
+
html += ICONS.folderOpen + ' <span class="tree-dir-name">' + escHtml(dir) + '/</span>';
|
|
1543
|
+
html += '<span class="tree-dir-count">(' + countFiles(child) + ')</span></div>';
|
|
1544
|
+
html += renderFileTree(child, depth + 1, hash, parentHash, section);
|
|
1545
|
+
}
|
|
1546
|
+
const sortedFiles = [...node.files].sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
1547
|
+
for (const f of sortedFiles) {
|
|
1548
|
+
html += '<div class="file-item file-clickable" style="padding-left:' + (depth * 16) + 'px" ';
|
|
1549
|
+
html += 'data-path="' + escHtml(f.path) + '" data-hash="' + escHtml(hash) + '" data-parent="' + escHtml(parentHash) + '">';
|
|
1550
|
+
html += '<span class="file-status file-status-' + escHtml(f.status) + '">' + escHtml(f.status) + '</span>';
|
|
1551
|
+
html += '<span class="file-name">' + escHtml(f.fileName) + '</span>';
|
|
1552
|
+
if (f.additions > 0 || f.deletions > 0) {
|
|
1553
|
+
html += '<span class="file-stat">';
|
|
1554
|
+
if (f.additions > 0) html += '<span class="add">+' + f.additions + '</span> ';
|
|
1555
|
+
if (f.deletions > 0) html += '<span class="del">-' + f.deletions + '</span>';
|
|
1556
|
+
html += '</span>';
|
|
1557
|
+
}
|
|
1558
|
+
if (section) html += renderFileActions(f, section);
|
|
1559
|
+
html += '</div>';
|
|
1560
|
+
}
|
|
1561
|
+
return html;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function renderFileListHtml(files, hash, parentHash, section) {
|
|
1565
|
+
if (state.fileViewMode === 'tree') {
|
|
1566
|
+
return renderFileTree(buildFileTree(files), 0, hash, parentHash, section);
|
|
1567
|
+
}
|
|
1568
|
+
return files.map(f =>
|
|
1569
|
+
'<div class="file-item file-clickable" data-path="' + escHtml(f.path) + '" data-hash="' + escHtml(hash) + '" data-parent="' + escHtml(parentHash || '') + '">' +
|
|
1570
|
+
'<span class="file-status file-status-' + escHtml(f.status) + '">' + escHtml(f.status) + '</span>' +
|
|
1571
|
+
'<span class="file-name">' + escHtml(f.path) + '</span>' +
|
|
1572
|
+
'<span class="file-stat">' +
|
|
1573
|
+
(f.additions > 0 ? '<span class="add">+' + f.additions + '</span> ' : '') +
|
|
1574
|
+
(f.deletions > 0 ? '<span class="del">-' + f.deletions + '</span>' : '') +
|
|
1575
|
+
'</span>' +
|
|
1576
|
+
(section ? renderFileActions(f, section) : '') +
|
|
1577
|
+
'</div>'
|
|
1578
|
+
).join('');
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function renderFileActions(file, section) {
|
|
1582
|
+
let html = '<span class="file-actions">';
|
|
1583
|
+
if (section === 'unstaged') {
|
|
1584
|
+
html += '<button class="file-action-btn" data-action="stage" data-file="' + escHtml(file.path) + '" title="Stage">' + ICONS.plus + '</button>';
|
|
1585
|
+
html += '<button class="file-action-btn" data-action="discard" data-file="' + escHtml(file.path) + '" title="Discard changes">' + ICONS.x + '</button>';
|
|
1586
|
+
} else if (section === 'staged') {
|
|
1587
|
+
html += '<button class="file-action-btn" data-action="unstage" data-file="' + escHtml(file.path) + '" title="Unstage">' + ICONS.minus + '</button>';
|
|
1588
|
+
}
|
|
1589
|
+
html += '<button class="file-action-btn" data-action="open" data-file="' + escHtml(file.path) + '" title="Open file">' + ICONS.fileOpen + '</button>';
|
|
1590
|
+
html += '</span>';
|
|
1591
|
+
return html;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function fileViewToggleHtml() {
|
|
1595
|
+
return '<div class="file-view-toggle">' +
|
|
1596
|
+
'<button class="toggle-btn' + (state.fileViewMode === 'list' ? ' active' : '') + '" data-view="list" title="List view">' + ICONS.list + '</button>' +
|
|
1597
|
+
'<button class="toggle-btn' + (state.fileViewMode === 'tree' ? ' active' : '') + '" data-view="tree" title="Tree view">' + ICONS.tree + '</button>' +
|
|
1598
|
+
'</div>';
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function renderUncommittedDetail() {
|
|
1602
|
+
const panel = document.getElementById('detail-panel');
|
|
1603
|
+
panel.classList.remove('hidden');
|
|
1604
|
+
const u = state.uncommitted;
|
|
1605
|
+
if (!u) { panel.classList.add('hidden'); return; }
|
|
1606
|
+
let html = '<h3>Uncommitted Changes</h3>';
|
|
1607
|
+
const hasFiles = u.staged.length > 0 || u.unstaged.length > 0 || (u.conflicted && u.conflicted.length > 0);
|
|
1608
|
+
if (hasFiles) {
|
|
1609
|
+
html += fileViewToggleHtml();
|
|
1610
|
+
}
|
|
1611
|
+
// Conflict section (above staged/unstaged)
|
|
1612
|
+
if (u.conflicted && u.conflicted.length > 0) {
|
|
1613
|
+
html += '<div class="file-list"><div class="conflict-header">⚠ Conflicts (' + u.conflicted.length + ')</div>';
|
|
1614
|
+
html += u.conflicted.map(f => {
|
|
1615
|
+
const fileName = f.path.split('/').pop() || f.path;
|
|
1616
|
+
return '<div class="file-item">'
|
|
1617
|
+
+ '<span class="file-status file-status-U">U</span>'
|
|
1618
|
+
+ '<span class="file-clickable" data-file="' + escHtml(f.path) + '" data-hash="uncommitted" data-parent="' + escHtml(state.head) + '">' + escHtml(f.path) + '</span>'
|
|
1619
|
+
+ '<div class="file-actions">'
|
|
1620
|
+
+ '<button class="file-action-btn" data-action="open-conflict" data-file="' + escHtml(f.path) + '" title="Open conflict file">' + ICONS.fileOpen + '</button>'
|
|
1621
|
+
+ '<button class="file-action-btn" data-action="stage" data-file="' + escHtml(f.path) + '" title="Mark resolved (stage)">' + ICONS.plus + '</button>'
|
|
1622
|
+
+ '</div></div>';
|
|
1623
|
+
}).join('');
|
|
1624
|
+
html += '</div>';
|
|
1625
|
+
}
|
|
1626
|
+
if (u.staged.length > 0) {
|
|
1627
|
+
html += '<div class="file-list"><div class="section-actions"><strong>Staged (' + u.staged.length + '):</strong>';
|
|
1628
|
+
html += '<button class="btn-sm section-action-btn" data-action="unstage-all">Unstage All</button></div>';
|
|
1629
|
+
html += renderFileListHtml(u.staged, 'staged', state.head, 'staged');
|
|
1630
|
+
html += '</div>';
|
|
1631
|
+
}
|
|
1632
|
+
if (u.unstaged.length > 0) {
|
|
1633
|
+
html += '<div class="file-list"><div class="section-actions"><strong>Unstaged (' + u.unstaged.length + '):</strong>';
|
|
1634
|
+
html += '<button class="btn-sm section-action-btn" data-action="stage-all">Stage All</button></div>';
|
|
1635
|
+
html += renderFileListHtml(u.unstaged, 'uncommitted', state.head, 'unstaged');
|
|
1636
|
+
html += '</div>';
|
|
1637
|
+
}
|
|
1638
|
+
if (u.staged.length === 0 && u.unstaged.length === 0 && (!u.conflicted || u.conflicted.length === 0)) {
|
|
1639
|
+
html += '<p>No uncommitted changes.</p>';
|
|
1640
|
+
}
|
|
1641
|
+
html += '<div class="commit-section">';
|
|
1642
|
+
html += '<textarea id="commit-message" placeholder="Commit message..." rows="3"></textarea>';
|
|
1643
|
+
html += '<div class="commit-actions"><button id="btn-commit" class="btn-sm btn-commit" disabled>Commit</button></div>';
|
|
1644
|
+
html += '</div>';
|
|
1645
|
+
panel.innerHTML = html;
|
|
1646
|
+
wireCommitControls();
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function wireCommitControls() {
|
|
1650
|
+
const textarea = document.getElementById('commit-message');
|
|
1651
|
+
const commitBtn = document.getElementById('btn-commit');
|
|
1652
|
+
if (!textarea || !commitBtn) return;
|
|
1653
|
+
const updateBtn = () => {
|
|
1654
|
+
const hasMsg = textarea.value.trim().length > 0;
|
|
1655
|
+
const hasStaged = state.uncommitted && state.uncommitted.staged.length > 0;
|
|
1656
|
+
commitBtn.disabled = !(hasMsg && hasStaged);
|
|
1657
|
+
};
|
|
1658
|
+
textarea.addEventListener('input', updateBtn);
|
|
1659
|
+
updateBtn();
|
|
1660
|
+
commitBtn.addEventListener('click', () => {
|
|
1661
|
+
const message = textarea.value.trim();
|
|
1662
|
+
if (!message) return;
|
|
1663
|
+
vscode.postMessage({ command: 'gitAction', action: 'commit', args: { message } });
|
|
1664
|
+
textarea.value = '';
|
|
1665
|
+
commitBtn.disabled = true;
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// --- Detail panel ---
|
|
1670
|
+
function renderDetailPanel(detail) {
|
|
1671
|
+
state._lastDetail = detail;
|
|
1672
|
+
const panel = document.getElementById('detail-panel');
|
|
1673
|
+
panel.classList.remove('hidden');
|
|
1674
|
+
|
|
1675
|
+
let html = '<h3>Commit Details</h3>';
|
|
1676
|
+
html += '<div class="detail-field"><span class="label">Hash:</span> ' + escHtml(detail.hash) + '</div>';
|
|
1677
|
+
html += '<div class="detail-field"><span class="label">Author:</span> ' + escHtml(detail.author) + ' <' + escHtml(detail.authorEmail) + '></div>';
|
|
1678
|
+
html += '<div class="detail-field"><span class="label">Date:</span> ' + new Date(detail.authorDate * 1000).toLocaleString() + '</div>';
|
|
1679
|
+
if (detail.committer !== detail.author) {
|
|
1680
|
+
html += '<div class="detail-field"><span class="label">Committer:</span> ' + escHtml(detail.committer) + ' <' + escHtml(detail.committerEmail) + '></div>';
|
|
1681
|
+
}
|
|
1682
|
+
if (detail.parents.length > 0) {
|
|
1683
|
+
html += '<div class="detail-field"><span class="label">Parents:</span> ' + detail.parents.map(p => escHtml(p.substring(0, 7))).join(', ') + '</div>';
|
|
1684
|
+
}
|
|
1685
|
+
html += '<div class="detail-message">' + escHtml(detail.message) + '</div>';
|
|
1686
|
+
|
|
1687
|
+
if (detail.fileChanges && detail.fileChanges.length > 0) {
|
|
1688
|
+
html += '<div class="file-list">' + fileViewToggleHtml() + '<strong>Files changed (' + detail.fileChanges.length + '):</strong>';
|
|
1689
|
+
html += renderFileListHtml(detail.fileChanges, detail.hash, detail.parents[0] || '');
|
|
1690
|
+
html += '</div>';
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
panel.innerHTML = html;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// --- Context menu ---
|
|
1697
|
+
function showCommitContextMenu(x, y, commit) {
|
|
1698
|
+
const menu = document.getElementById('context-menu');
|
|
1699
|
+
const items = [
|
|
1700
|
+
{ label: 'Copy Commit Hash', action: () => copyText(commit.hash) },
|
|
1701
|
+
{ label: 'Copy Short Hash', action: () => copyText(commit.hash.substring(0, 7)) },
|
|
1702
|
+
{ separator: true },
|
|
1703
|
+
{ label: 'Checkout...', action: () => gitAction('checkout', { target: commit.hash }) },
|
|
1704
|
+
{ label: 'Create Branch Here...', action: () => promptAndAction('Branch name:', (name) => gitAction('createBranch', { name, startPoint: commit.hash })) },
|
|
1705
|
+
{ label: 'Create Tag Here...', action: () => promptAndAction('Tag name:', (name) => gitAction('createTag', { name, hash: commit.hash })) },
|
|
1706
|
+
{ label: 'Create Worktree Here...', action: () => showCreateWorktreeDialog(commit.hash) },
|
|
1707
|
+
{ separator: true },
|
|
1708
|
+
{ label: 'Rebase current branch onto this...', action: () => {
|
|
1709
|
+
showDialog({
|
|
1710
|
+
title: 'Rebase',
|
|
1711
|
+
message: 'Rebase current branch (' + escHtml(state.currentBranch) + ') onto commit ' + commit.hash.substring(0, 7) + '?',
|
|
1712
|
+
rawMessage: true,
|
|
1713
|
+
confirmLabel: 'Rebase',
|
|
1714
|
+
onConfirm: () => gitAction('rebase', { branch: commit.hash }),
|
|
1715
|
+
});
|
|
1716
|
+
}},
|
|
1717
|
+
];
|
|
1718
|
+
// Add "Create PR" if PR creation is configured and commit has a branch ref
|
|
1719
|
+
if (state.settings.prCreation && state.settings.prCreation.urlTemplate) {
|
|
1720
|
+
const branchRef = (commit.refs || []).find(r => r.type === 'local' || r.type === 'head');
|
|
1721
|
+
if (branchRef) {
|
|
1722
|
+
items.push({ separator: true });
|
|
1723
|
+
items.push({ label: 'Create Pull Request (' + branchRef.name + ')', action: () => openPrUrl(branchRef.name) });
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
items.push(
|
|
1727
|
+
{ separator: true },
|
|
1728
|
+
{ label: 'Cherry-pick', action: () => gitAction('cherryPick', { hash: commit.hash }) },
|
|
1729
|
+
{ label: 'Revert', action: () => gitAction('revert', { hash: commit.hash }) },
|
|
1730
|
+
{ separator: true },
|
|
1731
|
+
{ label: 'Reset Current Branch to Here...', destructive: true, action: () => promptResetMode(commit.hash) },
|
|
1732
|
+
);
|
|
1733
|
+
|
|
1734
|
+
let html = '';
|
|
1735
|
+
items.forEach((item, idx) => {
|
|
1736
|
+
if (item.separator) {
|
|
1737
|
+
html += '<div class="ctx-separator"></div>';
|
|
1738
|
+
} else {
|
|
1739
|
+
html += '<div class="ctx-item' + (item.destructive ? ' destructive' : '') + '" data-idx="' + idx + '">' + escHtml(item.label) + '</div>';
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
menu.innerHTML = html;
|
|
1743
|
+
|
|
1744
|
+
// Position (clamp to viewport)
|
|
1745
|
+
menu.style.left = Math.min(x, window.innerWidth - 200) + 'px';
|
|
1746
|
+
menu.style.top = Math.min(y, window.innerHeight - 300) + 'px';
|
|
1747
|
+
menu.classList.remove('hidden');
|
|
1748
|
+
|
|
1749
|
+
// Bind click handlers
|
|
1750
|
+
menu.querySelectorAll('.ctx-item').forEach(el => {
|
|
1751
|
+
const idx = parseInt(el.dataset.idx);
|
|
1752
|
+
const item = items[idx];
|
|
1753
|
+
if (item && item.action) el.addEventListener('click', () => { hideContextMenu(); item.action(); });
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
// Close on click outside
|
|
1757
|
+
setTimeout(() => document.addEventListener('click', hideContextMenu, { once: true }), 0);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function showUncommittedContextMenu(x, y) {
|
|
1761
|
+
const menu = document.getElementById('context-menu');
|
|
1762
|
+
const items = [
|
|
1763
|
+
{ label: 'Stash Uncommitted Changes...', action: () => {
|
|
1764
|
+
showDialog({
|
|
1765
|
+
title: 'Stash Changes',
|
|
1766
|
+
input: { placeholder: 'Stash message (optional)' },
|
|
1767
|
+
confirmLabel: 'Stash',
|
|
1768
|
+
onConfirm: (message) => gitAction('stashSave', message ? { message } : {}),
|
|
1769
|
+
});
|
|
1770
|
+
}},
|
|
1771
|
+
{ label: 'Reset Uncommitted Changes...', destructive: true, action: () => {
|
|
1772
|
+
showDialog({
|
|
1773
|
+
title: 'Reset Changes',
|
|
1774
|
+
message: 'Reset all uncommitted changes. Staged changes will be unstaged.',
|
|
1775
|
+
select: { options: ['mixed', 'hard'], defaultValue: 'mixed', label: 'Reset mode:' },
|
|
1776
|
+
destructive: true,
|
|
1777
|
+
confirmLabel: 'Reset',
|
|
1778
|
+
onConfirm: (mode) => {
|
|
1779
|
+
if (mode === 'hard') {
|
|
1780
|
+
showDialog({
|
|
1781
|
+
title: 'Confirm Hard Reset',
|
|
1782
|
+
message: 'WARNING: --hard will permanently discard ALL uncommitted changes!',
|
|
1783
|
+
destructive: true,
|
|
1784
|
+
confirmLabel: 'Reset Hard',
|
|
1785
|
+
onConfirm: () => gitAction('reset', { mode: 'hard', hash: 'HEAD' }),
|
|
1786
|
+
});
|
|
1787
|
+
} else {
|
|
1788
|
+
gitAction('reset', { mode, hash: 'HEAD' });
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
});
|
|
1792
|
+
}},
|
|
1793
|
+
{ label: 'Clean Untracked Files...', destructive: true, action: () => {
|
|
1794
|
+
showDialog({
|
|
1795
|
+
title: 'Clean Untracked Files',
|
|
1796
|
+
message: 'Permanently delete all untracked files and directories. This cannot be undone!',
|
|
1797
|
+
destructive: true,
|
|
1798
|
+
confirmLabel: 'Clean',
|
|
1799
|
+
onConfirm: () => gitAction('clean', {}),
|
|
1800
|
+
});
|
|
1801
|
+
}},
|
|
1802
|
+
{ separator: true },
|
|
1803
|
+
{ label: 'Open Source Control View', action: () => vscode.postMessage({ command: 'openSourceControl' }) },
|
|
1804
|
+
];
|
|
1805
|
+
|
|
1806
|
+
let html = '';
|
|
1807
|
+
items.forEach((item, idx) => {
|
|
1808
|
+
if (item.separator) {
|
|
1809
|
+
html += '<div class="ctx-separator"></div>';
|
|
1810
|
+
} else {
|
|
1811
|
+
html += '<div class="ctx-item' + (item.destructive ? ' destructive' : '') + '" data-idx="' + idx + '">' + escHtml(item.label) + '</div>';
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
menu.innerHTML = html;
|
|
1815
|
+
menu.style.left = Math.min(x, window.innerWidth - 200) + 'px';
|
|
1816
|
+
menu.style.top = Math.min(y, window.innerHeight - 300) + 'px';
|
|
1817
|
+
menu.classList.remove('hidden');
|
|
1818
|
+
menu.querySelectorAll('.ctx-item').forEach(el => {
|
|
1819
|
+
const idx = parseInt(el.dataset.idx);
|
|
1820
|
+
const item = items[idx];
|
|
1821
|
+
if (item && item.action) el.addEventListener('click', () => { hideContextMenu(); item.action(); });
|
|
1822
|
+
});
|
|
1823
|
+
setTimeout(() => document.addEventListener('click', hideContextMenu, { once: true }), 0);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function showBranchContextMenu(x, y, branchName, refType, commit) {
|
|
1827
|
+
const menu = document.getElementById('context-menu');
|
|
1828
|
+
const items = [];
|
|
1829
|
+
|
|
1830
|
+
if (refType === 'head' || refType === 'local') {
|
|
1831
|
+
items.push(
|
|
1832
|
+
{ label: 'Checkout "' + branchName + '"', action: () => gitAction('checkout', { target: branchName }) },
|
|
1833
|
+
{ label: 'Merge into current branch', action: () => showDialog({
|
|
1834
|
+
title: 'Merge "' + branchName + '"',
|
|
1835
|
+
message: 'Merge "' + branchName + '" into the current branch?',
|
|
1836
|
+
confirmLabel: 'Merge',
|
|
1837
|
+
onConfirm: () => gitAction('merge', { branch: branchName }),
|
|
1838
|
+
})
|
|
1839
|
+
},
|
|
1840
|
+
{ label: 'Rebase onto "' + branchName + '"', action: () => showDialog({
|
|
1841
|
+
title: 'Rebase onto "' + branchName + '"',
|
|
1842
|
+
message: 'Rebase current branch onto "' + branchName + '"?',
|
|
1843
|
+
confirmLabel: 'Rebase',
|
|
1844
|
+
onConfirm: () => gitAction('rebase', { branch: branchName }),
|
|
1845
|
+
})
|
|
1846
|
+
},
|
|
1847
|
+
{ separator: true },
|
|
1848
|
+
{ label: 'Rename branch...', action: () => showDialog({
|
|
1849
|
+
title: 'Rename Branch',
|
|
1850
|
+
input: { placeholder: 'New branch name', defaultValue: branchName },
|
|
1851
|
+
confirmLabel: 'Rename',
|
|
1852
|
+
onConfirm: (newName) => { if (newName && newName !== branchName) gitAction('renameBranch', { oldName: branchName, newName }); },
|
|
1853
|
+
})
|
|
1854
|
+
},
|
|
1855
|
+
);
|
|
1856
|
+
if (refType !== 'head') {
|
|
1857
|
+
items.push(
|
|
1858
|
+
{ label: 'Delete branch...', destructive: true, action: () => showDialog({
|
|
1859
|
+
title: 'Delete Branch',
|
|
1860
|
+
message: 'Delete local branch "' + branchName + '"?',
|
|
1861
|
+
destructive: true,
|
|
1862
|
+
confirmLabel: 'Delete',
|
|
1863
|
+
onConfirm: () => gitAction('deleteBranch', { name: branchName, force: false }),
|
|
1864
|
+
})
|
|
1865
|
+
},
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
if (state.settings.prCreation && state.settings.prCreation.urlTemplate) {
|
|
1869
|
+
items.push({ separator: true });
|
|
1870
|
+
items.push({ label: 'Create Pull Request', action: () => openPrUrl(branchName) });
|
|
1871
|
+
}
|
|
1872
|
+
} else if (refType === 'remote') {
|
|
1873
|
+
items.push(
|
|
1874
|
+
{ label: 'Checkout as local branch', action: () => gitAction('checkout', { target: branchName }) },
|
|
1875
|
+
{ separator: true },
|
|
1876
|
+
{ label: 'Delete remote branch...', destructive: true, action: () => showDialog({
|
|
1877
|
+
title: 'Delete Remote Branch',
|
|
1878
|
+
message: 'Delete remote branch "' + branchName + '"? This cannot be undone.',
|
|
1879
|
+
destructive: true,
|
|
1880
|
+
confirmLabel: 'Delete',
|
|
1881
|
+
onConfirm: () => {
|
|
1882
|
+
const parts = branchName.split('/');
|
|
1883
|
+
const remote = parts[0];
|
|
1884
|
+
const branch = parts.slice(1).join('/');
|
|
1885
|
+
gitAction('push', { remote, branch, force: false, delete: true });
|
|
1886
|
+
},
|
|
1887
|
+
})
|
|
1888
|
+
},
|
|
1889
|
+
);
|
|
1890
|
+
} else if (refType === 'tag') {
|
|
1891
|
+
items.push(
|
|
1892
|
+
{ label: 'Checkout tag "' + branchName + '"', action: () => gitAction('checkout', { target: branchName }) },
|
|
1893
|
+
{ separator: true },
|
|
1894
|
+
{ label: 'Delete tag...', destructive: true, action: () => showDialog({
|
|
1895
|
+
title: 'Delete Tag',
|
|
1896
|
+
message: 'Delete tag "' + branchName + '"?',
|
|
1897
|
+
destructive: true,
|
|
1898
|
+
confirmLabel: 'Delete',
|
|
1899
|
+
onConfirm: () => gitAction('deleteTag', { name: branchName }),
|
|
1900
|
+
})
|
|
1901
|
+
},
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
let html = '';
|
|
1906
|
+
items.forEach((item, idx) => {
|
|
1907
|
+
if (item.separator) {
|
|
1908
|
+
html += '<div class="ctx-separator"></div>';
|
|
1909
|
+
} else {
|
|
1910
|
+
html += '<div class="ctx-item' + (item.destructive ? ' destructive' : '') + '" data-idx="' + idx + '">' + escHtml(item.label) + '</div>';
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
menu.innerHTML = html;
|
|
1914
|
+
menu.style.left = Math.min(x, window.innerWidth - 220) + 'px';
|
|
1915
|
+
menu.style.top = Math.min(y, window.innerHeight - 300) + 'px';
|
|
1916
|
+
menu.classList.remove('hidden');
|
|
1917
|
+
menu.querySelectorAll('.ctx-item').forEach(el => {
|
|
1918
|
+
const idx = parseInt(el.dataset.idx);
|
|
1919
|
+
const item = items[idx];
|
|
1920
|
+
if (item && item.action) el.addEventListener('click', () => { hideContextMenu(); item.action(); });
|
|
1921
|
+
});
|
|
1922
|
+
setTimeout(() => document.addEventListener('click', hideContextMenu, { once: true }), 0);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function hideContextMenu() {
|
|
1926
|
+
document.getElementById('context-menu').classList.add('hidden');
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function gitAction(action, args) {
|
|
1930
|
+
vscode.postMessage({ command: 'gitAction', action, args });
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
function promptAndAction(title, callback) {
|
|
1934
|
+
showDialog({ title, input: { placeholder: title }, onConfirm: (val) => { if (val) callback(val); } });
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function promptResetMode(hash) {
|
|
1938
|
+
showDialog({
|
|
1939
|
+
title: 'Reset Current Branch',
|
|
1940
|
+
select: { options: ['soft', 'mixed', 'hard'], defaultValue: 'mixed', label: 'Reset mode:' },
|
|
1941
|
+
onConfirm: (mode) => {
|
|
1942
|
+
if (mode === 'hard') {
|
|
1943
|
+
showDialog({
|
|
1944
|
+
title: 'Confirm Hard Reset',
|
|
1945
|
+
message: 'WARNING: --hard will discard ALL uncommitted changes. This cannot be undone!',
|
|
1946
|
+
destructive: true,
|
|
1947
|
+
confirmLabel: 'Reset Hard',
|
|
1948
|
+
onConfirm: () => gitAction('reset', { mode, hash }),
|
|
1949
|
+
});
|
|
1950
|
+
} else {
|
|
1951
|
+
gitAction('reset', { mode, hash });
|
|
1952
|
+
}
|
|
1953
|
+
},
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function copyText(text) {
|
|
1958
|
+
navigator.clipboard.writeText(text).catch(() => {});
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// --- Dialog system ---
|
|
1962
|
+
function showDialog(opts) {
|
|
1963
|
+
const overlay = document.createElement('div');
|
|
1964
|
+
overlay.className = 'dialog-overlay';
|
|
1965
|
+
const dialog = document.createElement('div');
|
|
1966
|
+
dialog.className = 'dialog';
|
|
1967
|
+
dialog.innerHTML = '<h3>' + escHtml(opts.title || 'Dialog') + '</h3>';
|
|
1968
|
+
if (opts.message) {
|
|
1969
|
+
const msgHtml = opts.rawMessage ? opts.message : escHtml(opts.message);
|
|
1970
|
+
dialog.innerHTML += '<p' + (opts.destructive ? ' class="warning"' : '') + '>' + msgHtml + '</p>';
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
let inputEl = null;
|
|
1974
|
+
if (opts.input) {
|
|
1975
|
+
inputEl = document.createElement('input');
|
|
1976
|
+
inputEl.type = 'text';
|
|
1977
|
+
inputEl.placeholder = opts.input.placeholder || '';
|
|
1978
|
+
if (opts.input.defaultValue) inputEl.value = opts.input.defaultValue;
|
|
1979
|
+
dialog.appendChild(inputEl);
|
|
1980
|
+
}
|
|
1981
|
+
if (opts.select) {
|
|
1982
|
+
if (opts.select.label) dialog.innerHTML += '<p>' + escHtml(opts.select.label) + '</p>';
|
|
1983
|
+
inputEl = document.createElement('select');
|
|
1984
|
+
opts.select.options.forEach(o => {
|
|
1985
|
+
const opt = document.createElement('option');
|
|
1986
|
+
opt.value = o; opt.textContent = o;
|
|
1987
|
+
if (o === opts.select.defaultValue) opt.selected = true;
|
|
1988
|
+
inputEl.appendChild(opt);
|
|
1989
|
+
});
|
|
1990
|
+
dialog.appendChild(inputEl);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const actions = document.createElement('div');
|
|
1994
|
+
actions.className = 'dialog-actions';
|
|
1995
|
+
const cancelBtn = document.createElement('button');
|
|
1996
|
+
cancelBtn.textContent = opts.cancelLabel || 'Cancel';
|
|
1997
|
+
cancelBtn.className = 'secondary';
|
|
1998
|
+
cancelBtn.addEventListener('click', () => overlay.remove());
|
|
1999
|
+
const confirmBtn = document.createElement('button');
|
|
2000
|
+
confirmBtn.textContent = opts.confirmLabel || 'OK';
|
|
2001
|
+
confirmBtn.className = opts.destructive ? 'btn-danger' : 'btn-primary';
|
|
2002
|
+
confirmBtn.addEventListener('click', () => {
|
|
2003
|
+
overlay.remove();
|
|
2004
|
+
if (opts.onConfirm) opts.onConfirm(inputEl ? inputEl.value : undefined);
|
|
2005
|
+
});
|
|
2006
|
+
actions.appendChild(cancelBtn);
|
|
2007
|
+
actions.appendChild(confirmBtn);
|
|
2008
|
+
dialog.appendChild(actions);
|
|
2009
|
+
overlay.appendChild(dialog);
|
|
2010
|
+
document.body.appendChild(overlay);
|
|
2011
|
+
|
|
2012
|
+
// Focus input and handle Enter/Escape
|
|
2013
|
+
if (inputEl) setTimeout(() => inputEl.focus(), 50);
|
|
2014
|
+
overlay.addEventListener('keydown', (e) => {
|
|
2015
|
+
if (e.key === 'Escape') overlay.remove();
|
|
2016
|
+
if (e.key === 'Enter') confirmBtn.click();
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// --- Mobile long-press ---
|
|
2021
|
+
function setupLongPress(el, callback) {
|
|
2022
|
+
let timer = null;
|
|
2023
|
+
let startX = 0, startY = 0;
|
|
2024
|
+
el.addEventListener('touchstart', (e) => {
|
|
2025
|
+
startX = e.touches[0].clientX;
|
|
2026
|
+
startY = e.touches[0].clientY;
|
|
2027
|
+
timer = setTimeout(() => { e.preventDefault(); callback(startX, startY); }, 500);
|
|
2028
|
+
}, { passive: false });
|
|
2029
|
+
el.addEventListener('touchmove', (e) => {
|
|
2030
|
+
if (timer && (Math.abs(e.touches[0].clientX - startX) > 10 || Math.abs(e.touches[0].clientY - startY) > 10)) {
|
|
2031
|
+
clearTimeout(timer); timer = null;
|
|
2032
|
+
}
|
|
2033
|
+
}, { passive: true });
|
|
2034
|
+
el.addEventListener('touchend', () => { if (timer) { clearTimeout(timer); timer = null; } });
|
|
2035
|
+
el.addEventListener('touchcancel', () => { if (timer) { clearTimeout(timer); timer = null; } });
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// --- Text formatter (URLs, issues, commit hashes) ---
|
|
2039
|
+
function formatCommitMessage(msg) {
|
|
2040
|
+
let safe = escHtml(msg);
|
|
2041
|
+
// Apply issue linking rules from settings
|
|
2042
|
+
const rules = state.settings.issueLinkingRules || [];
|
|
2043
|
+
for (const rule of rules) {
|
|
2044
|
+
if (!rule.pattern) continue;
|
|
2045
|
+
if (rule.pattern.length > 200) continue; // ReDoS guard
|
|
2046
|
+
try {
|
|
2047
|
+
const re = new RegExp(rule.pattern, 'g');
|
|
2048
|
+
if (rule.url) {
|
|
2049
|
+
safe = safe.replace(re, function(match) {
|
|
2050
|
+
let href = rule.url;
|
|
2051
|
+
for (let i = 1; i < arguments.length - 2; i++) {
|
|
2052
|
+
if (typeof arguments[i] === 'string') href = href.split('$' + i).join(arguments[i]);
|
|
2053
|
+
}
|
|
2054
|
+
return '<a class="commit-link" href="' + escHtml(href) + '" target="_blank" title="' + escHtml(href) + '">' + match + '</a>';
|
|
2055
|
+
});
|
|
2056
|
+
} else {
|
|
2057
|
+
safe = safe.replace(re, '<span class="commit-link" title="$&">$&</span>');
|
|
2058
|
+
}
|
|
2059
|
+
} catch (e) { /* invalid regex — skip */ }
|
|
2060
|
+
}
|
|
2061
|
+
// Short commit hashes
|
|
2062
|
+
safe = safe.replace(/\\b([0-9a-f]{7,40})\\b/g, '<span class="commit-link" title="$1">$1</span>');
|
|
2063
|
+
// URLs — skip if already inside an <a> tag
|
|
2064
|
+
safe = safe.replace(/(<a[^>]*>.*?<\\/a>)|(https?:\\/\\/[^\\s<]+)/g, (m, linked, url) => {
|
|
2065
|
+
if (linked) return linked;
|
|
2066
|
+
return '<a class="commit-link" href="' + url + '" target="_blank">' + url + '</a>';
|
|
2067
|
+
});
|
|
2068
|
+
return safe;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// --- Find widget ---
|
|
2072
|
+
const findBar = document.getElementById('find-bar');
|
|
2073
|
+
const findInput = document.getElementById('find-input');
|
|
2074
|
+
|
|
2075
|
+
document.getElementById('btn-find').addEventListener('click', toggleFind);
|
|
2076
|
+
|
|
2077
|
+
function toggleFind() {
|
|
2078
|
+
findBar.classList.toggle('hidden');
|
|
2079
|
+
if (!findBar.classList.contains('hidden')) findInput.focus();
|
|
2080
|
+
else clearSearch();
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
findInput.addEventListener('input', () => doSearch(findInput.value));
|
|
2084
|
+
document.getElementById('find-next').addEventListener('click', () => navigateSearch(1));
|
|
2085
|
+
document.getElementById('find-prev').addEventListener('click', () => navigateSearch(-1));
|
|
2086
|
+
document.getElementById('find-close').addEventListener('click', () => { findBar.classList.add('hidden'); clearSearch(); });
|
|
2087
|
+
|
|
2088
|
+
function doSearch(query) {
|
|
2089
|
+
clearSearchHighlights();
|
|
2090
|
+
state.searchMatches = [];
|
|
2091
|
+
state.searchIndex = -1;
|
|
2092
|
+
if (!query.trim()) { document.getElementById('find-count').textContent = ''; return; }
|
|
2093
|
+
const q = query.toLowerCase();
|
|
2094
|
+
const displayCommits = getDisplayCommits();
|
|
2095
|
+
document.querySelectorAll('.commit-row:not(.header-row)').forEach((row, idx) => {
|
|
2096
|
+
const commit = displayCommits[idx];
|
|
2097
|
+
if (!commit || commit.hash === 'uncommitted') return;
|
|
2098
|
+
const match = commit.message.toLowerCase().includes(q) ||
|
|
2099
|
+
commit.author.toLowerCase().includes(q) ||
|
|
2100
|
+
commit.hash.toLowerCase().startsWith(q);
|
|
2101
|
+
if (match) { state.searchMatches.push(idx); row.classList.add('search-match'); }
|
|
2102
|
+
});
|
|
2103
|
+
document.getElementById('find-count').textContent = state.searchMatches.length + ' match(es)';
|
|
2104
|
+
if (state.searchMatches.length > 0) navigateSearch(0);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
function navigateSearch(dir) {
|
|
2108
|
+
if (state.searchMatches.length === 0) return;
|
|
2109
|
+
if (dir === 0) state.searchIndex = 0;
|
|
2110
|
+
else state.searchIndex = (state.searchIndex + dir + state.searchMatches.length) % state.searchMatches.length;
|
|
2111
|
+
const idx = state.searchMatches[state.searchIndex];
|
|
2112
|
+
const rows = document.querySelectorAll('.commit-row:not(.header-row)');
|
|
2113
|
+
if (rows[idx]) rows[idx].scrollIntoView({ block: 'center' });
|
|
2114
|
+
document.getElementById('find-count').textContent = (state.searchIndex + 1) + ' of ' + state.searchMatches.length;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
function clearSearch() {
|
|
2118
|
+
clearSearchHighlights();
|
|
2119
|
+
state.searchMatches = [];
|
|
2120
|
+
state.searchIndex = -1;
|
|
2121
|
+
findInput.value = '';
|
|
2122
|
+
document.getElementById('find-count').textContent = '';
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function clearSearchHighlights() {
|
|
2126
|
+
document.querySelectorAll('.search-match').forEach(el => el.classList.remove('search-match'));
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// --- Keyboard shortcuts ---
|
|
2130
|
+
document.addEventListener('keydown', (e) => {
|
|
2131
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'f') { e.preventDefault(); toggleFind(); }
|
|
2132
|
+
if (e.key === 'Escape') {
|
|
2133
|
+
hideContextMenu();
|
|
2134
|
+
if (!findBar.classList.contains('hidden')) { findBar.classList.add('hidden'); clearSearch(); }
|
|
2135
|
+
else if (state.expandedCommit) { state.selectedCommit = null; state.expandedCommit = null; document.getElementById('detail-panel').classList.add('hidden'); document.querySelectorAll('.commit-row.selected').forEach(el => el.classList.remove('selected')); }
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
// --- Scroll to load more ---
|
|
2140
|
+
document.getElementById('graph-container').addEventListener('scroll', (e) => {
|
|
2141
|
+
const container = e.target;
|
|
2142
|
+
if (container.scrollTop + container.clientHeight >= container.scrollHeight - 50) {
|
|
2143
|
+
if (!state.loading && state.commits.length >= state.maxCommits) {
|
|
2144
|
+
state.loading = true;
|
|
2145
|
+
document.getElementById('loading').classList.remove('hidden');
|
|
2146
|
+
vscode.postMessage({ command: 'requestCommits', maxCommits: state.maxCommits, skip: state.commits.length });
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
|
|
2151
|
+
// --- Utilities ---
|
|
2152
|
+
function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
2153
|
+
|
|
2154
|
+
function formatDate(ts) {
|
|
2155
|
+
const fmt = state.settings.dateFormat;
|
|
2156
|
+
if (fmt === 'iso') return new Date(ts * 1000).toISOString().substring(0, 16).replace('T', ' ');
|
|
2157
|
+
if (fmt === 'absolute') return new Date(ts * 1000).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
2158
|
+
const now = Date.now() / 1000;
|
|
2159
|
+
const diff = now - ts;
|
|
2160
|
+
if (diff < 60) return 'just now';
|
|
2161
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
2162
|
+
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
2163
|
+
if (diff < 2592000) return Math.floor(diff / 86400) + 'd ago';
|
|
2164
|
+
if (diff < 31536000) return Math.floor(diff / 2592000) + 'mo ago';
|
|
2165
|
+
return Math.floor(diff / 31536000) + 'y ago';
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
function updateStatus() {
|
|
2169
|
+
const parts = [];
|
|
2170
|
+
if (state.currentBranch) parts.push(state.currentBranch);
|
|
2171
|
+
parts.push(state.commits.length + ' commits');
|
|
2172
|
+
parts.push(state.branches.length + ' branches');
|
|
2173
|
+
parts.push(state.tags.length + ' tags');
|
|
2174
|
+
document.getElementById('status-text').textContent = parts.join(' | ');
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// --- Settings panel ---
|
|
2178
|
+
const settingsPanel = document.getElementById('settings-panel');
|
|
2179
|
+
|
|
2180
|
+
document.getElementById('btn-settings').addEventListener('click', () => {
|
|
2181
|
+
const isOpen = settingsPanel.classList.toggle('open');
|
|
2182
|
+
if (isOpen) {
|
|
2183
|
+
vscode.postMessage({ command: 'requestSettings' });
|
|
2184
|
+
vscode.postMessage({ command: 'requestUserDetails' });
|
|
2185
|
+
renderRemotesList();
|
|
2186
|
+
}
|
|
2187
|
+
});
|
|
2188
|
+
document.getElementById('settings-close').addEventListener('click', () => {
|
|
2189
|
+
settingsPanel.classList.remove('open');
|
|
2190
|
+
});
|
|
2191
|
+
|
|
2192
|
+
function applySettingsToUI() {
|
|
2193
|
+
const s = state.settings;
|
|
2194
|
+
document.getElementById('s-maxCommits').value = s.maxCommits;
|
|
2195
|
+
document.getElementById('s-showTags').checked = s.showTags;
|
|
2196
|
+
document.getElementById('s-showStashes').checked = s.showStashes;
|
|
2197
|
+
document.getElementById('s-showRemoteBranches').checked = s.showRemoteBranches;
|
|
2198
|
+
document.getElementById('s-graphStyle').value = s.graphStyle;
|
|
2199
|
+
document.getElementById('s-firstParentOnly').checked = s.firstParentOnly;
|
|
2200
|
+
document.getElementById('s-dateFormat').value = s.dateFormat;
|
|
2201
|
+
document.getElementById('s-commitOrdering').value = s.commitOrdering;
|
|
2202
|
+
document.getElementById('s-autoFetchInterval').value = s.autoFetchInterval || 0;
|
|
2203
|
+
graphConfig.style = s.graphStyle;
|
|
2204
|
+
startAutoFetch(s.autoFetchInterval);
|
|
2205
|
+
renderIssueRules();
|
|
2206
|
+
applyPrSettingsToUI();
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// General setting change handlers
|
|
2210
|
+
['showTags', 'showStashes', 'showRemoteBranches', 'firstParentOnly'].forEach(key => {
|
|
2211
|
+
document.getElementById('s-' + key).addEventListener('change', (e) => {
|
|
2212
|
+
vscode.postMessage({ command: 'updateSetting', key, value: e.target.checked });
|
|
2213
|
+
state.settings[key] = e.target.checked;
|
|
2214
|
+
renderCommitList();
|
|
2215
|
+
});
|
|
2216
|
+
});
|
|
2217
|
+
['graphStyle', 'dateFormat', 'commitOrdering'].forEach(key => {
|
|
2218
|
+
document.getElementById('s-' + key).addEventListener('change', (e) => {
|
|
2219
|
+
vscode.postMessage({ command: 'updateSetting', key, value: e.target.value });
|
|
2220
|
+
state.settings[key] = e.target.value;
|
|
2221
|
+
if (key === 'graphStyle') graphConfig.style = e.target.value;
|
|
2222
|
+
if (key === 'dateFormat') renderCommitList();
|
|
2223
|
+
});
|
|
2224
|
+
});
|
|
2225
|
+
document.getElementById('s-maxCommits').addEventListener('change', (e) => {
|
|
2226
|
+
const n = parseInt(e.target.value, 10);
|
|
2227
|
+
if (n > 0 && n <= 10000) {
|
|
2228
|
+
state.maxCommits = n;
|
|
2229
|
+
state.settings.maxCommits = n;
|
|
2230
|
+
vscode.postMessage({ command: 'updateSetting', key: 'maxCommits', value: n });
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
document.getElementById('s-autoFetchInterval').addEventListener('change', (e) => {
|
|
2234
|
+
const val = parseInt(e.target.value, 10);
|
|
2235
|
+
state.settings.autoFetchInterval = val;
|
|
2236
|
+
vscode.postMessage({ command: 'updateSetting', key: 'autoFetchInterval', value: val });
|
|
2237
|
+
startAutoFetch(val);
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
// User details
|
|
2241
|
+
document.getElementById('s-saveUser').addEventListener('click', () => {
|
|
2242
|
+
const name = document.getElementById('s-userName').value.trim();
|
|
2243
|
+
const email = document.getElementById('s-userEmail').value.trim();
|
|
2244
|
+
vscode.postMessage({ command: 'updateUserDetails', name, email });
|
|
2245
|
+
});
|
|
2246
|
+
|
|
2247
|
+
// Remotes
|
|
2248
|
+
function renderRemotesList() {
|
|
2249
|
+
const container = document.getElementById('s-remotes-list');
|
|
2250
|
+
if (state.remotes.length === 0) {
|
|
2251
|
+
container.innerHTML = '<p style="font-size:12px;color:var(--subtext)">No remotes configured.</p>';
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
container.innerHTML = state.remotes.map(r =>
|
|
2255
|
+
'<div class="remote-item">' +
|
|
2256
|
+
'<div class="remote-name">' + escHtml(r.name) + '</div>' +
|
|
2257
|
+
'<div class="remote-url">' + escHtml(r.fetchUrl) + '</div>' +
|
|
2258
|
+
'<div class="remote-actions">' +
|
|
2259
|
+
'<button class="btn-sm" data-edit-remote="' + escHtml(r.name) + '">Edit URL</button>' +
|
|
2260
|
+
'<button class="btn-sm" style="color:var(--red)" data-rm-remote="' + escHtml(r.name) + '">Remove</button>' +
|
|
2261
|
+
'</div>' +
|
|
2262
|
+
'</div>'
|
|
2263
|
+
).join('');
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
document.getElementById('s-remotes-list').addEventListener('click', (e) => {
|
|
2267
|
+
const editBtn = e.target.closest('[data-edit-remote]');
|
|
2268
|
+
if (editBtn) {
|
|
2269
|
+
const name = editBtn.dataset.editRemote;
|
|
2270
|
+
const remote = state.remotes.find(r => r.name === name);
|
|
2271
|
+
showDialog({
|
|
2272
|
+
title: 'Edit Remote URL: ' + name,
|
|
2273
|
+
input: { placeholder: 'New URL', defaultValue: remote ? remote.fetchUrl : '' },
|
|
2274
|
+
onConfirm: (url) => { if (url) vscode.postMessage({ command: 'editRemoteUrl', name, url }); },
|
|
2275
|
+
});
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
const rmBtn = e.target.closest('[data-rm-remote]');
|
|
2279
|
+
if (rmBtn) {
|
|
2280
|
+
const name = rmBtn.dataset.rmRemote;
|
|
2281
|
+
showDialog({
|
|
2282
|
+
title: 'Remove Remote',
|
|
2283
|
+
message: 'Remove remote "' + name + '"? This cannot be undone.',
|
|
2284
|
+
destructive: true,
|
|
2285
|
+
confirmLabel: 'Remove',
|
|
2286
|
+
onConfirm: () => vscode.postMessage({ command: 'removeRemote', name }),
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
|
|
2291
|
+
document.getElementById('s-addRemote').addEventListener('click', () => {
|
|
2292
|
+
const name = document.getElementById('s-newRemoteName').value.trim();
|
|
2293
|
+
const url = document.getElementById('s-newRemoteUrl').value.trim();
|
|
2294
|
+
if (name && url) {
|
|
2295
|
+
vscode.postMessage({ command: 'addRemote', name, url });
|
|
2296
|
+
document.getElementById('s-newRemoteName').value = '';
|
|
2297
|
+
document.getElementById('s-newRemoteUrl').value = '';
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2300
|
+
|
|
2301
|
+
// --- Issue Linking ---
|
|
2302
|
+
function renderIssueRules() {
|
|
2303
|
+
const rules = state.settings.issueLinkingRules || [];
|
|
2304
|
+
const container = document.getElementById('issue-rules-list');
|
|
2305
|
+
container.innerHTML = rules.map((r, i) =>
|
|
2306
|
+
'<div class="issue-rule-row" data-idx="' + i + '">' +
|
|
2307
|
+
'<input type="text" class="rule-pattern" placeholder="Regex, e.g. #(\\d+)" value="' + escHtml(r.pattern) + '">' +
|
|
2308
|
+
'<input type="text" class="rule-url" placeholder="URL with $1, e.g. https://..." value="' + escHtml(r.url) + '">' +
|
|
2309
|
+
'<button class="rule-remove" title="Remove">×</button>' +
|
|
2310
|
+
'</div>'
|
|
2311
|
+
).join('');
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
let issueRuleDebounce = null;
|
|
2315
|
+
document.getElementById('issue-rules-list').addEventListener('input', (e) => {
|
|
2316
|
+
const row = e.target.closest('.issue-rule-row');
|
|
2317
|
+
if (!row) return;
|
|
2318
|
+
const idx = parseInt(row.dataset.idx);
|
|
2319
|
+
const rules = [...(state.settings.issueLinkingRules || [])];
|
|
2320
|
+
if (!rules[idx]) return;
|
|
2321
|
+
if (e.target.classList.contains('rule-pattern')) {
|
|
2322
|
+
rules[idx] = { ...rules[idx], pattern: e.target.value };
|
|
2323
|
+
try { new RegExp(e.target.value); e.target.classList.remove('rule-error'); }
|
|
2324
|
+
catch { e.target.classList.add('rule-error'); return; }
|
|
2325
|
+
}
|
|
2326
|
+
if (e.target.classList.contains('rule-url')) {
|
|
2327
|
+
rules[idx] = { ...rules[idx], url: e.target.value };
|
|
2328
|
+
}
|
|
2329
|
+
state.settings.issueLinkingRules = rules;
|
|
2330
|
+
clearTimeout(issueRuleDebounce);
|
|
2331
|
+
issueRuleDebounce = setTimeout(() => {
|
|
2332
|
+
vscode.postMessage({ command: 'updateSetting', key: 'issueLinkingRules', value: rules });
|
|
2333
|
+
}, 500);
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
document.getElementById('issue-rules-list').addEventListener('click', (e) => {
|
|
2337
|
+
if (!e.target.closest('.rule-remove')) return;
|
|
2338
|
+
const row = e.target.closest('.issue-rule-row');
|
|
2339
|
+
const idx = parseInt(row.dataset.idx);
|
|
2340
|
+
const rules = [...(state.settings.issueLinkingRules || [])];
|
|
2341
|
+
rules.splice(idx, 1);
|
|
2342
|
+
state.settings.issueLinkingRules = rules;
|
|
2343
|
+
vscode.postMessage({ command: 'updateSetting', key: 'issueLinkingRules', value: rules });
|
|
2344
|
+
renderIssueRules();
|
|
2345
|
+
});
|
|
2346
|
+
|
|
2347
|
+
document.getElementById('add-issue-rule').addEventListener('click', () => {
|
|
2348
|
+
const rules = [...(state.settings.issueLinkingRules || []), { pattern: '', url: '' }];
|
|
2349
|
+
state.settings.issueLinkingRules = rules;
|
|
2350
|
+
vscode.postMessage({ command: 'updateSetting', key: 'issueLinkingRules', value: rules });
|
|
2351
|
+
renderIssueRules();
|
|
2352
|
+
});
|
|
2353
|
+
|
|
2354
|
+
// --- PR Creation ---
|
|
2355
|
+
const PR_TEMPLATES = {
|
|
2356
|
+
github: 'https://github.com/\${owner}/\${repo}/compare/\${targetBranch}...\${sourceBranch}?expand=1',
|
|
2357
|
+
gitlab: 'https://gitlab.com/\${owner}/\${repo}/-/merge_requests/new?source_branch=\${sourceBranch}&target_branch=\${targetBranch}',
|
|
2358
|
+
bitbucket: 'https://bitbucket.org/\${owner}/\${repo}/pull-requests/new?source=\${sourceBranch}&dest=\${targetBranch}',
|
|
2359
|
+
custom: '',
|
|
2360
|
+
};
|
|
2361
|
+
|
|
2362
|
+
document.getElementById('pr-provider').addEventListener('change', (e) => {
|
|
2363
|
+
const provider = e.target.value;
|
|
2364
|
+
const prConfig = document.getElementById('pr-config');
|
|
2365
|
+
if (!provider) {
|
|
2366
|
+
prConfig.classList.add('hidden');
|
|
2367
|
+
state.settings.prCreation = null;
|
|
2368
|
+
vscode.postMessage({ command: 'updateSetting', key: 'prCreation', value: null });
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
prConfig.classList.remove('hidden');
|
|
2372
|
+
document.getElementById('pr-url-template').value = PR_TEMPLATES[provider] || '';
|
|
2373
|
+
document.getElementById('pr-target').value = 'main';
|
|
2374
|
+
vscode.postMessage({ command: 'requestOwnerRepo' });
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
document.getElementById('pr-save').addEventListener('click', () => {
|
|
2378
|
+
const provider = document.getElementById('pr-provider').value;
|
|
2379
|
+
if (!provider) return;
|
|
2380
|
+
const config = {
|
|
2381
|
+
provider,
|
|
2382
|
+
urlTemplate: document.getElementById('pr-url-template').value.trim(),
|
|
2383
|
+
owner: document.getElementById('pr-owner').value.trim(),
|
|
2384
|
+
repo: document.getElementById('pr-repo').value.trim(),
|
|
2385
|
+
defaultTargetBranch: document.getElementById('pr-target').value.trim() || 'main',
|
|
2386
|
+
};
|
|
2387
|
+
state.settings.prCreation = config;
|
|
2388
|
+
vscode.postMessage({ command: 'updateSetting', key: 'prCreation', value: config });
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
function applyPrSettingsToUI() {
|
|
2392
|
+
const pr = state.settings.prCreation;
|
|
2393
|
+
if (!pr) {
|
|
2394
|
+
document.getElementById('pr-provider').value = '';
|
|
2395
|
+
document.getElementById('pr-config').classList.add('hidden');
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
document.getElementById('pr-provider').value = pr.provider;
|
|
2399
|
+
document.getElementById('pr-config').classList.remove('hidden');
|
|
2400
|
+
document.getElementById('pr-owner').value = pr.owner || '';
|
|
2401
|
+
document.getElementById('pr-repo').value = pr.repo || '';
|
|
2402
|
+
document.getElementById('pr-target').value = pr.defaultTargetBranch || 'main';
|
|
2403
|
+
document.getElementById('pr-url-template').value = pr.urlTemplate || '';
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
function openPrUrl(sourceBranch) {
|
|
2407
|
+
const pr = state.settings.prCreation;
|
|
2408
|
+
if (!pr || !pr.urlTemplate) return;
|
|
2409
|
+
const url = pr.urlTemplate
|
|
2410
|
+
.replace(/\\$\\{owner\\}/g, encodeURIComponent(pr.owner))
|
|
2411
|
+
.replace(/\\$\\{repo\\}/g, encodeURIComponent(pr.repo))
|
|
2412
|
+
.replace(/\\$\\{sourceBranch\\}/g, encodeURIComponent(sourceBranch))
|
|
2413
|
+
.replace(/\\$\\{targetBranch\\}/g, encodeURIComponent(pr.defaultTargetBranch || 'main'));
|
|
2414
|
+
window.open(url, '_blank');
|
|
2415
|
+
}
|
|
2416
|
+
`;
|
|
2417
|
+
}
|