Note
This doc describes shipped code in packages/computer/src/git/
and the git custom command in
packages/computer/src/backends/worker/. Everything below
works today.
workspace.git is a major typed surface on Workspace, alongside fs, runtime, Assets, and Artifacts. It is opt-in: pass createGitClient() from @cloudflare/computer/git as WorkspaceOptions.git to enable it. Git runs every operation against the local SQLite-backed VFS through isomorphic-git, so a
filesystem-only workspace (no backend) can drive a full
clone/commit/diff cycle. The git subpath bundles isomorphic-git
lazily and replaces its pako dependency with a small
node:zlib shim for Workers running with nodejs_compat; the
default @cloudflare/computer graph stays free of git.
Two doors into the same implementation:
- A typed JavaScript API —
workspace.git.clone({ url }),workspace.git.commit({ message }), and so on. The argument shapes are object-options with named fields; return values are structured. - An argv-driven entry point —
workspace.git.cli({ argv, cwd, env, stdin }). Every flag-shape decision lives ingit/cli.ts; the typed methods route to the same dependency- injected cores so the two surfaces cannot drift.
The worker backend's shell isolate registers a git custom
command that forwards across the loopback to the host
durable object's workspace.git.cli(...). Running git ...
inside bash.exec reaches the same dispatcher and the same
underlying methods.
Supported subcommands today:
add clone init merge rm
branch commit log pull show
cat-file config ls-files push stash
checkout diff ls-tree remote status
clean fetch reset rev-parse switch
symbolic-ref tag
update-ref
Global options accepted before the subcommand:
-C <path>— run the subcommand as though invoked from<path>. A relative path joins onto the caller's cwd. A single occurrence is supported; a second-Cis rejected.
Deliberate omissions, with rationale:
- SSH transport.
isomorphic-githas none.https://,http://, andfile://are the only supported URL schemes; every command that takes a URL validates the scheme up front and refuses anything else. - Partial clone (
--filter=blob:none).isomorphic-gitdoesn't speak the v2filtercapability.pathsoncloneprovides partial checkout (the working tree is restricted), which is the agent-relevant subset; every blob reachable from the tip tree is still fetched. git gc,repack,prune, hooks, worktrees, submodules. Noisomorphic-gitsurface for any of them.reset --soft/--mixed,cherry-pick,revert,blame. Out of scope until a concrete caller needs them; each can land as its own follow-up.resetcovers path unstaging and--hard; the other modes exit 129.
The items below are intended to work but currently don't, or work in a way that differs from real git in a surprising way. Each is something to fix rather than a deliberate omission.
-
The working tree is shared across branches;
checkoutupdates HEAD and the index but does not reconcile untracked files. A file created onfeatureand never committed remains on disk after switching tomain, andstatusreports it as untracked on every branch. Real git carries the same behavior for untracked files, but it also removes tracked-on-one-branch / absent-on-the-other files from disk on checkout; the workspace CLI does not. Reach forrmbeforecheckoutif a clean working tree matters. -
Several real-git flags are rejected as unknown.
branch -a,show --stat,hash-object <path>(the file-path form, only--stdinis supported), and a wider set of long-option flags listed in each command's Not mapped block above. The CLI exits 129 with anunknown option '...'line on stderr. -
Symlinks in a cloned tree are checked out as symlinks but the target may not resolve.
clonematerializes mode-120000entries as symbolic links in the workspace VFS, but a downstreamread/catof one through the shell can produceNo such file or directoryif the target is a path outside what the clone wrote. Top-level redirects (README.md -> packages/foo/README.md) on symlinked-readme repositories are the most common case. Workaround: read the symlink target withreadlinkor walk the link manually throughfs.readlink.
File a bug if you hit one of these and need a fix prioritized.
Three flavors of the same task — clone a repo, mutate a file, diff against HEAD — through each entry point.
import { Workspace } from "@cloudflare/computer";
import { createGitClient } from "@cloudflare/computer/git";
const ws = new Workspace({ storage: ctx.storage, git: createGitClient() });
await ws.git.clone({ url: "https://github.com/example/repo.git" });
await ws.fs.writeFile("/README.md", "hello world\n");
const patch = await ws.git.diff();
console.log(patch);await ws.git.cli({ argv: ["clone", "https://github.com/example/repo.git"] });
await ws.fs.writeFile("/README.md", "hello world\n");
const { stdout } = await ws.git.cli({ argv: ["diff"] });
console.log(stdout);const handle = await ws.runtime.exec(
"git clone https://github.com/example/repo.git . && " +
"echo 'hello world' > README.md && " +
"git diff",
);
const { stdout } = await handle.result();The shell-side git is the same dispatcher behind a custom
command; the just-bash shell never resolves a binary on PATH.
The sibling artifacts command
follows the same pattern through workspace.artifacts.cli(...) when the Workspace is configured with an Artifacts binding.
Every operation takes an optional dir working-tree path. It
defaults to /. The argv side falls back to cwd from the
shell command's environment when no positional dir is given.
Identity defaulting and config reads only honor the local
<dir>/.git/config — there is no global ~/.gitconfig
fallback. Layer one on top of the typed surface if you need
it.
Commit-producing subcommands (commit, pull and merge
when they materialize a merge commit) resolve the author /
committer in this order:
- Explicit
options.author/options.committeron the typed call.--author "Name <email>"ongit commitfrom the CLI. GIT_AUTHOR_NAME/GIT_AUTHOR_EMAILandGIT_COMMITTER_NAME/GIT_COMMITTER_EMAILfrom theenvpassed to the call (or tocli({ env })). The shell-side custom command flattens the just-bash env Map into this shape automatically.- Local repo config
user.name/user.email, as written bygit config user.email "...". Only the local<dir>/.git/configis consulted; there is no global~/.gitconfigfallback. defaultIdentityfromcreateGitClient()/new Workspace({ git: createGitClient(), defaultGitIdentity }).
If none of the four yields a name and email,
MissingIdentityError fires. The CLI surfaces it as git commit: author identity unknown with exit code 128. This
config-after-env order matches real git.
Network commands (fetch, pull, push, clone) accept the
same auth contract isomorphic-git exposes:
headers— applied verbatim to every HTTP request. Use this for static authentication, e.g. a GitHub PAT:await ws.git.clone({ url: "https://github.com/private/repo.git", headers: { Authorization: `Bearer ${env.GITHUB_TOKEN}` }, });
onAuth(url, auth)— called when the remote sends a 401. Return aGitAuth(username/password/headers) andisomorphic-gitretries with it.
The CLI does not surface onAuth (no clean argv shape for a
callback); for interactive flows reach for the typed API.
The typed surface throws subclasses of GitError:
| Class | Code | CLI exit |
|---|---|---|
NotARepositoryError |
ENOTAREPO |
128 |
AlreadyInitializedError |
EALREADYINIT |
128 |
MissingIdentityError |
EIDENTITY |
128 |
PathOutsideRepoError |
EPATHOUTSIDE |
128 |
PathspecNotFoundError |
EPATHSPEC |
128 |
any other GitError |
per-call (EFETCHFAIL, etc.) |
1 |
Exit code 129 on the CLI is reserved for argv-shape errors
(unknown options, missing required values, malformed inputs).
The dispatcher prints git <subcommand>: <message>\n on
stderr for every failure, matching the shape real git uses for
unknown commands and parse errors so callers can match on it.
Network commands accept onProgress and onMessage callbacks
on the typed surface. Both fire from inside isomorphic-git
during the operation.
The CLI surface buffers progress on the host side and flushes
it on stderr at the end. A long git clone from inside the
shell produces no output until the call returns; streaming
progress would need a second exec-event channel. The shell
isolate has globalOutbound: null, so network requests
themselves happen on the host durable object, not in the
isolate — they work despite the restriction.
Each GitClient owns one shared pack/index cache passed to
every isomorphic-git call against this workspace. Without
this every readBlob / statusMatrix / walk re-parses the
packfile, which is the difference between a sub-second diff
and one that hangs for minutes on a real repository.
The cache is not bounded. A workspace that clones a 1 GB repository will hold the parsed pack in DO memory. Workable for the agent-scale workspaces this package targets, but worth calling out — bounded or per-call-opt-out caching is a follow-up if it becomes a problem in practice.
Each entry lists the canonical CLI invocation, the TS signature, the flag-to-option mapping, and any flags from real git that aren't covered.
git init [--initial-branch=<name>] [--bare] [<dir>]
ws.git.init({
dir?: string,
defaultBranch?: string, // default "main"
bare?: boolean, // default false
}): Promise<void>| Flag | TS field |
|---|---|
--initial-branch <name> / -b <name> |
defaultBranch |
--bare |
bare |
<dir> (positional) |
dir |
Not mapped: --separate-git-dir, --shared, --template,
--object-format. None are wired into isomorphic-git's
init.
git status [--porcelain[=v2]] [--short | -s]
ws.git.status({
dir?: string,
}): Promise<StatusEntry[]>
interface StatusEntry {
path: string;
index: " " | "A" | "M" | "D";
worktree: " " | "A" | "M" | "D" | "?";
}The CLI default is porcelain v2. --porcelain=v1 (and the
1 spelling git also accepts) produces the v1 XY <path>
form with ?? for untracked files; --short / -s produces
the short form ( ? for untracked). The typed surface returns
the underlying StatusEntry[]; format it with
formatPorcelainV2, formatPorcelainV1, or formatShort
from @cloudflare/computer/git.
Not mapped: --branch, --ignored, --untracked-files,
the long human-readable form. The structured return is the
primary surface.
git add [-A|--all] [-f|--force] <pathspec>...
ws.git.add({
dir?: string,
paths: string[],
all?: boolean,
trackedOnly?: boolean,
force?: boolean,
}): Promise<void>| Flag | TS field |
|---|---|
-A / --all |
all |
--force / -f |
force |
<pathspec>... (positional) |
paths |
-A stages every change under the repo — new, modified, and
deleted tracked files — and ignores any pathspec. trackedOnly
(no CLI flag of its own; set by commit -a) restricts all
mode to paths already in HEAD, so untracked files are left
alone. Not mapped: --update, --intent-to-add, -p
interactive.
git rm <pathspec>...
ws.git.rm({
dir?: string,
paths: string[],
}): Promise<void>isomorphic-git's remove only unstages — it does not touch
the working tree. --cached is the only mode supported.
Not mapped: -r, -f, --ignore-unmatch.
git commit [-a] -m <msg> [--amend] [--author="Name <email>"]
ws.git.commit({
dir?: string,
message: string,
author?: { name: string; email: string },
committer?: { name: string; email: string },
amend?: boolean,
env?: Record<string, string>,
}): Promise<{ oid: string }>| Flag | TS field |
|---|---|
-m <msg> / --message <msg> |
message |
-a |
(stages tracked changes first) |
--amend |
amend |
--author "Name <email>" |
author |
Identity precedence: options.author → env → local config
(user.name / user.email) → defaultIdentity. -a (and the
-am cluster) stages tracked modifications and deletions —
never untracked files — before committing; a staging failure
aborts before the commit runs. The CLI prints [<short-oid>] <subject> on success, matching the first line of real git's
commit summary.
Not mapped: --no-edit, --signoff, --gpg-sign,
-F <file>.
git log [-n <N>] [-<N>] [--oneline] [<ref>]
ws.git.log({
dir?: string,
ref?: string, // default "HEAD"
depth?: number,
}): Promise<CommitView[]>| Flag | TS field |
|---|---|
-n <N> |
depth |
-<N> (e.g. -1, -5) |
depth |
--oneline |
(CLI formatter) |
<ref> (positional) |
ref |
The positional <ref> accepts revision suffixes (HEAD~2,
<branch>^); they resolve through rev-parse before the walk.
The CLI default emits commit / Author / Date / message
blocks; --oneline collapses each entry to <short-oid> <subject>. The typed surface returns CommitView[] (oid,
message, tree, parent, author, committer) so callers can
format their own way.
Not mapped: --graph, --all, --since, --until,
-p, --stat, --follow, --reverse.
git show [<ref>]
ws.git.show({
dir?: string,
ref: string,
}): Promise<CommitView>CLI defaults ref to HEAD. Output matches the full log
form for one commit. Not mapped: the diff-attached form
real git's show produces — use git log plus git diff <parent> <ref> for that.
git rev-parse [--abbrev-ref] [--show-toplevel] <ref>
ws.git.revParse({
dir?: string,
ref: string,
}): Promise<string>
ws.git.repoRoot({ dir?: string }): Promise<string>Resolves a ref (branch, tag, short oid prefix) to its full
SHA-1. Revision suffixes from gitrevisions(7) are supported:
HEAD^, HEAD~N, <ref>^N, and chained forms like HEAD~2^2.
^/~N follow first parents; ^N selects parent N. Walking
past the root commit is an error.
| Flag | Behavior |
|---|---|
--abbrev-ref HEAD |
Print the current branch; fall back to the oid on detached HEAD. |
--show-toplevel |
Print the working-tree root (repoRoot); walks up to find .git. Exits 128 outside a repo. |
Not mapped: --git-dir, --is-inside-work-tree, and the
rest of rev-parse's wide flag surface.
git symbolic-ref [--short] [-q|--quiet] [HEAD]
ws.git.currentBranch({
dir?: string,
fullname?: boolean, // default true
}): Promise<string | undefined>Only HEAD is supported. --short returns <name>,
otherwise refs/heads/<name>. Returns / prints nothing on
detached HEAD; the CLI exits 1.
git ls-files [--ref <ref>]
ws.git.lsFiles({
dir?: string,
ref?: string,
}): Promise<string[]>Without --ref, lists files in the index. With --ref,
lists files in that tree. Not mapped: -s, --stage,
--others, --exclude-standard.
git ls-tree <tree-ish> [<path>]
ws.git.lsTree({
dir?: string,
ref: string,
path?: string,
}): Promise<TreeEntryView[]>
interface TreeEntryView {
mode: string; // "100644", "040000", "160000"
path: string;
oid: string;
type: "blob" | "tree" | "commit";
}CLI output matches real git's
<mode> <type> <oid>\t<path>. Not mapped: -r recursive,
-l long form, --name-only.
git clone [--depth N] [--branch B] [--single-branch] [--no-tags]
<url> [<dir>]
ws.git.clone({
url: string,
dir?: string,
ref?: string,
paths?: string[], // partial checkout only
depth?: number, // default 1
singleBranch?: boolean,
noTags?: boolean,
headers?: Record<string, string>,
corsProxy?: string,
onProgress?: ProgressCallback,
onMessage?: MessageCallback,
}): Promise<void>| Flag | TS field |
|---|---|
--depth <N> |
depth |
--branch <B> / -b <B> |
ref |
--single-branch / --no-single-branch |
singleBranch |
--no-tags / --tags |
noTags |
<url> (positional) |
url |
<dir> (positional) |
dir |
When <dir> is omitted, the CLI derives it from the last path
segment of the URL, stripping a trailing .git — git clone https://github.com/owner/repo.git lands in ./repo, matching
real git. A URL whose basename can't produce a safe directory
name exits 129; pass an explicit destination. After clone, HEAD
is a symbolic ref to the checked-out branch, so branch --show-current and symbolic-ref HEAD work immediately.
Only https://, http://, and file:// schemes are
accepted. Not mapped: --bare, --mirror, --recurse- submodules, --filter, ssh URLs.
git diff [--stat|--name-only|--name-status] [<from> [<to>]] [-- <path>...]
ws.git.diff({
dir?: string,
ref?: string,
to?: string,
paths?: string[],
}): Promise<string>
ws.git.diffSummary({
dir?: string,
ref?: string,
to?: string,
paths?: string[],
}): Promise<DiffSummaryEntry[]>
interface DiffSummaryEntry {
path: string;
status: "A" | "M" | "D";
insertions: number;
deletions: number;
}Three modes:
git diff— working tree vs HEAD.git diff <ref>— working tree vs<ref>.git diff <from> <to>—<from>vs<to>(commit pair).
Paths after -- filter the output. Matching is exact-or-
directory-prefix; globs are not supported. The <from> / <to>
refs accept revision suffixes (HEAD~1).
The summary flags share the same change set as the patch:
| Flag | Output |
|---|---|
--stat |
Per-file path | total +++--- bar plus a N files changed, … footer. |
--name-only |
One changed path per line. |
--name-status |
<status>\t<path> per line. |
Not mapped: --cached, -U <N>, the three-dot form.
git branch # list
git branch --show-current # print the current branch
git branch <name> [<start-point>] # create
git branch -d <name> # delete
git branch --force <name> <start> # overwrite
ws.git.branch({
dir?: string,
name: string,
startPoint?: string,
force?: boolean,
}): Promise<void>
ws.git.branchDelete({ dir?: string, name: string }): Promise<void>
ws.git.branchList({ dir?: string }): Promise<string[]>
ws.git.currentBranch({ dir?: string }): Promise<string | undefined>Bare git branch lists local branches with the current one
prefixed * . The CLI mode is selected by positional shape:
zero positionals lists, one creates at HEAD, two creates at
a start point, -d switches to delete (and consumes one or
more positionals). --show-current prints the checked-out
branch (nothing on detached HEAD).
git tag # list
git tag <name> [<object>] # create
git tag -d <name> # delete
ws.git.tag({
dir?: string,
name: string,
object?: string,
force?: boolean,
}): Promise<void>
ws.git.tagDelete({ dir?: string, name: string }): Promise<void>
ws.git.tagList({ dir?: string }): Promise<string[]>Only lightweight tags are supported. Not mapped: -a
annotated tags, -s signed tags, -n <N> annotation
preview.
git checkout [--force] <ref> [-- <paths>...]
git checkout -b <new-branch> [<start-point>]
ws.git.checkout({
dir?: string,
ref: string,
paths?: string[],
force?: boolean,
}): Promise<void>With paths set, the working tree updates to match ref
for those paths only; HEAD does not move (matching git checkout <ref> -- <path>). Without paths, HEAD moves to
ref. -b creates a branch (optionally at a start point)
and switches to it; the branch is created first, so a name
collision leaves the working tree untouched.
Not mapped: --detach, --orphan, --theirs, --ours.
git switch <branch>
git switch -c <new-branch> [<start-point>]
The modern spelling of checkout for branch movement. git switch <branch> moves HEAD; -c is the checkout -b
equivalent. Both delegate to checkout / branch on the
typed surface. Not mapped: --detach, -C force-create,
--orphan.
git fetch [--depth N] [--prune] [--tags|--no-tags] [<remote>|<url>] [<ref>]
ws.git.fetch({
dir?: string,
remote?: string,
url?: string,
ref?: string,
remoteRef?: string,
depth?: number,
singleBranch?: boolean,
tags?: boolean,
prune?: boolean,
headers?: Record<string, string>,
onAuth?: AuthCallback,
}): Promise<{ defaultBranch: string | null; fetchHead: string | null }>The CLI routes a positional that starts with a URL scheme to
url; anything else is a remote name. Not mapped:
--unshallow, --all, --shallow-since, --prune-tags.
git push [--force|-f] [--delete|-d] [<remote>|<url>] [<ref>]
ws.git.push({
dir?: string,
remote?: string,
url?: string,
ref?: string,
remoteRef?: string,
force?: boolean,
delete?: boolean,
headers?: Record<string, string>,
onAuth?: AuthCallback,
}): Promise<PushResult>
interface PushResult {
ok: boolean;
error: string | null;
refs: Record<string, { ok: boolean; error?: string }>;
}A non-ok PushResult surfaces as exit 1 on the CLI with the
error string on stderr. Not mapped: --tags, --set- upstream, --atomic, --mirror.
git pull [--ff-only] [--no-ff] [<remote>|<url>] [<ref>]
ws.git.pull({
dir?: string,
remote?: string,
url?: string,
ref?: string,
fastForward?: boolean,
fastForwardOnly?: boolean,
headers?: Record<string, string>,
onAuth?: AuthCallback,
author?: { name: string; email: string },
committer?: { name: string; email: string },
env?: Record<string, string>,
}): Promise<void>Identity follows the same precedence as commit; if a merge
commit is needed and no identity resolves,
MissingIdentityError fires.
git merge [--ff-only] [--no-ff] [-m <msg>] <ref>
ws.git.merge({
dir?: string,
theirs: string,
ours?: string,
fastForward?: boolean,
fastForwardOnly?: boolean,
message?: string,
author?: { name: string; email: string },
committer?: { name: string; email: string },
env?: Record<string, string>,
}): Promise<{ oid?: string; alreadyMerged?: boolean; fastForward?: boolean }>The CLI prints a one-line summary: Already up to date.,
Fast-forward to <short-oid>, or Merge commit <short-oid>.
git remote # list names
git remote -v # list with URLs
git remote add [-f] <name> <url>
git remote remove <name> # or `rm`
ws.git.remoteAdd({
dir?: string,
name: string,
url: string,
force?: boolean,
}): Promise<void>
ws.git.remoteRemove({ dir?: string, name: string }): Promise<void>
ws.git.remoteList({ dir?: string }): Promise<RemoteView[]>
interface RemoteView { name: string; url: string }Not mapped: remote rename, remote set-url, remote show, remote prune, remote update.
git hash-object [-w] --stdin
ws.git.hashObject({
dir?: string,
content: Uint8Array | string,
write?: boolean,
}): Promise<string>The CLI surface only covers the --stdin form. The file-path
form would pull in workspace path resolution and a buffered
readback that the typed API serves better directly.
git cat-file -p <oid>[:<path>]
ws.git.catFile({
dir?: string,
oid: string,
filepath?: string,
}): Promise<{ oid: string; bytes: Uint8Array }>Supports the <oid>:<path> shorthand for tree subreads.
Not mapped: -t type, -s size, --batch.
git update-ref [--force] <ref> <value>
ws.git.updateRef({
dir?: string,
ref: string,
value: string,
force?: boolean,
symbolic?: boolean,
}): Promise<void>Not mapped: -d delete, --stdin batch.
git config <key> # get
git config --get-all <key> # get all values
git config <key> <value> # set
git config --add <key> <value> # append multi-valued
git config --unset <key> # unset
ws.git.configGet({
dir?: string,
path: string,
all?: boolean,
}): Promise<string | string[] | undefined>
ws.git.configSet({
dir?: string,
path: string,
value: string | boolean | number | undefined,
append?: boolean,
}): Promise<void>A missing key returns undefined from configGet and exits
1 with empty stdout from the CLI, matching git config --get's behavior. Only the local <dir>/.git/config file is
read or written; global and system configs are not consulted.
git reset [-- <paths>...] # unstage paths (against HEAD)
git reset --hard [<ref>] # restore tracked files to <ref>
ws.git.reset({
dir?: string,
paths?: string[],
hard?: boolean,
ref?: string, // default "HEAD"
}): Promise<void>Path reset unstages the listed paths back to ref and leaves
the working tree alone (built on resetIndex). Bare git reset
and git reset HEAD unstage all staged paths. --hard moves the
current branch (when HEAD is symbolic) to ref and rewrites both
the index and working tree. With --hard, a single positional is
the ref. Refs accept revision suffixes (HEAD~1). Not mapped:
--soft, --mixed (both exit 129), --merge, --keep.
git stash [push [-m <msg>]] # stash tracked changes (bare = push)
git stash list # list entries
git stash pop # restore the latest entry
ws.git.stashPush({ dir?: string, message?: string }): Promise<void>
ws.git.stashList({ dir?: string }): Promise<string[]>
ws.git.stashPop({ dir?: string, index?: number }): Promise<void>push stashes tracked working-tree changes and restores a
clean tree; it creates a commit internally, so it needs a
resolvable identity (see Identity). list
returns stash@{N}: <message> entries newest-first. pop
restores the latest entry. Not mapped: apply, drop,
clear, --include-untracked, stash@{N} selectors on pop.
git clean -f [-d] [-n|--dry-run]
ws.git.clean({
dir?: string,
directories?: boolean,
dryRun?: boolean,
}): Promise<string[]> // repo-relative paths removedRemoves untracked files under the repo and returns the paths
removed. Like real git, it refuses to act without -f unless
previewing with -n / --dry-run, and only descends into
untracked directories with -d. The untracked set is derived
from a status-matrix walk; ignored-file handling is not
modeled, so every untracked path is a candidate. Not mapped:
-x / -X ignore handling, pathspec limiting.
| Flag | Subcommand | TS option |
|---|---|---|
-C <path> |
(global) | rewrites effective cwd |
-A / --all |
add |
all |
--abbrev-ref |
rev-parse |
(via currentBranch) |
-a |
commit |
(stages tracked changes) |
--amend |
commit |
amend |
--author |
commit |
author |
-b <new> |
checkout |
(create + switch) |
-b <B> (branch) |
clone, init |
ref / defaultBranch |
--bare |
init |
bare |
--branch <B> |
clone |
ref |
-c <new> |
switch |
(create + switch) |
--cached |
rm |
(implicit) |
-d |
clean |
directories |
-d / --delete |
branch, tag, push |
branchDelete / tagDelete / delete |
--depth <N> |
clone, fetch |
depth |
--dry-run / -n |
clean |
dryRun |
--ff-only |
pull, merge |
fastForwardOnly |
-f / --force |
add, branch, checkout, clean, push, update-ref, remote add |
force |
--get-all |
config |
all |
--hard |
reset |
hard |
--initial-branch |
init |
defaultBranch |
-m <msg> |
commit, merge, stash push |
message |
-n <N> |
log |
depth |
-<N> (e.g. -1) |
log |
depth |
--name-only |
diff |
(via diffSummary) |
--name-status |
diff |
(via diffSummary) |
--no-ff |
pull, merge |
fastForward: false |
--no-single-branch |
clone |
singleBranch: false |
--no-tags |
clone, fetch |
noTags / tags: false |
--oneline |
log |
(formatter) |
--porcelain[=v1|v2] |
status |
(formatter) |
-p |
cat-file |
(implicit) |
--prune |
fetch |
prune |
-q / --quiet |
symbolic-ref |
(formatter) |
--ref <r> |
ls-files |
ref |
--short |
symbolic-ref, status |
fullname: false / (formatter) |
--show-current |
branch |
(via currentBranch) |
--show-toplevel |
rev-parse |
(via repoRoot) |
-s |
status |
(formatter) |
--single-branch |
clone |
singleBranch |
--stat |
diff |
(via diffSummary) |
--stdin |
hash-object |
(implicit) |
--tags |
clone, fetch |
noTags: false / tags: true |
--unset |
config |
value: undefined |
-v / --verbose |
remote |
(formatter) |
-w |
hash-object |
write |
The worker backend's shell isolate registers a git custom
command on every Bash construction (see
docs/12_worker_backend.md). The
command forwards across the loopback to the host durable
object's workspace.git.cli(...). There is no git binary
on PATH; the just-bash interpreter only knows about its
built-in commands and the ones registered through
customCommands.
ShellWorker exposes a protected extraCommands(ws) hook for
subclasses that want to layer their own custom commands on top
of git. The base class returns an empty list; an override
prepends or appends to the array passed to Bash. Subclass
commands receive the live host stub the shell already reached,
so they share its lifetime without refetching.
import { ShellWorker, defineGitCommand } from "@cloudflare/computer/backends/worker";
import { type CustomCommand } from "just-bash";
class MyShell extends ShellWorker {
protected override extraCommands(ws: HostWorkspaceStub): CustomCommand[] {
return [defineMyCommand(ws)];
}
}Two properties of the shell-side path worth pinning:
- Network commands work despite
globalOutbound: null. The Dynamic Worker hosting the shell has its globalOutbound set tonull, blocking all outbound HTTP.git clone,git fetch,git pull, andgit pushstill work because the actual HTTP request happens on the host durable object side, not in the shell isolate. The shell just hands the argv across the loopback and waits for the result. - Progress is buffered. A long
cloneproduces no output until the call returns. Progress callbacks fire on the host side; their output is collected into stderr and flushed at the end. Streaming progress through the shell would need a second exec-event channel.