Search code examples
gitgit-stash

How to ignore added hunks in `git stash -p`


Imagine this scenario:

# edit two files
git add -p // add hunks from one file

Now when you run git stash -p, it will again ask you whether you want to stash the hunks that you just selected via git add -p. Is there some way to configure git to ignore these already-added hunks by default? Most of the time, I don't want to stash stuff that I added already.


Solution

  • Okay, from comments, what's wanted is a stash of all changes that haven't already been added (whether excluded during a git add -p, or just not added yet).

    The reason for this is to apply some tests/potential tweaks to the staged state, before committing it.

    That's a straight git stash -k, stash everything as usual but keep the indexed state and the corresponding worktree around, i.e. clean from the worktree everything I'm not about to commit.

    So:

    git stash -k
    git clang-format
    git commit
    

    and the repository now has four interesting snapshots: the original content aka stash base, the snapshotted index, the snapshotted worktree, and the current index(, commit, and worktree) which is the index snapshot at stash^2 with the cleanups applied. Note that all three of the new snapshots aka commits here have the stash base as parent.

    Now you want your worktree changes back, but clearly the changes from the base to the stashed index and worktree do not match the ones in the current index and worktree (and new commit, those all match), so when git goes to pop the stash it will find conflicts: changes from the stashed base to the stashed index do not match the changes from the stash base to the current index.

    If Git offered what you want directly, a "stash all the worktree changes except the ones in the index", you could have used that and then the stash pop wouldn't have any trouble, a straight git stash pop would do it. Fortunately, if Git's good at anything, it's merging, combining, splitting, and general all-around munging of differences.

    git cherry-pick -nm2 stash
    # cherry-pick updated the index, too. maybe git reset here to unstage that stuff.
    git stash drop
    

    Stash pop is a merge of the changes from the stash base to the stashed state with the changes from the stash base (which is usually remarkably similar to the current base) to the current state. You want the stashed worktree changes back in your worktree, but only the ones you hadn't already added, since the ones you had added are all still here, they're just a little different now.

    So the cherry-pick is -n, no commit, -m2, mainline for changes is its second parent, i.e. all the differences you had made but not added when stashing.

    An example might help,

     cd `mktemp -d`
     git init
     printf >file %s\\n 1 2 3 4 5
     git add .;git commit -m1
     printf >file %s\\n 1 2a 3 4 5
     git add .
     printf >file %s\\n 1 2a 3 4a 5
    

    now you've effectively git add -p'd the 2a change, and the 4a change is only in your worktree.

     $ git stash -k
     $ cat file
     1
     2a
     3
     4
     5
     $ sed -i '2s,^,_,' file   # indent the 2a line
     $ git commit -am2
    

    Now, the initial commit, :/1, is 1 2 3 4 5, your current commit, index and worktree are all 1 _2a 3 4 5, your stashed index is 1 2a 3 4 5 and your stashed worktree is 1 2a 3 4a 5.

    The changes you want back are the difference between your stashed index and your stashed worktree, that's the stash commit's differences from its second parent. Hence, that cherry-pick.


    Alternate ways of spelling the cherry-pick include

    git cherry-pick -nm1 -Xours stash
    

    which applies all the stashed worktree changes but takes the local version in case of conflict (basically it's finding and throwing away the conflicting differences instead of just avoiding them as the -m2 does) and

    git diff stash^2..stash|git apply -3
    

    Making all this more easy on yourself is scripting territory, the easiest way to talk about setting it up is as a git alias,

    git config --global alias.poptree '!git cherry-pick -nm2 stash; git reset; git stash pop'
    

    and you now have a git poptree command to do what you want.


    edit: as a further fillip, suppose you had gone ahead and done some more work before remembering your stashed worktree changes, the cherry-pick will correctly update the worktree and index but the reset will back out any changes you had already added to the new index. Now you're in core-command territory,

    ( index=`git write-tree` &&
      git cherry-pick -nm2 stash &&
      git read-tree $index &&
      git stash drop
    )
    

    is how I'd implement this for real.