280

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.

Robert Munteanu
  • 67,031
  • 36
  • 206
  • 278
  • 3
    git status will return 1 if there are unstaged modified files. But in general, I find that the git tools are not particularly thorough with return status. EG git diff returns 0 whether or not there are differences. – intuited Apr 17 '10 at 11:38
  • 11
    @intuited: If you need `diff` to indicate the presence or absence of differences rather than the successful running of the command then you need to use `--exit-code` or `--quiet`. git commands are generally very consistent with returning a zero or non-zero exit code to indicate the success of the command. – CB Bailey Apr 17 '10 at 19:22
  • 1
    @Charles Bailey: Hey great, I missed that option. Thanks! I guess I never really needed it to do that, or I probably would have scoured the manpage for it. Glad you corrected me :) – intuited Apr 17 '10 at 20:14
  • 1
    robert - this is a very confusing topic for the community (doesn't help that 'porcelain' is used differently in different contexts). The answer you've selected ignores a 2.5x times higher-voted answer that is more robust, and follows the git design. Have you seen @ChrisJ 's answer? – some bits flipped May 03 '16 at 21:20
  • 1
    @Robert - I'm hoping you might consider changing your accepted answer. The one you selected relies on more-fragile git 'porcelain' commands; the actual correct answer (also with **2x** the upvotes) relies on the correct git 'plumbing' commands. That correct answer was originally by Chris Johnsen. I bring this up as today is the 3rd time I have had to refer someone to this page, but i can't just point to the answer, i have explain why the accepted answer is sub-optimal / borderline wrong. Thank you! – some bits flipped Nov 01 '16 at 20:03
  • My combination answer I just added: [I want to see my commit hash as `72361c8` or `72361c8-dirty`, for use in build version numbers. Here is how.](https://stackoverflow.com/a/76856090/4561887) – Gabriel Staples Aug 08 '23 at 05:47

14 Answers14

455

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

(Note that in the below commands, HEAD -- is used instead of HEAD because otherwise the command fails if there is a file named HEAD.)

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 ignored 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 thought 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 --others --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.

Eduard Wirch
  • 9,785
  • 9
  • 61
  • 73
Chris Johnsen
  • 214,407
  • 26
  • 209
  • 186
  • 6
    I'd like to point out that `git ls-files --others` gives *local* untracked files, while the `git status --porcelain` of the accepted answer gives all the untracked files that are under the git repository. I'm not which of these the original poster wanted, but the difference between both is interesting. – Eric O. Lebigot Dec 01 '10 at 21:20
  • On Ubuntu 10.04, `git ls-files --other --error-unmatch --exclude-standard` always returns 0, no matter whether I have untracked files. Am I missing something? – phunehehe Apr 10 '13 at 07:51
  • 1
    @phunehehe: You need to supply a pathspec with `--error-unmatch`. Try (e.g.) `git ls-files --other --error-unmatch --exclude-standard .` (note the trailing period, it refers to the cwd; run this from the top-level directory of the working tree). – Chris Johnsen Apr 10 '13 at 09:58
  • In `1.8.3.4`, the exit code of `git diff-index --quiet HEAD file` appears to change based on whether or not `git status` has been run since the file was mutated. – phs Oct 16 '13 at 04:29
  • 8
    @phs: You may need to do `git update-index -q --refresh` before the `diff-index` to avoid some “false positives” caused by mismatching stat(2) information. – Chris Johnsen Oct 16 '13 at 05:26
  • 1
    I was bitten by the `git update-index` need! This is important if something is touching the files without making modifications. – Nakedible Sep 10 '14 at 23:39
  • 1
    @EOL: What is a “local” untracked file? Are there other types of untracked files? Which types? – Robert Siemer Jul 21 '16 at 08:49
  • 2
    @RobertSiemer By "local" I meant the files under your _current directory_, which might be _below_ its main git repository. The `--porcelain` solution lists all the untracked files found in the _whole git repository_ (minus git-ignored files), even if you are inside one of its subdirectories. – Eric O. Lebigot Jul 24 '16 at 20:38
  • That would be more explicit, but comments cannot be edited anymore, after a few minutes. I think that the comment is still useful, so I prefer to not change its position in the list of comments and therefore to not edit it. – Eric O. Lebigot Jul 26 '16 at 20:45
  • I suggested an edit to change from `git diff-index --quiet --cached HEAD` to `git diff-index --quiet --cached HEAD --`. Without this, the command will fail if there's a file named `HEAD` (you can `touch HEAD` to try this). – nh2 Jun 15 '17 at 18:07
  • Alright. I'm convinced that `git status --porcelain` is inferior to *proper plumbing commands*. But how can I get the number of modified files? `git diff-index HEAD -- | wc -l`? – Steven Lu Feb 02 '18 at 19:45
  • All 3 commands, git diff-index and git diff-files return an exit code 1, when combined with the --quiet parameter and 0 without quiet for a clean workdir (using Git 2.5.1) – BlackEye Mar 01 '18 at 15:54
  • It seems that `git diff-files --quiet` indicates whether there are changes that could be _committed_, not merely changes that could be _staged_, as indicated in the excellent answer. The difference is that this command returns 1 even if all commitable changes have already been staged, ie there are no changes remaining outside the stage. – Greg Hill Jul 19 '20 at 01:17
  • thank you for this answer, @ChrisJohnsen - learning about the difference between plumbing and porcelain commands was most useful! – Modern Ronin Sep 03 '20 at 14:51
  • My problem is that the plumbing commands are significantly slower than `git status --porcelain, as reported [here](https://gist.github.com/sindresorhus/3898739#gistcomment-3571721). Do there exist any plumbing commands that are as fast as the porcelain commands? – nh2 Dec 24 '20 at 01:24
  • This answer is just marvelous. – Devin Rhode Dec 05 '21 at 05:40
  • @phunehehe you missing an "s" --others maybe, sorry I took a lot of time to answer :D – Richard Jan 11 '22 at 21:48
181

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

Edit:

Here are the posts:

Adding Git Status Information to your Terminal Prompt

Improved Git-enabled Shell Prompt

Izkata
  • 8,961
  • 2
  • 40
  • 50
0xfe
  • 4,541
  • 1
  • 18
  • 14
  • 9
    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) – Cascabel Apr 18 '10 at 06:29
  • 8
    A warning: `git diff --shortstat` will give a false negative if changes are already in the index. – Marko Topolnik Mar 21 '13 at 16:13
  • 5
    `git status --porcelain` is preferable, because `git diff --shortstat` will not catch newly-created empty files. You can try it in any clean working tree: `touch foo && git diff --shortstat` – Campadrenalin Aug 05 '14 at 15:37
  • 7
    NO - porcelain means the output is for _humans_ and breaks easily!! see @ChrisJohnsen 's answer, which correctly uses the stable, script-friendly options. – some bits flipped Oct 12 '15 at 21:54
  • @0xfe , there is one more case for output of `git status --porcelain` , I you add a file to staging and them modify it before committing. Result of that command will be `AM filename` – Eray Dec 01 '15 at 22:35
  • 11
    @mike from the git-status man page about the porcelain option: "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." – itsadok Dec 24 '15 at 15:45
  • 1
    @itsadok - ```git status``` itself is a _porcelain_ command. See ["Programmatically" means never ever rely on porcelain commands](http://stackoverflow.com/a/3879077/309433). Now, for various historical reasons the "more stable" output of the _porcelain_ (e.g. unstable) command ```git status``` _is_ confusingly named ```--porcelain``` - but that does not change the fact that scripts should use ***Plumbing*** not porcelain commands. – some bits flipped May 03 '16 at 21:12
  • 1
    @itsadok - unfortuntely that that's incorrect & missleading. I'll quote the manual, here too, but first consider why they are called 'porcelain' - porcelain is ceramic, it's **FRAGILE** ... anyway, directly from the manual, comparing _"porcelain"_ with _"plumbing"_ : "The interface ... to these low-level commands are meant to be **a lot _more_ stable than Porcelain** level commands, because these commands are primarily for scripted use. The interface to Porcelain commands on the other hand are subject to change in order to improve the end user experience." – some bits flipped Nov 01 '16 at 19:51
  • @itsadok, my quote had to be long enough to reach the max comment length - here is the direct link to the git man page: https://www.kernel.org/pub/software/scm/git/docs/#_low_level_commands_plumbing – some bits flipped Nov 01 '16 at 20:00
  • 3
    @mike, you're wrong. The `--porcelain` option carves out a specific, explicit exception to the guideline, guaranteeing the command output format will be stable across versions specifically so scripts can rely on it. That option is implemented by multiple git commands, always with the same meaning: make the output suitable for scripts. – jthill Nov 27 '16 at 17:18
  • @jthill - if we bubble back up to what the OP is asking, implied is what is the _best_ way to do it. Have somewhat-improved scripting api's been tacked on porcelain commands? Yes. Are porcelain commands the best way to script Git? - You'll get an an emphatic **NO**, if you read the entirety of the relevant manual sections. You don't have to take my word for it; as of today the better answer (plumbing commands) has **> 2x the votes** of this ("accepted") answer. The point is to _steer people to the best way_, and it seems the community has answered that pretty clearly here. – some bits flipped Dec 14 '16 at 22:37
  • 2
    One of the most underutilized functions in grep is `-c`, you don't need to pipe grep into `wc -l` – Luke Exton Feb 07 '18 at 15:25
  • Why grep vs egrep? – jtlz2 May 15 '23 at 11:21
170

Assuming you are on git 1.7.0 or later...

After reading all of the answers on this page and some experimenting, I think the method that hits the right combination of correctness and brevity is:

test -n "$(git status --porcelain)"

While git allows for a lot of nuance between what's tracked, ignore, untracked but unignored, and so on, I believe the typical use case is for automating build scripts, where you want to stop everything if your checkout isn't clean.

In that case, it makes sense to simulate what the programmer would do: type git status and look at the output. But we don't want to rely on specific words showing up, so we use the --porcelain mode introduced in 1.7.0; when enabled, a clean directory results in no output.

Then we use test -n to see if there was any output or not.

This command will return 1 if the working directory is clean and 0 if there are changes to be committed. You can change the -n to a -z if you want the opposite. This is useful for chaining this to a command in a script. For example:

test -z "$(git status --porcelain)" || red-alert "UNCLEAN UNCLEAN"

This effectively says "either there are no changes to be made or set off an alarm"; this one-liner might be preferable to an if-statement depending on the script you are writing.

benzado
  • 82,288
  • 22
  • 110
  • 138
  • 1
    For me all other commands were giving different results on same rep between Linux and windows. This command gave me same output in both. – Adarsha Mar 19 '16 at 11:56
  • 12
    Thank you for answering the question and not rambling on and on, never providing a clear answer. – NateS Nov 17 '16 at 21:23
  • 2
    For a manual deployment script, combine this with `test -n "$(git diff origin/$branch)"`, to help prevent local commits from being allowed in a deployment – Erik Aronesty Oct 04 '19 at 13:38
  • 1
    My kodus for this answer. I think you cut straight to the point by using your “simulate the user typing `git status`” approach. – Guildenstern Sep 04 '20 at 09:11
  • Upvoted! Here is my preferred form: `test -z "$(git status --porcelain)" && echo "clean" || echo "dirty"`. It outputs a nice and simple `clean` or `dirty`. – Gabriel Staples Aug 08 '23 at 01:51
17

An implementation from VonC's answer:

if [[ -n $(git status --porcelain) ]]; then echo "repo is dirty"; fi
Community
  • 1
  • 1
Dean Rather
  • 31,756
  • 15
  • 66
  • 72
12

Had a look through a few of these answers... (and had various issues on *nix and windows, which was a requirement I had)... found the following worked well...

git diff --no-ext-diff --quiet --exit-code

To check the exit code in *nix

echo $?   
#returns 1 if the repo has changes (0 if clean)

To check the exit code in window$

echo %errorlevel% 
#returns 1 if the repos has changes (0 if clean) 

Sourced from https://github.com/sindresorhus/pure/issues/115 Thanks to @paulirish on that post for sharing

mlo55
  • 6,663
  • 6
  • 33
  • 26
  • 3
    Great short answer. Very happy that you also mentioned how to check exit code in Linux and Windows, this saves another Google search. A side note: You can omit the `--exit-code` because it is implied by `--quiet`. – msa Oct 02 '21 at 19:42
  • 1
    This returns the wrong answer if your working copy is dirty, but you've staged all changes for commit. git diff is empty, but the working copy isn't clean. – Glenn Maynard Oct 06 '21 at 05:30
6

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.

Community
  • 1
  • 1
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
6

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.

Community
  • 1
  • 1
Robert Munteanu
  • 67,031
  • 36
  • 206
  • 278
  • 6
    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 Apr 17 '10 at 17:12
  • @0xfe - Do you know when the `--porcelain` flag was added? Does not work with 1.6.4.2 . – Robert Munteanu Apr 17 '10 at 17:31
  • @Robert: try `git status --short` then. – VonC Apr 17 '10 at 17:34
  • 3
    `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 Apr 17 '10 at 18:12
  • @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 Apr 17 '10 at 19:12
  • @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 Apr 17 '10 at 19:13
3

You can also do

git describe --dirty

. It will append the word "-dirty" at the end if it detects a dirty working tree. According to git-describe(1):

   --dirty[=<mark>]
       Describe the working tree. It means describe HEAD and appends <mark> (-dirty by default) if
       the working tree is dirty.

. Caveat: untracked files are not considered "dirty", because, as the manpage states, it only cares about the working tree.

Linus Arver
  • 1,331
  • 1
  • 13
  • 18
3

I needed quite often a simple way to fail a build if at the end of execution there are any modified tracked files or any untracked files that are not ignored.

This is quite important for avoiding case where builds produce leftovers.

So far, the best command I ended up using looks like:

 test -z "$(git status --porcelain | tee /dev/fd/2)" || \
     {{ echo "ERROR: git unclean at the end, failing build." && return 1 }}

It may look bit complex and I would appreciate if anyone finds a shorted variant that this maintains desired behavior:

  • no output and succcess exit code if evenything is in order
  • exit code 1 if it fails
  • error message on stderr explaining why it fails
  • display list of files causing the failure, stderr again.
sorin
  • 161,544
  • 178
  • 535
  • 806
2

This is a more shell friendly variation for finding out if any untracked files exist in the repository:

# Works in bash and zsh
if [[ "$(git status --porcelain 2>/dev/null)" = *\?\?* ]]; then
  echo untracked files
fi

This doesn't fork a second process, grep, and doesn't need a check for if you are in a git repository or not. Which is handy for shell prompts, etc.

docwhat
  • 11,435
  • 6
  • 55
  • 54
2

@eduard-wirch answer was quite complete, but as I wanted to check both at the same time, here is my final variant.

        set -eu

        u="$(git ls-files --others)"
        if ! git diff-index --name-only --quiet HEAD -- || [ -z "${u:-}" ]; then
            dirty="-dirty"
        fi

When not executing using set -e or equivalent, we instead can do an u="$(git ls-files --others)" || exit 1 (or return if this works for a used function)

So untracked_files, is only set if the command succeeds properly.

after which, we can check for both properties, and set a variable (or whatever).

oliver
  • 761
  • 6
  • 4
0

There may be a better combination of answers from this thread.. but this works for me... for your .gitconfig's [alias] section ...

          # git untracked && echo "There are untracked files!"
untracked = ! git status --porcelain 2>/dev/null | grep -q "^??"
          # git unclean && echo "There are uncommited changes!"
  unclean = ! ! git diff --quiet --ignore-submodules HEAD > /dev/null 2>&1
          # git dirty && echo "There are uncommitted changes OR untracked files!"
    dirty = ! git untracked || git unclean
Alex Gray
  • 16,007
  • 9
  • 96
  • 118
0

The simplest automatic test I use to detect dirty state = any changes including untracked files:

git add --all
git diff-index --exit-code HEAD

Remarks:

  • Without add --all, diff-index does not notice untracked files.
  • Normally I run git reset after testing error code to unstage everything back.
  • Consider --quiet instead of --exit-code to avoid output.
Asclepius
  • 57,944
  • 17
  • 167
  • 143
uvsmtid
  • 4,187
  • 4
  • 38
  • 64
  • 3
    The question is specifically "from a script"...it's not a good idea to change the index just to test for dirty state. – ScottJ Feb 22 '17 at 20:45
  • @ScottJ, when it solves the problem, not everyone is necessarily strict on whether index can be modified or not. Consider the case of automated job which is about auto-patch sources with version number and make a tag - all you need to make sure is that absolutely no other local modifications stay in the way (regardless whether they are in the index or not). So far, this was reliable test for _any changes including untracked files_. – uvsmtid Mar 02 '17 at 09:12
  • Furthermore, `git diff-index --exit-code HEAD` asks HEAD to exist, it won't work for the first commit of an empty repository. – Dereckson Mar 08 '22 at 13:34
-3

Here is the best, cleanest way. The selected answer didn't work for me for some reason, it didn't pick up changes staged that were new files that weren't committed.

function git_dirty {
    text=$(git status)
    changed_text="Changes to be committed"
    untracked_files="Untracked files"

    dirty=false

    if [[ ${text} = *"$changed_text"* ]];then
        dirty=true
    fi

    if [[ ${text} = *"$untracked_files"* ]];then
        dirty=true
    fi

    echo $dirty
}
codyc4321
  • 9,014
  • 22
  • 92
  • 165