Git: How to add and remove execute permissions

Careful management of permissions on Windows.

POSIX permissions include the execute permission, which allows the file to be executed as a program. The permission can be set for each of the three classes: user, group, and others. So, for example, you can have a file that is executable only by its owning user.

Git doesn’t store per-class permissions, nor read and write permissions, since they wouldn’t make sense when copying repositories onto other computers. However it does store the execute permission, as a single bit, so you can keep programs in Git and run them when checked out.

Despite only storing this one bit, Git displays full POSIX file permissions. You may have noticed such permissions being displayed when running certain commands. For example, when you commit, Git shows “create mode” and a permissions number for new files:

$ git commit -m "Start on masterpiece"
[main 9e9584b] Start on masterpiece
 1 file changed, 1 insertion(+)
 create mode 100644 huge-ships.txt

Likewise, Git’s diffs report mode numbers, at the end of the “index” lines:

$ git diff
diff --git a/huge-ships.txt b/huge-ships.txt
index 8005d59..2259717 100644
--- a/huge-ships.txt
+++ b/huge-ships.txt
@@ -1 +1,2 @@
 How to Avoid Huge Ships
+Or: I Never Met a Ship I Liked

For regular files, Git will show:

The 100 at the start of each means “regular file”, as opposed to a symlink (120).

644 means “readable by all classes, and writeable by user”. And 755 means the same, with the executable bit added for each class, which increments each digit.

Thus, Git stores the executable permission as a single bit, but represents it with three bits, one in each class.

Change execute permissions

On POSIX operating systems (Linux, macOS, BSD), use the standard chmod command to change a file’s execute permissions. Git will detect the change and track it when committing:

$ chmod +x download_ships.py

$ git commit -am "Mark download_ships.py as executable"
[main 6899d99] Mark download_ships.py as executable
 1 file changed, 0 insertions(+), 0 deletions(-)
 mode change 100644 => 100755 download_ships.py

Use chmod -x to remove execute permissions.

On Windows, though, there’s a wrinkle. Windows has a much more complicated permissions model, so Git doesn’t detect execute permissions there. Git’s escape hatch here is add --chmod - pass +x to add execute permissions:

$ git add --chmod +x download_ships.py

Likewise, use -x to remove execute permissions.

Listing files with permissions

To list all files with their execute permissions tracked in Git, use the ls-files command with this format:

$ git ls-files --format '%(objectmode) %(path)'
100755 download_ships.py
100644 huge-ships.txt

To filter by name, pass a Git pathspec:

$ git ls-files --format='%(objectmode) %(path)' '*.py'
100755 download_ships.py

To filter by their mode, use grep:

$ git ls-files --format '%(objectmode) %(path)' | grep '^100644'
100644 huge-ships.txt

Cool beans.

Remove all executable permissions

Sometimes when you transfer files from Windows to a POSIX system, they’re all marked as executable. If this is the case in your repository, you can remove executable permissions from every file with:

$ git ls-files -z | xargs -0 chmod -x

You can ignore files with a certain extension with a negative pathspec:

$ git ls-files -z '!*.py' | xargs -0 chmod -x

That’ll fix it.

Prevent bad execute permissions with pre-commit

You might find contributors to your repository accidentally add execute permissions to non-executable scripts, or fail to add execute permissions to executable scripts. Either can cause confusion, or even errors. The pre-commit framework can prevent such incorrect files from being committed.

With pre-commit set up add the pre-commit-hooks repo, and select two hooks: check-executables-have-shebangs and check-shebang-scripts-are-executable. Here’s a minimal configuration file:

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
    -   id: check-executables-have-shebangs
    -   id: check-shebang-scripts-are-executable

The first check will fail for executable text files that don’t start with a shebang (#!):

$ pre-commit run --all
check that executables have shebangs.....................................Failed
- hook id: check-executables-have-shebangs
- exit code: 1

download_ships.py: marked executable but has no (or invalid) shebang!
  If it isn't supposed to be executable, try: `chmod -x download_ships.py`
  If on Windows, you may also need to: `git add --chmod=-x download_ships.py`
  If it is supposed to be executable, double-check its shebang.

…and the second will fail in the opposite case:

$ pre-commit run --all
check that executables have shebangs.....................................Passed
check that scripts with shebangs are executable..........................Failed
- hook id: check-shebang-scripts-are-executable
- exit code: 1

download_ships.py: has a shebang but is not marked executable!
  If it is supposed to be executable, try: `chmod +x download_ships.py`
  If on Windows, you may also need to: `git add --chmod=+x download_ships.py`
  If it not supposed to be executable, double-check its shebang is wanted.

Boom.

Fin

May your permissions always be appropriate,

—Adam


Learn more about pre-commit, particularly for Python projects, in my DX book.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: ,