# GitButler Push Operation Edge Cases: Handling Remote Divergence, Force Protection, and Virtual Stacks

> Master GitButler push edge cases like remote divergence and force protection. GitButler's target-centric abstraction ensures safe and validated pushes.

- Repository: [GitButler/gitbutler](https://github.com/gitbutlerapp/gitbutler)
- Tags: deep-dive
- Published: 2026-02-16

---

**GitButler handles complex push scenarios—including diverged remotes, missing upstreams, and force-push protection—through a target-centric abstraction that resolves remote URLs dynamically and validates branch state before executing the underlying git push.**

When working with the `gitbutlerapp/gitbutler` codebase, understanding how the push operation manages edge cases is critical for contributing to the virtual branch workflow. The **GitButler push operation edge cases** span from simple remote resolution to complex divergence detection and force-push safeguards. This guide examines the actual implementation in the Rust source code, showing exactly how the system handles misconfigured remotes, diverged histories, and virtual stack serialization.

## What the GitButler Push Operation Does

When a user initiates a push, GitButler constructs a **push request** containing the target SHA, remote reference name (e.g., `refs/heads/feature-xyz`), and optional flags for force-push or CI-skip. This request flows through `Context::push`, which delegates to the low-level `gitbutler_repo_actions::RepoActionsExt` trait implementation.

The core entry point resides in [`crates/gitbutler-branch-actions/src/base.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-branch-actions/src/base.rs):

```rust
pub(crate) fn push(ctx: &Context, with_force: bool) -> Result<()> {
    let target = default_target(&ctx.project_data_dir())?;
    let _ = ctx.push(
        target.sha,
        &target.branch,
        with_force,
        ctx.legacy_project.force_push_protection,
        None,
        None,
        vec![],
    );
    Ok(())
}

```

This function retrieves the **default target** from `VirtualBranchesHandle`, then invokes the context's push method with the target's SHA and branch reference, respecting the project's force-push protection settings.

## How GitButler Resolves Remote and Refname

Before executing the push, GitButler must resolve the remote URL and reference name through a multi-layer fallback system:

1. **Default target retrieval** – Loaded from `VirtualBranchesHandle` ([`crates/gitbutler-stack/src/target.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-stack/src/target.rs)), containing the branch, SHA, and remote configuration.
2. **Push remote override** – Users may set a specific push remote via `set_target_push_remote`, stored in `target.push_remote_name`.
3. **Fallback logic** – If the push remote lacks a URL, the system falls back to the branch's original remote URL (`target.remote_url`).

The resolution logic in [`crates/gitbutler-branch-actions/src/base.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-branch-actions/src/base.rs) handles this gracefully:

```rust
let push_remote_url = match target.push_remote_name {
    Some(ref name) => repo.find_remote(name).ok()
        .and_then(|r| r.url().map(|u| u.to_string()))
        .unwrap_or_else(|| target.remote_url.clone()),
    None => target.remote_url.clone(),
};

```

This ensures that even when the push remote is misconfigured or missing, the operation can still proceed using the upstream remote URL.

## Critical Edge Cases in GitButler Push Operations

GitButler's push implementation handles several complex scenarios that traditional git workflows encounter. Here is how the source code addresses each **GitButler push operation edge case**:

### No Push Remote Configured

When `target.push_remote_name` is `None`, the system defaults to the branch's upstream remote URL (`target.remote_url`). If both are empty, the low-level `RepoActionsExt` implementation returns an error that bubbles up to the UI. This detection occurs in `push` (base.rs) during the `default_target` call.

### Remote Divergence Detection

GitButler detects when the local branch has diverged from the remote using `graph_ahead_behind` calculations in `target_to_base_branch`. The resulting `BaseBranch` struct populates `diverged: true` along with `diverged_ahead` and `diverged_behind` vectors containing the commit lists. This allows the UI to warn users before they attempt to push over diverged history.

### Force Push Protection

The system respects `ctx.legacy_project.force_push_protection`, passed through from the high-level `push` function to `Context::push`. When enabled, `RepoActionsExt` aborts force pushes unless the user explicitly overrides the protection. This prevents accidental history rewriting on shared branches.

### Pushing Virtual Stacks

When pushing a virtual stack (series of dependent branches), `Stack::push_details` builds the exact remote refname using `reference.remote_reference(remote_name)`. This guarantees that the ref name matches the target's push remote, ensuring atomic pushes of complex branch structures without refname collisions.

### Missing Remote Branch

If the upstream branch does not exist on the remote, `BaseBranch::set_base_branch` invokes `maybe_find_branch_by_refname`, which returns a clear error message: `"remote branch '{}' not found"`. This prevents pushes to non-existent upstreams and guides users to set up tracking correctly.

### Push After Rebasing

GitButler does not automatically recalculate the target SHA after a rebase operation. Callers must explicitly invoke `set_base_branch` to update the target SHA before pushing. This design guarantees that the push target reflects the new base commit, preventing accidental pushes of stale commit references.

## How GitButler Determines What to Push

The `Stack::push_details` method in [`crates/gitbutler-stack/src/stack.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-stack/src/stack.rs) extracts the precise commit SHA and remote refname for the specific series the user wants to push:

```rust
pub fn push_details(&self, ctx: &Context, branch_name: String) -> Result<PushDetails> {
    self.ensure_initialized()?;
    let (_, reference) = get_head(&self.heads, &branch_name)?;
    let oid = reference.head_oid(&*ctx.repo.get()?)?.to_git2();
    let git2_repo = ctx.git2_repo.get()?;
    let commit = git2_repo.find_commit(oid)?;
    let remote_name = branch_state(ctx).get_default_target()?.push_remote_name();
    let upstream_refname = RemoteRefname::from_str(&reference.remote_reference(remote_name.as_str()))?;
    Ok(PushDetails { head: commit.id(), remote_refname: upstream_refname })
}

```

This method ensures that:
- The **commit SHA** corresponds to the exact head of the selected virtual branch or series.
- The **remote reference** is constructed from the branch's stored upstream combined with the active push remote name.

## Practical Usage Patterns

### Push the Current Base Branch (No Force)

```rust
use gitbutler_branch_actions;

// Context is provided by the application runtime
gitbutler_branch_actions::push_base_branch(&ctx, false)?;

```

This invokes `push` in [`base.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/base.rs), delegating to `Context::push` with `with_force` set to false.

### Force Push with Protection Override

```rust
gitbutler_branch_actions::push_base_branch(&ctx, true)?;

```

The `force_push_protection` setting from the project configuration is automatically consulted. If enabled, the operation aborts unless the user explicitly confirms.

### Push a Specific Virtual Series

```rust
use gitbutler_stack::Stack;

let stack = Stack::load(&ctx, "feature-xyz")?;
let details = stack.push_details(&ctx, "feature-xyz".into())?;
ctx.push(
    details.head,
    &details.remote_refname,
    false,  // no force
    false,  // no protection override
    None, None, vec![],
)?;

```

The `push_details` call guarantees the correct remote refname for the virtual stack's series.

### Change the Push Remote Target

```rust
gitbutler_branch_actions::set_target_push_remote(&ctx, "origin")?;

```

This updates `Target.push_remote_name` in the project state, affecting subsequent push operations.

## Key Source Files for Push Operations

| File | Responsibility | Important Symbols |
|------|----------------|-------------------|
| [`crates/gitbutler-branch-actions/src/base.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-branch-actions/src/base.rs) | High-level push orchestration, push-remote handling, base-branch data | `push`, `set_target_push_remote`, `target_to_base_branch` |
| [`crates/gitbutler-stack/src/stack.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-stack/src/stack.rs) | Determines *what* to push (SHA + remote refname) | `push_details` |
| [`crates/gitbutler-stack/src/target.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-stack/src/target.rs) | Stores the default push target (`Target`) | `Target { branch, remote_url, sha, push_remote_name }` |
| [`crates/gitbutler-branch-actions/src/actions.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-branch-actions/src/actions.rs) | Public API exposed to UI / Tauri | `push_base_branch`, `set_base_branch` |
| [`crates/gitbutler-repo-actions/src/lib.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-repo-actions/src/lib.rs) | Low-level `RepoActionsExt::push` implementation (executes the actual `git push`) | `RepoActionsExt` |
| [`crates/gitbutler-stack/tests/mod.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-stack/tests/mod.rs) | Test suite exercising push edge cases (e.g., diverged, force) | `push_series_success`, `update_name_after_push` |

## Summary

- **GitButler push operation edge cases** are handled through a **target-centric model** that dynamically resolves remote URLs and reference names.
- The system detects **remote divergence** via `graph_ahead_behind` calculations, surfacing `diverged_ahead` and `diverged_behind` vectors to the UI.
- **Force-push protection** is enforced by default through `ctx.legacy_project.force_push_protection`, requiring explicit user confirmation to override.
- **Virtual stacks** use `Stack::push_details` to guarantee atomic pushes with correct remote refnames derived from the active push remote.
- Callers must explicitly invoke `set_base_branch` after rebasing to ensure the push target SHA reflects the new base commit.

## Frequently Asked Questions

### How does GitButler handle pushing when no push remote is configured?

When `target.push_remote_name` is `None`, GitButler falls back to the branch's upstream remote URL (`target.remote_url`). If both values are empty, the low-level `RepoActionsExt` implementation returns an error that propagates to the UI, preventing pushes to undefined remotes.

### What happens when the local branch diverges from the remote?

GitButler detects divergence through `target_to_base_branch`, which invokes `graph_ahead_behind` to compute commit lists. The resulting `BaseBranch` struct sets `diverged: true` and populates `diverged_ahead` and `diverged_behind` vectors, enabling the UI to warn users before they force-push over remote commits.

### How does force-push protection work in GitButler?

The `push` function in [`base.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/base.rs) passes `ctx.legacy_project.force_push_protection` to the low-level `Context::push` method. When enabled, `RepoActionsExt` aborts force pushes unless the user explicitly overrides the protection, preventing accidental history rewriting on shared branches.