megit-app 0.10.0 → 0.11.1
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 +23 -1
- package/SECURITY.md +60 -0
- package/dist/assets/{ConflictView-Cda-OXAc.js → ConflictView-DQ18uChu.js} +1 -1
- package/dist/assets/{DiffView-yzJtjZx7.js → DiffView-BEICOows.js} +1 -1
- package/dist/assets/{TerminalPanel-Nvk8NI9h.js → TerminalPanel-CX-U7wb1.js} +1 -1
- package/dist/assets/index-BXzSxW-2.css +1 -0
- package/dist/assets/index-_ttj49Ya.js +251 -0
- package/dist/index.html +2 -2
- package/dist-server/avatars.js +18 -10
- package/dist-server/index.js +61 -1
- package/package.json +5 -4
- package/dist/assets/index-C4TwOraO.css +0 -1
- package/dist/assets/index-l_8r494M.js +0 -251
package/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
7
7
|
<title>megit</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-_ttj49Ya.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BXzSxW-2.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/dist-server/avatars.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
1
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
2
|
import { homedir } from 'node:os';
|
|
4
3
|
import { join } from 'node:path';
|
|
4
|
+
// This file holds every outbound network call the server makes, and every
|
|
5
|
+
// external host it knows about:
|
|
6
|
+
//
|
|
7
|
+
// api.github.com — commit -> GitHub account lookup (resolveAvatar)
|
|
8
|
+
// avatars.githubusercontent.com — the photo itself, fetched by the browser
|
|
9
|
+
// github.com/<user>.png — same, for legacy noreply emails
|
|
10
|
+
//
|
|
11
|
+
// All three are literals, never assembled from remote input, and nothing here
|
|
12
|
+
// runs unless author avatars are on in Settings. Requests are anonymous: no
|
|
13
|
+
// token is read, sent or stored, so GitHub's 60 req/h unauthenticated limit
|
|
14
|
+
// applies — the on-disk cache below makes that one request per author, ever.
|
|
15
|
+
// See SECURITY.md, "Threat model".
|
|
5
16
|
// GitHub noreply emails encode the account: "12345+user@users.noreply.github.com"
|
|
6
17
|
// (current form) or legacy "user@users.noreply.github.com".
|
|
7
18
|
export function noreplyAvatar(email) {
|
|
@@ -12,7 +23,8 @@ export function noreplyAvatar(email) {
|
|
|
12
23
|
? `https://avatars.githubusercontent.com/u/${m[1]}?s=48`
|
|
13
24
|
: `https://github.com/${m[2]}.png?size=48`;
|
|
14
25
|
}
|
|
15
|
-
//
|
|
26
|
+
// owner/repo out of either remote spelling — ssh (git@host:owner/repo.git) or
|
|
27
|
+
// https (host/owner/repo, with or without the .git suffix)
|
|
16
28
|
export function parseGithubRemote(url) {
|
|
17
29
|
const m = url.trim().match(/github\.com[:/]([^/]+)\/([^/\s]+?)(?:\.git)?$/);
|
|
18
30
|
return m ? { owner: m[1], repo: m[2] } : null;
|
|
@@ -34,9 +46,6 @@ function saveCache() {
|
|
|
34
46
|
mkdirSync(join(homedir(), '.config', 'megit'), { recursive: true });
|
|
35
47
|
writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
|
|
36
48
|
}
|
|
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
49
|
// email -> GitHub profile avatar. Strategy: noreply emails resolve locally; anything
|
|
41
50
|
// else looks up one commit by that author and asks the GitHub commits API which
|
|
42
51
|
// account it belongs to. Definitive answers (incl. "no account") persist to disk;
|
|
@@ -56,17 +65,16 @@ export async function resolveAvatar(repo, email, git) {
|
|
|
56
65
|
const rx = email.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
57
66
|
const sha = (await git(repo, ['log', '--all', '-1', `--author=${rx}`, '--format=%H'])).trim();
|
|
58
67
|
if (sha) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
headers.Authorization = `Bearer ${token}`;
|
|
63
|
-
const resp = await fetch(`https://api.github.com/repos/${remote.owner}/${remote.repo}/commits/${sha}`, { headers });
|
|
68
|
+
const resp = await fetch(`https://api.github.com/repos/${remote.owner}/${remote.repo}/commits/${sha}`, {
|
|
69
|
+
headers: { 'User-Agent': 'megit', Accept: 'application/vnd.github+json' },
|
|
70
|
+
});
|
|
64
71
|
if (resp.ok) {
|
|
65
72
|
const data = (await resp.json());
|
|
66
73
|
const raw = data.author?.avatar_url;
|
|
67
74
|
url = raw ? raw + (raw.includes('?') ? '&' : '?') + 's=48' : null;
|
|
68
75
|
}
|
|
69
76
|
else if (resp.status !== 404 && resp.status !== 422) {
|
|
77
|
+
// 403/429 = rate limited: leave it uncached so a later run retries
|
|
70
78
|
definitive = false;
|
|
71
79
|
}
|
|
72
80
|
}
|
package/dist-server/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createApp, serveStatic } from './http.js';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
4
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';
|
|
@@ -941,6 +941,66 @@ app.get('/api/blob', repoGuard, async (req, res) => {
|
|
|
941
941
|
res.status(404).end();
|
|
942
942
|
}
|
|
943
943
|
});
|
|
944
|
+
// The repo's own favicon, used as its icon in the picker. Checked against the
|
|
945
|
+
// framework conventions people actually ship; no hit is a 404 and the client
|
|
946
|
+
// falls back to a letter chip. Not repoGuard'd: a recent repo need not be open,
|
|
947
|
+
// but it must still be one the user has added at some point.
|
|
948
|
+
// Most projects don't ship a file literally called favicon.*: they declare one
|
|
949
|
+
// in their entry html (megit's own is /logo.svg). Reading that link first is
|
|
950
|
+
// what makes the icon right rather than merely present.
|
|
951
|
+
function declaredIcon(repo) {
|
|
952
|
+
for (const html of ['index.html', 'public/index.html', 'app/index.html', 'src/index.html']) {
|
|
953
|
+
const file = join(repo, html);
|
|
954
|
+
if (!existsSync(file))
|
|
955
|
+
continue;
|
|
956
|
+
const href = /<link[^>]+rel=["'][^"']*icon[^"']*["'][^>]*>/i.exec(readFileSync(file, 'utf8'))?.[0]
|
|
957
|
+
?.match(/href=["']([^"']+)["']/i)?.[1];
|
|
958
|
+
// data: and http: hrefs have nothing on disk to serve
|
|
959
|
+
if (!href || /^[a-z]+:/i.test(href))
|
|
960
|
+
continue;
|
|
961
|
+
const rel = href.split(/[?#]/)[0].replace(/^\/+/, '');
|
|
962
|
+
return [rel, join('public', rel), join('static', rel), join(dirname(html), rel)];
|
|
963
|
+
}
|
|
964
|
+
return [];
|
|
965
|
+
}
|
|
966
|
+
const FAVICONS = [
|
|
967
|
+
'public/favicon.svg', 'public/favicon.ico', 'public/favicon.png', 'public/favicon-32x32.png',
|
|
968
|
+
'app/favicon.ico', 'src/favicon.svg', 'static/favicon.svg', 'static/favicon.ico', 'static/favicon.png',
|
|
969
|
+
'assets/favicon.ico', 'favicon.svg', 'favicon.ico', 'favicon.png',
|
|
970
|
+
];
|
|
971
|
+
app.get('/api/favicon', async (req, res) => {
|
|
972
|
+
const repo = String(req.query.repo ?? '');
|
|
973
|
+
const cfg = loadConfig();
|
|
974
|
+
if (!cfg.repos.includes(repo) && !cfg.recent.includes(repo)) {
|
|
975
|
+
res.status(400).json({ error: 'unknown repo' });
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const hit = [...declaredIcon(repo), ...FAVICONS].map(f => join(repo, f)).find(f => existsSync(f) && MIME[f.split('.').pop().toLowerCase()]);
|
|
979
|
+
if (!hit) {
|
|
980
|
+
res.status(404).end();
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
try {
|
|
984
|
+
// same symlink escape as /api/blob: a clone can carry public/favicon.ico ->
|
|
985
|
+
// /etc/passwd, and serving it would leak the bytes under an image type
|
|
986
|
+
const abs = await realpath(hit);
|
|
987
|
+
const root = await realpath(repo);
|
|
988
|
+
if (!abs.startsWith(root + sep)) {
|
|
989
|
+
res.status(400).json({ error: 'path outside repo' });
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
// repo content is untrusted (an SVG can carry script)
|
|
993
|
+
res.set('Content-Security-Policy', "sandbox; default-src 'none'").set('X-Content-Type-Options', 'nosniff');
|
|
994
|
+
// A project's icon changes about never, and every picker open would otherwise
|
|
995
|
+
// re-walk the filesystem for each recent repo. Short enough that swapping a
|
|
996
|
+
// favicon shows up without a restart.
|
|
997
|
+
res.set('Cache-Control', 'private, max-age=300');
|
|
998
|
+
res.type(MIME[hit.split('.').pop().toLowerCase()]).send(await readFile(abs));
|
|
999
|
+
}
|
|
1000
|
+
catch {
|
|
1001
|
+
res.status(404).end();
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
944
1004
|
const dist = join(import.meta.dirname, '..', 'dist');
|
|
945
1005
|
if (existsSync(dist))
|
|
946
1006
|
app.fallback(serveStatic(dist));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "megit-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
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",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"bin",
|
|
26
26
|
"dist",
|
|
27
27
|
"dist-server",
|
|
28
|
-
"CHANGELOG.md"
|
|
28
|
+
"CHANGELOG.md",
|
|
29
|
+
"SECURITY.md"
|
|
29
30
|
],
|
|
30
31
|
"engines": {
|
|
31
32
|
"node": ">=22"
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@types/node": "^26.4.1",
|
|
50
51
|
"@types/react": "^19.2.18",
|
|
51
|
-
"@types/react-dom": "^19.2.
|
|
52
|
+
"@types/react-dom": "^19.2.7",
|
|
52
53
|
"@types/ws": "^8.18.1",
|
|
53
54
|
"@vitejs/plugin-react": "^6.1.1",
|
|
54
55
|
"@xterm/addon-fit": "^0.11.0",
|
|
@@ -60,6 +61,6 @@
|
|
|
60
61
|
"react-dom": "^19.2.8",
|
|
61
62
|
"typescript": "^7.0.2",
|
|
62
63
|
"vite": "^8.2.2",
|
|
63
|
-
"vitest": "^
|
|
64
|
+
"vitest": "^5.0.0"
|
|
64
65
|
}
|
|
65
66
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@font-face{font-family:Ubuntu Mono;font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/ubuntu-mono-400.woff2)format("woff2")}@font-face{font-family:Ubuntu Mono;font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/ubuntu-mono-700.woff2)format("woff2")}@font-face{font-family:Ubuntu Mono;font-style:italic;font-weight:400;font-display:swap;src:url(/fonts/ubuntu-mono-400-italic.woff2)format("woff2")}:root{--font-stack:"Ubuntu Mono", ui-monospace, monospace;--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{font:16px/1.4 var(--font-stack);background:var(--bg);color:var(--fg)}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}.brand{align-self:center;align-items:flex-end;gap:5px;margin:0 6px 6px 2px;display:flex}.logo{pointer-events:none;width:18px;height:18px;display:block}.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;font-size:11px;line-height:1}.tab-cog{color:var(--fg-faint);background:0 0;border:none;align-self:center;align-items:center;margin:0 4px 6px auto;padding:3px;display:inline-flex}.tab-cog:hover{color:var(--fg);background:0 0}.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:70%;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}.modal.settings{--s1:4px;--s2:8px;--s3:12px;--s4:16px;--s5:24px;--t1:12px;--t2:14px;--t3:16px;--ctrl-h:26px;--ctrl-w:232px;flex-direction:column;width:620px;max-width:92%;display:flex}.modal.settings .modal-head{padding:var(--s3) var(--s5)}.modal.settings .modal-head b{font-size:var(--t3);color:var(--fg)}.settings-close{width:24px;height:24px;color:var(--fg-dim);background:0 0;border:none;border-radius:4px;justify-content:center;align-items:center;padding:0;display:inline-flex}.settings-close:hover{background:var(--bg-hover);color:var(--fg)}.settings-body{padding:var(--s4) var(--s5) var(--s5);flex:1;overflow-y:auto}.settings-group+.settings-group{margin-top:var(--s5)}.settings-h{margin-bottom:var(--s2);font-size:var(--t2);color:var(--fg);font-weight:600}.settings-row{grid-template-columns:1fr var(--ctrl-w);align-items:center;gap:var(--s3);min-height:30px;display:grid}.settings-row>label{font-size:var(--t2);color:var(--fg)}.modal.settings select,.modal.settings input:not([type=checkbox]){width:100%;height:var(--ctrl-h);padding:0 var(--s2);font:inherit;font-size:var(--t2);background:var(--bg-panel);color:var(--fg);border:1px solid var(--border);border-radius:5px}.modal.settings select{appearance:none;padding-right:var(--s5);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%239aa4b2' stroke-width='1.5'%3E%3Cpath d='M3 4.5 6 7.5 9 4.5'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right var(--s2) center;background-size:12px}.modal.settings input[type=checkbox]{accent-color:#5a80b0;justify-self:end;width:15px;height:15px;margin:0}.modal.settings .theme-switch{justify-self:end}.zoom-presets{grid-template-columns:repeat(4,1fr);display:grid}.zoom-presets button{height:var(--ctrl-h);color:var(--fg-dim);border-radius:0;justify-content:center;align-items:center;padding:0;display:inline-flex}.zoom-presets button+button{border-left:none}.zoom-presets button:first-child{border-radius:5px 0 0 5px}.zoom-presets button:last-child{border-radius:0 5px 5px 0}.zoom-presets button.active{background:var(--bg-selected);color:var(--fg)}.zoom-presets [data-z=sm]{font-size:10px}.zoom-presets [data-z=md]{font-size:12px}.zoom-presets [data-z=lg]{font-size:14px}.zoom-presets [data-z=xl]{font-size:16px}.settings-warn{margin:var(--s1) 0 var(--s3);font-size:var(--t1);color:var(--fg-faint);line-height:1.45}.sc-group+.sc-group{margin-top:var(--s3)}.sc-group-name{margin-bottom:var(--s1);font-size:var(--t1);color:var(--fg-faint)}.sc-row{gap:var(--s3);grid-template-columns:108px 1fr;align-items:center;min-height:26px;display:grid}.sc-keys{justify-content:flex-end;align-items:center;gap:3px;display:flex}.sc-alt,.sc-alt>span{align-items:center;gap:3px;display:inline-flex}.sc-row kbd{border:1px solid var(--border);background:var(--bg-hover);min-width:22px;height:20px;font:inherit;font-size:var(--t1);color:var(--fg-dim);border-radius:4px;justify-content:center;align-items:center;padding:0 5px;line-height:1;display:inline-flex}.sc-plus{color:var(--fg-faint);flex:none}.sc-label{font-size:var(--t2);color:var(--fg-dim)}.settings-foot{margin-top:var(--s5);padding-top:var(--s3);border-top:1px solid var(--border);font-size:var(--t1);color:var(--fg-faint);text-align:center}.settings-foot a{color:var(--fg-dim)}.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:80%;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-filter{background:var(--bg-panel);color:var(--fg);border:1px solid var(--border);font:inherit;border-radius:5px;min-width:0;margin:2px 12px 4px;padding:3px 6px}.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:var(--font-stack);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}.amend-toggle{color:var(--fg-dim);cursor:pointer;flex-shrink:0;align-items:center;gap:5px;font-size:12px;display:flex}.amend-toggle input{cursor:pointer;margin:0}.amend-warn{color:#e5c07b;margin-top:6px;font-size:12px;line-height:1.4}.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:var(--font-stack);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{font-family:var(--font-stack);flex-shrink:0;width:14px}.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:var(--font-stack)}.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}
|