Reputation: 13921
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
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
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
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