inkel

Change several Git commits' author easily

4min.

Recently I’ve changed jobs, and that meant, among many other things, that my work email address changed, which means that my commits are now using the wrong email address! I need to fix that, but of course I’ve already committed stuff using my old email address. How can I fix that?

Luckily the solution isn’t difficult, though it took me a while to properly figure it out.

The first thing I needed to do was to configure my repository to use my work email address:

path/to/work-repo $ git config user.name "Leandro López (inkel)"
path/to/work-repo $ git config user.email "work@example.com"

Note that I’m doing this only for my work related repositories, so all of my other work (e.g. public or open source) uses my personal email address.

Next I want to know which commits SHA1s are using my personal email address. I can use the --author flag of git-log(1) and get that information easily:

$ git log --pretty=format:"%h - %aN <%ae> - %s" --abbrev-commit --author=personal@example.com
86b860dcc - inkel <personal@example.com> - Use stringSet for collecting child modules
9822062a9 - inkel <personal@example.com> - Use stringSet for collecting top root modules
d6d397f3c - inkel <personal@example.com> - Add stringSet type to collect unique set of strings

Now what I want to do is to reset the author to use the one I configured for this repository without editing the commit message; additionally I want to add a Signed off by line at the end of each commit message. One way to do this is to manually execute the following for every commit:

$ git commit --amend --signoff --author="Leandro López (inkel) <work@example.com>" $COMMITSHA

But as you can figure out, this becomes tedious if you have multiple commits. So let’s use the power of git-rebase(1) and automate this:

$ git rebase --exec='git commit --no-edit --amend -s --reset-author' d6d397f3c^
Executing: git commit --no-edit --amend -s --reset-author
[detached HEAD f29b70ad2] Add stringSet type to collect unique set of strings
 2 files changed, 120 insertions(+)
 create mode 100644 docker/terraform/automation/stringset.go
 create mode 100644 docker/terraform/automation/stringset_test.go
Executing: git commit --no-edit --amend -s --reset-author
[detached HEAD 3ca17042b] Use stringSet for collecting root modules
 1 file changed, 7 insertions(+), 21 deletions(-)
Executing: git commit --no-edit --amend -s --reset-author
[detached HEAD 5d168a566] Use stringSet to collect modules
 1 file changed, 3 insertions(+), 14 deletions(-)
Successfully rebased and updated refs/heads/foo.

And that’s it! All the commits got fixed to use my work email address instead.

What is going on?! Let’s dig into it

First is the --exec flag. This tells Git that for the given list of commits it needs to execute that command after each commit. In this case the command to execute is git commit --no-edit --amend -s --reset-author which amends the commit (--amend) without changing the commit message (--no-edit) except by adding a Signed off by line at the end of the message (-s which is short for --signoff) and changing the author to the one this repository should use (--reset-author, remember we add a custom configuration for it).

Then comes the commit list, in this case d6d397f3c^. Look again at the list of commits above. If you see, the SHA is the same as the one at the bottom of the list, which is the first commit of the list (they are shown in reverse order). The ^ at the end of the SHA instructs Git to go through the current list of commits until the parent of that commit, without including it.

And that’s it! Once this is done, I pull the list of commits again and it looks right:

$ git log --pretty=format:"%h - %aN <%ae> - %s (%cr)' --abbrev-commit --date=relative" --author=work@example.com
5d168a566 - Leandro López (inkel) <work@example.com> - Use stringSet for collecting child modules
3ca17042b - Leandro López (inkel) <work@example.com> - Use stringSet for collecting top root modules
f29b70ad2 - Leandro López (inkel) <work@example.com> - Add stringSet type to collect unique set of strings

This is a very particular use case, but what else we could do with it? Well, you can pass anything to the exec flag, not only Git commands, so you could say run your test suite for each commit and if the command fails then it will stop executing the rebase, allowing you to fix things before continuing. Definitely a very powerful tool to have in your toolbox!