Skip to content

workspace: add a bunch of extra git flags and fixes - #8

Merged
aron-cf merged 20 commits into
mainfrom
git-pt2
Jun 16, 2026
Merged

workspace: add a bunch of extra git flags and fixes#8
aron-cf merged 20 commits into
mainfrom
git-pt2

Conversation

@aron-cf

@aron-cf aron-cf commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

The Workspace shell exposes a custom git command backed by @cloudflare/workspace and isomorphic-git. It handled the core path, but common agent workflows expected more of real Git's porcelain behavior: cloning into a repo-named directory, running commands with -C, checking the current branch, inspecting compact diffs, resolving HEAD~1, staging and committing common change sets, and cleaning or resetting a work tree. Several of those commands either failed as unknown options or had surprising behavior.

This change expands the shell git surface while keeping the Workspace-native implementation. The CLI now supports top-level -C, real-Git-like default clone destinations, symbolic HEAD after clone, current-branch helpers, log -N, porcelain v1 status, revision suffixes such as HEAD^ and HEAD~N, rev-parse --show-toplevel, and diff summary modes (--stat, --name-only, and --name-status).

The edit and commit loop also works more like the commands agents already use. git config user.name and user.email now provide commit identity, add -A stages new, modified, and deleted paths, and commit -a / commit -am stages tracked changes before committing without adding untracked files. The branch and work-tree commands now cover checkout -b, switch, switch -c, stash push/list/pop, reset path unstaging, reset --hard, and clean -f[d][n].

The hard-reset path preserves symbolic HEAD and moves the current branch before restoring tracked files, so reset --hard HEAD~1 leaves branch --show-current intact and rewrites the working tree to the target commit. git reset HEAD now unstages all staged paths instead of silently treating HEAD as a pathspec.

The git package tests cover the parser layer with fake clients and the behavior layer with real isomorphic-git plus in-memory filesystems. The real Workspace smoke test exercises the agent edit loop end to end: configure identity, add -A, commit -am, inspect with diff --stat and revision suffixes, branch with switch -c, run reset --hard, use stash, and clean untracked files.

Documentation

docs/13_git_interface.md now describes the expanded command set, supported global options, identity precedence, revision suffix support, the new diff summary modes, reset/stash/clean behavior, and the remaining differences from real Git.

Follow-up

git clean -fd does not model .gitignore; every untracked path reported by the status matrix is a candidate for removal. The docs call this out, but ignore-aware clean behavior would be a useful follow-up if callers depend on real Git's ignored-file protection.

aron-cf added 19 commits June 16, 2026 19:44
A bare `git clone <url>` resolved the missing positional
destination to cwd, so the working tree was unpacked directly
into the current directory instead of into a subdirectory named
after the repository. This surprised agents and scripts that
relied on real git's behavior of cloning into `./<repo>`.

Derive the destination from the last path segment of the URL,
stripping a trailing `.git`, when no explicit destination is
given. Reject a URL whose basename cannot produce a safe
directory name so the failure is loud rather than silently
falling back to cwd. An explicit destination still takes
precedence and resolves relative to cwd as before.
Scripts and agents lean on `git -C <path> <subcommand>` to run a
command against another working tree without changing the
process directory. The dispatcher treated `-C` as a subcommand
and failed with an unknown-command error.

Parse a leading `-C <path>` off the front of argv before
dispatch and use it as the effective cwd that each subcommand
resolves its `dir` default against. A relative path joins onto
the caller's cwd. A missing value or a second `-C` exits 129;
agent use only needs a single occurrence.
Agents check the current branch with `git branch
--show-current` or `git rev-parse --abbrev-ref HEAD`. Both were
rejected as unknown options, leaving `symbolic-ref HEAD` as the
only spelling.

Wire both to the existing current-branch lookup. `branch
--show-current` prints the checked-out branch or nothing on
detached HEAD. `rev-parse --abbrev-ref HEAD` prints the branch
name and falls back to the resolved oid when HEAD is detached,
matching real git.
After a clone, HEAD resolved as a detached oid rather than a
symbolic ref to the checked-out branch, so `symbolic-ref HEAD`
and `branch --show-current` reported nothing until an explicit
checkout rewrote it. The detach came from the checkout phase:
isomorphic-git's clone writes a symbolic HEAD, but `cloneWith`
then checked out `HEAD`, which re-resolves to an oid and detaches
because the ref does not expand to refs/heads/*.

Pass noUpdateHead to the checkout phase so it materializes the
working tree without rewriting HEAD, preserving the symbolic ref
the clone left in place.
`git log -1` and the wider `-<N>` family are the muscle-memory
spelling for limiting commit output, but the dispatcher rejected
the bare numeric short option as unknown and only accepted `-n
<N>`.

Rewrite `-<N>` to `-n <N>` before flag parsing. `-0` and
non-numeric forms still fail through the existing `-n`
validation, which requires a positive integer.
Most tooling parses porcelain v1, but `status` accepted only the
short and porcelain v2 forms and rejected `--porcelain=v1` as an
unsupported value. The internal short formatter is close but
renders untracked files as ` ?` rather than the `??` two-char
code v1 consumers expect.

Add a dedicated v1 formatter that matches git's `XY <path>`
output, including `??` for untracked, and route `--porcelain=v1`
(and the `1` spelling) to it. The bare `--porcelain` default
stays v2 so existing machine-readable consumers are unaffected.
`HEAD^`, `HEAD~1`, `HEAD~2`, and `<branch>~N` are ubiquitous in
agent and CI workflows, but rev-parse accepted only a literal ref
or an oid prefix and rejected any ancestry suffix.

Parse the gitrevisions(7) suffix operators `^`, `^N`, and `~N`
off the base ref, then walk commit parents from the resolved
base oid. `~N` expands to N first-parent hops; `^` and `^N`
select a parent by index. Walking past the root commit fails
with a clear error.
rev-parse learned the `HEAD^` / `HEAD~N` ancestry grammar, but
show, diff, and log resolve their refs through resolveRef, which
only understands literal refs and oids. A suffixed ref handed to
any of the three failed to resolve.

Pre-resolve a ref carrying an ancestry suffix to a concrete oid
through rev-parse before forwarding it to the typed method. Plain
refs pass through untouched so branch and tag resolution stays
where it was.
Scripts find the repository root with `git rev-parse
--show-toplevel`, but rev-parse rejected the flag and exposed no
way to discover the working-tree root.

Add a repoRoot operation that walks up from the working
directory until it finds a .git entry and returns that directory,
and route `rev-parse --show-toplevel` to it. The walk fails with
NotARepositoryError outside a repository, surfacing as exit 128
on the CLI.
Agents reach for `git diff --stat` to size a change before
reading a full patch, and for `--name-only` / `--name-status` to
get a changed-file list. All three were rejected as unknown
options.

Add a diffSummary operation that reuses the existing diff
traversal to return per-file status and insertion/deletion
counts, sharing the change set with the patch path so the two
cannot drift. Route the three flags to it: --name-only prints
paths, --name-status prefixes each with its status, and --stat
renders a per-file bar with a files-changed summary footer. The
flags honor ref-to-ref comparison and revision suffixes like the
plain diff does.
`git config user.name` and `user.email` wrote to the local
config, but commit never read them back: identity resolved only
from an explicit author, then the GIT_AUTHOR_* env, then the
GitClient default. Configuring identity the way real git
documents had no effect.

Read user.name / user.email from the local repo config and slot
them between the environment and the GitClient default in the
precedence chain. An explicit author still wins and the
environment still overrides config, matching real git's
config-after-env order.
Agents stage everything with `git add -A` before committing, but
add accepted only explicit pathspecs and rejected the bare flag.

Add an all mode that walks the status matrix and stages every
change: new and modified paths through add, worktree deletions
through remove, which add alone cannot express. Wire `-A` and
`--all` to it; the flag needs no pathspec and ignores any that
are passed, matching real git.
Scripts stage and commit tracked changes in one step with `git
commit -am`, but commit took no `-a` flag and the parser could
not split the combined `-am` cluster.

Stage tracked modifications and deletions through the add all
path with a trackedOnly restriction that skips untracked files,
then commit. Expand the `-am` short cluster into `-a -m` before
parsing, leaving `-ma` for the parser since real git reads that
as `-m` with value `a`. A staging failure aborts before the
commit runs.
Agents create a branch and switch to it in one step with `git
checkout -b` or `git switch -c`. checkout had no -b shortcut and
switch was not a recognized command at all.

Add -b to checkout and a switch subcommand with -c, both routed
through a shared create-and-switch helper. The branch is created
first, optionally at a start point, and HEAD moves only after the
branch exists, so a name collision leaves the working tree
untouched. Plain `switch <branch>` moves HEAD like checkout.
Agents set aside changes, discard them, or sweep build output;
none of stash, reset, or clean existed on the shell git surface.

Add a worktree module wrapping isomorphic-git's stash (push with
an optional message, list, pop) and resetIndex, plus a clean that
derives the untracked set from a status-matrix walk and removes
paths through the filesystem. Reset covers path unstaging and
`--hard` restore-to-ref; --soft and --mixed are rejected as
unsupported. Clean refuses to run without -f unless previewing
with -n, and only descends into untracked directories with -d,
matching real git.
Bring docs/13_git_interface.md in line with the shell git
command's new behavior: the global -C option; clone deriving its
destination from the URL and leaving HEAD symbolic; status
porcelain v1; commit reading identity from local config and the
-a/-am staging flags; add -A; log -N; the rev-parse --abbrev-ref
and --show-toplevel flags and revision-suffix grammar; diff
--stat / --name-only / --name-status; checkout -b; and the new
switch, reset, stash, and clean subcommands.

Move the now-fixed items out of the known-gaps list and refresh
the scope summary, identity precedence, and flag-to-option
table.
Exercise the expanded shell git surface end to end through a real
Workspace: configure identity in local config, stage with add -A,
commit with -am, inspect with diff --stat / --name-only and a
revision suffix, branch with switch -c, then reset --hard, stash,
and clean -fd.

The smoke test keeps the agent-oriented edit/commit workflow
covered as the shell git surface grows.
Fix reset --hard so it preserves a symbolic HEAD while moving the
current branch to the target commit, then checking that branch out
to restore tracked file content. Also make git reset HEAD unstage
all staged paths instead of silently treating HEAD as a pathspec.

Strengthen the real-Workspace smoke coverage for branch switching,
ancestor hard reset, reset HEAD, and stash push/list/pop. Tighten
diff --stat counting so content lines beginning with diff-header
prefixes are counted inside hunks, and pin the stat formatter with
an exact assertion. Consolidate status-matrix tuple typing across
modules and replace a few inline import types with explicit type
imports.
Return exit 129 for git reset --mixed so the CLI matches its
documented unsupported-mode contract instead of silently treating
the command like a bare reset.

Cover hard reset from detached HEAD and reset that path by
checking out the resolved oid directly, avoiding a write through
the symbolic HEAD name.
@aron-cf
aron-cf marked this pull request as ready for review June 16, 2026 19:38
When a newly staged file is removed from disk before git add -A,
remove its index entry instead of leaving the deleted file staged.
This keeps the index aligned with the working tree for staged files
that never existed in HEAD.

The trackedOnly guard still preserves commit -a behavior by leaving
staged-but-untracked paths alone.
@aron-cf
aron-cf merged commit d7d326f into main Jun 16, 2026
9 checks passed
@aron-cf
aron-cf deleted the git-pt2 branch June 16, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant