megit-app 0.2.0 → 0.4.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 +31 -1
- package/README.md +10 -8
- package/dist/assets/{DiffView-BwLi9YYf.js → DiffView-D1JOBBbA.js} +1 -1
- package/dist/assets/{TerminalPanel-4klkMTPW.js → TerminalPanel-C4Tm1SN5.js} +11 -11
- package/dist/assets/index-DPM0na5E.css +1 -0
- package/dist/assets/index-bXmlM6Hk.js +249 -0
- package/dist/index.html +2 -2
- package/dist-server/config.js +8 -2
- package/dist-server/index.js +6 -4
- package/dist-server/term.js +28 -11
- package/package.json +2 -2
- package/dist/assets/index-BIuZ7qWL.js +0 -249
- package/dist/assets/index-CEcQ7PbO.css +0 -1
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-bXmlM6Hk.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DPM0na5E.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/dist-server/config.js
CHANGED
|
@@ -5,16 +5,22 @@ const dir = join(homedir(), '.config', 'megit');
|
|
|
5
5
|
const file = join(dir, 'config.json');
|
|
6
6
|
export function loadConfig() {
|
|
7
7
|
try {
|
|
8
|
-
|
|
8
|
+
// Spread order matters: a config written before `recent` existed gets the default.
|
|
9
|
+
return { recent: [], ...JSON.parse(readFileSync(file, 'utf8')) };
|
|
9
10
|
}
|
|
10
11
|
catch {
|
|
11
|
-
return { repos: [], activeRepo: null };
|
|
12
|
+
return { repos: [], activeRepo: null, recent: [] };
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
export function saveConfig(c) {
|
|
15
16
|
mkdirSync(dir, { recursive: true });
|
|
16
17
|
writeFileSync(file, JSON.stringify(c, null, 2));
|
|
17
18
|
}
|
|
19
|
+
// MRU: most recent first, deduped, capped. The cap is what keeps the config file
|
|
20
|
+
// from growing without bound as repos come and go.
|
|
21
|
+
export function touchRecent(recent, path, limit = 10) {
|
|
22
|
+
return [path, ...recent.filter(r => r !== path)].slice(0, limit);
|
|
23
|
+
}
|
|
18
24
|
export function isPermutation(a, b) {
|
|
19
25
|
if (a.length !== b.length)
|
|
20
26
|
return false;
|
package/dist-server/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
4
4
|
import { readFile, realpath } from 'node:fs/promises';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
7
|
-
import { loadConfig, saveConfig, isPermutation } from './config.js';
|
|
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
10
|
import { subscribe } from './watch.js';
|
|
@@ -82,6 +82,7 @@ app.post('/api/repos', async (req, res) => {
|
|
|
82
82
|
if (!cfg.repos.includes(path))
|
|
83
83
|
cfg.repos.push(path);
|
|
84
84
|
cfg.activeRepo = path;
|
|
85
|
+
cfg.recent = touchRecent(cfg.recent, path);
|
|
85
86
|
saveConfig(cfg);
|
|
86
87
|
res.json(withCaps(cfg));
|
|
87
88
|
});
|
|
@@ -582,9 +583,10 @@ app.post('/api/wip', repoGuard, async (req, res) => {
|
|
|
582
583
|
break;
|
|
583
584
|
}
|
|
584
585
|
case 'discard':
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
|
|
586
|
+
// unstaged only: worktree back to the index, so staged content survives.
|
|
587
|
+
// clean removes untracked files — a staged new file is tracked, so it stays.
|
|
588
|
+
// No -x — ignored files (node_modules, .env) are not "changes".
|
|
589
|
+
await git(repo, ['restore', '--worktree', '--', '.']);
|
|
588
590
|
await git(repo, ['clean', '-fd']);
|
|
589
591
|
break;
|
|
590
592
|
case 'stash': {
|
package/dist-server/term.js
CHANGED
|
@@ -25,8 +25,18 @@ export function pushCapped(buf, size, chunk, cap = MAX_BUFFER) {
|
|
|
25
25
|
return size;
|
|
26
26
|
}
|
|
27
27
|
const sessions = new Map();
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Split panes are numbered 0..MAX_PANES-1 and each gets its own shell. The bound is
|
|
29
|
+
// enforced here, not by the client: this socket spawns login shells, so an unvalidated
|
|
30
|
+
// pane param is an unbounded shell factory for anything that reaches the upgrade.
|
|
31
|
+
export const MAX_PANES = 4;
|
|
32
|
+
export function termKey(repo, pane) {
|
|
33
|
+
const p = pane ?? '0';
|
|
34
|
+
if (!/^\d$/.test(p) || Number(p) >= MAX_PANES)
|
|
35
|
+
return null;
|
|
36
|
+
return `${repo}\0${p}`;
|
|
37
|
+
}
|
|
38
|
+
async function getSession(key, repo) {
|
|
39
|
+
const existing = sessions.get(key);
|
|
30
40
|
if (existing)
|
|
31
41
|
return existing;
|
|
32
42
|
// dynamic import: the native module never loads until a terminal is actually opened
|
|
@@ -40,25 +50,25 @@ async function getSession(repo) {
|
|
|
40
50
|
env: process.env,
|
|
41
51
|
});
|
|
42
52
|
const s = { pty, buffer: [], size: 0, clients: new Set() };
|
|
43
|
-
sessions.set(
|
|
53
|
+
sessions.set(key, s);
|
|
44
54
|
pty.onData(d => {
|
|
45
55
|
s.size = pushCapped(s.buffer, s.size, d);
|
|
46
56
|
for (const ws of s.clients)
|
|
47
57
|
ws.send(d);
|
|
48
58
|
});
|
|
49
59
|
pty.onExit(() => {
|
|
50
|
-
sessions.delete(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
sessions.delete(key);
|
|
61
|
+
// 4000 tells the client "the shell is gone" (vs. a plain close, which is the
|
|
62
|
+
// server going away) so it can drop the pane instead of showing a reconnect hint
|
|
63
|
+
for (const ws of s.clients)
|
|
64
|
+
ws.close(4000, 'exit');
|
|
55
65
|
});
|
|
56
66
|
return s;
|
|
57
67
|
}
|
|
58
|
-
async function attach(repo, ws) {
|
|
68
|
+
async function attach(key, repo, ws) {
|
|
59
69
|
let s;
|
|
60
70
|
try {
|
|
61
|
-
s = await getSession(repo);
|
|
71
|
+
s = await getSession(key, repo);
|
|
62
72
|
}
|
|
63
73
|
catch {
|
|
64
74
|
// resolve() said the package is there but the native binding failed to load
|
|
@@ -85,6 +95,8 @@ async function attach(repo, ws) {
|
|
|
85
95
|
s.buffer.length = 0;
|
|
86
96
|
s.size = 0;
|
|
87
97
|
}
|
|
98
|
+
else if (msg.t === 'k')
|
|
99
|
+
s.pty.kill(); // pane closed by its ✕ — onExit does the cleanup
|
|
88
100
|
});
|
|
89
101
|
ws.on('close', () => s.clients.delete(ws));
|
|
90
102
|
}
|
|
@@ -108,6 +120,11 @@ export function wireTerminal(server) {
|
|
|
108
120
|
socket.destroy();
|
|
109
121
|
return;
|
|
110
122
|
}
|
|
111
|
-
|
|
123
|
+
const key = termKey(repo, url.searchParams.get('pane'));
|
|
124
|
+
if (!key) {
|
|
125
|
+
socket.destroy();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
wss.handleUpgrade(req, socket, head, ws => { void attach(key, repo, ws); });
|
|
112
129
|
});
|
|
113
130
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "megit-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"CHANGELOG.md"
|
|
30
30
|
],
|
|
31
31
|
"engines": {
|
|
32
|
-
"node": ">=
|
|
32
|
+
"node": ">=22"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"dev": "PORT=4500 UI_PORT=4000 concurrently -k \"node --watch server/index.ts\" \"vite\"",
|