Esempio n. 1
0
    def __reject_frozen_ref_update(self):
        """Raise InvalidUpdate if trying to update a frozen branch.

        PARAMETERS:
            short_ref_name: The reference's short name (see short_ref_name
                attribute in class AbstractUpdate).

        REMARKS
            Frozen and retired mean the same thing, in this case, except
            we use a {CONFIG_FILENAME}-based approach to determining whether
            updates are allowed on this branch or not. Eventually, we
            might probably retire reject_retired_branch_update...
        """
        frozen_refs = git_config("hooks.frozen-ref")
        for frozen_ref in frozen_refs:
            if self.ref_name != frozen_ref.strip():
                continue
            # Reject the update. Try to provide a more user-friendly
            # message for the majority of the cases where the user
            # is trying to update a branch, but still handle the case
            # where the user is updating an arbitrary reference.
            if self.ref_name.startswith("refs/heads/"):
                info = {"who": "the %s branch" % self.short_ref_name, "what": "branch"}
            else:
                info = {"who": self.ref_name, "what": "reference"}
            raise InvalidUpdate(
                "Updates to %(who)s are no longer allowed because" % info,
                'this %(what)s is now frozen (see "hooks.frozen-ref" in file' % info,
                "{CONFIG_FILENAME}, from the special branch {CONFIG_REF}).".format(
                    CONFIG_FILENAME=CONFIG_FILENAME, CONFIG_REF=CONFIG_REF
                ),
            )
Esempio n. 2
0
def reject_merge_conflict_section(commit):
    """Raise InvalidUpdate if the commit's revlog contains "Conflicts:" in it.

    More precisely, we are trying to catch the cases where a user
    performed a merge which had conflicts, resolved them, but then
    forgot to remove the "Conflicts:" section provided in the default
    revision history when creating the commit.

    PARAMETERS
        commit: A CommitInfo object corresponding to the commit being checked.
    """
    RH_PATTERN = "Conflicts:"

    for line in commit.raw_revlog_lines:
        if line.strip() == RH_PATTERN:
            info = [
                'Pattern "%s" has been detected.' % RH_PATTERN,
                '(in commit %s)' % commit.rev, '',
                'This usually indicates a merge commit where some'
                ' merge conflicts',
                'had to be resolved, but where the "Conflicts:"'
                ' section has not ', 'been deleted from the revision history.',
                '', 'Please edit the commit\'s revision history to'
                ' either delete',
                'the section, or to avoid using the pattern above'
                ' by itself.'
            ]
            raise InvalidUpdate(*info)
def reject_lines_too_long(rev, raw_rh):
    """Raise InvalidUpdate if raw_rh contains a line that's too long.

    Does nothing if the project was configured to skip this check.

    PARAMETERS
        rev: The revision of the commit being checked.
        raw_rh: A list of lines corresponding to the raw revision
            history (as opposed to the revision history as usually
            displayed by git where the subject lines are wrapped).
            See --pretty format option "%B" for more details.
    """
    max_line_length = git_config('hooks.max-rh-line-length')
    if max_line_length <= 0:
        # A value of zero (or less) means that the project does not
        # want this check to be applied.  Skip it.
        return

    for line in raw_rh:
        if len(line) > max_line_length:
            raise InvalidUpdate(
                'Invalid revision history for commit %s:' % rev,
                '',
                'The following line in the revision history is too long',
                '(%d characters, when the maximum is %d characters):'
                % (len(line), max_line_length),
                '',
                '>>> %s' % line)
Esempio n. 4
0
    def __init__(
        self,
        ref_name,
        ref_kind,
        object_type,
        old_rev,
        new_rev,
        all_refs,
        submitter_email,
    ):
        """The constructor.

        Also calls self.auto_sanity_check() at the end.

        PARAMETERS
            ref_name: Same as the attribute.
            ref_kind: Same as the attribute.
            object_type: Same as the attribute.
            old_rev: Same as the attribute.
            new_rev: Same as the attribute.
            all_refs: Same as the attribute.
            submitter_email: Same as parameter from_email in class
                EmailInfo's constructor.
                This is used to override the default "from" email when
                the user sending the emails is different from the user
                that pushed/submitted the update.
        """
        # If the repository's configuration does not provide
        # the minimum required to email update notifications,
        # refuse the update.
        if not git_config("hooks.mailinglist"):
            raise InvalidUpdate(
                "Error: hooks.mailinglist config option not set.",
                "Please contact your repository's administrator.",
            )

        self.ref_name = ref_name
        self.ref_namespace, self.short_ref_name = split_ref_name(ref_name)
        self.ref_kind = ref_kind
        self.object_type = object_type
        self.old_rev = old_rev
        self.new_rev = new_rev
        self.all_refs = all_refs
        self.email_info = EmailInfo(email_from=submitter_email)

        # Implement the new_commits_for_ref "attribute" as a property,
        # to allow for initialization only on-demand. This allows
        # us to avoid computing this list until the moment we
        # actually need it. To help caching its value, avoiding
        # the need to compute it multiple times, we introduce
        # a private attribute named __new_commits_for_ref.
        #
        # Same treatment for the following other "attribute":
        #  - commits_to_check
        #  - lost_commits
        self.__new_commits_for_ref = None
        self.__commits_to_check = None
        self.__lost_commits = None

        self.self_sanity_check()
Esempio n. 5
0
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)
    update_cls = new_update(ref_name, old_rev, new_rev, git_show_ref(),
                            submitter_email=None)
    if update_cls is None:
        raise InvalidUpdate(
            "This type of update (%s,%s) is currently unsupported."
            % (ref_name, get_object_type(new_rev)))
    with FileLock('git-hooks::update.token'):
        update_cls.validate()
def reject_merge_conflict_section(rev, raw_rh):
    """Raise InvalidUpdate if raw_rh contains "Conflicts:" in it.

    More precisely, we are trying to catch the cases where a user
    performed a merge which had conflicts, resolved them, but then
    forgot to remove the "Conflicts:" section provided in the default
    revision history when creating the commit.

    PARAMETERS
        rev: The revision of the commit being checked.
        raw_rh: A list of lines corresponding to the raw revision
            history (as opposed to the revision history as usually
            displayed by git where the subject lines are wrapped).
            See --pretty format option "%B" for more details.
    """
    RH_PATTERN = "Conflicts:"

    for line in raw_rh:
        if line.strip() == RH_PATTERN:
            info = ['Pattern "%s" has been detected.' % RH_PATTERN,
                    '(in commit %s)' % rev,
                    '',
                    'This usually indicates a merge commit where some'
                    ' merge conflicts',
                    'had to be resolved, but where the "Conflicts:"'
                    ' section has not ',
                    'been deleted from the revision history.',
                    '',
                    'Please edit the commit\'s revision history to'
                    ' either delete',
                    'the section, or to avoid using the pattern above'
                    ' by itself.']
            raise InvalidUpdate(*info)
Esempio n. 7
0
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)
Esempio n. 8
0
    def validate_ref_update(self):
        """See AbstractUpdate.validate_ref_update."""
        if (git_config("hooks.restrict-branch-deletion")
                and self.search_config_option_list(
                    option_name="hooks.allow-delete-branch",
                    ref_name=self.ref_name) is None):
            # The repository is configured to restrict branch deletion
            # and the reference is not in the list of references that
            # are allowed to be deleted. Reject the update with a helpful
            # error message.
            err = []

            allowed_list = git_config("hooks.allow-delete-branch")
            if not allowed_list:
                err.append(
                    "Deleting branches is not allowed for this repository.")

            else:
                err.extend([
                    "Deleting branch {name} is not allowed.".format(
                        name=self.human_readable_ref_name()),
                    "",
                    "This repository currently only allow the deletion of"
                    " references",
                    "whose name matches the following:",
                    "",
                ] + ["    {}".format(allowed) for allowed in allowed_list])

            tip = git_config("hooks.rejected-branch-deletion-tip")
            if tip is None:
                tip = DEFAULT_REJECTED_BRANCH_DELETION_TIP
            err.append("")
            err.extend(tip.splitlines())

            raise InvalidUpdate(*err)
def ensure_empty_line_after_subject(rev, raw_rh):
    """Raise InvalidUpdate if there is no empty line after the subject.

    More precisely, verify that if there is some text besides
    the commit subject, both parts are separated by an empty line.

    PARAMETERS
        rev: The revision of the commit being checked.
        raw_rh: A list of lines corresponding to the raw revision
            history (as opposed to the revision history as usually
            displayed by git where the subject lines are wrapped).
            See --pretty format option "%B" for more details.
    """
    if len(raw_rh) < 2:
        # No body other than the subject.  No violation possible.
        return

    if not raw_rh[1].strip() == '':
        info = (
            ['Invalid revision history for commit %s:' % rev,
             'The first line should be the subject of the commit,',
             'followed by an empty line.',
             '',
             'Below are the first few lines of the revision history:'] +
            ['| %s' % line for line in raw_rh[:5]] +
            ['',
             "Please amend the commit's revision history and try again."])
        raise InvalidUpdate(*info)
Esempio n. 10
0
def check_missing_ticket_number(commit):
    """Raise InvalidUpdate if a TN in the commit's revlog is missing...

    Note: This only applies if the project is configured to require TNs.

    PARAMETERS
        commit: A CommitInfo object corresponding to the commit being checked.
    """
    if not git_config('hooks.tn-required'):
        return

    tn_re = [  # Satisfy pep8's 2-spaces before inline comment.
        # The word 'no-tn-check' anywhere in the RH removes the need
        # for a TN in the RH.
        r'\bno-tn-check\b',
        # TN regexp.
        r'\b[0-9A-Z][0-9A-Z][0-9][0-9]-[0-9A-Z][0-9][0-9]\b',
    ]
    for line in commit.raw_revlog_lines:
        if re.search('|'.join(tn_re), line, re.IGNORECASE):
            return

    raise InvalidUpdate(*[
        'The following commit is missing a ticket number inside',
        'its revision history.  If the change is sufficiently',
        'minor that a ticket number is not meaningful, please use',
        'the word "no-tn-check" in place of a ticket number.',
        '',
        'commit %s' % commit.rev,
        'Subject: %s' % commit.subject,
    ])
Esempio n. 11
0
def check_filename_collisions(commit):
    """raise InvalidUpdate if the name of two files only differ in casing.

    PARAMETERS
        commit: A CommitInfo object representing the commit to be checked.
    """
    filename_map = {}
    for filename in commit.all_files():
        key = filename.lower()
        if key not in filename_map:
            filename_map[key] = [filename]
        else:
            filename_map[key].append(filename)
    collisions = [
        filename_map[k] for k in filename_map.keys()
        if len(filename_map[k]) > 1
    ]
    if collisions:
        info = [
            'The following filename collisions have been detected.',
            'These collisions happen when the name of two or more files',
            'differ in casing only (Eg: "hello.txt" and "Hello.txt").',
            'Please re-do your commit, chosing names that do not collide.', '',
            '    Commit: %s' % commit.rev,
            '    Subject: %s' % commit.subject, '', 'The matching files are:'
        ]
        for matching_names in collisions:
            info.append('')  # Empty line to separate each group...
            info += ['    %s' % filename for filename in matching_names]
        raise InvalidUpdate(*info)
Esempio n. 12
0
def ensure_iso_8859_15_only(commit):
    """Raise InvalidUpdate if the revision log contains non-ISO-8859-15 chars.

    The purpose of this check is make sure there are no unintended
    characters that snuck in, particularly non-printable characters
    accidently copy/pasted (this has been seen on MacOS X for instance,
    where the <U+2069> character was copy/pasted without the user
    even realizing it). This, in turn, can have serious unintended
    consequences, for instance when checking for ticket numbers, because
    tickets numbers need to be on a word boundary, and such invisible
    character prevents that.

    PARAMETERS
        commit: A CommitInfo object corresponding to the commit being checked.
    """
    if git_config('hooks.no-rh-character-range-check'):
        # The users of this repository explicitly requested that
        # all characters be allowed in revision logs, so do not perform
        # this verification.
        return

    for lineno, line in enumerate(commit.raw_revlog_lines, start=1):
        try:
            u = line.decode('UTF-8')
            u.encode('ISO-8859-15')
        except UnicodeEncodeError as e:
            raise InvalidUpdate(
                'Invalid revision history for commit %s:' % commit.rev,
                'It contains characters not in the ISO-8859-15 charset.', '',
                'Below is the first line where this was detected'
                ' (line %d):' % lineno, '| ' + line,
                '  ' + ' ' * e.start + '^', '  ' + ' ' * e.start + '|', '',
                "Please amend the commit's revision history to remove it",
                "and try again.")
Esempio n. 13
0
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)
Esempio n. 14
0
def check_filename_collisions(rev):
    """raise InvalidUpdate if the name of two files only differ in casing.

    PARAMETERS
        rev: The commit to be checked.
    """
    all_files = git.ls_tree('--full-tree', '--name-only', '-r', rev,
                            _split_lines=True)
    filename_map = {}
    for filename in all_files:
        key = filename.lower()
        if key not in filename_map:
            filename_map[key] = [filename]
        else:
            filename_map[key].append(filename)
    collisions = [filename_map[k] for k in filename_map.keys()
                  if len(filename_map[k]) > 1]
    if collisions:
        raw_body = git.log(rev, max_count='1', pretty='format:%B',
                           _split_lines=True)
        info = [
            'The following filename collisions have been detected.',
            'These collisions happen when the name of two or more files',
            'differ in casing only (Eg: "hello.txt" and "Hello.txt").',
            'Please re-do your commit, chosing names that do not collide.',
            '',
            '    Commit: %s' % rev,
            '    Subject: %s' % raw_body[0],
            '',
            'The matching files are:']
        for matching_names in collisions:
            info.append('')  # Empty line to separate each group...
            info += ['    %s' % filename for filename in matching_names]
        raise InvalidUpdate(*info)
Esempio n. 15
0
def check_missing_ticket_number(rev, raw_rh):
    """Raise InvalidUpdate if a TN in the RH is missing...

    Note: This only applies if the project is configured to require TNs.

    PARAMETERS
        rev: The revision of the commit being checked.
        raw_rh: A list of lines corresponding to the raw revision
            history (as opposed to the revision history as usually
            displayed by git where the subject lines are wrapped).
            See --pretty format option "%B" for more details.
    """
    if not git_config('hooks.tn-required'):
        return

    tn_re = [  # Satisfy pep8's 2-spaces before inline comment.
        # The word 'no-tn-check' anywhere in the RH removes the need
        # for a TN in the RH.
        r'\bno-tn-check\b',
        # TN regexp.
        r'\b[0-9A-Z][0-9A-Z][0-9][0-9]-[0-9A-Z][0-9][0-9]\b',
        ]
    for line in raw_rh:
        if re.search('|'.join(tn_re), line, re.IGNORECASE):
            return

    raise InvalidUpdate(*[
        'The following commit is missing a ticket number inside',
        'its revision history.  If the change is sufficiently',
        'minor that a ticket number is not meaningful, please use',
        'the word "no-tn-check" in place of a ticket number.',
        '',
        'commit %s' % rev,
        'Subject: %s' % raw_rh[0],
        ])
Esempio n. 16
0
    def validate_ref_update(self):
        """See AbstractUpdate.validate_ref_update.

        REMARKS
            This method handles both lightweight and annotated tags.
        """
        if not git_config("hooks.allow-delete-tag"):
            raise InvalidUpdate("Deleting a tag is not allowed in this repository")
Esempio n. 17
0
def check_fast_forward(ref_name, old_rev, new_rev):
    """Raise InvalidUpdate if the update violates the fast-forward policy.

    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.
    """
    # Non-fast-foward updates can be characterized by the fact that
    # there is at least one commit that is accessible from the old
    # revision which would no longer be accessible from the new revision.
    if not git.rev_list("%s..%s" % (new_rev, old_rev)):
        # This is a fast-forward update.
        return

    # Non-fast-forward update.  See if this is one of the references
    # where such an update is allowed.
    ok_refs = git_config("hooks.allow-non-fast-forward")

    for ok_ref_re in ok_refs + FORCED_UPDATE_OK_REFS:
        if re.match(ok_ref_re, ref_name) is not None:
            # This is one of the branches where a non-fast-forward update
            # is allowed.  Allow the update, but print a warning for
            # the user, just to make sure he is completely aware of
            # the changes that just took place.
            warn("!!! WARNING: This is *NOT* a fast-forward update.")
            warn("!!! WARNING: You may have removed some important commits.")
            return

    # This non-fast-forward update is not allowed.
    err_msg = NON_FAST_FORWARD_ERROR_MESSAGE

    # In the previous version of these hooks, the allow-non-fast-forward
    # config was assuming that all such updates would be on references
    # whose name starts with 'refs/heads/'. This is no longer the case.
    # For repositories using this configuration option with the old
    # semantics, non-fast-forward updates will now start getting rejected.
    #
    # To help users facing this situation understand what's going on,
    # see if this non-fast-forward update would have been accepted
    # when interpreting the config option the old way; if yes, then
    # we are probably in a situation where it's the config rather than
    # the update that's a problem. Add some additional information
    # to the error message in order to help him understand what's
    # is likely happening.
    if ref_name.startswith("refs/heads/"):
        for ok_ref_re in [
                "refs/heads/" + branch.strip() for branch in ok_refs
        ]:
            if re.match(ok_ref_re, ref_name) is not None:
                err_msg += "\n\n" + OLD_STYLE_CONFIG_WARNING
                break

    raise InvalidUpdate(*err_msg.splitlines())
Esempio n. 18
0
    def call(self, hook_input=None, hook_args=None, cwd=None):
        """Call the script specified via self.hook_option_name.

        This method assumes that the repository's configuration
        defines a hook via the self.hook_option_name option.

        It calls that hook with the given argument.

        Raises InvalidUpdate if we failed to call the hook for whatever
        reason (typically, the hook's path does not exist, or we do not
        have the right permissions for us to execute it).

        PARAMETERS
            hook_input: A string, containing the data to be sent to
                the hook via its stdin stream. None if no data needs
                to be sent.
            hook_args: An iterable of command-line arguments to pass to
                the hook. None if no arguments are needed.
            cwd: The working directory from which to execute the hook.
                If None, the hook is executed from the current working
                directory.

        RETURN VALUE
            Return a tuple with the following elements:
              - The name of the script called as a hook;
              - The Popen object corresponding the script's execution
                (which, by the time this function returns, has finished
                executing);
              - The output of the script (stdout + stderr combined).
        """
        hook_cmd = [self.hook_exe]
        if hook_args is not None:
            hook_cmd.extend(hook_args)
        try:
            p = Popen(
                hook_cmd,
                stdin=PIPE if hook_input is not None else None,
                stdout=PIPE,
                stderr=STDOUT,
                cwd=cwd,
            )
        except OSError as E:
            raise InvalidUpdate("Invalid {self.hook_option_name} configuration"
                                " ({self.hook_exe}):\n"
                                "{err_info}".format(self=self,
                                                    err_info=str(E)))

        if hook_input is not None:
            hook_input = encode_utf8(hook_input)
        out, _ = p.communicate(hook_input)

        return (self.hook_exe, p, safe_decode(out))
Esempio n. 19
0
def reject_retired_branch_update(short_ref_name, all_refs):
    """Raise InvalidUpdate if trying to update a retired branch.

    PARAMETERS:
        short_ref_name: The reference's short name (see short_ref_name
            attribute in class AbstractUpdate).
        all_refs: See the all_refs attribute in class AbstractUpdate.

    REMARKS
        By convention, retiring a branch means "moving" it to the
        "retired/" sub-namespace.  Normally, it would make better
        sense to just use a tag instead of a branch (for the reference
        in "retired/"), but we allow both.
    """
    # If short_ref_name starts with "retired/", then the user is either
    # trying to create the retired branch (which is allowed), or else
    # trying to update it (which seems suspicious).  In the latter
    # case, we could disallow it, but it could also be argued that
    # updating the retired branch is sometimes useful. Keep it simple
    # for now, and allow.
    if short_ref_name.startswith("retired/"):
        return

    retired_short_ref_name = "retired/%s" % short_ref_name
    if "refs/heads/%s" % retired_short_ref_name in all_refs:
        raise InvalidUpdate(
            "Updates to the %s branch are no longer allowed, because" % short_ref_name,
            "this branch has been retired (and renamed into `%s')."
            % retired_short_ref_name,
        )
    if "refs/tags/%s" % retired_short_ref_name in all_refs:
        raise InvalidUpdate(
            "Updates to the %s branch are no longer allowed, because" % short_ref_name,
            "this branch has been retired (a tag called `%s' has been"
            % retired_short_ref_name,
            "created in its place).",
        )
Esempio n. 20
0
    def __ensure_fast_forward(self):
        """Raise InvalidUpdate if the update is not a fast-forward update.
        """
        if is_null_rev(self.old_rev):
            # Git Notes creation, and thus necessarily a fast-forward.
            return

        # Non-fast-foward updates are characterized by the fact that
        # there is at least one commit that is accessible from the old
        # revision which would no longer be accessible from the new
        # revision.
        if git.rev_list("%s..%s" % (self.new_rev, self.old_rev)) == "":
            return

        raise InvalidUpdate('Your Git Notes are not up to date.', '',
                            'Please update your Git Notes and push again.')
def reject_commit_if_merge(commit, ref_name):
    """Raise InvalidUpdate if commit is a merge commit.

    Raises an assertion failure if commit.parent_revs is not None
    (see PARAMETERS for meore info on this parameter's type).

    PARAMETERS
        commit: A CommitInfo object.
        ref_name: The name of the reference being updated.
    """
    assert commit.parent_revs is not None
    if len(commit.parent_revs) > 1:
        raise InvalidUpdate(*(MERGE_NOT_ALLOWED_ERROR_MSG
                              % {'ref_name': ref_name,
                                 'rev': commit.rev,
                                 'subject': commit.subject}).splitlines())
Esempio n. 22
0
    def __init__(self, ref_name, old_rev, new_rev, all_refs, submitter_email):
        """The constructor.

        Also calls self.auto_sanity_check() at the end.

        PARAMETERS
            ref_name: Same as the attribute.
            old_rev: Same as the attribute.
            new_rev: Same as the attribute.
            all_refs: Same as the attribute.
            submitter_email: Same as parameter from_email in class
                EmailInfo's constructor.
                This is used to override the default "from" email when
                the user sending the emails is different from the user
                that pushed/submitted the update.
        """
        # If the repository's configuration does not provide
        # the minimum required to email update notifications,
        # refuse the update.
        if not git_config('hooks.mailinglist'):
            raise InvalidUpdate(
                'Error: hooks.mailinglist config option not set.',
                'Please contact your repository\'s administrator.')

        m = re.match(r"([^/]+/[^/]+)/(.+)", ref_name)

        self.ref_name = ref_name
        self.short_ref_name = m.group(2) if m else ref_name
        self.ref_namespace = m.group(1) if m else None
        self.old_rev = old_rev
        self.new_rev = new_rev
        self.new_rev_type = get_object_type(self.new_rev)
        self.all_refs = all_refs
        self.email_info = EmailInfo(email_from=submitter_email)

        # Implement the added_commits "attribute" as a property,
        # to allow for initialization only on-demand. This allows
        # us to avoid computing this list until the moment we
        # actually need it. To help caching its value, avoiding
        # the need to compute it multiple times, we introduce
        # a private attribute named __added_commits.
        #
        # Same treatment for the lost_commits "attribute".
        self.__added_commits = None
        self.__lost_commits = None

        self.self_sanity_check()
Esempio n. 23
0
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()

    # Do nothing if the reference is in the hooks.ignore-refs list.
    ignore_refs_match = utils.search_config_option_list("hooks.ignore-refs", ref_name)
    if ignore_refs_match is not None:
        debug(f"{ref_name} ignored due to hooks.ignore-refs" f" ({ignore_refs_match})")
        return

    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)
Esempio n. 24
0
    def validate_ref_update(self):
        """See AbstractUpdate.validate_ref_update.

        REMARKS
            This method is capable of handling both creation update.
        """
        # If lightweight tags are not allowed, refuse the update.
        if not git_config('hooks.allow-lightweight-tag'):
            raise InvalidUpdate(
                "Lightweight tags (%s) are not allowed in this repository." %
                self.human_readable_tag_name(),
                "Use 'git tag [ -a | -s ]' for tags you want to propagate.")

        # If this is a pre-existing tag being updated, there are pitfalls
        # that the user should be warned about.
        if not is_null_rev(self.old_rev) and not is_null_rev(self.new_rev):
            warn_about_tag_update(self.human_readable_tag_name(), self.old_rev,
                                  self.new_rev)
Esempio n. 25
0
def maybe_call_thirdparty_hook(hook_option_name, hook_input=None,
                               hook_args=None):
    """Call the script specified via hook_option_name if defined.

    This function checks the repository's configuration for the given
    hook_option_name. If defined, it uses that configuration as
    the name of a script that it calls with the given arguments.

    Raises InvalidUpdate if the hook is defined, but points to
    a non-existing file.

    PARAMETERS
        hook_option_name: The name of the git config option to query
            in order to get the name of the thirdparty script to call.
        hook_input: A string, containing the data to be sent to
            the script via its stdin stream. None if no data needs
            to be sent.
        hook_args: An iterable of command-line arguments to pass to
            the script. None if no arguments are needed.

    RETURN VALUE
        If the hook is defined, returns a tuple with the following elements:
          - The name of the script called as a hook;
          - The Popen object corresponding the script's execution
            (which, by the time this function returns, has finished
            executing);
          - The output of the script (stdout + stderr combined).
        Otherwise, returns None.
    """
    hook_exe = git_config(hook_option_name)
    if hook_exe is None:
        return
    if not os.path.isfile(hook_exe):
        raise InvalidUpdate(
            'Invalid {} configuration, no such file:'.format(hook_option_name),
            hook_exe)
    hook_cmd = [hook_exe]
    if hook_args is not None:
        hook_cmd.extend(hook_args)
    p = Popen(hook_cmd, stdin=PIPE if hook_input is not None else None,
              stdout=PIPE, stderr=STDOUT)
    out, _ = p.communicate(hook_input)

    return (hook_exe, p, out)
Esempio n. 26
0
    def __check_max_commit_emails(self):
        """Raise InvalidUpdate is this update will generate too many emails.
        """
        # Determine the number of commit emails that would be sent if
        # we accept the update, and raise an error if it exceeds
        # the maximum number of emails.

        self.__set_send_email_p_attr(self.new_commits_for_ref)
        nb_emails = len([commit for commit in self.new_commits_for_ref
                         if commit.send_email_p])
        max_emails = git_config('hooks.max-commit-emails')
        if nb_emails > max_emails:
            raise InvalidUpdate(
                "This update introduces too many new commits (%d),"
                " which would" % nb_emails,
                "trigger as many emails, exceeding the"
                " current limit (%d)." % max_emails,
                "Contact your repository adminstrator if you really meant",
                "to generate this many commit emails.")
Esempio n. 27
0
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)
Esempio n. 28
0
    def __init__(self, email_from):
        """The constructor.

        PARAMETERS
            email_from: If not None, a string that provides the email
                address of the sender.  Eg: 'David Smith <*****@*****.**>'.
                If None, this address is computed from the environment.
        """
        self.project_name = get_module_name()

        from_domain = git_config('hooks.from-domain')
        if not from_domain:
            raise InvalidUpdate(
                'Error: hooks.from-domain config variable not set.',
                'Please contact your repository\'s administrator.')
        if email_from is None:
            self.email_from = '%s <%s@%s>' % (get_user_full_name(),
                                              get_user_name(), from_domain)
        else:
            self.email_from = email_from
Esempio n. 29
0
def maybe_pre_receive_hook(pre_receive_data):
    """Call the pre-receive-hook if defined.

    This function implements supports for the hooks.pre-receive-hook
    config variable, by calling this function if the config variable
    is defined.

    ARGUMENTS
        pre_receive_data: The data received via stdin by the pre-receive hook.
    """
    result = maybe_call_thirdparty_hook('hooks.pre-receive-hook',
                                        hook_input=pre_receive_data)
    if result is not None:
        hook_exe, p, out = result
        if p.returncode != 0:
            raise InvalidUpdate(
                "Update rejected by this repository's hooks.pre-receive-hook"
                " script", '({}):'.format(hook_exe), *out.splitlines())
        else:
            sys.stdout.write(out)
Esempio n. 30
0
def reject_lines_too_long(commit):
    """Raise InvalidUpdate if the commit's revlog has a line that's too long.

    Does nothing if the project was configured to skip this check.

    PARAMETERS
        commit: A CommitInfo object corresponding to the commit being checked.
    """
    max_line_length = git_config('hooks.max-rh-line-length')
    if max_line_length <= 0:
        # A value of zero (or less) means that the project does not
        # want this check to be applied.  Skip it.
        return

    for line in commit.raw_revlog_lines:
        if len(line) > max_line_length:
            raise InvalidUpdate(
                'Invalid revision history for commit %s:' % commit.rev, '',
                'The following line in the revision history is too long',
                '(%d characters, when the maximum is %d characters):' %
                (len(line), max_line_length), '', '>>> %s' % line)