megit-app 0.4.0 → 0.5.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,48 @@ surface, and the HTTP API may change in any minor release.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.5.0] - 2026-08-08
13
+
14
+ ### Added
15
+
16
+ - Merge conflicts can be resolved inside megit. A merge, rebase, cherry-pick or revert that
17
+ stops on a conflict now shows a banner above the graph naming the operation and the number of
18
+ conflicts left, with Abort (confirmed first) and Continue (enabled once nothing is unmerged).
19
+ Conflicted files get their own "Merge Changes" section in the commit panel — no Stage and no
20
+ Discard, because staging a file that still holds `<<<<<<<` commits the markers — and opening
21
+ one replaces the diff with a picker: every conflict block renders ours above theirs with Use
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.
33
+
34
+ ### Changed
35
+
36
+ - The add-repo dialog is now two panes: recent repositories on the left, the current
37
+ directory's path header and folder list on the right. Recent entries show just the
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.
40
+
41
+ ### Fixed
42
+
43
+ - "Stage all" and "Discard all" no longer touch unmerged paths. `git add -A` swept conflicted
44
+ files — markers and all — into the index, and `git restore --worktree -- .` failed outright on
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.
47
+
48
+ ## [0.4.1] - 2026-08-05
49
+
50
+ ### Fixed
51
+
52
+ - <kbd>Esc</kbd> closes the add-repo dialog. The dialog had no keyboard dismissal at all, so the only way out was the ✕ or a click on the backdrop.
53
+
12
54
  ## [0.4.0] - 2026-08-05
13
55
 
14
56
  ### Added
@@ -126,7 +168,10 @@ First public release.
126
168
  - 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
127
169
  - Linux: everything except the terminal — `node-pty` is an `optionalDependency` with no Linux prebuild, and the terminal button is hidden when it is unavailable
128
170
 
129
- [Unreleased]: https://github.com/vuongvu1/megit/compare/v0.3.0...HEAD
171
+ [Unreleased]: https://github.com/vuongvu1/megit/compare/v0.5.0...HEAD
172
+ [0.5.0]: https://github.com/vuongvu1/megit/compare/v0.4.1...v0.5.0
173
+ [0.4.1]: https://github.com/vuongvu1/megit/compare/v0.4.0...v0.4.1
174
+ [0.4.0]: https://github.com/vuongvu1/megit/compare/v0.3.0...v0.4.0
130
175
  [0.3.0]: https://github.com/vuongvu1/megit/compare/v0.2.0...v0.3.0
131
176
  [0.2.0]: https://github.com/vuongvu1/megit/compare/v0.1.0...v0.2.0
132
177
  [0.1.0]: https://github.com/vuongvu1/megit/releases/tag/v0.1.0
package/README.md CHANGED
@@ -8,19 +8,28 @@ Nothing leaves your machine: the server binds `127.0.0.1`, shells out to your ow
8
8
 
9
9
  ![megit — commit graph with branch lanes, stash rows and the sticky WIP row](docs/graph-dark.png)
10
10
 
11
- ## Install
11
+ ## Getting started
12
12
 
13
- Requires Node ≥ 22.
13
+ Requires Node ≥ 22. The package is 1.7 MB, with `express` and `ws` as its only runtime dependencies.
14
+
15
+ **Run it once, without installing:**
14
16
 
15
17
  ```bash
16
18
  npx megit-app
17
19
  ```
18
20
 
19
- The package is `megit-app` `npx megit` is an unrelated package by someone else, so keep the `-app`.
21
+ **Or install it, and run `megit` from anywhere:**
22
+
23
+ ```bash
24
+ npm i -g megit-app
25
+ megit
26
+ ```
27
+
28
+ Upgrade later with `npm i -g megit-app@latest`.
20
29
 
21
- Installed globally (`npm i -g megit-app`), the command is just `megit`.
30
+ Either way the server starts on port 3411 — set `PORT` to change that and opens your browser at it.
22
31
 
23
- `PORT` picks the port (default 3411). The server opens your browser at it.
32
+ <sub>The package is `megit-app`; `megit` on npm is unrelated, so keep the `-app`. The installed command is still `megit`.</sub>
24
33
 
25
34
  ## Features
26
35
 
@@ -0,0 +1 @@
1
+ import{a as e,i as t,n,r}from"./index-Bfrt9LxG.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};