views:

299

answers:

4

How can I check if I have any uncommitted changes in my git repository:

  1. Changes added to the index but not committed
  2. Untracked files

from a script?

git-status seems to always return zero with git version 1.6.4.2.

A: 

Why not encapsulate 'git status with a script which:

  • will analyze the output of that command
  • will return the appropriate error code based on what you need

That way, you can use that 'enhanced' status in your script.


As 0xfe mentions in his excellent answer, git status --porcelain is instrumental in any script-based solution

--porcelain

Give the output in a stable, easy-to-parse format for scripts.
Currently this is identical to --short output, but is guaranteed not to change in the future, making it safe for scripts.

VonC
Because I'm lazy , probably. I thought there was a built-in for this, as it seems a pretty oft-encountered use case.
Robert Munteanu
I posted a solution based on your suggestion, although I'm not extremely satisfied with it.
Robert Munteanu
+6  A: 

Great timing! I wrote a blog post about exactly this a few days ago, when I figured out how to add git status information to my prompt.

Here's what I do:

1) For dirty status:

# Returns "*" if the current git branch is dirty.
function evil_git_dirty {
  [[ $(git diff --shortstat 2> /dev/null | tail -n1) != "" ]] && echo "*"
}

2) For untracked files (Notice the --porcelain flag to "git status" which gives you nice parse-able output):

# Returns the number of untracked files

function evil_git_num_untracked_files {
  expr `git status --porcelain 2>/dev/null| grep "^??" | wc -l` 
}

Although "git diff --shortstat" is more convenient, you can also use "git status --porcelain" for getting dirty files:

# Get number of files added to the index (but uncommitted)
expr $(git status --porcelain 2>/dev/null| grep "^M" | wc -l)

# Get number of files that are uncommitted and not added
expr $(git status --porcelain 2>/dev/null| grep "^ M" | wc -l)

# Get number of total uncommited files
expr $(git status --porcelain 2>/dev/null| egrep "^(M| M)" | wc -l)

Note: The 2>/dev/null filters out the error messages so you can use these commands on non-git directories. (They'll simply return 0 for the file counts.)

Here's what my prompt looks like (it includes the current branch name, an asterisk for "dirty", number of stashed / unmerged / unpushed branches):

alt text

I'm not sure if it's okay to link to personal blogs on SO, so I won't link to my post (which has all the details of the prompt). Let me know if you want it, and I'll follow up with the link.

Edit: Here are the posts:

Adding Git Status Information to your Terminal Prompt

Improved Git-enabled Shell Prompt

Or links straight to the code: prompt.sh and evilgit.sh

0xfe
Sure, link your blog as long as it is on the topic of the question. Thanks.
Robert Munteanu
Great details. +1
VonC
Best all around answer and helpful comments. Thanks!
Robert Munteanu
It's worth noting that the git bash completion comes with a shell function for doing pretty much what you're doing with your prompt - `__git_ps1`. It shows branch names, including special treatment if you're in the process of a rebase, am-apply, merge, or bisect. And you can set the environment variable `GIT_PS1_SHOWDIRTYSTATE` to get an asterisk for unstaged changes and plus for staged changes. (I think you can also get it to indicate untracked files, and give you some `git-describe` output)
Jefromi
A: 

One DIY possibility, updated to follow 0xfe's suggestion

#!/bin/sh
exit $(git status --porcelain | wc -l) 

As noted by Chris Johnsen, this only works on Git 1.7.0 or newer.

Robert Munteanu
The issue with this is that you can't reliably expect the string 'working directory clean' in future versions. The --porcelain flag was meant for parsing, so a better solution would be: exit $(git status --porcelain | wc -l)
0xfe
@0xfe - Do you know when the `--porcelain` flag was added? Does not work with 1.6.4.2 .
Robert Munteanu
@Robert: try `git status --short` then.
VonC
`git status --porcelain` and `git status --short` were both introduced in 1.7.0. `--porcelain` It was introduced specifically to allow `git status --short` to vary its format in the future. So, `git status --short` would suffer the same problem as `git status` (output may change at any time since it is not a ‘plumbing’ command).
Chris Johnsen
@Chris, thanks for the background information. I've updated the answer to reflect the best way of doing this as of Git 1.7.0 .
Robert Munteanu
@VonC : `--status` and `--short` were both introduced in 1.7.0, as underlined by Chris Johnsen. I've updated my answer to reflect this.
Robert Munteanu
+4  A: 

The key to reliably “scripting” Git is to use the ‘plumbing’ commands.

The developers take care when changing the plumbing commands to make sure they provide very stable interfaces (i.e. a given combination of repository state, stdin, command line options, arguments, etc. will produce the same output in all versions of Git where the command/option exists). New output variations in plumbing commands can be introduced via new options, but that can not introduce any problems for programs that have already been written against older versions (they would not be using the new options, since they did not exist (or at least were not used) at the time the script was written).

Unfortunately the ‘everyday’ Git commands are the ‘porcelain’ commands, so most Git users may not be familiar with with the plumbing commands. The distinction between porcelain and plumbing command is made in the main git manpage (see subsections titled High-level commands (porcelain) and Low-level commands (plumbing)).


To find out about uncomitted changes, you will likely need git diff-index (compare index (and maybe tracked bits of working tree) against some other treeish (e.g. HEAD)), maybe git diff-files (compare working tree against index), and possibly git ls-files (list files; e.g. list untracked, unignored files).

To check whether a repository has staged changes (not yet committed) use this:

git diff-index --quiet --cached HEAD
  • If it exits with 0 then there were no differences (1 means there were differences).

To check whether a working tree has changes that could be staged:

git diff-files --quiet
  • The exit code is the same as for git diff-index (0 == no differences; 1 == differences).

To check whether the combination of the index and the tracked files in the working tree have changes with respect to HEAD:

git diff-index --quiet HEAD
  • This is like a combination of the previous two. One prime difference is that it will still report “no differences” if you have a staged change that you have “undone” in the working tree (gone back to the contents that are in HEAD). In this same situation, the two separate commands would both return reports of “differences present”.

You also mentioned untracked files. You might mean “untracked and unignored”, or you might mean just plain “untracked” (including ugnored files). Either way, git ls-files is the tool for the job:

For “untracked” (will include ignored files, if present):

git ls-files --others

For “untracked and unignored”:

git ls-files --exclude-standard --others

My first though is to just check whether these commands have output:

test -z "$(git ls-files --others)"
  • If it exits with 0 then there are no untracked files. If it exits with 1 then there are untracked files.

There is a small chance that this will translate abnormal exits from git ls-files into “no untracked files” reports (both result in non-zero exits of the above command). A bit more robust version might look like this:

u="$(git ls-files --others)" && test -z "$u"
  • The idea is the same as the previous command, but it allows unexpected errors from git ls-files to propagate out. In this case a non-zero exit could mean “there are untracked files” or it could mean an error occurred. If you want the “error” results combined with the “no untracked files” result instead, use test -n "$u" (where exit of 0 means “some untracked files”, and non-zero means error or “no untracked files”).

Another idea is to use --error-unmatch to cause a non-zero exit when there are no untracked files. This also runs the risk of conflating “no untracked files” (exit 1) with “an error occurred” (exit non-zero, but probably 128). But checking for 0 vs. 1 vs. non-zero exit codes is probably fairly robust:

git ls-files --other --error-unmatch . >/dev/null 2>&1; ec=$?
if test "$ec" = 0; then
    echo some untracked files
elif test "$ec" = 1; then
    echo no untracked files
else
    echo error from ls-files
fi

Any of the above git ls-files examples can take --exclude-standard if you want to consider only untracked and unignored files.

Chris Johnsen
Best answer. Much more correct than the accepted one
mislav