How GitButler Commit Editing Works: Amend, Reword, and Squash Explained
GitButler routes commit editing operations through a unified pipeline of Svelte drop-handlers and RTK-Query mutations, translating UI interactions into Git commands via the amend_virtual_branch, commit_reword, and squash_commits backend operations.
GitButler's visual branch management interface simplifies complex Git workflows by abstracting amend, reword, and squash operations into drag-and-drop interactions. Under the hood, these actions follow a consistent architecture: UI components trigger specialized drop-handler classes that validate operations before invoking the central StackService to execute Rust-backed Git commands.
The Architecture of GitButler Commit Editing
GitButler commit editing relies on three distinct layers: the UI drop-handlers that validate user interactions, the StackService mutation layer that manages API calls, and the Rust backend that executes raw Git operations.
From Drag-and-Drop to Git Command
When a user initiates a commit editing action, the UI routes the interaction through specific drop-handler classes defined in apps/desktop/src/lib/commits/dropHandler.ts. These handlers validate whether the dragged item can legally modify the target commit.
For amend operations, the AmendCommitWithChangeDzHandler class accepts FileChangeDropData or FolderChangeDropData instances and verifies that the target commit has no conflicts. For squash operations, the SquashCommitDzHandler class accepts CommitDropData and validates that the commits can be combined.
The StackService Mutation Layer
After validation, drop-handlers invoke methods from apps/desktop/src/lib/stacks/stackService.svelte.ts, which exposes RTK-Query mutations for each commit editing operation. The service translates TypeScript calls into commands sent to the Rust backend:
amendCommit→amend_virtual_branchcommitReword→commit_rewordsquashCommits→squash_commits
How Amend Works in GitButler
Amending allows users to add new file changes or hunks to an existing commit without creating a new one. GitButler implements this through specialized drop-handlers that support both full file changes and individual hunks.
The AmendCommitWithChangeDzHandler Class
Located in apps/desktop/src/lib/commits/dropHandler.ts at lines 83-101, the AmendCommitWithChangeDzHandler class manages the validation and execution of amend operations. The handler checks that the target commit has no conflicts before allowing the drop operation to proceed.
When a user drags a file or folder onto a commit, the handler packages the change data and calls stackService.amendCommit(), which triggers the amend_virtual_branch backend command.
Code Example: Amending a Commit with File Changes
import { AmendCommitWithChangeDzHandler } from '$lib/commits/dropHandler';
const amendHandler = new AmendCommitWithChangeDzHandler(
projectId,
stackService,
hooksService,
stackId,
true,
{ id: commitId, hasConflicts: false, isRemote: false, isIntegrated: false },
(result) => console.log('Amend result:', result),
uiState
);
amendHandler.ondrop(fileChangeDropData);
How Reword Works in GitButler
Rewording changes a commit's message without modifying its content or tree. Unlike amend and squash, rewording does not use a drop-handler because it involves direct text input rather than drag-and-drop interactions.
Direct Mutation Without Drop Handling
The reword functionality is exposed directly through the commitReword mutation in apps/desktop/src/lib/stacks/stackService.svelte.ts at line 1223. When a user edits a commit message in the UI and confirms the change, the component calls this mutation with the new message content.
The mutation maps to the commit_reword backend command, which executes the equivalent of git commit --amend --message="new message" while preserving the original tree.
Code Example: Changing a Commit Message
await stackService
.commitReword({
projectId,
stackId,
commitId,
message: 'Refactor authentication logic for OAuth2 compatibility'
})
.unwrap();
How Squash Works in GitButler
Squashing combines multiple consecutive commits into a single commit, preserving the changes but collapsing the history. GitButler implements this through the SquashCommitDzHandler class, which manages the drag-and-drop interaction when users combine commits.
The SquashCommitDzHandler Class
Defined in apps/desktop/src/lib/commits/dropHandler.ts at lines 361-380, the SquashCommitDzHandler class accepts CommitDropData instances representing the commits to be combined. The handler validates that the commits can be squashed, typically checking that they are consecutive and belong to the same stack, before invoking the service method.
When validation passes, the handler calls stackService.squashCommits(), which triggers the squash_commits backend command.
Code Example: Combining Consecutive Commits
await stackService
.squashCommits({
projectId,
stackId,
sourceCommitIds: [olderCommitId, newerCommitId],
targetCommitId: olderCommitId
})
.unwrap();
Key Source Files for GitButler Commit Editing
Understanding the commit editing implementation requires familiarity with these specific files in the gitbutlerapp/gitbutler repository:
apps/desktop/src/lib/commits/dropHandler.ts– ImplementsAmendCommitWithChangeDzHandlerandSquashCommitDzHandlerfor drag-and-drop validation and execution.apps/desktop/src/lib/stacks/stackService.svelte.ts– Defines RTK-Query mutations includingamendCommit,commitReword, andsquashCommitsthat interface with the Rust backend.apps/desktop/src/lib/history/types.ts– Enumerates history actions such asAmendCommitused by the UI to determine available operations.apps/desktop/src/lib/commits/commit.ts– Contains helper functions likegetMoveCommitIllegalActionMessagefor error handling in commit operations.apps/desktop/src/lib/stacks/stack.ts– Defines TypeScript types forStackActionandCommitobjects used throughout the commit editing pipeline.crates/gitbutler-workspace/src/lib.rs– Rust backend that executes the actual Git commands (amend_virtual_branch,commit_reword,squash_commits).
Summary
GitButler commit editing operations follow a consistent three-layer architecture that separates UI interactions from Git execution:
- Amend operations use
AmendCommitWithChangeDzHandlerorAmendCommitWithHunkDzHandlerto validate drag-and-drop actions before callingstackService.amendCommit(), which executes theamend_virtual_branchbackend command. - Reword operations bypass drop-handlers and directly invoke
stackService.commitReword()to trigger thecommit_rewordcommand, changing commit messages without modifying content. - Squash operations utilize
SquashCommitDzHandlerto validate that commits can be combined, then callstackService.squashCommits()to execute thesquash_commitscommand.
All operations route through the StackService RTK-Query mutations defined in apps/desktop/src/lib/stacks/stackService.svelte.ts, which communicate with the Rust backend in crates/gitbutler-workspace/src/lib.rs to perform the actual Git repository manipulation.
Frequently Asked Questions
How does GitButler handle conflicts during an amend operation?
The AmendCommitWithChangeDzHandler class in apps/desktop/src/lib/commits/dropHandler.ts checks the hasConflicts property of the target commit before allowing the drop operation. If conflicts exist, the handler prevents the amend action and surfaces an error message through the getMoveCommitIllegalActionMessage helper, ensuring users resolve conflicts before modifying commit history.
What is the difference between amend and squash in GitButler's implementation?
While both operations use drag-and-drop handlers, they serve different purposes and use distinct validation logic. Amend (AmendCommitWithChangeDzHandler) adds new file changes or hunks to an existing commit's content without creating new history entries. Squash (SquashCommitDzHandler) combines two or more existing commits into one, collapsing the commit graph. Amend modifies a commit's tree, while squash modifies the commit graph structure by merging histories.
Can GitButler reword merge commits or only regular commits?
According to the source code in apps/desktop/src/lib/stacks/stackService.svelte.ts, the commitReword mutation sends the commit_reword command to the backend without specific restrictions on commit type. However, the UI typically exposes the reword action through commit card interfaces that check properties like isRemote and isIntegrated to determine eligibility. Merge commits would be subject to the same backend capabilities as regular commits, though the UI may filter them based on stack state and remote synchronization status.
Where does the actual Git execution happen in GitButler's commit editing flow?
The actual Git operations occur in the Rust backend, specifically within the gitbutler-workspace crate at crates/gitbutler-workspace/src/lib.rs. When the TypeScript frontend calls mutations like amendCommit, commitReword, or squashCommits through stackService.svelte.ts, these map to commands (amend_virtual_branch, commit_reword, squash_commits) sent to the Rust layer. The backend then executes the equivalent Git plumbing commands directly on the repository.
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 →