megit-app 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -1
- package/dist/assets/ConflictView-DGLXjOa1.js +1 -0
- package/dist/assets/{DiffView-BKo75iUG.js → DiffView-DHHWx0R5.js} +21 -21
- package/dist/assets/TerminalPanel-DtRWBLUl.js +20 -0
- package/dist/assets/index-Bfrt9LxG.js +251 -0
- package/dist/assets/index-C845Od_q.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/index.js +131 -5
- package/dist-server/operation.js +16 -0
- package/package.json +6 -6
- package/dist/assets/TerminalPanel-uV6s4v6R.js +0 -37
- package/dist/assets/index-DPM0na5E.css +0 -1
- package/dist/assets/index-o0rujUbV.js +0 -249
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg:#1e2228;--bg-panel:#23272e;--bg-tabbar:#14171c;--bg-tab:#1a1e24;--bg-hover:#2c313a;--bg-selected:#354254;--bg-wip:#2a2e25;--border:#3a404b;--fg:#d4d8de;--fg-dim:#9aa4b2;--fg-faint:#6b7280;--head-bg:#2e4436;--head-bg-hover:#3d5a47;--head-fg:#a8d99a}:root[data-theme=light]{--bg:#f6f8fa;--bg-panel:#fff;--bg-tabbar:#e4e7eb;--bg-tab:#eef0f3;--bg-hover:#e8eaed;--bg-selected:#cfdff5;--bg-wip:#f3f5e8;--border:#d0d7de;--fg:#1f2328;--fg-dim:#57606a;--fg-faint:#8b949e;--head-bg:#d3f2d9;--head-bg-hover:#b4e7c0;--head-fg:#116329}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg);color:var(--fg);font:16px/1.4 Ubuntu Mono,ui-monospace,monospace}button{background:var(--bg-hover);color:inherit;border:1px solid var(--border);cursor:pointer;border-radius:4px}button:hover{background:var(--border)}.app{flex-direction:column;height:100%;display:flex}.empty{color:var(--fg-faint);flex:1;justify-content:center;align-items:center;display:flex}.error{color:#e06c75;white-space:pre-wrap;padding:8px}.toasts{z-index:20;flex-direction:column;gap:6px;max-width:380px;display:flex;position:fixed;bottom:12px;right:12px}.toast{background:var(--bg-panel);color:var(--fg);white-space:pre-wrap;cursor:pointer;border:1px solid #e06c75;border-left-width:3px;border-radius:6px;padding:8px 10px;animation:.15s ease-out appear;box-shadow:0 2px 8px #0000004d}.tabbar{background:var(--bg-tabbar);flex-shrink:0;gap:2px;padding:6px 6px 0;display:flex}.logo{pointer-events:none;align-self:center;width:18px;height:18px;margin:0 4px 6px 2px}.tab{background:var(--bg-tab);cursor:pointer;border-radius:6px 6px 0 0;align-items:center;gap:6px;padding:6px 10px;display:flex}.tab.active{background:var(--bg-panel)}.tab.dragging{opacity:.5}.tab-close{width:18px;height:18px;color:var(--fg-dim);background:0 0;border:none;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;margin:-2px -4px -2px 0;padding:0;display:flex}.tab-close:hover{background:var(--border);color:var(--fg)}.tab-add{background:0 0;border:none;padding:6px 12px;font-size:15px}.build-tag{color:var(--fg-faint);-webkit-user-select:none;user-select:none;align-self:center;margin:0 4px 6px auto;font-size:11px}.modal-backdrop{z-index:10;background:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.modal{background:var(--bg-panel);border:1px solid var(--border);border-radius:8px;flex-direction:column;width:480px;max-height:70vh;display:flex}.modal-head{border-bottom:1px solid var(--border);align-items:center;gap:8px;padding:10px;display:flex}.modal-path{text-overflow:ellipsis;white-space:nowrap;color:var(--fg-dim);flex:1;overflow:hidden}.dirlist{margin:0;padding:4px;list-style:none;overflow-y:auto}.dirlist li{border-radius:4px;align-items:center;padding:4px 8px;display:flex}.dirlist li:hover{background:var(--bg-hover)}.dirname{cursor:pointer;text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.modal-label{text-transform:uppercase;letter-spacing:.5px;color:var(--fg-dim);padding:8px 12px 2px;font-size:11px}.modal-path b{color:var(--fg)}.picker{flex-direction:row;width:720px;height:440px;max-height:80vh;overflow:hidden}.picker-pane{flex-direction:column;flex:1;min-width:0;display:flex}.picker-recent{border-right:1px solid var(--border);background:var(--bg-tab);flex:none;width:240px}.picker-recent .dirlist li.sel{background:var(--bg-selected)}.recent-dot{background:#2ea043;border-radius:50%;flex-shrink:0;width:6px;height:6px}.dir-chevron{color:var(--fg-faint);cursor:pointer;padding-left:8px}.repoview{background:var(--bg-panel);flex-direction:column;flex:1;min-height:0;animation:.5s ease-out appear;display:flex}@keyframes appear{0%{opacity:0}}@media (prefers-reduced-motion:reduce){.repoview{animation:none}}.toolbar{border-bottom:1px solid var(--border);flex-shrink:0;grid-template-columns:1fr auto 1fr;align-items:center;gap:10px;margin-bottom:6px;padding:6px 10px;display:grid}.tb-left{align-items:center;gap:8px;min-width:0;display:flex}.tb-right{justify-content:flex-end;align-items:center;gap:6px;display:flex}.toolbar button{padding:3px 10px}.refresh-btn{align-items:center;padding:3px 5px;display:inline-flex}.refresh-btn .spin{animation:.5s cubic-bezier(.45,0,.55,1) infinite spin}@keyframes spin{to{transform:rotate(360deg)}}.tb-actions{align-items:center;gap:2px;display:flex}.toolbar .tb-btn{color:var(--fg-dim);background:0 0;border:none;align-items:center;gap:5px;padding:4px 8px;display:inline-flex}.tb-btn:hover:not(:disabled){background:var(--bg-hover);color:var(--fg)}.tb-btn:disabled{opacity:.4;cursor:default}.tb-badge{background:var(--bg-selected);min-width:15px;color:var(--fg);text-align:center;border-radius:7px;padding:0 4px;font-size:11px;line-height:15px}.repo-path{min-width:0;color:var(--fg-dim);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.github-link{color:var(--fg-dim);flex-shrink:0;align-items:center;display:inline-flex}.github-link:hover{color:var(--fg)}.term-btn{color:var(--fg-dim);align-items:center;padding:3px 5px;display:inline-flex}.term-btn:hover{color:var(--fg)}.term-btn.active{color:var(--fg);background:var(--bg-selected)}.term-panel{border-top:1px solid var(--border);background:var(--bg);flex-direction:column;flex-shrink:0;height:220px;display:flex;position:relative}.term-resizer{cursor:ns-resize;touch-action:none;z-index:3;height:6px;position:absolute;top:-3px;left:0;right:0}.term-resizer:hover,.term-resizer:active{background:#5a80b0}.term-head{flex-shrink:0;justify-content:space-between;align-items:center;padding:3px 6px 3px 8px;display:flex}.term-title{color:var(--fg-dim);align-items:center;gap:6px;font-size:12px;display:inline-flex}.term-head-actions{align-items:center;gap:2px;display:inline-flex}.term-close{color:var(--fg-dim);background:0 0;border:none;align-items:center;padding:0 4px;font-size:13px;display:inline-flex}.term-close:hover{color:var(--fg);background:0 0}.term-close:disabled{opacity:.35}.term-panes{flex:1;min-height:0;display:flex}.term-pane{flex:1;min-width:0;display:flex;position:relative}.term-pane+.term-pane{border-left:1px solid var(--border)}.term-pane-kill{z-index:2;color:#0000;background:0 0;border:none;padding:0 3px;font-size:12px;line-height:1.2;position:absolute;top:0;right:4px}.term-pane:hover .term-pane-kill{color:var(--fg-dim)}.term-pane-kill:hover,.term-pane-kill:focus-visible{color:var(--fg);background:0 0}.term-host{flex:1;min-width:0;min-height:0;padding:4px 0 0 8px}.term-host .xterm{height:100%}.theme-switch{background:var(--bg-hover);border:1px solid var(--border);width:40px;height:20px;color:var(--fg-dim);cursor:pointer;border-radius:10px;flex-shrink:0;justify-content:space-between;align-items:center;padding:0 4px;display:inline-flex;position:relative}.theme-switch input{opacity:0;cursor:pointer;margin:0;position:absolute;inset:0}.theme-switch svg{position:relative}.theme-switch .knob{background:var(--fg);border-radius:50%;width:14px;height:14px;transition:left .15s;position:absolute;top:2px;left:2px}.theme-switch input:checked~.knob{left:22px}.theme-switch:has(input:focus-visible){outline-offset:1px;outline:2px solid #5a80b0}.panes{flex:1;min-height:0;display:flex}.graph-pane{width:var(--graph-w,55%);flex-direction:column;flex-shrink:0;min-width:0;display:flex;position:relative}.graphview{flex:1;padding-bottom:8px;overflow:auto}.refs,.graph-col{transition:filter .15s}.graph-pane.busy .refs,.graph-pane.busy .graph-col{filter:blur(1px);pointer-events:none}.graph-pane.busy,.graph-pane.busy .row{cursor:progress}.graphview .row:not(.wip){content-visibility:auto;contain-intrinsic-size:auto 28px}.diff-overlay{z-index:2;background:var(--bg-panel);flex-direction:column;display:flex;position:absolute;inset:0}.diff-overlay-head{border-bottom:1px solid var(--border);flex-shrink:0;align-items:center;gap:8px;padding:6px 10px;display:flex}.diff-overlay-head .file-path{flex:1}.diff-close{background:0 0;border:none;padding:0 4px;font-size:14px}.searchbar{top:8px;left:calc(var(--refs-w) + var(--graph-col-w) + 24px);z-index:3;background:var(--bg-selected);border:1px solid var(--border);border-radius:8px;align-items:center;gap:6px;padding:5px 8px;animation:.12s ease-out appear;display:flex;position:absolute;box-shadow:0 3px 12px #00000059}.searchbar input{background:var(--bg-panel);width:220px;color:var(--fg);border:1px solid var(--border);font:inherit;border-radius:5px;padding:3px 6px}.searchbar .count{text-align:center;min-width:78px;color:var(--fg-dim);font-size:12px}.searchbar button{min-width:22px;color:var(--fg-dim);background:0 0;border:none;justify-content:center;align-items:center;padding:3px 5px;display:inline-flex}.searchbar button:hover,.searchbar button.deep.active{background:var(--bg-hover);color:var(--fg)}.splitter{cursor:col-resize;background:var(--border);touch-action:none;background-clip:padding-box;border:2px solid #0000;flex-shrink:0;width:5px}.splitter:hover,.splitter:active{background-color:#5a80b0}.row{cursor:pointer;white-space:nowrap;align-items:center;gap:8px;height:28px;padding:0 8px;scroll-margin-top:28px;display:flex;position:relative}.row.wip{background-color:var(--bg-panel);background-image:linear-gradient(to right, transparent calc(8px + var(--refs-w,120px)), var(--bg-wip) 0);z-index:1;font-style:italic;position:sticky;top:0}.row:hover:not(:has(.refs:hover)):not(.selected){background-image:linear-gradient(to right, transparent calc(8px + var(--refs-w,120px)), var(--bg-hover) 0)}.row.selected{background-image:linear-gradient(to right, transparent calc(8px + var(--refs-w,120px)), var(--bg-selected) 0)}.graph-cell{flex-shrink:0;overflow:visible}.refs{width:var(--refs-w,120px);cursor:default;flex-shrink:0;justify-content:flex-start;align-self:stretch;align-items:center;gap:4px;display:flex;overflow:hidden}.graph-col{width:var(--graph-col-w,90px);flex-shrink:0;justify-content:flex-start;align-self:stretch;align-items:center;padding:0 4px;display:flex;overflow:hidden}.col-splitter{cursor:col-resize;touch-action:none;z-index:1;width:5px;position:absolute;top:0;bottom:0}.col-splitter:hover,.col-splitter:active{background:#5a80b0}.ref-chips{gap:4px;min-width:0;display:flex}.ref-chip{color:var(--fg);background:var(--bg-panel);flex:1 1 calc(14px + var(--n-icons,0) * 15px);-webkit-user-select:none;user-select:none;cursor:pointer;border:1px solid;border-radius:8px;align-items:center;gap:3px;min-width:0;max-width:max-content;padding:1px 6px;font-size:14px;display:inline-flex}.ref-chip:hover{background:var(--bg-hover)}.ref-chip.head{background:var(--head-bg);color:var(--head-fg);font-weight:600}.ref-chip.head:hover{background:var(--head-bg-hover)}.ref-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.ref-line{opacity:.5;flex:1;align-self:center;min-width:6px;height:1px}.ref-bridge{opacity:.5;pointer-events:none;height:1px;position:absolute;top:calc(50% - .5px)}.ref-line.head,.ref-bridge.head{opacity:1;height:2px}.ref-bridge.head{top:calc(50% - 1px)}.ref-icon{opacity:.8;flex-shrink:0}.subject{text-overflow:ellipsis;flex:1;overflow:hidden}.date{color:var(--fg-dim);font-size:12px}.hash{color:var(--fg-faint);font-family:Ubuntu Mono,ui-monospace,monospace;font-size:12px}.load-more{margin:8px;padding:4px 14px}.ctx-menu{border:1px solid var(--border);background:var(--bg-panel);border-radius:6px;min-width:150px;margin:0;padding:4px;position:fixed;inset:auto;box-shadow:0 6px 20px #00000059}.ctx-item{width:100%;color:var(--fg);font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:4px;padding:5px 10px;font-size:14px;display:block}.ctx-item:hover{background:var(--bg-hover)}.ctx-item.sep{border-top:1px solid var(--border);border-radius:0 0 4px 4px;margin-top:5px;padding-top:9px}.ctx-item.danger{color:#e06c75}.ctx-item.danger:hover{color:#fff;background:#e06c75}.panel{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.commit-msg{border-bottom:1px solid var(--border);flex-shrink:0;max-height:30%;padding:10px 12px;position:relative;overflow-y:auto}.commit-subject{font-weight:600}.commit-body{white-space:pre-wrap;color:var(--fg-dim);margin-top:8px;font-size:13px}.msg-edit{vertical-align:-4px;color:var(--fg-dim);opacity:0;margin-left:8px;padding:4px;line-height:0;transition:opacity .1s;display:inline-flex}.msg-edit:hover{color:var(--fg)}.commit-msg:hover .msg-edit,.msg-edit:focus-visible{opacity:1}.msg-box{border:1px solid var(--border);background:var(--bg);width:100%;min-height:84px;color:var(--fg);font:inherit;resize:vertical;border-radius:4px;padding:6px 8px;display:block}.msg-actions{align-items:center;gap:6px;margin-top:6px;display:flex}.msg-hint{color:var(--fg-faint);flex:1;font-size:12px}.msg-actions .primary{background:var(--head-bg);color:var(--head-fg);border-color:var(--head-fg)}.msg-actions .primary:disabled{opacity:.45}.head-actions,.row-action{gap:2px;display:flex}.head-actions button,.row-action{color:var(--fg-dim);background:0 0;border-color:#0000;padding:3px;line-height:0}.head-actions button:hover,.row-action:hover{border-color:var(--border);background:var(--bg-hover);color:var(--fg)}.head-actions .danger:hover,.row-action.danger:hover{color:#e06c75;border-color:#e06c75}.head-actions button:disabled,.row-action:disabled{opacity:.4}.section-head{z-index:1;background:var(--bg-panel);color:var(--fg);text-transform:uppercase;letter-spacing:.05em;cursor:pointer;-webkit-user-select:none;user-select:none;align-items:center;gap:5px;padding:8px 6px 5px;font-size:13px;font-weight:600;display:flex;position:sticky;top:0}.section-head:hover{background:var(--bg-hover)}.section-head .caret{color:var(--fg-dim);line-height:0;transition:transform .1s;display:flex}.section-head .caret.open{transform:rotate(90deg)}.section-head .count{background:var(--bg-selected);min-width:19px;height:19px;color:var(--fg);letter-spacing:0;border-radius:10px;justify-content:center;align-items:center;padding:0 5px;font-size:11px;display:inline-flex}.section-head .count.zero{background:var(--bg-hover);color:var(--fg-faint)}.section-head .head-actions{visibility:hidden;margin-left:auto}.section-head:hover .head-actions,.section-head .head-actions:focus-within{visibility:visible}.composer{border-bottom:1px solid var(--border);flex-shrink:0;padding:8px}.composer .msg-box{min-height:56px}.row-action{visibility:hidden}.row-action:first-of-type{margin-left:auto}.file-row:hover .row-action,.file-row.selected .row-action,.row-action:focus-visible{visibility:visible}.diff-side{border:1px solid var(--border);color:var(--fg-dim);border-radius:8px;padding:0 6px;font-size:12px}.commit-people{border-bottom:1px solid var(--border);flex-direction:column;flex-shrink:0;gap:6px;padding:10px 12px;display:flex;position:relative}.person{align-items:center;gap:8px;min-width:0;display:flex}.person-text{flex-direction:column;min-width:0;display:flex}.person-name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.person-when{color:var(--fg-dim);font-size:12px}.face{object-fit:cover;border-radius:50%;flex-shrink:0;width:26px;height:26px}.face-fallback{background:var(--bg-hover);color:var(--fg-dim);justify-content:center;align-items:center;font-size:11px;display:inline-flex}.commit-hash{color:var(--fg-faint);font-family:Ubuntu Mono,ui-monospace,monospace;font-size:12px;position:absolute;top:10px;right:12px}.files-head{border-bottom:1px solid var(--border);flex-shrink:0;justify-content:space-between;align-items:center;gap:8px;padding:6px 12px;display:flex}.files-counts{color:var(--fg-dim);white-space:nowrap;gap:10px;font-size:12px;display:flex;overflow:hidden}.files-head .view-toggle button{padding:2px 10px;font-size:12px}.filelist{flex:1;padding:4px;overflow-y:auto}.file-dir{color:var(--fg-faint);text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;overflow:hidden}.dir-row{color:var(--fg-dim)}.dir-arrow{width:14px;color:var(--fg-faint);font-size:10px;line-height:inherit;flex-shrink:0}.file-row{cursor:pointer;border-radius:4px;gap:8px;padding:3px 8px;display:flex}.file-row:hover{background:var(--bg-hover)}.file-row.selected{background:var(--bg-selected)}.file-status{flex-shrink:0;width:14px;font-family:Ubuntu Mono,ui-monospace,monospace}.file-path{white-space:nowrap;min-width:0;display:flex;overflow:hidden}.file-name{flex:none}.diffview{flex-direction:column;flex:1;min-height:0;display:flex;overflow:auto}.diffview.empty{text-align:center;justify-content:center;align-items:center}.diff-state{text-align:center;flex:1;justify-content:center;align-items:center;display:flex}.diff-toolbar{border-bottom:1px solid var(--border);flex-shrink:0;justify-content:center;gap:8px;padding:6px 10px;display:flex}.diff-toolbar button{padding:3px 10px}.view-toggle{display:inline-flex}.view-toggle button{border-radius:0}.view-toggle button:first-child{border-radius:4px 0 0 4px}.view-toggle button:last-child{border-left:none;border-radius:0 4px 4px 0}.view-toggle button.active{background:var(--bg-selected)}.diff-html{flex:1;overflow:auto}.diff-html .d2h-file-wrapper{position:relative}.diff-html .d2h-file-diff{overflow-x:auto}.diff-html .d2h-diff-table{font-family:Ubuntu Mono,ui-monospace,monospace}.diff-html .d2h-code-linenumber,.diff-html .d2h-code-side-linenumber{cursor:default}.diff-html td.d2h-info .diff-expand-group{direction:ltr;gap:1px;padding-left:2px;display:flex}.diff-html .diff-expand{width:18px;height:18px;color:var(--fg-dim);cursor:pointer;background:0 0;border:0;border-radius:3px;justify-content:center;align-items:center;padding:0;display:flex}.diff-html .diff-expand:hover{background:var(--bg-hover);color:var(--fg)}.diff-plain{color:var(--fg-dim);padding:10px}.image-diff{flex:1;grid-template-columns:1fr 1fr;align-content:start;gap:12px;padding:12px;display:grid;overflow:auto}.image-pane{flex-direction:column;gap:6px;min-width:0;display:flex}.image-pane-label{color:var(--fg-dim);font-size:12px}.image-pane img{border:1px solid var(--border);background:repeating-conic-gradient(var(--bg-panel) 0% 25%, var(--bg) 0% 50%) 0 0 / 16px 16px;border-radius:4px;max-width:100%}.image-pane-none{text-align:center;color:var(--fg-dim);border:1px dashed var(--border);border-radius:4px;padding:20px}.d2h-wrapper{--d2h-bg-color:var(--bg-panel);--d2h-ins-bg-color:#e6ffec;--d2h-ins-highlight-bg-color:#abf2bc;--d2h-ins-border-color:#ccffd8;--d2h-ins-label-color:#1a7f37;--d2h-del-bg-color:#ffebe9;--d2h-del-highlight-bg-color:#ffcecb;--d2h-del-border-color:#ffd7d5;--d2h-del-label-color:#cf222e;--d2h-change-ins-color:#e6ffec;--d2h-change-del-color:#ffebe9;--d2h-info-bg-color:#ddf4ff;--d2h-info-border-color:#ddf4ff;--d2h-line-border-color:#d0d7de;--d2h-dark-bg-color:#23272e;--d2h-dark-border-color:#30363d;--d2h-dark-line-border-color:#30363d;--d2h-dark-ins-bg-color:#2ea04326;--d2h-dark-ins-highlight-bg-color:#2ea04366;--d2h-dark-ins-border-color:#3fb9504d;--d2h-dark-ins-label-color:#3fb950;--d2h-dark-del-bg-color:#f851491a;--d2h-dark-del-highlight-bg-color:#f8514966;--d2h-dark-del-border-color:#f851494d;--d2h-dark-del-label-color:#f85149;--d2h-dark-change-ins-color:#2ea04326;--d2h-dark-change-del-color:#f851491a;--d2h-dark-info-bg-color:#388bfd1a;--d2h-dark-info-border-color:#388bfd4d}.d2h-wrapper .hljs{background:0 0}.conflict-banner{background:var(--bg-wip);border-bottom:1px solid var(--border);flex-shrink:0;align-items:center;gap:10px;padding:6px 10px;font-size:12px;display:flex}.conflict-banner .cb-verb{font-weight:600}.conflict-banner .cb-label{color:var(--fg-dim);text-overflow:ellipsis;white-space:nowrap;max-width:40ch;overflow:hidden}.conflict-banner .cb-count{color:var(--fg-dim);margin-left:auto}.conflict-banner button{padding:3px 10px;font-size:12px}.conflict-banner button.danger{color:#e06c75;border-color:#e06c75}.conflict-banner button.primary:not(:disabled){background:var(--head-bg);color:var(--head-fg);border-color:var(--head-bg-hover)}.conflict-banner button:disabled{opacity:.5;cursor:default}.cf-view{flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.5;overflow:auto}.cf-progress{z-index:1;background:var(--bg-panel);border-bottom:1px solid var(--border);color:var(--fg-dim);padding:4px 10px;position:sticky;top:0}.cf-view pre{white-space:pre-wrap;word-break:break-word;margin:0;padding:0 10px}.cf-context{color:var(--fg-dim)}.cf-block{border:1px solid var(--border);border-radius:4px;margin:6px 0;overflow:hidden}.cf-block.picked{border-color:var(--head-bg-hover)}.cf-bar{background:var(--bg-hover);border-bottom:1px solid var(--border);align-items:center;gap:6px;padding:3px 8px;display:flex}.cf-side-name{color:var(--fg-dim);margin-right:auto}.cf-chosen{color:var(--head-fg);margin-right:auto}.cf-bar button{padding:2px 8px;font-size:11px}.cf-ours{background:#98c3791f}.cf-theirs{background:#61afef1f}.cf-mid{color:var(--fg-dim);border-top:1px dashed var(--border);border-bottom:1px dashed var(--border);padding:2px 8px}.cf-card{border:1px solid var(--border);text-align:center;border-radius:6px;max-width:420px;margin:20px auto;padding:16px;font-family:inherit}.cf-card-note{color:var(--fg-dim);margin-bottom:12px}.cf-card-actions{justify-content:center;gap:8px;display:flex}.cf-card-actions button.danger{color:#e06c75;border-color:#e06c75}.diff-side.conflict{color:#e06c75}
|
package/dist/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" />
|
|
10
10
|
<title>megit</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-Bfrt9LxG.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C845Od_q.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/dist-server/index.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { existsSync, readdirSync } from 'node:fs';
|
|
4
|
-
import { readFile, realpath } from 'node:fs/promises';
|
|
4
|
+
import { readFile, realpath, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
7
7
|
import { loadConfig, saveConfig, isPermutation, touchRecent } from './config.js';
|
|
8
8
|
import { resolveAvatar, parseGithubRemote } from './avatars.js';
|
|
9
9
|
import { mergeMatches, parseBranchHeader, parseLog, parseMatches, parseMeta, parseNameStatus, parseStatus, stashIndex, LOG_FORMAT, META_FORMAT } from './parse.js';
|
|
10
|
+
import { pickOperation, STATE_FILES } from './operation.js';
|
|
10
11
|
import { subscribe } from './watch.js';
|
|
11
12
|
import { wireTerminal, hasPty } from './term.js';
|
|
12
13
|
const app = express();
|
|
13
|
-
|
|
14
|
+
// 10mb, not the 100kb default: a resolved conflicted file goes back as a JSON
|
|
15
|
+
// string, and a few thousand lines of source blows past the default. The GET
|
|
16
|
+
// side refuses anything over DIFF_CAP, so the client can't assemble a body
|
|
17
|
+
// larger than 1 MB.
|
|
18
|
+
app.use(express.json({ limit: '10mb' }));
|
|
14
19
|
// The server listens on loopback only, but that alone doesn't stop a page on
|
|
15
20
|
// attacker.tld from rebinding its DNS to 127.0.0.1: the browser then treats this
|
|
16
21
|
// API as same-origin and CORS never applies. Pinning Host closes that — a rebound
|
|
@@ -219,12 +224,44 @@ app.get('/api/avatar', repoGuard, async (req, res) => {
|
|
|
219
224
|
}
|
|
220
225
|
res.json({ url: await resolveAvatar(String(req.query.repo), email, git) });
|
|
221
226
|
});
|
|
227
|
+
// `git rev-parse` once per repo: .git is a file, not a directory, in linked
|
|
228
|
+
// worktrees and submodules, so the path can't be assumed. Repos are registered
|
|
229
|
+
// and long-lived, so caching keeps steady-state detection at zero git processes.
|
|
230
|
+
const gitDirs = new Map();
|
|
231
|
+
async function gitDir(repo) {
|
|
232
|
+
let dir = gitDirs.get(repo);
|
|
233
|
+
if (!dir) {
|
|
234
|
+
dir = (await git(repo, ['rev-parse', '--absolute-git-dir'])).trim();
|
|
235
|
+
gitDirs.set(repo, dir);
|
|
236
|
+
}
|
|
237
|
+
return dir;
|
|
238
|
+
}
|
|
239
|
+
// A short label for the banner, from files git already wrote — no extra process.
|
|
240
|
+
async function opLabel(dir, kind) {
|
|
241
|
+
const read = async (name) => (await readFile(join(dir, name), 'utf8').catch(() => '')).trim();
|
|
242
|
+
if (kind === 'merge')
|
|
243
|
+
return (await read('MERGE_MSG')).split('\n')[0];
|
|
244
|
+
if (kind === 'cherry-pick')
|
|
245
|
+
return (await read('CHERRY_PICK_HEAD')).slice(0, 7);
|
|
246
|
+
if (kind === 'revert')
|
|
247
|
+
return (await read('REVERT_HEAD')).slice(0, 7);
|
|
248
|
+
return ''; // rebase: the banner says "Rebasing" and stops there
|
|
249
|
+
}
|
|
250
|
+
async function readOperation(repo) {
|
|
251
|
+
const dir = await gitDir(repo);
|
|
252
|
+
const kind = pickOperation(STATE_FILES.filter(n => existsSync(join(dir, n))));
|
|
253
|
+
return kind ? { kind, label: await opLabel(dir, kind) } : null;
|
|
254
|
+
}
|
|
222
255
|
app.get('/api/status', repoGuard, async (req, res) => {
|
|
223
256
|
try {
|
|
224
257
|
// --branch prepends the branch/upstream/ahead-behind headers to the output this
|
|
225
258
|
// already parses — the toolbar's Pull/Push badges for no extra git process
|
|
226
259
|
const raw = await git(String(req.query.repo), ['status', '--porcelain=v2', '-uall', '--branch', '-z']);
|
|
227
|
-
res.json({
|
|
260
|
+
res.json({
|
|
261
|
+
files: parseStatus(raw),
|
|
262
|
+
branch: parseBranchHeader(raw),
|
|
263
|
+
operation: await readOperation(String(req.query.repo)),
|
|
264
|
+
});
|
|
228
265
|
}
|
|
229
266
|
catch (e) {
|
|
230
267
|
res.status(500).json({ error: e.message });
|
|
@@ -567,7 +604,10 @@ app.post('/api/wip', repoGuard, async (req, res) => {
|
|
|
567
604
|
await git(repo, ['restore', '--staged', '--', (await mustBeDirty()).path]);
|
|
568
605
|
break;
|
|
569
606
|
case 'stage-all':
|
|
570
|
-
|
|
607
|
+
// conflicts excluded: a bare `git add -A` stages files that still have
|
|
608
|
+
// markers in them, which is the mistake the Merge Changes section exists
|
|
609
|
+
// to prevent — the batch button must not walk around it
|
|
610
|
+
await git(repo, ['add', '-A', '--', '.', ...await excludeUnmerged(repo)]);
|
|
571
611
|
break;
|
|
572
612
|
case 'unstage-all':
|
|
573
613
|
await git(repo, ['restore', '--staged', '--', '.']);
|
|
@@ -586,7 +626,9 @@ app.post('/api/wip', repoGuard, async (req, res) => {
|
|
|
586
626
|
// unstaged only: worktree back to the index, so staged content survives.
|
|
587
627
|
// clean removes untracked files — a staged new file is tracked, so it stays.
|
|
588
628
|
// No -x — ignored files (node_modules, .env) are not "changes".
|
|
589
|
-
|
|
629
|
+
// Conflicts are excluded too: they have no stage-0 entry to restore from,
|
|
630
|
+
// so including one makes git fail the whole call.
|
|
631
|
+
await git(repo, ['restore', '--worktree', '--', '.', ...await excludeUnmerged(repo)]);
|
|
590
632
|
await git(repo, ['clean', '-fd']);
|
|
591
633
|
break;
|
|
592
634
|
case 'stash': {
|
|
@@ -723,6 +765,90 @@ app.get('/api/diff', repoGuard, async (req, res) => {
|
|
|
723
765
|
res.status(500).json({ error: e.message });
|
|
724
766
|
}
|
|
725
767
|
});
|
|
768
|
+
// git's own list of unmerged paths. Every conflict action checks against it
|
|
769
|
+
// BEFORE touching the filesystem: `file` arrives from the client, and without
|
|
770
|
+
// this check POST /api/conflict is an arbitrary-file-write primitive bounded
|
|
771
|
+
// only by repoGuard. Paths from git can't traverse out of the repo; paths from
|
|
772
|
+
// the client can. Do not reorder this behind a join() or a read.
|
|
773
|
+
async function unmergedPaths(repo) {
|
|
774
|
+
const out = await git(repo, ['diff', '--name-only', '--diff-filter=U', '-z']);
|
|
775
|
+
return out.split('\0').filter(Boolean);
|
|
776
|
+
}
|
|
777
|
+
// Pathspecs that subtract the conflicted files from a whole-tree operation, for
|
|
778
|
+
// the batch buttons that would otherwise stage or clobber a half-merged file.
|
|
779
|
+
const excludeUnmerged = async (repo) => (await unmergedPaths(repo)).map(p => `:(exclude)${p}`);
|
|
780
|
+
app.get('/api/conflict', repoGuard, async (req, res) => {
|
|
781
|
+
const repo = String(req.query.repo);
|
|
782
|
+
const file = String(req.query.file ?? '');
|
|
783
|
+
try {
|
|
784
|
+
if (!(await unmergedPaths(repo)).includes(file))
|
|
785
|
+
throw httpError(409, 'file is not conflicted');
|
|
786
|
+
// delete/modify where our side deleted it: nothing on disk, and nothing to
|
|
787
|
+
// pick — the client shows the whole-file card instead of a block list
|
|
788
|
+
const buf = await readFile(join(repo, file)).catch(() => null);
|
|
789
|
+
if (!buf)
|
|
790
|
+
res.json({ missing: true });
|
|
791
|
+
else if (buf.length > DIFF_CAP)
|
|
792
|
+
res.json({ tooLarge: true, size: buf.length });
|
|
793
|
+
else if (buf.includes(0))
|
|
794
|
+
res.json({ binary: true });
|
|
795
|
+
else
|
|
796
|
+
res.json({ content: buf.toString('utf8') });
|
|
797
|
+
}
|
|
798
|
+
catch (e) {
|
|
799
|
+
const err = e;
|
|
800
|
+
res.status(err.status ?? 500).json({ error: err.message });
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
app.post('/api/conflict', repoGuard, async (req, res) => {
|
|
804
|
+
const repo = String(req.query.repo);
|
|
805
|
+
const action = String(req.body.action ?? '');
|
|
806
|
+
try {
|
|
807
|
+
if (action === 'abort' || action === 'continue') {
|
|
808
|
+
const op = await readOperation(repo);
|
|
809
|
+
if (!op)
|
|
810
|
+
throw httpError(409, 'no operation in progress');
|
|
811
|
+
// GIT_EDITOR=true: GIT_ENV deliberately sets no editor, and `--continue`
|
|
812
|
+
// opens one for the commit message — without this the request never
|
|
813
|
+
// returns. `--no-edit` won't do instead: `git rebase --continue` rejects it.
|
|
814
|
+
await git(repo, [op.kind, `--${action}`], [0], NET_TIMEOUT, { GIT_EDITOR: 'true' });
|
|
815
|
+
res.json({ ok: true });
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const file = String(req.body.file ?? '');
|
|
819
|
+
// the security boundary — see unmergedPaths. Runs before any path is joined.
|
|
820
|
+
if (!(await unmergedPaths(repo)).includes(file))
|
|
821
|
+
throw httpError(409, 'file is not conflicted');
|
|
822
|
+
switch (action) {
|
|
823
|
+
case 'resolve': {
|
|
824
|
+
const content = req.body.content;
|
|
825
|
+
if (typeof content !== 'string')
|
|
826
|
+
throw httpError(400, 'content must be a string');
|
|
827
|
+
await writeFile(join(repo, file), content);
|
|
828
|
+
await git(repo, ['add', '--', file]);
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
case 'ours':
|
|
832
|
+
case 'theirs':
|
|
833
|
+
// fails on a delete/modify conflict where that side has no version —
|
|
834
|
+
// the error reaches the toast, and Delete is the answer there
|
|
835
|
+
await git(repo, ['checkout', `--${action}`, '--', file]);
|
|
836
|
+
await git(repo, ['add', '--', file]);
|
|
837
|
+
break;
|
|
838
|
+
case 'delete':
|
|
839
|
+
// -f: git rm refuses an unmerged path without it
|
|
840
|
+
await git(repo, ['rm', '-f', '--', file]);
|
|
841
|
+
break;
|
|
842
|
+
default:
|
|
843
|
+
throw httpError(400, `unknown action: ${action}`);
|
|
844
|
+
}
|
|
845
|
+
res.json({ ok: true });
|
|
846
|
+
}
|
|
847
|
+
catch (e) {
|
|
848
|
+
const err = e;
|
|
849
|
+
res.status(err.status ?? 409).json({ error: err.message });
|
|
850
|
+
}
|
|
851
|
+
});
|
|
726
852
|
// raw bytes of one blob, for rendering image diffs as two <img>. `which` picks
|
|
727
853
|
// the before/after side; a missing blob (added or deleted file) is a 404 the
|
|
728
854
|
// client renders as an empty pane.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Order is the whole point: a rebase applying a commit also writes
|
|
2
|
+
// CHERRY_PICK_HEAD, and the user is rebasing, not cherry-picking.
|
|
3
|
+
const ORDER = [
|
|
4
|
+
['rebase', ['rebase-merge', 'rebase-apply']],
|
|
5
|
+
['merge', ['MERGE_HEAD']],
|
|
6
|
+
['cherry-pick', ['CHERRY_PICK_HEAD']],
|
|
7
|
+
['revert', ['REVERT_HEAD']],
|
|
8
|
+
];
|
|
9
|
+
export const STATE_FILES = ORDER.flatMap(([, names]) => names);
|
|
10
|
+
export function pickOperation(present) {
|
|
11
|
+
const set = new Set(present);
|
|
12
|
+
for (const [kind, names] of ORDER)
|
|
13
|
+
if (names.some(n => set.has(n)))
|
|
14
|
+
return kind;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "megit-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Git repository viewer in the browser: commit graph with branch lanes, diffs, stashes and WIP",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hoang Vuong Vu",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"packageManager": "pnpm@11.18.0+sha512.33d83c77da82f49fba836925c6f1b841181ec3132b670639bd012f7075f5c7cf634c5f870147c19aae7478fac01df09d8892e880454896edd23ee9b33757563c",
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"express": "^5.2.1",
|
|
46
|
-
"ws": "^8.21.
|
|
46
|
+
"ws": "^8.21.2"
|
|
47
47
|
},
|
|
48
48
|
"optionalDependencies": {
|
|
49
49
|
"node-pty": "^1.1.0"
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/express": "^5.0.6",
|
|
53
53
|
"@types/node": "^26.1.2",
|
|
54
|
-
"@types/react": "^19.2.
|
|
55
|
-
"@types/react-dom": "^19.2.
|
|
54
|
+
"@types/react": "^19.2.18",
|
|
55
|
+
"@types/react-dom": "^19.2.4",
|
|
56
56
|
"@types/ws": "^8.18.1",
|
|
57
|
-
"@vitejs/plugin-react": "^6.0.
|
|
57
|
+
"@vitejs/plugin-react": "^6.0.5",
|
|
58
58
|
"@xterm/addon-fit": "^0.11.0",
|
|
59
59
|
"@xterm/xterm": "^6.0.0",
|
|
60
60
|
"concurrently": "^10.0.4",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"react": "^19.2.8",
|
|
64
64
|
"react-dom": "^19.2.8",
|
|
65
65
|
"typescript": "^7.0.2",
|
|
66
|
-
"vite": "^8.
|
|
66
|
+
"vite": "^8.2.0",
|
|
67
67
|
"vitest": "^4.1.10"
|
|
68
68
|
}
|
|
69
69
|
}
|