19

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?

7
  • Why do you need the stash step for? Commented Dec 6, 2015 at 6:26
  • If I have local changes that conflict with the pull, it says "error: Your local changes to the following files would be overwritten by merge: file.ext Please, commit your changes or stash them before you can merge. Aborting." So the stash, pull, pop way allows me to pull the changes and then merge them in via the mergetool. Commented Dec 6, 2015 at 6:29
  • so why not simply commiting? Commented Dec 6, 2015 at 6:32
  • If I commit and then pull, the merge adds another commit to git, (iirc even if it's just a fast-forward) which makes the history messier than the stash, pull, pop way, which just leaves the commits from the pull and then my own commit(s) after the merge. Commented Dec 6, 2015 at 6:34
  • Also open to better workflow suggestions :) I haven't used git for that long, still getting the hang of it. Commented Dec 6, 2015 at 6:34

6 Answers 6

17

Edit, July 2022: Time (and Git) have moved on and depending on your Git version much of the below is not necessarily accurate any more. One of the most important changes is that there are now git stash push and git stash create commands. See the footnotes and comments.


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.

Edit, July 2022: git pull is no longer a script and autostash works with it now. There were intermediate transition states along the way.

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).

Edit, July 2022: git stash is no longer a script and has new options and verbs. See comments.

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.

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you. In my scripting use-case, this is perfect. Thanks!
git stash create does not currently support the -u option for untracked files. So your answer using git stash save is more flexible, in case untracked files are present.
@steampowered: interesting that -u isn't supported there. The stash code has gotten some attention lately in Git, but last time I tested it (a while ago) it was still broken for "make change in index, set work-tree to match HEAD". (And thanks for the fix for the swapped true/false state in the example!)
On recent git I get: fatal: subcommand wasn't specified; 'push' can't be assumed due to unexpected token 'save'
6

When using git stash save <messsage>, the message you pass will show upon a successful save.

So one trick is to generate a timestamp, which will be used as the message, and drop the most recent stash if the timestamp was found in the resulting message.

One line:

t=timestamp-$(date +%s); r=$(git stash save $t); v=$(echo $r|grep $t); if [ "$v" ]; then git stash list; echo "SAVED! NOW REMOVING..."; git stash drop stash@{0}; else echo "Nothing to Stash!"; fi; echo "Stashes: "; git stash list; echo "Done!" 

Expanded:

# unique timestamp t=timestamp-$(date +%s) # stash with message r=$(git stash save $t) # check if the value exists v=$(echo $r|grep $t) # if the message is found... if [ "$v" ] then # DEBUG: Before git stash list echo "SAVED! NOW REMOVING..." # remove last stash git stash drop stash@{0} else echo "Nothing to Stash!" fi # DEBUG: after echo "Stash List: " git stash list echo "Done!" 

Comments

4

Reading your explanation of why do you do what you do I'd probably go for a completely different approach. First, I'd fetch the remote you want to use:

git fetch <remote> (e.g. git fetch origin) 

And then, I'd carry out a rebase against a specific branch of that remote:

git rebase <remote>/<branch> (e.g. git rebase origin/master) 

This would merge your changes and you'd still be able to solve any conflicts.

If you don't like this approach, you might want to use git pull with the --no-commit flag instead:

git pull --no-commit 

This way no autocommit would be performed after the merge.

1 Comment

That sounds like a much more natural workflow, and it looks like it basically does what I'm trying to do. Thanks
2

How about running a git status first? If there are local changes, run the stash command. If not, skip it. Save this result in a bool and then don't run the pop if there was no fresh stash.

3 Comments

Hmm, that could work. I wonder if there's a nicer way to check if there are local changes than trying to parse the output of that though
Write a shellscript that does this for you and execute stash pop only on the right conditions, bobroxsox
@bobroxsox needStash="$(git status -s)"; [[ -n "${needStash}" ]] && git stash; ! git pull; [[ -n "${needStash}" ]] && git stash pop
2

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

Comments

-1

I got the same problem and resolved for Windows using the following simple script:

@rem The following is unsetting one environment variable if already set by (normally by previous executions of the script) set STASH_SUCCEED= @rem If push succeeds the variable is set git stash push && set STASH_SUCCEED=1 @rem Do your stuff here... if defined STASH_SUCCEED ( git stash pop && echo "STASH POPPED" ) 

1 Comment

git stash push always exits with 0 for me on Mac regardless of its success (only with a different message each time). I wonder why this ever worked for you, surely the exit codes aren't different between win and linux?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.