Package Exports
- just-git
Readme
just-git
Git implementation for virtual bash environments (particularly just-bash). Pure TypeScript, zero dependencies. Works in Node, Bun, Deno, and the browser. ~97 kB gzipped.
Install
npm install just-gitQuick start
import { Bash } from "just-bash";
import { createGit } from "just-git";
const git = createGit({
identity: { name: "Alice", email: "alice@example.com" },
});
const bash = new Bash({
cwd: "/repo",
customCommands: [git],
});
await bash.exec("git init");
await bash.exec("echo 'hello' > README.md");
await bash.exec("git add .");
await bash.exec('git commit -m "initial commit"');
await bash.exec("git log --oneline");Options
createGit(options?) accepts:
| Option | Description |
|---|---|
identity |
Author/committer override. With locked: true, always wins over env vars and git config. Without locked, acts as a fallback. |
credentials |
(url) => HttpAuth | null callback for Smart HTTP transport auth. |
disabled |
GitCommandName[] of subcommands to block (e.g. ["push", "rebase"]). |
network |
{ allowed?: string[] } to restrict HTTP access by hostname or URL prefix. Set to false to block all network access. |
const git = createGit({
identity: { name: "Agent Bot", email: "bot@company.com", locked: true },
credentials: async (url) => ({ type: "bearer", token: "ghp_..." }),
disabled: ["rebase"],
network: { allowed: ["github.com"] },
});Middleware
Middleware wraps every git <subcommand> invocation. Each middleware receives a CommandEvent and a next() function. Call next() to proceed, or return an ExecResult to short-circuit. Middlewares compose in registration order (first registered = outermost).
The CommandEvent provides the execution context: { command, rawArgs, fs, cwd, env, stdin }, plus optional exec and signal when available.
// Audit log — record every command the agent runs
git.use(async (event, next) => {
const result = await next();
auditLog.push({ command: `git ${event.command}`, exitCode: result.exitCode });
return result;
});
// Gate pushes on human approval
git.use(async (event, next) => {
if (event.command === "push" && !(await getHumanApproval(event.rawArgs))) {
return { stdout: "", stderr: "Push blocked — awaiting approval.\n", exitCode: 1 };
}
return next();
});
// Block commits that add large files (uses event.fs to read the worktree)
git.use(async (event, next) => {
if (event.command === "add") {
for (const path of event.rawArgs.filter((a) => !a.startsWith("-"))) {
const resolved = path.startsWith("/") ? path : `${event.cwd}/${path}`;
const stat = await event.fs.stat(resolved).catch(() => null);
if (stat && stat.size > 5_000_000) {
return { stdout: "", stderr: `Blocked: ${path} exceeds 5 MB\n`, exitCode: 1 };
}
}
}
return next();
});git.use() returns an unsubscribe function to remove the middleware dynamically.
Hooks
Hooks fire at specific points inside command execution (after middleware, inside operation logic). Register with git.on(event, handler), which returns an unsubscribe function.
Pre-hooks
Pre-hooks can abort the operation by returning { abort: true, message? }.
// Block secrets from being committed
git.on("pre-commit", (event) => {
const forbidden = event.index.entries.filter((e) => /\.(env|pem|key)$/.test(e.path));
if (forbidden.length) {
return { abort: true, message: `Blocked: ${forbidden.map((e) => e.path).join(", ")}` };
}
});
// Enforce conventional commit messages
git.on("commit-msg", (event) => {
if (!/^(feat|fix|docs|refactor|test|chore)(\(.+\))?:/.test(event.message)) {
return { abort: true, message: "Commit message must follow conventional commits format" };
}
});| Hook | Payload |
|---|---|
pre-commit |
{ index, treeHash } |
commit-msg |
{ message } (mutable) |
merge-msg |
{ message, treeHash, headHash, theirsHash } (mutable message) |
pre-merge-commit |
{ mergeMessage, treeHash, headHash, theirsHash } |
pre-checkout |
{ target, mode } |
pre-push |
{ remote, url, refs[] } |
pre-fetch |
{ remote, url, refspecs, prune, tags } |
pre-clone |
{ repository, targetPath, bare, branch } |
pre-pull |
{ remote, branch } |
pre-rebase |
{ upstream, branch } |
pre-reset |
{ mode, target } |
pre-clean |
{ dryRun, force, removeDirs, removeIgnored, onlyIgnored } |
pre-rm |
{ paths, cached, recursive, force } |
pre-cherry-pick |
{ mode, commit } |
pre-revert |
{ mode, commit } |
pre-stash |
{ action, ref } |
Post-hooks
Post-hooks are observational -- return value is ignored. Handlers are awaited in registration order.
// Feed agent activity to your UI or orchestration layer
git.on("post-commit", (event) => {
onAgentCommit({ hash: event.hash, branch: event.branch, message: event.message });
});
git.on("post-push", (event) => {
onAgentPush({ remote: event.remote, refs: event.refs });
});| Hook | Payload |
|---|---|
post-commit |
{ hash, message, branch, parents, author } |
post-merge |
{ headHash, theirsHash, strategy, commitHash } |
post-checkout |
{ prevHead, newHead, isBranchCheckout } |
post-push |
same payload as pre-push |
post-fetch |
{ remote, url, refsUpdated } |
post-clone |
{ repository, targetPath, bare, branch } |
post-pull |
{ remote, branch, strategy, commitHash } |
post-reset |
{ mode, targetHash } |
post-clean |
{ removed, dryRun } |
post-rm |
{ removedPaths, cached } |
post-cherry-pick |
{ mode, commitHash, hadConflicts } |
post-revert |
{ mode, commitHash, hadConflicts } |
post-stash |
{ action, ok } |
Low-level events
Fire-and-forget events emitted on every object/ref write. Handler errors are caught and forwarded to hooks.onError (no-op by default).
| Event | Payload |
|---|---|
ref:update |
{ ref, oldHash, newHash } |
ref:delete |
{ ref, oldHash } |
object:write |
{ type, hash } |
Command coverage
33 commands implemented. See CLI.md for full usage details.
| Command | Flags / options |
|---|---|
init [<dir>] |
--bare, --initial-branch |
clone <repo> [<dir>] |
--bare, -b <branch> |
blame <file> |
-L <start>,<end>, -l/--long, -e/--show-email, -s/--suppress, -p/--porcelain, --line-porcelain |
add <paths> |
., --all/-A, --update/-u, --force/-f, -n/--dry-run, glob pathspecs |
rm <paths> |
--cached, -r, -f, -n/--dry-run, glob pathspecs |
mv <src> <dst> |
-f, -n/--dry-run, -k |
commit |
-m, -F <file> / -F -, --allow-empty, --amend, --no-edit, -a |
status |
-s/--short, --porcelain, -b/--branch |
log |
--oneline, -n, --all, --reverse, --decorate, --format/--pretty, A..B, A...B, -- <path>, --author=, --grep=, --since/--after, --until/--before |
show [<object>] |
Commits (with diff), annotated tags, trees, blobs |
diff |
--cached/--staged, <commit>, <commit> <commit>, A..B, A...B, -- <path>, --stat, --shortstat, --numstat, --name-only, --name-status |
branch |
-d, -D, -m, -M, -r, -a/--all, -v/-vv, -u/--set-upstream-to |
tag [<name>] [<commit>] |
-a -m (annotated), -d, -l <pattern>, -f |
switch |
-c/-C (create/force-create), --detach/-d, --orphan, - (previous branch), --guess/--no-guess |
restore |
-s/--source, -S/--staged, -W/--worktree, -S -W (both), --ours/--theirs, pathspec globs |
checkout |
-b, -B, --orphan, detached HEAD, -- <paths>, --ours/--theirs, pathspec globs |
reset [<commit>] |
-- <paths>, --soft, --mixed, --hard, pathspec globs |
merge <branch> |
--no-ff, --ff-only, --squash, -m, --abort, --continue, conflict markers |
revert <commit> |
--abort, --continue, -n/--no-commit, --no-edit, -m/--mainline |
cherry-pick <commit> |
--abort, --continue, --skip, -x, -m/--mainline, -n/--no-commit, preserves original author |
rebase <upstream> |
--onto <newbase>, --abort, --continue, --skip |
stash |
push, pop, apply, list, drop, show, clear, -m, -u/--include-untracked, stash@{N} |
remote |
add, remove/rm, rename, set-url, get-url, -v |
config |
get, set, unset, list, --list/-l, --unset |
fetch [<remote>] [<refspec>...] |
--all, --tags, --prune/-p |
push [<remote>] [<refspec>...] |
--force/-f, -u/--set-upstream, --all, --tags, --delete/-d |
pull [<remote>] [<branch>] |
--ff-only, --no-ff, --rebase/-r, --no-rebase |
clean |
-f, -n/--dry-run, -d, -x, -X, -e/--exclude |
reflog |
show [<ref>], exists, -n/--max-count |
gc |
--aggressive |
repack |
-a/--all, -d/--delete |
rev-parse |
--verify, --short, --abbrev-ref, --symbolic-full-name, --show-toplevel, --git-dir, --is-inside-work-tree, --is-bare-repository, --show-prefix, --show-cdup |
ls-files |
-c/--cached, -m/--modified, -d/--deleted, -o/--others, -u/--unmerged, -s/--stage, --exclude-standard, -z, -t |
Transport
- Local paths -- direct filesystem transfer between repositories.
- Smart HTTP -- clone, fetch, and push against real Git servers (e.g. GitHub) via Git Smart HTTP protocol. Auth via
credentialsoption orGIT_HTTP_BEARER_TOKEN/GIT_HTTP_USER+GIT_HTTP_PASSWORDenv vars.
Internals
.gitignoresupport (hierarchical, negation,info/exclude,core.excludesFile)- Merge-ort strategy with rename detection and recursive merge bases
- Reflog for HEAD, branches, and tracking refs
- Index in Git binary v2 format
- Object storage in real Git format (SHA-1 addressed)
- Packfiles with zlib compression for storage and transport
- Pathspec globs across
add,rm,diff,reset,checkout,restore,log
Goals and testing
High fidelity to real git (2.53.0) state and output. Tested using real git as an oracle across hundreds of randomized command traces.
If you're running just-bash over a real filesystem, mixing commands between this implementation and real git in the same repo has not been extensively tested yet.
Disclaimer
This project is not affiliated with just-bash or Vercel.