mirror of
https://github.com/AdaCore/git-hooks.git
synced 2026-02-12 12:43:11 -08:00
227 lines
8.5 KiB
Python
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
|
|
)
|