Search code examples
gitgit-stashgit-resetgit-cherry-pickgit-rm

GIT remove changes of specific commit from current HEAD


Assume

I have recent changes added or not added to the index. Now I'm cherry-picking a specific commit without creating a new commit on my HEAD ...

git cherry-pick -n <commit>

How do I remove the cherry-pick changes from the index? I could do a

git reset HEAD

but I'd have to redo all changes I had added before.


The purpose

If one does a stash the stash cannot be pushed to the remote. The current WIP cannot be pulled from the remote on a another system to work with. So I wrote shell functions to simulate the git-stash except I'm using branches for each stash.

An apply or pop normally would apply the stashed changes to the WIP but not the current index. While I'm using a cherry-pick to apply the changes from the stash branch all these changes would be added to the index and I need to remove them from the index afterwards.


Edit (2018-01-29)

I read the answere of @torek and understand it. Nevertheless I like to share my bash functions I already used before.

function git-stash {
    local gitbranch="$( git branch | grep \* )"
    local currentbranch="$( [ "${gitbranch}" == "* (HEAD"* ] && echo "${gitbranch}" | cut -d ' ' -f5 | cut -d ')' -f1 || echo "${gitbranch}" | cut -d ' ' -f2- )"
    local stashname="stash/$( date +%s )"
    git stash save -u ${stashname}
    git checkout -b ${stashname}
    git stash pop
    git add .
    [ ${1} ] && git commit -m "WIP: "$1 || git commit -m "WIP"
    git checkout ${currentbranch}
}

function git-stash-apply {
    local stashbranches="$( git branch | grep stash/ | cut -d ' ' -f3- | sort -r )"
    local stashbranches=(${stashbranches[@]})
    local lateststashbranch="${stashbranches[0]}"
    git cherry-pick -n "${lateststashbranch}"
}

function git-stash-pop {
    local stashbranches="$( git branch | grep stash/ | cut -d ' ' -f3- | sort -r )"
    local stashbranches=(${stashbranches[@]})
    local lateststashbranch="${stashbranches[0]}"
    git cherry-pick -n "${lateststashbranch}"
    git branch -D "${lateststashbranch}"
    git push origin :"${lateststashbranch}"
}

This isn't a proper solution yet, not to mention the missing error handling in the stash pop.


Solution

  • The immediate problem

    In this particular case you can try git revert -n <commit>, which is basically the same as applying those changes in reverse. Generally, though, this is not an invertible operation and it's unwise to have done the git cherry-pick -n in the first place.

    Consider, for instance, what happens if the delta computed by:

    git diff $commit^ $commit
    

    says that git cherry-pick $commit should add a line to README, remove a line from f1.txt, and change a line in f2.txt ("change" being implied by remove-old-add-new, really).

    But if you've already added that line to README and made that change to f2.txt, actually running the cherry-pick will only modify f1.txt. (This happens because Git uses its merge machinery, which will discover that your changes and their changes overlap, and hence reduce away the overlaps.) If you now decide to un-do the cherry-pick and run git revert -n $commit, Git will undo it by removing the line from README, adding the line back to f1.txt, and restoring the original line in f2.txt. Git won't know that the merge operation dropped two of those three changes as "already in place", and will undo all three.

    The more general problem

    If one does a stash the stash cannot be pushed to the remote.

    This is not quite true (but not quite false either). What git stash does is to make two, or sometime three, commits, none of which are on a branch: one stores the current index, one stores the current work-tree (but only for files that are in the current index). If the third commit exists, it stores untracked files minus ignored files, or untracked files including ignored files.

    The commits are arranged so that the work-tree commit is the last one made, and has the other two (plus the current commit) as its parent. Then the final commit's hash ID is "pushed onto" the stash stack using git update-ref.

    Because these are commits, they can be git push-ed. You must, however, invent a name for them on the remote, that the remote will allow you to set. The refs/stash name is generally not writeable. So for instance:

    git push fred stash:refs/heads/sneaky
    

    will create the branch name sneaky on remote fred using refs/stash.

    It's possible to send the commits under another name as above, then—by logging in to the other system—smuggle them into the refs/stash name if you want to do that. You don't even have to do that, though, as git stash apply and the like will take any identifier that resolves to a "stash-like" commit (specifically the work-tree commit, which has either two parents if the stash is a two-commit entity or three parents if the stash is a three-commit entity):

    fred$ git stash apply sneaky
    

    If all goes well and you don't want this anymore, you can then just forcibly delete the branch:

    fred$ git branch -D sneaky
    

    One other technical note

    An apply or pop normally would apply the stashed changes to the WIP but not the current index.

    That's also true, unless you use the --index option. In this case the stash code tries to restore the index using git show <index-commit-hash> | git apply --index.

    Applying such a stash invokes the internal Git merge machinery, though in general you can get a very similar effect by using git apply -3. (Note that there are subtle differences between this and an actual cherry-pick or merge or stash invocation.)