GitButler Stacked Branch Restacking on Commit Amend: A Technical Deep Dive
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.
// 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.
// 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:
- The target commit is recreated with the supplied
worktree_changes - Subsequent commits are re-applied (picked) on top of the new commit
// 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.
// 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.
// 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.
// 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:
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:
# 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::AmendCommitin 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.rsfor the API,crates/gitbutler-branch-actions/src/actions.rsfor validation, andcrates/but-workspace/src/commit_engine/mod.rsfor the rebase logic. - The frontend implements drag-and-drop amendments via
AmendCommitWithChangeDzHandlerinapps/desktop/src/lib/commits/dropHandler.tsand synchronizes state throughstackService.amendCommitMutationinapps/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 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 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →