Tristan Penman's Blog

Fixing the authorship of a Git commit

11 December 2015

When working on code across multiple organisations, one problem that I have run into more often than I would like, is correcting the authorship of Git commits. You know the scenario - you create a new Git repository, but forget to set the appropriate user name and email address before a flurry of commits. This post covers one strategy that I have found useful for fixing incorrect Git commits.

Author vs Committer

Before diving in, it is important to understand the different meanings of ‘author’ and a ‘committer’. In Git parlance, the author of a commit is the person who produced the original patch (e.g. as part of a pull request), whereas the committer is the person who applied the patch (and possibly modified it while doing so). This allows both contributers to be identified via the record of a single commit.

You can see both the author and committer using:

git log --format=full

which will give you something like this:

commit 3152bacbcddd23414ca4eeff12a1abe23455878a
Author: tristan <tristan@localhost>
Commit: tristan <tristan@localhost>

    Fix error logging in dev environment

It would be nice to fix this before pushing the branch somewhere where collaborators might find it. However, it turns out that when you amend a commit using a command such as:

git commit --amend --reset-author

only the author of the commit will be updated. However, in the ‘newly created repo’ scenario I described above, you would typically want to correct both the author and committer of your commits. So even if you only need to correct the most recent commit, you will need to take a different approach.

Filtering a Branch

The command we’re going to use is git filter-branch. This command allows you to rewrite the history of a set of branches, using various kinds of filters.

The filter we’re going to use is --env-filter, which uses environment variables to expose the author and committer details for a given revision. The argument to --env-filter is a string containing a sequence of shell commands that will be run for each revision that is filtered. You can use this mini-script to reset these environment variables to selectively update those details for each revision.

As an example, this is the command you would run to replace the author name and email address for all revisions a branch’s history:

git filter-branch --env-filter '
  export GIT_AUTHOR_EMAIL="new@address"
  export GIT_AUTHOR_NAME="New name"
' HEAD

(HEAD can be replaced with the name of the branch you would like to apply the filter to, or omitted entirely to apply the filter to the current branch)

This command would update the author of every revision in the active branch. The committer could also be updated by re-exporting the following environment variables:

  • GIT_COMMITTER_EMAIL
  • GIT_COMMITTER_NAME

Conditional updates

For the sake of reusability, let’s assume that we have exported environment variables that contain the details of the new author and the author that we would like to replace:

OLD_ADDR="old@address"
OLD_NAME="Old name"
NEW_ADDR="new@address"
NEW_NAME="New name"

Using a Bash conditional expression, we can craft a filter that will update the author name, but only if it matched the name that we were expecting:

git filter-branch --env-filter '
  [ "$GIT_AUTHOR_NAME" = "$OLD_NAME" ] && export GIT_AUTHOR_NAME=$NEW_NAME
' HEAD

Using && ensures that the export portion of the statement is only executed if the conditional expression evaluates to true.

A series of conditional updates can be combined into one filter (e.g. updating the author and committer at the same time), like so:

git filter-branch --env-filter '
  [ "$GIT_AUTHOR_EMAIL"    = "$OLD_ADDR" ] && export GIT_AUTHOR_EMAIL=$NEW_ADDR
  [ "$GIT_AUTHOR_NAME"     = "$OLD_NAME" ] && export GIT_AUTHOR_NAME=$NEW_NAME
  [ "$GIT_COMMITTER_EMAIL" = "$OLD_ADDR" ] && export GIT_COMMITTER_EMAIL=$NEW_ADDR
  [ "$GIT_COMMITTER_NAME"  = "$OLD_NAME" ] && export GIT_COMMITTER_NAME=$NEW_NAME
' HEAD

And to apply this filter to all branches in a repository, replace HEAD with -- --all.

Rewriting History

Remember when you begin making changes like this, that you are effectively rewriting the history of your git repository. Making these kinds of changes to public branches is an effective way to alienate and infuriate collaborators, so be careful!