changeset 6655:6452215a54ee

api: add get_pullrequest and comment_pullrequest methods Modified by Mads Kiilerich, mainly to let the test helper function create_pullrequest use model directly.
author domruf <dominikruf@gmail.com>
date Wed, 29 Mar 2017 22:12:50 +0200
parents bf9900e6e177
children f0705609e494
files docs/api/api.rst kallithea/controllers/api/api.py kallithea/model/db.py kallithea/tests/api/api_base.py kallithea/tests/api/test_api_git.py kallithea/tests/api/test_api_hg.py kallithea/tests/fixture.py
diffstat 7 files changed, 315 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- 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 : <id_for_response>
+    api_key : "<api_key>"
+    method  : "get_pullrequest"
+    args:     {
+                "pullrequest_id" : "<pullrequest_id>",
+              }
+
+OUTPUT::
+
+    id : <id_given_in_input>
+    result: {
+        "status": "<pull_request_status>",
+        "pull_request_id": <pull_request_id>,
+        "description": "<pull_request_description>",
+        "title": "<pull_request_title>",
+        "url": "<pull_request_url>",
+        "reviewers": [
+          {
+            "username": "<user_name>",
+          },
+          ...
+        ],
+        "org_repo_url": "<repo_url>",
+        "org_ref_parts": [
+          "<ref_type>",
+          "<ref_name>",
+          "<raw_id>"
+        ],
+        "other_ref_parts": [
+          "<ref_type>",
+          "<ref_name>",
+          "<raw_id>"
+        ],
+        "comments": [
+          {
+            "username": "<user_name>",
+            "text": "<comment text>",
+            "comment_id": "<comment_id>",
+          },
+          ...
+        ],
+        "owner": "<username>",
+        "statuses": [
+          {
+            "status": "<status_of_review>",        # "under_review", "approved" or "rejected"
+            "reviewer": "<user_name>",
+            "modified_at": "<date_time_of_review>" # iso 8601 date, server's timezone
+          },
+          ...
+        ],
+        "revisions": [
+          "<raw_id>",
+          ...
+        ]
+    },
+    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 : <id_for_response>
+    api_key : "<api_key>"
+    method  : "comment_pullrequest"
+    args:     {
+                "pull_request_id":  "<pull_request_id>",
+                "comment_msg":      Optional(''),
+                "status":           Optional(None),     # "under_review", "approved" or "rejected"
+                "close_pr":         Optional(False)",
+              }
+
+OUTPUT::
+
+    id : <id_given_in_input>
+    result: True
+    error:  null
+
 
 API access for web views
 ------------------------
--- 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
--- 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'
--- 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'
--- 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']
--- 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']
--- 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