Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Workspace shell exposes a custom
gitcommand backed by@cloudflare/workspaceandisomorphic-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, resolvingHEAD~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
gitsurface while keeping the Workspace-native implementation. The CLI now supports top-level-C, real-Git-like default clone destinations, symbolicHEADafter clone, current-branch helpers,log -N, porcelain v1 status, revision suffixes such asHEAD^andHEAD~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.nameanduser.emailnow provide commit identity,add -Astages new, modified, and deleted paths, andcommit -a/commit -amstages tracked changes before committing without adding untracked files. The branch and work-tree commands now covercheckout -b,switch,switch -c,stash push/list/pop,resetpath unstaging,reset --hard, andclean -f[d][n].The hard-reset path preserves symbolic
HEADand moves the current branch before restoring tracked files, soreset --hard HEAD~1leavesbranch --show-currentintact and rewrites the working tree to the target commit.git reset HEADnow unstages all staged paths instead of silently treatingHEADas a pathspec.The git package tests cover the parser layer with fake clients and the behavior layer with real
isomorphic-gitplus in-memory filesystems. The real Workspace smoke test exercises the agent edit loop end to end: configure identity,add -A,commit -am, inspect withdiff --statand revision suffixes, branch withswitch -c, runreset --hard, use stash, and clean untracked files.Documentation
docs/13_git_interface.mdnow 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 -fddoes 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.