The glob guard compared the name with `_sanitize_branch`, which also
rewrites valid names such as "fix+1" or "feat/foo@bar". Their base
fetch was skipped, so the new worktree started from a stale tracking
ref. The Python and Electron guards also disagreed on non-ASCII names,
because Python's `\w` is Unicode and JavaScript's is ASCII.
Ask git instead: `git check-ref-format --branch` accepts every valid
branch name and rejects the refspec metacharacters (`* ? [ : ^ \`),
and both twins now make the same call.
The remote fallback for narrow clones read any "<remote>/<name>" with no
tracking ref as a remote branch, even when it was an existing local
branch such as "origin/feature". That meant a network fetch on every
such checkout, and when the remote had the branch, the request quietly
became a new `feature` tracking it instead of the user's own branch.
Check refs/heads/<requested> first, in the web_git mirror and in the
Electron twin.
The tests share one seeded origin. `feature` now sits past tag v0, so
"branched from the remote, not the tag" has teeth. The pair of
conversion tests is parametrized, and #125699's hand-built narrow clones
use the shared helpers.
Converting "origin/<branch>" registered the branch on the remote
(`remote set-branches --add`) before fetching it, on every clone shape.
For a branch the remote doesn't have, such as a typo, that left a
refspec whose source is missing, and every later plain `git fetch` in
the repo failed with "couldn't find remote ref". Normal clones also
gained a redundant fetch line for each converted branch.
Fetch with the explicit tracking refspec first and try `--track`, which
needs no config change on a normal clone. Only when `--track` cannot
map the ref (a narrow clone) and the fetch proved the branch exists is
the branch registered and its upstream set.
The base arm reuses the same fetch, and only for names the branch
sanitizer leaves unchanged: `base` comes from the API, and "origin/*"
inside a refspec fetches every branch.
`existingBranch: "origin/<branch>"` failed on both halves of the
existing-branch conversion arm when the checkout is a tag-pinned
narrow clone (remote.origin.fetch maps only the tag):
- tracking ref absent: the ref-based remote detection misread
"origin/feature" as a local branch name, so worktree creation died
with `fatal: invalid reference`;
- tracking ref present: `worktree add --track` could not reverse-map
the ref through the tag-only refspec, dying with `cannot set up
tracking information`.
Register the branch on the remote additively (set-branches --add
keeps the existing refspec), fetch it through the updater's forced
refspec, and fall back to explicit `--set-upstream-to` wiring when
`--track` cannot do it. Normal clones keep today's behavior: the
wildcard refspec already covers the branch, so the registration is a
no-op.
Complements #125699, which fixes the `base: "origin/<branch>"` arm.
Fixes#125686 (existing-branch half).
(cherry picked from commit 8b32ee69b7879804b74b5e59cc99deb5ba0a3dbc)
Tag-pinned narrow clones map remote.origin.fetch to the tag, so a
by-name fetch never creates origin/<branch> and worktree add fails
with an invalid reference. Use the same refspec the updater already uses.
(cherry picked from commit 673aa8491f7b9a064445dd8b69992cd871e64365)
Every non-zero gh exit collapsed to a generic "is gh installed and
authenticated?" — a lie whenever gh was fine and the real failure was
"no commits between main and feature", a missing upstream, or a refused
push (#87731). The user had to drop to a terminal to learn what gh
already printed.
- apps/desktop git-review-ops.ts runGh() now resolves {ok, stdout,
stderr} (execFile's err.stderr carries the exit's own stderr), and
reviewCreatePr() prefixes the surfaced message with gh's reason,
keeping the generic text only when gh reported nothing.
- hermes_cli web_git.py _gh() keeps (ok, stdout, stderr) through the
same collapse — _run already captured stderr; it was discarded at the
tuple boundary — and review_create_pr() surfaces a bounded stderr
tail (400 chars) plus the gh context.
Tests pin the contract with unique stderr markers and non-zero exits on
both wrappers; successful-path return contracts unchanged.
Electron half salvaged from PR #87751 (author preserved); CLI half added
per the issue's acceptance scope.
Fixes#87731
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.
Hermes gathers workspace context by running git against the session
directory automatically — the coding-workspace snapshot, gateway
project-tree build, /diff, @diff|@staged context refs, goal-gate
fingerprint, and -w startup worktree add — before any prompt, tool call,
approval, or trust gate. Those probes ran the system git without
stripping the repository's own config, so a repo delivered as files with
its .git directory intact (a shared zip, sync folder, or USB stick;
git clone never transfers .git/config) could set an execution-sink git
setting and get arbitrary host code execution as the user with nothing
on screen.
- core.fsmonitor / core.hooksPath / pager / editor / credential helper:
neutralized by routing every automatic probe through
noninteractive_git_env(), which pins those keys to inert values via
GIT_CONFIG_* and ignores global/system config. bounded_git_probe (the
reported sink, coding_context._git + tui_gateway.git_probe) now defaults
to that env; worktree-add, working_diff, web_git, context_references,
goals, and subagent_worktree route through it too.
- Attribute-scoped [diff "x"] command=/textconv= drivers: the attacker
names the driver in .gitattributes, so GIT_CONFIG_KEY overrides can't
enumerate them. Added harden_git_argv(), which inserts
--no-ext-diff --no-textconv on diff-rendering subcommands (diff/show/
log/blame) only — status et al reject the flags. Both flags required
(verified empirically; each alone leaves the other live).
Builds on the noninteractive_git_env config-scrubbing from the
gemini-cli #28792 port. Real-git E2E regression suite arms a malicious
repo and asserts every automatic path neutralizes fsmonitor, hooks,
external-diff, and textconv; a baseline test proves the repo is armed.
Cmd/Ctrl+Shift+B worktree flows on a remote gateway route through the
backend's /api/git mirror (hermes_cli/web_git.py), but that mirror had
drifted behind the Electron-local git ops the same UI drives locally, so
the flows broke exactly and only on remote connections:
- Convert-a-branch: the picker offers remote-tracking refs, and the
Electron op turns "origin/feature" into a local tracking branch. The
mirror ran `git worktree add <dir> origin/feature` verbatim, which
either fails or detaches HEAD. It now resolves the ref's remote via
git (never assuming "origin"), fetches best-effort, and creates the
worktree with `--track -b <short-name>`.
- branch_list omitted remote-tracking refs entirely and never set the
`isRemote` flag the renderer's HermesGitBranch contract requires —
the convert picker on a remote gateway couldn't reach a teammate's
branch and mislabeled every row's action.
- Branching off an `origin/…` base silently wired the new branch to the
remote upstream; the mirror now passes `--no-track` like the Electron
op does.
Renderer side, replace the silent degradation with a capability gate:
when a remote backend predates the /api/git worktree routes, worktree
creation failed with an opaque "Expected JSON … got HTML" toast. The
route-missing shapes now surface a clear "update the Hermes backend"
message (isGitEndpointMissingError, mirroring the sidebar batch-endpoint
detector); real git errors still pass through untouched.
Sibling audit (documented, no code change needed): repo status / review /
file-diff / git-root / default-cwd already route through desktopGit()'s
REST bridge or /api/fs on remote; repo scan is deliberately a no-op there.
Stale comments claiming "empty/false on a remote backend" in projects.ts
and coding-status.ts updated to describe the backend-routed reality.
Fixes#81724
A session row can say whether its work is open, merged or closed, and link
to it. The join is the session's own repo + branch, asked of GitHub in one
batched GraphQL request per repo (branch aliases, not a `gh pr list` page
that a busy repo crowds ours out of), through the remote-aware git facade so
a desktop on a remote gateway asks the backend's `gh`.
Two ways a session's branch can't answer, both covered:
- It ran on trunk. Fork PRs share our branch namespace, so asking about
`main` badges a stranger's PR onto it — trunk is never asked about, and
cross-repository PRs are dropped server-side either way.
- It worked in a worktree, so the branch it recorded at start isn't where
the PR came from. Creating a PR from the review pane binds the session to
the branch it actually used, and for sessions that predate that, the PR is
recovered from the transcript: `gh pr create` prints a bare PR url and
nothing else, so a tool result whose whole output is one is a claim rather
than a mention. Scanned read-only across profiles, once per session ever.
Port from openai/codex#34540 / #34612 ("detach non-interactive
subprocesses from stdin"): internal git invocations that run with nobody
attached — MCP catalog installs, plugin install/update, profile
distribution staging, worktree base fetches, and the desktop review
pane's git/gh backend — could hang on a credential prompt when a remote
is private, misconfigured, or requires auth. git prompts on the
inherited terminal (or via Git Credential Manager on Windows), so the
operation silently waits until its timeout, or forever at sites without
one (mcp_catalog clones have no timeout at all and inherit the parent
terminal).
- Add noninteractive_git_env() to hermes_cli/_subprocess_compat.py:
GIT_TERMINAL_PROMPT=0 + GCM_INTERACTIVE=Never on a copy of the
environment; GIT_ASKPASS/SSH_ASKPASS deliberately preserved so
working non-interactive auth still succeeds.
- Wire it + stdin=DEVNULL into: mcp_catalog._do_git_install (clone/
checkout), plugins_cmd (clone + pull), profile_distribution._git_clone,
web_git._git/_gh (gh also gets GH_PROMPT_DISABLED=1), and cli.py's
worktree base fetch helper.
- Tests: env contract, a real-git E2E against a local 401 Basic-auth
HTTP server proving fail-fast ("terminal prompts disabled") instead
of a hang, and per-call-site plumbing assertions. Sabotage-verified:
removing the env from web_git._git fails the site test.
On Windows with Chinese locale (GBK), subprocess.run(text=True) without
explicit encoding causes UnicodeDecodeError crashes. This fix adds
encoding='utf-8', errors='replace' to all subprocess.run() and
subprocess.Popen() calls that use text=True across 76 non-test Python files.
Fixes#53428 (master tracker for Windows GBK locale crash).
Note: credential_pool.py and electron changes excluded per reviewer request —
those will be submitted as separate focused PRs.
When the base is an origin/… ref, fetch just that branch so the
local tracking ref is fresh before `git worktree add -b new origin/main`.
Fetch failures (offline / no remote) are silently ignored — git uses
whatever local ref exists, or raises a clear error if it's missing.
The sidebar "New worktree" button branched off whatever HEAD you were
on — now a filterable Popover+Command combobox lets you pick any local
or remote-tracking branch as the base, defaulting to origin/HEAD.
Backend listBaseBranches() queries refs/heads + refs/remotes via
for-each-ref, flags origin/HEAD (falling back to the local default for
no-remote repos). Mirrored on the Python REST API side
(base_branch_list + /api/git/base-branches route).
Collapse the two near-duplicate status parsers (_parse_status_v2 +
_iter_status_entries) into a single _walk_entries generator feeding the rail,
review list, and commit flow; share the staged predicate; hoist `import re`.
Behavior unchanged.
After the folder picker fix, an added remote folder was still half-usable:
the desktop's git GUI (coding-rail status, worktree lanes, review pane,
branch switch, file diff) all ran Electron-local git on the USER's machine,
so against a remote-gateway repo they silently degraded to empty.
Mirror the whole surface over the dashboard REST API so it acts on the
BACKEND repo where sessions actually run:
- hermes_cli/web_git.py: git/gh logic (status, worktrees, branches, review
list/diff/stage/unstage/revert/commit/commit-context/push/ship-info/
create-pr, file-diff, worktree add/remove, branch switch) shelling to the
system git, mirroring the Electron ops' shapes.
- web_server.py: /api/git/* routes (same auth gate + _fs_path hardening as
/api/fs, executor-offloaded, mutations -> 400).
- apps/desktop desktop-git.ts: remote-aware facade exposing the same shape as
window.hermesDesktop.git; coding-status / review / projects / model /
desktop-fs route through desktopGit() so local stays Electron, remote hits
/api/git/*.
Tests: tests/hermes_cli/test_web_server_git.py (real repo: status counts,
review classification, diff incl. untracked all-add, stage+commit roundtrip,
worktree/branch lifecycle, commit-context, gh-absent ship-info, auth) and
desktop-git.test.ts (local vs remote routing, envelope unwrap, POST bodies).