Files
git-hooks/hooks/updates/notes/update.py
Joel Brobecker 9c82498e7b Introduce (the concept of) git command output decoding
This commit is preparation work for the transition to Python 3.x,
where the output obtained by running Git commands will become
bytes as opposed to a string. In the vast majority of cases,
we'll want to decode that output into a string. Ideally, we would
want to do this in a way that is both compatible with Python 2.x
and Python 3.x, but we have found that this requires a lot of
work with many changes spread all over the code. So, instead,
what this commit does is introduce the concept of decoding
the output, but with the decoding only occurring when running
under Python 3.x.

That way, we can make progress towards Python 3.x while preserving
the behavior under Python 2.x intact.

Change-Id: I189577798ee96cba1fa55c7356babf102575642f
TN: U530-006
2021-10-06 11:27:20 -07:00

171 lines
6.2 KiB
Python

"""Handling of Git Notes updates."""
from config import git_config
from errors import InvalidUpdate
from git import git, is_null_rev, is_valid_commit
from updates import AbstractUpdate, RefKind
from updates.commits import commit_info_list
from updates.emails import Email
from updates.notes import GitNotes
from utils import indent
# The template to be used as the body of the email to be sent
# for a notes commit which either adds, or modifies a git notes.
UPDATED_NOTES_COMMIT_EMAIL_BODY_TEMPLATE = """\
A Git note has been updated; it now contains:
%(notes_contents)s
This note annotates the following commit:
%(annotated_rev_log)s
"""
# The template to be used as the body of the email to be sent
# for a notes commit which deletes a git notes.
DELETED_NOTES_COMMIT_EMAIL_BODY_TEMPLATE = """\
Git notes annotating the following commit have been deleted.
%(annotated_rev_log)s
"""
class NotesUpdate(AbstractUpdate):
"""Update object for Git Notes creation or update.
The difference between Notes creation and Notes update is very
small, so this class has been implemented in a way to support
both (in other words, self.old_rev may be null).
"""
def self_sanity_check(self):
"""See AbstractUpdate.self_sanity_check."""
assert self.ref_kind == RefKind.notes_ref and self.object_type == "commit"
assert self.ref_name.startswith("refs/notes/")
def validate_ref_update(self):
"""See AbstractUpdate.validate_ref_update."""
# Only fast-forward changes are allowed.
self.__ensure_fast_forward()
# Also iterate over all new notes, and verify that
# the associated commit is available. We need these
# associated commits in order to create the emails
# to be sent for those notes.
for notes_commit in self.new_commits_for_ref:
notes = GitNotes(notes_commit.rev)
if not is_valid_commit(notes.annotated_rev):
error_message = [
"The commit associated to the following notes update",
"cannot be found. Please push your branch commits first",
"and then push your notes commits.",
"",
"Notes commit: %s" % notes.rev,
"Annotated commit: %s" % notes.annotated_rev,
"",
"Notes contents:",
] + notes.contents.splitlines()
raise InvalidUpdate(*error_message)
def pre_commit_checks(self):
"""See AbstractUpdate.pre_commit_checks."""
# Notes are a bit special, and mostly handled automatically
# by Git, so most of the pre-commit checks don't apply.
self.call_project_specific_commit_checker()
def get_update_email_contents(self):
"""See AbstractUpdate.get_update_email_contents."""
# No update email needed for notes (this is always
# a fast-forward commit)...
return None
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)
)
# 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, _decode=True)[1:]
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,
diff,
)
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), _decode=True) == "":
return
raise InvalidUpdate(
"Your Git Notes are not up to date.",
"",
"Please update your Git Notes and push again.",
)