Skip to content

fix: stop the Git workflow getting stuck after a push or page reload - #37

Merged
uniskela merged 2 commits into
mainfrom
fix/continue-after-pushed-commit
Oct 7, 2026
Merged

uniskela merged 2 commits into
mainfrom
fix/continue-after-pushed-commit

Conversation

@uniskela

@uniskela uniskela commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Two dead ends found while preparing v0.5.0. In both, Stack Manager could no longer commit or push without someone getting into the container to fix it. Each is a separate commit with its own regression test.

1. Commits blocked forever after a pushed commit is built on elsewhere

After Stack Manager pushed a commit, the private retained ref (refs/stack-manager/heads/<branch>) stayed on that commit. As soon as anyone else pushed on top of it (for example from a laptop), every later commit returned remote_changed, and fetching could not clear it.

Fix (GitMutations.fetchState): a retained head that the remote already contains counts as published work. When the last fetched remote-tracking head is newer than that published head, the branch state continues from the fetched head. This is the same behaviour as when no commit was ever retained.

  • Right after a push, the status still reports the pushed commit (ahead 0, behind 0).
  • A retained commit that was never pushed still blocks while the remote is ahead (unchanged).
  • The ref compare-and-swap on commit still uses the real ref value. Inspection moves or deletes nothing.
  • If the ancestry check fails, the old, stricter behaviour applies.

Test: continues committing after a pushed commit is built upon elsewhere (integration, real Git server). It fails on main with expected 'remote_changed' to be 'commit_succeeded'.

2. A kept local commit could not be pushed after leaving the Changes tab

The Push button for a kept commit (after Commit, or after a failed push) lived only in React state. A reload or a visit to History lost it. Committing the same drafts again then failed with draft_outdated, because they're already in that commit. So there was no way to push from the UI.

Fix (ChangesView.applyResult): workflow responses already include the branch state. When a commit attempt reports unpushed local commits (ahead > 0) and the remote hasn't moved (behind 0), the tab offers Push for the local head again. The server still re-checks the exact retained commit and remote head before pushing.

Test: e2e an unpushed commit from an earlier visit can still be pushed (mocked workflow responses; checks the push request body).

Validation

  • pnpm check: format, lint, typecheck, 26 files / 348 tests pass.
  • pnpm build passes.
  • pnpm test:e2e with E2E_GIT_REMOTE=https://github.com/docker/awesome-compose.git E2E_STACK_ROOT=nginx-golang: 33 passed, 9 skipped (mobile-only skips).

User-facing docs for these behaviours are in the separate docs: prepare v0.5.0 Git workflow release PR. Please merge this PR first.

🤖 Generated with Claude Code

https://claude.ai/code/session_018E16JisaGNSCUUfrJpDmuZ

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of repositories updated from another checkout. After syncing, you can commit a draft on top of the latest remote changes and push it successfully.
  • New Features
    • You can push a retained local commit from an earlier visit when it is ahead of the remote branch.

After Stack Manager pushed a commit, its private retained ref stayed on
that commit. Once anyone else pushed on top of it, every later commit
reported "remote changed" forever, and fetching could not clear it.

A retained head that the remote already contains is published work, so
the branch state now continues from the fetched head when that is newer,
exactly as it does when no commit was ever retained. Unpushed retained
commits keep blocking as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018E16JisaGNSCUUfrJpDmuZ
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Walkthrough

GitMutations now checks commit ancestry when selecting a local head after fetching. The UI retains commit details when the local branch is ahead of the remote. Tests cover remote advancement, repository sync, a subsequent commit, and a successful push.

Changes

Remote advancement handling

Layer / File(s) Summary
Head selection and workflow validation
src/server/providers/git/mutations.ts, tests/integration/git-workflow.test.ts
fetchState falls back to the cached remote head when the retained head is an ancestor of the fetched head, but the cached head is not an ancestor of the retained head. The integration test verifies that syncing after a remote advance allows the retained draft to be committed on top of the advanced remote head and pushed.
Retained commit push
src/ui/source/changes-view.tsx, tests/e2e/changes-commit.spec.ts
applyResult saves the local head SHA, remote head SHA, and branch when a non-push result reports commits ahead and none behind. The end-to-end test checks that the UI offers a push action for the retained commit and sends the expected commit and remote SHAs.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 0a643

The changed workflow blocks a commit until the advanced remote is synced, then allows committing on top of it. No actionable merge-blocking risk remains.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 0a643

The recovery change preserves the existing authorization and push safeguards. No new publishing bypass was identified in the reviewed flow, but broader security coverage and recovery after abrupt process interruption remain unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • observed — Each publication targets the stored repository connection's current branch and remote using its existing credentials. The maximum existing application authority is an enabled admin's access across workspaces, not a newly granted per-repository capability.

Security Findings and Attack Paths

  • inferred — Making an older local commit available in the UI does not establish a stale-SHA or cross-repository publication bypass. A manipulated request must still match the current repository's retained branch ref and freshly fetched remote head before reaching the push command. No bypass was identified in this inspected path.

Trust Boundaries and Controls

  • observed — The client controls the submitted SHA claims, but not the authenticated actor or stored Git destination. Server-side access resolution supplies the branch, clone, remote URL, and credentials after actor and repository checks.
  • observed — The provider validates both object IDs, locks the repository, requires retained-ref equality, checks the fetched remote SHA and behind count, verifies commit metadata, and sends an ordinary push without force, mirror, or tag-following behavior. These controls remain unchanged despite the new UI caller state and ancestry-based selection.

Resilience and Maintainability Implications

  • observed — Existing containment uses canonical clone-path checks, an exclusive filesystem lock with finally-based release, cleanup before retained-ref publication, and compare-and-swap against the actual old ref. These mechanisms cover ordinary failure and concurrency paths; the inspected implementation does not demonstrate automatic recovery from a lock left by abrupt process termination. That limitation predates this PR.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 4 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title describes the Git workflow issue addressed by the changes, including recovery after a push or page reload.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

The Push button for a kept local commit lived only in page state, so a
reload or a visit to another tab lost it. Committing the same drafts
again then failed as outdated (they are already in that commit), leaving
no way to push from the UI.

Workflow responses already carry the branch state. When it shows
unpushed local commits and the remote has not moved, the Changes tab now
offers Push for the local head again; the server still re-checks the
retained commit and remote head before pushing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018E16JisaGNSCUUfrJpDmuZ
@uniskela uniskela changed the title fix: keep committing after a pushed commit is built on elsewhere fix: stop the Git workflow getting stuck after a push or page reload Oct 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔇 Additional comments (1)
tests/e2e/changes-commit.spec.ts (1)

319-320: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.

Route cleanup is skipped when an assertion fails.

page.unroute runs only if every earlier assertion passes. Playwright creates a new page for each test, so a failure here does not leak routes into other tests. The cleanup is therefore redundant. Remove both unroute calls, or wrap the body in try/finally if you keep them.


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 33c7d6cd-03e1-4e12-a97b-c400371b1608
📥 Commits

Reviewing files that changed from the base of the PR and between 076863e and 0a6431c.

📒 Files selected for processing (2)
  • src/ui/source/changes-view.tsx
  • tests/e2e/changes-commit.spec.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

@uniskela
uniskela merged commit ae0adb1 into main Oct 7, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants