# GitButler Stacked Branch Restacking on Commit Amend: A Technical Deep Dive

> Discover how GitButler's stacked branch restacking automatically rebasess descendant commits on commit amend This technical deep dive explains the linear and consistent virtual stack

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

---

**GitButler automatically rebases all descendant commits when you amend a commit in a stacked branch, ensuring the virtual stack remains linear and consistent.**

When working with stacked (virtual) branches in the `gitbutlerapp/gitbutler` repository, amending a commit triggers a sophisticated restacking mechanism. This process rewrites the target commit and reapplies every descendant commit on top of it, preserving the stack's integrity. The implementation spans the Rust backend—handling branch actions, commit engines, and rebasing—and the TypeScript frontend, which manages user interactions and state synchronization.

## Backend Architecture: The Amend and Restack Flow

The backend handles GitButler stacked branch restacking on commit amend through a pipeline that validates the workspace, generates a rebase plan, and updates stack metadata.

### API Entry Point: `/amend_virtual_branch`

The process begins when the frontend calls the `/amend_virtual_branch` endpoint. The request payload includes `stack_id`, `commit_id`, and a list of `worktree_changes` representing the amendments.

```rust
// crates/but-api/src/legacy/virtual_branches.rs:242-250
pub fn amend_virtual_branch(
    &self,
    project_id: &str,
    stack_id: &str,
    commit_id: &str,
    worktree_changes: Vec<WorktreeChange>,
) -> Result<String, Error> {
    // Delegates to branch actions
}

```

### Branch Actions Validation

The API forwards to `gitbutler_branch_actions::amend`, located in `crates/gitbutler-branch-actions/src/actions.rs:90-108`. This function validates the workspace state, creates a snapshot for safety, and delegates to the commit engine.

```rust
// crates/gitbutler-branch-actions/src/actions.rs
pub fn amend(
    ctx: &CommandContext,
    stack_id: StackId,
    commit_id: CommitId,
    worktree_changes: Vec<WorktreeChange>,
) -> Result<CommitId> {
    // Validation and snapshot logic
    commit_engine::create_commit_and_update_refs_with_project(...)
}

```

### Commit Engine and Rebase Planning

Inside `crates/but-workspace/src/commit_engine/mod.rs:136-181`, the commit engine interprets the request as `Destination::AmendCommit { commit_id, new_message: None }`. It constructs a **rebase plan** that includes the target commit and all its descendants within the same stack.

The engine executes a rebase where:
1. The target commit is recreated with the supplied `worktree_changes`
2. Subsequent commits are **re-applied** (picked) on top of the new commit

```rust
// crates/but-workspace/src/commit_engine/mod.rs
pub enum Destination {
    AmendCommit { commit_id: CommitId, new_message: Option<String> },
    // ... other variants
}

```

### Stack Metadata Updates

After the rebase completes, `Stack::set_heads_from_rebase_output` in `crates/gitbutler-stack/src/stack.rs:604-610` updates the stack metadata—including head OID and order—so the UI reflects the new state.

```rust
// crates/gitbutler-stack/src/stack.rs
pub fn set_heads_from_rebase_output(&mut self, output: &RebaseOutput) -> Result<()> {
    // Updates stack heads based on rebase result
}

```

## Frontend Integration: Drag-and-Drop Amend

The TypeScript frontend handles user interactions through drop handlers and a specialized stack service that communicates with the backend API.

### Drop Handlers and User Interactions

When a user drags a file or hunk onto an existing commit in the stack view, the `AmendCommitWithChangeDzHandler` or `AmendCommitWithHunkDzHandler` captures the interaction in `apps/desktop/src/lib/commits/dropHandler.ts:140-148`.

```typescript
// apps/desktop/src/lib/commits/dropHandler.ts
class AmendCommitWithChangeDzHandler {
    handle(dropData: WorktreeChangeDropData) {
        // Extracts commit ID and worktree changes
        return {
            commitId: this.commit.id,
            worktreeChanges: [/* DiffSpec objects */]
        };
    }
}

```

### Stack Service and Cache Invalidation

The `stackService.amendCommitMutation` in `apps/desktop/src/lib/stacks/stackService.svelte.ts:787-792` is an RTK-Query mutation that POSTs to `/amend_virtual_branch`.

```typescript
// apps/desktop/src/lib/stacks/stackService.svelte.ts
amendCommitMutation: builder.mutation<string, AmendCommitArgs>({
    query: ({ projectId, stackId, commitId, worktreeChanges }) => ({
        url: `projects/${projectId}/stack/${stackId}/amend`,
        method: 'POST',
        body: { commit_id: commitId, worktree_changes: worktreeChanges }
    }),
    invalidatesTags: ['Stacks', 'StackDetails']
})

```

After the mutation resolves, the service invalidates the `Stacks` and `StackDetails` tags (`apps/desktop/src/lib/stacks/stackService.svelte.ts:111-118`), forcing the UI to re-fetch the updated stack state.

## Code Examples

### TypeScript API Client

You can interact with the GitButler stacked branch restacking on commit amend functionality programmatically via the HTTP API:

```typescript
import fetch from 'node-fetch';

async function amendCommit({
  projectId,
  stackId,
  commitId,
  worktreeChanges
}: {
  projectId: string;
  stackId: string;
  commitId: string;
  worktreeChanges: any[]; // Array of DiffSpec objects
}) {
  const response = await fetch(
    `http://localhost:8000/api/v1/projects/${projectId}/stack/${stackId}/amend`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        commit_id: commitId,
        worktree_changes: worktreeChanges,
      }),
    }
  );

  if (!response.ok) throw new Error(`Failed: ${await response.text()}`);
  const newOid: string = await response.json();
  console.log('Amended commit is now', newOid);
}

```

### CLI Usage

The `but` CLI provides direct access to the amend functionality:

```bash

# Amend the topmost commit of stack "abc123" with current work-tree changes

but amend \
  --stack-id abc123 \
  --commit-id 4f2d3e1 \
  --worktree-changes "$(but diff --format=json)"

```

The CLI constructs the same `Destination::AmendCommit` request that the UI sends to the backend.

## Summary

- **GitButler stacked branch restacking on commit amend** is handled by a coordinated flow between the Rust backend and TypeScript frontend.
- The backend uses `Destination::AmendCommit` in the commit engine to generate a rebase plan that recreates the target commit and reapplies all descendants.
- Key backend files include [`crates/but-api/src/legacy/virtual_branches.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-api/src/legacy/virtual_branches.rs) for the API, [`crates/gitbutler-branch-actions/src/actions.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-branch-actions/src/actions.rs) for validation, and [`crates/but-workspace/src/commit_engine/mod.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-workspace/src/commit_engine/mod.rs) for the rebase logic.
- The frontend implements drag-and-drop amendments via `AmendCommitWithChangeDzHandler` in [`apps/desktop/src/lib/commits/dropHandler.ts`](https://github.com/gitbutlerapp/gitbutler/blob/main/apps/desktop/src/lib/commits/dropHandler.ts) and synchronizes state through `stackService.amendCommitMutation` in [`apps/desktop/src/lib/stacks/stackService.svelte.ts`](https://github.com/gitbutlerapp/gitbutler/blob/main/apps/desktop/src/lib/stacks/stackService.svelte.ts).
- All descendants retain their relative order after the operation, but receive new commit SHAs because the amended commit's hash changes.

## Frequently Asked Questions

### What happens to commit SHAs when I amend a commit in a GitButler stack?

When you amend a commit, Git generates a new SHA-1 hash for that commit because the content or metadata has changed. During GitButler stacked branch restacking on commit amend, the backend rebases all descendant commits onto this new SHA, meaning every commit in the tail of the stack receives a new hash. The stack metadata in [`crates/gitbutler-stack/src/stack.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/gitbutler-stack/src/stack.rs) updates to track these new OIDs.

### Does GitButler preserve the order of commits during restacking?

Yes, the commit engine in [`crates/but-workspace/src/commit_engine/mod.rs`](https://github.com/gitbutlerapp/gitbutler/blob/main/crates/but-workspace/src/commit_engine/mod.rs) constructs a rebase plan that maintains the original sequence of descendant commits. The engine uses a standard pick operation to re-apply each commit on top of the amended one, ensuring the logical order remains intact even though the underlying Git objects are recreated.

### How does the frontend know when to refresh the stack view after an amend?

The frontend uses RTK-Query's cache invalidation mechanism. When `stackService.amendCommitMutation` in [`apps/desktop/src/lib/stacks/stackService.svelte.ts`](https://github.com/gitbutlerapp/gitbutler/blob/main/apps/desktop/src/lib/stacks/stackService.svelte.ts) resolves successfully, it invalidates the `Stacks` and `StackDetails` tags. This triggers an automatic refetch of the stack data, causing the UI to display the new commit hashes and restacked structure without requiring a manual refresh.

### Can I amend commits via the GitButler CLI?

Yes, the `but` CLI supports amending commits directly. You can use the `but amend` command with `--stack-id`, `--commit-id`, and `--worktree-changes` parameters to trigger the same `Destination::AmendCommit` logic used by the desktop application. This is useful for automation or when working in terminal-centric workflows.