just-git 1.2.0 → 1.2.3

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/README.md CHANGED
@@ -22,7 +22,15 @@ npm install just-git
22
22
  import { createGit, MemoryFileSystem } from "just-git";
23
23
 
24
24
  const fs = new MemoryFileSystem();
25
- const git = createGit({ identity: { name: "Alice", email: "alice@example.com" } });
25
+ const git = createGit({
26
+ identity: { name: "Alice", email: "alice@example.com" },
27
+ credentials: (url) => ({ type: "bearer", token: process.env.GITHUB_TOKEN! }),
28
+ hooks: {
29
+ beforeCommand: ({ command }) => {
30
+ if (command === "push") return { reject: true, message: "push requires approval" };
31
+ },
32
+ },
33
+ });
26
34
 
27
35
  await git.exec("git init", { fs, cwd: "/repo" });
28
36
  await git.exec("git add .", { fs, cwd: "/repo" });
@@ -30,9 +38,7 @@ await git.exec('git commit -m "initial commit"', { fs, cwd: "/repo" });
30
38
  await git.exec("git log --oneline", { fs, cwd: "/repo" });
31
39
  ```
32
40
 
33
- `MemoryFileSystem` is a minimal in-memory filesystem included with just-git. You can also provide your own implementation of the `FileSystem` interface (e.g. wrapping `node:fs/promises`). Tokenization handles single and double quotes. Pass `env` as a plain object when needed (e.g. `GIT_AUTHOR_NAME`).
34
-
35
- For a full virtual shell with file I/O, pipes, and scripting, pair with [just-bash](https://github.com/vercel-labs/just-bash):
41
+ `MemoryFileSystem` is a minimal in-memory filesystem for standalone use. Tokenization handles single and double quotes; pass `env` as a plain object when needed (e.g. `GIT_AUTHOR_NAME`). The `FileSystem` interface is built around [just-bash](https://github.com/vercel-labs/just-bash)'s implementations. For anything beyond bare git commands, it's recommended to use just-git as a custom command in just-bash:
36
42
 
37
43
  ```ts
38
44
  import { Bash } from "just-bash";
@@ -52,10 +58,10 @@ await bash.exec("git add . && git commit -m 'initial commit'");
52
58
  Stand up a git server with built-in storage (SQLite or [PostgreSQL](docs/SERVER.md#pgstorage)), branch protection, and push hooks:
53
59
 
54
60
  ```ts
55
- import { createGitServer, SqliteStorage } from "just-git/server";
61
+ import { createGitServer, BunSqliteStorage } from "just-git/server";
56
62
  import { Database } from "bun:sqlite";
57
63
 
58
- const storage = new SqliteStorage(new Database("repos.sqlite"));
64
+ const storage = new BunSqliteStorage(new Database("repos.sqlite"));
59
65
 
60
66
  const server = createGitServer({
61
67
  resolveRepo: (path) => storage.repo(path),
@@ -74,252 +80,79 @@ Bun.serve({ fetch: server.fetch });
74
80
  // git clone http://localhost:3000/my-repo ← works with real git
75
81
  ```
76
82
 
77
- Uses web-standard `Request`/`Response` works with Bun, Hono, Cloudflare Workers, or any fetch-compatible runtime. For Node.js, use `toNodeHandler(server)` with `http.createServer` and `wrapBetterSqlite3` for `better-sqlite3`. See [SERVER.md](docs/SERVER.md) for the full API.
83
+ Uses web-standard `Request`/`Response`. Works with Bun, Hono, Cloudflare Workers, or any fetch-compatible runtime. For Node.js, use `toNodeHandler(server)` with `http.createServer` and `BetterSqlite3Storage` for `better-sqlite3`. See [SERVER.md](docs/SERVER.md) for the full API.
78
84
 
79
- ## Options
85
+ ## createGit options
80
86
 
81
87
  `createGit(options?)` accepts:
82
88
 
83
- | Option | Description |
84
- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
85
- | `identity` | Author/committer override. With `locked: true`, always wins over env vars and git config. Without `locked`, acts as a fallback. |
86
- | `credentials` | `(url) => HttpAuth \| null` callback for Smart HTTP transport auth. |
87
- | `disabled` | `GitCommandName[]` of subcommands to block (e.g. `["push", "rebase"]`). |
88
- | `network` | `{ allowed?: string[], fetch?: FetchFunction }` to restrict HTTP access and/or provide a custom `fetch`. `allowed` accepts hostnames (`"github.com"`) or URL prefixes (`"https://github.com/myorg/"`). Set to `false` to block all network access. |
89
- | `config` | `{ locked?, defaults? }` config overrides. `locked` values always win over `.git/config`; `defaults` supply fallbacks when a key is absent. Keys are dotted config names (e.g. `"push.default"`, `"merge.ff"`). See [Config overrides](#config-overrides). |
90
- | `hooks` | `GitHooks` config object with named callback properties. See [Hooks](#hooks). |
91
- | `resolveRemote` | `(url) => GitRepo \| null` callback for cross-VFS remote resolution. See [Multi-agent collaboration](#multi-agent-collaboration). |
92
-
93
- ```ts
94
- const git = createGit({
95
- identity: { name: "Agent Bot", email: "bot@company.com", locked: true },
96
- credentials: async (url) => ({ type: "bearer", token: "ghp_..." }),
97
- disabled: ["rebase"],
98
- network: false, // no HTTP access
99
- config: {
100
- locked: { "push.default": "nothing" },
101
- defaults: { "merge.ff": "only" },
102
- },
103
- });
104
- ```
89
+ | Option | Description |
90
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `identity` | Author/committer override. With `locked: true`, always wins over env vars and git config. Without `locked`, acts as a fallback. |
92
+ | `credentials` | `(url) => HttpAuth \| null` callback for Smart HTTP transport auth. |
93
+ | `disabled` | `GitCommandName[]` of subcommands to block (e.g. `["push", "rebase"]`). |
94
+ | `network` | `{ allowed?: string[], fetch? }` to restrict HTTP access. Set to `false` to block all network access. |
95
+ | `config` | `{ locked?, defaults? }` config overrides. `locked` values always win over `.git/config`; `defaults` supply fallbacks when a key is absent. |
96
+ | `hooks` | Lifecycle hooks for pre/post command interception, commit gating, message enforcement, and audit logging. |
97
+ | `resolveRemote` | `(url) => GitRepo \| null` callback for cross-VFS remote resolution (multi-agent setups). |
105
98
 
106
- ## Hooks
99
+ See [CLIENT.md](docs/CLIENT.md) for detailed usage, config overrides, and multi-agent collaboration.
107
100
 
108
- Hooks fire at specific points inside command execution. Specified as a `GitHooks` config object at construction time. All hook event payloads include `repo: GitRepo`, providing access to the [repo module helpers](src/repo/) inside hooks.
101
+ ## Client hooks
109
102
 
110
- Pre-hooks can reject the operation by returning `{ reject: true, message? }`. Post-hooks are observational return value is ignored.
103
+ Hooks fire at specific points inside command execution. Pre-hooks can reject the operation by returning `{ reject: true, message? }`. Post-hooks are observational. All hook payloads include `repo: GitRepo` for [programmatic repo access](docs/REPO.md).
111
104
 
112
105
  ```ts
113
- import { createGit, type GitHooks } from "just-git";
106
+ import { createGit } from "just-git";
114
107
  import { getChangedFiles } from "just-git/repo";
115
108
 
116
109
  const git = createGit({
117
110
  hooks: {
118
- // Block secrets from being committed
119
111
  preCommit: ({ index }) => {
120
112
  const forbidden = index.entries.filter((e) => /\.(env|pem|key)$/.test(e.path));
121
113
  if (forbidden.length) {
122
114
  return { reject: true, message: `Blocked: ${forbidden.map((e) => e.path).join(", ")}` };
123
115
  }
124
116
  },
125
-
126
- // Enforce conventional commit messages
127
- commitMsg: (event) => {
128
- if (!/^(feat|fix|docs|refactor|test|chore)(\(.+\))?:/.test(event.message)) {
129
- return { reject: true, message: "Commit message must follow conventional commits format" };
130
- }
131
- },
132
-
133
- // Feed agent activity to your UI — with changed file list
134
117
  postCommit: async ({ repo, hash, branch, parents }) => {
135
118
  const files = await getChangedFiles(repo, parents[0] ?? null, hash);
136
119
  onAgentCommit({ hash, branch, changedFiles: files });
137
120
  },
138
-
139
- // Audit log — record every command
140
- afterCommand: ({ command, args, result }) => {
141
- auditLog.push({ command: `git ${command}`, exitCode: result.exitCode });
142
- },
143
-
144
- // Gate pushes on human approval
145
- beforeCommand: async ({ command }) => {
146
- if (command === "push" && !(await getHumanApproval())) {
147
- return { reject: true, message: "Push blocked — awaiting approval." };
148
- }
149
- },
150
121
  },
151
122
  });
152
123
  ```
153
124
 
154
- Use `composeGitHooks()` to combine multiple hook sets:
155
-
156
- ```ts
157
- import { createGit, composeGitHooks } from "just-git";
158
-
159
- const git = createGit({
160
- hooks: composeGitHooks(auditHooks, policyHooks, loggingHooks),
161
- });
162
- ```
125
+ Combine multiple hook sets with `composeGitHooks(auditHooks, policyHooks, loggingHooks)`. See [HOOKS.md](docs/HOOKS.md) for the full type reference and [CLIENT.md](docs/CLIENT.md) for more examples.
163
126
 
164
- Available pre-hooks: `preCommit`, `commitMsg`, `mergeMsg`, `preMergeCommit`, `preCheckout`, `prePush`, `preFetch`, `preClone`, `prePull`, `preRebase`, `preReset`, `preClean`, `preRm`, `preCherryPick`, `preRevert`, `preStash`. Available post-hooks: `postCommit`, `postMerge`, `postCheckout`, `postPush`, `postFetch`, `postClone`, `postPull`, `postReset`, `postClean`, `postRm`, `postCherryPick`, `postRevert`, `postStash`. Low-level events: `onRefUpdate`, `onRefDelete`, `onObjectWrite`. Command-level: `beforeCommand`, `afterCommand`.
127
+ ## Repo module
165
128
 
166
- ## Config overrides
129
+ `just-git/repo` provides programmatic access to git repositories: reading commits, diffing trees, creating objects, and merging, all without going through command execution.
167
130
 
168
- Control git config values at the operator level, without touching `.git/config`. Works like the `identity` option `locked` values always win, `defaults` act as fallbacks.
131
+ Everything operates on `GitRepo`, a minimal `{ objectStore, refStore }` interface shared by the client and server. A `GitRepo` can be backed by a virtual filesystem, SQLite, Postgres, or any custom storage. The same helpers work inside both client-side hooks and server-side hooks, and `createWorktree` lets you spin up a full git client against a database-backed repo.
169
132
 
170
133
  ```ts
171
- const git = createGit({
172
- config: {
173
- locked: {
174
- "push.default": "nothing", // agent must always specify a refspec
175
- "merge.conflictstyle": "diff3", // always show base in conflict markers
176
- },
177
- defaults: {
178
- "pull.rebase": "true", // default to rebase-on-pull (agent can change)
179
- "merge.ff": "only", // default to ff-only (agent can change)
180
- },
181
- },
182
- });
183
- ```
134
+ import { readFileAtCommit, getChangedFiles, mergeTrees } from "just-git/repo";
184
135
 
185
- - **`locked`** — values that take absolute precedence. The agent can run `git config set` (the write succeeds on the VFS), but the locked value always wins on every read. Useful for enforcing policies.
186
- - **`defaults`** — fallback values when a key is absent from `.git/config`. The agent _can_ override these with `git config set`. Useful for sensible defaults without restricting the agent.
187
-
188
- Applied transparently via `getConfigValue()` — all commands respect overrides automatically. Any dotted config key works (e.g. `"merge.ff"`, `"push.default"`, `"pull.rebase"`, `"merge.conflictstyle"`, `"branch.autoSetupMerge"`).
189
-
190
- ## Repo module
191
-
192
- `just-git/repo` provides a high-level API for working with repositories programmatically — reading commits, diffing trees, creating objects, and merging — without going through command execution. This is what you use inside hooks (all hook payloads include `repo: GitRepo`) and anywhere else you need direct repo access.
193
-
194
- ```ts
195
- import {
196
- readCommit,
197
- readFileAtCommit,
198
- getChangedFiles,
199
- resolveRef,
200
- listBranches,
201
- diffTrees,
202
- flattenTree,
203
- createCommit,
204
- writeBlob,
205
- writeTree,
206
- mergeTrees,
207
- checkoutTo,
208
- readonlyRepo,
209
- } from "just-git/repo";
210
-
211
- // Read a file at a specific commit
212
136
  const content = await readFileAtCommit(repo, commitHash, "src/index.ts");
213
-
214
- // Diff two commits
215
137
  const changes = await getChangedFiles(repo, parentHash, commitHash);
216
-
217
- // Merge two branches at the tree level (no worktree needed)
218
138
  const result = await mergeTrees(repo, oursCommit, theirsCommit);
219
- if (!result.clean) console.log("conflicts:", result.conflicts);
220
139
  ```
221
140
 
222
- Also re-exports `PackedObjectStore` and `FileSystemRefStore` for custom storage backends, and `readonlyRepo()` to wrap a repo so all write operations throw.
141
+ See [REPO.md](docs/REPO.md) for the full API, the `GitRepo` interface, and the hybrid worktree pattern.
223
142
 
224
143
  ## Multi-agent collaboration
225
144
 
226
- Multiple agents can work on clones of the same repository in the same process, each with full VFS isolation. The `resolveRemote` option maps remote URLs to `GitRepo` instances (any object/ref store VFS-backed, SQLite, etc.), so clone/fetch/push/pull cross VFS boundaries without any network or shared filesystem.
227
-
228
- ```ts
229
- import { Bash, InMemoryFs } from "just-bash";
230
- import { createGit, findRepo } from "just-git";
231
-
232
- // Origin repo on its own filesystem
233
- const originFs = new InMemoryFs();
234
- const setupBash = new Bash({
235
- fs: originFs,
236
- cwd: "/repo",
237
- customCommands: [
238
- createGit({ identity: { name: "Setup", email: "setup@example.com", locked: true } }),
239
- ],
240
- });
241
- await setupBash.exec("git init");
242
- await setupBash.exec("echo 'hello' > README.md");
243
- await setupBash.exec("git add . && git commit -m 'initial'");
244
-
245
- const alice = new Bash({
246
- fs: new InMemoryFs(),
247
- cwd: "/repo",
248
- customCommands: [
249
- createGit({
250
- identity: { name: "Alice", email: "alice@example.com", locked: true },
251
- resolveRemote: () => findRepo(originFs, "/repo"),
252
- }),
253
- ],
254
- });
145
+ Multiple agents can clone, fetch, push, and pull across isolated in-memory filesystems within the same process via the `resolveRemote` option, without needing a network or shared filesystem. Concurrent pushes are automatically serialized with proper non-fast-forward rejection. See [CLIENT.md](docs/CLIENT.md#multi-agent-collaboration) and [`examples/multi-agent.ts`](examples/multi-agent.ts).
255
146
 
256
- const bob = new Bash({
257
- fs: new InMemoryFs(),
258
- cwd: "/repo",
259
- customCommands: [
260
- createGit({
261
- identity: { name: "Bob", email: "bob@example.com", locked: true },
262
- resolveRemote: () => findRepo(originFs, "/repo"),
263
- }),
264
- ],
265
- });
266
-
267
- await alice.exec("git clone /origin /repo");
268
- await bob.exec("git clone /origin /repo");
269
-
270
- // Alice and Bob work independently, push to origin, fetch each other's changes
271
- ```
147
+ ## Commands
272
148
 
273
- Concurrent pushes to the same remote are automatically serialized if two agents push simultaneously, one succeeds and the other gets a proper non-fast-forward rejection, just like real git.
274
-
275
- See [`examples/multi-agent.ts`](examples/multi-agent.ts) for a full working example with a coordinator agent that merges feature branches.
276
-
277
- ## Command coverage
278
-
279
- See [CLI.md](docs/CLI.md) for full usage details.
280
-
281
- | Command | Flags / options |
282
- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
283
- | `init [<dir>]` | `--bare`, `--initial-branch` |
284
- | `clone <repo> [<dir>]` | `--bare`, `-b <branch>`, `--depth` |
285
- | `blame <file>` | `-L <start>,<end>`, `-l`/`--long`, `-e`/`--show-email`, `-s`/`--suppress`, `-p`/`--porcelain`, `--line-porcelain` |
286
- | `add <paths>` | `.`, `--all`/`-A`, `--update`/`-u`, `--force`/`-f`, `-n`/`--dry-run`, glob pathspecs |
287
- | `rm <paths>` | `--cached`, `-r`, `-f`, `-n`/`--dry-run`, glob pathspecs |
288
- | `mv <src> <dst>` | `-f`, `-n`/`--dry-run`, `-k` |
289
- | `commit` | `-m`, `-F <file>` / `-F -`, `--allow-empty`, `--amend`, `--no-edit`, `-a` |
290
- | `status` | `-s`/`--short`, `--porcelain`, `-b`/`--branch` |
291
- | `log` | `--oneline`, `-n`, `--all`, `--reverse`, `--decorate`, `--graph`, `--format`/`--pretty`, `-p`/`--patch`, `--stat`, `--name-status`, `--name-only`, `--shortstat`, `--numstat`, `A..B`, `A...B`, `-- <path>`, `--author=`, `--grep=`, `--since`/`--after`, `--until`/`--before` |
292
- | `show [<object>]` | Commits (with diff), annotated tags, trees, blobs |
293
- | `diff` | `--cached`/`--staged`, `<commit>`, `<commit> <commit>`, `A..B`, `A...B`, `-- <path>`, `--stat`, `--shortstat`, `--numstat`, `--name-only`, `--name-status` |
294
- | `branch` | `-d`, `-D`, `-m`, `-M`, `-r`, `-a`/`--all`, `-v`/`-vv`, `-u`/`--set-upstream-to` |
295
- | `tag [<name>] [<commit>]` | `-a -m` (annotated), `-d`, `-l <pattern>`, `-f` |
296
- | `switch` | `-c`/`-C` (create/force-create), `--detach`/`-d`, `--orphan`, `-` (previous branch), `--guess`/`--no-guess` |
297
- | `restore` | `-s`/`--source`, `-S`/`--staged`, `-W`/`--worktree`, `-S -W` (both), `--ours`/`--theirs`, pathspec globs |
298
- | `checkout` | `-b`, `-B`, `--orphan`, `--detach`/`-d`, detached HEAD, `-- <paths>`, `--ours`/`--theirs`, pathspec globs |
299
- | `reset [<commit>]` | `-- <paths>`, `--soft`, `--mixed`, `--hard`, pathspec globs |
300
- | `merge <branch>` | `--no-ff`, `--ff-only`, `--squash`, `-m`, `--abort`, `--continue`, conflict markers |
301
- | `revert <commit>` | `--abort`, `--continue`, `-n`/`--no-commit`, `--no-edit`, `-m`/`--mainline` |
302
- | `cherry-pick <commit>` | `--abort`, `--continue`, `--skip`, `-x`, `-m`/`--mainline`, `-n`/`--no-commit`, preserves original author |
303
- | `rebase <upstream>` | `--onto <newbase>`, `--abort`, `--continue`, `--skip` |
304
- | `stash` | `push`, `pop`, `apply`, `list`, `drop`, `show`, `clear`, `-m`, `-u`/`--include-untracked`, `stash@{N}` |
305
- | `remote` | `add`, `remove`/`rm`, `rename`, `set-url`, `get-url`, `-v` |
306
- | `config` | `get`, `set`, `unset`, `list`, `--list`/`-l`, `--unset`, `--get-all`, `--add` |
307
- | `fetch [<remote>] [<refspec>...]` | `--all`, `--tags`, `--prune`/`-p`, `--depth`, `--unshallow` |
308
- | `push [<remote>] [<refspec>...]` | `--force`/`-f`, `-u`/`--set-upstream`, `--all`, `--tags`, `--delete`/`-d` |
309
- | `pull [<remote>] [<branch>]` | `--ff-only`, `--no-ff`, `--rebase`/`-r`, `--no-rebase`, `--depth`, `--unshallow` |
310
- | `bisect` | `start`, `bad`/`good`/`new`/`old`, `skip`, `reset`, `log`, `replay`, `run`, `terms`, `visualize`/`view`, `--term-new`/`--term-old`, `--no-checkout`, `--first-parent` |
311
- | `clean` | `-f`, `-n`/`--dry-run`, `-d`, `-x`, `-X`, `-e`/`--exclude` |
312
- | `reflog` | `show [<ref>]`, `exists`, `-n`/`--max-count` |
313
- | `gc` | `--aggressive` |
314
- | `repack` | `-a`/`--all`, `-d`/`--delete` |
315
- | `rev-parse` | `--verify`, `--short`, `--abbrev-ref`, `--symbolic-full-name`, `--show-toplevel`, `--git-dir`, `--is-inside-work-tree`, `--is-bare-repository`, `--show-prefix`, `--show-cdup` |
316
- | `ls-files` | `-c`/`--cached`, `-m`/`--modified`, `-d`/`--deleted`, `-o`/`--others`, `-u`/`--unmerged`, `-s`/`--stage`, `--exclude-standard`, `-z`, `-t` |
149
+ 34 commands: `init`, `clone`, `fetch`, `push`, `pull`, `add`, `rm`, `mv`, `commit`, `status`, `log`, `show`, `diff`, `blame`, `branch`, `tag`, `checkout`, `switch`, `restore`, `reset`, `merge`, `rebase`, `cherry-pick`, `revert`, `stash`, `remote`, `config`, `bisect`, `clean`, `reflog`, `gc`, `repack`, `rev-parse`, `ls-files`. See [CLI.md](docs/CLI.md) for full usage details.
317
150
 
318
151
  ### Transport
319
152
 
320
- - **Local paths** -- direct filesystem transfer between repositories.
321
- - **Cross-VFS** -- clone, fetch, and push between isolated in-memory filesystems via `resolveRemote`. See [Multi-agent collaboration](#multi-agent-collaboration).
322
- - **Smart HTTP** -- clone, fetch, and push against real Git servers (e.g. GitHub) via Git Smart HTTP protocol. Auth via `credentials` option or `GIT_HTTP_BEARER_TOKEN` / `GIT_HTTP_USER` + `GIT_HTTP_PASSWORD` env vars.
153
+ - **Local paths**: direct filesystem transfer between repositories.
154
+ - **Cross-VFS**: clone, fetch, and push between isolated in-memory filesystems via `resolveRemote`. See [CLIENT.md](docs/CLIENT.md#multi-agent-collaboration).
155
+ - **Smart HTTP**: clone, fetch, and push against real Git servers (e.g. GitHub) via Git Smart HTTP protocol. Auth via `credentials` option or `GIT_HTTP_BEARER_TOKEN` / `GIT_HTTP_USER` + `GIT_HTTP_PASSWORD` env vars.
323
156
 
324
157
  ### Internals
325
158