Skip to content

Commit 6db765c

Browse files
committed
fix(vcs): clarify selected commit and stash recovery
🤖 Co-authored by GPT-6 in Codex via T3 Code
1 parent fecd55a commit 6db765c

4 files changed

Lines changed: 133 additions & 16 deletions

File tree

‎FORK.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,7 @@ Thread source-control metadata update failures should surface on the thread with
810810

811811
`apps/web/src/components/BranchToolbarBranchSelector.tsx` keeps its virtualized ref list inside the web type-check guard by typing key and item-type callbacks as `string` and the row renderer as `LegendListRenderItemProps<string>`; branch selection does not cross an implicit `any` boundary.
812812

813-
Selected commits reconcile the real index in an interruption-safe success finalizer before temporary-index cleanup. Generation and commit execution remain cancellable; cleanup cancellation must not leave a completed commit staged again. If reconciliation fails after the commit is durable, report that the commit exists, stop before push, and provide index recovery rather than inviting another commit.
813+
Selected commits reconcile the real index in an interruption-safe success finalizer before temporary-index cleanup. Generation and commit execution remain cancellable; cleanup cancellation must not leave a completed commit staged again. If reconciliation fails after the commit is durable, report that the commit exists, stop before push, and provide a shell-quoted, bounded index recovery command rather than inviting another commit. A stash pop whose apply succeeded but removal failed reports that distinction and warns against reapplying.
814814

815815
Required edge cases: the current default branch remains a valid default compare ref and retains that stable base in its own branch details, status-derived default branch names such as `develop` are preferred over hardcoded `main`/`master` guesses, compare-history pagination queries the selected comparison range, branch pull/fetch parsing handles slashful remotes and remote-looking local branch names without treating slashless local upstreams as remote refs, fetch-before-sync refreshes the authoritative snapshot before choosing the same action's push/pull/diverged result and does not fetch an unchanged branch twice, the panel and composer share live Git status with a per-environment checkout fallback remembered for the current session, known non-Git projects disable the Source Control action and hide any retained surface while status reloads, and unseen checkouts assume Git until VCS status resolves, diverged normal merge sync is available only for the current branch, checked-out branch worktree paths fall back from porcelain worktree output to branch-format placeholders without failing on older Git versions, sibling worktree watcher refreshes keep root Actionable rows live while skipping stale/prunable worktree paths, working-tree refreshes that race an authoritative full snapshot cannot retain pre-mutation branch/remote/stash data, failed full snapshots release the in-flight full-refresh barrier so later working-tree refreshes can remain incremental, queued web refreshes still drain when the active refresh fails or is interrupted, duplicate web and mobile actions with the same key are synchronously suppressed while the first mutation remains in flight, web and mobile stash actions carry the selected immutable SHA beside the positional ref, reject shifted or missing selections, and serialize app-owned mutations by canonical Git common directory across linked worktrees, branch sync and undo operations for checked-out branches target the owning worktree cwd, upstream remote identity keeps local-to-local tracking separate from remote pairing and sync, local and remote deletion preserve the selected identity when names collide and reject a missing target without crossing into the other kind, checkout and deletion remain rejected by both the client and server for branches checked out in a worktree whose directory exists, a local branch still registered to a worktree whose directory is gone (`prunable` in `git worktree list --porcelain`) is pruned with `git worktree prune` before `git branch -d`/`-D` runs, local delete failures for unmerged branches or branches still used by a worktree surface as stable details that copy only the branch name and worktree path Git reports while unmatched stderr stays behind the generic Git failure, cwd-scoped working-tree enrichment avoids cross-worktree file-detail reuse, a fallback File surface uses its own cwd and reveal metadata, stale background-stop failures cannot clear a newer thread's pending stop, selected-file commits omit pathspecs after staging, commit-hook output only enriches a failed Git result and never interrupts a still-running commit, merge refs are passed after `--`, tracked discard restore failures surface instead of being swallowed, fallback rename parsing preserves original paths, empty working-tree diffs use the full-file fallback only for paths Git currently lists as untracked in panel-cwd-relative form, Review patches disable user-configured diff rendering, merged staged-plus-unstaged row stats are summed, collapsed mobile remotes hide their branch rows, mobile conflict-only rows open the working-tree diff side, sibling mobile working trees are not marked expansion-initialized until they become current, failed mobile branch/stash details replace loading placeholders with errors, invalid mobile branch and stash dates are omitted instead of appearing as recent activity, and late-month relative dates do not fall through to `0 years ago`.
816816

‎SOURCE_CONTROL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ Non-current branch fetches are scoped to the selected branch. Operation busy sta
299299

300300
On web, a normal ahead-branch push coordinates the same logical project across connected environments. Before pushing, it records only peers whose current branch has the same name, has a clean working tree and no local commits ahead of its upstream, and tracks the same normalized remote URL and remote branch. Peers already behind remain eligible so missed updates can catch up. After the source push succeeds, each prepared peer fetches that branch and receives a fast-forward-only pull only if a fresh snapshot still shows the same clean checkout with no local commits. A peer that changed during the push is skipped. Peer reads and mutations are best-effort so a disconnected peer or failed peer fetch cannot turn the already-completed source push into a reported push failure. Force pushes, unpublished branches, diverged branches, different remotes, and disconnected environments are not auto-pulled. Collapsed connected environments remain eligible because the coordinator uses the parent panel's complete target list rather than mounted child sections.
301301

302-
Selected-file commit, stash, and discard operations preserve rename source paths when needed, so selecting an `R` row sends both the destination path and the original path to the Git operation; copied rows send only their selected destination because the original is still a separate live file. Rename source paths are preserved for normal porcelain status entries and for fallback numstat parsing, including line-based fallback output. Porcelain status paths are decoded from Git's quoted path format before matching numstat output or rendering rows, so non-ASCII and escaped path names use the real repository path. Shared staging, unstaging, temporary intent-to-add, selected-file stash, and every discard classification/mutation command treat selected paths literally. Selected-file commits construct a temporary index from `HEAD`, or an empty index when `HEAD` is unborn, stage only the selected paths into that index with literal-pathspec semantics, generate the message and commit without pathspecs through that isolated index, then reset the selected real-index entries to the new `HEAD` in an interruption-safe success finalizer before temporary-index cleanup. Message generation and commit execution remain cancellable; cancellation during cleanup cannot skip reconciliation of a completed commit. Selected staged deletions retain their removal intent even when the files still exist on disk, as with `git rm --cached` followed by an ignore rule; adding those paths again would either fail on ignored files or silently undo the removal. Resetting the real-index entries also avoids staging working-tree edits made after the commit snapshot. This keeps mixed selected files complete while excluding deselected changes that were already staged in the user's real index; those deselected changes remain staged after the commit, while generation or commit failures leave the real index untouched. If post-commit real-index synchronization fails or defects, the server reports that the commit already exists, prevents push, and asks the user to reset only the selected paths before pushing the existing commit. Retrying the commit would duplicate completed work. Discard operations also split staged and unstaged portions explicitly. Server-side discard handling partitions tracked, untracked, HEAD-backed, and newly added paths before running Git restore/reset/clean commands, so mixed selections such as tracked edits plus untracked files do not cause one path class to prevent the rest of the selected discard from applying. If the tracked unstaged restore step fails, the failure is reported to the panel instead of being swallowed after staged cleanup succeeds. Compare RPC results preserve left-to-right direction, including reversing the patch when the working tree is the left operand and a branch or stash is on the right. Switching a remote ref creates a local tracking branch; configured remote names, including names containing slashes, determine the local branch name. When the remote branch's short name already belongs to a local branch tracking another upstream, Git chooses the next available suffixed local name instead of checking out the remote ref in detached-HEAD state. Merge and rebase operations pass the selected ref after a positional `--` separator so option-shaped refs cannot be interpreted as command flags.
302+
Selected-file commit, stash, and discard operations preserve rename source paths when needed, so selecting an `R` row sends both the destination path and the original path to the Git operation; copied rows send only their selected destination because the original is still a separate live file. Rename source paths are preserved for normal porcelain status entries and for fallback numstat parsing, including line-based fallback output. Porcelain status paths are decoded from Git's quoted path format before matching numstat output or rendering rows, so non-ASCII and escaped path names use the real repository path. Shared staging, unstaging, temporary intent-to-add, selected-file stash, and every discard classification/mutation command treat selected paths literally. Selected-file commits construct a temporary index from `HEAD`, or an empty index when `HEAD` is unborn, stage only the selected paths into that index with literal-pathspec semantics, generate the message and commit without pathspecs through that isolated index, then reset the selected real-index entries to the new `HEAD` in an interruption-safe success finalizer before temporary-index cleanup. Message generation and commit execution remain cancellable; cancellation during cleanup cannot skip reconciliation of a completed commit. Selected staged deletions retain their removal intent even when the files still exist on disk, as with `git rm --cached` followed by an ignore rule; adding those paths again would either fail on ignored files or silently undo the removal. Resetting the real-index entries also avoids staging working-tree edits made after the commit snapshot. This keeps mixed selected files complete while excluding deselected changes that were already staged in the user's real index; those deselected changes remain staged after the commit, while generation or commit failures leave the real index untouched. If post-commit real-index synchronization fails or defects, the server reports that the commit already exists, prevents push, and asks the user to reset only the selected paths before pushing the existing commit. Retrying a completed selected-file commit before reconciling the index can stage deletions for newly added files because their stale real-index entries no longer match the new HEAD. Discard operations also split staged and unstaged portions explicitly. Server-side discard handling partitions tracked, untracked, HEAD-backed, and newly added paths before running Git restore/reset/clean commands, so mixed selections such as tracked edits plus untracked files do not cause one path class to prevent the rest of the selected discard from applying. If the tracked unstaged restore step fails, the failure is reported to the panel instead of being swallowed after staged cleanup succeeds. Compare RPC results preserve left-to-right direction, including reversing the patch when the working tree is the left operand and a branch or stash is on the right. Switching a remote ref creates a local tracking branch; configured remote names, including names containing slashes, determine the local branch name. When the remote branch's short name already belongs to a local branch tracking another upstream, Git chooses the next available suffixed local name instead of checking out the remote ref in detached-HEAD state. Merge and rebase operations pass the selected ref after a positional `--` separator so option-shaped refs cannot be interpreted as command flags.
303303

304304
Generated commit and stash messages use the registered checkout project's effective `Source control writer model` and writing-style overrides. Unregistered sibling worktrees inherit the main checkout project's settings; unavailable project lookup leaves environment defaults in effect. When that setting is disabled or its configured writer is not usable in the current provider snapshot, they fall back to the configured `Text generation model`. Both the panel and the generic Git workflow resolve `Source control writing style` through the same repository-context policy reader, so Version Control does not add its own writing instructions. Repository-convention mode reads recent commit subjects and the repository's `AGENTS.md`; Claude writers also read `CLAUDE.md`.
305305

‎apps/server/src/sourceControl/SourceControlPanelActions.ts‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -572,8 +572,14 @@ export function makeSourceControlPanelActions(
572572
return yield* gitError(
573573
"vcs.panel.commitStaged.syncIndex",
574574
input.cwd,
575-
["reset", "HEAD", "--", ...paths],
576-
"The commit was already created but was not pushed. Do not commit these changes again. Reset only the selected paths with git reset HEAD -- <selected paths> to reconcile the index, then push the existing commit if needed.",
575+
[
576+
"--literal-pathspecs",
577+
"reset",
578+
"HEAD",
579+
"--pathspec-from-file=-",
580+
"--pathspec-file-nul",
581+
],
582+
"The commit was already created but was not pushed. Do not commit these changes again. Reset only the selected paths with git --literal-pathspecs reset HEAD -- <selected paths>, quoting each pathname for your shell, to reconcile the index. Then push the existing commit if needed.",
577583
sanitizeErrorCause(indexSyncExit.cause),
578584
);
579585
}
@@ -990,27 +996,30 @@ export function makeSourceControlPanelActions(
990996
yield* run(operation, input.cwd, ["stash", action, stashRef]);
991997
return;
992998
}
993-
const verify = Effect.gen(function* () {
994-
const sha = (yield* run(operation, input.cwd, [
995-
"rev-parse",
996-
"--verify",
997-
`${stashRef}^{commit}`,
998-
])).trim();
999+
const verify = Effect.fnUntraced(function* (applied = false) {
1000+
const sha = (yield* run(
1001+
operation,
1002+
input.cwd,
1003+
["rev-parse", "--verify", `${stashRef}^{commit}`],
1004+
{ allowNonZeroExit: true },
1005+
)).trim();
9991006
if (sha.toLowerCase() !== input.expectedSha?.toLowerCase()) {
10001007
return yield* gitError(
10011008
operation,
10021009
input.cwd,
10031010
["stash", action, stashRef],
1004-
"The selected stash has changed. Refresh Source Control and select the stash again.",
1011+
applied
1012+
? "The stash changes were applied, but this operation did not drop the stash because its position changed. Refresh Source Control before dropping it; do not apply it again."
1013+
: "The selected stash has changed. Refresh Source Control and select the stash again.",
10051014
);
10061015
}
10071016
return sha;
10081017
});
1009-
const sha = yield* verify;
1018+
const sha = yield* verify();
10101019
if (action === "apply" || action === "pop") {
10111020
yield* run(operation, input.cwd, ["stash", "apply", sha]);
10121021
}
1013-
if (action === "pop") yield* verify;
1022+
if (action === "pop") yield* verify(true);
10141023
if (action === "drop" || action === "pop") {
10151024
yield* run(operation, input.cwd, ["stash", "drop", stashRef]);
10161025
}

0 commit comments

Comments
 (0)