blob: ced6c281a514b2d88f788043b80a52f38b96a990 [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 json
from collections import defaultdict
from flask import abort, jsonify, request
from resultsdbpy.flask_support.util import AssertRequest, query_as_kwargs, limit_for_query
from resultsdbpy.model.repository import SCMException
from resultsdbpy.controller.commit import Commit
def _find_comparison(commit_context, repository_id, branch, id, uuid, timestamp, priority=min):
if bool(id) + bool(uuid) + bool(timestamp) > 1:
abort(400, description='Can only search by one of [commit id, commit uuid, timestamp] in a single request')
try:
if uuid:
# We don't need real commit to search by uuid and CommitContexts have trouble diffrentiating between uuids and timestamps.
uuid = priority([int(element) for element in uuid])
return Commit('?', '?', '?', uuid // Commit.TIMESTAMP_TO_UUID_MULTIPLIER, uuid % Commit.TIMESTAMP_TO_UUID_MULTIPLIER)
if timestamp:
return priority([round(float(element)) for element in timestamp])
except ValueError:
abort(400, description='Timestamp and uuid must be integers')
if not repository_id and not id:
return None
if not repository_id:
repository_id = commit_context.repositories.keys()
for repository in repository_id:
if repository not in commit_context.repositories.keys():
abort(404, description=f"\'{repository}\' is not a registered repository")
result = []
for repository in repository_id:
for b in branch:
if id:
for elm in id:
result += commit_context.find_commits_by_id(repository, b, elm)
else:
result += commit_context.find_commits_in_range(repository, b, limit=1)
if not result:
abort(404, description='No commits found matching the specified criteria')
return priority(result)
def uuid_range_for_commit_range_query():
def decorator(method):
def real_method(obj, branch=None,
after_repository_id=None, after_branch=None, after_id=None, after_uuid=None,
after_timestamp=None,
before_repository_id=None, before_branch=None, before_id=None, before_uuid=None,
before_timestamp=None, **kwargs):
# We're making an asumption that the class using this decorator actually has a commit_context, if it does not,
# this decorator will fail spectacularly
with obj.commit_context:
if not branch:
branch = [None]
begin = _find_comparison(
obj.commit_context, repository_id=after_repository_id, branch=after_branch or branch,
id=after_id, uuid=after_uuid, timestamp=after_timestamp, priority=min,
)
end = _find_comparison(
obj.commit_context, repository_id=before_repository_id, branch=before_branch or branch,
id=before_id, uuid=before_uuid, timestamp=before_timestamp, priority=max,
)
return method(obj, begin=begin, end=end, branch=branch, **kwargs)
real_method.__name__ = method.__name__
return real_method
return decorator
def uuid_range_for_query():
def decorator(method):
@uuid_range_for_commit_range_query()
def real_method(obj, repository_id=None, branch=None, id=None, uuid=None, timestamp=None, begin=None, end=None, **kwargs):
# We're making an asumption that the class using this decorator actually has a commit_context, if it does not,
# this decorator will fail spectacularly
with obj.commit_context:
index = _find_comparison(
obj.commit_context, repository_id=repository_id, branch=branch,
id=id, uuid=uuid, timestamp=timestamp, priority=max,
)
if index:
if begin or end:
abort(400, description='Cannot define commit and range when searching')
begin = index
end = index
if isinstance(end, Commit) and end.repository_id != '?':
repositories = list(obj.commit_context.repositories.keys())
repositories.remove(end.repository_id)
commits = [end]
[commits.extend(siblings) for siblings in obj.commit_context.sibling_commits(end, repositories).values()]
end = max(commits)
return method(obj, branch=branch, begin=begin, end=end, **kwargs)
real_method.__name__ = method.__name__
return real_method
return decorator
class HasCommitContext(object):
def __init__(self, commit_context):
self.commit_context = commit_context
class CommitController(HasCommitContext):
DEFAULT_LIMIT = 100
def default(self):
if request.method == 'POST':
return self.register()
return self.find()
@uuid_range_for_commit_range_query()
@limit_for_query(DEFAULT_LIMIT)
def _find(self, repository_id=None, branch=None, id=None, uuid=None, timestamp=None, limit=None, begin=None, end=None, **kwargs):
AssertRequest.query_kwargs_empty(**kwargs)
with self.commit_context:
if not repository_id:
repository_id = self.commit_context.repositories.keys()
for repository in repository_id:
if repository not in self.commit_context.repositories.keys():
abort(404, description=f"\'{repository}\' is not a registered repository")
if not branch:
branch = [None]
if bool(id) + bool(uuid) + bool(timestamp) > 1:
abort(400, description='Can only search by one of [commit id, commit uuid, timestamp] in a single request')
result = []
for repository in repository_id:
# Limit makes most sense on a per-repository basis
results_for_repo = []
for b in branch:
if len(results_for_repo) >= limit:
continue
if id:
for elm in id:
results_for_repo += self.commit_context.find_commits_by_id(repository, b, elm, limit=limit - len(results_for_repo))
elif uuid:
for elm in uuid:
results_for_repo += self.commit_context.find_commits_by_uuid(repository, b, int(elm), limit=limit - len(results_for_repo))
elif timestamp:
for elm in timestamp:
results_for_repo += self.commit_context.find_commits_by_timestamp(repository, b, round(float(elm)), limit=limit - len(results_for_repo))
else:
results_for_repo += self.commit_context.find_commits_in_range(repository, b, limit=limit - len(results_for_repo), begin=begin, end=end)
result += results_for_repo
return sorted(result)
def find(self):
AssertRequest.is_type()
result = self._find(**request.args.to_dict(flat=False))
if not result:
abort(404, description='No commits found matching the specified criteria')
return jsonify(Commit.Encoder().default(result))
def repositories(self):
AssertRequest.is_type()
AssertRequest.no_query()
return jsonify(sorted(self.commit_context.repositories.keys()))
@query_as_kwargs()
@limit_for_query(DEFAULT_LIMIT)
def branches(self, repository_id=None, branch=None, limit=None, **kwargs):
AssertRequest.is_type()
AssertRequest.query_kwargs_empty(**kwargs)
result = defaultdict(list)
with self.commit_context:
for repository in repository_id or self.commit_context.repositories.keys():
limit_for_repo = limit
for b in branch or [None]:
if not limit_for_repo:
continue
matching_branches = self.commit_context.branches(repository, branch=b, limit=limit_for_repo)
if not matching_branches:
continue
limit_for_repo -= len(matching_branches)
result[repository] += matching_branches
return jsonify(result)
@query_as_kwargs()
def siblings(self, limit=None, **kwargs):
AssertRequest.is_type()
AssertRequest.query_kwargs_empty(limit=limit)
with self.commit_context:
commits = self._find(**kwargs)
if not commits:
abort(404, description='No commits found matching the specified criteria')
if len(commits) > 1:
abort(404, description=f'{len(commits)} commits found matching the specified criteria')
repositories = sorted(self.commit_context.repositories.keys())
repositories.remove(commits[0].repository_id)
return jsonify(Commit.Encoder().default(self.commit_context.sibling_commits(commits[0], repositories)))
@query_as_kwargs()
def next(self, limit=None, **kwargs):
AssertRequest.is_type()
AssertRequest.query_kwargs_empty(limit=limit)
with self.commit_context:
commits = self._find(**kwargs)
if not commits:
abort(404, description='No commits found matching the specified criteria')
if len(commits) > 1:
abort(404, description=f'{len(commits)} commits found matching the specified criteria')
commit = self.commit_context.next_commit(commits[0])
return jsonify(Commit.Encoder().default([commit] if commit else []))
@query_as_kwargs()
def previous(self, limit=None, **kwargs):
AssertRequest.is_type()
AssertRequest.query_kwargs_empty(limit=limit)
with self.commit_context:
commits = self._find(**kwargs)
if not commits:
abort(404, description='No commits found matching the specified criteria')
if len(commits) > 1:
abort(404, description=f'{len(commits)} commits found matching the specified criteria')
commit = self.commit_context.previous_commit(commits[0])
return jsonify(Commit.Encoder().default([commit] if commit else []))
def register(self, commit=None):
is_endpoint = not bool(commit)
if is_endpoint:
AssertRequest.is_type(['POST'])
AssertRequest.no_query()
if is_endpoint:
try:
commit = request.form or json.loads(request.get_data())
if 'api_key' in commit:
del commit['api_key']
except ValueError:
abort(400, description='Expected uploaded data to be json')
try:
self.commit_context.register_commit(Commit.from_json(commit))
if is_endpoint:
return jsonify({'status': 'ok'})
return Commit.from_json(commit)
except ValueError:
pass
required_args = ['repository_id', 'id']
optional_args = ['branch']
for arg in required_args:
if arg not in commit:
abort(400, description=f"'{arg}' required to define commit")
for arg in commit.keys():
if arg in required_args + optional_args:
continue
if arg in ['timestamp', 'order', 'committer', 'message']:
abort(400, description='Not enough arguments provided to define a commit, but too many to search for a commit')
abort(400, description=f"'{arg}' is not valid for defining commits")
try:
commit = self.commit_context.register_commit_with_repo_and_id(
repository_id=commit.get('repository_id'),
branch=commit.get('branch'),
commit_id=commit.get('id'),
)
except (RuntimeError, SCMException) as error:
abort(404, description=str(error))
if is_endpoint:
return jsonify({'status': 'ok'})
return commit