Linux Fu: Deep Git Rebasing

If you spend much time helping people with word processor programs, you’ll find that many people don’t really use much of the product. They type, change fonts, save, and print. But cross-references? Indexing? Largely, those parts of the program go unused. I’ve noticed the same thing with Git. We all use it constantly. But do we? You clone a repo. Work on it. Maybe switch branches and create a pull request. That’s about 80% of what you want to do under normal circumstances. But what if you want to do something out of the ordinary? Git is very flexible, but you do have to know the magic incantations.

For example, suppose you mess up a commit message — we never do that, of course, but just pretend. Or you accidentally added a file you didn’t want in the commit. Git has some very useful ways to deal with situations like this, especially the interactive rebase.

Identify a Commit

If you haven’t realized it, every version of your project in Git boils down to a commit. Branches and tags are just “pointers’ to commits. In addition, commits point to their parent commit. So, suppose you have the following sequence of commands:

mkdir project
cd project
git init
touch readme.md
git add readme.md
git commit -a -m "First Commit"

So far, this is pretty standard stuff. Next, we are going to make our first change, and we’ll simulate an emacs backup file. This will be the first change we will commit.

touch hackaday.txt
touch hackaday.txt~
git add hackaday*
git commit -a -m "Add hackaday.text"

Oops. We have two problems here, but for the sake of the example, suppose we only noticed the typo in the commit message (“text” instead of “txt”).

If any of this doesn’t make sense, you might want to review the basics of Git before you keep going. The video below can help, although there are plenty of other options. If you’d rather read, there’s also the Pro Git book.

Quick Fix

If you need to fix the last commit, it is pretty easy. It helps to notice what the ID of each commit is. There’s a long ID string, but Git can show you the first few characters of it. Yours will be different, but when I did this, that last commit was 86d63c3. You can use these ids or tags like HEAD (the current commit) to go further up the git commit graph.

If you ask git to show you what’s going on with “git log” you can see that the original first commit was 51a18eb. In addition, you can see that HEAD, the pointer to the tip of the commit graph, is pointing to 86d63c3. But since we’re just changing the last commit, you don’t need to know that. In this case, here’s what you can do:

git commit --amend -m "Add hackaday.txt"

If you do a git log now, you’ll see the commit message changed. However, since this is a new commit, the ID number changes also (for me, 9b0125c). That means if you have already pushed the commit to a remote repo, you shouldn’t do the amend.

There are still only two commits, the original one and our new one with the corrected message. One problem solved, but we still have that backup file. Unfortunately, we don’t notice it until later.

A New Commit

Next, we are going to get a Jolly Wrencher graphic in and commit again.

cp ~/assets/wrencher.png .
git add wrencher.png
git commit -am 'Add logo'

This is fine, and we now have three commits in our current branch. The problem is we need to fix the second one now. We now have three commits. The original initial commit was (for me) 51a18eb. The updated second commit is 9b0125c. The last commit with the logo is 3451549.

To fix the commit, we will rebase the last commit. Remember that the last commit is “based” on the previous commit, which is based on the commit before that, and so on. To change our commit, we need to rebase to one commit beyond the one we want to change. One way to do that is to specify 51a18eb. We can do that interactively with:

git rebase -i 51a18eb

This opens an editor with a “script”:

pick 9b0125c Add Hackaday.txt
pick 3451549 Add logo

# Rebase 51a18eb..3451549 onto 51a18eb (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#

The comments are helpful. By default, all the commits use the “pick” command which just keeps them. You can also do things like drop them or use reword to change a commit message. We want to do an edit, so you can change the first pick command to an edit command:

edit 9b0125c Add Hackaday.txt
pick 3451549 Add logo

When you exit your editor, you get a helpful message:

Stopped at 9b0125c...  Add Hackaday.txt 
You can amend the commit now, with 

 git commit --amend  

Once you are satisfied with your changes, run 

 git rebase --continue

Looking around now, you’ll see everything is as it was for that commit. That is, there’s no PNG file, and you have both hackaday.txt files. Let’s fix it:

git rm hackaday.txt~
git commit --amend
git rebase --continue

You can always use a git status to see what git thinks you need to do next.  After these commands, you still have three commits, but the accidental add of hackaday.txt~ has vanished.

As you can see, there are other commands, too. You can merge multiple commits together. You can also squash them or fix them up. These essentially turn one commit into two. The difference is a squash gives you a chance to keep or change the commit message, while fixup keeps only one of the original messages. Remember that these are both different from a merge, which creates a new commit from multiple parent commits. A squash or a fixup converts multiple commits into a single commit. You can also hit the panic button with “git rebase –abort.”

Picking Your Commit

Rember that each commit has an ID, but you also have tags and even relative indexing available. You could also use the notation HEAD~2 (in this case). This tells git to start at the HEAD and go back two generations. If you are familiar with merge commits, this assumes you are going back in the same branch. When a merge commit gives you a choice of parents, you can also use the notation HEAD^ to pick one of the parents. You can even mix and match these. But you can also always get the ID from git log and use that. Or, you can use a tag if you’ve tagged a certain commit.

Just make sure you are working with the commit before the last commit you want to edit. In this case, we wanted to edit HEAD~ (or HEAD~1, if you prefer), so we had to rebase HEAD~2. If you really want to edit using the commit number you actually want to edit, just put a ~ after it. That selects the parent of the specified ID.

Non-Interactive Mode

You can, of course, do rebasing without the interactive mode, but it is a lot of work. The interactive mode is good, too, for things like splitting a commit into multiple commits. That’s because when you edit a commit, you can actually add multiple commits as part of the edit.

You’ll find if you start rebasing, you’ll use git log a lot. There is a post that shows how to make better-looking output if you prefer. Turns out, you can use git for a lot of things. If you crave something simpler, try the gitless shell that runs over git.

22 thoughts on “Linux Fu: Deep Git Rebasing

  1. As someone who really values a clean gut history, got rebase is one of my most favorite power commands.

    So not ever use it on a master-like branch, but your own branches (even if pushed, and people are aware it being changed) is fair game.

  2. I don’t want to hijack the Git discussion, but the opening premise about word processor programs got my attention. While I agree that many people don’t use most of the available features, this can create some odd responses from commercial vendors. Microsoft’s recent decision to drop Wordpad is a good example.

    Wordpad offers most of the features that most users typically want. It is very utilitarian, has a much smaller footprint than Microsoft Word and its UI is far less complex and easier to navigate. And did I mention that (unlike Word) it is included without additional cost in a Windows installation?

    Oh well, time to learn MS Word for doing my shopping list and greeting cards.

      1. I use LibreOffice daily for writing complex technical documents and adhering to templates that other members of my work group require for conformance with our business practices. It is slow and has a very large footprint, but that’s OK when you need the extensive capabilities that it offers.

        I write a lot, and much of it is notes and brief outlines written on a small, inexpensive Windows pocket sized tablet that I always have with me. These serve as reference whether I’m using LibreOffice on my development machine or shopping at Walmart.

        It’s a matter of convenience.

    1. I could never wrap my head around word processos. Every time I tried I just recoiled in terror, for they apparently seem to force you to do far more work manually than it is necessary.

      There is a lot ot better options – markdown, org, various TeX flavours, scribble, etc.

      What is the point in word processors, both simple Wordpad-like and full blown Word-scale monsters?

      1. Although only available comercially, I think Confluence naled this. Their editor is limited to a few fonts, a few colors, basic tables and a bunch of header levels. Great to not get distracted or bogged down in making sure everything is consistent. And, figures have a field for a subtitle straight away:)

    2. It got my attention too – I’m a very basic user of word processors, but still run on people who create complex looking documents done like:
      – add more “enters” to start new page.
      – double enter for line spacing.
      – don’t use tabs – spaces to the rescue.
      – weird font usage (two people edit document? two different font in use)
      Editing that later on is so much fun – usually done under time pressure.

      There are some small text processors like Abiword (not a fan o fit) or Pathetic Writer (part of SIGAG Office) and few others. But Word Pad was really forgotten option – good for necessary basics not overloaded with functions. Just like Paint.

      1. I think we all have our own styles. I use (internally) something similar to gitflow so I should NEVER have anything in the branch that doesn’t get pushed up every time (except junk that NEVER gets pushed up). So my way of doing it is add the first time and -a every other time. That keeps you from breaking a build by having something in your branch that changed but never got picked up. If I have something that isn’t ready to go up, then I should have been in a different branch. But there are a dozen different ways to do things in Git. Gitflow is heavy for some people. Working by yourself is different than working in a team, etc. So that’s the nice thing. You can do you, and I can do me.

  3. One thing I’ve never liked about git is how resolving conflicts in rebases and merges let the resolver basically do whatever they want. If I get a merge conflict, my “resolution” could be just totally overwrite the file with something else and as far as anyone’s concerned, I’ve just resolved the conflict (especially when squash merging).

    Maybe the issue is more with GitHub, since it can require approvals on the head of a branch before merging but generally there’s no way to require approval of the final merge commit that gets put in main
    (If anyone knows about how to do this better, please say)

    Similarly, with rebasing, the rebaser can write whatever they want for each conflict resolution. I once had to rebase some commits we made onto kernel drivers from one version of the kernel to a later one, I didn’t know half of what I was rebasing so the end result introduced a bunch of bugs. At the time of rebasing, I tried to get approvals from the original authors but time pressure generally meant the commits were just tried by fire

  4. One thing I’ve never liked about git is how resolving conflicts in rebases and merges let the resolver basically do whatever they want. If I get a merge conflict, my “resolution” could be just totally overwrite the file with something else and as far as anyone’s concerned, I’ve just resolved the conflict (especially when squash merging).

    Maybe the issue is more with GitHub, since it can require approvals on the head of a branch before merging but generally there’s no way to require approval of the final merge commit that gets put in main
    (If anyone knows about how to do this better, please say)

    Similarly, with rebasing, the rebaser can write whatever they want for each conflict resolution. I once had to rebase some commits we made onto kernel drivers from one version of the kernel to a later one, I didn’t know half of what I was rebasing so the end result introduced a bunch of bugs. At the time of rebasing, I tried to get approvals from the original authors but time pressure generally meant the commits were just tried by fire

  5. Seems to be a typo in the text accompanying the rebase example.
    First we see “The original initial commit was (for me) 51a18eb. ”
    Next we see “One way to do that is to specify 51a183b.” Note the first ID ends in “eb” ; the second “3b”.
    The rebase command uses the former number, ending in eb.
    git rebase -i 51a18eb
    https://hackaday.com/2023/10/17/linux-fu-deep-git-rebasing/#:~:text=One%20way%20to%20do%20that%20is%20to%20specify,.%20&text=(for%20me)%2051a18eb

  6. There is a typo in the ID used in the text talking about the rebase.
    The text says “One way to do that is to specify 51a183b” The next to last character in the ID should be “e” instead of “3”.

    1. Dang it. I messed up in the example and had to “rebase” in mind with a different ID. So I manually fixed the early ones to hide that it really wasn’t the original commit and I just fat fingered it. Fixed.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.