Git tab: update a task branch from its base or upstream (#101)#116
Merged
Conversation
The Git tab can show and switch branches, but a task worktree goes stale as its base moves and there was no way to catch it up from inside the app. This finishes simion#101's update/pull/rebase. Adds task_git_update(mode = pull | merge | rebase): - pull = the branch's own upstream. Hidden until the branch is pushed, since task branches are cut --no-track and have no upstream yet. - merge / rebase = fetch, then integrate the task's base_branch. Mechanics worth reviewing: - Network git goes through the hardened fetch path (GIT_TERMINAL_PROMPT=0, batch SSH, 15s deadline), never a bare `git pull`. The plain git() helper has no timeout and would wedge a spawn_blocking thread on a credential prompt. I extracted that hardening out of git_fetch_base into fetch_ref; git_fetch_base is now a best-effort wrapper over it, so the task-create path behaves exactly as before. - --autostash rather than a hand-rolled stash push/pop: git holds the stash outside `stash list` and re-applies it exactly when the op concludes, including after a `rebase --continue`. - A compound git op can exit 0 and still leave conflicts. When the autostash RE-APPLY collides, git commits the merge, prints "Applying autostash resulted in conflicts", retains the stash, and exits 0 with UU markers. That is reported separately as stash_conflicted, or we would tell the user their work was restored while it sat in a stash they were never told about. - Classification is by repo state (MERGE_HEAD / rebase dir / diff-filter=U), not by matching git's stderr, which is locale-dependent. - Conflicts are left in progress rather than auto-aborted: every task has a terminal attached, so the user resolves in place and an auto-abort would throw away a resolvable merge. - Members have no base_branch of their own, so it is joined from the owning project's member entry by repo_path. Guards: detached HEAD; a base equal to the branch (repo-root and adopted tasks record their own branch as the base); a removed remote (staying silent there would merge a stale local-tracking ref); a local branch whose name contains a slash (not mistaken for <remote>/<ref>). Deliberately out of scope: a rebased branch that was already pushed will hit a non-fast-forward rejection on the next push. Nothing is silently overwritten, and force-push handling felt like a separate decision. 15 tests over real repos and worktrees, including the autostash re-apply conflict and a file:// remote proving the fetch is load-bearing.
simion
approved these changes
Jul 17, 2026
simion
left a comment
Owner
There was a problem hiding this comment.
Merging.
One edge worth a follow-up: an update fired while a merge or rebase is already in progress gets misreported. The new git merge fails with "you have not concluded your merge", but merge_or_rebase_in_progress sees the old MERGE_HEAD and returns conflicted: true, so the toast claims the update hit conflicts. The user resolves and commits the old merge believing they are updated, and they are still behind base. A pre-flight in-progress check that errors with "conclude it first" closes it.
Nits:
--no-editon the merge:git()runs without a tty, but a globalmerge.edit=trueforces an editor spawn that fails and aborts with a confusing error.loadBranchesusesPromise.all, so a transienttask_git_update_infofailure also drops the branch list and the menu shows "No local branches".allSettledwould keep the switcher usable.- Tests: the autostash re-apply conflict is covered for merge but not rebase, and pull has no happy-path test.
On the question in the description: keeping conflicts in progress is right. Every task has a terminal attached, and auto-aborting would throw away a resolvable merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Finishes #101. The
show current branch+checkout a different branchbullets shipped in #111; this is the title's remainingupdate/pull/rebase.A task worktree goes stale as its base moves, and there was no way to catch it up without dropping to the terminal.
What it adds
task_git_update(id, dir_name, mode)withmode = pull | merge | rebase, surfaced as an "Update" section at the top of the existing BranchBar dropdown:--no-trackand genuinely have no upstream yet. (Worth noting: a plaingit worktree add -b task origin/maindoes auto-set upstream toorigin/main, which would make "Pull" silently mean "merge base" - the--no-trackalready in the create path is what keeps this honest.)<base>into this branch / Rebase onto<base>- fetch, then integrate the task'sbase_branch. Hidden when the base is empty or equals the branch.Plus a lazy
task_git_update_infothe dropdown calls on open, rather than puttingupstream/baseon the 4s-polledtask_git_statuspayload (two extra subprocesses per repo per poll to serve a menu nobody opened).Mechanics worth a look
git pull. The plaingit()helper sets noGIT_TERMINAL_PROMPT=0and has no timeout, so agit pullthrough it can block forever on a credential prompt and wedge thespawn_blockingthread. I extracted the hardening already ingit_fetch_baseintofetch_ref;git_fetch_baseis now a thin best-effort wrapper over it, so the task-create path behaves exactly as before (its()return type means fatality can't change by construction). Pull is therefore implemented as fetch + merge upstream.--autostash, not a hand-rolled stash push/pop. git holds the autostash outsidestash listand applies it exactly when the op concludes, including after arebase --continue. Hand-stashing around a conflicting rebase strands the user's work behind a half-finished op.Applying autostash resulted in conflicts. Your changes are safe in the stash., retains the stash, and exits 0 withUUmarkers - and there's noMERGE_HEAD/rebase dir to detect. Reported separately asstash_conflictedwith its own message; otherwise we'd tell the user their work was restored while it sat in a stash they were never told about.MERGE_HEAD/ rebase dir /diff --diff-filter=U, and HEAD sha before-vs-after forup_to_date.base_branchof their own, so it's joined from the owning project's member entry byrepo_path(with a legacyproject_idfallback). Unresolvable degrades to "no menu item" rather than a wrong merge.Guards
Detached HEAD (
git rebase mainon a detached HEAD otherwise succeeds and silently rewrites it); a base equal to the branch (repo-root and adopted tasks record their own branch as the base); a removed remote (staying silent there would merge a stale local-tracking ref); and a local branch whose name contains a slash (not mistaken for<remote>/<ref>).Deliberately out of scope
A rebased branch that was already pushed hits a non-fast-forward rejection on the next push. Nothing is silently overwritten - the user gets git's own rejection - but force-push handling felt like a separate decision rather than something to smuggle in here.
Verification
15 tests over real git repos and worktrees, including the autostash re-apply conflict, a
file://remote proving the fetch is load-bearing (a staleorigin/mainwould otherwise falsely report up-to-date), and the slashed-local-branch case.cargo test(161) andtsc -b --noEmitgreen on this branch'supstream/mainbase. Also exercised live through the app: pull-without-upstream, up-to-date, base-moved merge, rebase conflict left resolvable, and autostash restore.