1. Overview

Git is a well-known version control system that’s highly popular among developers. It provides an extensive command-line interface that enables developers to integrate Git commands into shell scripts.

In this tutorial, we’ll learn how to detect changes in a Git directory using a shell script.

2. Git Repository Test Case

In this section, we’ll initialize and populate a new Git repository.

2.1. Git Repository Initialization

First, let’s create a test Git project:

$ mkdir myproject
$ cd myproject
$ git init --initial-branch=main
Initialized empty Git repository in /home/ubuntu/article40/myproject/.git/
$ git config user.email "[email protected]"

First, we created the root directory of our project and named it myproject. Then, we created a new Git repository under myproject using the git init command. In addition, we set main as the initial branch with the –initial-branch option. Finally, we set the user.email property, which is required for committing files.

2.2. Git Repository Population

Next, let’s create some files and directories in our workspace:

$ mkdir mydirectory
$ echo Hello >> mydirectory/myfile1
$ echo Hello >> myfile2
$ echo Hello >> myfile3
echo Hello >> myfile4

As we can see, we’ve created four files, namely myfile1myfile2myfile3, and myfile4.

Firstly, we add myfile2, myfile3, and myfile4 to the index:

$ git add myfile2 myfile3 myfile4

Secondly, let’s commit myfile3 and myfile4:

$ git commit -m 'commit message' myfile3 myfile4
[main (root-commit) 818740d] commit message
 2 files changed, 2 insertions(+)
 create mode 100644 myfile3
 create mode 100644 myfile4

Finally, we modify myfile3 and delete myfile4:

$ echo Hello Again>> myfile3
$ rm myfile4

Consequently, we have a different status for each of the four files:

  1. myfile1 is untracked
  2. myfile2 is added to the index
  3. myfile3 is committed to the local repository and modified in the workspace
  4. myfile4 is committed to the local repository and deleted from the workspace

To clarify, the index is an intermediary space between the workspace and the local repository. The purpose of the index is to gather changed files to commit them together.

3. The git status Command

The git status command reports changes between the workspace, the index, and the local repository:

$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   myfile2

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   myfile3
        deleted:    myfile4

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        mydirectory/

$ echo $?
0

Indeed, git status reported all changes in our project. The output included untracked, unstaged, and staged files.

Furthermore, we echoed the shell variable $? to get the exit status code, which was 0. However, the exit status code remains 0 even if no changes are found.

4. The porcelain Option

The default output of the git status command is lengthy and it may not be easy to parse inside a shell script. Because of this, the –porcelain option can be used to produce a concise output.

4.1. Using porcelain

Let’s run git status with the –porcelain option:

$ git status --porcelain
A  myfile2
 M myfile3
 D myfile4
?? mydirectory/

Indeed, we see that only a list of files was printed. Moreover, a two-letter status flag precedes each file.

However, we also see that the last row contains a directory instead of a file. We can modify this behavior by using the -u option that displays untracked files:

$ git status --porcelain -u
A  myfile2
 M myfile3
 D myfile4
?? mydirectory/myfile1

As we can see, all rows now contain a filename.

4.2. Status Flags

Furthermore, the status flag consists of two characters. If we’re not performing a merge operation, the first character shows the index status while the second shows the workspace status.

The above example shows four different status flags:

  • M: modified
  • A: Added
  • D: deleted
  • ??: unknown to the index

Additionally, the –porcelain option accepts a version property with two available values, 1 and 2. If we don’t set a value, version 1 is the default. Also, version 1 is guaranteed not to change in the future and is more concise than version 2. As a result, it’s easier and safer to use it within a script.

On the other hand, version 2 of the –porcelain option includes extra information in the command output, like the file mode, the current commit, the current branch, and others.

5. The git commit Command

Instead of the git status command, we can use git commit to produce a concise list of the files changed in our project. For this purpose, we can use the –dry-run option together with the –porcelain and -u options:

$ git commit --dry-run --porcelain -u
A  myfile2
 M myfile3
 D myfile4
?? mydirectory/myfile1

Indeed, we can see that the git commit –dry-run produced a similar output to that of the git status command. Specifically, the –dry-run option provides an overview of the files that will be committed without actually committing them.

6. Using git status Within a Shell Script

After getting to know several methods, let’s create a small shell script that checks if there are changes in our working directory:

$ cat check-git-status.sh
#!/bin/bash

statusResult=$(git status -u --porcelain)
if [ -z statusResult ]
then
   echo 'no changes found'
else
   echo 'The workspace is modified:'
   echo "$statusResult"
fi

As we can see, first, we interpolate the git status command and save its output in the statusResult variable. Next, we check if the statusResult variable is empty using the -z conditional operator. If the variable has no value, we print a no changes found message to the terminal. Otherwise, we print the value of the statusResult variable.

Let’s run the script inside the Git directory:

$ chmod u+rwx ./check-git-status.sh 
$ ./check-git-status.sh
The workspace is modified:
A  myfile2
 M myfile3
 D myfile4
?? mydirectory/myfile1

Indeed, the shell script correctly found the same changes in our Git project.

7. Parsing the Output of git status

We can expand our shell script to also parse the output of the git status command. Our objective is to break down the lines of the git status output and extract the flag and filename fields from each line:

$ cat ./parse-git-status.sh
#!/bin/bash
statusResult="$(git status -u --porcelain)" 
if [ -z statusResult ]
then
   echo 'no changes found'
else
   echo 'The workspace is modified:'
   tmpIFS=$IFS
   IFS=$'\r\n'
   for i in $statusResult; do
      flag=${i:0:2}
      filename=$(tr -d $flag <<< $i)
      filename=$(echo $filename | xargs)
      echo $flag '-' $filename
   done
   IFS=$tmpIFS
fi

As we can see, we assigned the line break characters \r\n to the IFS shell variable. The IFS characters function as delimiters within the for loop. As a result, the for loop divides the lines within the statusResult variable.

Following, we extract the first two characters of each line. These two characters hold the Git status flag. For this reason, we used the parameter substitution syntax ${string:position:length} to extract them.

Next, we used the tr -d option to eliminate the status flag and save the resulting string to the filename variable. Finally, we trim the filename variable from leading and trailing spaces using the xargs command.

Let’s run the parse-git-status.sh script within the Git directory:

$ chmod u+rwx ./parse-git-status.sh 
$ ./parse-git-status.sh
The workspace is modified:
A  - myfile2
 M - myfile3
 D - myfile4
?? - mydirectory/myfile1

Indeed, we can see that the script parses the output of the git status command correctly.

8. Conclusion

In this article, we learned how to check if a Git directory is clean within a script.

Initially, we set up a small Git project and changed some files. Next, we examined the git status and git commit commands. Following that, we developed two shell scripts. The first script checks if a Git directory has modifications. Finally, we established the foundation for managing each altered file in the second shell script.