blob: bfd468a11656d9bed781cf5387f1637113a7391c [file] [log] [blame]
# Copyright (C) 2019 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import calendar
import json
import re
from datetime import datetime
from resultsdbpy.flask_support.util import FlaskJSONEncoder
class Commit(object):
TIMESTAMP_TO_UUID_MULTIPLIER = 100
MAX_KEY_LENGTH = 128
@classmethod
def from_json(cls, data):
data = data if isinstance(data, dict) else json.loads(data)
return cls(
repository_id=data.get('repository_id'),
branch=data.get('branch'),
id=data.get('id'),
timestamp=data.get('timestamp'),
order=data.get('order'),
committer=data.get('committer'),
message=data.get('message'),
)
def __init__(self, repository_id, branch, id, timestamp=None, order=None, committer=None, message=None):
for argument in [('repository_id', repository_id), ('branch', branch), ('id', id), ('timestamp', timestamp)]:
if argument[1] is None:
raise ValueError(f'{argument[0]} is not defined for commit')
self.repository_id = str(repository_id)
if not re.match(r'^[a-zA-Z?]+$', self.repository_id) or len(self.repository_id) > self.MAX_KEY_LENGTH:
raise ValueError(f"'{self.repository_id}' is an invalid repository id")
self.branch = str(branch)
if not re.match(r'^[a-zA-Z0-9-.?/]+$', self.branch) or len(self.branch) > self.MAX_KEY_LENGTH:
raise ValueError(f"'{self.branch}' is an invalid branch name")
# An id is either a git commit or SVN revision.
self.id = str(id)
if not re.match(r'^[a-fA-F0-9?]+$', self.id) or len(self.id) > 40:
raise ValueError(f"'{self.id}' is an invalid commit id")
if isinstance(timestamp, datetime):
self.timestamp = timestamp
else:
self.timestamp = datetime.utcfromtimestamp(int(timestamp))
self.order = int(order) if order else 0
self.committer = committer if committer else None
self.message = message if message else None
def timestamp_as_epoch(self):
return calendar.timegm(self.timestamp.timetuple())
@property
def uuid(self):
# Rebase-based workflows in git can cause commits to have the same commit timestamp. To uniquely recognize them,
# multiply each timestamp by the `TIMESTAMP_TO_UUID_MULTIPLIER`.
return self.timestamp_as_epoch() * self.TIMESTAMP_TO_UUID_MULTIPLIER + self.order
def __eq__(self, other):
return (isinstance(other, type(self)) and (self.repository_id, self.branch, self.id) == (other.repository_id, other.branch, other.id))
def __ne__(self, other):
return not (self == other)
def __lt__(self, other):
if self.timestamp < other.timestamp:
return True
if self.timestamp > other.timestamp:
return False
return self.order < other.order
def __le__(self, other):
return self < other or self == other
def __gt__(self, other):
if self.timestamp > other.timestamp:
return True
if self.timestamp < other.timestamp:
return False
return self.order > other.order
def __ge__(self, other):
return self > other or self == other
def __hash__(self):
return hash(self.repository_id) ^ hash(self.branch) ^ hash(self.id)
def to_json(self, pretty_print=False):
return json.dumps(self, cls=self.Encoder, sort_keys=pretty_print, indent=4 if pretty_print else None)
class Encoder(FlaskJSONEncoder):
def default(self, obj):
if isinstance(obj, Commit):
result = {}
for key, value in obj.__dict__.items():
if value is None:
continue
result[key] = value
result['timestamp'] = obj.timestamp_as_epoch()
return result
return super(Commit.Encoder, self).default(obj)