# HG changeset patch # User domruf # Date 1490818370 -7200 # Node ID 6452215a54ee786d65cf96131dfc135a081f274a # Parent bf9900e6e177375e3abd8d2df363fc939f6dba69 api: add get_pullrequest and comment_pullrequest methods Modified by Mads Kiilerich, mainly to let the test helper function create_pullrequest use model directly. diff -r bf9900e6e177 -r 6452215a54ee docs/api/api.rst --- a/docs/api/api.rst Wed Mar 29 22:12:50 2017 +0200 +++ b/docs/api/api.rst Wed Mar 29 22:12:50 2017 +0200 @@ -1140,6 +1140,95 @@ } } +get_pullrequest +^^^^^^^^^^^^^^^ + +Get information and review status for a given pull request. This command can only be executed +using the api_key of a user with read permissions to the original repository. + +INPUT:: + + id : + api_key : "" + method : "get_pullrequest" + args: { + "pullrequest_id" : "", + } + +OUTPUT:: + + id : + result: { + "status": "", + "pull_request_id": , + "description": "", + "title": "", + "url": "", + "reviewers": [ + { + "username": "", + }, + ... + ], + "org_repo_url": "", + "org_ref_parts": [ + "", + "", + "" + ], + "other_ref_parts": [ + "", + "", + "" + ], + "comments": [ + { + "username": "", + "text": "", + "comment_id": "", + }, + ... + ], + "owner": "", + "statuses": [ + { + "status": "", # "under_review", "approved" or "rejected" + "reviewer": "", + "modified_at": "" # iso 8601 date, server's timezone + }, + ... + ], + "revisions": [ + "", + ... + ] + }, + error: null + +comment_pullrequest +^^^^^^^^^^^^^^^^^^^ + +Add comment, change status or close a given pull request. This command can only be executed +using the api_key of a user with read permissions to the original repository. + +INPUT:: + + id : + api_key : "" + method : "comment_pullrequest" + args: { + "pull_request_id": "", + "comment_msg": Optional(''), + "status": Optional(None), # "under_review", "approved" or "rejected" + "close_pr": Optional(False)", + } + +OUTPUT:: + + id : + result: True + error: null + API access for web views ------------------------ diff -r bf9900e6e177 -r 6452215a54ee kallithea/controllers/api/api.py --- a/kallithea/controllers/api/api.py Wed Mar 29 22:12:50 2017 +0200 +++ b/kallithea/controllers/api/api.py Wed Mar 29 22:12:50 2017 +0200 @@ -48,14 +48,17 @@ from kallithea.model.user_group import UserGroupModel from kallithea.model.gist import GistModel from kallithea.model.changeset_status import ChangesetStatusModel +from kallithea.model.comment import ChangesetCommentsModel +from kallithea.model.pull_request import PullRequestModel from kallithea.model.db import ( Repository, Setting, UserIpMap, Permission, User, Gist, - RepoGroup, UserGroup) + RepoGroup, UserGroup, PullRequest, ChangesetStatus) from kallithea.lib.compat import json from kallithea.lib.exceptions import ( DefaultUserException, UserGroupsAssignedException) from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError from kallithea.lib.vcs.backends.base import EmptyChangeset +from kallithea.lib.utils import action_logger log = logging.getLogger(__name__) @@ -2506,3 +2509,65 @@ info["reviews"] = reviews return info + + # permission check inside + def get_pullrequest(self, pullrequest_id): + """ + Get given pull request by id + """ + pull_request = PullRequest.get(pullrequest_id) + if pull_request is None: + raise JSONRPCError('pull request `%s` does not exist' % (pullrequest_id,)) + if not HasRepoPermissionLevel('read')(pull_request.org_repo.repo_name): + raise JSONRPCError('not allowed') + return pull_request.get_api_data() + + # permission check inside + def comment_pullrequest(self, pull_request_id, comment_msg='', status=None, close_pr=False): + """ + Add comment, close and change status of pull request. + """ + apiuser = get_user_or_error(request.authuser.user_id) + pull_request = PullRequest.get(pull_request_id) + if pull_request is None: + raise JSONRPCError('pull request `%s` does not exist' % (pull_request_id,)) + if (not HasRepoPermissionLevel('read')(pull_request.org_repo.repo_name)): + raise JSONRPCError('No permission to add comment. User needs at least reading permissions' + ' to the source repository.') + owner = apiuser.user_id == pull_request.owner_id + reviewer = apiuser.user_id in [reviewer.user_id for reviewer in pull_request.reviewers] + if close_pr and not (apiuser.admin or owner): + raise JSONRPCError('No permission to close pull request. User needs to be admin or owner.') + if status and not (apiuser.admin or owner or reviewer): + raise JSONRPCError('No permission to change pull request status. User needs to be admin, owner or reviewer.') + if pull_request.is_closed(): + raise JSONRPCError('pull request is already closed') + + comment = ChangesetCommentsModel().create( + text=comment_msg, + repo=pull_request.org_repo.repo_id, + author=apiuser.user_id, + pull_request=pull_request.pull_request_id, + f_path=None, + line_no=None, + status_change=(ChangesetStatus.get_status_lbl(status)), + closing_pr=close_pr + ) + action_logger(apiuser, + 'user_commented_pull_request:%s' % pull_request_id, + pull_request.org_repo, request.ip_addr) + if status: + ChangesetStatusModel().set_status( + pull_request.org_repo_id, + status, + apiuser.user_id, + comment, + pull_request=pull_request_id + ) + if close_pr: + PullRequestModel().close_pull_request(pull_request_id) + action_logger(apiuser, + 'user_closed_pull_request:%s' % pull_request_id, + pull_request.org_repo, request.ip_addr) + Session().commit() + return True diff -r bf9900e6e177 -r 6452215a54ee kallithea/model/db.py --- a/kallithea/model/db.py Wed Mar 29 22:12:50 2017 +0200 +++ b/kallithea/model/db.py Wed Mar 29 22:12:50 2017 +0200 @@ -1515,7 +1515,12 @@ return repo def __json__(self): - return dict(landing_rev = self.landing_rev) + return dict( + repo_id=self.repo_id, + repo_name=self.repo_name, + landing_rev=self.landing_rev, + ) + class RepoGroup(Base, BaseDbModel): __tablename__ = 'groups' @@ -2238,6 +2243,13 @@ elif self.pull_request_id is not None: return self.pull_request.url(anchor=anchor) + def __json__(self): + return dict( + comment_id=self.comment_id, + username=self.author.username, + text=self.text, + ) + def deletable(self): return self.created_on > datetime.datetime.now() - datetime.timedelta(minutes=5) @@ -2408,9 +2420,24 @@ '''Return the id of this pull request, nicely formatted for displaying''' return self.make_nice_id(self.pull_request_id) + def get_api_data(self): + return self.__json__() + def __json__(self): return dict( - revisions=self.revisions + pull_request_id=self.pull_request_id, + url=self.url(), + reviewers=self.reviewers, + revisions=self.revisions, + owner=self.owner.username, + title=self.title, + description=self.description, + org_repo_url=self.org_repo.clone_url(), + org_ref_parts=self.org_ref_parts, + other_ref_parts=self.other_ref_parts, + status=self.status, + comments=self.comments, + statuses=self.statuses, ) def url(self, **kwargs): @@ -2446,6 +2473,11 @@ user = relationship('User') pull_request = relationship('PullRequest') + def __json__(self): + return dict( + username=self.user.username if self.user else None, + ) + class Notification(Base, BaseDbModel): __tablename__ = 'notifications' diff -r bf9900e6e177 -r 6452215a54ee kallithea/tests/api/api_base.py --- a/kallithea/tests/api/api_base.py Wed Mar 29 22:12:50 2017 +0200 +++ b/kallithea/tests/api/api_base.py Wed Mar 29 22:12:50 2017 +0200 @@ -19,6 +19,7 @@ import os import random import mock +import re from kallithea.tests.base import * from kallithea.tests.fixture import Fixture @@ -31,7 +32,8 @@ from kallithea.model.meta import Session from kallithea.model.scm import ScmModel from kallithea.model.gist import GistModel -from kallithea.model.db import Repository, User, Setting, Ui +from kallithea.model.changeset_status import ChangesetStatusModel +from kallithea.model.db import Repository, User, Setting, Ui, PullRequest, ChangesetStatus from kallithea.lib.utils2 import time_to_datetime @@ -2506,3 +2508,89 @@ response = api_call(self, params) expected = u'Access denied to repo %s' % self.REPO self._compare_error(id_, expected, given=response.body) + + def test_api_get_pullrequest(self): + pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'get test') + random_id = random.randrange(1, 9999) + params = json.dumps({ + "id": random_id, + "api_key": self.apikey, + "method": 'get_pullrequest', + "args": {"pullrequest_id": pull_request_id}, + }) + response = api_call(self, params) + pullrequest = PullRequest().get(pull_request_id) + expected = { + "status": "new", + "pull_request_id": pull_request_id, + "description": "No description", + "url": "/%s/pull-request/%s/_/%s" % (self.REPO, pull_request_id, "stable"), + "reviewers": [{"username": "test_regular"}], + "org_repo_url": "http://localhost:80/%s" % self.REPO, + "org_ref_parts": ["branch", "stable", self.TEST_PR_SRC], + "other_ref_parts": ["branch", "default", self.TEST_PR_DST], + "comments": [{"username": TEST_USER_ADMIN_LOGIN, "text": "", + "comment_id": pullrequest.comments[0].comment_id}], + "owner": TEST_USER_ADMIN_LOGIN, + "statuses": [{"status": "under_review", "reviewer": TEST_USER_ADMIN_LOGIN, "modified_at": "2000-01-01T00:00:00.000"} for i in range(0, len(self.TEST_PR_REVISIONS))], + "title": "get test", + "revisions": self.TEST_PR_REVISIONS, + } + self._compare_ok(random_id, expected, + given=re.sub("\d\d\d\d\-\d\d\-\d\dT\d\d\:\d\d\:\d\d\.\d\d\d", + "2000-01-01T00:00:00.000", response.body)) + + def test_api_close_pullrequest(self): + pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'close test') + random_id = random.randrange(1, 9999) + params = json.dumps({ + "id": random_id, + "api_key": self.apikey, + "method": "comment_pullrequest", + "args": {"pull_request_id": pull_request_id, "close_pr": True}, + }) + response = api_call(self, params) + self._compare_ok(random_id, True, given=response.body) + pullrequest = PullRequest().get(pull_request_id) + assert pullrequest.comments[-1].text == '' + assert pullrequest.status == PullRequest.STATUS_CLOSED + assert pullrequest.is_closed() == True + + def test_api_status_pullrequest(self): + pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, "status test") + + random_id = random.randrange(1, 9999) + params = json.dumps({ + "id": random_id, + "api_key": User.get_by_username(TEST_USER_REGULAR2_LOGIN).api_key, + "method": "comment_pullrequest", + "args": {"pull_request_id": pull_request_id, "status": ChangesetStatus.STATUS_APPROVED}, + }) + response = api_call(self, params) + pullrequest = PullRequest().get(pull_request_id) + self._compare_error(random_id, "No permission to change pull request status. User needs to be admin, owner or reviewer.", given=response.body) + assert ChangesetStatus.STATUS_UNDER_REVIEW == ChangesetStatusModel().calculate_pull_request_result(pullrequest)[2] + params = json.dumps({ + "id": random_id, + "api_key": User.get_by_username(TEST_USER_REGULAR_LOGIN).api_key, + "method": "comment_pullrequest", + "args": {"pull_request_id": pull_request_id, "status": ChangesetStatus.STATUS_APPROVED}, + }) + response = api_call(self, params) + self._compare_ok(random_id, True, given=response.body) + pullrequest = PullRequest().get(pull_request_id) + assert ChangesetStatus.STATUS_APPROVED == ChangesetStatusModel().calculate_pull_request_result(pullrequest)[2] + + def test_api_comment_pullrequest(self): + pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, "comment test") + random_id = random.randrange(1, 9999) + params = json.dumps({ + "id": random_id, + "api_key": self.apikey, + "method": "comment_pullrequest", + "args": {"pull_request_id": pull_request_id, "comment_msg": "Looks good to me"}, + }) + response = api_call(self, params) + self._compare_ok(random_id, True, given=response.body) + pullrequest = PullRequest().get(pull_request_id) + assert pullrequest.comments[-1].text == u'Looks good to me' diff -r bf9900e6e177 -r 6452215a54ee kallithea/tests/api/test_api_git.py --- a/kallithea/tests/api/test_api_git.py Wed Mar 29 22:12:50 2017 +0200 +++ b/kallithea/tests/api/test_api_git.py Wed Mar 29 22:12:50 2017 +0200 @@ -20,3 +20,9 @@ REPO = GIT_REPO REPO_TYPE = 'git' TEST_REVISION = GIT_TEST_REVISION + TEST_PR_SRC = u'c60f01b77c42dce653d6b1d3b04689862c261929' + TEST_PR_DST = u'10cddef6b794696066fb346434014f0a56810218' + TEST_PR_REVISIONS = [u'1bead5880d2dbe831762bf7fb439ba2919b75fdd', + u'9bcd3ecfc8832a8cd881c1c1bbe2d13ffa9d94c7', + u'283de4dfca8479875a1befb8d4059f3bbb725145', + u'c60f01b77c42dce653d6b1d3b04689862c261929'] diff -r bf9900e6e177 -r 6452215a54ee kallithea/tests/api/test_api_hg.py --- a/kallithea/tests/api/test_api_hg.py Wed Mar 29 22:12:50 2017 +0200 +++ b/kallithea/tests/api/test_api_hg.py Wed Mar 29 22:12:50 2017 +0200 @@ -20,3 +20,10 @@ REPO = HG_REPO REPO_TYPE = 'hg' TEST_REVISION = HG_TEST_REVISION + TEST_PR_SRC = u'4f7e2131323e0749a740c0a56ab68ae9269c562a' + TEST_PR_DST = u'92831aebf2f8dd4879e897024b89d09af214df1c' + TEST_PR_REVISIONS = [u'720bbdb27665d6262b313e8a541b654d0cbd5b27', + u'f41649565a9e89919a588a163e717b4084f8a3b1', + u'94f45ed825a113e61af7e141f44ca578374abef0', + u'fef5bfe1dc17611d5fb59a7f6f95c55c3606f933', + u'4f7e2131323e0749a740c0a56ab68ae9269c562a'] diff -r bf9900e6e177 -r 6452215a54ee kallithea/tests/fixture.py --- a/kallithea/tests/fixture.py Wed Mar 29 22:12:50 2017 +0200 +++ b/kallithea/tests/fixture.py Wed Mar 29 22:12:50 2017 +0200 @@ -22,6 +22,10 @@ import tarfile from os.path import dirname +import mock +from tg import request +from tg.util.webtest import test_context + from kallithea.model.db import Repository, User, RepoGroup, UserGroup, Gist, ChangesetStatus from kallithea.model.meta import Session from kallithea.model.repo import RepoModel @@ -32,9 +36,13 @@ from kallithea.model.scm import ScmModel from kallithea.model.comment import ChangesetCommentsModel from kallithea.model.changeset_status import ChangesetStatusModel +from kallithea.model.pull_request import CreatePullRequestAction#, CreatePullRequestIterationAction, PullRequestModel +from kallithea.lib import helpers +from kallithea.lib.auth import AuthUser from kallithea.lib.db_manage import DbManage from kallithea.lib.vcs.backends.base import EmptyChangeset -from kallithea.tests.base import invalidate_all_caches, GIT_REPO, HG_REPO, TESTS_TMP_PATH, TEST_USER_ADMIN_LOGIN +from kallithea.tests.base import invalidate_all_caches, GIT_REPO, HG_REPO, \ + TESTS_TMP_PATH, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN log = logging.getLogger(__name__) @@ -317,6 +325,21 @@ Session().commit() return csm + def create_pullrequest(self, testcontroller, repo_name, pr_src_rev, pr_dst_rev, title='title'): + org_ref = 'branch:stable:%s' % pr_src_rev + other_ref = 'branch:default:%s' % pr_dst_rev + with test_context(testcontroller.app): # needed to be able to mock request user + org_repo = other_repo = Repository.get_by_repo_name(repo_name) + owner_user = User.get_by_username(TEST_USER_ADMIN_LOGIN) + reviewers = [User.get_by_username(TEST_USER_REGULAR_LOGIN)] + request.authuser = request.user = AuthUser(dbuser=owner_user) + # creating a PR sends a message with an absolute URL - without routing that requires mocking + with mock.patch.object(helpers, 'url', (lambda arg, qualified=False, **kwargs: ('https://localhost' if qualified else '') + '/fake/' + arg)): + cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, 'No description', owner_user, reviewers) + pull_request = cmd.execute() + Session().commit() + return pull_request.pull_request_id + #============================================================================== # Global test environment setup