It doesn't look like there is a out-of-the-box way to resolve conflicts and keep the index at the same time.
During conflict resolution, Git uses the staging area for its own purposes, which effectively erases the data there.
However, a stash entry is just a few commits in the repository.
The command git stash
is provided for our convenience to manage those commits but we don't have to use it.
You can instead pop the stash manually in a way that preserves the index.
The key is to merge the stash into the current HEAD
in 2 steps: first only the index and later the rest.
(You can use multiple commits to keep track of which files are from where and make sure that solving conflicts won't remove any information.)
First, you need to convert the commit structure of the stash entry into something sane.
The normal stash entry comprises of 2 or 3 commits woven into a bizarre web of merges. This is not only pointlessly complicated but also hard to work with.
Instead, you could have just two linear commits: first with indexed changes and the second with the non-index ones.
First we move HEAD
to the commit that stores the indexed files from the 1st stash entry.
stash@{N}
is the top commit of the stash entry number N
and stash@{N}^2
is its second parent. (Stash entry always has at least 2 parent commits: the base commit at which the entry was created and a commit storing stashed index.)
You can use the option --detach
because these commits will be temporary and there is no use for a branch.
git switch --detach stash@{0}^2
For the second commit, you should convert the tip of the stash entry from a merge commit into a normal commit, using git merge --squash
.
The code below additionally checks if that merge has a 3rd parent that stores untracked files. If this is the case, they are also added.
git merge --squash stash@{0}
if git rev-parse stash@{0}^3 1>/dev/null 2>&1
then
git ls-tree -r --name-only stash@{0}^3 -z \
| xargs -0 -- git restore --source=stash@{0}^3 --
git add .
fi
git commit
At this point the Git repository should look like follows:
A -----> stash index -----> stash non-index (HEAD)
\
\-> B
A
is the initial commit where the changes were pushed to stash and B
is the new commit where you want to apply the changes.
(Btw, the original stash entry is not drawn here but it still exists. It isn't lost it or anything.)
The second step is to just rebase the simplified stash entry onto the branch where you want to apply it.
It's just one command:
git rebase --onto B HEAD~2 HEAD
At this stage you will have to resolve the conflicts that blocked you from applying the stash before.
After it's all finished, the repository should look like this:
A -----> B -----> stash index -----> stash non-index (HEAD)
The third step is to remove the commits, without losing any changes or the contents of the index.
It is as simple as:
git reset --mixed HEAD~
git reset --soft HEAD~
The forth and the last step is just some cleanup.
Currently you are in a detached HEAD
state and you most likely started the whole operation at a top of some branch like a sane Git user.
You need to switch back to your branch:
git switch your_branch
You can also remove the stash entry if it's no longer needed:
git stash drop
An entire script
Popping stash in this way takes a lot of command and is quite error-prone.
A much better idea is a script that can do it automatically and also provides some rudimentary idiot-proofing.
#!/usr/bin/env sh
set -e
git_dir="$(git rev-parse --git-dir)"
rebase_failed=0
if [ "$1" = '--continue' ]
then
shift
if [ $# -gt 0 ]
then
printf 'Too many arguments!\n' 1>&2
exit 1
fi
if ! [ -f "$git_dir/better-unstash" ]
then
printf 'There is no "better-unstash" operation in progress!\n' 1>&2
exit 1
fi
{
read -r current_branch
read -r detached
} <"$git_dir/better-unstash"
rm -f "$git_dir/better-unstash"
if ! git -c 'core.editor=true' rebase --continue
then
rebase_failed=1
fi
else
if [ $# -eq 0 ]
then
stash='stash@{0}'
elif [ $# -eq 1 ]
then
if [ "$1" -eq "$1" ] 2>/dev/null
then
stash="stash@{$1}"
else
stash="$1"
fi
else
printf 'Too many arguments!\n' 1>&2
exit 1
fi
if ! git diff --quiet HEAD
then
# There are still are some limitations.
printf 'There are uncommitted changes in the working directory!\n' 1>&2
printf 'Commit or stash them before attempting unstashing with index.\n' 1>&2
exit 1
fi
detached=0
current_branch="$(git rev-parse --abbrev-ref HEAD)"
if [ "$current_branch" = 'HEAD' ]
then
detached=1
current_branch="$(git rev-parse HEAD)"
fi
git switch --detach "$stash^2"
git merge --ff-only --squash "$stash"
if git rev-parse "$stash^3" 1>/dev/null 2>&1
then
git ls-tree -r --name-only "$stash^3" -z \
| xargs -0 -- git restore --source="$stash^3" --
git add .
fi
git commit --no-edit --no-verify --allow-empty
if ! git rebase --onto "$current_branch" "HEAD~2" "HEAD"
then
rebase_failed=1
fi
fi
if [ "$rebase_failed" -ne 0 ]
then
printf 'USE `%s --continue` INSTEAD OF `git rebase --continue`!\n' "$0"
printf '%s\n%s\n' "$current_branch" "$detached" >"$git_dir/better-unstash"
exit 1
fi
git reset --mixed HEAD~
git reset --soft HEAD~
if [ "$detached" -eq 0 ]
then
git switch "$current_branch"
fi
printf 'The stash is kept because this is a higher-risk non-standard script.\n'
This script works similarly to the git rebase
in the sense that it will exit on conflicts and it needs to be restarted with a flag --continue
after they are fixed.
For the initial run, you can pass an optional argument that specifies the stash entry to pop.