megit-app 0.5.2 → 0.7.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 +27 -1
- package/README.md +6 -8
- package/bin/megit.js +113 -21
- package/dist/assets/{ConflictView-DB6LAnvd.js → ConflictView-CHWx-7wL.js} +1 -1
- package/dist/assets/{DiffView-D_q9I79M.js → DiffView-DXXDhOBr.js} +1 -1
- package/dist/assets/{TerminalPanel-ddwB1LzO.js → TerminalPanel-DI0qzKCw.js} +1 -1
- package/dist/assets/index-C5YS6cns.css +1 -0
- package/dist/assets/{index-BZBjYCd4.js → index-Ch9W5KCh.js} +3 -3
- package/dist/index.html +2 -2
- package/dist-server/config.js +1 -1
- package/package.json +4 -4
- package/dist/assets/index-ByYQYfuO.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,28 @@ surface, and the HTTP API may change in any minor release.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.7.0] - 2026-08-19
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- A search box filters the recent-repositories list in the picker. It matches the full
|
|
17
|
+
path, not just the folder name, so two checkouts of the same repo stay distinguishable.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- The recent-repositories list keeps 12 entries instead of 10.
|
|
22
|
+
|
|
23
|
+
## [0.6.0] - 2026-08-16
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `megit start` runs the server in the background and `megit stop` shuts it down, so the
|
|
28
|
+
terminal that launched megit can be closed. Output goes to `~/.config/megit/megit.log`.
|
|
29
|
+
|
|
30
|
+
### Removed
|
|
31
|
+
|
|
32
|
+
- The `[repo-path]` argument is gone — open repositories from the picker in the app.
|
|
33
|
+
|
|
12
34
|
## [0.5.2] - 2026-08-12
|
|
13
35
|
|
|
14
36
|
### Fixed
|
|
@@ -190,7 +212,11 @@ First public release.
|
|
|
190
212
|
- Windows (arm64, x64): builds and runs, but untested on real hardware — recursive `fs.watch` crashes the test worker there, so auto-refresh is not covered by CI
|
|
191
213
|
- Linux: everything except the terminal — node-pty is an optionalDependency with no Linux prebuild, and the terminal button is hidden when it is unavailable
|
|
192
214
|
|
|
193
|
-
[Unreleased]: https://github.com/vuongvu1/megit/compare/v0.
|
|
215
|
+
[Unreleased]: https://github.com/vuongvu1/megit/compare/v0.7.0...HEAD
|
|
216
|
+
[0.7.0]: https://github.com/vuongvu1/megit/compare/v0.6.0...v0.7.0
|
|
217
|
+
[0.6.0]: https://github.com/vuongvu1/megit/compare/v0.5.2...v0.6.0
|
|
218
|
+
[0.5.2]: https://github.com/vuongvu1/megit/compare/v0.5.1...v0.5.2
|
|
219
|
+
[0.5.1]: https://github.com/vuongvu1/megit/compare/v0.5.0...v0.5.1
|
|
194
220
|
[0.5.0]: https://github.com/vuongvu1/megit/compare/v0.4.1...v0.5.0
|
|
195
221
|
[0.4.1]: https://github.com/vuongvu1/megit/compare/v0.4.0...v0.4.1
|
|
196
222
|
[0.4.0]: https://github.com/vuongvu1/megit/compare/v0.3.0...v0.4.0
|
package/README.md
CHANGED
|
@@ -12,22 +12,20 @@ It writes, too: stage/unstage/discard, commit and amend, branch and tag create/d
|
|
|
12
12
|
|
|
13
13
|
Requires Node ≥ 22. The package is 1.8 MB, with `ws` as its only runtime dependency and no install scripts.
|
|
14
14
|
|
|
15
|
-
**Run it once, without installing:**
|
|
16
|
-
|
|
17
15
|
```bash
|
|
18
16
|
npx megit-app
|
|
19
17
|
```
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
That starts the server on port 3411 — set `PORT` to change that — and opens your browser at it. <kbd>Ctrl</kbd><kbd>C</kbd> stops it.
|
|
20
|
+
|
|
21
|
+
**To keep it running after you close the terminal:**
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
|
|
25
|
-
megit
|
|
24
|
+
npx megit-app start
|
|
25
|
+
npx megit-app stop
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Either way the server starts on port 3411 — set `PORT` to change that — and opens your browser at it.
|
|
28
|
+
Or install it once with `npm i -g megit-app` and run `megit`, `megit start` and `megit stop` from anywhere; upgrade later with `npm i -g megit-app@latest`.
|
|
31
29
|
|
|
32
30
|
<sub>The package is `megit-app`; `megit` on npm is unrelated, so keep the `-app`. The installed command is still `megit`.</sub>
|
|
33
31
|
|
package/bin/megit.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// megit CLI —
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import {
|
|
2
|
+
// megit CLI — run the local server in this terminal, or detach it with
|
|
3
|
+
// `start`/`stop` so it outlives the terminal that launched it.
|
|
4
|
+
import { execFile, spawn } from 'node:child_process'
|
|
5
|
+
import { mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
6
6
|
import { createRequire } from 'node:module'
|
|
7
|
-
import {
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
8
10
|
|
|
9
|
-
const
|
|
11
|
+
const cmd = process.argv[2]
|
|
10
12
|
|
|
11
13
|
// handled before importing the server, so --help never binds a port
|
|
12
|
-
if (
|
|
14
|
+
if (cmd === '-h' || cmd === '--help') {
|
|
13
15
|
console.log(`megit — git repository viewer in the browser
|
|
14
16
|
|
|
15
17
|
Usage:
|
|
16
|
-
megit
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
repo-path open this repository in a tab (default: reopen last session)
|
|
18
|
+
megit run in this terminal
|
|
19
|
+
megit start run in the background
|
|
20
|
+
megit stop stop the background server
|
|
20
21
|
|
|
21
22
|
Options:
|
|
22
23
|
-h, --help show this message
|
|
@@ -27,7 +28,7 @@ Environment:
|
|
|
27
28
|
process.exit(0)
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
if (
|
|
31
|
+
if (cmd === '-v' || cmd === '--version') {
|
|
31
32
|
console.log(createRequire(import.meta.url)('../package.json').version)
|
|
32
33
|
process.exit(0)
|
|
33
34
|
}
|
|
@@ -35,19 +36,110 @@ if (arg === '-v' || arg === '--version') {
|
|
|
35
36
|
const port = Number(process.env.PORT) || 3411
|
|
36
37
|
const url = `http://127.0.0.1:${port}`
|
|
37
38
|
|
|
38
|
-
const
|
|
39
|
-
|
|
39
|
+
const dir = join(homedir(), '.config', 'megit')
|
|
40
|
+
const stateFile = join(dir, 'daemon.json')
|
|
41
|
+
const logFile = join(dir, 'megit.log')
|
|
42
|
+
|
|
43
|
+
const readState = () => {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(stateFile, 'utf8'))
|
|
46
|
+
} catch {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const pidAlive = pid => {
|
|
52
|
+
try {
|
|
53
|
+
process.kill(pid, 0) // signal 0 tests for existence, sends nothing
|
|
54
|
+
return true
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// "Is megit up?" is answered by megit answering, not by a PID existing. A PID
|
|
61
|
+
// can be reused after an unclean exit, and a port can be held by anything.
|
|
62
|
+
const megitAnswers = async p => {
|
|
63
|
+
try {
|
|
64
|
+
const r = await fetch(`http://127.0.0.1:${p}/api/config`, { signal: AbortSignal.timeout(1000) })
|
|
65
|
+
return r.ok && 'hasTerminal' in (await r.json())
|
|
66
|
+
} catch {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (cmd === 'start') {
|
|
72
|
+
const prev = readState()
|
|
73
|
+
if (prev && pidAlive(prev.pid) && (await megitAnswers(prev.port))) {
|
|
74
|
+
// ponytail: one daemon is tracked, so a second PORT is a conflict rather than
|
|
75
|
+
// a second server. Key the state file by port if anyone actually wants two.
|
|
76
|
+
if (prev.port === port) {
|
|
77
|
+
console.log(`megit already running → http://127.0.0.1:${prev.port}`)
|
|
78
|
+
process.exit(0)
|
|
79
|
+
}
|
|
80
|
+
console.error(`megit: already running on http://127.0.0.1:${prev.port} — run 'megit stop' first`)
|
|
81
|
+
process.exit(1)
|
|
82
|
+
}
|
|
40
83
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
84
|
+
mkdirSync(dir, { recursive: true })
|
|
85
|
+
// Without a log, a daemon that dies during startup dies invisibly.
|
|
86
|
+
const log = openSync(logFile, 'a')
|
|
87
|
+
// Re-run this same script with no arguments: the foreground path below is the
|
|
88
|
+
// one startup sequence, and the daemon must not diverge from it. `detached`
|
|
89
|
+
// gives the child its own process group so the shell's SIGHUP on exit misses
|
|
90
|
+
// it; `unref` lets this process leave without waiting.
|
|
91
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url)], {
|
|
92
|
+
detached: true,
|
|
93
|
+
stdio: ['ignore', log, log],
|
|
94
|
+
windowsHide: true,
|
|
46
95
|
})
|
|
47
|
-
|
|
48
|
-
|
|
96
|
+
child.unref()
|
|
97
|
+
|
|
98
|
+
// unref() only stops the child from holding this process open — 'exit' still
|
|
99
|
+
// fires, which turns the common failure (port already in use) into an
|
|
100
|
+
// immediate answer instead of five seconds of polling a corpse.
|
|
101
|
+
let died = false
|
|
102
|
+
child.once('exit', () => (died = true))
|
|
103
|
+
|
|
104
|
+
// Poll rather than assume: a failed bind would otherwise be reported as success.
|
|
105
|
+
for (let i = 0; i < 50 && !died; i++) {
|
|
106
|
+
if (await megitAnswers(port)) {
|
|
107
|
+
writeFileSync(stateFile, JSON.stringify({ pid: child.pid, port }))
|
|
108
|
+
console.log(`megit → ${url}`)
|
|
109
|
+
process.exit(0)
|
|
110
|
+
}
|
|
111
|
+
await new Promise(r => setTimeout(r, 100))
|
|
112
|
+
}
|
|
113
|
+
console.error(`megit: server did not come up on ${url} — see ${logFile}`)
|
|
114
|
+
process.exit(1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (cmd === 'stop') {
|
|
118
|
+
const state = readState()
|
|
119
|
+
if (!state || !pidAlive(state.pid)) {
|
|
120
|
+
rmSync(stateFile, { force: true })
|
|
121
|
+
console.log('megit: no background server running')
|
|
122
|
+
process.exit(0)
|
|
123
|
+
}
|
|
124
|
+
if (!(await megitAnswers(state.port))) {
|
|
125
|
+
rmSync(stateFile, { force: true })
|
|
126
|
+
console.error(`megit: stale state file — PID ${state.pid} is not megit, leaving it alone`)
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
process.kill(state.pid, 'SIGTERM')
|
|
130
|
+
rmSync(stateFile, { force: true })
|
|
131
|
+
console.log('megit stopped')
|
|
132
|
+
process.exit(0)
|
|
49
133
|
}
|
|
50
134
|
|
|
135
|
+
if (cmd) {
|
|
136
|
+
console.error(`megit: unknown command '${cmd}' — try 'megit --help'`)
|
|
137
|
+
process.exit(1)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { server } = await import('../dist-server/index.js')
|
|
141
|
+
if (!server.listening) await new Promise(r => server.once('listening', r))
|
|
142
|
+
|
|
51
143
|
// ponytail: no `open` dependency — three platform names cover what it does.
|
|
52
144
|
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open'
|
|
53
145
|
execFile(opener, [url], () => {}) // exit code ignored: explorer.exe returns 1 on success
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e,i as t,n,r}from"./index-
|
|
1
|
+
import{a as e,i as t,n,r}from"./index-Ch9W5KCh.js";var i=e(),a=/^<<<<<<< ?(.*)$/,o=/^\|\|\|\|\|\|\| ?(.*)$/,s=/^=======$/,c=/^>>>>>>> ?(.*)$/,l=e=>e.replace(/\r?\n$/,``);function u(e){let t=e.split(/(?<=\n)/),n=[],r=[],i=null,u=`context`,d=()=>{r.length&&n.push({kind:`context`,lines:r}),r=[]};for(let e of t){let t=l(e),f=a.exec(t);if(f){if(u!==`context`)return null;d(),i={ours:[],base:null,theirs:[],oursLabel:f[1].trim(),theirsLabel:``},u=`ours`;continue}if(u===`context`){r.push(e);continue}if(o.exec(t)){if(u!==`ours`)return null;i.base=[],u=`base`;continue}if(s.test(t)){if(u!==`ours`&&u!==`base`)return null;u=`theirs`;continue}let p=c.exec(t);if(p){if(u!==`theirs`)return null;i.theirsLabel=p[1].trim(),n.push({kind:`conflict`,block:i}),i=null,u=`context`;continue}u===`ours`?i.ours.push(e):u===`base`?i.base.push(e):i.theirs.push(e)}return u===`context`?(d(),n.some(e=>e.kind===`conflict`)?n:null):null}function d(e,t){let n=[];return e.forEach((e,r)=>{if(e.kind===`context`){n.push(...e.lines);return}let i=t.get(r);if(!i)throw Error(`no pick for conflict at segment ${r}`);(i===`ours`||i===`both`)&&n.push(...e.block.ours),(i===`theirs`||i===`both`)&&n.push(...e.block.theirs)}),n.join(``)}var f=n(),p=[[`ours`,`Use ours`],[`theirs`,`Use theirs`],[`both`,`Use both`]];function m({note:e,busy:t,onPick:n,onMarkResolved:r}){return(0,f.jsxs)(`div`,{className:`cf-card`,children:[(0,f.jsx)(`div`,{className:`cf-card-note`,children:e}),(0,f.jsxs)(`div`,{className:`cf-card-actions`,children:[r&&(0,f.jsx)(`button`,{className:`primary`,disabled:t,onClick:r,children:`Mark resolved`}),(0,f.jsx)(`button`,{disabled:t,onClick:()=>n(`ours`),children:`Keep ours`}),(0,f.jsx)(`button`,{disabled:t,onClick:()=>n(`theirs`),children:`Keep theirs`}),(0,f.jsx)(`button`,{className:`danger`,disabled:t,onClick:()=>n(`delete`),children:`Delete file`})]})]})}function h({repo:e,file:n,onResolved:a}){let[o,s]=(0,i.useState)(null),[c,l]=(0,i.useState)(new Map),[h,g]=(0,i.useState)(``),[_,v]=(0,i.useState)(!1),y=`repo=${encodeURIComponent(e)}`;(0,i.useEffect)(()=>{s(null),l(new Map),g(``),r(`/api/conflict?${y}&file=${encodeURIComponent(n)}`).then(s).catch(e=>g(e.message))},[e,n]);let b=(0,i.useMemo)(()=>o?.content?u(o.content):null,[o]),x=(0,i.useMemo)(()=>b?.filter(e=>e.kind===`conflict`).length??0,[b]);(0,i.useEffect)(()=>{!b||!x||c.size<x||(v(!0),r(`/api/conflict?${y}`,t(`POST`,{action:`resolve`,file:n,content:d(b,c)})).then(a).catch(e=>{g(e.message),v(!1)}))},[c,b,x]);let S=e=>{v(!0),r(`/api/conflict?${y}`,t(`POST`,{action:e,file:n})).then(a).catch(e=>{g(e.message),v(!1)})},C=()=>{v(!0),r(`/api/conflict?${y}`,t(`POST`,{action:`resolve`,file:n,content:o?.content??``})).then(a).catch(e=>{g(e.message),v(!1)})},w=(e,t)=>l(n=>new Map(n).set(e,t)),T=e=>l(t=>{let n=new Map(t);return n.delete(e),n});return h?(0,f.jsx)(`div`,{className:`diffview error`,children:h}):o?o.tooLarge?(0,f.jsxs)(`div`,{className:`diffview empty`,children:[`File too large to resolve here (`,Math.round((o.size??0)/1024),` KB) — use the terminal`]}):o.binary?(0,f.jsx)(`div`,{className:`cf-view`,children:(0,f.jsx)(m,{note:`Binary file — there is nothing to merge line by line.`,busy:_,onPick:S})}):o.missing?(0,f.jsx)(`div`,{className:`cf-view`,children:(0,f.jsx)(m,{note:`Deleted on one side and modified on the other.`,busy:_,onPick:S})}):b?(0,f.jsxs)(`div`,{className:`cf-view`,children:[(0,f.jsxs)(`div`,{className:`cf-progress`,children:[c.size,` of `,x,` resolved`]}),b.map((e,t)=>{if(e.kind===`context`)return(0,f.jsx)(`pre`,{className:`cf-context`,children:e.lines.join(``)},t);let n=c.get(t);return(0,f.jsxs)(`div`,{className:`cf-block${n?` picked`:``}`,children:[(0,f.jsxs)(`div`,{className:`cf-bar`,children:[(0,f.jsx)(`span`,{className:`cf-side-name`,children:e.block.oursLabel||`ours`}),n?(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(`span`,{className:`cf-chosen`,children:[`took `,n]}),(0,f.jsx)(`button`,{disabled:_,onClick:()=>T(t),children:`Reset`})]}):p.map(([e,n])=>(0,f.jsx)(`button`,{disabled:_,onClick:()=>w(t,e),children:n},e))]}),(!n||n===`ours`||n===`both`)&&(0,f.jsx)(`pre`,{className:`cf-ours`,children:e.block.ours.join(``)}),!n&&(0,f.jsx)(`div`,{className:`cf-mid`,children:e.block.theirsLabel||`theirs`}),(!n||n===`theirs`||n===`both`)&&(0,f.jsx)(`pre`,{className:`cf-theirs`,children:e.block.theirs.join(``)})]},t)})]}):(0,f.jsx)(`div`,{className:`cf-view`,children:(0,f.jsx)(m,{note:`No conflict markers left in this file — it looks already resolved.`,busy:_,onPick:S,onMarkResolved:C})}):(0,f.jsx)(`div`,{className:`diffview empty`,children:`Loading…`})}export{h as default};
|