Files
git-hooks/hooks/updates/factory.py
Joel Brobecker a075b1653e reformat all the code using black
Change-Id: Idbc70777233ab2d40ab59765abb9cbbeeb88ec63
2021-04-18 14:59:01 +04:00

227 lines
8.5 KiB
Python

"""A module providing an AbstractUpdate factory."""
from collections import namedtuple
from config import git_config
from git import is_null_rev, get_object_type
from errors import InvalidUpdate
from updates import RefKind, UpdateKind
from updates.branches.creation import BranchCreation
from updates.branches.deletion import BranchDeletion
from updates.branches.update import BranchUpdate
from updates.notes.creation import NotesCreation
from updates.notes.deletion import NotesDeletion
from updates.notes.update import NotesUpdate
from updates.tags.atag_creation import AnnotatedTagCreation
from updates.tags.atag_update import AnnotatedTagUpdate
from updates.tags.atag_deletion import AnnotatedTagDeletion
from updates.tags.ltag_creation import LightweightTagCreation
from updates.tags.ltag_update import LightweightTagUpdate
from updates.tags.ltag_deletion import LightweightTagDeletion
from utils import ref_matches_regexp
# A named tuple used to determine a repository's namespace information
# for a given kind of reference (RefKind).
NamespaceKey = namedtuple(
"NamespaceKey",
[
# The name of the config option to use in order to retrieve,
# for the associated RefKind, the repository's namespace.
# Should be None if alternate (non-standard) namespaces are not
# supported for the associated kind of reference.
"opt_name",
# The name of the option to use to decide whether the namespaces,
# which are normally standard for the associated kind of reference,
# should be used or not.
# Should be None if alternate (non-standard) namespaces are not
# supported for this kind of reference.
"use_std_opt_name",
# The standard namespaces for that kind of reference:
# A tuple of strings, each being a regular expression, matching
# references which are recognized, by default, as this RefKind.
"std",
],
)
# A dictionary providing namespace information for each kind of reference.
#
# The dictionary is architected as follow:
# + The key is a RefKind enum;
# + The value is a NamespaceKey.
NAMESPACES_INFO = {
RefKind.branch_ref: NamespaceKey(
opt_name="hooks.branch-ref-namespace",
use_std_opt_name="hooks.use-standard-branch-ref-namespace",
std=(
"refs/heads/.*", # Git/Gerrit branches.
"refs/meta/.*", # Git/Gerrit/git-hooks configuration.
"refs/drafts/.*", # Gerrit branches.
"refs/for/.*", # Gerrit branches.
"refs/publish/.*", # Gerrit branches.
),
),
RefKind.notes_ref: NamespaceKey(
opt_name=None, # No alternate namespace support.
use_std_opt_name=None, # No alternate namespace support.
std=("refs/notes/.*",),
),
RefKind.tag_ref: NamespaceKey(
opt_name="hooks.tag-ref-namespace",
use_std_opt_name="hooks.use-standard-tag-ref-namespace",
std=("refs/tags/.*",),
),
}
REF_CHANGE_MAP = {
(RefKind.branch_ref, UpdateKind.create, "commit"): BranchCreation,
(RefKind.branch_ref, UpdateKind.delete, "commit"): BranchDeletion,
(RefKind.branch_ref, UpdateKind.update, "commit"): BranchUpdate,
(RefKind.notes_ref, UpdateKind.create, "commit"): NotesCreation,
(RefKind.notes_ref, UpdateKind.delete, "commit"): NotesDeletion,
(RefKind.notes_ref, UpdateKind.update, "commit"): NotesUpdate,
(RefKind.tag_ref, UpdateKind.create, "tag"): AnnotatedTagCreation,
(RefKind.tag_ref, UpdateKind.delete, "tag"): AnnotatedTagDeletion,
(RefKind.tag_ref, UpdateKind.update, "tag"): AnnotatedTagUpdate,
(RefKind.tag_ref, UpdateKind.create, "commit"): LightweightTagCreation,
(RefKind.tag_ref, UpdateKind.delete, "commit"): LightweightTagDeletion,
(RefKind.tag_ref, UpdateKind.update, "commit"): LightweightTagUpdate,
}
def get_namespace_info(ref_kind):
"""Return the repository's namespace info for the given type of reference.
PARAMETERS
ref_kind: A RefKind object, indicating which kind of reference
we want the namespace information for.
RETURN VALUE
A list of regular expressions, matching the references which
are recognized by the repository as a reference of the kind
that was given as ref_kind.
"""
namespace_info = []
namespace_key = NAMESPACES_INFO[ref_kind]
if namespace_key.use_std_opt_name is None or git_config(
namespace_key.use_std_opt_name
):
namespace_info.extend(namespace_key.std)
if namespace_key.opt_name is not None:
namespace_info.extend(git_config(namespace_key.opt_name))
return namespace_info
def get_ref_kind(ref_name):
"""Return the kind of reference ref_name is (None if unrecognized).
PARAMETERS
ref_name: The name of the reference we want to identify via
matching against the repository's various namespaces.
RETURN VALUE
The kind of reference ref_name corresponds to (a RefKind object).
None if we couldn't identify the kind of reference.
"""
# Try to determine which kind of reference we are dealing with,
# by matching the reference name to the declared namespaces
# of each reference kind.
#
# Normally, the repository should be configured in such a way
# that it doesn't matter which type of reference we test first.
# However, iterating over the order that the different kinds
# are declared in class RefKind has a couple of advantages:
# - the iteration order is consistent (compared to iterating
# over NAMESPACES_INFO's keys, for instance);
# - the iteration order follows the order specified in
# class RefKind, where the most likely kinds of updates
# are listed first (very minor optimization, but comes
# pretty much for free).
for ref_kind in RefKind:
namespace_info = get_namespace_info(ref_kind)
for ref_re in namespace_info:
if ref_matches_regexp(ref_name, ref_re):
return ref_kind
return None
def raise_unrecognized_ref_name(ref_name):
"""Raise InvalidUpdate explaining ref_name is not a recognized reference.
While at it, try to be helpful to the user by providing,
in the error message, the repository's actual namespace.
PARAMETERS
ref_name: The name of the reference we did not recognize.
"""
err = [
"Unable to determine the type of reference for: {}".format(ref_name),
"",
"This repository currently recognizes the following types",
"of references:",
]
for ref_kind in RefKind:
err.append("")
err.append(
" * {}:".format(
{
RefKind.branch_ref: "Branches",
RefKind.notes_ref: "Git Notes",
RefKind.tag_ref: "Tags",
}[ref_kind]
)
)
err.extend(
[" {}".format(ref_re) for ref_re in get_namespace_info(ref_kind)]
)
raise InvalidUpdate(*err)
def new_update(ref_name, old_rev, new_rev, all_refs, submitter_email):
"""Return the correct object for the given parameters.
PARAMETERS
See AbstractUpdate.__init__.
RETURN VALUE
An object of the correct AbstractUpdate (child) class.
"""
if is_null_rev(old_rev) and is_null_rev(new_rev):
# This happens when the user is trying to delete a specific
# reference which does not exist in the repository.
#
# Note that this seems to only happen when the user passes
# the full reference name in the delete-push. When using
# a branch name (i.e. 'master' instead of 'refs/heads/master'),
# git itself notices that the branch doesn't exist and returns
# an error even before calling the hooks for validation.
raise InvalidUpdate(
"unable to delete '{}': remote ref does not exist".format(ref_name)
)
if is_null_rev(old_rev):
change_type = UpdateKind.create
object_type = get_object_type(new_rev)
elif is_null_rev(new_rev):
change_type = UpdateKind.delete
object_type = get_object_type(old_rev)
else:
change_type = UpdateKind.update
object_type = get_object_type(new_rev)
ref_kind = get_ref_kind(ref_name)
if ref_kind is None:
raise_unrecognized_ref_name(ref_name)
new_cls = REF_CHANGE_MAP.get((ref_kind, change_type, object_type), None)
if new_cls is None:
return None
return new_cls(
ref_name, ref_kind, object_type, old_rev, new_rev, all_refs, submitter_email
)