Whenever you develop UNIX shell scripts on Microsoft Windows and commit them with Git, they lack the UNIX execute permission bit. When you check out the file with Git on a UNIX-based system, these scripts cannot be started, and the software breaks. This article explains how to set the execute permission bit on Windows, and how to automate the process.
On UNIX-based systems, shell scripts are files with a “.sh” extension that can be executed like binary programs, as long as the execute permission bit is set. This is typically achieved by a command like
chmod +x script.sh.
Git is able to transfer UNIX permissions as part of committing and checking out files. This works right out of the box if you develop on UNIX-based machines, like Linux distribution, or macOS. A script.sh file that was made executable prior to committing it with Git will also have the execute permission bit set, when checking out the respective branch on a different UNIX machine.
However, if you develop software involving shell scripts on Windows, which should also run on UNIX, you have a problem. Windows filesystems like NTFS do not support UNIX permission bits. Whenever you create new shell scripts on Windows, or rename existing ones (which may have been executable at the time of check-out), these won’t be executable. When you push the code, these scripts won’t run a UNIX-based machine. [Side note: if you merely edit an existing shell script on Windows (already under version control) and commit the change, the execute bit in Git won’t be affected and will stay in place!]
Git offers commands to discover the UNIX permission bits of your files, or to set (virtual) permission bits in your staging area (also called Git index) on Windows. Let’s see how to use these commands manually, followed by an automated approach that fixes permissions on each commit automatically.
Fixing permissions the manual way
To discover which shell scripts are lacking the execute bit, we can run the Git ls-files command. An example is shown below, assuming your working directory in the command line window is the root of your Git repository:
git ls-files -s -- *.sh Output: 100644 b9945... 0 sources/some-script.sh 100644 f29de... 0 sources/some-other-script.sh 100755 5d107... 0 sources/correct-permissions.sh ...
-s flag considers the current state of the staging area, and forces the output to have above form, including the permission bits. The
-- syntax limits the listed files to those with the
.sh extension. Let’s look at the output. The first column indicates the permission bits. Ignoring the first three digits, a value of
100644 indicates that it is a regular file without the execute bit. The third file in the above output has permission bits
100755, thus it has the execute bit set. I’m assuming that you understand UNIX permission bits and know what
755 means. The second and third column are the file hash and the stage number, see here for more information.
To make a shell script executable, use the following command. Don’t forget to commit and push your changes after running it!
git update-index --chmod=+x script.sh
An automated approach using Git commit hooks
To avoid mistakes we can automate the process, using Git hooks. They are scripts that Git executes automatically on specific events, such as before or after committing or pushing. We will use the pre-commit hook to make all those shell scripts executable in the Git index which aren’t executable yet, prior to committing. I’ve written a small Python 3 script that does the job for you, so you’ll need Python 3 for this to work. Just follow below instructions.
Create a new file
<repository root>/.git/hooks/pre-commit (without any file extension) with the following content:
#!/usr/bin/env python import subprocess if __name__ == '__main__': output = subprocess.check_output(["git", "ls-files", "-s", "--", "*.sh"], shell=True).decode("utf-8") # type: str files_to_fix =  for line in output.splitlines(): # Example for "line": '100644 82f6a7d558e1b38c8b47ec5084fe20f970f09981 0 test-update.sh' entry = line.replace('\t', ' ').split(" ", maxsplit=3) mode = entry[3:] # strips the first 3 chars ("100") which we don't care about filename = entry if mode == "644": files_to_fix.append(filename) for file_path in files_to_fix: # git update-index --chmod=+x script.sh subprocess.check_call(["git", "update-index", "--chmod=+x", file_path], shell=True)
You may have to adapt the first line in case
python.exe is not on
PATH. If you’re unsure, just open the Git bash program and type
which python to determine whether this is the case, and what its actual location is. If you want to provide an absolute path to your Python interpreter instead, make sure to use the UNIX-style notation. For instance, put
#!/c/Python/python in the first line of the file, to use
Note: as documented here, older versions of Git on Windows had problems with the shebang operator, so I recommend that you update Git to a later version, which is good idea anyway.
Automatic pre-commit hook setup
Handling the UNIX permission bit is a pain if you develop on Windows, because you will forget to set it, prior to committing. Using the provided script, triggered by the pre-commit hook, will solve this problem once and for all.