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 myfile1, myfile2, myfile3, 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:
- myfile1 is untracked
- myfile2 is added to the index
- myfile3 is committed to the local repository and modified in the workspace
- 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.