Skip to content

Conversation

@Caleb-T-Owens
Copy link
Contributor

@Caleb-T-Owens Caleb-T-Owens commented Nov 12, 2025

Graph based rebasing?

As a high level goal, we want to improve our existing operations to have the following qualities:

  • Operations should work in single branch mode.
  • Operations should use use the git commit graph as their source of truth.
  • Operations should not rely on "legacy" code.
  • Operations should (though even most of the old operations do) perform their operations in memory, determine success, and then materialize their outcomes.

The vast majority of operations in GitButler don't live some combination of these ideals. One set of operations that we care to improve are the ones that involve manipulating commits in some way.

To name a few:

  • renaming commits
  • reordering commits (moving a commit from one location in the graph to another)
  • squashing commits
  • amending commits with uncommitted changes
  • moving changes between commits
  • removing changes from a commit
  • integrating worktrees

Each of these operations modify commits and since commits are not mutable, some portion of the git commit graph needs to be updated - and any cooresponding references need updated.

With each of these operations, we could do the history rewrites by hand each time - but that would expose a large amount of detail in the implementations.

In order to help simplify the current implementations of the listed operations, we have opted to break out the history-rewrite into it's own function, letting the body of these operations focus just on manipulating the specific commits that they are interested in.

Some examples of existing functions

Renaming commits

The current implementation can be found at https://github.com/gitbutlerapp/gitbutler/blob/master/crates/gitbutler-branch-actions/src/virtual.rs#L525 in full.

A high-level overview of this function is as follows:

  • Take the stack that contains the commit we want to rename, and prepare it for our old rebase engine by gathering a linear set of pick steps similar to a git rebase -i output.
  • Then find the correct step that we want to modify, and change the step to update the commit's message.
  • Then check if it violates users who require no force pushes
  • Then rewrite the history
  • Update cooresponding references
  • Update workspace commit

This implementation doesn't check for workspace conflicts because it's not changing any trees.

Of note here, the old rebase engine has a "new_message" property in it's steps. It origionally had a "new_tree" property too, but the general concensus was that rewriting the specific really ought to be the concern of the operation's implementation, rather than the rebase-engine, so re-treeing was remove. I only presume that "new_message" wasn't removed due to it being less offencive & the relative effort required to remove it.

Reordering commits

The implementation can be seen in full at: https://github.com/gitbutlerapp/gitbutler/blob/master/crates/gitbutler-branch-actions/src/reorder.rs#L25.

A high-level overview of this function is as follows:

  • It's given a spec which specifies the order of every reference and commit inside a given stack
  • The spec is validated to ensure no commits or references are added or removed
  • A list of rebase steps is created for the stack
  • The steps are re-ordered to align with the spec
  • The history gets rewritten
  • Cooresponding references get updated
  • The workspace commit is updated

Strangly this doesn't look ahead to see if the workspace commit merge will succeed.

Moving changes between commits

The implementation can be seen in full at: https://github.com/gitbutlerapp/gitbutler/blob/master/crates/but-workspace/src/legacy/tree_manipulation/move_between_commits.rs#L58.

A high-level overview of this function is as follows:

  • It's given the source & destination stacks, source & destination commits, and a description of what should be moved.
  • The source commit get's rewritten with the desired changes removed.
  • The source stack's rebase steps get created
  • The source stack has has the source commit pick replaced with the new modified commit's id
  • The history of the source stack is rewritten
  • If the the destination stack is the same as the source
    • Using the rebase engine's commit mapping, we update the source stack's rebase steps to pick all the new IDs
    • The destination is found, using the updated version if it was rebased.
    • The destination commit is rewritten with the desired changes added back
    • The steps are updated with an updated destination pick
    • The history is rewritten again.
  • Else, the destination is a different stack
    • The destination steps are found
    • The destination commit is rewritten to include the desired changes
    • The desination steps have the destination commit's pick to point to the rewritten commit
    • The history of the destination is rewritten

The updating of the workspace is handled externally.

We do the rebase in two-steps in the destination_stack_id == source_stack_id case so we can make use of the rebased destination commit (if it was "above" the source commit).

How do we want to represent these operations going forwards?

Throughout the history of GitButler, we've always performed series of cherry-picks with differing utilities, where some might cherry pick an array of commits, and others might take a special range spec. The consolidation of history rewriting into the linear rebase engine however was a healthy step forward into standardizing how we build these operations that modify git history, and abstracted out excess detail from main buisness logic of the operations.

The current solution does have some downsides:

  • It's coupled in some ways to the stack model
  • It's not designed to work on arbitrary graphs of commits. It's designed with first-parent-traversals in mind.
  • It doesn't take a wholistic view of the workspace into account which means:
    • It doesn't provide a good way of guaging whether the result might result in a workspace commit conflict.
    • Keeping metadata in line has to be handled seperately.

The over-all idea of abstracting out history-rewriting still seems to be a helpful concept. As such it seems reasonable to try and build a new rebase engine that addresses the above listed downsides, and adheres to the goals listed further above.

It seems to make sense to have something that can operate on an arbitrary commit graph, with some additional GitButler-specific introspections.

There is some prior art in reguards to rebasing commit graphs. Namely:

A general flow

A general flow for some new system at a high level could be along the lines of:

  • Create an editor that gives us the ability to maniuplate the commits that get picked, similar to the old rebase engine.

  • Update the pick statements like the old rebase engine to use commits that were rewritten by the specific operation

  • Perform the rebase

    • This should be an output that describes the full output, including but not limited to

      • References that were added, removed, or updated
      • How commits were mapped
      • If any commits became conflicted
    • It should be transformable into a second editor to better support usages like moving changes between commits

    • We should ensure that we're not going to require a force push for users that care about that don't want that. Perhaps this is done through having a function that takes the rebase output, and performs extra validations, and the output of that function which performs extra validations is what we actually pass to the materializer.

  • Materialize the rebase

    • The materializer should only be able to recieve a rebase output that complies with any assertions we want to make about the workspace (IE, no conflicting gitbutler/workspace commits)
    • This would perform a safe_checkout

What would the capabilities of a graph editor need to be?

In the examples of the old rebase engine that I went over, which are representative of the other operations mentioned, there are a small set of operations on the linear sets of commits:

  • Replacing a pick
  • Removing a pick (can be considered replacing with none)
  • Inserting a pick
  • Replacing a reference
  • Removing a reference (can be considered replacing with none)
  • Inserting a reference

It's possible that some of the raw operations that we want to perform, like amending a commit could be done without a full cherry-pick based rebase, but since we'd like to generally abstract rewriting history into one higher-level system, we instead need to be able to rebase an arbitrary commit graph.

Rebasing an arbitrary commit graph

For linear runs of commits - rebasing remains the the old cherry-pick operations. However, we need to consider what happens if we need to replace one or more parents of a merge commit. For a cherry pick, we can only operate with one "base" and one "ours" tree. For these cases we can find the merges of all the bases, and all the ours commit and use the resulting trees for our cherry-pick. This is already implemented and tested in the but-graph/src/graph-rebase/cherry-pick.rs with hopfully a good amount of tests as examples of the behaviour.

@vercel
Copy link

vercel bot commented Nov 12, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
gitbutler-web Ignored Ignored Preview Nov 28, 2025 1:47pm

@github-actions github-actions bot added the rust Pull requests that update Rust code label Nov 12, 2025
@Caleb-T-Owens Caleb-T-Owens force-pushed the graph-based-rebasing branch 5 times, most recently from 0251bf7 to 1e8dd78 Compare November 17, 2025 14:24
@Caleb-T-Owens Caleb-T-Owens force-pushed the graph-based-rebasing branch 6 times, most recently from c6608cf to 83041a9 Compare November 25, 2025 12:33
@Caleb-T-Owens Caleb-T-Owens force-pushed the graph-based-rebasing branch 5 times, most recently from ac252ba to 0c1e750 Compare November 28, 2025 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants