megit-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/bin/megit.js +54 -0
- package/dist/assets/DiffView-CJb6qgum.js +143 -0
- package/dist/assets/DiffView-DhpiFlgV.css +1 -0
- package/dist/assets/TerminalPanel-BrP-ENHg.css +1 -0
- package/dist/assets/TerminalPanel-Cwtdgirw.js +37 -0
- package/dist/assets/index-CfQ1U4Ld.js +249 -0
- package/dist/assets/index-DZyh3SKi.css +1 -0
- package/dist/index.html +17 -0
- package/dist/logo.svg +7 -0
- package/dist-server/avatars.js +83 -0
- package/dist-server/config.js +24 -0
- package/dist-server/index.js +805 -0
- package/dist-server/parse.js +144 -0
- package/dist-server/term.js +113 -0
- package/dist-server/watch.js +104 -0
- package/package.json +69 -0
- package/scripts/fix-pty-perms.mjs +21 -0
|
@@ -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{background:0 0;border:none;padding:0 2px}.tab-add{background:0 0;border:none;padding:6px 12px;font-size:15px}.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;flex:1}.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-close{color:var(--fg-dim);background:0 0;border:none;padding:0 4px;font-size:13px}.term-close:hover{color:var(--fg);background:0 0}.term-host{flex:1;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{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;top:8px;left:50%;transform:translate(-50%);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,.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-toolbar{border-bottom:1px solid var(--border);flex-shrink:0;justify-content:center;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}
|
package/dist/index.html
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" />
|
|
10
|
+
<title>megit</title>
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-CfQ1U4Ld.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DZyh3SKi.css">
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div id="root"></div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
package/dist/logo.svg
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<path d="M12 50 V28 C12 15 32 15 32 28" stroke="#61afef" stroke-width="6" fill="none" stroke-linecap="round"/>
|
|
3
|
+
<path d="M32 28 V50 M32 28 C32 15 52 15 52 28 V50" stroke="#c678dd" stroke-width="6" fill="none" stroke-linecap="round"/>
|
|
4
|
+
<circle cx="12" cy="50" r="6" fill="#61afef"/>
|
|
5
|
+
<circle cx="32" cy="50" r="6" fill="#98c379"/>
|
|
6
|
+
<circle cx="52" cy="50" r="6" fill="#c678dd"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
// GitHub noreply emails encode the account: "12345+user@users.noreply.github.com"
|
|
6
|
+
// (current form) or legacy "user@users.noreply.github.com".
|
|
7
|
+
export function noreplyAvatar(email) {
|
|
8
|
+
const m = email.match(/^(?:(\d+)\+)?([^@+]+)@users\.noreply\.github\.com$/);
|
|
9
|
+
if (!m)
|
|
10
|
+
return null;
|
|
11
|
+
return m[1]
|
|
12
|
+
? `https://avatars.githubusercontent.com/u/${m[1]}?s=48`
|
|
13
|
+
: `https://github.com/${m[2]}.png?size=48`;
|
|
14
|
+
}
|
|
15
|
+
// git@github.com:owner/repo.git | https://github.com/owner/repo(.git)
|
|
16
|
+
export function parseGithubRemote(url) {
|
|
17
|
+
const m = url.trim().match(/github\.com[:/]([^/]+)\/([^/\s]+?)(?:\.git)?$/);
|
|
18
|
+
return m ? { owner: m[1], repo: m[2] } : null;
|
|
19
|
+
}
|
|
20
|
+
const cacheFile = join(homedir(), '.config', 'megit', 'avatars.json');
|
|
21
|
+
let cache = null;
|
|
22
|
+
function loadCache() {
|
|
23
|
+
if (!cache) {
|
|
24
|
+
try {
|
|
25
|
+
cache = JSON.parse(readFileSync(cacheFile, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
cache = {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return cache;
|
|
32
|
+
}
|
|
33
|
+
function saveCache() {
|
|
34
|
+
mkdirSync(join(homedir(), '.config', 'megit'), { recursive: true });
|
|
35
|
+
writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
|
|
36
|
+
}
|
|
37
|
+
// gh CLI token when the user is logged in — lifts the API limit from 60/h to 5000/h
|
|
38
|
+
let tokenP = null;
|
|
39
|
+
const ghToken = () => (tokenP ??= new Promise(r => execFile('gh', ['auth', 'token'], (e, out) => r(e ? null : out.trim() || null))));
|
|
40
|
+
// email -> GitHub profile avatar. Strategy: noreply emails resolve locally; anything
|
|
41
|
+
// else looks up one commit by that author and asks the GitHub commits API which
|
|
42
|
+
// account it belongs to. Definitive answers (incl. "no account") persist to disk;
|
|
43
|
+
// rate limits / network errors stay uncached so a later run retries.
|
|
44
|
+
export async function resolveAvatar(repo, email, git) {
|
|
45
|
+
const direct = noreplyAvatar(email);
|
|
46
|
+
if (direct)
|
|
47
|
+
return direct;
|
|
48
|
+
const c = loadCache();
|
|
49
|
+
if (email in c)
|
|
50
|
+
return c[email];
|
|
51
|
+
let url = null;
|
|
52
|
+
let definitive = true;
|
|
53
|
+
try {
|
|
54
|
+
const remote = parseGithubRemote(await git(repo, ['remote', 'get-url', 'origin']));
|
|
55
|
+
if (remote) {
|
|
56
|
+
const rx = email.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
57
|
+
const sha = (await git(repo, ['log', '--all', '-1', `--author=${rx}`, '--format=%H'])).trim();
|
|
58
|
+
if (sha) {
|
|
59
|
+
const headers = { 'User-Agent': 'megit', Accept: 'application/vnd.github+json' };
|
|
60
|
+
const token = await ghToken();
|
|
61
|
+
if (token)
|
|
62
|
+
headers.Authorization = `Bearer ${token}`;
|
|
63
|
+
const resp = await fetch(`https://api.github.com/repos/${remote.owner}/${remote.repo}/commits/${sha}`, { headers });
|
|
64
|
+
if (resp.ok) {
|
|
65
|
+
const data = (await resp.json());
|
|
66
|
+
const raw = data.author?.avatar_url;
|
|
67
|
+
url = raw ? raw + (raw.includes('?') ? '&' : '?') + 's=48' : null;
|
|
68
|
+
}
|
|
69
|
+
else if (resp.status !== 404 && resp.status !== 422) {
|
|
70
|
+
definitive = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
definitive = false;
|
|
77
|
+
}
|
|
78
|
+
if (definitive) {
|
|
79
|
+
c[email] = url;
|
|
80
|
+
saveCache();
|
|
81
|
+
}
|
|
82
|
+
return url;
|
|
83
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const dir = join(homedir(), '.config', 'megit');
|
|
5
|
+
const file = join(dir, 'config.json');
|
|
6
|
+
export function loadConfig() {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return { repos: [], activeRepo: null };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function saveConfig(c) {
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
16
|
+
writeFileSync(file, JSON.stringify(c, null, 2));
|
|
17
|
+
}
|
|
18
|
+
export function isPermutation(a, b) {
|
|
19
|
+
if (a.length !== b.length)
|
|
20
|
+
return false;
|
|
21
|
+
const as = [...a].sort();
|
|
22
|
+
const bs = [...b].sort();
|
|
23
|
+
return as.every((v, i) => v === bs[i]);
|
|
24
|
+
}
|