- Inicio
- Plugins
- Git y revisión de código
- dsh-git-flow
dsh-git-flow
hunan36/dsh-git-flow
Branch chip in the composer tool row that switches, creates, and refreshes workspace branches, commits a checked subset of changed files with a message the session's own model writes from the diff, and pushes the current branch.
Instalar
dsh plugin --profile web add github:hunan36/dsh-git-flowREADME
dsh-git-flow
English | 中文
A DeepSeek Harness workspace git plugin: a branch chip in the composer tool row. Open it to browse branches, switch branches, create one, and commit a checked subset of files — the commit message is written by the session's own model from the diff.
composer tool row: [⑂ feature/login 3] ← click
├ Search branches
├ Local branches · 2 ✓ feature/login
│ main
├ New branch…
├ Commit… (enabled while the tree is dirty)
└ Refresh status
Install
Two ways in; either one is enough.
From the plugin page (GUI)
Sidebar → Plugins → Add plugin, then put this in "Package or address":
https://github.com/hunan36/dsh-git-flow
Press Install and enable it afterwards ("Enable"). That path runs pnpm add <spec> in the profile serving the page (usually web) and writes this package into dsh.profile.bundles for you — you do neither by hand. The URL is accepted with or without the .git suffix.
Reload the page (⌘⇧R) afterwards so the browser fetches the new client bundle; if that profile does not have patchReload: live, restart dsh web once for the host side.
From the CLI (a dedicated profile is recommended)
# 1. Create a dedicated profile (leaves your existing web profile alone)
dsh --profile gitflow-web --from-default-profile web
# 2. Add the plugin — a git URL, or the absolute path of a local checkout
dsh plugin --profile gitflow-web add https://github.com/hunan36/dsh-git-flow
# dsh plugin --profile gitflow-web add "$PWD"
# 3. Enable it on the plugin page ("Enable"), or edit the bundle list directly:
# dsh.profile.bundles += "dsh-git-flow"
# The package declares dsh.bundle.patch, so enabling it mounts the GitFlow
# service from the bundle's own cordis.patch.yml.
# 4. Boot
dsh --profile gitflow-web
The headless profile installs too, but it has no webServer: no /api/dsh-git-flow/* route is registered, the browser half does not exist there, and boot reports no error.
A machine without pnpm
dsh's plugin management — the plugin page's Install, and dsh plugin … — shells out to pnpm, and fails outright when there is none:
dsh: pnpm was not found; install pnpm and make it available on PATH.
Install one (Node ships npm):
npm install -g pnpm # the latest 10.x
# or: corepack enable && corepack prepare pnpm@10 --activate
Do not let Corepack use its own default: after a bare corepack enable you get pnpm 8.15.7, which walks straight into the ERR_PNPM_ADDING_TO_ROOT failure below.
If you would rather not install pnpm at all, the plugin itself does not need it (its dependencies are empty):
- Use Node's npm:
cd ~/.dsh/profiles/<profile> && npm install https://github.com/hunan36/dsh-git-flow - Copy the directory: drop the whole
dsh-git-flowfolder (includinglib/) into<profile>/node_modules/, for offline or fully manual setups
Both need one extra step: add dsh-git-flow to that profile's dsh.profile.bundles in its package.json, then restart dsh web. The plugin page and dsh plugin add do that for you; a manual install does not, and the package stays unloaded even though it sits in node_modules.
Point the plugin page at npm (a machine that only has npm)
The plugin page runs pnpm add <spec> by default, but the plugin manager's command is configurable — set it to npm in the profile's cordis.patch.yml and the page installs with npm:
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: plugin-manager
config:
pnpmCommand: npm
Then use the plugin page exactly as usual (paste the address → Install → Enable). The CLI's dsh plugin … add does not honour this setting and still needs pnpm.
Three caveats:
- This is an internal dsh setting and may move between versions; prefer installing pnpm when the machine allows it.
- The arguments dsh passes are
add <spec>,remove <name>, andview <spec> … --json, all of which npm accepts; but pnpm-specific semantics (-w,onlyBuiltDependencies, lockfiles) do not exist under npm, so do not mix both managers in one profile. - The command line shown in the UI and logs still reads
pnpm add …(dsh builds that label literally). To tell which manager really ran, look forpackage-lock.jsonversuspnpm-lock.yamlin the profile directory.
Installation fails with ERR_PNPM_ADDING_TO_ROOT
A dsh profile directory is itself a pnpm workspace (pnpm-workspace.yaml with packages: [.]), and pnpm 8 treats pnpm add there as adding to a workspace root — which it refuses. When the profile has no packageManager field, Node's Corepack fills one in (commonly pnpm@8.15.7), and the install then dies on that check before it ever resolves this plugin, so the failure is not about this package.
Any one of these gets you through:
-
Installing from the plugin page (GUI): that path runs
pnpm add <spec>and cannot pass-w, so do step 2 first (write the.npmrc) and then press "Retry" in the dialog — the same command then succeeds under pnpm 8. -
Teach the profile to accept a root dependency, then install exactly as documented:
echo 'ignore-workspace-root-check=true' >> ~/.dsh/profiles/gitflow-web/.npmrc -
From the CLI, pass
-wthrough (dsh forwards extra arguments to pnpm):dsh plugin --profile gitflow-web add -w https://github.com/hunan36/dsh-git-flow.git -
Put the profile on pnpm 10 (the version this plugin is tested with): run
corepack use pnpm@10in the profile, or set thepackageManagerfield Corepack added topnpm@10.22.0.
Note that after the first failure Corepack has already written packageManager: pnpm@8.15.7+sha512… into that profile's package.json, so every later plugin operation there uses pnpm 8 — step 4 is the durable fix.
Requires Node ^22.19.0 || >=24.0.0 and pnpm 10; this repository pins it with packageManager: pnpm@10.22.0.
Configuration
Set in cordis.patch.yml:
| Key | Default | Meaning |
|---|---|---|
timeoutMs | 15000 | Deadline for read-only and local git commands |
pushTimeoutMs | 120000 | Deadline for git push, which waits on a remote |
messageMaxDiffBytes | 65536 | Cap on the diff bytes handed to the model (truncated past it) |
messageLanguage | en | Default commit-message language, zh or en |
The commit panel carries an English | 中文 switch beside "Draft message". It overrides messageLanguage for that call and remembers the choice in localStorage, so English stays the default until you pick otherwise.
Security boundaries
- The browser sends only a
sessionId. The host resolves the repository from that session's workspace, so a page can never name a directory for git to run in. - Arguments are always an array, never a shell string, with
stdin: 'ignore'andGIT_TERMINAL_PROMPT=0, so a credential prompt can never hang a call. pushnever passes--forceor--force-with-lease, and only pushes the current branch's upstream (with--set-upstream <remote> HEAD:refs/heads/<branch>when it has none).- Commits stage only the files checked in the panel. An empty selection answers
git/no-files-selectedand never falls back to an implicitgit add -A. - Switching branches on a dirty worktree fails by default (
git/dirty-worktree). Only the user's second confirmation in the dialog sends--force. - Discarding runs only after the destructive action is picked in a confirmation dialog: tracked paths go back to HEAD with
git restore --source=HEAD --staged --worktree, untracked paths are removed withgit clean -fd(no-x, so ignored files stay), and a conflicted path is refused rather than guessed at. workspaceRegistryis read lazily throughctx.get('workspaceRegistry')instead of being declared instatic inject, so a profile without one (the shippedheadlessprofile) still activates this entry rather than leaving a permanently pending plugin; every git call there answersgit/not-a-repository.- A missing git, a non-repository workspace, a timeout, and every refusal travel as structured codes (
git/not-installed,git/not-a-repository,git/timeout, …) that the UI renders as copy. Outside a repository — or without git — the chip renders nothing at all.
Layout
src/
├── index.ts host entry (default-exports the GitFlow service)
├── service.ts GitFlow: status / branches / checkout / createBranch / commit / discard / push / generateMessage
├── git.ts GitRunner: subprocess + scrubbed env + timeout + porcelain v2 parsing
├── routes.ts /api/dsh-git-flow/* (registered only where a webServer exists)
├── message.ts commit message: ctx.llm.stream on the session's own provider/model, template fallback
├── contract.ts host↔browser types only
└── client/
├── index.ts slots.inject('conversation.input.left') + locale registration
├── BranchChip.tsx container: status, menu, dialogs, toast
├── BranchMenu.tsx the chip and its anchored menu
├── CommitDialog.tsx commit panel / new branch / forced-switch and discard confirmations
├── api.ts fetch wrapper + error codes to copy
├── styles.ts the few CSS rules the popover width and file-row ellipsis need
└── locales.ts zh/en dictionaries (en is typed against zh, so a missing key is a compile error)
Build
pnpm install
pnpm build # tsc -> lib/types/**, tsdown -> lib/index.js + lib/client.js
lib/ is committed to the repository (it is not in .gitignore) and the package no longer carries a prepare script, so installing from the git URL needs no build, no dev dependencies, and no entry in pnpm's onlyBuiltDependencies allowlist:
- pnpm 10.34+ blocks a git dependency's
prepare(ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED), and the allowlist entry it demands isdsh-git-flow@<spec>#<commit sha>— a string that goes stale on every upstream commit; - shipping the build instead makes
dsh plugin add <spec>takelib/as-is: measured at 263 ms under pnpm 10.34.5.
After editing src/, run pnpm build and commit lib/ along with it, or everyone installing from git gets the previous build.
Artifact contract:
lib/index.js: Node ESM, imported directly by the cordis loader.lib/client.js: a CJS factory wrapped aswindow.__ModuleLoader__.load({ id: "dsh-git-flow", factory: (require) => … });react,react/jsx-runtime, and@deepseek-ai/dsh-client-ui-primitivesstay external and come from the page's static module table.
Compatibility
Developed and verified against @deepseek-ai/dsh@0.1.6-alpha.2, with peer and dev dependencies pinned to it. dsh is still alpha, so contracts such as conversation.input.left, ctx.webServer.register, and ctx.slots.inject may drift between minor versions. After upgrading dsh, rebuild and confirm the first line of lib/client.js is still window.__ModuleLoader__.load({ id: "dsh-git-flow".
Known limits
- An AI commit message needs one turn already sent in the session (the provider/model recorded in
request/header). Without a route the panel answersgit/no-routeand tells you to send one message first; when the model fails or returns nothing usable, thechore: update N filestemplate fills in and says so. - Messages default to English. The panel's language switch (remembered per browser) and the
messageLanguageconfig are the only two inputs; the template fallback follows the same choice. - Remote branches appear only once
refs/remotes/**exists locally (the plugin never fetches in the background); "Refresh status" makes no network call. - Conflicted files cannot be checked; resolve the conflict in the conversation first.
git switch --forcediscards all local changes, not just the conflicting ones — the confirmation copy says exactly that. To drop a single file's changes instead, use that file's Discard action in the commit panel.- Switching to a ref that exists nowhere fails inside git and is reported as
git/failedwith git's own text. Only stale UI state can reach that path, since the menu lists branches that exist.
Verification (0.1.6-alpha.2)
Measured on an isolated DSH_HOME with profile gitflow-web (dsh-git-flow appended to dsh.profile.bundles): the chip text equals git branch --show-current; the menu lists every local branch plus each remote-only one; search filters rows; switching moves both the chip and the worktree files; a dirty switch is refused and only confirms --force after a second dialog; an invalid branch name errors while a valid one is created and switched to; with a single file checked, the model returned a feat: … message and git log -1 --name-only showed only that file; commit-and-push made the branch appear in git ls-remote with its upstream set; a session whose workspace is not a repository renders no chip; the headless profile boots with no warning and no routes.
Plugins relacionados
dsh-web (dsh-git-graph)
zhu1090093659/dsh-web
dsh-web-ui (dsh-git-graph)
zhu1090093659/dsh-web-ui
codex-guard (dsh)
akimiya-z/codex-guard
dsh-file-review
left0ver/dsh-file-review