megit-app 0.1.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 +88 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/bin/megit.js +54 -0
- package/dist/assets/DiffView-CJb6qgum.js +143 -0
- package/dist/assets/DiffView-DhpiFlgV.css +1 -0
- package/dist/assets/TerminalPanel-BrP-ENHg.css +1 -0
- package/dist/assets/TerminalPanel-Cwtdgirw.js +37 -0
- package/dist/assets/index-CfQ1U4Ld.js +249 -0
- package/dist/assets/index-DZyh3SKi.css +1 -0
- package/dist/index.html +17 -0
- package/dist/logo.svg +7 -0
- package/dist-server/avatars.js +83 -0
- package/dist-server/config.js +24 -0
- package/dist-server/index.js +805 -0
- package/dist-server/parse.js +144 -0
- package/dist-server/term.js +113 -0
- package/dist-server/watch.js +104 -0
- package/package.json +69 -0
- package/scripts/fix-pty-perms.mjs +21 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
|
|
6
|
+
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). While the
|
|
7
|
+
version is below 1.0.0, the config file format (`~/.config/megit/config.json`), the CLI
|
|
8
|
+
surface, and the HTTP API may change in any minor release.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.1.0] - 2026-07-28
|
|
13
|
+
|
|
14
|
+
First public release.
|
|
15
|
+
|
|
16
|
+
### Graph
|
|
17
|
+
|
|
18
|
+
- Commit graph with per-branch lanes and colours, greedy top-down lane assignment, and reserved leftmost lanes so WIP and stash connectors get straight runs into HEAD
|
|
19
|
+
- The checked-out branch's path is drawn thicker and highlighted
|
|
20
|
+
- Merge edges bulge around the lanes they cross rather than cutting through them
|
|
21
|
+
- Commits page in 200 at a time, loading more on demand
|
|
22
|
+
- Stash entries render as their own rows, dot-connected to the commit they were taken from
|
|
23
|
+
- Sticky WIP row at the top of the list whenever the worktree is dirty
|
|
24
|
+
- Resizable ref / graph / message columns, with widths persisted
|
|
25
|
+
- Gravatar avatars with initials fallback
|
|
26
|
+
|
|
27
|
+
### Diffs
|
|
28
|
+
|
|
29
|
+
- Unified and split (side-by-side) views
|
|
30
|
+
- Syntax highlighting and word-level intra-line highlighting
|
|
31
|
+
- Collapsed context expandable a hunk at a time
|
|
32
|
+
- Per-file "Viewed" checkbox
|
|
33
|
+
- Merge commits diff against their first parent
|
|
34
|
+
- Images diff visually instead of as binary noise
|
|
35
|
+
- Untracked files diff via `git diff --no-index`
|
|
36
|
+
|
|
37
|
+
### Search
|
|
38
|
+
|
|
39
|
+
- <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
|
|
40
|
+
- <kbd>Enter</kbd> / <kbd>Shift+Enter</kbd> step through matches, wrapping at both ends
|
|
41
|
+
- 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
|
|
42
|
+
- A match below the loaded window pulls the graph down to it
|
|
43
|
+
|
|
44
|
+
### Working tree
|
|
45
|
+
|
|
46
|
+
- Stage, unstage, and discard individual files
|
|
47
|
+
- Commit message composer with staged / unstaged sections and counts
|
|
48
|
+
- Amend the last commit's message
|
|
49
|
+
- Non-ASCII paths handled correctly (`core.quotePath=false`, NUL-delimited output)
|
|
50
|
+
|
|
51
|
+
### Git operations
|
|
52
|
+
|
|
53
|
+
- Toolbar: pull (fast-forward only), push, create branch, stash all, pop latest stash, undo last commit (soft reset)
|
|
54
|
+
- Ref chips: checkout, create branch here, rename, delete, set upstream, merge, rebase, delete tag, copy name, copy GitHub link
|
|
55
|
+
- Commit rows: checkout, cherry-pick, revert, reset (soft / mixed / hard), copy hash, copy GitHub link
|
|
56
|
+
- Checkout auto-stashes a dirty worktree first
|
|
57
|
+
- Destructive actions are marked, and hidden where they would be meaningless
|
|
58
|
+
|
|
59
|
+
### Shell
|
|
60
|
+
|
|
61
|
+
- Full PTY terminal (xterm.js + node-pty) opened in the active repository, lazy-loaded so it costs nothing until used
|
|
62
|
+
- Sessions survive panel hides and tab switches, with a replay buffer on reattach
|
|
63
|
+
|
|
64
|
+
### App
|
|
65
|
+
|
|
66
|
+
- Multiple repositories in draggable tabs
|
|
67
|
+
- Light and dark themes
|
|
68
|
+
- Auto-refresh: the server watches each open repo (`fs.watch`, filtered and debounced) and pushes updates over SSE
|
|
69
|
+
- Keyboard navigation through rows, plus shortcuts for refresh, terminal, theme, and commit
|
|
70
|
+
- Toast notifications for errors
|
|
71
|
+
|
|
72
|
+
### Security
|
|
73
|
+
|
|
74
|
+
- Server binds `127.0.0.1` only
|
|
75
|
+
- `Host` header pinned to loopback names, closing DNS-rebinding access
|
|
76
|
+
- WebSocket `Origin` checked before handing out a PTY
|
|
77
|
+
- Repositories are only reachable once registered in `~/.config/megit/config.json`
|
|
78
|
+
- Client-supplied revs whitelisted rather than escaped, so a leading-dash rev cannot smuggle in a git option
|
|
79
|
+
- Git invocations cannot block on credential prompts
|
|
80
|
+
|
|
81
|
+
### Platforms
|
|
82
|
+
|
|
83
|
+
- macOS (arm64, x64): full support, and the only platform verified by hand
|
|
84
|
+
- 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
|
|
85
|
+
- Linux: everything except the terminal — `node-pty` is an `optionalDependency` with no Linux prebuild, and the terminal button is hidden when it is unavailable
|
|
86
|
+
|
|
87
|
+
[Unreleased]: https://github.com/vuongvu1/megit/compare/v0.1.0...HEAD
|
|
88
|
+
[0.1.0]: https://github.com/vuongvu1/megit/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hoang Vuong Vu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# <img src="public/logo.svg" width="28"/> megit
|
|
2
|
+
|
|
3
|
+
A git repository viewer that runs in your browser. Commit graph with real branch lanes, diffs including your uncommitted work, stashes, staging, search, and a shell — pointed at as many local repos as you like, each in its own tab.
|
|
4
|
+
|
|
5
|
+
It writes, too: stage/unstage/discard, commit and amend, branch and tag create/delete, stash push/pop/drop, checkout (with auto-stash when the worktree is dirty), plus revert, reset, cherry-pick, merge, rebase, pull and push.
|
|
6
|
+
|
|
7
|
+
Nothing leaves your machine: the server binds `127.0.0.1`, shells out to your own `git`, and has no account, telemetry, or network dependency beyond optional Gravatar avatars.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Requires Node ≥ 24.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx megit-app # reopen your last session
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Installed globally (`npm i -g megit-app`), the command is just `megit`.
|
|
20
|
+
|
|
21
|
+
`PORT` picks the port (default 3411). The server opens your browser at it.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
### Commit graph with lanes
|
|
26
|
+
|
|
27
|
+
Branches get their own colour and lane. The checked-out branch's path is drawn thicker so you can follow it at a glance, and merges bulge around the lanes they cross instead of cutting through them. Commits page in 200 at a time and load more on demand, so opening a repository never waits on the full history.
|
|
28
|
+
|
|
29
|
+
Ref chips sit in their own resizable column to the left — local branches, remotes, and tags — and the graph column and message column can be dragged to whatever widths suit the repo.
|
|
30
|
+
|
|
31
|
+
### Light and dark themes
|
|
32
|
+
|
|
33
|
+
Toggle with the switch in the toolbar or <kbd>⌘</kbd><kbd>⇧</kbd><kbd>0</kbd>. The choice persists.
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
### Uncommitted work is part of the graph
|
|
38
|
+
|
|
39
|
+
A sticky WIP row sits at the top of the list whenever the worktree is dirty, connected into HEAD like any other node. Click it to stage, unstage, or discard individual files, write a message, and commit — staged and unstaged changes are separate collapsible sections with counts.
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
Stashes appear as their own rows, attached to the commit they were taken from with a dotted connector, and can be popped, deleted, or retitled in place.
|
|
44
|
+
|
|
45
|
+
### Diffs
|
|
46
|
+
|
|
47
|
+
Click a commit to see its changed files, then a file to see the diff. Syntax highlighting, word-level intra-line highlighting, collapsed context you can expand a hunk at a time, and a per-file "Viewed" checkbox.
|
|
48
|
+
|
|
49
|
+
**Unified:**
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
**Split**, side by side:
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
Merge commits diff against their first parent. Images diff visually rather than as binary noise. Untracked files diff too, via `git diff --no-index`.
|
|
58
|
+
|
|
59
|
+
### Commit detail
|
|
60
|
+
|
|
61
|
+
Author and committer are shown separately when they differ — including the dates, which is the bit most tools hide. The changed-file list toggles between a flat path list and a directory tree.
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
### Search
|
|
66
|
+
|
|
67
|
+
<kbd>⌘</kbd><kbd>F</kbd> opens a find bar that filters the rows already loaded, as you type — matching commit message, author name, email, hash prefix, or ref name. That costs no request and can't go stale when the graph refreshes underneath you. <kbd>↵</kbd> and <kbd>⇧</kbd><kbd>↵</kbd> walk the matches, wrapping at the ends the way a find bar should.
|
|
68
|
+
|
|
69
|
+
If what you want is further back than the rows you've loaded, the globe button re-runs the same query as a full-history `git log` search. Because git ANDs its commit-limiting options, "message OR author OR hash" is three searches unioned into one date-ordered list, capped at 500 results — the counter shows `12 of 340 · all`, with a `+` when the cap truncated it. A match below the loaded window pulls the graph down to it.
|
|
70
|
+
|
|
71
|
+
### A real shell, in the repo
|
|
72
|
+
|
|
73
|
+
<kbd>⌘</kbd><kbd>J</kbd> opens a full PTY already `cd`'d into the active repository — your shell, your prompt, your aliases. It survives panel hides and tab switches, and <kbd>⌘</kbd><kbd>K</kbd> clears it. xterm.js is lazy-loaded, so it costs nothing until you open it.
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
|
|
77
|
+
### Git operations
|
|
78
|
+
|
|
79
|
+
megit started as a viewer, but the common operations are here:
|
|
80
|
+
|
|
81
|
+
- **Toolbar** — pull (fast-forward only), push, create branch, stash all, pop latest stash, undo last commit (soft reset, keeps changes staged)
|
|
82
|
+
- **Ref chips** — checkout, create branch here, rename, delete, set upstream, merge, rebase, delete tag, copy name, copy GitHub link
|
|
83
|
+
- **Commit rows** — checkout, cherry-pick, revert, reset (soft / mixed / hard), copy hash, copy GitHub link
|
|
84
|
+
- **Files** — stage, unstage, discard, amend the last commit's message
|
|
85
|
+
|
|
86
|
+
Checkout auto-stashes a dirty worktree first. Destructive items are marked as such and are hidden where they'd be meaningless.
|
|
87
|
+
|
|
88
|
+
### Auto-refresh
|
|
89
|
+
|
|
90
|
+
The server watches each open repository (`fs.watch`, filtered and debounced) and pushes changes to the browser over SSE. Commit in your terminal and the graph updates within about a second. <kbd>r</kbd> forces a refresh if you want one.
|
|
91
|
+
|
|
92
|
+
### Keyboard
|
|
93
|
+
|
|
94
|
+
| Key | Action |
|
|
95
|
+
| -------------------------------------------------------- | ---------------------------------- |
|
|
96
|
+
| <kbd>↑</kbd> <kbd>↓</kbd> <kbd>Home</kbd> <kbd>End</kbd> | move through rows |
|
|
97
|
+
| <kbd>⌘</kbd><kbd>F</kbd> | search commits |
|
|
98
|
+
| <kbd>↵</kbd> / <kbd>⇧</kbd><kbd>↵</kbd> | next / previous match |
|
|
99
|
+
| <kbd>r</kbd> | refresh |
|
|
100
|
+
| <kbd>⌘</kbd><kbd>J</kbd> | toggle terminal |
|
|
101
|
+
| <kbd>⌘</kbd><kbd>K</kbd> | clear terminal |
|
|
102
|
+
| <kbd>⌘</kbd><kbd>⇧</kbd><kbd>0</kbd> | toggle theme |
|
|
103
|
+
| <kbd>⌘</kbd><kbd>↵</kbd> | commit |
|
|
104
|
+
| <kbd>Esc</kbd> | close search or menu / cancel edit |
|
|
105
|
+
|
|
106
|
+
## Platform support
|
|
107
|
+
|
|
108
|
+
| Platform | Status |
|
|
109
|
+
| -------------------- | ----------------------------------------- |
|
|
110
|
+
| macOS (arm64, x64) | full |
|
|
111
|
+
| Windows (arm64, x64) | untested on real hardware; auto-refresh unverified |
|
|
112
|
+
| Linux | everything except the built-in terminal |
|
|
113
|
+
|
|
114
|
+
The terminal needs [node-pty](https://github.com/microsoft/node-pty), which ships prebuilt binaries for macOS and Windows only. It is an `optionalDependency`: on Linux the install either compiles it from source (needs python3 and a C++ toolchain) or skips it, and megit hides the terminal button. Nothing else is affected.
|
|
115
|
+
|
|
116
|
+
On Windows, the watcher integration tests crash the test worker outright, so they are skipped there and auto-refresh is not exercised by CI. Everything else in the suite runs. If you use megit on Windows, please report whether the graph updates on its own after a commit — that is the part we cannot currently verify.
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
The list of open repositories lives in `~/.config/megit/config.json`. Repositories are only reachable through the API if they are registered there, so pointing megit at a repo is always an explicit act.
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
Requires pnpm.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pnpm install
|
|
128
|
+
pnpm dev # Express API on :4500 + Vite dev server on :4000
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Production build:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pnpm build # vite → dist/
|
|
135
|
+
pnpm build:server # tsc → dist-server/ (only needed for publishing)
|
|
136
|
+
pnpm start # serves dist/ + API on http://127.0.0.1:4500
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Ports come from `PORT` (API, default 3411) and `UI_PORT` (Vite dev server, default 5173); the dev/start scripts pin 4500/4000.
|
|
140
|
+
|
|
141
|
+
In development the server runs its TypeScript directly via Node's native type-stripping — no build step. That does not work for a published package, because Node refuses to strip types under `node_modules`, so `pnpm build:server` compiles `server/` to `dist-server/` at publish time.
|
|
142
|
+
|
|
143
|
+
`scripts/make-test-repo.sh` generates `test-repo/` — a throwaway fixture with interleaved branches, merges, stashes and a dirty worktree, used for manual testing and for the screenshots above.
|
|
144
|
+
|
|
145
|
+
[`docs/architecture.md`](docs/architecture.md) explains how the two halves fit together and why the graph layout lives in a pure module. [CONTRIBUTING.md](CONTRIBUTING.md) has the setup and the house rules; [SECURITY.md](SECURITY.md) has the threat model.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pnpm test # vitest — parsers, lane layout, watcher, menus
|
|
149
|
+
npx tsc --noEmit # typecheck
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
package/bin/megit.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// megit CLI — start the local server, open the browser at it, and register the
|
|
3
|
+
// repo path given as an argument (if any) by POSTing to our own /api/repos, so
|
|
4
|
+
// path validation stays in one place.
|
|
5
|
+
import { execFile } from 'node:child_process'
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
import { resolve } from 'node:path'
|
|
8
|
+
|
|
9
|
+
const arg = process.argv[2]
|
|
10
|
+
|
|
11
|
+
// handled before importing the server, so --help never binds a port
|
|
12
|
+
if (arg === '-h' || arg === '--help') {
|
|
13
|
+
console.log(`megit — git repository viewer in the browser
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
megit [repo-path]
|
|
17
|
+
|
|
18
|
+
Arguments:
|
|
19
|
+
repo-path open this repository in a tab (default: reopen last session)
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-h, --help show this message
|
|
23
|
+
-v, --version print the version
|
|
24
|
+
|
|
25
|
+
Environment:
|
|
26
|
+
PORT port to listen on (default 3411)`)
|
|
27
|
+
process.exit(0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (arg === '-v' || arg === '--version') {
|
|
31
|
+
console.log(createRequire(import.meta.url)('../package.json').version)
|
|
32
|
+
process.exit(0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const port = Number(process.env.PORT) || 3411
|
|
36
|
+
const url = `http://127.0.0.1:${port}`
|
|
37
|
+
|
|
38
|
+
const { server } = await import('../dist-server/index.js')
|
|
39
|
+
if (!server.listening) await new Promise(r => server.once('listening', r))
|
|
40
|
+
|
|
41
|
+
if (arg) {
|
|
42
|
+
await fetch(`${url}/api/repos`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'content-type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({ path: resolve(arg) }),
|
|
46
|
+
})
|
|
47
|
+
.then(async r => { if (!r.ok) console.error(`megit: ${(await r.json()).error}`) })
|
|
48
|
+
.catch(() => {})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ponytail: no `open` dependency — three platform names cover what it does.
|
|
52
|
+
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open'
|
|
53
|
+
execFile(opener, [url], () => {}) // exit code ignored: explorer.exe returns 1 on success
|
|
54
|
+
console.log(`megit → ${url}`)
|