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)
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)
Beispiel #3
0
    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()
Beispiel #4
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()
Beispiel #5
0
    def test_no_output_lstrip(self):
        """Unit test to verify that git doesn't lstrip the output.
        """
        self.enable_unit_test()

        from git import git

        cd('%s/repo' % TEST_DIR)

        out = git.log('-n1', 'space-subject', pretty='format:%s')
        self.assertEqual(out, '  Commit Subject starting with spaces')
Beispiel #6
0
    def test_git_config(self):
        """Unit test AbstractUpdate child class missing methods.
        """
        self.enable_unit_test()

        from git import git

        cd('%s/repo' % TEST_DIR)

        # Test the git --switch=False attribute.
        out = git.log('-n1', pretty='format:%P').strip()
        self.assertEqual(out, 'd065089ff184d97934c010ccd0e7e8ed94cb7165')
Beispiel #7
0
    def raw_revlog(self):
        """Return the commit's raw revlog.

        This is what Git calls the commit's "raw body (unwrapped subject
        and lines)".

        Note that the revlog is computed lazily and then cached.
        """
        if self.__raw_revlog is None:
            self.__raw_revlog = git.log(self.rev,
                                        max_count="1",
                                        pretty="format:%B",
                                        _decode=True)
        return self.__raw_revlog
Beispiel #8
0
def log_helper(all=False, extra_args=None):
    """Return parallel arrays containing the SHA-1s and summaries."""
    revs = []
    summaries = []
    args = []
    if extra_args:
        args = extra_args
    output = git.log(pretty='oneline', no_color=True, all=all, *args)
    for line in map(core.decode, output.splitlines()):
        match = REV_LIST_REGEX.match(line)
        if match:
            revs.append(match.group(1))
            summaries.append(match.group(2))
    return (revs, summaries)
Beispiel #9
0
    def email_commit(self, commit):
        """See AbstractUpdate.email_commit."""
        notes = GitNotes(commit.rev)

        # Create a partial CommitInfo object for the commit that
        # our note annotates.  We create a partial one in order
        # to avoid computing some info we do not need...
        annotated_commit = CommitInfo(notes.annotated_rev, None, None, None)

        # 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_info = git.log(annotated_commit.rev,
                                     no_notes=True,
                                     max_count="1")
        notes_contents = (None if notes.contents is None else indent(
            notes.contents, ' ' * 4))

        subject = '[%s] notes update for %s' % (self.email_info.project_name,
                                                notes.annotated_rev)

        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_info': annotated_rev_info,
            '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 = Email(self.email_info, annotated_commit.email_to, subject,
                      body, commit.author, self.ref_name,
                      commit.base_rev_for_display(), commit.rev, diff)
        email.enqueue()
Beispiel #10
0
    def email_commit(self, commit):
        """See AbstractUpdate.email_commit."""
        notes = GitNotes(commit.rev)

        # Create a partial CommitInfo object for the commit that
        # our note annotates.  We create a partial one in order
        # to avoid computing some info we do not need...
        annotated_commit = CommitInfo(notes.annotated_rev, None, None, None)

        # 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_info = git.log(annotated_commit.rev, no_notes=True,
                                     max_count="1")
        notes_contents = (None if notes.contents is None
                          else indent(notes.contents, ' ' * 4))

        subject = '[%s] notes update for %s' % (self.email_info.project_name,
                                                notes.annotated_rev)

        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_info': annotated_rev_info,
            '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 = Email(self.email_info, annotated_commit.email_to,
                      subject, body, commit.author, self.ref_name,
                      commit.base_rev_for_display(), commit.rev, diff)
        email.enqueue()
def check_revision_history(rev):
    """Apply pre-commit checks to the commit's revision history.

    Raise InvalidUpdate if one or more style violation are detected.

    PARAMETERS
        rev: The commit to be checked.
    """
    raw_body = git.log(rev, max_count='1', pretty='format:%B',
                       _split_lines=True)

    for line in raw_body:
        if '(no-rh-check)' in line:
            return

    # Various checks on the revision history...
    ensure_empty_line_after_subject(rev, raw_body)
    reject_lines_too_long(rev, raw_body)
    reject_unedited_merge_commit(rev, raw_body)
    reject_merge_conflict_section(rev, raw_body)
    check_missing_ticket_number(rev, raw_body)
def check_revision_history(rev):
    """Apply pre-commit checks to the commit's revision history.

    Raise InvalidUpdate if one or more style violation are detected.

    PARAMETERS
        rev: The commit to be checked.
    """
    raw_body = git.log(rev, max_count='1', pretty='format:%B',
                       _split_lines=True)

    for line in raw_body:
        if 'no-rh-check' in line:
            return

    # Various checks on the revision history...
    ensure_iso_8859_15_only(rev, raw_body)
    ensure_empty_line_after_subject(rev, raw_body)
    reject_lines_too_long(rev, raw_body)
    reject_unedited_merge_commit(rev, raw_body)
    reject_merge_conflict_section(rev, raw_body)
    check_missing_ticket_number(rev, raw_body)
Beispiel #13
0
    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)

        if git_config('hooks.disable-email-diff'):
            diff = None
        else:
            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_bcc = git_config('hooks.filer-email')

        email = Email(self.email_info,
                      commit.email_to(self.ref_name),
                      email_bcc,
                      subject,
                      body,
                      commit.author,
                      self.ref_name,
                      commit.base_rev_for_display(),
                      commit.rev,
                      diff,
                      filer_cmd=filer_cmd)
        email.enqueue()
Beispiel #14
0
    def summary_of_changes(self):
        """A summary of changes to be added at the end of the ref-update email.

        PARAMETERS
            None.

        RETURN VALUE
            A string containing the summary of changes.
        """
        summary = []
        # Display the lost commits (if any) first, to increase
        # our chances of attracting the reader's attention.
        if self.lost_commits:
            summary.append('')
            summary.append('')
            summary.append('!!! WARNING: THE FOLLOWING COMMITS ARE'
                           ' NO LONGER ACCESSIBLE (LOST):')
            summary.append('--------------------------------------'
                           '-----------------------------')
            summary.append('')
            for commit in reversed(self.lost_commits):
                summary.append('  ' + commit.oneline_str())
            for commit in reversed(self.lost_commits):
                summary.append('')
                summary.append(git.log('-n1', commit.rev, pretty='medium'))

        if self.added_commits:
            has_silent = False
            summary.append('')
            summary.append('')
            summary.append('Summary of changes (added commits):')
            summary.append('-----------------------------------')
            summary.append('')
            # Note that we want the summary to include all commits
            # now accessible from this reference, not just the new
            # ones.
            for commit in reversed(self.added_commits):
                if commit.send_email_p:
                    marker = ''
                else:
                    marker = ' (*)'
                    has_silent = True
                summary.append('  ' + commit.oneline_str() + marker)

            # If we added a ' (*)' marker to at least one commit,
            # add a footnote explaining what it means.
            if has_silent:
                summary.extend([
                    '',
                    '(*) This commit exists in a branch whose name matches',
                    '    the hooks.noemail config option. No separate email',
                    '    sent.'
                ])

            # Print a more verbose description of the added commits.
            #
            # We do this by calling "git log -n1" for each and every
            # commit in self.added_commits. This not ideal, as this list
            # can be pretty long. We still do it this way, because
            # it's very simple, and allows us to be certain that
            # the list of commits in the "verbose" section is the exact
            # same as the "short" list above.
            #
            # If it turns out to be unnacceptable, we can try producing
            # the full log using a single git command. Because of
            # merge commits, it's not sufficient to only exclude
            # the parent of the base commit. One must exclude all
            # parents which are not in self.added_commits.  Otherwise,
            # the log will be including commits accessible from
            # the merge's other parents.

            for commit in reversed(self.added_commits):
                summary.append('')
                summary.append(git.log('-n1', '--pretty=medium', commit.rev))

        # We do not want that summary to be used for filing purposes.
        # So add a "Diff:" marker.
        if summary:
            # We know that the output starts with a couple of
            # empty strings. To avoid too much of a break between
            # the "Diff:" marker and the rest of the summary,
            # replace the first empty line with our diff marker
            # (instead of pre-pending it, for instance).
            summary[0] = '\n\nDiff:'

        return '\n'.join(summary)
Beispiel #15
0
def style_check_commit(old_rev, new_rev, project_name):
    """Call check_file for every file changed between old_rev and new_rev.

    Raise InvalidUpdate if one or more style violation are detected.

    PARAMETERS
        old_rev: The commit to be used as a reference to determine
            the list of files that have been modified/added by
            the new commit.  Must be a valid revision.
        new_rev: The commit to be checked.
        project_name: The name of the project (same as the attribute
            in updates.emails.EmailInfo).
    """
    debug("style_check_commit(old_rev=%s, new_rev=%s)" % (old_rev, new_rev))

    # We allow users to explicitly disable pre-commit checks for
    # specific commits via the use of a special keyword placed anywhere
    # in the revision log. If found, then return immediately.
    raw_revlog = git.log("-1", new_rev, pretty="format:%B", _decode=True)
    if "no-precommit-check" in raw_revlog:
        debug("pre-commit checks explicity disabled for commit %s" % new_rev)
        return

    changes = diff_tree("-r", old_rev, new_rev)
    files_to_check = []

    for item in changes:
        (old_mode, new_mode, old_sha1, new_sha1, status, filename) = item
        debug(
            "diff-tree entry: %s %s %s %s %s %s" %
            (old_mode, new_mode, old_sha1, new_sha1, status, filename),
            level=5,
        )

        if status in ("D"):
            debug("deleted file ignored: %s" % filename, level=2)
        elif new_mode == "160000":
            debug("subproject entry ignored: %s" % filename, level=2)
        else:
            # Note: We treat a file rename as the equivalent of the old
            # file being deleted and the new file being added. This means
            # that we should run the pre-commit checks if applicable.
            # This is why we did not tell the `git diff-tree' command
            # above to detect renames, and why we do not have a special
            # branch for status values starting with `R'.
            files_to_check.append(filename)

    no_style_check_map = git_attribute(new_rev, files_to_check,
                                       "no-precommit-check")

    def needs_style_check_p(filename):
        """Return True if the file should be style-checked, False otherwise.

        In addition to returning True/False, it generates a debug log
        when the file does have a no-precommit-check attribute.
        """
        if no_style_check_map[filename] == "set":
            debug("no-precommit-check: %s commit_rev=%s" % (filename, new_rev))
            return False
        else:
            return True

    files_to_check = tuple(filter(needs_style_check_p, files_to_check))
    if not files_to_check:
        debug("style_check_commit: no files to style-check")
        return

    style_check_files(files_to_check, new_rev, project_name)
Beispiel #16
0
from git import git, get_object_type

print("DEBUG: Test the git --switch=False attribute")
print(git.log("-n1", pretty="format:%P", _decode=True).strip())

print("DEBUG: A Test to verify that git does not do any lstrip-ing...")
print(git.log("-n1", "space-subject", pretty="format:%s", _decode=True))

print("DEBUG: Unit test get_object_type with a null SHA1...")
print(get_object_type("0000000000000000000000000000000000000000"))
Beispiel #17
0
def diff_info(sha1, git=git):
    log = git.log('-1', '--pretty=format:%b', sha1)
    decoded = core.decode(log).strip()
    if decoded:
        decoded += '\n\n'
    return decoded + sha1_diff(sha1)
Beispiel #18
0
    def summary_of_changes(self):
        """A summary of changes to be added at the end of the ref-update email.

        PARAMETERS
            None.

        RETURN VALUE
            A string containing the summary of changes.
        """
        summary = []
        # Display the lost commits (if any) first, to increase
        # our chances of attracting the reader's attention.
        if self.lost_commits:
            summary.append('')
            summary.append('')
            summary.append('!!! WARNING: THE FOLLOWING COMMITS ARE'
                           ' NO LONGER ACCESSIBLE (LOST):')
            summary.append('--------------------------------------'
                           '-----------------------------')
            summary.append('')
            for commit in reversed(self.lost_commits):
                summary.append('  ' + commit.oneline_str())
            for commit in reversed(self.lost_commits):
                summary.append('')
                summary.append(git.log('-n1', commit.rev, pretty='medium'))

        if self.added_commits:
            has_silent = False
            summary.append('')
            summary.append('')
            summary.append('Summary of changes (added commits):')
            summary.append('-----------------------------------')
            summary.append('')
            # Note that we want the summary to include all commits
            # now accessible from this reference, not just the new
            # ones.
            for commit in reversed(self.added_commits):
                if commit.send_email_p:
                    marker = ''
                else:
                    marker = ' (*)'
                    has_silent = True
                summary.append('  ' + commit.oneline_str() + marker)

            # If we added a ' (*)' marker to at least one commit,
            # add a footnote explaining what it means.
            if has_silent:
                summary.extend([
                    '',
                    '(*) This commit exists in a branch whose name matches',
                    '    the hooks.noemail config option. No separate email',
                    '    sent.'])

            # Print a more verbose description of the added commits.
            #
            # We do this by calling "git log -n1" for each and every
            # commit in self.added_commits. This not ideal, as this list
            # can be pretty long. We still do it this way, because
            # it's very simple, and allows us to be certain that
            # the list of commits in the "verbose" section is the exact
            # same as the "short" list above.
            #
            # If it turns out to be unnacceptable, we can try producing
            # the full log using a single git command. Because of
            # merge commits, it's not sufficient to only exclude
            # the parent of the base commit. One must exclude all
            # parents which are not in self.added_commits.  Otherwise,
            # the log will be including commits accessible from
            # the merge's other parents.

            for commit in reversed(self.added_commits):
                summary.append('')
                summary.append(git.log('-n1', '--pretty=medium', commit.rev))

        # We do not want that summary to be used for filing purposes.
        # So add a "Diff:" marker.
        if summary:
            # We know that the output starts with a couple of
            # empty strings. To avoid too much of a break between
            # the "Diff:" marker and the rest of the summary,
            # replace the first empty line with our diff marker
            # (instead of pre-pending it, for instance).
            summary[0] = '\n\nDiff:'

        return '\n'.join(summary)
Beispiel #19
0
    def get_standard_commit_email(self, commit):
        """Return an Email object for the given commit.

        Here, "standard" means that the Email returned corresponds
        to the Email the git-hooks sends by default, before any
        possible project-specific customization is applied.

        Before sending this email, users of this method are expected
        to apply those customizations as needed.

        PARAMETERS
            commit: A CommitInfo object.
        """
        subject_prefix = commit_email_subject_prefix(
            self.email_info.project_name,
            self.ref_name,
        )

        subject = f"{subject_prefix} {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.

        body = git.log(commit.rev, max_count="1", _decode=True) + "\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

        if git_config("hooks.disable-email-diff"):
            diff = None
        else:
            diff = git.show(commit.rev, p=True, M=True, stat=True, pretty="format:")
            # Decode the diff line-by-line:
            #
            # It seems conceivable that the diff may cover multiple files,
            # and that the files may have different encodings, so we cannot
            # assume that the entire output follows the same encoding.
            diff = safe_decode_by_line(diff)

            # Add a small "---" separator line at the beginning of
            # the diff section we just computed. This mimicks what
            # "git show" would do if we hadn't provided an empty
            # "format:" string to the "--pretty" command-line option.
            diff = "---\n" + diff

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

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

        return Email(
            self.email_info,
            commit.email_to(self.ref_name),
            email_bcc,
            subject,
            body,
            commit.full_author_email,
            self.ref_name,
            commit.base_rev_for_display(),
            commit.rev,
            diff,
            filer_cmd=filer_cmd,
        )
Beispiel #20
0
    def get_standard_commit_email(self, commit):
        """See AbstractUpdate.get_standard_commit_email."""
        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",
                                    _decode=True)
        notes_contents = (None if notes.contents is None else indent(
            notes.contents, " " * 4))

        # Get the list of references the annotated commit is contained in.
        annotated_commit_ref_names = git.for_each_ref(
            contains=annotated_commit.rev,
            format="%(refname)",
            _decode=True,
            _split_lines=True,
        )
        # Strip from that list all the references which are to be ignored
        # (typically, those are internal references).
        annotated_commit_ref_names = [
            ref_name for ref_name in annotated_commit_ref_names
            if search_config_option_list("hooks.ignore-refs", ref_name) is None
        ]

        subject_prefix = commit_email_subject_prefix(
            project_name=self.email_info.project_name,
            ref_names=annotated_commit_ref_names,
        )

        # 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 = f"[notes{subject_tag}]{subject_prefix} {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,
                        _decode=True)[1:]

        refs_containing_annotated_commit_section = (
            REFS_CONTAINING_ANNOTATED_COMMIT_TEMPLATE.format(
                annotated_commit_references="\n".join([
                    f"    {ref_name}"
                    for ref_name in annotated_commit_ref_names
                ])))

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

        return Email(
            self.email_info,
            annotated_commit.email_to(self.ref_name),
            email_bcc,
            subject,
            body,
            commit.full_author_email,
            self.ref_name,
            commit.base_rev_for_display(),
            commit.rev,
            # Place the refs_containing_annotated_commit_section inside
            # the "Diff:" section to avoid having that section trigger
            # some unexpected filing.
            refs_containing_annotated_commit_section + diff,
        )