blob: 4993592fd8b0ba4dcc42e790b30ea0f707e852a4 [file] [log] [blame]
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from common.frozendict import frozendict
from common.gerrit.time import ParseGerritTime
# Gerrit API version
__version__ = '2.10'
class GerritObject(frozendict):
"""Base object to encapsulate and enhance immutable Gerrit REST API objects.
Each GerritObject is instantiated from a dictionary that is directly
deserialized from the Gerrit JSON. The GerritObject, in turn, provides
read-only accessors to object's internal fields.
"""
__gerrit_object_name__ = 'GerritObject'
def __init__(self, *args, **kwargs):
super(GerritObject, self).__init__(self._wrap(dict(*args, **kwargs)))
@classmethod
def _wrap(cls, value):
"""Wraps a JSON object value in a suitable immutable structure. Since JSON
only deserializes into specific field types, this is easily scoped."""
if isinstance(value, (dict, frozendict)):
d = {}
for k, v in value.iteritems():
if not isinstance(k, basestring):
raise TypeError("JSON key is not a string")
d[k] = cls._wrap(v)
return frozendict(**d)
elif isinstance(value, (list, tuple)):
return tuple([cls._wrap(x) for x in value])
else:
return value
def __repr__(self):
return '%s%s' % (self.name, str(self.data))
@property
def name(self):
return self.__gerrit_object_name__
@property
def data(self):
return self._data
@classmethod
def fromJsonDict(cls, json_dict):
"""Loads an instance of this class from a deserialized JSON dictionary"""
return cls(**json_dict)
class LabelInfo(GerritObject):
"""A class that implements the Gerrit LabelInfo JSON object.
https://gerrit-review.googlesource.com/Documentation/
rest-api-changes.html#label-info
"""
__gerrit_object_name__ = 'LabelInfo'
@property
def all(self):
"""Returns the set of 'ApprovalInfo' objects associated with this label.
This field will only be populated if DETAILED_LABELS is specified in the
query.
Returns: (tuple) A tuple of 'ApprovalInfo' objects associated with the
label
"""
return tuple(ApprovalInfo(**ai) for ai in self.get('all', ()))
@property
def values(self):
"""Returns a sorted tuple of unique values (+1, +2, etc.)
Note that a 'ChangeInfo' query must include 'DETAILED_LABELS' to contain
this information.
Returns: (set) A sorted tuple of label values for 'name'
"""
values = set()
for ai in self.all:
value = ai.get('value')
if value is not None:
values.add(int(value))
return tuple(sorted(values))
def isApproved(self):
"""Returns 'True' if this label has an 'approved' field"""
return 'approved' in self.keys()
def isRejected(self):
"""Returns 'True' if this label has a 'rejected' field"""
return 'rejected' in self.keys()
def isRecommended(self):
"""Returns 'True' if this label has a 'recommended' field"""
return 'recommended' in self.keys()
def isDisliked(self):
"""Returns 'True' if this label has a 'disliked' field"""
return 'disliked' in self.keys()
class AccountInfo(GerritObject):
"""A class that implements the Gerrit 'AccountInfo' JSON object.
https://gerrit-review.googlesource.com/Documentation/
rest-api-accounts.html#account-info
"""
__gerrit_object_name__ = 'AccountInfo'
class ApprovalInfo(AccountInfo):
"""A class that implements the Gerrit 'ApprovalInfo' JSON object.
'ApprovalInfo' has the same fields as 'AccountInfo' plus some more.
https://gerrit-review.googlesource.com/Documentation/
rest-api-changes.html#approval-info
"""
__gerrit_object_name__ = 'ApprovalInfo'
def __getitem__(self, key):
if key == 'date':
return self.date
return self._data.get(key)
@property
def isPermitted(self):
"""Returns whether or not the user is permitted to vote on this label."""
return self.get('value') is not None
@property
def date(self):
value = self._data.get('date')
if value is None:
return None
return ParseGerritTime(value)
class ChangeInfo(GerritObject):
"""A class that wraps a single Gerrit ChangeInfo JSON object.
https://gerrit-review.googlesource.com/Documentation/
rest-api-changes.html#change-info
"""
__gerrit_object_name__ = 'ChangeInfo'
@property
def _revisions(self):
"""Constructs our forward-revision mapping on-demand."""
return frozendict(
(k, RevisionInfo(k, **v))
for k, v in self.get('revisions', {}).iteritems())
@property
def _revision_number_map(self):
"""Constructs our reverse-revision mapping on-demand."""
return frozendict(
(v.get('_number'), v)
for v in self._revisions.itervalues())
@staticmethod
def parse_id(value):
"""Returns the parsed Gerrit change ID fields.
The Gerrit change ID takes the form: [project]~[branch]~[ID]
Returns: (project, branch, id)
"""
return tuple(value.split('~', 2))
@property
def id_tuple(self):
"""Parse the 'id' field into a (project, branch, change-id) tuple"""
return self.parse_id(self['id'])
@property
def messages(self):
"""Constructs our 'messages' objects on-demand."""
return tuple(ChangeMessageInfo(**msg)
for msg in self.get('messages', ()))
@property
def unique_id(self):
"""Returns a comparable value that uniquely identifies this 'ChangeInfo'.
The returned value is more specific than the 'id' field, which applies to
all 'ChangeInfo's resulting from the same Gerrit issue ID.
The composition of this is intentionally vague; external objects must
not rely on this field's actual value. The only guarantee that this field
offers is that it is suitable for equality comparisons to other 'unique_id'
results.
This field MUST NOT be used as a persistent key for this ChangeInfo.
Returns: (object) The unique ID object for this change.
"""
# We are assuming that no two changes for the same Gerrit ID will have the
# same update time. If this is false, we will need to find an alternative
# 'unique_id', but any code using it should automatically work.
return (self.get('id'), self.get('updated'))
@property
def update_time(self):
"""Returns a 'datetime.datetime' corresponding to this change's 'updated'
field. If the field was missing, this will return 'None'.
Return: A datetime.datetime object representing the update time, or None
if the update time is not present.
Raises:
ValueError if the 'updated' field could not be successfully parsed.
"""
result = self.get('updated')
if result is not None:
result = ParseGerritTime(result)
return result
@property
def created_time(self):
"""Returns a 'datetime.datetime' corresponding to this change's 'created'
field. If the field was missing, this will return 'None'.
Return: A datetime.datetime object representing the created time, or None
if the created time is not present.
Raises:
ValueError if the 'created' field could not be successfully parsed.
"""
result = self.get('created')
if result is not None:
result = ParseGerritTime(result)
return result
@property
def revisions(self):
"""
Calculates the latest revision ID of a given change. This will query the
change's cached information and pull out the revision ID for the revision
with the highest "_number" (patch set number) field.
Returns: (tuple) An ordered tuple of 'RevisionInfo' associated with a
change, oldest-to-newest. This will return an empty list if the change
is unregistered or if the change has no associated revisions.
"""
return tuple(self._revision_number_map[k]
for k in sorted(
self._revision_number_map.keys(),
reverse=True))
def revisionInfoForNumber(self, value):
"""Gets the revision hash that corresponds to a patch set number.
Args:
value: (int) the revision number value
Returns: (RevisionInfo/None) The RevisionInfo for 'value', or None if one
is not defined.
"""
return self._revision_number_map.get(value)
@property
def latest_revision(self):
current_revision = self.get('current_revision')
if current_revision is None:
return None
revision = self._revisions.get(current_revision)
if revision is not None:
return revision
# Return an empty RevisionInfo
return RevisionInfo(current_revision)
@property
def owner(self):
owner = self.get('owner')
if owner is not None:
return AccountInfo(**owner)
return None
def label(self, name):
"""Retrieves the value of a Gerrit label field. The label field contains
the current set of labels that this issue has applied to it along with
associated metadata.
Args:
name: (str) The name of the label to retrieve (case-sensitive).
Returns: (LabelInfo) A LabelInfo for the requested label, or None if it is
not present.
"""
return self.labels.get(name)
@property
def labels(self):
return frozendict((label_name, LabelInfo(**label_dict))
for label_name, label_dict in
self.get('labels', {}).iteritems())
def __repr__(self):
return '%s{%s}' % (type(self).__name__, self.get('id'))
class ChangeMessageInfo(GerritObject):
"""Represents a 'ChangeMessageInfo' Gerrit API object"""
__gerrit_object_name__ = 'ChangeMessageInfo'
@property
def date(self):
return ParseGerritTime(self['date'])
class RevisionInfo(GerritObject):
"""Represents a 'RevisionInfo' Gerrit API object"""
__gerrit_object_name__ = 'RevisionInfo'
def __init__(self, revision, *args, **kwargs):
GerritObject.__init__(self, *args, **kwargs)
self._revision = revision
@property
def revision(self):
"""The revision value for this 'RevisionInfo' object"""
return self._revision
class ReviewInput(GerritObject):
"""Interfaces with a Gerrit ReviewInput object.
https://gerrit-review.googlesource.com/Documentation/
rest-api-changes.html#review-input
"""
__gerrit_object_name__ = 'ReviewInput'
DRAFTS_DELETE = 'DELETE'
DRAFTS_PUBLISH = 'PUBLISH'
DRAFTS_KEEP = 'KEEP'
NOTIFY_NONE = 'NONE'
NOTIFY_OWNER = 'OWNER'
NOTIFY_OWNER_REVIEWERS = 'OWNER_REVIEWERS'
NOTIFY_ALL = 'ALL'
@classmethod
def New(cls, message=None, strict_labels=None, drafts=None, notify=None,
on_behalf_of=None):
"""Constructs a new Gerrit ReviewInput object from a set of composite
fields.
Args:
message: (str) If supplied, the review input message.
strict_labels (bool): If supplied, a boolean for 'strict_labels'
drafts: (str) If supplied, one of the 'DRAFTS_*' strings belongining to
this class.
notify: (str) If supplied, one of the 'NOTIFY_*' trings belonging to
this class.
on_behalf_of: (str) If supplied, the 'on_behalf_of' field value.
Returns: (ReviewInput) A ReviewInput instance.
"""
review_info_dict = {}
def addIfPopulated(key, value, map_fn=None):
if value is not None:
if map_fn is not None:
value = map_fn(value)
review_info_dict[key] = value
addIfPopulated('message', message)
addIfPopulated('strict_labels', strict_labels, map_fn=bool)
addIfPopulated('drafts', drafts)
addIfPopulated('notify', notify)
addIfPopulated('on_behalf_of', on_behalf_of)
return cls(**review_info_dict)
def addLabel(self, label, value):
"""Adds a label/value pair
Args:
label: (str) the name of the label (e.g., 'Code-Review')
value: (int) the label value (e.g., -1)
Returns: (ReviewInput) A new ReviewInput with the specified labels set.
"""
nlabels = self.get('labels')
if nlabels is None:
nlabels = {}
else:
nlabels = nlabels.mutableDict()
nlabels[label] = value
return self.extend(labels=nlabels)
class ReviewInfo(GerritObject):
"""Wrapper class for the ReviewInfo object.
https://gerrit-review.googlesource.com/Documentation/
rest-api-changes.html#review-info
"""
__gerrit_object_name__ = 'ReviewInfo'