# Git

> Git repository manager — commit history & graph, branches, staging, diff viewer, fetch/pull/push (Fork-style)

| | |
|---|---|
| Version | 2.8.0 |
| Category | Dev Tools |
| Permissions | process.spawn, filesystem.read, filesystem.write |
| Depends on | ai-chat |

# Git

Fork-style Git repository manager: commit history & graph, branches / remotes / tags /
stashes sidebar, working-directory staging & commit, a colored diff viewer, and
fetch / pull / push.

**Type:** plugin with a `main/` backend — see
`.claude/skills/kazuo-create-plugin/references/plugin-examples.md`

Every git command runs in the main process as `spawn('git', argv)` with **no shell**
involved. The renderer never executes git itself.

## Files

### Backend (`main/`) — compiled, and the `.js` is committed

- `main/git-run.ts` — `runGit(repoPath, argv, opts)` → `{ ok, code, stdout, stderr, notFound? }`.
  Imports no Electron and needs no `PluginContext`, so tests `require()` it directly.
- `main/git-http.ts` — `apiGet(url, headers, opts)` for the hosting APIs. Also never rejects.
  `opts.request` is injectable so tests need no TLS certificate — the same reason `gitBin` exists.
- `main/index.ts` — `activate` / `deactivate`, `ensureIsSelf`, and five channels
  (`:ping`, `:run`, `:api`, `:clone`, `:testremote`). Exports `BACKEND_VERSION`. Builds the
  per-provider request headers and the clone credentials itself — the renderer sends a provider
  name and a token, never raw headers, so it cannot have the backend attach an arbitrary header to
  an arbitrary host. `:testremote` (`git ls-remote`, for the clone dialog's Test Connection button)
  shares its auth-building with `:clone` through `buildAuthEnv()`, so the two cannot disagree about
  whether a URL is reachable.

### Renderer — twenty files, one namespace each

No bundler and no module system: each file is an IIFE publishing exactly one `window.KZGit*`.
**The `<script>` order in `widget.html` IS the dependency order** — a file may only use
namespaces declared above it. Calls pointing the other way go through `window.KZGitApp` at call
time. Every file but one stays under 400 lines; see `js/view-clone.js` below.

| File | Namespace | Contents |
|---|---|---|
| `js/git-parse.js` | `KZGitParse` | git text → objects. Pure: no DOM, no bridge, no git. |
| `js/graph.js` | `KZGitGraph` | commits → graph lanes and segments. Pure. |
| `js/branch-tree.js` | `KZGitBranchTree` | branch names → folder tree. Pure. |
| `js/bridge.js` | `KZGitBridge` | The only door to git; detects a dead backend. |
| `js/state.js` | `KZGitState` | `S.tabs[]`, `tab()`, hydrate/persist, shared helpers. |
| `js/git-api.js` | `KZGitApi` | `gitRun`, `refreshAll`, `reloadWorking`, selection. The individual loaders stay private. |
| `js/view-diff.js` | `KZGitViewDiff` | The coloured diff pane. |
| `js/view-sidebar.js` | `KZGitViewSidebar` | Left rail, five sections (Submodules included). |
| `js/view-changes.js` | `KZGitViewChanges` | Staged / Changes columns + commit box. |
| `js/view-history.js` | `KZGitViewHistory` | Commit list + three-tab detail pane. |
| `js/view-tabs.js` | `KZGitViewTabs` | The repository tab strip: chips, scroll arrows, drag-to-reorder. |
| `js/git-host.js` | `KZGitHost` | GitHub/GitLab differences and clone-progress parsing. Pure. |
| `js/accounts.js` | `KZGitAccounts` | Saved accounts, token checks, repository listings. |
| `js/clone-run.js` | `KZGitCloneRun` | Runs one clone. Draws nothing; both callers share it. |
| `js/view-accounts.js` | `KZGitViewAccounts` | The Accounts window: list on the left, tabs on the right. |
| `js/view-account-add.js` | `KZGitViewAccountAdd` | The seven service tiles, the sign-in form, Update token. |
| `js/view-account-repos.js` | `KZGitViewAccountRepos` | The Repositories tab: grouped by owner, Clone per row. |
| `js/view-clone.js` | `KZGitViewClone` | The + menu, the repository browser, and the clone confirm screen (protocol, parent folder, name, Test Connection). 457 lines — the one file over the 400-line target, see the file's own header comment. |
| `js/actions-repo.js` | `KZGitActRepo` | Repository selection, fetch/pull/push. |
| `js/actions-branch.js` | `KZGitActBranch` | Branches, tags, stashes. |
| `js/actions-stage.js` | `KZGitActStage` | Stage / unstage / discard / commit. |
| `js/widget.js` | `KZGitApp` | Shell: boot, `render()`, click dispatch, settings overlay. |

`settings.html` / `js/settings.js` mirror the widget's settings overlay for the Plugin Manager
screen and the pop-out window (intentionally duplicated — each plugin HTML file is its own sandbox).

## Build

**After editing anything in `main/`:**

```bash
tsc -p tsconfig.plugins.json      # from the repository root
```

Commit the generated `main/*.js` and `main/*.js.map` — the host loads the `.js`.

Forgetting this is silent: the host keeps using the previous build with no warning. The guard is
`BACKEND_VERSION` in `main/index.ts` versus `EXPECTED_BACKEND_VERSION` in `js/bridge.js` — a
mismatch shows a "rebuild the plugin" screen instead of failing somewhere confusing later.

**Bump `version` in `manifest.json` in any release that changes `js/`.** The installed copy under
`userData/plugins/` is refreshed by `syncInstalledMainFolders()` only when the library declares a
`mainEntry` the installed copy lacks, or a higher version — and it runs only at app startup. Without
a bump, renderer changes never reach users. During development: restart the app; toggling the plugin
off and on is not enough.

## Test

```bash
cd plugins-library/git-manager
node --test test/*.test.js          # 494 tests, about 30-40 seconds
```

Refresh the recorded git output (only when the sample repo changes, or a new command is added):

```bash
node test/fixtures/capture.js
```

- `test/fixtures/make-repo.js` builds a deterministic repository — pinned dates, identity and
  config, isolated from the machine's gitconfig. Two runs produce identical SHAs.
- `test/fixtures/golden/` holds real git output. Its `.gitattributes` sets `* -text`: without it,
  `core.autocrlf=true` (the Git for Windows default) rewrites every LF on checkout and the parsers
  receive input no git ever produced.
- `test/widget-harness.js` loads the real `widget.html` and every `js/` file into jsdom behind
  a fake bridge, and documents every place jsdom differs from the webview.
- `test/load-renderer.js` loads a pure renderer file with `vm` into a sandbox with **no `document`**.

Characterization tests are labelled `[LOCK]` (must stay green, body must not be edited) or
`[CHANGES IN Pn]` (that phase changes it deliberately, with the reason).

## Default settings

- `openTabs` — default: `[]` — repository path per open tab, in order. **Source of truth.**
- `activeTabIndex` — default: `0`
- `repoPath` — default: `""` — always equals `openTabs[activeTabIndex]`. Kept so `js/settings.js`
  can go on reading and writing one path without knowing tabs exist.
- `accounts` — default: `[]` — saved GitHub/GitLab accounts. Each holds a personal access token
  IN CLEAR: this app has no safeStorage, and file-storage keeps its Google refresh token the same
  way. The token is never shown again after saving, and never reaches a log, a status message,
  the DOM or a git argument.
- `recentRepos` — default: `[]` — most-recently-opened repositories
- `logLimit` — default: `1000` — max commits loaded into the history list
- `showAllBranches` — default: `true` — `--branches --tags --remotes` vs the current branch only

Settings from before `openTabs` existed hold only `repoPath`; that becomes the single tab on first
load after upgrading.

## Architecture notes

- **argv arrays, never a shell.** Branch names and paths containing spaces, `'`, `&`, `;` or `$`
  are ordinary data. There is no quoting layer because there is nothing to quote for.
- **`exec`'s 5MB cap is gone.** `git show` of a large commit used to fail outright; chunks are
  collected and joined at close. A 256MB ceiling remains as a backstop — this runs in the main
  process, so an unbounded read of a multi-gigabyte blob would take the whole app down rather than
  failing one command.
- **`runGit` never rejects** — every path resolves `{ ok, … }` and callers branch on `ok`. A missing
  git binary sets `notFound` rather than being recognised by matching an error string.
- **One transport, no fallback.** A backend that is not answering gets an explicit screen with a
  Retry button and one of four distinct explanations, never a blank widget.
- **Tab-shaped state.** `S.tabs[]`, one entry per open repository, shown as a tab strip. An empty
  list is legal — the strip hides and the welcome screen takes the pane — but `tab()` still never
  returns undefined, because 24 of its call sites dereference a property straight away — it answers
  with an empty placeholder tab instead.
- **Wholesale re-render.** Everything derives from the active tab; actions mutate the repository,
  reload the affected slice and repaint.
- **Virtualised history.** Only the rows on screen exist in the DOM, so a ten thousand commit
  repository scrolls without stutter. Row height is fixed in both CSS and JS, and a test asserts the
  two agree — drift shows up as rows sliding away from the scroll position.
- **Graph lanes are pure and cached.** js/graph.js reproduces `git log --graph` row for row; the
  layout is computed once per commit list and reused across scrolls.
- **The graph column may never be shorter than a row.** Its SVG carries a `viewBox` exactly `ROW_H`
  tall, but the CSS height resolves against `.commit-row`'s content box — give that row vertical
  padding or a border and the browser silently scales the whole drawing down and centres it, so the
  lines stop short of both edges and the graph breaks between rows while the markup stays correct.
  jsdom computes no layout, so a stylesheet test pins it instead.

## Open repository — via the shared in-app file picker

"Open repository" calls `window['kazuo-ai'].openFileDialog({ properties: ['openDirectory'] })`, which
opens the host's shared in-app file picker (`FilePickerDialog`, styled like Windows File Explorer and
identical on desktop and the Web Dashboard — see `.claude/features/shared-in-app-dialogs.md`). If
file-explorer is not installed or enabled, the picker itself offers a manual path-entry fallback.

## Known limitations

- No merge / rebase / cherry-pick / reset UI, no blame, no line-level staging.
- Submodules are read-only: list, status and open-as-tab. No init / update / sync, and no recursive
  nesting.
- Tabs have no rename, context menu, keyboard shortcuts, or drag between windows.
- Accounts cover seven services by personal access token — no OAuth device flow, no
  key management. Gitea, Bitbucket and Bitbucket Server were written from their
  documentation and have never been tried against a real server; their tiles say so.
  No creating repositories, pull requests or issues.
- Cloning needs **git 2.31 or newer**: the token is passed through GIT_CONFIG_* environment
  variables so it stays out of argv (readable by other processes) and out of .git/config (written to
  disk). The consequence is that a freshly cloned private repository can only fetch and push if the
  machine already has a credential stored — the widget says so when the clone finishes.
- Untracked files show a placeholder instead of a synthetic diff until staged.
- Push/pull use the default upstream; no per-remote/-branch picker yet.
- No progress streaming channel; no `gitPath` setting.
- The commit filter searches only the commits already loaded (`logLimit`), not the whole history.
- Two known defects are deliberately preserved and locked by tests — see
  `plans/2026-08-09-git-manager-fork-parity/notes-for-m1.md`. The third, the phantom "origin" remote
  row, was fixed in M1.
- Running under the web dashboard is **unverified**: the backend channels are not blocked there, but
  nobody has tried it.

## Requirements

The `git` CLI must be installed and on `PATH` — the PATH of the **main process**, which is not
necessarily the one your terminal has. If it cannot be found the widget says so explicitly.
