Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Git stash pop only if successfully stashed before

Tags:

git

bash

Part of my workflow involves doing a lot of this:

  • git stash changes
  • git pull
  • pop stashed changes
  • launch mergetool to resolve conflicts

I am trying to write a script to do all of these things at once, so I can just call it from the terminal.

#!/bin/bash

# First stash our local changes
git stash

# Then git pull to update our repo
git pull

# Pop the stash
git stash pop

# Launch mergetool if necessary
git mergetool

The problem I'm running into is that if I run this accidentally, and there are no changes to stash, the git stash pop applies some (usually super old) stash. What I want to do is run git stash pop only if I actually stashed something before. Is there a way to do this?

like image 474
bobroxsox Avatar asked Dec 06 '15 06:12

bobroxsox


People also ask

Does git stash pop overwrite?

When the pop command runs, it's expected that files from the stash will overwrite the contents of the files in the local working tree, and the updated files will be staged in the git index.

How do I pop changes after git stash?

To retrieve changes out of the stash and apply them to the current branch you're on, you have two options: git stash apply STASH-NAME applies the changes and leaves a copy in the stash. git stash pop STASH-NAME applies the changes and removes the files from the stash.

Can I git stash pop twice?

If you made two stashes, then just call git stash pop twice. As opposed to git stash apply , pop applies and removes the latest stash. You can also reference a specific stash, e.g.

Does git stash only stash staged changes?

By default, git stash will stash only modified and staged tracked files. If you specify --include-untracked or -u , Git will include untracked files in the stash being created.


2 Answers

As Xavier Álvarez noted and codeWizard wrote, it's probably wiser to avoid git stash entirely here. For instance I'd look at using separate git fetch and git rebase steps (see Xavier's answer), and note that rebase now has --autostash which essentially does just what you want, it's just not directly available via the git pull convenience script.1

That said, there is a way to do what you've asked. It's a little bit tricky. It would be a lot easier if git stash save had a "force" option similar to git commit --allow-empty, but it doesn't have such an option.2 Instead, what you can do is detect whether git stash save pushed a new stash. This too would be a lot easier if git stash save had an exit status indicating whether it pushed a stash, but again it doesn't. That means we must rely on a different trick entirely. We start with two facts: git rev-parse finds SHA-1s from "references", and git stash uses one particular reference.

The git rev-parse command will translate any reference into an SHA-1:

$ git rev-parse refs/remotes/origin/master
2635c2b8bfc9aec07b7f023d8e3b3d02df715344

A reference is just a name, usually starting with refs, that names some SHA-1 ID. The most common ones are branches: refs/heads/branch. You may have also used tags: refs/tags/tag, and you have probably used remote-tracking branches like origin/master, which is short for the full name, refs/remotes/origin/master.

The stash script uses refs/stash, so we can simply run git rev-parse refs/stash.3 We want to run it before git stash save, then again after git stash save. If the output changes, the git stash save step must have pushed a new stash onto the stash stack.

We do have to be a bit careful since if the stash stack is empty (because the last stash was popped or dropped earlier, or no stashes have ever been created yet), git rev-parse will give an error message and produce no SHA-1:

$ git rev-parse refs/stash
fatal: ambiguous argument 'refs/stash': unknown revision or path not in
the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

Hence we actually need git rev-parse -q --verify refs/stash, which silently produces nothing if the reference does not exist, and then we just need a little care in any shell script that uses the result:

oldsha=$(git rev-parse -q --verify refs/stash)
git stash -q save  # add options as desired here
newsha=$(git rev-parse -q --verify refs/stash)
if [ "$oldsha" = "$newsha" ]; then
    made_stash_entry=false
else
    made_stash_entry=true
fi
... all of your other code goes here ...
if $made_stash_entry; then git stash pop; fi

1The git pull command is basically a short-hand for git fetch followed by git merge, or, if you tell it, to run git fetch followed by the usually-more-appropriate git rebase. If you break it up into its two separate steps, though, you get a lot more control, along with the ability to inspect the incoming changes before merging or rebasing.

2You can effectively force stash creation using the relatively new create and store subcommands: create a stash, then store the resulting SHA-1, and you've forced a stash-save even if there is nothing to stash. But not everyone is up to date with a recent git, so for scripts, it's probably wiser to rely on the old way (or as noted earlier, not use stash at all, especially since it has various minor but annoying bugs, in various versions of git).

3It's wise to spell out the full name, because git rev-parse stash will first look for a branch named stash. This is true in general with all references when writing aliases or scripts: spell out full names (and use -- syntax as necessary) to make sure git doesn't do what it thinks you meant, in odd corner cases.

like image 98
torek Avatar answered Oct 13 '22 08:10

torek


I was looking for something similar for automating merging master. I ended up just creating an empty file with a unique name. The next step was to include untracked files when stashing (stash -u). Now I know I can always pop, since I am creating something to stash. To finish up I delete the new file I created once everything else is done.

I then created the following aliases:

 up  - pull with rebase and sub-modules*
 mm  - merge master
 tm  - create file with novel name
 rtm - remove said file

...and the actual aliases:

[alias]
up           = !git pull --rebase --prune --recurse-submodules $@ && git submodule update --init --recursive && git submodule foreach git up && echo 'git on up'
mm           = "!f() { git tm; git stash -u; git co ${1-master}; git up; git co -; git merge ${1-master}; git stash pop; git rtm; }; f"
tm           = "!f() { touch __nothing_to_see_here__; }; f"
rtm          = "!f() { rm __nothing_to_see_here__; }; f"

*up stolen from haacked

like image 30
Justin Avatar answered Oct 13 '22 06:10

Justin