Exemple #1
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)
Exemple #2
0
    def test_git_config(self):
        """Unit test git_config with invalid config name...
        """
        self.enable_unit_test()

        from config import git_config, UnsupportedOptionName
        with self.assertRaises(UnsupportedOptionName):
            git_config('bad option name')
    def email_commit(self, commit):
        """Send an email describing the given commit.

        PARAMETERS
            commit: A CommitInfo object.
        """
        if self.ref_namespace in ('refs/heads', 'refs/tags'):
            if self.short_ref_name == 'master':
                branch = ''
            else:
                branch = '/%s' % self.short_ref_name
        else:
            # Unusual namespace for our reference. Use the reference
            # name in full to label the branch name.
            branch = '(%s)' % self.ref_name

        subject = '[%(repo)s%(branch)s] %(subject)s' % {
            'repo': self.email_info.project_name,
            'branch': branch,
            'subject': commit.subject[:SUBJECT_MAX_SUBJECT_CHARS],
            }

        # Generate the body of the email in two pieces:
        #   1. The commit description without the patch;
        #   2. The diff stat and patch.
        # This allows us to insert our little "Diff:" marker that
        # bugtool detects when parsing the email for filing (this
        # part is now performed by the Email class). The purpose
        # is to prevent bugtool from searching for TNs in the patch
        # itself.
        #
        # For the diff, there is one subtlelty:
        # Git commands calls strip on the output, which is usually
        # a good thing, but not in the case of the diff output.
        # Prevent this from happening by putting an artificial
        # character at the start of the format string, and then
        # by stripping it from the output.

        body = git.log(commit.rev, max_count="1") + '\n'
        if git_config('hooks.commit-url') is not None:
            url_info = {'rev': commit.rev,
                        'ref_name': self.ref_name}
            body = (git_config('hooks.commit-url') % url_info
                    + '\n\n'
                    + body)

        diff = git.show(commit.rev, p=True, M=True, stat=True,
                        pretty="format:|")[1:]

        filer_cmd = git_config('hooks.file-commit-cmd')
        if filer_cmd is not None:
            filer_cmd = shlex.split(filer_cmd)

        email = Email(self.email_info,
                      commit.email_to, subject, body, commit.author,
                      self.ref_name, commit.base_rev_for_display(),
                      commit.rev, diff, filer_cmd=filer_cmd)
        email.enqueue()
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)
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.")
    def __check_max_commit_emails(self):
        """Raise InvalidUpdate is this update will generate too many emails.
        """
        # Check that the update wouldn't generate too many commit emails.

        if self.search_config_option_list('hooks.no-emails') is not None:
            # This repository was configured to skip emails on this branch.
            # Nothing to do.
            return

        # We know that commit emails would only be sent for commits which
        # are new for the repository, so we count those.

        self.__set_commits_attr(self.added_commits, 'send_email_p',
                                'hooks.no-emails')
        nb_emails = len([commit for commit in self.added_commits
                         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.")
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,
            )
Exemple #8
0
    def __email_body_with_diff(self):
        """Return self.email_body with the diff at the end (if any).

        This attributes returns self.email_body augmentted with
        self.diff (if not None), possibly truncated to fit the
        hooks.max-email-diff-size limit, with a "diff marker"
        between email_body and diff.  The diff marker is meant
        to be used by scripts processing the contents of those
        emails but not wanting to include the diff as part of
        their processing.
        """
        email_body = self.email_body
        if self.diff is not None:
            # Append the "Diff:" marker to email_body, followed by
            # the diff. Truncate the patch if necessary.
            diff = self.diff
            max_diff_size = git_config('hooks.max-email-diff-size')
            if len(diff) > max_diff_size:
                diff = diff[:max_diff_size]
                diff += ('[...]\n\n[diff truncated at %d bytes]\n'
                         % max_diff_size)

            email_body += '\nDiff:\n'
            email_body += diff
        return email_body
Exemple #9
0
    def __email_body_with_diff(self):
        """Return self.email_body with the diff at the end (if any).

        This attributes returns self.email_body augmentted with
        self.diff (if not None), possibly truncated to fit the
        hooks.max-email-diff-size limit, with a "diff marker"
        between email_body and diff.  The diff marker is meant
        to be used by scripts processing the contents of those
        emails but not wanting to include the diff as part of
        their processing.
        """
        email_body = self.email_body
        if self.diff is not None:
            # Append the "Diff:" marker to email_body, followed by
            # the diff. Truncate the patch if necessary.
            diff = self.diff
            max_diff_size = git_config('hooks.max-email-diff-size')
            if len(diff) > max_diff_size:
                diff = diff[:max_diff_size]
                diff += ('[...]\n\n[diff truncated at %d bytes]\n' %
                         max_diff_size)

            email_body += '\nDiff:\n'
            email_body += diff
        return email_body
Exemple #10
0
    def search_config_option_list(self, option_name, ref_name=None):
        """Search the given config option as a list, and return the first match.

        This function first extracts the value of the given config,
        expecting it to be a list of regular expressions.  It then
        iterates over that list until it finds one that matches
        REF_NAME.

        PARAMETERS
            option_name: The name of the config option to be using
                as the source for our list of regular expressions.
            ref_name: The name of the reference used for the search.
                If None, use self.ref_name.

        RETURN VALUE
            The first regular expression matching REF_NAME, or None.
        """
        if ref_name is None:
            ref_name = self.ref_name
        ref_re_list = git_config(option_name)
        for ref_re in ref_re_list:
            ref_re = ref_re.strip()
            if ref_matches_regexp(ref_name, ref_re):
                return ref_re
        return None
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)
Exemple #12
0
    def __check_max_commit_emails(self):
        """Raise InvalidUpdate is this update will generate too many emails.
        """
        # Check that the update wouldn't generate too many commit emails.

        if self.search_config_option_list('hooks.no-emails') is not None:
            # This repository was configured to skip emails on this branch.
            # Nothing to do.
            return

        # We know that commit emails would only be sent for commits which
        # are new for the repository, so we count those.

        self.__set_send_email_p_attr(self.added_commits)
        nb_emails = len(
            [commit for commit in self.added_commits 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.")
Exemple #13
0
    def __do_style_checks(self):
        if self.search_config_option_list('hooks.no-style-checks') is not None:
            # The respository has been configured to disable all style
            # checks on this branch.
            debug('no style check on this branch (hooks.no-style-checks)')
            return

        added = self.__added_commits
        if git_config('hooks.combined-style-checking'):
            # This project prefers to perform the style check on
            # the cumulated diff, rather than commit-per-commit.
            # Behave as if the update only added one commit (new_rev),
            # with a single parent being old_rev.  If old_rev is nul
            # (branch creation), then use the first parent of the oldest
            # added commit.
            debug('(combined style checking)')
            if not added[-1].pre_existing_p:
                base_rev = (added[0].base_rev_for_git()
                            if is_null_rev(self.old_rev) else self.old_rev)
                style_check_commit(base_rev, self.new_rev,
                                   self.email_info.project_name)
        else:
            debug('(commit-per-commit style checking)')
            # Perform the pre-commit checks, as needed...
            for commit in added:
                if not commit.pre_existing_p:
                    style_check_commit(commit.base_rev_for_git(), commit.rev,
                                       self.email_info.project_name)
Exemple #14
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))
Exemple #15
0
    def __email_ref_update(self):
        """Send the email describing to the reference update.

        This email can be seen as a "cover email", or a quick summary
        of the update that was performed.

        REMARKS
            The hooks may decide that such an email may not be necessary,
            and thus send nothing. See self.get_update_email_contents
            for more details.
        """
        update_email_contents = self.get_update_email_contents()

        if update_email_contents is not None:
            (email_to, subject, body) = update_email_contents
            email_bcc = (
                git_config("hooks.filer-email")
                if self.send_cover_email_to_filer
                else None
            )
            update_email = Email(
                self.email_info,
                email_to,
                email_bcc,
                subject,
                body,
                None,
                self.ref_name,
                self.old_rev,
                self.new_rev,
            )
            update_email.enqueue()
    def search_config_option_list(self, option_name, ref_name=None):
        """Search the hooks.no-emails list, and returns the first match.

        This function first extracts the value of the given config,
        expecting it to be a list of regular expressions.  It then
        iterates over that list until it finds one that matches
        REF_NAME.

        PARAMETERS
            option_name: The name of the config option to be using
                as the source for our list of regular expressions.
            ref_name: The name of the reference used for the search.
                If None, use self.ref_name.

        RETURN VALUE
            The first regular expression matching REF_NAME, or None.
        """
        if ref_name is None:
            ref_name = self.ref_name
        ref_re_list = git_config(option_name)
        for ref_re in ref_re_list:
            ref_re = ref_re.strip()
            if re.match(ref_re, ref_name):
                return ref_re
        return None
Exemple #17
0
def expanded_mailing_list(ref_name, get_files_changed_cb):
    """Return the list of emails after expanding the hooks.mailinglist config.

    This function iterates over all entries in hooks.mailinglist, and
    replaces all entries which are a script by the result of calling
    that script with the list of changed files.

    PARAMETERS
        ref_name: The name of the reference being updated.
        get_files_changed_cb: A function to call in order to get
            the list of files changed, to be passed to mailinglist
            scripts.  This is a function rather than a list to allow
            us to compute that list of files only if needed.
            None for no file changed.
    """
    result = []
    files_changed = () if get_files_changed_cb is None else None

    for entry in git_config("hooks.mailinglist"):
        if is_mailinglist_script(entry):
            if files_changed is None:
                files_changed = get_files_changed_cb()
            result.extend(
                get_emails_from_script(entry, ref_name, files_changed))
        else:
            result.append(entry)
    return result
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],
        ])
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,
    ])
Exemple #20
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()
Exemple #21
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")
Exemple #22
0
    def email_commit(self, commit):
        """See AbstractUpdate.email_commit."""
        notes = GitNotes(commit.rev)

        # Get commit info for the annotated commit
        annotated_commit = commit_info_list("-1", notes.annotated_rev)[0]

        # Get a description of the annotated commit (a la "git show"),
        # except that we do not want the diff.
        #
        # Also, we have to handle the notes manually, as the commands
        # get the notes from the HEAD of the notes/commits branch,
        # whereas what we needs is the contents at the commit.rev.
        # This makes a difference when a single push updates the notes
        # of the same commit multiple times.
        annotated_rev_log = git.log(annotated_commit.rev,
                                    no_notes=True,
                                    max_count="1")
        notes_contents = (None if notes.contents is None else indent(
            notes.contents, ' ' * 4))

        # Determine subject tag based on ref name:
        #   * remove "refs/notes" prefix
        #   * remove entire tag if remaining component is "commits"
        #     (case of the default refs/notes/commits ref)
        notes_ref = self.ref_name.split('/', 2)[2]
        if notes_ref == "commits":
            subject_tag = ""
        else:
            subject_tag = "(%s)" % notes_ref

        subject = '[notes%s][%s] %s' % (subject_tag,
                                        self.email_info.project_name,
                                        annotated_commit.subject)

        body_template = (DELETED_NOTES_COMMIT_EMAIL_BODY_TEMPLATE
                         if notes_contents is None else
                         UPDATED_NOTES_COMMIT_EMAIL_BODY_TEMPLATE)
        body = body_template % {
            'annotated_rev_log': annotated_rev_log,
            'notes_contents': notes_contents,
        }

        # Git commands calls strip on the output, which is usually
        # a good thing, but not in the case of the diff output.
        # Prevent this from happening by putting an artificial
        # character at the start of the format string, and then
        # by stripping it from the output.
        diff = git.show(commit.rev, pretty="format:|", p=True)[1:]

        email_bcc = git_config('hooks.filer-email')

        email = Email(self.email_info,
                      annotated_commit.email_to(self.ref_name), email_bcc,
                      subject, body, commit.author, self.ref_name,
                      commit.base_rev_for_display(), commit.rev, diff)
        email.enqueue()
    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")
Exemple #24
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())
Exemple #25
0
def maybe_post_receive_hook(post_receive_data):
    """Call the post-receive-hook is required.

    This function implements supports for the hooks.post-receive-hook
    config variable, by calling this function if the config variable
    is defined.
    """
    hook_exe = git_config('hooks.post-receive-hook')
    if hook_exe is None:
        return
    p = Popen([hook_exe], stdin=PIPE, stdout=sys.stdout, stderr=STDOUT)
    p.communicate(post_receive_data)
    if p.returncode != 0:
        warn('!!! WARNING: %s returned code: %d.' % (hook_exe, p.returncode))
Exemple #26
0
    def send(self):
        """Perform all send operations related to this email...

        These consists in:
            - send the notification email;
            - call self.filer_cmd if not None.

        REMARKS
            If the GIT_HOOKS_TESTSUITE_MODE environment variable
            is set, then a trace of the email is printed, instead
            of sending it.  This is for testing purposes.
        """
        e_msg = MIMEText(self.__email_body_with_diff)

        # Create the email's header.
        e_msg['From'] = self.email_info.email_from
        e_msg['To'] = ', '.join(map(strip, self.email_to))
        # Bcc FILER_EMAIL, but only in testsuite mode.  This allows us
        # to turn this feature off by default, while still testing it.
        # That's because this is an AdaCore-specific feature which is
        # otherwise on by default, and we do not want to non-AdaCore
        # projects to send emails to AdaCore by accident.
        if (('GIT_HOOKS_TESTSUITE_MODE' in os.environ
             and self.send_to_filer
             and git_config('hooks.bcc-file-ci'))):
            e_msg['Bcc'] = FILER_EMAIL
        e_msg['Subject'] = self.email_subject
        e_msg['X-Act-Checkin'] = self.email_info.project_name
        e_msg['X-Git-Author'] = self.author or self.email_info.email_from
        e_msg['X-Git-Refname'] = self.ref_name
        e_msg['X-Git-Oldrev'] = self.old_rev
        e_msg['X-Git-Newrev'] = self.new_rev

        # email_from = e_msg.get('From')
        email_recipients = [addr[1] for addr
                            in getaddresses(e_msg.get_all('To', [])
                                            + e_msg.get_all('Cc', [])
                                            + e_msg.get_all('Bcc', []))]

        if 'GIT_HOOKS_TESTSUITE_MODE' in os.environ:
            # Use debug level 0 to make sure that the trace is always
            # printed.
            debug(e_msg.as_string(), level=0)
        else:  # pragma: no cover (do not want real emails during testing)
            sendmail(self.email_info.email_from, email_recipients,
                     e_msg.as_string(), 'localhost')

        if self.filer_cmd is not None:
            self.__call_filer_cmd()
Exemple #27
0
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 __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()
Exemple #29
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()
    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.short_ref_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.short_ref_name,
                                  self.old_rev, self.new_rev)
Exemple #31
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)
Exemple #32
0
def git_show_ref(*args):
    """Call "git show-ref [args]" and return the result as a dictionary.

    The key of the dictionary is the reference name, and the value
    is a string containing the reference's rev (SHA1).

    This function assumes that all arguments are valid, and
    the usual CalledProcessError will be raised if not.

    PARAMETERS
        *args: Each argument is passed to the "git show-ref"
            as a pattern.

    RETURN VALUE
        A dictionary of references that matched the given patterns,
        minus the references matching the hooks.ignore-refs config.
    """
    # We cannot import that at module level, because module config
    # actually depends on this module.  So we import it here instead.
    from config import git_config

    matching_refs = git.show_ref(*args, _split_lines=True)
    result = {}
    for ref_info in matching_refs:
        rev, ref = ref_info.split(None, 2)
        result[ref] = rev

    # Remove all references which matching the hooks.ignore-refs config.
    #
    # It would probably have been more efficient to check the reference
    # against the exclusion list before adding them to the dictionary.
    # I felt that the resulting code was harder to read.  Given the
    # typical number of entries, the impact should be barely measurable.
    ignore_refs_list = [
        regex.strip() for regex in git_config('hooks.ignore-refs')
    ]

    for ref_name in result.keys():
        for ignore_ref_re in ignore_refs_list:
            if re.match(ignore_ref_re, ref_name):
                del result[ref_name]
                break

    return result
Exemple #33
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)
Exemple #34
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.")
Exemple #35
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
Exemple #36
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
def reject_unedited_merge_commit(rev, raw_rh):
    """Raise InvalidUpdate if raw_rh looks like an unedited merge commit's RH.

    More precisely, we are trying to catch the cases where a merge
    was performed without the user being aware of it.  This can
    happen for instance if the user typed "git pull" instead of
    "git pull --rebase".

    We implement a very crude identification mechanism at the moment,
    based on matching the default revision history for merge commits.

    If the merge commit was intended, the user is expected to provide
    a non-default revision history, thus satisfying 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.
    """
    if git_config('hooks.disable-merge-commit-checks'):
        # The users of this repository do not want this safety guard.
        # So do not perform this check.
        return

    # We have seen cases (with git version 1.7.10.4), where the default
    # revision history for a merge commit is just: "Merge branch 'xxx'.".
    RH_PATTERN = "Merge branch '.*'"

    for line in raw_rh:
        if re.match(RH_PATTERN, line):
            info = ['Pattern "%s" has been detected.' % RH_PATTERN,
                    '(in commit %s)' % rev,
                    '',
                    'This usually indicates an unintentional merge commit.',
                    'If you would really like to push a merge commit,'
                    ' please',
                    "edit the merge commit's revision history."]
            raise InvalidUpdate(*info)
def reject_unedited_merge_commit(rev, raw_rh):
    """Raise InvalidUpdate if raw_rh looks like an unedited merge commit's RH.

    More precisely, we are trying to catch the cases where a merge
    was performed without the user being aware of it.  This can
    happen for instance if the user typed "git pull" instead of
    "git pull --rebase".

    We implement a very crude identification mechanism at the moment,
    based on matching the default revision history for merge commits.

    If the merge commit was intended, the user is expected to provide
    a non-default revision history, thus satisfying 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.
    """
    if git_config('hooks.disable-merge-commit-checks'):
        # The users of this repository do not want this safety guard.
        # So do not perform this check.
        return

    # We have seen cases (with git version 1.7.10.4), where the default
    # revision history for a merge commit is just: "Merge branch 'xxx'.".
    RH_PATTERN = "Merge branch '.*'"

    for line in raw_rh:
        if re.match(RH_PATTERN, line):
            info = ['Pattern "%s" has been detected.' % RH_PATTERN,
                    '(in commit %s)' % rev,
                    '',
                    'This usually indicates an unintentional merge commit.',
                    'If you would really like to push a merge commit,'
                    ' please',
                    "edit the merge commit's revision history."]
            raise InvalidUpdate(*info)
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 'minor' (as in "Minor reformatting")
        # anywhere in the RH removes the need for a TN
        # in the RH.
        r'\bminor\b',
        # Same for '(no-tn-check)'.
        r'\(no-tn-check\)',
        # TN regexp.
        '[0-9A-Z][0-9A-Z][0-9][0-9]-[0-9A-Z][0-9][0-9]',
        ]
    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',
        'either the word "Minor" or the "(no-tn-check)" string',
        'in place of a ticket number.',
        '',
        'commit %s' % rev,
        'Subject: %s' % raw_rh[0],
        ])
Exemple #40
0
def debug(msg, level=1):
    """Print a debug message on stderr if appropriate.

    The debug trace is generated if the debug level is greater or
    equal to the given trace priority.

    The debug level is an integer value which can be changed either
    by setting the `GIT_HOOKS_DEBUG_LEVEL' environment variable, or else
    by setting the hooks.debug-level git config value.  The value
    must be an integer value, or this function raises InvalidUpdate.
    By default, the debug level is set to zero (no debug traces).


    PARAMETERS
        msg: The debug message to be printed.  The message will be
            prefixed with "DEBUG: " (an indentation proportional to
            the level will be used).
        level: The trace level. The smaller the number, the important
            the trace message. Traces that are repetitive, or part
            of a possibly large loop, or less important, should use
            a value that is higher than 1.

    REMARKS
        Raising InvalidUpdate for an invalid debug level value is
        a little abusive.  But it simplifies a bit the update script,
        which then only has to handle a single exception...
    """
    if 'GIT_HOOKS_DEBUG_LEVEL' in environ:
        debug_level = environ['GIT_HOOKS_DEBUG_LEVEL']
        if not debug_level.isdigit():
            raise InvalidUpdate('Invalid value for GIT_HOOKS_DEBUG_LEVEL: %s '
                                '(must be integer)' % debug_level)
        debug_level = int(debug_level)
    else:
        debug_level = git_config('hooks.debug-level')

    if debug_level >= level:
        warn(msg, prefix='  ' * (level - 1) + 'DEBUG: ')
Exemple #41
0
def debug(msg, level=1):
    """Print a debug message on stderr if appropriate.

    The debug trace is generated if the debug level is greater or
    equal to the given trace priority.

    The debug level is an integer value which can be changed either
    by setting the `GIT_HOOKS_DEBUG_LEVEL' environment variable, or else
    by setting the hooks.debug-level git config value.  The value
    must be an integer value, or this function raises InvalidUpdate.
    By default, the debug level is set to zero (no debug traces).


    PARAMETERS
        msg: The debug message to be printed.  The message will be
            prefixed with "DEBUG: " (an indentation proportional to
            the level will be used).
        level: The trace level. The smaller the number, the important
            the trace message. Traces that are repetitive, or part
            of a possibly large loop, or less important, should use
            a value that is higher than 1.

    REMARKS
        Raising InvalidUpdate for an invalid debug level value is
        a little abusive.  But it simplifies a bit the update script,
        which then only has to handle a single exception...
    """
    if 'GIT_HOOKS_DEBUG_LEVEL' in environ:
        debug_level = environ['GIT_HOOKS_DEBUG_LEVEL']
        if not debug_level.isdigit():
            raise InvalidUpdate('Invalid value for GIT_HOOKS_DEBUG_LEVEL: %s '
                                '(must be integer)' % debug_level)
        debug_level = int(debug_level)
    else:
        debug_level = git_config('hooks.debug-level')

    if debug_level >= level:
        warn(msg, prefix='  ' * (level - 1) + 'DEBUG: ')
def reject_unedited_merge_commit(commit):
    """Raise InvalidUpdate if the commit looks like an unedited merge commit.

    More precisely, we are trying to catch the cases where a merge
    was performed without the user being aware of it.  This can
    happen for instance if the user typed "git pull" instead of
    "git pull --rebase".

    We implement a very crude identification mechanism at the moment,
    based on matching the default revision history for merge commits.

    If the merge commit was intended, the user is expected to provide
    a non-default revision history, thus satisfying this check.

    PARAMETERS
        commit: A CommitInfo object corresponding to the commit being checked.
    """
    if git_config("hooks.disable-merge-commit-checks"):
        # The users of this repository do not want this safety guard.
        # So do not perform this check.
        return

    # We have seen cases (with git version 1.7.10.4), where the default
    # revision history for a merge commit is just: "Merge branch 'xxx'.".
    RH_PATTERN = "Merge branch '.*'"

    for line in commit.raw_revlog_lines:
        if re.match(RH_PATTERN, line):
            info = [
                'Pattern "%s" has been detected.' % RH_PATTERN,
                "(in commit %s)" % commit.rev,
                "",
                "This usually indicates an unintentional merge commit.",
                "If you would really like to push a merge commit,"
                " please",
                "edit the merge commit's revision history.",
            ]
            raise InvalidUpdate(*info)
Exemple #43
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 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 branches where
    # such an update is allowed.
    ok_branches = git_config('hooks.allow-non-fast-forward')

    for branch in ["refs/heads/" + branch.strip()
                   for branch in ok_branches + FORCED_UPDATE_OK_BRANCHES]:
        if re.match(branch, 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.
    raise InvalidUpdate(
        'Non-fast-forward updates are not allowed on this branch;',
        'Please rebase your changes on top of the latest HEAD,',
        'and then try pushing again.')
def check_file(filename, sha1, commit_rev, project_name):
    """Check a file for style violations if appropriate.

    Raise InvalidUpdate if one or more style violations are detected.

    PARAMETERS
        filename: The name of the file to check.
        sha1: The sha1 of the file to be checked.
        commit_rev: The associated commit sha1.  This piece of information
            helps us find the correct version of the .gitattributes files,
            in order to determine whether pre-commit-checks should be
            applied or not.
        project_name: The name of the project (same as the attribute
            in updates.emails.EmailInfo).
    """
    debug("check_file (filename=`%s', sha1=%s)" % (filename, sha1), level=3)

    # Determine whether this file has the no-precommit-check attribute
    # set, in which case style-checks should not be performed.
    if git_attribute(commit_rev, filename, 'no-precommit-check') == 'set':
        debug('no-precommit-check: %s commit_rev=%s'
              % (filename, commit_rev))
        syslog('Pre-commit checks disabled for %(rev)s on %(repo)s'
               ' (%(file)s) by repo attribute'
               % {'rev': sha1,
                  'repo': project_name,
                  'file': filename,
                  })
        return

    # Get a copy of the file and save it in our scratch dir.
    # In order to allow us to call the style-checker using
    # the full path (from the project's root directory) of
    # the file being checked, we re-create the path to that
    # filename, and then copy the file at that same path.
    #
    # Providing the path as part of the filename argument is useful,
    # because it allows the messages printed by the style-checker
    # to be unambiguous in the situation where the same project
    # has multiple files sharing the same name. More generally,
    # it can also be useful to quickly locate a file in the project
    # when trying to make the needed corrections outlined by the
    # style-checker.
    path_to_filename = "%s/%s" % (utils.scratch_dir,
                                  os.path.dirname(filename))
    if not os.path.exists(path_to_filename):
        os.makedirs(path_to_filename)
    git.show(sha1, _outfile="%s/%s" % (utils.scratch_dir, filename))

    # Call the style-checker.

    # For testing purposes, provide a back-door allowing the user
    # to override the style-checking program to be used.  That way,
    # the testsuite has a way to control what the program returns,
    # and easily test all execution paths without having to maintain
    # some sources specifically designed to trigger the various
    # error conditions.
    if 'GIT_HOOKS_STYLE_CHECKER' in os.environ:
        style_checker = os.environ['GIT_HOOKS_STYLE_CHECKER']
    else:
        style_checker = git_config('hooks.style-checker')

    # ??? It appears that cvs_check, the official style-checker,
    # requires the SVN path of the file to be checked as the first
    # argument. Not sure why, but that does not really apply in
    # our context. Use `trunk/<module>/<path>' to work around
    # the issue.
    style_checker_args = ['trunk/%s/%s' % (project_name, filename),
                          filename]

    try:
        # In order to allow the style-checker to be a script, we need to
        # run it through a shell.  But when we do so, the Popen class no
        # longer allows us to pass the arguments as a list.  In order to
        # avoid problems with spaces or special characters, we quote the
        # arguments as needed.
        quoted_args = [quote(arg) for arg in style_checker_args]
        out = check_output('%s %s' % (style_checker, ' '.join(quoted_args)),
                           shell=True, cwd=utils.scratch_dir, stderr=STDOUT)

        # If we reach this point, it means that the style-checker returned
        # zero (success). Print any output, it might be a non-fatal
        # warning.
        if out:
            warn(*out.splitlines())

    except subprocess.CalledProcessError, E:
        debug(str(E), level=4)
        info = (
            ["pre-commit check failed for file `%s' at commit: %s"
             % (filename, commit_rev)]
            + E.output.splitlines())
        raise InvalidUpdate(*info)
    def pre_commit_checks(self):
        """Run the pre-commit checks on this update's new commits.

        Determine the list of new commits introduced by this
        update, and perform the pre-commit-checks on them as
        appropriate.  Raise InvalidUpdate if one or more style
        violation was detected.
        """
        if is_null_rev(self.new_rev):
            # We are deleting a reference, so there cannot be any
            # new commit.
            return

        # Check to see if any of the entries in hooks.no-precommit-check
        # might be matching our reference name...
        for exp in git_config('hooks.no-precommit-check'):
            exp = exp.strip()
            if re.match(exp, self.ref_name):
                # Pre-commit checks are explicitly disabled on this branch.
                debug("(hooks.no-precommit-check match: `%s')" % exp)
                syslog('Pre-commit checks disabled for %(rev)s on %(repo)s'
                       ' by hooks.no-precommit-check config (%(ref_name)s)'
                       % {'rev': self.new_rev,
                          'repo': self.email_info.project_name,
                          'ref_name': self.ref_name,
                          })
                return

        if self.__no_cvs_check_user_override():
            # Just return. All necessary traces have already been
            # handled by the __no_cvs_check_user_override method.
            return

        added = self.added_commits
        if not added:
            # There are no new commits, so nothing further to check.
            return

        # Determine whether we should be doing RH style checking...
        do_rh_style_checks = (
            self.search_config_option_list('hooks.no-rh-style-checks')
            is None)

        # Perform the revision-history of all new commits, unless
        # specifically disabled by configuration.
        #
        # Done separately from the rest of the pre-commit checks,
        # which check the files changed by the commits, because of
        # the case where hooks.combined-style-checking is true;
        # we do not want to forget checking the revision history
        # of some of the commits.
        if do_rh_style_checks:
            for commit in added:
                if not commit.pre_existing_p:
                    check_revision_history(commit.rev)

        reject_merge_commits = (
            self.search_config_option_list('hooks.reject-merge-commits')
            is not None)
        if reject_merge_commits:
            for commit in added:
                reject_commit_if_merge(commit, self.ref_name)

        # Perform the filename-collision checks.  These collisions
        # can cause a lot of confusion and fustration to the users,
        # so do not provide the option of doing the check on the
        # final commit only (following hooks.combined-style-checking).
        # Do it on all new commits.
        for commit in added:
            if not commit.pre_existing_p:
                check_filename_collisions(commit.rev)

        if git_config('hooks.combined-style-checking'):
            # This project prefers to perform the style check on
            # the cumulated diff, rather than commit-per-commit.
            # Behave as if the update only added one commit (new_rev),
            # with a single parent being old_rev.  If old_rev is nul
            # (branch creation), then use the first parent of the oldest
            # added commit.
            debug('(combined style checking)')
            if not added[-1].pre_existing_p:
                base_rev = (
                    added[0].base_rev_for_git() if is_null_rev(self.old_rev)
                    else self.old_rev)
                check_commit(base_rev, self.new_rev,
                             self.email_info.project_name)
        else:
            debug('(commit-per-commit style checking)')
            # Perform the pre-commit checks, as needed...
            for commit in added:
                if not commit.pre_existing_p:
                    check_commit(commit.base_rev_for_git(), commit.rev,
                                 self.email_info.project_name)