Пример #1
0
    def __set_send_email_p_attr(self, commit_list):
        # Make sure we have at least one commit in the list.  Otherwise,
        # nothing to do.
        if not commit_list:
            return

        # Determine the list of commits accessible from NEW_REV, after
        # having excluded all commits accessible from the branches
        # matching the hooks.no-emails hooks config.  These are the
        # non-excluded commits, ie the comments whose attribute
        # should be set to True.
        exclude = [
            '^%s' % ref_name
            for ref_name in self.get_refs_matching_config('hooks.no-emails')
        ]
        base_rev = commit_list[0].base_rev_for_display()
        if base_rev is not None:
            # Also reduce the list already present in this branch
            # prior to the update.
            exclude.append('^%s' % base_rev)
        included_refs = git.rev_list(self.new_rev, *exclude, _split_lines=True)

        # Also, we always send emails for first-parent commits.
        # This is useful in the following scenario:
        #
        #   - The repository has a topic/feature branch for which
        #     there is a no-email configuration;
        #   - When ready to "merge" his topic/feature branch to master,
        #     the user first rebases it, and pushes the new topic/feature
        #     branch. Because the push is for a no-emails branch, commit
        #     emails are turned off (as expected).
        #   - The user then merges the topic/feature branch into
        #     master, and pushes the new master branch.
        #
        # Unless we ignore the hooks.no-emails config for first-parent
        # commits, we end up never sending any commit emails for the
        # commits being pushed on master. This is not what we want,
        # here, because the commits aren't really comming from an
        # external source.  We want to disable commit emails while
        # the commits are being worked on the topic/feature branch,
        # but as soon as they make it to tracked branches, a commit
        # email should be sent.
        first_parents_expr = [self.new_rev]
        if base_rev is not None:
            first_parents_expr.append('^%s' % base_rev)
        first_parents = git.rev_list(*first_parents_expr,
                                     first_parent=True,
                                     _split_lines=True)

        for commit in commit_list:
            commit.send_email_p = (commit.rev in included_refs
                                   or commit.rev in first_parents)
Пример #2
0
def commit_info_list(*args):
    """Return a list of CommitInfo objects in chronological order.

    PARAMETERS
        Same as in the "git rev-list" command.
    """
    rev_info_in_bytes = git.rev_list(*args,
                                     pretty="format:%P%n%an%n%ae%n%s",
                                     _split_lines=True,
                                     reverse=True)
    # Each commit should generate 5 lines of output.
    assert len(rev_info_in_bytes) % 5 == 0

    rev_info = [safe_decode(b) for b in rev_info_in_bytes]

    result = []
    while rev_info:
        commit_keyword, rev = rev_info.pop(0).split(None, 1)
        parents = rev_info.pop(0).split()
        author_name = rev_info.pop(0)
        author_email = rev_info.pop(0)
        subject = rev_info.pop(0)
        assert commit_keyword == "commit"
        result.append(
            CommitInfo(rev, author_name, author_email, subject, parents))

    return result
Пример #3
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())
Пример #4
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.')
Пример #5
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.')
Пример #6
0
def commit_info_list(*args):
    """Return a list of CommitInfo objects in chronological order.

    PARAMETERS
        Same as in the "git rev-list" command.
    """
    rev_info = git.rev_list(*args, pretty='format:%P%n%an <%ae>%n%s',
                            _split_lines=True, reverse=True)
    # Each commit should generate 4 lines of output.
    assert len(rev_info) % 4 == 0

    result = []
    while rev_info:
        commit_keyword, rev = rev_info.pop(0).split(None, 1)
        parents = rev_info.pop(0).split()
        author = rev_info.pop(0)
        subject = rev_info.pop(0)
        assert commit_keyword == 'commit'
        result.append(CommitInfo(rev, author, subject, parents))

    return result
Пример #7
0
def commit_info_list(*args):
    """Return a list of CommitInfo objects in chronological order.

    PARAMETERS
        Same as in the "git rev-list" command.
    """
    rev_info = git.rev_list(*args, pretty='format:%P%n%an <%ae>%n%s',
                            _split_lines=True, reverse=True)
    # Each commit should generate 4 lines of output.
    assert len(rev_info) % 4 == 0

    result = []
    while rev_info:
        commit_keyword, rev = rev_info.pop(0).split(None, 1)
        parents = rev_info.pop(0).split()
        author = rev_info.pop(0)
        subject = rev_info.pop(0)
        assert commit_keyword == 'commit'
        result.append(CommitInfo(rev, author, subject, parents))

    return result
Пример #8
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.')
Пример #9
0
    def __set_commits_attr(self, commit_list, attr_name,
                           exclude_config_name):
        # Make sure we have at least one commit in the list.  Otherwise,
        # nothing to do.
        if not commit_list:
            return

        # Determine the list of commits accessible from NEW_REV, after
        # having excluded all commits accessible from the branches
        # matching the exclude_config_name option.  These are the
        # non-excluded commits, ie the comments whose attribute
        # should be set to True.
        exclude = ['^%s' % ref_name for ref_name
                   in self.get_refs_matching_config(exclude_config_name)]
        base_rev = commit_list[0].base_rev_for_display()
        if base_rev is not None:
            # Also reduce the list already present in this branch
            # prior to the update.
            exclude.append('^%s' % base_rev)
        included_refs = git.rev_list(self.new_rev, *exclude,
                                     _split_lines=True)

        for commit in commit_list:
            setattr(commit, attr_name, commit.rev in included_refs)
Пример #10
0
    def __get_added_commits(self):
        """Return a list of CommitInfo objects added by our update.

        RETURN VALUE
            A list of CommitInfo objects, or the empty list if
            the update did not introduce any new commit.
        """
        if is_null_rev(self.new_rev):
            return []

        # Compute the list of commits that are not accessible from
        # any of the references.  These are the commits which are
        # new in the repository.
        #
        # Note that we do not use the commit_info_list function for
        # that, because we only need the commit hashes, and a list
        # of commit hashes is more convenient for what we want to do
        # than a list of CommitInfo objects.

        exclude = [
            '^%s' % self.all_refs[ref_name]
            for ref_name in self.all_refs.keys() if ref_name != self.ref_name
        ]
        if not is_null_rev(self.old_rev):
            exclude.append('^%s' % self.old_rev)

        new_repo_revs = git.rev_list(self.new_rev,
                                     *exclude,
                                     reverse=True,
                                     _split_lines=True)

        # If this is a reference creation (base_rev is null), try to
        # find a commit which can serve as base_rev.  We try to find
        # a pre-existing commit making the base_rev..new_rev list
        # as short as possible.
        base_rev = self.old_rev
        if is_null_rev(base_rev):
            if len(new_repo_revs) > 0:
                # The ref update brings some new commits.  The first
                # parent of the oldest of those commits, if it exists,
                # seems like a good candidate.  If it does not exist,
                # we are pushing an entirely new headless branch, and
                # base_rev should remain null.
                parents = commit_parents(new_repo_revs[0])
                if parents:
                    base_rev = parents[0]
            else:
                # This reference update does not bring any new commits
                # at all. This means new_rev is already accessible
                # through one of the references, thus making it a good
                # base_rev as well.
                base_rev = self.new_rev

        # Expand base_rev..new_rev to compute the list of commits which
        # are new for the reference.  If there is no actual base_rev
        # (Eg. a headless branch), then expand to all commits accessible
        # from that reference.
        if not is_null_rev(base_rev):
            commit_list = commit_info_list(self.new_rev, '^%s' % base_rev)
            base_rev = commit_rev(base_rev)
        else:
            commit_list = commit_info_list(self.new_rev)
            base_rev = None

        # Iterate over every commit, and set their pre_existing_p attribute.
        for commit in commit_list:
            commit.pre_existing_p = commit.rev not in new_repo_revs

        debug('update base: %s' % base_rev)

        return commit_list
Пример #11
0
def rev_list_range(start, end):
    """Return a (SHA-1, summary) pairs between start and end."""
    revrange = '%s..%s' % (start, end)
    raw_revs = git.rev_list(revrange, pretty='oneline')
    return parse_rev_list(raw_revs)
Пример #12
0
    def __get_added_commits(self):
        """Return a list of CommitInfo objects added by our update.

        RETURN VALUE
            A list of CommitInfo objects, or the empty list if
            the update did not introduce any new commit.
        """
        if is_null_rev(self.new_rev):
            return []

        # Compute the list of commits that are not accessible from
        # any of the references.  These are the commits which are
        # new in the repository.
        #
        # Note that we do not use the commit_info_list function for
        # that, because we only need the commit hashes, and a list
        # of commit hashes is more convenient for what we want to do
        # than a list of CommitInfo objects.

        exclude = ['^%s' % self.all_refs[ref_name]
                   for ref_name in self.all_refs.keys()
                   if ref_name != self.ref_name]
        if not is_null_rev(self.old_rev):
            exclude.append('^%s' % self.old_rev)

        new_repo_revs = git.rev_list(self.new_rev, *exclude, reverse=True,
                                     _split_lines=True)

        # If this is a reference creation (base_rev is null), try to
        # find a commit which can serve as base_rev.  We try to find
        # a pre-existing commit making the base_rev..new_rev list
        # as short as possible.
        base_rev = self.old_rev
        if is_null_rev(base_rev):
            if len(new_repo_revs) > 0:
                # The ref update brings some new commits.  The first
                # parent of the oldest of those commits, if it exists,
                # seems like a good candidate.  If it does not exist,
                # we are pushing an entirely new headless branch, and
                # base_rev should remain null.
                parents = commit_parents(new_repo_revs[0])
                if parents:
                    base_rev = parents[0]
            else:
                # This reference update does not bring any new commits
                # at all. This means new_rev is already accessible
                # through one of the references, thus making it a good
                # base_rev as well.
                base_rev = self.new_rev

        # Expand base_rev..new_rev to compute the list of commits which
        # are new for the reference.  If there is no actual base_rev
        # (Eg. a headless branch), then expand to all commits accessible
        # from that reference.
        if not is_null_rev(base_rev):
            commit_list = commit_info_list(self.new_rev, '^%s' % base_rev)
            base_rev = commit_rev(base_rev)
        else:
            commit_list = commit_info_list(self.new_rev)
            base_rev = None

        # Iterate over every commit, and set their pre_existing_p attribute.
        for commit in commit_list:
            commit.pre_existing_p = commit.rev not in new_repo_revs

        debug('update base: %s' % base_rev)

        return commit_list
Пример #13
0
    def __set_send_email_p_attr(self, commit_list):
        # Make sure we have at least one commit in the list.  Otherwise,
        # nothing to do.
        if not commit_list:
            return

        # If the reference being updated matches the hooks.no-emails,
        # send the send_email_p attribute to False for all commits
        # and return.
        if self.search_config_option_list("hooks.no-emails") is not None:
            for commit in commit_list:
                commit.send_email_p = False
            return

        # If the reference being updated matches the
        # hooks.email-new-commits-only config option, then only select
        # the commits which are new to this repository.
        if self.search_config_option_list("hooks.email-new-commits-only") is not None:
            exclude = [
                "^%s" % ref_name
                for ref_name in self.all_refs.keys()
                if ref_name != self.ref_name
            ]
            base_rev = commit_list[0].base_rev_for_display()
            if base_rev is not None:
                exclude.append("^%s" % base_rev)
            included_refs = git.rev_list(
                self.new_rev, *exclude, _split_lines=True, _decode=True
            )

            for commit in commit_list:
                commit.send_email_p = commit.rev in included_refs

            return

        # If we reach this point, we are in the standard situation,
        # where we want to send emails for commits which are new
        # to the reference, minus the commits which are present in
        # references matching the hooks.no-emails config option.

        # Determine the list of commits accessible from NEW_REV, after
        # having excluded all commits accessible from the branches
        # matching the hooks.no-emails hooks config.  These are the
        # non-excluded commits, ie the comments whose attribute
        # should be set to True.
        exclude = [
            "^%s" % ref_name
            for ref_name in self.get_refs_matching_config("hooks.no-emails")
        ]
        base_rev = commit_list[0].base_rev_for_display()
        if base_rev is not None:
            # Also reduce the list already present in this branch
            # prior to the update.
            exclude.append("^%s" % base_rev)
        included_refs = git.rev_list(
            self.new_rev, *exclude, _split_lines=True, _decode=True
        )

        # Also, we always send emails for first-parent commits.
        # This is useful in the following scenario:
        #
        #   - The repository has a topic/feature branch for which
        #     there is a no-email configuration;
        #   - When ready to "merge" his topic/feature branch to master,
        #     the user first rebases it, and pushes the new topic/feature
        #     branch. Because the push is for a no-emails branch, commit
        #     emails are turned off (as expected).
        #   - The user then merges the topic/feature branch into
        #     master, and pushes the new master branch.
        #
        # Unless we ignore the hooks.no-emails config for first-parent
        # commits, we end up never sending any commit emails for the
        # commits being pushed on master. This is not what we want,
        # here, because the commits aren't really comming from an
        # external source.  We want to disable commit emails while
        # the commits are being worked on the topic/feature branch,
        # but as soon as they make it to tracked branches, a commit
        # email should be sent.
        first_parents_expr = [self.new_rev]
        if base_rev is not None:
            first_parents_expr.append("^%s" % base_rev)
        # Python-2.x compatibility: When the list of arguments in a call
        # are so long that they get formatted over multiple lines, black
        # adds a comma at the end of the last argument. Unfortunately,
        # it looks like Python 2.x doesn't like that comma when one of
        # the parameter is a non-keyworded variable-length argument list
        # (aka *args).
        #
        # So, work around this issue until the transition to Python 3.x
        # is complete by passing the named arguments via a kwargs dict
        # so as to keep the call short-enough to fit in a single line.
        rev_list_kwargs = {
            "first_parent": True,
            "_split_lines": True,
            "_decode": True,
        }
        first_parents = git.rev_list(*first_parents_expr, **rev_list_kwargs)

        for commit in commit_list:
            commit.send_email_p = (
                commit.rev in included_refs or commit.rev in first_parents
            )