Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Git - How to edit old (not previous) commit with some of the unstaged changes from current index (current state)?

Tags:

git

I've taken a look at these previous questions already:

  • Howto add a changed file to an older (not last) commit in Git
  • How do I edit a previous git commit?

They don't exactly address a particular issue though - there are other changes in the index! When running the rebase command, git complains: Cannot rebase: You have unstaged changes.

Scenario:

The commit previous to the last one (do I refer to that as "2 HEADs ago"?) was a refactor commit. I currently have in the index many unstaged changes, but only some of which I want to add to the previous to last commit.

I'm imagining the way to do this would be to:

  1. stash all of my current changes
  2. rebase -i to the previous to last commit (changing index and moving Head, right?)
  3. load the stash into my index without changing Head (how?)
  4. use add -p and commit --amend to selectively modify this old commit
  5. rebase --continue to finish (updates children, moves Head back to where I started, what happens to index?)
  6. then pop/clear the stash (index back to where I started).

Is this correct? If it is, how do I do step 3? If it isn't, what should I be doing instead?

Also, note that I'm still learning about git and am still not 100% sure I'm referencing things in git (Head, index, stash, etc) properly.


Solution:

For anyone else this may help, these are the steps I actually took:

  1. git stash all of my current changes
  2. git rebase -i <ID> to the parent of the previous to last commit, changing index and moving Head
  3. git stash apply load the stash into my index without changing Head
    • if you have conflicts, git reset HEAD <file> to unload files staging. Make sure staging is clear.
  4. use add -p and commit --amend to selectively stage changes and commit them
  5. git reset --hard to discard index so it matches Head
  6. git rebase --continue to finish. updates children, moves Head back to very start, but with changes
    • history is now forked into two versions. The other branch ends at the WIP previously stashed
  7. then pop the stash to bring index back to where I started. The other branch is also cleared.
like image 450
Kache Avatar asked Dec 14 '12 00:12

Kache


1 Answers

With git1.8.4 (July 2013), you can choose to:

  • rebase
  • while keeping your local unstaged changes!

"git rebase" learned "--[no-]autostash" option to save local changes instead of refusing to run (to which people's normal response was to stash them and re-run).

So in your case, this could work (and save your work in progress in the meantime):

git rebase --autostash -i <ID>

See commit 587947750bd73544a6a99811f0ddfd64e1ff1445:

This new feature allows a rebase to be executed on a dirty worktree or index.
It works by creating a temporary "dangling merge commit" out of the worktree and index changes (via 'git stash create'), and automatically applying it after a successful rebase or abort.

rebase stores the SHA-1 hex of the temporary merge commit, along with the rest of the rebase state, in either .git/{rebase-merge,rebase-apply}/autostash depending on the kind of rebase.
Since $state_dir is automatically removed at the end of a successful rebase or abort, so is the autostash.

The advantage of this approach is that we do not affect the normal stash's reflogs, making the autostash invisible to the end-user.
This means that you can use 'git stash' during a rebase as usual.

When the autostash application results in a conflict, we push $state_dir/autostash onto the normal stash and remove $state_dir ending the rebase.
The user can inspect the stash, and pop or drop at any time.


Note: git 2.0.1 (Jult 25th, 2014) handle the autostash case.
See commit e4244eb from Ramkumar Ramachandra (artagnon) (also in his blog):

rebase -i: handle "Nothing to do" case with autostash

When a user invokes

$ git rebase -i @~3

with dirty files and rebase.autostash turned on, and exits the $EDITOR with an empty buffer, the autostash fails to apply.

Although the primary focus of rr/rebase-autostash was to get the git-rebase--backend.sh scripts to return control to git-rebase.sh, it missed this case in git-rebase--interactive.sh.
Since this case is unlike the other cases which return control for housekeeping, assign it a special return status and handle that return value explicitly in git-rebase.sh.


With the latest git, simply do git config rebase.autostash true to make rebase [-i] work in a dirty worktree.

This new feature allows a rebase to be executed on a dirty worktree or index.
It works by creating a temporary "dangling merge commit" out of the worktree and index changes (via 'git stash create'), and automatically applying it after a successful rebase or abort.

rebase stores the SHA-1 hex of the temporary merge commit, along with the rest of the rebase state, in either .git/{rebase-merge,rebase-apply}/autostash depending on the kind of rebase. Since $state_dir is automatically removed at the end of a successful rebase or abort, so is the autostash.

The advantage of this approach is that we do not affect the normal stash's reflogs, making the autostash invisible to the end-user. This means that you can use 'git stash' during a rebase as usual.

like image 97
VonC Avatar answered Sep 25 '22 15:09

VonC