megit-app 0.1.0 → 0.3.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/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-CfQ1U4Ld.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-DZyh3SKi.css">
11
+ <script type="module" crossorigin src="/assets/index-WjpXTtcO.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-CjT1mano.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -428,7 +428,9 @@ app.post('/api/branch', repoGuard, async (req, res) => {
428
428
  }
429
429
  case 'merge':
430
430
  case 'rebase': {
431
- const branch = mustExist(await locals(), req.body.branch, 'branch');
431
+ // remote-tracking refs count: once a local branch has diverged from its
432
+ // upstream, origin/x is its own chip and merging it is the point
433
+ const branch = mustExist([...await locals(), ...await knownRefs(repo, 'refs/remotes')], req.body.branch, 'branch');
432
434
  // --autostash, matching the auto-stash /api/checkout does — git's flag,
433
435
  // not our own stash dance. A conflict stops and stays stopped: the files
434
436
  // show up in the WIP row and get resolved in the terminal.
@@ -441,6 +443,12 @@ app.post('/api/branch', repoGuard, async (req, res) => {
441
443
  await git(repo, ['branch', `--set-upstream-to=${upstream}`, branch]);
442
444
  break;
443
445
  }
446
+ case 'fetch':
447
+ // --prune so remote-tracking chips don't outlive branches deleted upstream.
448
+ // ponytail: no --all — the default remote is the one Pull/Push and the
449
+ // ahead/behind badges act on, so it's the one the badges need fresh.
450
+ await git(repo, ['fetch', '--prune'], [0], NET_TIMEOUT);
451
+ break;
444
452
  case 'pull':
445
453
  await git(repo, ['pull', '--ff-only', '--autostash'], [0], NET_TIMEOUT);
446
454
  break;
@@ -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
- async function getSession(repo) {
29
- const existing = sessions.get(repo);
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(repo, s);
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(repo);
51
- for (const ws of s.clients) {
52
- ws.send('\r\n[session ended]\r\n');
53
- ws.close();
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
- wss.handleUpgrade(req, socket, head, ws => { void attach(repo, ws); });
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.1.0",
3
+ "version": "0.3.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": ">=24"
32
+ "node": ">=22"
33
33
  },
34
34
  "scripts": {
35
35
  "dev": "PORT=4500 UI_PORT=4000 concurrently -k \"node --watch server/index.ts\" \"vite\"",