blog What is in an Interactive Rebase?

Tags:
git

If we understand what files git rebase creates, then we can manually create our own rebase plan.

# Lets check out a branch to work with
git checkout origin/main demo-branch
# and kick off a rebase against root
git rebase -i --root

While our EDITOR is open, we have a directory that looks like this.

tree .git/rebase-merge
.git/rebase-merge
├── .git-rebase-todo.swp
├── git-rebase-todo
├── git-rebase-todo.backup
├── head-name
├── interactive
├── no-reschedule-failed-exec
├── onto
├── orig-head
└── squash-onto

We have a few files that have data in them

  • git-rebase-todo is the list of commits that we are used to seeing.
  • head-name is a reference to our starting branch. In this case refs/heads/demo-branch
  • orig-head is a reference to our starting branch as a commitish.
  • onto and squash-onto are our new parents (since we used --root)

There are also a few files that mark flags we set

  • interactive
  • no-reschedule-failed-exec

( A lot of tools like starship.rs will also look at the existence of files under our .git tree for displaying status )

Knowing what is in this directory, we should be able to craft a rebase programmatically, and then execute it with git rebase --continue

Creating a Rebase

First, we want to setup our basic rebase

# Directory where our rebase request will be built (running `git rebase` also makes this directory)
mkdir .git/rebase-merge
# Ensure we treat this as an interactive rebase
touch .git/rebase-merge/interactive
# And optionally set any flags we want
touch .git/rebase-merge/no-reschedule-failed-exec

Next, we need to setup the branches that will be affected by our rebase.

# Save our branch name
git symbolic-ref HEAD > .git/rebase-merge/head-name
# Save the original head
git rev-parse HEAD > .git/rebase-merge/orig-head

Normally running git rebase will populate git-rebase-todo with all the commits, and you can use keywords like pick and squash and so on to direct how you want to rebase.

# Example git-rebase-todo from
# https://git-scm.com/docs/git-rebase
pick deadbee Implement feature XXX
fixup f1a5c00 Fix to feature XXX
exec make
pick c0ffeee The oneline of the next commit
edit deadbab The oneline of the commit after
exec cd subdir; make test
...

Since we’re rebasing, we need to configure the commit we will rebase onto. If we want to rebase against a new root, we need to create a new, empty commit

# All commits need a tree, and a 'null' commit is often a tree pointing to /dev/null
NULL_TREE=`git hash-object -t tree /dev/null`
# Given our tree, we create a new commit
NULL_COMMIT=`echo 'Some commit message' | git commit-tree $NULL_TREE`
# We want to save our commit into our rebase-merge config
echo $NULL_COMMIT > .git/rebase-merge/onto
echo $NULL_COMMIT > .git/rebase-merge/squash-onto

# Then we checkout our detached head
# Since we'll start applying our commits here.
git checkout --quiet $NULL_COMMIT

With this in place, we have created the state that a normal git rebase -i --root would have. Now we can run git rebase --continue and git will run through our .git/rebase-merge/git-rebase-todo and apply all of our changes.

Because we made a .git/rebase-merge/interactive file, it will work the same as any other interactive merge, and will prompt us any time there are commits to be resolved.