mirror of
https://github.com/AdaCore/git-hooks.git
synced 2026-02-12 12:43:11 -08:00
As part of determining which Update class it needs to instantiate
for the reference udpate being evaluated, the udpate factory
computes the following information:
- The kind of reference being updated (branch? notes? tag?)
- What operation on the branch is being performed (create? delete?
update?)
- The type of object the commit targeted by the reference
(commit? tag?)
In the context of enhancing the git-hooks to allow projects to provide
custom-checks on commits, the first and third items seem like these
could be useful information to pass to those hooks. In order to allow
this, without recomputing that information, this commit enhances
the AbstractUpdate class __init__ method to add those as additional
required paramaters, and then stores them as two new attributes.
As a bonus side-effect of this change, the new_rev_type attribute
is no longer necessary, which allows us to save one external call
to Git.
In the meantime, having these new attributes means that we can use
those to cross-check, within each AbstractUpdate child class'
self_sanity_check method, that the ref_kind and object_type values
correspond to each class' expectation (in other words, we cross-check
that the factory instantiated the correct class).
One other side-effect of that change is that we are no longer calling
the get_object_type function with a null SHA1 anymore. We could
modify the function's implementation to only accept non-null SHA1
revisions, but this would denature the function, in my opinion.
There is a genuine chance that perhaps, one day, we'll need that
again. So, instead, we cover that function by adding a new unit test
instead.
Change-Id: I8fd1ce180a6e17c0401b4dee07b9bc07d2abfdda
TN: T209-005
215 lines
8.5 KiB
Python
215 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)
|