How can I split up a Git commit buried in history?

49,162

Solution 1

There is a guide to splitting commits in the rebase manpage. The quick summary is:

  • Perform an interactive rebase including the target commit (e.g. git rebase -i <commit-to-split>^ branch) and mark it to be edited.

  • When the rebase reaches that commit, use git reset HEAD^ to reset to before the commit, but keep your work tree intact.

  • Incrementally add changes and commit them, making as many commits as desired. add -p can be useful to add only some of the changes in a given file. Use commit -c ORIG_HEAD if you want to re-use the original commit message for a certain commit.

  • If you want to test what you're committing (good idea!) use git stash to hide away the part you haven't committed (or stash --keep-index before you even commit it), test, then git stash pop to return the rest to the work tree. Keep making commits until you get all modifications committed, i.e. have a clean work tree.

  • Run git rebase --continue to proceed applying the commits after the now-split commit.

Solution 2

Here's how to do it with Magit.

Say commit ed417ae is the one you want to change; it contains two unrelated changes and is buried under one or more commits. Hit ll to show the log, and navigate to ed417ae:

initial log

Then hit r to open the rebase popup

rebase popup

and m to modify the commit at point.

Notice how the @ there is now on the commit you want to split – that means HEAD is now at that commit:

modifying a commit

We want to move HEAD to the parent, so navigate to the parent (47e18b3) and hit x (magit-reset-quickly, bound to o if you're using evil-magit) and enter to say "yes I meant commit at point". Your log should now look like:

log after resetting

Now, hit q to go to the regular Magit status, then use the regular unstage u command to unstage what doesn't go in the first commit, commit c the rest as usual, then stage and commit what goes in the second commit, and when done: hit r to open the rebase popup

rebase popup

and another r to continue, and you're done! ll now shows:

all done log

Solution 3

To split a commit <commit> and add the new commit before this one, and save the author date of <commit>, — the steps are following:

  1. Edit the commit before <commit>

    git rebase -i <commit>^^
    

    NB: perhaps it will be also needed to edit <commit> as well.

  2. Cherry pick <commit> into the index

    git cherry-pick -n <commit>
    
  3. Interactively reset unneeded changes from the index and reset the working tree

    git reset -p && git checkout-index -f -a
    

    As alternative, just stash unneeded changes interactively: git stash push -p -m "tmp other changes"

  4. Make other changes (if any) and create the new commit

    git commit -m "upd something" .
    

    Optionally, repeat the items 2-4 to add more intermediate commits.

  5. Continue rebasing

    git rebase --continue
    

Solution 4

There's a faster version if you only want to extract content from just one file. It's faster because the interactive rebase is not actually interactive anymore (and it's of course even faster if you want to extract from the last commit, then no need to rebase at all)

  1. Use your editor and delete the lines you want to extract from the_file. Close the_file. That's the only edition you need, all the rest is just git commands.
  2. Stage that deletion in the index:

    git  add  the_file
    
  3. Restore the lines you just deleted back into the file without affecting the index!

    git show HEAD:./the_file > the_file
    
  4. "SHA1" is the commit you want to extract the lines from:

    git commit -m 'fixup! SHA1' 
    
  5. Create the second, brand new commit with the content to extract restored by step 3:

    git commit -m 'second and new commit' the_file 
    
  6. Don't edit, don't stop/continue - just accept everything:

    git rebase --autosquash -i SHA1~1
    

Of course even faster when the commit to extract from is the last commit:

4. git commit -C HEAD --amend
5. git commit -m 'second and new commit' thefile
6. no rebase, nothing

If you use magit then step 4, 5 and 6 are a single action: Commit, instant Fixup

Share:
49,162

Related videos on Youtube

Ben
Author by

Ben

Updated on July 05, 2022

Comments

  • Ben
    Ben almost 2 years

    I flubbed up my history and want to do some changes to it. Problem is, I have a commit with two unrelated changes, and this commit is surrounded by some other changes in my local (non-pushed) history.

    I want to split up this commit before I push it out, but most of the guides I'm seeing have to do with splitting up your most recent commit, or uncommitted local changes. Is it feasible to do this to a commit that is buried in history a bit, without having to "re-do" my commits since then?

  • Dean Burge
    Dean Burge over 13 years
    ... but don't any of it if you already pushed the history since the commit to split.
  • Cascabel
    Cascabel over 13 years
    @wilhelmtell: I omitted my usual "potentially dangerous; see 'recovering from upstream rebase'" boilerplate because the OP explicitly said he hadn't pushed this history.
  • Ben
    Ben over 13 years
    and you made a perfect read. I was trying to avoid the 'boilerplate' when I specified it wasn't shared history yet :) In any regard, I have had success with your suggestion. It is a big pain to do this stuff after the fact though. I've learned a lesson here, and that is to make sure the commits are put in correctly to begin with!
  • Qiang Xu
    Qiang Xu about 12 years
    The first step may be better stated as git rebase -i <sha1_of_the_commit_to_split>^ branch. And git gui is a nice tool for the splitting task, which can be used to add different parts of a file into different commits.
  • Cascabel
    Cascabel about 12 years
    @QiangXu: The first is a reasonable suggestion. The second is exactly why I suggested git add -p, which can do more than git gui can in this department (notably editing hunks, staging everything starting from the current hunk, and searching for hunks by regex).
  • HRJ
    HRJ almost 9 years
    I wish rebase -i had a command to automate the reset HEAD^ step. Perhaps a command called review or re-edit.