Files
git-hooks/hooks/update.py
Joel Brobecker f18c10b78c stop bypassing the updates.sendmail module during testsuite runs
The goal of this commit is to include the updates.sendmail module
in our testing strategy, in order to make sure that the hooks are
passing email data down to the sendmail program without issues.
This will become particularly important when we switch over to
using Python 3.x, because of the strong distinction between bytes
and strings with newer versions of Python which can cause a lot
problems. Hence the need to use this code during our testing.

The main strategy introduced by this commit to achieve this is
fairly simple: The testsuite framework introduces a new minimal
script to be called in place of the standard sendmail. A new
environment variable called GIT_HOOKS_SENDMAIL is introduced
allowing the testsuite to tell the hooks to use its own (fake)
sendmail instead of the system one. With that in place,
the old code bypassing the use of updates.sendmail can be removed,
thus allowing the testsuite to include it as part of the testing.
The testsuite's (fake) sendmail script was written in a way to
mimick the old bypassing code, so there is no change in output.

Parallel to that, the hooks are enhanced to check that we can
indeed find sendmail, and otherwise return immediately with
an error if not. This way, we avoid emails silently being
dropped due to the missing sendmail.

A couple of testcases are also added to double-check some
specific error situations.

Note that I tried to think of ways to split this patch into
smaller individual parts, but couldn't really find a way to
do so in a meaningful way, while at the same time producing
a commit where the coverage report stays clean (0 lines missed).

TN: U530-006 (transition to Python 3.x)
TN: U924-032 (test for sendmail not found)
TN: U924-034 (test for sendmail override when in testsuite mode)
Change-Id: I74b993592ec6d701347bbca5283a42e037411f1c
2021-09-24 17:41:10 -07:00

123 lines
4.4 KiB
Python

from argparse import ArgumentParser
from collections import OrderedDict
from shutil import rmtree
import sys
from config import ThirdPartyHook
from errors import InvalidUpdate
from git import get_object_type, git_show_ref
from init import init_all_globals
from requirements import check_minimum_system_requirements
from utils import debug, warn, create_scratch_dir, FileLock
# We have to import utils, because we cannot import scratch_dir
# directly into this module. Otherwise, our scratch_dir seems
# to not see the update when create_scratch_dir is called.
import utils
from updates.factory import new_update
def parse_command_line():
"""Return a namespace built after parsing the command line."""
# The command-line interface is very simple, so we could possibly
# handle it by hand. But it's nice to have features such as
# -h/--help switches which come for free if we use argparse.
#
# We use ArgumentParser, which means that we are requiring
# Python version 2.7 or later, because it handles mandatory
# command-line arguments for us as well.
ap = ArgumentParser(description='Git "update" hook.')
ap.add_argument("ref_name", help="the name of the reference being updated")
ap.add_argument("old_rev", help="the SHA1 before update")
ap.add_argument("new_rev", help="the new SHA1, if the update is accepted")
return ap.parse_args()
def maybe_update_hook(ref_name, old_rev, new_rev):
"""Call the update-hook if set in the repository's configuration.
Raises InvalidUpdate if the hook returned nonzero, indicating
that the update should be rejected.
PARAMETERS
ref_name: The name of the reference being update (Eg:
refs/heads/master).
old_rev: The commit SHA1 of the reference before the update.
new_rev: The new commit SHA1 that the reference will point to
if the update is accepted.
"""
result = ThirdPartyHook("hooks.update-hook").call_if_defined(
hook_args=(ref_name, old_rev, new_rev)
)
if result is not None:
hook_exe, p, out = result
if p.returncode != 0:
raise InvalidUpdate(
"Update rejected by this repository's hooks.update-hook" " script",
"({}):".format(hook_exe),
*out.splitlines()
)
else:
sys.stdout.write(out)
def check_update(ref_name, old_rev, new_rev):
"""General handler of the given update.
Raises InvalidUpdate if the update cannot be accepted (usually
because one of the commits fails a style-check, for instance).
PARAMETERS
ref_name: The name of the reference being update (Eg:
refs/heads/master).
old_rev: The commit SHA1 of the reference before the update.
new_rev: The new commit SHA1 that the reference will point to
if the update is accepted.
REMARKS
This function assumes that scratch_dir has been initialized.
"""
debug(
"check_update(ref_name=%s, old_rev=%s, new_rev=%s)"
% (ref_name, old_rev, new_rev),
level=2,
)
check_minimum_system_requirements()
update_cls = new_update(
ref_name, old_rev, new_rev, git_show_ref(), submitter_email=None
)
if update_cls is None:
# Report an error. We could look more precisely into what
# might be the reason behind this error, and print more precise
# diagnostics, but it does not seem like this would be worth
# the effort: It requires some pretty far-fetched scenarios
# for this to trigger; so, this should happen only very seldomly,
# and when a user does something very unusual.
raise InvalidUpdate(
"This type of update (%s,%s) is not valid."
% (ref_name, get_object_type(new_rev))
)
with FileLock("git-hooks::update.token"):
update_cls.validate()
maybe_update_hook(ref_name, old_rev, new_rev)
if __name__ == "__main__":
args = parse_command_line()
try:
init_all_globals(OrderedDict([(args.ref_name, (args.old_rev, args.new_rev))]))
create_scratch_dir()
check_update(args.ref_name, args.old_rev, args.new_rev)
except InvalidUpdate as E:
# The update was rejected. Print the rejection reason, and
# exit with a nonzero status.
warn(*E.args)
sys.exit(1)
finally:
# Delete our scratch directory.
if utils.scratch_dir is not None:
rmtree(utils.scratch_dir)