9

I am trying to create a Bash script that knows if there are changes in current working directory. I know that

$ git status

returns a message like "nothing to commit". What I am trying to, is to define a variable to true or false. This boolean value will tell me if there are or not changes.

Obviously I am not an expert of bash scripts. I tried something like this,

there_are_changes=$(git status | grep nothin)
echo $there_are_changes

but it doesn't work as expected. What should I do?

jub0bs
  • 60,866
  • 25
  • 183
  • 186
sensorario
  • 20,262
  • 30
  • 97
  • 159

3 Answers3

35

The git-diff man page describes two options of relevance here:

--quiet
Disable all output of the program. Implies --exit-code.

and

--exit-code
Make the program exit with codes similar to diff(1). That is, it
exits with 1 if there were differences and 0 means no differences.

Therefore, a robust approach would be to run

git diff --quiet; nochanges=$?

The shell variable nochanges will be equal to 0 (i.e. true) if there are no changes, and 1 (i.e. false) otherwise.

You can then use the value of nochanges in conditional statements as follows:

if [ $nochanges -eq 0 ]; then
    # there are no changes
else
    # there are changes
fi

Alternatively, if you don't need to store the exit status in a variable, you can do:

if git diff --quiet; then
    # there are no changes
else
    # there are changes
fi

Since git diff is a porcelain Git command and you want to do things programmatically, you should probably use the plumbing Git command called git diff-index instead (which also has a --quiet flag, but which must be supplied a tree-ish argument):

if git diff-index --quiet HEAD; then
    # there are no changes
else
    # there are changes
fi

As pointed out in a comment below, the approach outlined above does not cover untracked files. To cover them as well, you can use the following instead:

if [ -z "$(git status --porcelain)" ]; then
    # there are no changes
else
    # there are changes
fi
jub0bs
  • 60,866
  • 25
  • 183
  • 186
  • 1
    I didn't know about the `--quiet` flag for `git diff`, that's neat! – jmervine Feb 27 '15 at 19:10
  • As `git diff --quiet; nochanges=$?` will stop execution in scripts that are set to stop after a non zero exit code, you can instead use `CHANGES=false; (git diff --quiet) || CHANGES=true` – Alasdair McLeay Jun 02 '20 at 16:11
  • How do we only check for changes? i.e. avoid the `else`? – Richard Jan 06 '21 at 15:25
  • This does NOT work to detect untracked files. To check for create/modify/delete, you should check if `git status --porcelain` outputs text (such as via `if [[ -n "$(git status --porcelain)" ]]; then echo "things changed"; fi` ) – jeremysprofile Aug 23 '21 at 17:38
  • 1
    @jeremysprofile Good point. I've amended by answer accordingly. – jub0bs Sep 10 '21 at 07:50
  • @jub0bs I get an issue of still getting unstaged error when running `git diff-index --quiet HEAD ` even if i staged all files as i edited already staged files. Unstaging and staging them again will still give me error for `git diff-index --quiet HEAD `. So Aaron solution was the only one that worked for me. – SwiftiSwift May 14 '23 at 08:03
6

You can check if the variable is set by using the -n expression.

#!/bin/bash
CHANGESTOCOMMIT=$(git status | grep 'Changes to be com')
UNSTAGEDCHANGES=$(git status | grep 'Changes not staged')

# If there are staged changes:
if [ -n "$CHANGESTOCOMMIT" ]; then
    echo "Changes need to be committed"
fi
if [ -n "$UNSTAGEDCHANGES" ]; then
    echo "Changes made but not staged."
fi

Git tracks changed files that are both staged for committing, and also unstaged files, so your script might want to check both options (or not). The -n operator checks to see if the variable has been set - if it is empty it will return false.

An alternative is -z which returns True if it is empty (the logical opposite of -n. For a full list of conditional expressions please refer to the Bash Reference Manual.

Aaron D
  • 7,540
  • 3
  • 44
  • 48
  • Yeah, the downvote doesn't make sense for this. @Jubobs answer is a bit cleaner, but you got a +1 from me. – jmervine Feb 27 '15 at 19:08
  • I mixed up `-z` and `-n` so my script had a logic error. Now fixed. – Aaron D Feb 27 '15 at 19:11
  • 1
    That command substitution is inelegant; `git diff --quiet` already provides the functionality sought after by the user. – jub0bs Feb 27 '15 at 19:12
  • 1
    You're right, your solution is more elegant for this use case. The question was about how to check if the variable was set (true or false) in bash, but your solution sidesteps the need for grep completely. – Aaron D Feb 27 '15 at 19:14
  • @AaronD Thank you!! This fixed my issue of still getting unstaged error when running `git diff-index --quiet HEAD ` even if i staged all files as i edited already staged files. Unstaging and staging them again will still give me error for `git diff-index --quiet HEAD `. So Aaron solution was the only one that worked for me – SwiftiSwift May 14 '23 at 08:02
-2

Using this script you can check for changes not staged for commit. If there are changes the script will abord and exit. This works in any cases and is a safer check than Aaron D's answer because if you are using another language for git, that check will fail and it will say that you don't have unstaged stages even if you actually have.

So use this:

#!/bin/sh

# Check for changes not staged for commit
git diff --cached | grep -v ^$ > /dev/null

# If there are changes, exit with an error code
if [ $? -ne 0 ]; then
    echo "There are changes not staged for commit."
    exit 1
fi

# Check for changes to already staged files
git diff --cached --name-only | while read file; do
    if [ -f "$file" ]; then
        # Check if there are unstaged changes to the file
        if git diff --name-only "$file" | grep -v ^$ > /dev/null; then
            echo "There are unstaged changes to the file '$file'."
            exit 1
        else
            echo "Good to go! Commit now"
            exit 0
        fi
    fi
done

Explanation:

The code provided is a pre-commit hook that checks for changes not staged for commit, changes to already staged files, and unstaged changes to already staged files.

The hook first checks for changes not staged for commit using the git diff --cached command. If there are any changes, the hook will print a message and exit with an error code.

If there are no changes not staged for commit, the hook will then check for changes to already staged files using the git diff --cached --name-only command. If there are any files that have been modified, the hook will print a message and exit with an error code.

The hook will then check if there are any unstaged changes to the files that have been modified. If there are any unstaged changes, the hook will print a message and exit with an error code.

If there are no changes to already staged files or unstaged changes to already staged files, the hook will exit with a success code. In general text, the hook will check if there are any changes to your files that have not been committed. If there are any changes, the hook will print a message and exit with an error code. This will prevent you from committing changes that have not been staged.

Here is an example of how to use this hook:

  1. Create a file called * git/hooks/pre-commit
  2. Copy the code above into the file
  3. Make the file executable.
  4. The next time you try to commit, the pre-commit hook will check for changes not staged for commit, changes to already staged files, and unstaged changes to already staged files. If there are any changes, the hook will print a message and exit with an error code.

I actually use it to avoid code getting pushed to git that failed unit tests for flutter like this:

#!/bin/sh

# Check for changes not staged for commit
git diff --cached | grep -v ^$ > /dev/null

# If there are changes, exit with an error code
if [ $? -ne 0 ]; then
    echo "There are no changes or some changes are not staged for commit."
    git status
    exit 1
fi

# Check for changes to already staged files
git diff --cached --name-only | while read file; do
    if [ -f "$file" ]; then
        # Check if there are unstaged changes to the file
        if git diff --name-only "$file" | grep -v ^$ > /dev/null; then
            echo "There are unstaged changes to the file '$file'."
            exit 1
        else 
            flutter test

            # If the unit tests fail, exit with a non-zero exit code.
            if [[ $? -ne 0 ]]; then
                echo "Unit tests failed. Aborting commit."
                exit 1
            fi
        fi
    fi
done
Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
SwiftiSwift
  • 7,528
  • 9
  • 56
  • 96
  • [Why is testing ”$?” to see if a command succeeded or not, an anti-pattern?](https://stackoverflow.com/questions/36313216/why-is-testing-to-see-if-a-command-succeeded-or-not-an-anti-pattern) – tripleee May 14 '23 at 11:01