5

I want to write a script that allows me to easily change branches keeping my current changes.

git stash
go checkout main
git stash pop

The problem is if there are currently no changes in the repository. Then stash pop will pop off a previous stash. What is the easiest way to check if there are changes to pop?

Casebash
  • 114,675
  • 90
  • 247
  • 350

3 Answers3

11

A easy way to check if there are changed files in the working tree, building on Chris Hayes suggestion, provides a combination of git status --porcelain --untracked-files=no (alternativly -uno) and wc -l.

This will give you the number of changed files.

Here is a script example which should satisfy your needs:

changed_files=$(git status --porcelain --untracked-files=no | wc -l)
[ $changed_files -gt 0 ] && git stash

< do work > 

[ $changed_files -gt 0 ] && git stash pop

For more information on git status take a look at the documentation (respectively for wc).

Sascha Wolf
  • 18,810
  • 4
  • 51
  • 73
5

git stash is actually a shell (plain sh, not bash) script, so you can look at what it does to decide whether to make a stash:

no_changes () {
        git diff-index --quiet --cached HEAD --ignore-submodules -- &&
        git diff-files --quiet --ignore-submodules &&
        (test -z "$untracked" || test -z "$(untracked_files)")
}

untracked_files () {
        excl_opt=--exclude-standard
        test "$untracked" = "all" && excl_opt=
        git ls-files -o -z $excl_opt
}

You can therefore repeat these tests to see if git stash will do anything—but that's actually not the best option:

  • If git stash will do something, first it repeats these tests (and they say there is something to stash), which makes them happen twice in that case.
  • If the "right" set of tests ever change, you can mis-predict git stash's action and get into trouble.

Clearly, what you want is to:

  1. Run git stash, and keep track of whether this did anything.
  2. Run git checkout on the target branch.
  3. If git stash stashed something, run git stash pop (or perhaps --apply --keep-index first, to see if that will do the trick, and resort to applying without --keep-index only if required, etc.).

The tricky part is that first step. It turns out to be easy, you just need to know how git stash works in, um, extensive detail, plus some other bits about git's "plumbing commands". You simply grab the previous stash stack top if any before the git stash save step, and then grab the new top if any, and compare:

old_stash=$(git rev-parse -q --verify refs/stash)
git stash save [additional arguments here if desired, e.g., -q]
new_stash=$(git rev-parse -q --verify refs/stash)

If the two refs name different commits, or the old ref is empty and the new is not empty, then git stash pushed a new stash on the stack. If the two refs are both empty, or the two refs name the same commit, then git stash did nothing:

if [ "$old_stash" = "$new_stash" ]; then ...; else ...; fi

or, in your case, perhaps something more like:

[ "$old_stash" != "$new_stash" ] && git stash pop

(these tests make use of the fact that the two empty strings are equal to each other, and not-equal to any non-empty string, in terms of the result from /bin/[ and the shell's built-in copy thereof).

Community
  • 1
  • 1
torek
  • 448,244
  • 59
  • 642
  • 775
4

Using git status

git status --porcelain is one way, though possibly not the best (i.e. fastest). From the man page:

--porcelain

Give the output in an easy-to-parse format for scripts. This is similar to the short output, but will remain stable across Git versions and regardless of user configuration. See below for details.

Usage example:

$ git status --porcelain

M tmp/myfile.c

$ git stash
$ git status --porcelain
$ 

There's no output from git status --porcelain if there are no changes. One flaw, though - new files will be displayed as ?? newfile.txt, so you may have to post-process the output to get what you want, since new files will not be stashed:

$ git status --porcelain
?? newfile.txt

$ git status --porcelain | grep -v "^\?\?"
$

You could do something similar with git diff, but since calculating a diff is likely going to take much longer than enumerating the status for any realistic repository, I wouldn't recommend it.

Chris Hayes
  • 11,471
  • 4
  • 32
  • 47
  • (mathspace)chris@cl:~/Projects/mathspace$ git stash No local changes to save (mathspace)chris@cl:~/Projects/mathspace$ echo $? 0 – Casebash Jul 16 '14 at 05:19
  • @Casebash Whoops.. I apparently did something weird when testing it because I can't reproduce that behavior. Sorry about that. – Chris Hayes Jul 16 '14 at 05:20
  • 1
    @ChrisHayes To hide untracked files from `git status` output you can use `--untracked-files=no` (or `-uno`), take a look at the [documentation](https://www.kernel.org/pub/software/scm/git/docs/git-status.html). – Sascha Wolf Jul 16 '14 at 07:44