75

I have been wanting to use a git command that saves a stash without modifying my working tree, as a lightweight backup that's safe from any git resets or whatever I might do to screw up my index. Basically the functional equivalent of "git stash save && git stash apply" except that the working copy is never touched, since this can make certain text editors/IDE's cranky.

Something like this is approaching what I want, but not quite:

git update-ref refs/stash `git stash create "Stash message"`

This works functionally, but the issue I'm having is that no stash message shows up in "git stash list" even though the actual stash commit does have my message in it. Considering how large a stash can get, stash messages are pretty important.

Eliot
  • 5,450
  • 3
  • 32
  • 30

5 Answers5

47

git stash store "$(git stash create)"

Will create stash entry similar to what you would get with git stash without actually touching and clearing your working directory and index.

If you check stash list or look at all commit graph (including stash) you'll see that it's similar result to what you would get with normal call to git stash. Just the message in stash list is different (normally it's something like "stash@{0}: WIP on master: 14e009e init commit", here we'll get "stash@{0}: Created via "git stash store"")

$ git status --short
M file.txt
A  file2.txt

$ git stash list

$ git stash store "$(git stash create)"

$ git stash list
stash@{0}: Created via "git stash store".

$ git stash show 'stash@{0}'
 file.txt  | 2 +-
 file2.txt | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

$ git log --oneline --graph --all
*   85f937b (refs/stash) WIP on master: 14e009e init commit
|\
| * 26295a3 index on master: 14e009e init commit
|/
* 14e009e (HEAD -> master) init commit

$ git status
M file.txt
A  file2.txt

A bit more explanation:

A git stash entry is represented using normal commits with some defined structure. Basically it is a regular commit object that has 2 parents (or 3 if you use --include-untracked option) (more info 1,2).

git stash create creates this commits that represents stash entry and returns you the object name (SHA-1) of commit object (the one that has 2 or 3 parents). It is a dangling commit (you can verify it by calling git fsck after git stash create). You need to make refs/stash point to this dangling commit and you do it by git stash store (or by git update-ref like in other answers, because git stash store uses git update-ref to do its work).

It's good to look at actual source code of git stash push and see that it's basically calling git stash create and git stash store and then does some logic to clean files (which one depends on what options you used in git stash push).

Mariusz Pawelski
  • 25,983
  • 11
  • 67
  • 80
  • 1
    Great answer and explanation. Definitely a new part of my workflow. It works great when I want to do a `sed -i 's/oldname/newname/'`. No need to worry about backup suffices or anything like that. – Nathan Chappell Jul 03 '20 at 08:07
  • Oh man! This works great. Exactly what I needed. I tweak it bit with a comment i.e. `git stash store "$(git stash create 'comment')"` – Matthew MacFarland Jan 28 '22 at 15:28
  • The message you provide via "git stash create "message"` is going to be in the stash commit. The message you provide with `git stash store -m "message"` will be the message shown in the stash reflog. If you'd like both, provide the message with: `git stash store $(git stash create "message") -m "message"` because otherwise after `git stash store` the message in the stash reflog ends up being `Created via "git stash store".`. – webninja Aug 12 '22 at 11:48
  • 1
    There is no way to pass `--include-untracked` to `git stash create`, so a way to stash *with* untracked files included (although it's more invasive) can be done by stashing (with `--include-untracked` and `--keep-index`) and then applying the newly created stash back onto the working tree. [Here](https://github.com/specious/dotfiles/blob/106c335/bashrc#L225-L262) is shell code I use for fully fledged functions to do either kind of stashing without disrupting the current working tree. – webninja Aug 12 '22 at 15:15
  • I added this to my list in .bash_aliases. The last option lets me pass any create arguments including comments. Note that those are backticks around git stash create. alias gs='git stash store \`git stash create $@\`' – bfuzze Aug 20 '22 at 23:33
  • How do you get back to your stashed WIP after you run `git stash store "$(git stash create)"` in case you continued working on your WIP but realized that you messed up and want to get back to the stashed WIP? I tried `git stash pop` but get `error: Your local changes to the following files would be overwritten by merge: Please commit your changes or stash them before you merge.` error. – LLaP Mar 07 '23 at 11:41
35

Thanks to Charles' tip, I whipped up a bash script to do exactly what I wanted (I was running into issues implementing this as only an alias). It takes an optional stash message just like git stash save. If none is supplied it will use the default message generated by git stash.

#!/bin/sh
#
# git-stash-snap
# Save snapshot of working tree into the stash without modifying working tree.
# First argument (optional) is the stash message.
if [ -n "$1" ]; then
        git update-ref -m "$1" refs/stash "$(git stash create \"$1\")"
else
        HASH=`git stash create`
        MESSAGE=`git log --no-walk --pretty="tformat:%-s" "$HASH"`
        git update-ref -m "$MESSAGE" refs/stash "$HASH"
fi

Edit: As pointed out in a comment below, saving this script as git-stash-snap somewhere in your path is sufficient to be able to invoke it by typing git stash-snap.

The nice thing here is that even if you drop a stash made with this method, you will still be able to see the stash message using git log [commit-hash] of the dangling commit!

Edit: since git 2.6.0 you can add --create-reflog to update-ref and then git stash list will show this even if git stash was not used before.

Edit: Git has introduced a new stash subcommand called stash push so I have updated my recommendation for naming this script from git-stash-push to git-stash-snap.

Eliot
  • 5,450
  • 3
  • 32
  • 30
  • 5
    If you call that file `git-stash-push` and put it somewhere in your PATH, there's no need to create an alias for it. `git stash-push` will find (and call) `git-stash-push` – Stefan Näwe Jun 12 '11 at 08:42
  • Really? Interesting, I'll try it. – Eliot Jun 13 '11 at 18:54
  • One caveat with the above script, it doesn't seem to work properly when the stash is empty. – Eliot Jun 24 '11 at 09:00
  • @Eliot, this is amazing stuff, thanks for putting it together! – Dan Rosenstark Jul 05 '11 at 21:27
  • 6
    Based on this answer, I've added the following to the `[alias]` section of my `~/.gitconfig`, which should provide the same behaviour: `stash-push = "!f() { if (( $# > 0)); then branch=$(git branch | sed -n 's/^\\* //p'); git update-ref -m \"On ${branch}: $*\" refs/stash $(git stash create \"On ${branch}: $*\"); else hash=$(git stash create); msg=$(git log --no-walk --pretty=\"tformat:%-s\" $hash); git update-ref -m \"$msg\" refs/stash $hash; fi;}; f"` – me_and Jul 02 '12 at 17:07
  • `stash-push` is a bad name for this script for two reasons. (1) It is not a mirror of `stash-pop`. If you use this to "push" onto your stack, your working copy remains unchanged. Then when you try to `stash-pop` it off your stack, it will try to reapply the same changes that you already have/had in your working tree. (2) `stash-push` now exists as a git command, which has superseded the now-deprecated `stash save`. https://www.git-scm.com/docs/git-stash – cp.engr Mar 13 '18 at 14:43
  • @cp.engr Thanks for the heads up. I had a look at the docs for the new `git stash push` but there doesn't seem to be an option to achieve the behavior of my script. Do you have a suggestion for a new name? `stash-backup` maybe? Happy to update the answer... – Eliot Mar 13 '18 at 23:52
  • 1
    @Eliot, I think `stash-save` would've been a good name, if not for git already using it when they should've used `push` to match `pop`. ;) Maybe `stash-snapshot`, or `stash-snap` for short? – cp.engr Mar 14 '18 at 22:29
14

You need to pass the message to update-ref, not stash create as stash create doesn't take a message (it doesn't update any ref, so it has no reflog entry to populate).

git update-ref -m "Stash message" refs/stash "$(git stash create)"
CB Bailey
  • 755,051
  • 104
  • 632
  • 656
  • Thanks, that's what I was looking for! You're incorrect about git stash create not taking a message, however. The message supplied to it becomes the commit message. So to perfectly emulate the stash commit created by git stash save, I think the following command will do the job: `git update-ref -m "Stash message" refs/stash "$(git stash create 'Stash message')"`. Can you think of a clever way to add this directly as a git alias? If not I can make it a simple bash script. – Eliot Jun 11 '11 at 20:13
  • @Eliot: The message that shows up in `git stash list` comes from the reflog, not the commit message. That's what I was referring to. You are correct that `create` does happen take a message but this is not a documented feature so I don't know whether this is deliberately intended to work or whether it's just a artefact of the implementation. – CB Bailey Jun 11 '11 at 20:23
  • Yes, good point. I discovered it accidentally. I found some docs on the git wiki about using arguments in aliases so I am good to go! – Eliot Jun 11 '11 at 20:27
  • Thanks @Charles Bailey and +1, you've helped me out of yet another git problem. I remixed this all a bit http://stackoverflow.com/questions/6589050/git-stash-create-x-where-is-it/6589093#6589093. – Dan Rosenstark Jul 05 '11 at 21:46
11

The following combines the answer by @Mariusz Pawelski and a similar answer to a related question, letting you comfortably stash with a message.

Using git stash store with a message

  1. Use git stash create to create a stash commit, and then save it to the stash using git stash store. This will not change any files in your worktree. And you can add a helpful message to find the right version again later. In total:

    git stash store -m "saving intermediate results: my note 1" $(git stash create)
    
  2. When you decide you want to throw away your current work and restore to a previously stashed state, first do another git stash. This "throws away" any uncommitted changes, hiding them on the stash, so that there will be no merge conflicts when you apply another stashed state in the next steps.

    git stash
    
  3. Now look up what you have saved in your stash ref:

    $ git stash list
    
    stash@{0}: saving intermediate results: my note 1
    stash@{1}: saving intermediate results: my note 2
    
  4. And finally, to restore to a stashed previous work state, throwing away your current worktree state, you would do this:

    git stash apply 1
    

    Instead of 1, choose the index number of the stash commit to restore to, as seen inside stash@{…} in the previous step's list.

  5. If you discover that you want your thrown-away work back: it is also saved in the stash and can be restored with the same technique as above.

Wrapping as a Git alias

The command used in step 1 above can be used with a git alias to make it more convenient. To create the alias (note the final # technique):

git config --global alias.stash-copy \
  '!git stash store -m "saving intermediate results: $1" $(git stash create) #'

From now on, you can use the Git alias like this:

git stash-copy "my note"
tanius
  • 14,003
  • 3
  • 51
  • 63
6

Inspired by Eliot's solution, I extended his script a little bit:

#!/bin/sh
#
# git-stash-push
# Push working tree onto the stash without modifying working tree.
# First argument (optional) is the stash message.
#
# If the working dir is clean, no stash will be generated/saved.
#
# Options:
#   -c "changes" mode, do not stash if there are no changes since the
#      last stash.
if [ "$1" == "-c" ]; then
        CHECK_CHANGES=1
        shift
fi


if [ -n "$1" ]; then
        MESSAGE=$1
        HASH=$( git stash create "$MESSAGE" )
else
        MESSAGE=`git log --no-walk --pretty="tformat:%-s" "HEAD"`
        MESSAGE="Based on: $MESSAGE"
        HASH=$( git stash create )
fi

if [ "$CHECK_CHANGES" ]; then
        # "check for changes" mode: only stash if there are changes
        # since the last stash

        # check if nothing has changed since last stash
        CHANGES=$( git diff stash@{0} )
        if [ -z "$CHANGES" ] ; then
                echo "Nothing changed since last stash."
                exit 0
        fi
fi

if [ -n "$HASH" ]; then
        git update-ref -m "$MESSAGE" refs/stash "$HASH"
        echo "Working directory stashed."
else
        echo "Working tree clean, nothing to do."
fi

I implemented the following changes to Eliot's script:

  1. When working dir is clean, the script will exit gracefully
  2. When switch -c is used, if there no changes compared to the last stash, the script will exit. This is useful if you use this script as a "time machine", making an automated stash every 10 minutes. If nothing has changed, no new stash is created. Without this switch, you might end up with n consecutive stashes which are the same.

Not that in order for the switch -c to work properly, at least one stash must exist, otherwise the script throws an error on git diff stash@{0} and will do nothing.

I use this script as a "time machine", snapshotting every 10 minutes using the following bash loop:

 while true ; do date ; git stash-push ; sleep 600 ; done
andimeier
  • 1,213
  • 2
  • 12
  • 19