From a3adee2044a13b502bd5be608109ecbaf54057cb Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Sun, 31 May 2026 23:51:21 +0800 Subject: [PATCH] fix(git): disable pull/push like TS (no remote / nothing ahead) Pull/push always looked enabled. Plumb the ahead count (op-git status already computes it) onto GitPanelState via the host snapshot, and gate the buttons in paint AND hit-test: pull needs a remote + no in-flight op; push also needs ahead > 0 (TS pullDisabled=!hasRemote||busy, push adds ahead===0). has_remote derives from the existing remotes list. --- crates/op-editor-core/src/editor_ui_state.rs | 3 +++ .../src/widgets/git_panel_ready.rs | 20 +++++++++++++++---- .../src/widgets/git_panel_tests.rs | 4 ++++ crates/op-host-desktop/src/git_host.rs | 2 ++ crates/op-host-desktop/src/git_jobs.rs | 4 ++++ 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/crates/op-editor-core/src/editor_ui_state.rs b/crates/op-editor-core/src/editor_ui_state.rs index e9348581c..66fc1dc39 100644 --- a/crates/op-editor-core/src/editor_ui_state.rs +++ b/crates/op-editor-core/src/editor_ui_state.rs @@ -413,6 +413,9 @@ pub struct GitPanelState { pub branch_create_focused: bool, /// Number of changed (dirty) files in the working tree. pub dirty_count: usize, + /// Commits the current branch is ahead of its upstream — gates the + /// Push button (TS disables Push when `ahead === 0`). + pub ahead: u32, /// Number of files with unresolved merge conflicts. pub conflicted_count: usize, /// Whether a merge is in progress — drives the panel's conflict diff --git a/crates/op-editor-ui/src/widgets/git_panel_ready.rs b/crates/op-editor-ui/src/widgets/git_panel_ready.rs index 58930b267..d0765d09f 100644 --- a/crates/op-editor-ui/src/widgets/git_panel_ready.rs +++ b/crates/op-editor-ui/src/widgets/git_panel_ready.rs @@ -140,6 +140,18 @@ impl GitPanel<'_> { !self.state.commit_message.trim().is_empty() } + /// Whether Pull can fire — a configured remote and no in-flight + /// pull / push (TS `pullDisabled = !hasRemote || busy`). + fn pull_enabled(&self) -> bool { + !self.state.remotes.is_empty() && !self.state.pulling && !self.state.pushing + } + + /// Whether Push can fire — Pull's conditions plus `ahead > 0` (TS + /// also disables Push when up-to-date, `ahead === 0`). + fn push_enabled(&self) -> bool { + self.pull_enabled() && self.state.ahead > 0 + } + /// Paint the ready view. pub(super) fn paint_ready(&self, cx: &mut PaintCx<'_>, rect: Rect) { let t = self.theme; @@ -200,8 +212,8 @@ impl GitPanel<'_> { // Pull / Push / Overflow icon buttons. let (pull_r, push_r, overflow_r) = self.ready_header_buttons(rect); - self.paint_ready_icon(cx, pull_r, Icon::ArrowDown, !self.state.pulling); - self.paint_ready_icon(cx, push_r, Icon::ArrowUp, !self.state.pushing); + self.paint_ready_icon(cx, pull_r, Icon::ArrowDown, self.pull_enabled()); + self.paint_ready_icon(cx, push_r, Icon::ArrowUp, self.push_enabled()); // Overflow `…` — TS colors this `text-muted-foreground` at size 13, // dimmer than the pull / push glyphs (git-panel-header.tsx:127-129). let overflow_s = 13.0; @@ -431,10 +443,10 @@ impl GitPanel<'_> { if contains(self.ready_branch_rect(rect), point) { return Some(GitPanelHit::BranchPicker); } - if contains(pull, point) { + if self.pull_enabled() && contains(pull, point) { return Some(GitPanelHit::Pull); } - if contains(push, point) { + if self.push_enabled() && contains(push, point) { return Some(GitPanelHit::Push); } if contains(self.ready_commit_box(rect), point) { diff --git a/crates/op-editor-ui/src/widgets/git_panel_tests.rs b/crates/op-editor-ui/src/widgets/git_panel_tests.rs index 0760d3ccd..bbeb00adf 100644 --- a/crates/op-editor-ui/src/widgets/git_panel_tests.rs +++ b/crates/op-editor-ui/src/widgets/git_panel_tests.rs @@ -196,6 +196,10 @@ fn ready_view_maps_each_header_and_commit_region() { let s = state_with(GitPanelState { branch: Some("main".to_string()), commit_message: "ship it".to_string(), + // A remote + commits-ahead so pull/push are enabled (they now + // disable when there's no remote / nothing to push, TS parity). + remotes: vec!["origin → https://example.com/r.git".to_string()], + ahead: 1, ..open_repo() }); let panel = GitPanel::for_editor(&s).unwrap(); diff --git a/crates/op-host-desktop/src/git_host.rs b/crates/op-host-desktop/src/git_host.rs index 9c8012c12..82997058e 100644 --- a/crates/op-host-desktop/src/git_host.rs +++ b/crates/op-host-desktop/src/git_host.rs @@ -70,6 +70,7 @@ impl DesktopApp { || panel.branch != snap.branch || panel.branches != snap.branches || panel.dirty_count != snap.dirty_count + || panel.ahead != snap.ahead || panel.conflicted_count != snap.conflicted_count || panel.merging != snap.merging || panel.conflicted_files != snap.conflicted_files @@ -80,6 +81,7 @@ impl DesktopApp { panel.branch = snap.branch; panel.branches = snap.branches; panel.dirty_count = snap.dirty_count; + panel.ahead = snap.ahead; panel.conflicted_count = snap.conflicted_count; panel.merging = snap.merging; panel.conflicted_files = snap.conflicted_files; diff --git a/crates/op-host-desktop/src/git_jobs.rs b/crates/op-host-desktop/src/git_jobs.rs index 3a6d2b6bc..ec41e8c52 100644 --- a/crates/op-host-desktop/src/git_jobs.rs +++ b/crates/op-host-desktop/src/git_jobs.rs @@ -22,6 +22,8 @@ pub struct GitSnapshot { pub branches: Vec, /// Changed-file count. pub dirty_count: usize, + /// Commits ahead of the upstream — gates the Push button. + pub ahead: u32, /// Conflicted-file count. pub conflicted_count: usize, /// Whether a merge is in progress. @@ -112,6 +114,7 @@ fn snapshot(repo: &GitRepo) -> GitSnapshot { .collect(); let status = repo.status().ok(); let dirty_count = status.as_ref().map(|s| s.files.len()).unwrap_or(0); + let ahead = status.as_ref().map(|s| s.ahead).unwrap_or(0); let conflicted_count = status .as_ref() .map(|s| { @@ -156,6 +159,7 @@ fn snapshot(repo: &GitRepo) -> GitSnapshot { branch, branches, dirty_count, + ahead, conflicted_count, merging, conflicted_files,