3

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?

Luuklag
  • 3,897
  • 11
  • 38
  • 57
oberlies
  • 11,503
  • 4
  • 63
  • 110

2 Answers2

4

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
oberlies
  • 11,503
  • 4
  • 63
  • 110
1

Paraphrasing the splitting commits section of the git-rebase documentation:

  1. Start an interactive rebase with git rebase -i <commit> for some commit before the one you want to split.

  2. Mark the commit you want to split as edit.

  3. When the rebase reaches the commit you want to split, do git reset HEAD^ to leave you in a state where the combined changes are in your working directory but not committed.

  4. Make two commits: one with your refactoring and one with your renames.[*]

  5. Do git rebase --continue to replay any later changes on top.

Obviously, the usual caveats apply about only doing this if you haven't already shared your changes.

--

[*] Edit

At this stage you have a load of unstaged deletions and a load of unstaged new files with different names and possibly edited content.

To reapply the refactoring, you can just do this:

a. copy new-file-name old-file-name (for each file with refactoring); they will appear as modifications

b. git add and commit just those changes in the usual way

c. delete the old files from disk again

Then all your renamings are present in the working folder and can form a second commit as they are.

Matthew Strawbridge
  • 19,940
  • 10
  • 72
  • 93
  • Depending on the refactoring, step 4 may be very hard. E.g. image that a class was renamed and then some code was added to it. In that case, it is very unlikely that you get it right - whereas in [this answer](https://stackoverflow.com/a/44325383/1523648), this just works out of the box. – oberlies Jun 02 '17 at 09:51
  • 1
    I guess it depends (in the general case) which is more difficult to do again, the renaming or the refactoring. Always good to see people answering their own questions for the benefit of the community, so thanks for that :-) – Matthew Strawbridge Jun 02 '17 at 11:00