Commit and Push
Turning staged changes into a commit and getting it upstream: the commit composer, committing, pushing, setting an upstream on a new branch, and what each success and failure state means.
Committing
The composer sits at the bottom of working-tree mode. It is a monospace textarea that grows from two to six lines as you type. A vertical ruler marks column 72 and a counter tracks your subject line, turning amber when any line runs past 72 characters, a nudge toward the standard Git convention.
With a remote configured you get two buttons: a ghost Commit and a primary Commit & Push (N). With no remote you get a single Commit (N).
Recalling recent messages
With the cursor at the very start of an empty composer, ↑ walks back through the last eight commit messages on that branch, bodies included, and ↓ walks forward again, restoring your in-progress draft at the end. Once you edit the text, the recall index resets.
What blocks a commit
The commit buttons are disabled when nothing is staged, the message is empty, conflicts are unresolved, or the worktree is in detached HEAD. Hover a disabled button to see the reasons. Click it to jump focus to the blocker, expanding the file list if that is what is needed.
When Review Hub is opened from a worktree card carrying an agent note, the composer can be pre-seeded with the note's first line, and only that. Nothing else is ever substituted as a commit message.
Pushing
Where the push actually goes
Every git write that touches a remote (the push, the rescue push that sets an upstream, pull-and-rebase, force-with-lease, and the discard preview behind it) resolves the branch's real destination from git's own configuration instead of assuming origin. Daintree asks git the same question git asks itself, honoring branch.NAME.pushRemote, then remote.pushDefault, then branch.NAME.remote.
When it cannot get an answer, it fails closed. The confirm button is disabled with "No push destination is configured for this branch. Set an upstream, or configure a push remote, before pushing." Two or more remotes with nothing saying which one this branch pushes to is an explicit ambiguity, and it stays an ambiguity even when one of them is named origin. Sending a write to a repository you did not choose is not recoverable; a blocked button is a visible prompt to configure a remote.
upstream/main but pushes to fork/topic. Naming origin would push to the wrong repository, or refuse a push that git would have accepted. Because the destination is resolved rather than assumed, the confirm dialog names the real target on its destination pill and its primary button ("Push to fork/topic"), and the force-push dialog reads "Force push to fork/topic?". The push refspec is written explicitly as local-to-remote too, since the remote-side branch need not share the local name.Pull-and-rebase deliberately resolves a different ref: your branch's upstream, not its push target. These are the same thing in the ordinary case and genuinely different in a fork workflow, and a rebase that used the push destination would replay your commits on top of a repository you never asked to integrate from.
The same discipline shows up around the change: behind-counts compare against the base branch's own tracking ref rather than assuming origin, and a PR head is fetched from the remote the PR was opened against: with the forge configured as upstream, fetching pull/N/head from origin simply fails with "couldn't find remote ref". Those two resolvers fail soft, falling back to previous behavior, because they feed a displayed number rather than a write.
The confirmation
Clicking Commit & Push opens a confirmation titled "Push commits?" carrying a destination pill, a full preview of the commit message, and a Don't ask again for this worktree checkbox. Tick it and future pushes from that worktree skip the dialog. On a protected branch the description adds a line noting that most teams use pull requests instead. The destination is named on the pill and again on the primary button: Push to origin/main, or whatever git resolved.
The title is deliberately fixed rather than interpolated. A heading that reads "Push to ''?" until the destination lands, then quietly swaps from naming your local branch to naming a remote ref, changes what the dialog is asking mid-flight. The refs sit in the body, which is a place that can hold a pending or unresolved state honestly.
The opt-out is not a blanket one. It is stored per worktree path and consulted only by Review Hub's own Commit & Push. Even inside Review Hub it is overridden when the destination will not resolve: an unresolvable push always shows the dialog, because the dialog is where that failure is explained.
During the push you get per-stage progress bars labeled with the resolved target. A branch with no tracking configuration yet (the normal state of a freshly created worktree branch) gets its upstream set for you on the first push, to the destination that was already resolved and confirmed. No need to run git push --set-upstream by hand.
The keyboard chord's own confirmation
The global Command-K, then Command-P Control-K, then Control-P Control-K, then Control-P chord and Git > Push on a worktree card open a different dialog, and silencing Review Hub's does not silence it. That is deliberate: a push you fire from a chord, without a file list in front of you, is the one that most deserves a preview. It has no Don't ask again checkbox for the same reason.
What it previews is the publish range, not the branch's recent history. A From row names your local branch and a To row names the resolved destination; under them, Commits to push lists what actually sits between the destination's tip and your branch, up to twelve rows of short hash, subject and author, with a total badge and an "…and N more" tail. The two are the same set only on a branch that has never been pushed; on every other branch a recent-history list would be showing you commits the push will not write.
Two labels qualify the destination when the range could not be settled the ordinary way:
- creates this branch: the remote confirmed the destination branch does not exist, so the push creates it and the rows are the branch's own history.
- unverified: there is no remote-tracking ref and the remote could not be reached, so the rows are a local approximation that can both overstate and understate. A red line repeats this under the list.
A broken read never renders as an all-clear. If git fails, the preview does not fall back to an empty list; it reports "Couldn't read what this would publish" with git's own message and a Retry, and Push stays disabled with the hint "Push stays blocked until the preview loads".
That is a different state from a genuine "Nothing to publish — origin/main already has everything on this branch", which is a settled answer and is approvable: confirming it simply pushes nothing.
And a third state sits between them. An empty unverified range reads "Nothing found to publish, but origin couldn't be reached to check origin/main — so this isn't confirmed": nothing was found locally, which is not the same claim as the destination being up to date.
Two more states block the button and name themselves rather than leaving it dead: a detached HEAD ("No branch checked out"), and a destination git cannot name ("No destination git can name", with git push -u <remote> <branch> spelled out, since setting an upstream resolves both the missing and the ambiguous case). The same preview backs the confirmation an agent gets when it dispatches git.push over MCP, so both approvers read one story.
When a push fails
An amber banner explains why and what to do about it. Authentication, rate-limit, network, and filesystem failures keep the detail short and offer a retry where retrying can help. Policy, hook, and unrecognized failures tuck the raw output behind a Show details toggle, because the server's own text is often the only actionable signal. An auth failure may also surface a forge error code and an Open forge settings shortcut routed to that provider's settings.
Calls to action are limited to things the app can actually do. A clear hint with no button beats a button that does not work.