开发者

How do I programmatically determine if there are uncommitted changes?

In a Makefile, I'd like to perform certain actions if t开发者_运维技巧here are uncommitted changes (either in the working tree or the index). What's the cleanest and most efficient way to do that? A command that exits with a return value of zero in one case and non-zero in the other would suit my purposes.

I can run git status and pipe the output through grep, but I feel like there must be a better way.


UPDATE: the OP Daniel Stutzbach points out in the comments that this simple command git diff-index worked for him:

git update-index --refresh 
git diff-index --quiet HEAD --

A more precise option would be to test git status --porcelain=v1 2>/dev/null | wc -l, using the porcelain option.
See Myridium's answer.

(nornagon mentions in the comments that, if there are files that have been touched, but whose contents are the same as in the index, you'll need to run git update-index --refresh before git diff-index, otherwise diff-index will incorrectly report that the tree is dirty)

You can then see "How to check if a command succeeded?" if you are using it in a bash script:

git diff-index --quiet HEAD -- || echo "untracked"; // do something about it

Note: as commented by Anthony Sottile

git diff-index HEAD ... will fail on a branch which has no commits (such as a newly initialized repository).
One workaround I've found is git diff-index $(git write-tree) ...

And haridsv points out in the comments that git diff-files on a new file doesn't detect it as a diff.
The safer approach seems to be to run git add on the file spec first and then use git diff-index to see if anything got added to index before running git commit.

git add ${file_args} && \
git diff-index --cached --quiet HEAD || git commit -m '${commit_msg}'

And 6502 reports in the comments:

One problem I bumped in is that git diff-index will tell that there are differences when indeed there is none except for timestamps of the files.
Running git diff once solves the issue (surprisingly enough, git diff does actually change the content of the sandbox, meaning here .git/index)

These timestamp issues can also occur if git is running in docker.


Original answer:

"Programmatically" means never ever rely on porcelain commands.
Always rely on plumbing commands.

See also "Checking for a dirty index or untracked files with Git" for alternatives (like git status --porcelain)

You can take inspiration from the new "require_clean_work_tree function" which is written as we speak ;) (early October 2010)

require_clean_work_tree () {
    # Update the index
    git update-index -q --ignore-submodules --refresh
    err=0

    # Disallow unstaged changes in the working tree
    if ! git diff-files --quiet --ignore-submodules --
    then
        echo >&2 "cannot $1: you have unstaged changes."
        git diff-files --name-status -r --ignore-submodules -- >&2
        err=1
    fi

    # Disallow uncommitted changes in the index
    if ! git diff-index --cached --quiet HEAD --ignore-submodules --
    then
        echo >&2 "cannot $1: your index contains uncommitted changes."
        git diff-index --cached --name-status -r --ignore-submodules HEAD -- >&2
        err=1
    fi

    if [ $err = 1 ]
    then
        echo >&2 "Please commit or stash them."
        exit 1
    fi
}


While the other solutions are very thorough, if you want something really quick and dirty, try something like this:

[[ -z $(git status -s) ]]

It just checks if there is any output in the status summary.


git diff --exit-code will return nonzero if there are any changes; git diff --quiet is the same with no output. Since you want to check for the working tree and the index, use

git diff --quiet && git diff --cached --quiet

Or

git diff --quiet HEAD

Either one will tell you if there are uncommitted changes that are staged or not.


Expanding on @Nepthar's answer:

if [[ -z $(git status -s) ]]
then
  echo "tree is clean"
else
  echo "tree is dirty, please commit changes before running this"
  exit
fi


Some answers are overcomplicating the matter or not achieving the desired result. E.g. the accepted answer misses untracked files.

You can use git status --porcelain=v1 and parse the output programatically. The output is empty if there is some uncommitted change, otherwise it is not empty.

A POSIX-compliant minimum working example:

[ -z "$(git status --porcelain=v1 2>/dev/null)" ] && echo "No uncommitted changes."

If run outside a git repository, this will still say No uncommitted changes.

Details

  • The option --porcelain gives a machine-parseable output.
  • The option specification --porcelain=v1 fixes the output version of the machine-parseable output, so that your script will never break under a future git update. As of my writing, you can check https://git-scm.com/docs/git-status for information about other version options, like --porcelain=v2. You may be able to do more advanced scripting with versions beyond v1.
  • The 2>/dev/null is there so that git status will fail silently, if at all (i.e., if run outside of a git repository).
  • As of this writing, the command git status ... will return exit code 128 if it is not inside a git repository. You can check explicitly for this exit code if you want a third option besides "uncommitted changes" or "no uncommitted changes".

Extra: counting dirty files

Inspired by this answer. You grep the lines of git status --porcelain=v1 output. The first two characters of each line indicate what the status is of the particular file. After grepping, you count how many have that status by piping the output to wc -l which counts the number of lines.

You may code up some more advanced behaviour this way, or pick and choose what you consider qualifies as "uncommitted changes".

E.g. this script will print some information if run inside a git repository.

#!/bin/sh
GS=$(git status --porcelain=v1 2>/dev/null) # Exit code 128 if not in git directory. Unfortunately this exit code is a bit generic but it should work for most purposes.
if [ $? -ne 128 ]; then
  function _count_git_pattern() {
    echo "$(grep "^$1" <<< $GS | wc -l)" 
  }                                           
  echo "There are $(_count_git_pattern "??") untracked files."                                 
  echo "There are $(_count_git_pattern " M") unstaged, modified files."
  echo "There are $(_count_git_pattern "M ")   staged, modified files."        
fi


I created some handy git aliases to list unstaged and staged files:

git config --global alias.unstaged 'diff --name-only'
git config --global alias.staged 'diff --name-only --cached'

Then you can easily do things like:

[[ -n "$(git unstaged)" ]] && echo unstaged files || echo NO unstaged files
[[ -n "$(git staged)" ]] && echo staged files || echo NO staged files

You can make it more readable by creating a script somewhere on your PATH called git-has:

#!/bin/bash
[[ $(git "$@" | wc -c) -ne 0 ]]

Now the above examples can be simplified to:

git has unstaged && echo unstaged files || echo NO unstaged files
git has staged && echo staged files || echo NO staged files

For completeness here are similar aliases for untracked and ignored files:

git config --global alias.untracked 'ls-files --exclude-standard --others'
git config --global alias.ignored 'ls-files --exclude-standard --others --ignored'


The working tree is "clean" if

git ls-files \
  --deleted \
  --modified \
  --others \
  --exclude-standard \
  -- :/

returns nothing.

Explanation

  • --deleted check for files deleted in the working tree
  • --modified check for files modified in the working tree
  • --others check for files added in the working tree
  • --exclude-standard ignore according to the usual .gitignore, .git/info/exclude ... rules
  • -- :/ pathspec for everything, needed if not running in the root of the repository

That output is empty if the working tree is clean


With python and the GitPython package:

import git
git.Repo(path).is_dirty(untracked_files=True)

Returns True if repository is not clean


As pointed in other answer, as simple as such command is sufficient:

git diff-index --quiet HEAD --

If you omit the last two dashes, the command would fail if you have a file named HEAD.

Example:

#!/bin/bash
set -e
echo -n "Checking if there are uncommited changes... "
trap 'echo -e "\033[0;31mFAILED\033[0m"' ERR
git diff-index --quiet HEAD --
trap - ERR
echo -e "\033[0;32mAll set!\033[0m"

# continue as planned...

Word of caution: this command ignores untracked files.


Tested in the bash terminal on Linux Ubuntu.

Shell script to programmatically interpret the output of git status

...and tell you if:

  1. it had an error
  2. it shows your working tree is clean (no uncommitted changes), or
  3. it shows your working tree is dirty (you have uncommited changes).

There's a great answer here: Unix & Llinux: Determine if Git working directory is clean from a script. My answer is based on that.

We will use the --porcelain option with git status because it is intended to be parsed by scripts!

From man git status (emphasis added):

--porcelain[=<version>]

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.

The version parameter is used to specify the format version. This is optional and defaults to the original version v1 format.

So, do this:

Option 1

if output="$(git status --porcelain)" && [ -z "$output" ]; then
    echo "'git status --porcelain' had no errors AND the working directory" \
         "is clean."
else 
    echo "Working directory has UNCOMMITTED CHANGES."
fi

The first part, if output=$(git status --porcelain) will fail and jump to the else clause if the git status --porcelain command has an error. the 2nd part, && [ -z "$output" ], tests to see if the output variable contains an empty (zero-length) string. If it does, then the git status is clean and there are no changes.

Option 2

Generally my preferred usage, however, is to negate the test with -n (nonzero) instead of -z (zero) and do it like this:

if output="$(git status --porcelain)" && [ -n "$output" ]; then
    echo "'git status --porcelain' had no errors AND the working directory" \
         "is dirty (has UNCOMMITTED changes)."
    # Commit the changes here
    git add -A
    git commit -m "AUTOMATICALLY COMMITTING UNCOMMITTED CHANGES"
fi

Option 3

A more-granular way to write the first code block above would be like this:

if ! git_status_output="$(git status --porcelain)"; then
    # `git status` had an error
    error_code="$?"
    echo "'git status' had an error: $error_code" 
    # exit 1  # (optional)
elif [ -z "$git_status_output" ]; then
    # Working directory is clean
    echo "Working directory is clean."
else
    # Working directory has uncommitted changes.
    echo "Working directory has UNCOMMITTED CHANGES."
    # exit 2  # (optional)
fi

I've tested all of the above code by copying and pasting the whole blocks into my terminal in a repo in varying states, and it works fine for all 3 conditions:

  1. your git status command is wrong or misspelled
  2. git status is clean (no uncommitted changes)
  3. git status is dirty (you have uncommitted changes)

To force the 'git status' had an error output, simply misspell the --porcelain option by spelling it as --porcelainn or something, and you'll see this output at the very end:

'git status' had an error: 0


Here is the best, cleanest way.

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
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜