Search code examples
gitrefactoringgit-commit

How to split a Git commit that contains a refactoring?


I have created a commit in my local Git repository that includes a functional change and a refactoring. In retrospect, it would have been better if I had created a separate commit for the refactoring: The refactoring was a rename, and hence caused diffs in many files. Between all these rename diffs, it is hard to see the functional changes. This e.g. makes a code review harder than it needs to be.

So, is there an easy way to fix this? I.e. is it possible to split the commit into a refactoring commit and a commit with the functional changes?

I tried to split the commit through a manual triage of the diffs - as proposed in the answers to Break a previous commit into multiple commits - but this was really tedious. Isn't there an easier way?


Solution

  • Yes, there is an easy way - assuming that you can easily repeat the refactoring. The approach is to create a commit with only the refactoring, and then "subtract" it from the mixed commit.

    You can do this in the following way:

    1. Check the branch that you are currently on, e.g. with git branch. For the remaining steps, we'll assume that you start on branch master
    2. (Optional) Create a backup branch: git branch backup
    3. Restore the state before the commit to be split. This is done by checking out the parent commit of the commit to be split: git checkout HEAD^
    4. Do the refactoring again and commit it with git add --all && git commit
    5. Create the commit that contains the remaining changes by "subtracting" the refactoring commit from the mixed commit:

      git reset --hard master
      git reset --soft HEAD@{1}
      git commit -c HEAD@{1}
      git branch -f master
      git checkout master
      
    6. (Optional) Verify that the two new commits in fact contain the same modifications: git diff backup should show no diffs.

    7. (Optional) Review the two commits you created. If you don't like the result, you can squash them back together (or revert with git reset --hard backup) and start over. Otherwise, delete the backput branch: git branch -D backup