Don't make hook script read-only

When a hook script is not executable, Git will ignore its presence
in the hooks dir. To work around that, git-review checks the
downloaded commit-message hook to see whether it's executable and
then adjusts its permissions accordingly. This behavior has been
included since the initial release, but its naive implementation
wiped all existing permissions and then added only read and execute
for the file's owner (0o500/r-x------), leaving it set read-only.
This is overly-restrictive and can lead to minor annoyances when
deleting directories or for atypical multi-user and group ownership
scenarios due to ignoring the umask set for the process.

It is expected that, at this time, the described behavior is not
widely observed outside workflows which rely on fetching the hook
script over HTTP, as the SCP protocol preserves filesystem
permission flags from the source system, but it will have a much
broader impact in the future if git-review's default workflow shifts
away from SCP.

Replace the naive chmod implementation with one which adds execute
for anyone who already has read permission, but does not remove any
existing permissions, for example:

    0o644/rw-r--r-- .. 0o755/rwx-r-xr-x
    0o640/rw-r----- .. 0o750/rwx-r-x---

This new behavior should be more intuitive and less surprising for
users.

Change-Id: I48ac230df09bc802610cfef65bd9818c5b01673d
This commit is contained in:
Jeremy Stanley 2024-02-26 14:57:49 +00:00
parent ae80cb6374
commit 4fddad6558
2 changed files with 30 additions and 2 deletions

View File

@ -24,6 +24,7 @@ import json
import os import os
import re import re
import shlex import shlex
import stat
import subprocess import subprocess
import sys import sys
import textwrap import textwrap
@ -429,8 +430,17 @@ def set_hooks_commit_msg(remote, target_file):
"git", "submodule", "foreach", "git", "submodule", "foreach",
'cp -p %s "$(git rev-parse --git-dir)/hooks/"' % target_file) 'cp -p %s "$(git rev-parse --git-dir)/hooks/"' % target_file)
if not os.access(target_file, os.X_OK): old_mask = stat.S_IMODE(os.stat(target_file).st_mode)
os.chmod(target_file, os.path.stat.S_IREAD | os.path.stat.S_IEXEC) # make sure the owner always has read+write+exec perms
mask = old_mask | stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
# add executable permission for everyone else who has read access
if mask | stat.S_IRGRP == mask:
mask |= stat.S_IXGRP
if mask | stat.S_IROTH == mask:
mask |= stat.S_IXOTH
# only call chmod if we need to change the mask
if mask != old_mask:
os.chmod(target_file, mask)
def test_remote_url(remote_url): def test_remote_url(remote_url):

View File

@ -105,6 +105,24 @@ class GitReviewTestCase(tests.BaseGitReviewTestCase):
self._run_git_review( self._run_git_review(
'-s', chdir=os.path.join(self.test_dir, 'subdirectory')) '-s', chdir=os.path.join(self.test_dir, 'subdirectory'))
def test_install_remote_hook(self):
"""Test whether git-review -s correctly creates the commit-msg hook
from the Gerrit server with appropriate permissions and content.
"""
hooks_subdir = ".git/hooks"
hook_file = os.path.join(self.test_dir, hooks_subdir, 'commit-msg')
self.reset_remote()
self._run_git_review('-s')
self.assertTrue(os.path.exists(hook_file))
self.assertTrue(os.access(hook_file, os.W_OK))
self.assertTrue(os.access(hook_file, os.X_OK))
with open(hook_file) as f:
content = f.read()
self.assertTrue(content.startswith('#!/'))
# This line is injected by the Gerrit server in its version
self.assertIn('# From Gerrit Code Review', content)
def test_git_review_s_without_core_hooks_path_option(self): def test_git_review_s_without_core_hooks_path_option(self):
"""Test whether git-review -s correctly creates the commit-msg hook, """Test whether git-review -s correctly creates the commit-msg hook,
with the Git core.hooksPath option unset. with the Git core.hooksPath option unset.