Search code examples
gitgit-stash

git discard specifically changes in the staging area


There are cases where I want to discard all the files currently in the staging area and only the files currently in the staging area. Essentially I want something that is just like making a commit and then immediately discarding the commit that was just made.

Right now in practice I mostly use git stash for this purpose and just remember that I don't actually want the thing that I'm stashing.

However, stashing and discarding are not really the same thing and stash appears to capture all the tracked files, not just the ones currently in the staging area, as the script below shows.

Is there a way to completely discard (changes to) exactly the files that are currently in the staging area?

#!/bin/bash

mkdir -p ./a

echo foo > ./a/foo
echo bar > ./a/bar
(
    # add initial commit
    cd ./a
    git init
    git add foo
    git add bar
    git commit --message='initial commit'

    # change first line of foo and bar
    ed ./foo <<'EOF'
s/$/ aaaaaa/
wq
EOF
    ed ./bar <<'EOF'
s/$/ aaaaaaa/
wq
EOF

    git add ./foo
    git stash save
    git status
)

When run, this script produces the following output

$ bash gitrepo.sh
Initialized empty Git repository in ~/git/a/a/.git/
[master (root-commit) a8f0104] initial commit
 2 files changed, 2 insertions(+)
 create mode 100644 bar
 create mode 100644 foo
4
11
4
12
Saved working directory and index state WIP on master: a8f0104 initial commit
HEAD is now at a8f0104 initial commit

I've also tried essentially the same script with git clean -f and git reset in place of git stash save

git clean -f appears to discard neither the changes to foo nor to bar.

[master (root-commit) 0c10b47] initial commit
 2 files changed, 2 insertions(+)
 create mode 100644 bar
 create mode 100644 foo
4
11
4
12
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   foo

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   bar

git reset in this specific case appears to simply remove foo from the staging area.

[master (root-commit) 65fce7b] initial commit
 2 files changed, 2 insertions(+)
 create mode 100644 bar
 create mode 100644 foo
4
11
4
12
Unstaged changes after reset:
M       bar
M       foo

The current best solution I have so far is the following script, which has the right behavior in the few cases I've tested it with. However, it accomplishes this behavior by creating, committing to, and then deleting a new branch named by a UUID. I'd prefer a way of discarding the changes in the staging area that's less circuitous.

#!/bin/bash

branch="$(python -c 'import uuid; print(str(uuid.uuid4()))')"
git checkout -b "$branch"
git commit -m "commit for $branch"
git checkout -
git branch -D "$branch"

When using gitdiscard (the script above), the tester script produces the right results, foo's change is discarded, but the unstaged modifications to bar are still there.

[master (root-commit) e4e81d9] initial commit
 2 files changed, 2 insertions(+)
 create mode 100644 bar
 create mode 100644 foo
4
11
4
12
M       bar
M       foo
Switched to a new branch 'abebe893-e579-4ecc-9422-6c99051d67f6'
[abebe893-e579-4ecc-9422-6c99051d67f6 05916d5] commit for abebe893-e579-4ecc-9422-6c99051d67f6
 1 file changed, 1 insertion(+), 1 deletion(-)
M       bar
Switched to branch 'master'
Deleted branch abebe893-e579-4ecc-9422-6c99051d67f6 (was 05916d5).

Non-exhaustive list of similar but non-duplicate questions:

This question is not a duplicate because it refers to discarding untracked files, not changes in the staging area.

This question is not a duplicate because it refers to clearing the entire working directly, not to discarding changes in the staging area.

This question covers untracked files and untracked directories, but doesn't cover discarding changes to files in the staging area.


Solution

  • You might consider the --keep-index flag for git stash

    It does, in fact, the contrary of what you're asking for : it stashes everything but what's in the index.

    You could take advantage of this and stash only the unstaged changes, then reset your index and working tree, and finally re-apply the stash.

    (I'm unsure of how you want the untracked files to be dealt with, but there's also the stash flag --include-untracked to keep in mind as a possibility. Use it on the first stash if you need to include them in the final result)

    git stash --keep-index [--include-untracked]
    git reset --hard
    git stash apply