megit-app 0.8.0 → 0.10.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 CHANGED
@@ -9,6 +9,30 @@ surface, and the HTTP API may change in any minor release.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.10.0] - 2026-09-03
13
+
14
+ ### Added
15
+
16
+ - A **Settings** dialog in a cog button at the end of the tab bar: font, app zoom, theme,
17
+ default diff view, an author avatars toggle (stops GitHub/Gravatar lookups when off),
18
+ and a shortcut reference.
19
+
20
+ ### Changed
21
+
22
+ - The version tag moved from the right end of the tab bar to beside the logo, making room
23
+ for the new settings cog.
24
+
25
+ ## [0.9.0] - 2026-08-28
26
+
27
+ ### Changed
28
+
29
+ - Returning to the megit browser tab runs a full refresh — spinner and remote fetch, at most
30
+ once every 10 seconds. A background tab can have its event stream throttled or dropped, so
31
+ waiting for a change event was not enough.
32
+ - `megit start` now restarts a server that is already running instead of reporting
33
+ "already running", so an upgraded version actually takes effect. It also replaces a
34
+ server running on another port rather than refusing to start.
35
+
12
36
  ## [0.8.0] - 2026-08-20
13
37
 
14
38
  ### Added
@@ -225,7 +249,9 @@ First public release.
225
249
  - 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
226
250
  - Linux: everything except the terminal — node-pty is an optionalDependency with no Linux prebuild, and the terminal button is hidden when it is unavailable
227
251
 
228
- [Unreleased]: https://github.com/vuongvu1/megit/compare/v0.8.0...HEAD
252
+ [Unreleased]: https://github.com/vuongvu1/megit/compare/v0.10.0...HEAD
253
+ [0.10.0]: https://github.com/vuongvu1/megit/compare/v0.9.0...v0.10.0
254
+ [0.9.0]: https://github.com/vuongvu1/megit/compare/v0.8.0...v0.9.0
229
255
  [0.8.0]: https://github.com/vuongvu1/megit/compare/v0.7.0...v0.8.0
230
256
  [0.7.0]: https://github.com/vuongvu1/megit/compare/v0.6.0...v0.7.0
231
257
  [0.6.0]: https://github.com/vuongvu1/megit/compare/v0.5.2...v0.6.0
package/README.md CHANGED
@@ -25,6 +25,8 @@ npx megit-app start
25
25
  npx megit-app stop
26
26
  ```
27
27
 
28
+ `start` restarts a server that is already running, so a fresh version takes effect.
29
+
28
30
  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`.
29
31
 
30
32
  <sub>The package is `megit-app`; `megit` on npm is unrelated, so keep the `-app`. The installed command is still `megit`.</sub>
@@ -102,6 +104,13 @@ The server watches each open repository (`fs.watch`, filtered and debounced) and
102
104
 
103
105
  Auto-refresh only ever reads local git, so commits pushed by someone else stay invisible until something fetches. <kbd>r</kbd> and the ⟳ button therefore fetch from the remote first, then refresh — that is the one path that surfaces new upstream commits and refreshes the Pull/Push badges. A fetch that fails (offline, no remote) is ignored and the local refresh still happens.
104
106
 
107
+ ### Settings
108
+
109
+ The cog at the right end of the tab bar opens font (bundled Ubuntu Mono, your system
110
+ monospace, or your system UI face), text size in four steps from 80% to 140%, the theme, the default diff
111
+ view, and a toggle for author avatars — off stops the GitHub and Gravatar lookups megit
112
+ otherwise makes per commit author. The same dialog lists every keyboard shortcut below.
113
+
105
114
  ### Keyboard
106
115
 
107
116
  | Key | Action |
@@ -112,6 +121,7 @@ Auto-refresh only ever reads local git, so commits pushed by someone else stay i
112
121
  | <kbd>r</kbd> | fetch from remote, then refresh |
113
122
  | <kbd>⌘</kbd><kbd>J</kbd> | toggle terminal |
114
123
  | <kbd>⌘</kbd><kbd>K</kbd> | clear terminal |
124
+ | <kbd>⌘</kbd><kbd>D</kbd> | split terminal pane |
115
125
  | <kbd>⌘</kbd><kbd>⇧</kbd><kbd>0</kbd> | toggle theme |
116
126
  | <kbd>⌘</kbd><kbd>↵</kbd> | commit |
117
127
  | <kbd>Esc</kbd> | close search or menu / cancel edit |
package/bin/megit.js CHANGED
@@ -16,7 +16,7 @@ if (cmd === '-h' || cmd === '--help') {
16
16
 
17
17
  Usage:
18
18
  megit run in this terminal
19
- megit start run in the background
19
+ megit start run in the background (restarts it if already running)
20
20
  megit stop stop the background server
21
21
 
22
22
  Options:
@@ -68,17 +68,28 @@ const megitAnswers = async p => {
68
68
  }
69
69
  }
70
70
 
71
+ // A dead process frees its port immediately, but `process.kill` returns before
72
+ // the exit has happened — spawning the replacement first would race the bind.
73
+ const waitForExit = async pid => {
74
+ for (let i = 0; i < 50 && pidAlive(pid); i++) await new Promise(r => setTimeout(r, 100))
75
+ return !pidAlive(pid)
76
+ }
77
+
71
78
  if (cmd === 'start') {
72
79
  const prev = readState()
80
+ // `start` restarts rather than no-ops: after `npm i -g megit-app@latest` the old
81
+ // process is still the old version, and reporting "already running" would leave
82
+ // the upgrade silently unapplied. ponytail: one daemon is tracked, so a running
83
+ // server is replaced whatever port it is on — key the state file by port if
84
+ // anyone actually wants two.
73
85
  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)
86
+ process.kill(prev.pid, 'SIGTERM')
87
+ rmSync(stateFile, { force: true })
88
+ if (!(await waitForExit(prev.pid))) {
89
+ console.error(`megit: server on http://127.0.0.1:${prev.port} (PID ${prev.pid}) did not stop`)
90
+ process.exit(1)
79
91
  }
80
- console.error(`megit: already running on http://127.0.0.1:${prev.port} — run 'megit stop' first`)
81
- process.exit(1)
92
+ console.log(`megit: restarting (was on http://127.0.0.1:${prev.port})`)
82
93
  }
83
94
 
84
95
  mkdirSync(dir, { recursive: true })
@@ -1 +1 @@
1
- import{a as e,i as t,n,r}from"./index-j_kzUnBQ.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};
1
+ import{d as e,i as t,n,r}from"./index-l_8r494M.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};