Greg Nisbet
Greg Nisbet

Reputation: 6994

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.

Upvotes: 2

Views: 541

Answers (2)

Romain Valeri
Romain Valeri

Reputation: 21998

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

Upvotes: 3

torek
torek

Reputation: 488453

First, remember that when git status says:

nothing to commit, working tree clean

this does not mean that the staging area is empty. In fact, the staging area is perfectly full: every file in the staging area matches every file in the current (or HEAD) commit. Meanwhile every file in the staging area also matches every tracked file in the work-tree.

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

Consider git reset, git checkout, and—in Git 2.23 (which I have not yet used) git restore:

  • The git reset command has a mode in which it copies one file from the current commit, i.e., HEAD, to the index / staging-area, without touching the work-tree at all:

    git reset -- file

    The -- is only needed if the file name file resembles a branch name or a git reset option, e.g., if you have a file named master and you want to make the staged version match the committed version, git reset master is wrong but git reset -- master is right.

  • git reset HEAD does exactly what you've asked for here: it copies every file from the HEAD commit into the staging area, so that no changes are staged.

  • If you would like the work-tree copy of the file updated as well, one of the many modes of git checkout does precisely that:

    git checkout HEAD -- file

    As before, you need the -- only if the file name resembles a git checkout option. For instance, if you want to discard staged and/or unstaged changes to a file named --patch, git checkout HEAD --patch is wrong but git checkout HEAD -- --patch is right. (Since you have to specify HEAD here, git checkout HEAD master works to throw out staged-and-unstaged changes to a file named master. However, it's safer to get in the habit of just using the -- every time.)

  • As before, you can check out every file from the current commit, but this time it requires using the fact that checkout out a folder (directory) name causes Git to check out the contents of that folder recursively, so:

    git checkout HEAD -- .

    does the trick, though you need . to refer to the correct level of your work-tree.

In Git 2.23, the new git restore command gives you the ability to overwrite index, work-tree, or both copies as you like.

Remember, Git keeps three active copies of each file at all times. The committed HEAD copy cannot be changed—it's in a commit and commits cannot be changed—but it can be accessed with git reset and git checkout HEAD. The index copy—also called the staging copy—can be overwritten at any time by:

  • copying from HEAD: now no changes are staged, or
  • copying from the work-tree: now whatever is in the work-tree is staged.

The work-tree copy is a plain ordinary file, so you can do whatever you want to it; but various forms of git checkout will overwrite it at your request.

Upvotes: 1

Related Questions