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.
This commit is contained in:
Kayshen-X 2026-05-31 23:51:21 +08:00
parent f521a249a2
commit a3adee2044
5 changed files with 29 additions and 4 deletions

View file

@ -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

View file

@ -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) {

View file

@ -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();

View file

@ -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;

View file

@ -22,6 +22,8 @@ pub struct GitSnapshot {
pub branches: Vec<String>,
/// 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,