Files

8.2 KiB
Raw Permalink Blame History

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-291PrimaryGitActionMode enum
  • app/src/code_review/code_review_view.rs:6702-6806primary_git_action_mode, update_git_operations_ui, git_operations_menu_items
  • app/src/code_review/code_review_view.rs:1308-1358 — button/menu construction in new()
  • app/src/code_review/code_review_header/header_revamp.rs:102-135render_git_operations_button
  • app/src/code_review/diff_state.rs:530-535DiffStateModel::unpushed_commits()
  • app/src/util/git.rs:247-290get_unpushed_commits with main-branch fallback
  • app/src/view_components/action_button.rs:296-321AdjoinedSide, 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 check
  • DiffStateModel::has_upstream() — upstream tracking branch check
  • DiffStateModel::unpushed_commits().is_empty() — unpushed commits check
  • DiffStateModel::pr_info() — existing PR check
  • DiffStateModel::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 menu
  • git_operations_menu_open: bool — menu state
  • show_git_operations_chevron: bool — controls chevron visibility (hidden in CreatePr mode)

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 dropdown
  • CommitAndPush, CommitAndCreatePr — compound actions (stubs for now)
  • ViewPr(String) — opens the PR URL in the browser
  • PublishBranch — 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 change
  • invalidate_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::row of 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_branch may 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, OpenCreatePrDialog to actual dialogs (child branches in stack).
  • Wire CommitAndPush and CommitAndCreatePr compound actions.
  • Wire PublishBranch to git 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).