Git edit - improving --fixup

I’m a big fan of attempting to make my git history useful. This often means reworking my commits so that work is done in a sensible order and in small logical units. There are many ways to rework your history in git but one I’ve used a lot for the past year is the --fixup flag when committing. This has often led to annoying problems, in this post I’ll look at:


Using --fixup

The --fixup flag is a bit like the --amend flag except you can specify the commit you want to squash your work into rather than it just being squashed into the most recent commit.

When given the following history:

* 0230dcb (HEAD -> master) Add git-edit post
* 4564152 Add testing tips post
* 9051216 Add tying things together post

If I stage some changes and run git commit --amend then the last commit Add git-edit post will be rewritten to include my staged changes.

If instead I want my changes to go into a commit that is not the most recent then I can use the following command git commit --fixup 4564152. In this example I would end up with the following

* a5f2563 (HEAD -> master) fixup! Add testing tips post
* 0230dcb Add git-edit post
* 4564152 Add testing tips post
* 9051216 Add tying things together post

Once I have this I can run git rebase --interactive 9051216 and accept the todo list to have git squash the new changes into the target commit.


Problems with --fixup

I love --fixup but it doesn’t quite do so well when you have heavy churn in your files (possibly a sign that the work could be reordered). The issue you run into a lot is that when you make your changes to fix up a commit you are potentially looking at a source file that has been changed in multiple steps. Any changes you make may only make sense in the current state of the file, this can lead to conflicts when targeting an earlier revision of the file.


A solution git-edit

We can avoid the issues above by:

This is achievable by using our friend git rebase --interactive <commitish>. Going back to our earlier example

* 0230dcb (HEAD -> master) Add git-edit post
* 4564152 Add testing tips post
* 9051216 Add tying things together post

If I want to make changes to the commit 4564152 then I can run git rebase --interactive 4564152~1 to be presented with a todo-list in an editor.

pick·4564152·Add·testing·tips·post¬
pick·0230dcb·Add·git-edit·post¬
¬
#·Rebase·9051216..0230dcb·onto·9051216·(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·<commit>·=·like·"squash",·but·discard·this·commit's·log·message¬
#·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.¬

#·Note·that·empty·commits·are·commented·out¬

I can now change the first pick line (which is for the commit we passed in) to say edit instead e.g.

- pick·4564152·Add·testing·tips·post¬
+ edit 4564152·Add·testing·tips·post

After saving this file I’ll be placed on the commit 4564152 where I can make my edits, add changes and finish up by calling git rebase --continue.

The above is fine but it’s a little manual so it would be nice if we could automate away some of these steps. Let’s see how we can do that.


Creating your own git commands

The first piece of the puzzle is creating a git command. I’d like the command to work as if it was a git subcommand by typing git edit <commitish>. This is actually not too difficult to do - add an executable command somewhere on your path with the name git-edit. Now when I call git edit <commitish> git will invoke my script. Let’s test it

~/bin/git-edit

echo "hey"

zsh

> git edit aabbcc
hey

GIT_SEQUENCE_EDITOR

The next challenge is how do I automate editing the todo list that you get when you call git edit? Again git has a nice seam for us to work with - we can set the environment variable for GIT_SEQUENCE_EDITOR to our own command. Our command will be invoked with one argument, the path to the todo-list file.


With this knowledge we can write a script that will:

Let’s do this in ruby:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env ruby

if ENV['GIT_SEQUENCE_EDITOR'] != __FILE__
  exec "GIT_SEQUENCE_EDITOR='#{__FILE__}' git rebase --interactive #{ARGV.first}~1"
end

todo_list = ARGV.first

lines = File.readlines(todo_list)
  .lazy
  .take_while { |line| line != "\n" }
  .map        { |line| line.gsub("fixup", "pick") }
  .to_a

lines[0] = lines[0].gsub("pick", "edit")

File.write(todo_list, lines.join)

With all of this in place we can now run a git subcommand that automates the process of rewinding us back to a particular commit to allow us to make changes and then reapply all subsequent commits in order.


Conclusion

There’s a few handy things to take away here around adding your own scripts to git. Knowing that it’s actually not too painful to add quite complex automation means that I’ll really pay attention more to manual tasks that I repeat and look at automating these tasks in future.