Scott
Scott

Reputation: 13921

Move some changes from a Git commit into a separate commit

I'm on a feature branch with a single commit (as a result of an interactive rebase which squashed all of my commits).

I've decided, prior to pushing to origin and creating a pull request that I'd like to take a subset of the changes (at the file level) in that commit and make a second commit out of them since they're logically separate issues. How can I do this?

The change I'd like to break out was never in it's own commit, it just got bundled up into the first commit I did as part of the work on my feature branch.

Upvotes: 6

Views: 2086

Answers (3)

TKF
TKF

Reputation: 628

This method using git checkout in interactive rebase supports creating arbitrary new commits from changes in a given commit. Depending on the situation, one may prefer this to the git reset->remake commits method. I like that being in an interactive rebase, if I mess up, I can simply git rebase --abort and start over.

For example, given several commits

Charlie  # Latest commit
Bravo    # The commit containing changes you want to extract
Alpha    # Some earlier commit

Begin an interactive rebase including the target commit. It's the second last commit here, so I would use HEAD~2 but there are alternate syntaxes you might prefer:

git rebase --interactive HEAD~2

You'll see the rebase TODO presented in your $EDITOR, noting the order is chronological instead of reverse chronological as in typical git log output:

pick abc123 Bravo    
pick def456 Charlie  

Insert a break instruction prior to the target commit:

break
pick abc123 Bravo    
pick def456 Charlie

You are now in an interactive rebase and your working directory reflects the repository as of commit Alpha. You can now use git checkout --patch <COMMIT> <FILE> to apply select changes from your (future) Bravo commit to your working directory. Running git status will show you the upcoming rebase commands, which here is 'pick/apply commit Bravo with hash abc123' (easy way to get the hash for below).

This command begins an interactive checkout; changes from the specified commit in the listed files can be yes/no selected.

git checkout --patch abc123 MyFileA.txt MyFileB.txt

Any selected hunks should apply cleanly and will be staged immediately, and you can commit them as desired (git commit --message "My new commit").

Once you have extracted and committed your changes, you can continue (and end) the rebase by proceeding with the remaining rebase commands (pick Bravo). When this original Bravo commit is applied, the changes you have extracted will already exist in code, and they will not be recorded under the Bravo commit.

git rebase --continue

With the rebase complete, your commits will now resemble:

Charlie  # Latest commit
Bravo    # The commit containing changes you want to extract
Bravo.3  # Some change extracted from Bravo
Bravo.2  # Some change extracted from Bravo
Bravo.1  # Some change extracted from Bravo
Alpha    # Some earlier commit

If this order does not suit you, start another interactive rebase and reorder the pick commands in the rebase TODO.

git rebase --interactive HEAD~5

Rather than extracting individual hunks interactively as in the above process, you can also use the rebase exec command to carry out the above as part of the rebase process.

As above, begin an interactive rebase including the target commit:

git rebase --interactive HEAD~2

You'll see the rebase TODO presented in your $EDITOR:

pick abc123 Bravo    
pick def456 Charlie  

This time, instead of using break, insert an exec command containing a non-interactive git checkout:

exec git checkout abc123 MyFileA.txt && git commit --message "Some changes to MyFileA"
exec git checkout abc123 MyFileB.txt && git commit --message "Some changes to MyFileB"
pick abc123 Bravo    
pick def456 Charlie

This will extract all changes from each of the above files and commit them separately, which may or may not be useful in some cases.

Upvotes: 1

janos
janos

Reputation: 124648

Move some changes from a Git commit into a separate commit

In a nutshell: reset the last commit and make 2 new commits

If I understand correctly, you want to keep most of the changes in the original commit, and put a smaller subset of changes in a second commit. If this is the case, then first you want to soft reset the last commit (thanks @hvd for the tip):

git reset --soft HEAD^

This will remove the last commit, but not the changes, as if the commit never happened, and the changes are all staged. At this point, you want to unstage the files that you don't want in the first commit. You can unstage file by file or directory by directory, like this:

git reset -- path/to/file-or-dir

Now you can do a commit for a majority of changes (already staged), and then a second commit for the rest.

If you have files where you want some of the changes in the first commit and other changes in the second commit, then you can reset the entire file first, and stage the changes selectively using git add -p, like this:

git reset -- path/to/file
git add -p path/to/file

The -p option of git add will let you select the hunks to stage. Note that you can split hunks and have a fine control of the lines to stage.

After the first commit, you can proceed with adding the still pending (not staged) changes.

Upvotes: 9

larsks
larsks

Reputation: 311248

First, make sure that your working directory is free from any pending changes. Use git stash if you have changes you want to preserve.

Use git reset to move the repository "backwards" to the previous commit:

$ git reset HEAD^

This won't make any changes to your work files, but it roles back the repository state so that anything that was part of that commit will now show up as modified.

Now, add those changes that you want to be part of a separate commit, using git add. Once you have your changes staged, commit those changes with git commit.

Now, add all other remaining changes, and then commit those as well.

Now you have split your previous single commit into two separate commits.

Upvotes: 3

Related Questions