Files
git-hooks/hooks/updates/notes/__init__.py
2017-02-27 08:54:09 +01:00

101 lines
4.2 KiB
Python

"""Git Notes updates root module."""
from git import git, CalledProcessError, diff_tree
class GitNotes(object):
"""An object representing a Git Notes change.
ATTRIBUTES
rev: The revision of the notes change.
filename: The name of the file in the notes/commits branch
used to store the contents of the notes.
contents: The contents of the git notes. None if the notes
have been removed.
annotated_rev: The revision of the commit being annotated.
"""
def __init__(self, notes_rev):
"""The constructor.
PARAMETERS
notes_rev: The revision of the notes change.
"""
self.rev = notes_rev
self.filename = self.__get_notes_filename(notes_rev)
# Implement the contents "attribute" as a property.
# This allows us to lazy-initialize it. And use
# a private attribute name __contents to cache its value.
self.__contents = None
self.annotated_rev = self.filename.replace('/', '')
@property
def contents(self):
"""The contents "attribute", lazily initialized."""
if self.__contents is None:
self.__contents = self.__get_notes_contents(self.rev,
self.filename)
return self.__contents
@classmethod
def __get_notes_filename(cls, notes_rev):
"""Return the filename used to store the git notes.
PARAMETERS
notes_rev: The revision of the notes change.
"""
# A git note for any given commit (called the "annotated commit"
# in this function) is maintained through a file in the special
# refs/notes/commits namespace. The name of that file seems
# to vary a little bit from case to case (sometimes it is just
# equal to "%s" % rev, while some other times it is equal to
# "%s/%s" % (rev[:2], rev[2:]) where rev is the revision of
# the annotated commit. The one constant is that it is easy
# to deduce the annotated commit ID, by just stripping the '/'
# characters.
# Look at the files modified by the git notes commit via
# diff-tree. There should be only one, pointing us towards
# the annotated commit.
all_changes = diff_tree('-r', notes_rev)
if not all_changes:
# notes_rev is probably the root commit. Just use the empty
# tree's sha1 as the reference.
empty_tree_rev = git.mktree(_input='')
all_changes = diff_tree('-r', empty_tree_rev, notes_rev)
# The output should be 2 lines...
# - The first line contains the hash of what is being compared,
# which should be notes_rev;
# - The second line contains the file change that interests us.
# ... except in the case where the notes_rev does not have
# a parent (first note). In that case, we diff-tree'ed against
# the empty tree rev, and the first line is omitted.
#
# Normally, there should only be one entry returned by diff_tree.
# However, there is a situation where the output is more than
# one entry: Newer version of git sometimes rename some of the
# files created by older versions of "git notes" during notes
# updates, and bunches those renamings together with a note
# update, thus creating commits that actually touch multiple
# files (N707-041). In that situation, it appears as though
# the first entry is always the one corresponding to the commit
# being annotated, so discard anything past the first line.
assert all_changes
(_, _, _, _, _, filename) = all_changes[0]
return filename
@classmethod
def __get_notes_contents(cls, notes_rev, notes_filename):
"""Return the contents of the notes at notes_rev.
PARAMETERS
notes_rev: The revision of the notes change.
notes_filename: The filename containing the notes.
"""
try:
return git.show('%s:%s' % (notes_rev, notes_filename))
except CalledProcessError:
# The note was probably deleted, so no more notes.
return None