megit-app 0.5.0 → 0.5.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 +50 -41
- package/README.md +2 -2
- package/dist/assets/{ConflictView-DGLXjOa1.js → ConflictView-C8umUTWy.js} +1 -1
- package/dist/assets/{DiffView-DHHWx0R5.js → DiffView-C-794XGt.js} +1 -1
- package/dist/assets/{TerminalPanel-DtRWBLUl.js → TerminalPanel-DMY2xwkh.js} +1 -1
- package/dist/assets/{index-Bfrt9LxG.js → index-BC7TJRRI.js} +3 -3
- package/dist/index.html +1 -1
- package/dist-server/http.js +174 -0
- package/dist-server/index.js +10 -14
- package/dist-server/term.js +29 -1
- package/package.json +5 -9
- package/scripts/fix-pty-perms.mjs +0 -21
package/CHANGELOG.md
CHANGED
|
@@ -9,41 +9,35 @@ surface, and the HTTP API may change in any minor release.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.5.1] - 2026-08-10
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Express is gone; routing and static serving are now ~150 lines on `node:http`. A megit install
|
|
17
|
+
pulls 3 packages instead of 70, and the server starts in about half the time.
|
|
18
|
+
- The `postinstall` script is gone. node-pty's `spawn-helper` is made executable on the first
|
|
19
|
+
terminal you open instead, so installing megit runs no scripts of its own.
|
|
20
|
+
|
|
12
21
|
## [0.5.0] - 2026-08-08
|
|
13
22
|
|
|
14
23
|
### Added
|
|
15
24
|
|
|
16
|
-
- Merge
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ours / Use theirs / Use both / Reset. Deciding the last block writes the file and stages it.
|
|
23
|
-
Files with nothing to pick — binary, delete/modify, submodule — get Keep ours / Keep theirs /
|
|
24
|
-
Delete instead, and a file fixed by hand in the terminal can be accepted with Mark resolved.
|
|
25
|
-
Picks are held in the client and written only on full resolution, so Reset costs nothing and
|
|
26
|
-
the watcher stays quiet. Stash-pop conflicts get the picker but no banner: git leaves no state
|
|
27
|
-
file to detect and has no `--continue`.
|
|
28
|
-
- A Rendered/Source toggle on `.svg` diffs. SVG is text rendered as a picture, which hides
|
|
29
|
-
exactly the edits worth reviewing — a changed `viewBox`, a renamed `id`, a `stroke-width`
|
|
30
|
-
tweak below visual threshold. Source mode shows the real patch with Unified/Split; the choice
|
|
31
|
-
is sticky across files and reloads. Raster images are deliberately excluded — their source
|
|
32
|
-
diff is `Binary files … differ`, a control that leads nowhere.
|
|
25
|
+
- Merge conflict resolution. A conflicted merge, rebase, cherry-pick or revert shows a banner
|
|
26
|
+
with Abort and Continue; conflicted files open a per-block picker (Use ours / theirs / both /
|
|
27
|
+
Reset) instead of a diff, and hand-fixed files take Mark resolved. Stash-pop conflicts get the
|
|
28
|
+
picker but no banner — git leaves no state to detect and has no `--continue`.
|
|
29
|
+
- Rendered/Source toggle on `.svg` diffs, so a changed `viewBox`, `id` or `stroke-width` is
|
|
30
|
+
reviewable as text. Sticky across files; raster images stay excluded.
|
|
33
31
|
|
|
34
32
|
### Changed
|
|
35
33
|
|
|
36
|
-
- The add-repo dialog is
|
|
37
|
-
|
|
38
|
-
repository name with a dot when it is already open and a highlight on the active one; folder
|
|
39
|
-
rows carry a chevron, and the path header bolds the directory you are in.
|
|
34
|
+
- The add-repo dialog is two panes: recent repositories on the left, directory browser on the
|
|
35
|
+
right.
|
|
40
36
|
|
|
41
37
|
### Fixed
|
|
42
38
|
|
|
43
|
-
- "Stage all" and "Discard all"
|
|
44
|
-
|
|
45
|
-
paths with no stage-0 entry to restore from, taking the whole batch with it. Both now exclude
|
|
46
|
-
every unmerged path by pathspec.
|
|
39
|
+
- "Stage all" and "Discard all" skip unmerged paths. `git add -A` staged conflict markers, and
|
|
40
|
+
`git restore --worktree -- .` failed on paths with no stage-0 entry, killing the whole batch.
|
|
47
41
|
|
|
48
42
|
## [0.4.1] - 2026-08-05
|
|
49
43
|
|
|
@@ -60,38 +54,53 @@ surface, and the HTTP API may change in any minor release.
|
|
|
60
54
|
|
|
61
55
|
### Changed
|
|
62
56
|
|
|
63
|
-
- "Discard all"
|
|
57
|
+
- "Discard all" discards only unstaged changes — tracked files go back to their staged content,
|
|
58
|
+
untracked files are deleted, staged work survives. It previously reset both sides to HEAD,
|
|
59
|
+
throwing away the hunk you had just staged to protect.
|
|
64
60
|
|
|
65
61
|
### Fixed
|
|
66
62
|
|
|
67
|
-
- The search bar (<kbd>⌘F</kbd>) no longer slides sideways when the commit panel opens
|
|
63
|
+
- The search bar (<kbd>⌘F</kbd>) no longer slides sideways when the commit panel opens. It was
|
|
64
|
+
centred on `.graph-pane`, which shrinks with the panel; it is now pinned to the subject column.
|
|
68
65
|
|
|
69
66
|
## [0.3.0] - 2026-08-01
|
|
70
67
|
|
|
71
68
|
### Added
|
|
72
69
|
|
|
73
|
-
- Split terminal panes: <kbd>⌘D</kbd>
|
|
70
|
+
- Split terminal panes: <kbd>⌘D</kbd> divides the terminal into up to four side-by-side shells,
|
|
71
|
+
each its own PTY and scrollback, remembered per repository. The four-pane cap is enforced
|
|
72
|
+
server-side — the WebSocket spawns login shells, so an unbounded pane index would be an
|
|
73
|
+
unbounded shell factory.
|
|
74
74
|
|
|
75
75
|
### Changed
|
|
76
76
|
|
|
77
|
-
- The
|
|
77
|
+
- The package now requires Node ≥ 22 instead of ≥ 24, so `npx megit-app` no longer warns
|
|
78
|
+
EBADENGINE on Node 22 or 23. Node 24 was only needed to run `server/*.ts` in development.
|
|
78
79
|
|
|
79
80
|
### Fixed
|
|
80
81
|
|
|
81
|
-
- A stash newer than every commit no longer sinks to its base commit's row.
|
|
82
|
+
- A stash newer than every commit no longer sinks to its base commit's row. The guard that
|
|
83
|
+
snaps a stash down to its base fired on any busy history — it now keeps the chronological row
|
|
84
|
+
whenever the clear lane fits inside the width the graph already draws.
|
|
82
85
|
|
|
83
86
|
## [0.2.0] - 2026-07-30
|
|
84
87
|
|
|
85
88
|
### Added
|
|
86
89
|
|
|
87
|
-
- Merge and Rebase in the right-click menu of a remote branch chip, not only a local one.
|
|
88
|
-
|
|
90
|
+
- Merge and Rebase in the right-click menu of a remote branch chip, not only a local one. The
|
|
91
|
+
actions address the branch by its full remote-tracking ref, so a diverged origin/main can
|
|
92
|
+
never resolve to the local main.
|
|
93
|
+
- A build badge in the tab bar: [DEV] under the Vite dev server, the package version in a
|
|
94
|
+
production build. Baked in at build time, so it costs no request.
|
|
89
95
|
|
|
90
96
|
### Fixed
|
|
91
97
|
|
|
92
|
-
- Refresh (⟳
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
- Refresh (⟳ and <kbd>r</kbd>) fetches from the remote before re-reading the repository, so
|
|
99
|
+
commits pushed by someone else and the Pull/Push counts no longer stay stale. Auto-refresh and
|
|
100
|
+
initial tab load remain local-only; a failed fetch is ignored.
|
|
101
|
+
- Ref chips are ordered branches-first, tags-last, instead of git's listing order.
|
|
102
|
+
- The tab close button is a real 18×18 target with a hover ring and a Close <repo> label for
|
|
103
|
+
screen readers; its cross is an SVG, since a × glyph centres on its line box, not its ink.
|
|
95
104
|
|
|
96
105
|
## [0.1.0] - 2026-07-28
|
|
97
106
|
|
|
@@ -122,7 +131,7 @@ First public release.
|
|
|
122
131
|
|
|
123
132
|
- <kbd>⌘F</kbd> find bar filtering the loaded rows as you type, matching commit message, author name, email, hash prefix, or ref name — no request, and immune to going stale across a background refresh
|
|
124
133
|
- <kbd>Enter</kbd> / <kbd>Shift+Enter</kbd> step through matches, wrapping at both ends
|
|
125
|
-
- Full-history search on demand: three `git log` passes (message, author, hash) unioned into one date-ordered list, capped at 500 results, with the query treated as a literal so a typed
|
|
134
|
+
- Full-history search on demand: three `git log` passes (message, author, hash) unioned into one date-ordered list, capped at 500 results, with the query treated as a literal so a typed ( or . is not a regex
|
|
126
135
|
- A match below the loaded window pulls the graph down to it
|
|
127
136
|
|
|
128
137
|
### Working tree
|
|
@@ -155,9 +164,9 @@ First public release.
|
|
|
155
164
|
|
|
156
165
|
### Security
|
|
157
166
|
|
|
158
|
-
- Server binds
|
|
159
|
-
-
|
|
160
|
-
- WebSocket
|
|
167
|
+
- Server binds 127.0.0.1 only
|
|
168
|
+
- Host header pinned to loopback names, closing DNS-rebinding access
|
|
169
|
+
- WebSocket Origin checked before handing out a PTY
|
|
161
170
|
- Repositories are only reachable once registered in `~/.config/megit/config.json`
|
|
162
171
|
- Client-supplied revs whitelisted rather than escaped, so a leading-dash rev cannot smuggle in a git option
|
|
163
172
|
- Git invocations cannot block on credential prompts
|
|
@@ -166,7 +175,7 @@ First public release.
|
|
|
166
175
|
|
|
167
176
|
- macOS (arm64, x64): full support, and the only platform verified by hand
|
|
168
177
|
- 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
|
|
169
|
-
- Linux: everything except the terminal —
|
|
178
|
+
- Linux: everything except the terminal — node-pty is an optionalDependency with no Linux prebuild, and the terminal button is hidden when it is unavailable
|
|
170
179
|
|
|
171
180
|
[Unreleased]: https://github.com/vuongvu1/megit/compare/v0.5.0...HEAD
|
|
172
181
|
[0.5.0]: https://github.com/vuongvu1/megit/compare/v0.4.1...v0.5.0
|
|
@@ -174,4 +183,4 @@ First public release.
|
|
|
174
183
|
[0.4.0]: https://github.com/vuongvu1/megit/compare/v0.3.0...v0.4.0
|
|
175
184
|
[0.3.0]: https://github.com/vuongvu1/megit/compare/v0.2.0...v0.3.0
|
|
176
185
|
[0.2.0]: https://github.com/vuongvu1/megit/compare/v0.1.0...v0.2.0
|
|
177
|
-
[0.1.0]: https://github.com/vuongvu1/megit/releases/tag/v0.1.0
|
|
186
|
+
[0.1.0]: https://github.com/vuongvu1/megit/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Nothing leaves your machine: the server binds `127.0.0.1`, shells out to your ow
|
|
|
10
10
|
|
|
11
11
|
## Getting started
|
|
12
12
|
|
|
13
|
-
Requires Node ≥ 22. The package is 1.7 MB, with `
|
|
13
|
+
Requires Node ≥ 22. The package is 1.7 MB, with `ws` as its only runtime dependency and no install scripts.
|
|
14
14
|
|
|
15
15
|
**Run it once, without installing:**
|
|
16
16
|
|
|
@@ -140,7 +140,7 @@ Requires Node ≥ 24 and pnpm — a development-only floor, since the server run
|
|
|
140
140
|
|
|
141
141
|
```bash
|
|
142
142
|
pnpm install
|
|
143
|
-
pnpm dev #
|
|
143
|
+
pnpm dev # API on :4500 + Vite dev server on :4000
|
|
144
144
|
```
|
|
145
145
|
|
|
146
146
|
Production build:
|
|
@@ -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-BC7TJRRI.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};
|