Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4bbd6d7
feat(web-shell): unblock git update on dirty working tree
wenshao Aug 23, 2026
ee9b1f2
fix(serve): harden dirty-worktree git pull resolution (#9769)
qwen-code-dev-bot Aug 23, 2026
71a3245
fix(serve): address round-2 review of dirty-worktree git pull (#9769)
qwen-code-dev-bot Aug 23, 2026
5e03ec2
fix(serve): classify dirty-worktree pull failures from repository sta…
qwen-code-dev-bot Aug 23, 2026
c6546fd
fix(serve): address round-4 review of dirty-worktree git pull (#9769)
qwen-code-dev-bot Aug 23, 2026
a7aff45
fix(serve): address round-5 review of dirty-worktree git pull (#9769)
qwen-code-dev-bot Aug 24, 2026
82cd1ea
fix(serve): address round-6 review of dirty-worktree git pull (#9769)
qwen-code-ci-bot Aug 24, 2026
8eb70af
Merge branch 'main' into feat/git-pull-dirty-worktree
wenshao Aug 24, 2026
280fa7d
fix(serve): address round-7/8 review of dirty-worktree git pull (#9769)
qwen-code-ci-bot Aug 24, 2026
4591a65
fix(serve): address round-9 review of dirty-worktree git pull (#9769)
qwen-code-ci-bot Aug 25, 2026
4998eb1
Merge remote-tracking branch 'origin/main' into feat/git-pull-dirty-w…
qwen-code-ci-bot Aug 25, 2026
302803f
fix(serve): address round-10 review of dirty-worktree git pull (#9769)
qwen-code-ci-bot Aug 25, 2026
5ab98db
fix(core): address round-11 review of dirty-worktree git pull (#9769)
qwen-code-ci-bot Aug 25, 2026
de03ee0
Merge branch 'main' into feat/git-pull-dirty-worktree
wenshao Aug 26, 2026
a6f32be
fix(core): address round-12 review of dirty-worktree git pull (#9769)
qwen-code-ci-bot Aug 26, 2026
39737cb
Merge branch 'feat/git-pull-dirty-worktree' of https://github.com/wen…
qwen-code-dev-bot Aug 26, 2026
3a47a27
Merge branch 'main' into feat/git-pull-dirty-worktree
wenshao Aug 26, 2026
b067bcd
fix(core): address round-14 review of dirty-worktree git pull (#9769)
qwen-code-ci-bot Aug 26, 2026
29342ad
fix(core): address round-15 review of dirty-worktree git pull (#9769)
qwen-code-ci-bot Aug 26, 2026
9b25ae4
Merge branch 'main' into feat/git-pull-dirty-worktree
qwen-code-dev-bot Aug 26, 2026
db0d9a2
Merge branch 'main' into feat/git-pull-dirty-worktree
qwen-code-dev-bot Aug 27, 2026
0863e06
fix(core): close gitPull guard holes in collision probes and session …
qwen-code-ci-bot Aug 27, 2026
7dc521e
test(cli): pre-warm the lazy core import in workspace-store tests (#9…
qwen-code-ci-bot Aug 27, 2026
198e3c4
Merge branch 'main' into feat/git-pull-dirty-worktree
qwen-code-dev-bot Aug 27, 2026
7688cc3
Merge branch 'main' into feat/git-pull-dirty-worktree
qwen-code-dev-bot Aug 28, 2026
690817a
Merge branch 'main' into feat/git-pull-dirty-worktree
wenshao Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions docs/design/git-pull-dirty-worktree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Git pull with a dirty working tree

## Goal

Let the Web Shell "Update Project" action succeed when the workspace has
uncommitted changes, instead of dead-ending at an opaque
`dirty_working_tree` error that forces users back to a terminal.

## Problem

The workspace git pull path (branch picker popover → SDK
`workspaceGitPull` → `POST /workspace(s)/git/pull` → core `gitPull`) ran a
plain `git pull`. With local modifications to files the incoming merge
touches, git refuses to merge; the route already classified that failure as
`409 dirty_working_tree` with a path-redacted git message, but the popover
only rendered the SDK's raw error string, so nothing actionable surfaced.

## Behavior

`POST /workspace/git/pull` and `POST /workspaces/:workspace/git/pull`
accept two new boolean options (both default false, mutually exclusive —
sending both is a 400):

- `stash`: stash local changes (`git stash push --include-untracked`, so
untracked files that could block the merge are covered), run the pull,
then `git stash pop`. If the pull fails, any partial merge/rebase is
aborted and the stash is popped back, restoring the pre-pull state. If
the restore fails after a successful pull — a conflict, or an
untracked-file collision — the response is still a success with
`stashRestoreConflict: true` and its `output` carries git's failure
notice; git keeps the stash entry, so nothing is lost.
- `force`: discard all local changes first (`git reset --hard` +
`git clean -fd`; ignored files are kept), then pull. Destructive.
`git reset --hard` acts on the whole repository regardless of the cwd,
but `git clean -fd` run from a subdirectory only removes untracked
files inside that subtree, so `force` is refused when the workspace
cwd is below the repository root (it would destroy tracked changes
outside the workspace while leaving the untracked files that block the
merge in place). The pull is validated before anything is
discarded: the fetch runs first and a diverged branch (local commits
on both sides) is refused, because the post-discard merge could wedge
the repository mid-merge; a failed post-discard pull still aborts any
partial merge/rebase.

Stash detection compares `refs/stash` before/after the push instead of
parsing git output, which varies by version and locale. The pull itself
passes `--no-rebase --no-edit` when rebase is not requested, so divergent
branches merge instead of fataling on git builds without a `pull.rebase`
/ `pull.ff` policy.

## UI

When the plain pull fails with `409 dirty_working_tree`, the branch picker
footer switches from the status line to a resolution panel offering:

- **Stash Changes and Update** — pulls with `stash: true`.
- **Discard Changes and Update…** — destructive, so it takes a second
click: the panel swaps to a warning plus a confirm button that pulls with
`force: true`.
- **Cancel** — dismisses the panel.

The panel stays mounted (with a spinner on the clicked button) while its
own stash/discard pull is in flight, and resets whenever the popover is
reopened. When the blocking state is an unmerged working tree (conflicting
restore or abandoned merge), the stash option is replaced by an
explanation — `git stash push` refuses unmerged entries — leaving discard
as the recovery path, except in workspaces below the repository root,
where discarding is unsupported and the conflicts must be resolved from
a terminal.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to the next round: this round implemented the review rounds 7-8 Critical batch (collision-probe restructure to enumeration, cherry-pick/revert foreign-state guard, fail-closed stash probe, merge/rebase of the captured tip, stash-restore note kept outside the message cap, and the serialization-test gate) under the per-round batch cap and the time budget this workflow flagged. This finding stays queued for the next round rather than being dropped. Docs/UI mismatch on the subdirectory unmerged-panel exception stays queued.

顺延至下一轮:本轮在单轮批次上限与工作流时间预算内实现了第 7/8 轮评审的 Critical 批次(将冲突探测重构为枚举比对、cherry-pick/revert 外部状态守卫、暂存探测失败即拒绝、合并/变基到已捕获的上游 tip、让暂存恢复提示在消息截断时保留、并发串行化测试的环境门槛)。该发现继续排队等待下一轮处理,不会被丢弃。


## Ownership

Both routes keep their existing scoping: the legacy route is
legacy-primary-workspace scoped; the qualified route resolves the trusted
runtime and contained `cwd` exactly as before. The new options only change
which git commands run inside the resolved workspace and add no new trust
surface.

## Alternatives considered

Automatic stash on every dirty pull was rejected: silently moving the
user's changes through a stash is surprising, and the discard option is
destructive, so both need an explicit choice. `git pull --autostash`
performs that same stash-then-restore implicitly, so it was not adopted.
Loading
Loading