8.2 KiB
APP-3918: Git Operations Button — Tech Spec
Product spec: specs/APP-3918/PRODUCT.md
Parent: APP-3632 (header UI refactor)
Problem
The code review header refactor (APP-3632) cleared space for git operation buttons, but left them as a placeholder. This branch adds a context-aware split button that adapts its label, icon, and dropdown to the current repository state (uncommitted changes → commit, unpushed commits → push, otherwise → create PR).
Relevant Code
app/src/code_review/code_review_view.rs:280-291—PrimaryGitActionModeenumapp/src/code_review/code_review_view.rs:6702-6806—primary_git_action_mode,update_git_operations_ui,git_operations_menu_itemsapp/src/code_review/code_review_view.rs:1308-1358— button/menu construction innew()app/src/code_review/code_review_header/header_revamp.rs:102-135—render_git_operations_buttonapp/src/code_review/diff_state.rs:530-535—DiffStateModel::unpushed_commits()app/src/util/git.rs:247-290—get_unpushed_commitswith main-branch fallbackapp/src/view_components/action_button.rs:296-321—AdjoinedSide,set_adjoined_side,clear_adjoined_side
Current State
The header refactor (APP-3632) restructured the code review header into two layers and moved contextual info to the right-panel header. The inner header now has space for action buttons on the right side. DiffStateModel already tracks unpushed_commits and provides unpushed_commits() accessor. Diff stats are available via the loaded state's to_diff_stats().
However, get_unpushed_commits previously returned an empty vec for branches with no upstream tracking, which meant new local branches would incorrectly show "Create PR" instead of "Push".
Changes
1. PrimaryGitActionMode enum
New private enum in code_review_view.rs with five variants: Commit, Publish, Push, ViewPr, CreatePr. Computed from:
LoadedState::to_diff_stats().has_no_changes()— uncommitted changes checkDiffStateModel::has_upstream()— upstream tracking branch checkDiffStateModel::unpushed_commits().is_empty()— unpushed commits checkDiffStateModel::pr_info()— existing PR checkDiffStateModel::is_on_main_branch()— main branch check
2. Split button in CodeReviewView
Four new fields on CodeReviewView:
git_primary_action_button: ViewHandle<ActionButton>— primary button (SecondaryTheme,ButtonSize::Small,AdjoinedSide::Right)git_operations_chevron: ViewHandle<ActionButton>— chevron button (AdjoinedSide::Left)git_operations_menu: ViewHandle<Menu<CodeReviewAction>>— dropdown menugit_operations_menu_open: bool— menu stateshow_git_operations_chevron: bool— controls chevron visibility (hidden inCreatePrmode)
Created once in new(). The menu subscribes to MenuEvent::ItemSelected / MenuEvent::Close to reset open state and chevron active state.
Passed to the header via six new fields on CodeReviewHeaderFields.
3. New CodeReviewAction variants
OpenCommitDialog,OpenPushDialog,OpenCreatePrDialog— dispatch to dialogs (currently TODO stubs, wired in child branches)OpenGitOperationsMenu— toggles the dropdownCommitAndPush,CommitAndCreatePr— compound actions (stubs for now)ViewPr(String)— opens the PR URL in the browserPublishBranch— pushes and sets upstream (stub for now)
4. Reactive state updates
update_git_operations_ui is called from two sites:
update_aggregate_stats(line 2777) — when diff stats changeinvalidate_all(line 2872) — when a full diff reload completes
It recomputes primary_git_action_mode and updates the button's label, icon, click handler, and adjoined-side. In CreatePr mode, the adjoined side is cleared via clear_adjoined_side and the chevron is hidden.
5. Header rendering
render_git_operations_button in header_revamp.rs builds a Stack with:
- A
Flex::rowof primary button + optional chevron - A positioned overlay for the dropdown menu (anchored
BottomRight → TopRight)
Gated on CodeReviewHeaderFields::show_git_operations (which is FeatureFlag::GitOperationsInCodeReview.is_enabled()).
6. get_unpushed_commits fallback
When git log @{u}..HEAD fails with "no upstream configured" or "unknown revision", now falls back to detect_main_branch and runs git log {main_branch}..HEAD. This ensures new local branches correctly report their commits as "unpushed". Extracted parse_commit_log helper to avoid duplication.
7. has_upstream detection
New has_upstream: bool field on DiffMetadata, computed during load_metadata_for_repo via git rev-parse --abbrev-ref --symbolic-full-name @{u}. Exposed as DiffStateModel::has_upstream(). Used by primary_git_action_mode to distinguish Publish (no upstream) from Push (has upstream) and to gate Create PR (only with upstream).
8. PR info refresh on metadata updates
Previously refresh_pr_info only ran on branch change. Now also runs during throttled metadata refreshes when pr_info is None, so the button updates to "PR #N" after an external push or PR creation without requiring a branch switch.
9. Overflow menu gated on changes
When the git operations flag is enabled, has_header_menu_items now requires actual changes to be present. The "Add comment" item is also gated on has_changes. This hides the three-dot overflow menu entirely when there are no changes.
10. New UploadCloud icon
Added app/assets/bundled/svg/upload-cloud-01.svg and registered as Icon::UploadCloud for the Publish button.
11. New GitCommit icon
Added app/assets/bundled/svg/git-commit.svg (a 24×24 SVG with fill="#FF0000" per the existing icon convention) and registered it as Icon::GitCommit in crates/warp_core/src/ui/icons.rs. This replaces Icon::GitBranch for the Commit action — a git-branch icon is semantically incorrect for a commit operation.
8. ActionButton::clear_adjoined_side
New public method to remove the adjoined-side styling at runtime, used when transitioning to CreatePr mode (standalone button, no chevron).
End-to-End Flow
graph TD
A[Diff stats change or full reload] --> B[update_git_operations_ui]
B --> C{has uncommitted changes?}
C -->|yes| D["Commit mode: label=Commit, icon=GitCommit, chevron=visible"]
C -->|no| E{has upstream?}
E -->|no, has commits| F["Publish mode: label=Publish, icon=UploadCloud, chevron=hidden"]
E -->|yes| G{unpushed commits?}
G -->|yes| H["Push mode: label=Push, icon=ArrowUp, chevron=visible"]
G -->|no| I{PR exists?}
I -->|yes| J["ViewPr mode: label=PR #N, icon=Github, chevron=hidden"]
I -->|no, not main| K["CreatePr mode: label=Create PR, icon=Github, chevron=hidden"]
I -->|no, on main| L["Disabled Commit: label=Commit, disabled, no chevron"]
E -->|no, no commits| L
Risks and Mitigations
- Stale button state: If diff stats and unpushed-commit data get out of sync, the button could show the wrong mode. Mitigated by recomputing on every diff reload and stats update.
- Fallback branch detection:
detect_main_branchmay fail in unusual repo setups (bare repos, orphan branches). Falls back to empty vec (shows "Create PR"), which is a reasonable degraded state. - Action stubs: The dialog actions are TODO stubs. If child branches aren't merged alongside this one, clicking the buttons does nothing. Acceptable because the feature is behind a flag that's off by default.
Testing and Validation
- Manual: enable feature flag, modify files, and step through Commit → Push → Create PR states by committing and pushing from the terminal.
- Verify dropdown items and disabled states in each mode.
- Test on a new local branch with no upstream to confirm "Push" is shown.
- Verify the flag-off path renders no git operations button.
Follow-ups
- Wire
OpenCommitDialog,OpenPushDialog,OpenCreatePrDialogto actual dialogs (child branches in stack). - Wire
CommitAndPushandCommitAndCreatePrcompound actions. - Wire
PublishBranchtogit push --set-upstream origin <branch>. - Add integration tests for mode transitions once dialogs are functional.
- Consider progress/error UI for push and PR creation flows.
- Handle the "no remote at all" edge case (currently treated same as no upstream).