##// END OF EJS Templates
api: added get_comment method, and return versions for comments to allow simple edits via API.
marcink -
r4440:c1776819 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,133 +1,134 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.api.utils import Optional, OAttr
23 from rhodecode.api.utils import Optional, OAttr
24 from rhodecode.api.tests.utils import (
24 from rhodecode.api.tests.utils import (
25 build_data, api_call, assert_error, assert_ok)
25 build_data, api_call, assert_error, assert_ok)
26
26
27
27
28 @pytest.mark.usefixtures("testuser_api", "app")
28 @pytest.mark.usefixtures("testuser_api", "app")
29 class TestApi(object):
29 class TestApi(object):
30 maxDiff = None
30 maxDiff = None
31
31
32 def test_Optional_object(self):
32 def test_Optional_object(self):
33
33
34 option1 = Optional(None)
34 option1 = Optional(None)
35 assert '<Optional:%s>' % (None,) == repr(option1)
35 assert '<Optional:%s>' % (None,) == repr(option1)
36 assert option1() is None
36 assert option1() is None
37
37
38 assert 1 == Optional.extract(Optional(1))
38 assert 1 == Optional.extract(Optional(1))
39 assert 'example' == Optional.extract('example')
39 assert 'example' == Optional.extract('example')
40
40
41 def test_Optional_OAttr(self):
41 def test_Optional_OAttr(self):
42 option1 = Optional(OAttr('apiuser'))
42 option1 = Optional(OAttr('apiuser'))
43 assert 'apiuser' == Optional.extract(option1)
43 assert 'apiuser' == Optional.extract(option1)
44
44
45 def test_OAttr_object(self):
45 def test_OAttr_object(self):
46 oattr1 = OAttr('apiuser')
46 oattr1 = OAttr('apiuser')
47 assert '<OptionalAttr:apiuser>' == repr(oattr1)
47 assert '<OptionalAttr:apiuser>' == repr(oattr1)
48 assert oattr1() == oattr1
48 assert oattr1() == oattr1
49
49
50 def test_api_wrong_key(self):
50 def test_api_wrong_key(self):
51 id_, params = build_data('trololo', 'get_user')
51 id_, params = build_data('trololo', 'get_user')
52 response = api_call(self.app, params)
52 response = api_call(self.app, params)
53
53
54 expected = 'Invalid API KEY'
54 expected = 'Invalid API KEY'
55 assert_error(id_, expected, given=response.body)
55 assert_error(id_, expected, given=response.body)
56
56
57 def test_api_missing_non_optional_param(self):
57 def test_api_missing_non_optional_param(self):
58 id_, params = build_data(self.apikey, 'get_repo')
58 id_, params = build_data(self.apikey, 'get_repo')
59 response = api_call(self.app, params)
59 response = api_call(self.app, params)
60
60
61 expected = 'Missing non optional `repoid` arg in JSON DATA'
61 expected = 'Missing non optional `repoid` arg in JSON DATA'
62 assert_error(id_, expected, given=response.body)
62 assert_error(id_, expected, given=response.body)
63
63
64 def test_api_missing_non_optional_param_args_null(self):
64 def test_api_missing_non_optional_param_args_null(self):
65 id_, params = build_data(self.apikey, 'get_repo')
65 id_, params = build_data(self.apikey, 'get_repo')
66 params = params.replace('"args": {}', '"args": null')
66 params = params.replace('"args": {}', '"args": null')
67 response = api_call(self.app, params)
67 response = api_call(self.app, params)
68
68
69 expected = 'Missing non optional `repoid` arg in JSON DATA'
69 expected = 'Missing non optional `repoid` arg in JSON DATA'
70 assert_error(id_, expected, given=response.body)
70 assert_error(id_, expected, given=response.body)
71
71
72 def test_api_missing_non_optional_param_args_bad(self):
72 def test_api_missing_non_optional_param_args_bad(self):
73 id_, params = build_data(self.apikey, 'get_repo')
73 id_, params = build_data(self.apikey, 'get_repo')
74 params = params.replace('"args": {}', '"args": 1')
74 params = params.replace('"args": {}', '"args": 1')
75 response = api_call(self.app, params)
75 response = api_call(self.app, params)
76
76
77 expected = 'Missing non optional `repoid` arg in JSON DATA'
77 expected = 'Missing non optional `repoid` arg in JSON DATA'
78 assert_error(id_, expected, given=response.body)
78 assert_error(id_, expected, given=response.body)
79
79
80 def test_api_non_existing_method(self, request):
80 def test_api_non_existing_method(self, request):
81 id_, params = build_data(self.apikey, 'not_existing', args='xx')
81 id_, params = build_data(self.apikey, 'not_existing', args='xx')
82 response = api_call(self.app, params)
82 response = api_call(self.app, params)
83 expected = 'No such method: not_existing. Similar methods: none'
83 expected = 'No such method: not_existing. Similar methods: none'
84 assert_error(id_, expected, given=response.body)
84 assert_error(id_, expected, given=response.body)
85
85
86 def test_api_non_existing_method_have_similar(self, request):
86 def test_api_non_existing_method_have_similar(self, request):
87 id_, params = build_data(self.apikey, 'comment', args='xx')
87 id_, params = build_data(self.apikey, 'comment', args='xx')
88 response = api_call(self.app, params)
88 response = api_call(self.app, params)
89 expected = 'No such method: comment. ' \
89 expected = 'No such method: comment. ' \
90 'Similar methods: changeset_comment, comment_pull_request, edit_comment, ' \
90 'Similar methods: changeset_comment, comment_pull_request, ' \
91 'get_pull_request_comments, comment_commit, get_repo_comments'
91 'get_pull_request_comments, comment_commit, edit_comment, ' \
92 'get_comment, get_repo_comments'
92 assert_error(id_, expected, given=response.body)
93 assert_error(id_, expected, given=response.body)
93
94
94 def test_api_disabled_user(self, request):
95 def test_api_disabled_user(self, request):
95
96
96 def set_active(active):
97 def set_active(active):
97 from rhodecode.model.db import Session, User
98 from rhodecode.model.db import Session, User
98 user = User.get_by_auth_token(self.apikey)
99 user = User.get_by_auth_token(self.apikey)
99 user.active = active
100 user.active = active
100 Session().add(user)
101 Session().add(user)
101 Session().commit()
102 Session().commit()
102
103
103 request.addfinalizer(lambda: set_active(True))
104 request.addfinalizer(lambda: set_active(True))
104
105
105 set_active(False)
106 set_active(False)
106 id_, params = build_data(self.apikey, 'test', args='xx')
107 id_, params = build_data(self.apikey, 'test', args='xx')
107 response = api_call(self.app, params)
108 response = api_call(self.app, params)
108 expected = 'Request from this user not allowed'
109 expected = 'Request from this user not allowed'
109 assert_error(id_, expected, given=response.body)
110 assert_error(id_, expected, given=response.body)
110
111
111 def test_api_args_is_null(self):
112 def test_api_args_is_null(self):
112 __, params = build_data(self.apikey, 'get_users', )
113 __, params = build_data(self.apikey, 'get_users', )
113 params = params.replace('"args": {}', '"args": null')
114 params = params.replace('"args": {}', '"args": null')
114 response = api_call(self.app, params)
115 response = api_call(self.app, params)
115 assert response.status == '200 OK'
116 assert response.status == '200 OK'
116
117
117 def test_api_args_is_bad(self):
118 def test_api_args_is_bad(self):
118 __, params = build_data(self.apikey, 'get_users', )
119 __, params = build_data(self.apikey, 'get_users', )
119 params = params.replace('"args": {}', '"args": 1')
120 params = params.replace('"args": {}', '"args": 1')
120 response = api_call(self.app, params)
121 response = api_call(self.app, params)
121 assert response.status == '200 OK'
122 assert response.status == '200 OK'
122
123
123 def test_api_args_different_args(self):
124 def test_api_args_different_args(self):
124 import string
125 import string
125 expected = {
126 expected = {
126 'ascii_letters': string.ascii_letters,
127 'ascii_letters': string.ascii_letters,
127 'ws': string.whitespace,
128 'ws': string.whitespace,
128 'printables': string.printable
129 'printables': string.printable
129 }
130 }
130 id_, params = build_data(self.apikey, 'test', args=expected)
131 id_, params = build_data(self.apikey, 'test', args=expected)
131 response = api_call(self.app, params)
132 response = api_call(self.app, params)
132 assert response.status == '200 OK'
133 assert response.status == '200 OK'
133 assert_ok(id_, expected, response.body)
134 assert_ok(id_, expected, response.body)
@@ -1,61 +1,63 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23
23
24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok
24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok
25
25
26
26
27 @pytest.mark.usefixtures("testuser_api", "app")
27 @pytest.mark.usefixtures("testuser_api", "app")
28 class TestGetMethod(object):
28 class TestGetMethod(object):
29 def test_get_methods_no_matches(self):
29 def test_get_methods_no_matches(self):
30 id_, params = build_data(self.apikey, 'get_method', pattern='hello')
30 id_, params = build_data(self.apikey, 'get_method', pattern='hello')
31 response = api_call(self.app, params)
31 response = api_call(self.app, params)
32
32
33 expected = []
33 expected = []
34 assert_ok(id_, expected, given=response.body)
34 assert_ok(id_, expected, given=response.body)
35
35
36 def test_get_methods(self):
36 def test_get_methods(self):
37 id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
37 id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
38 response = api_call(self.app, params)
38 response = api_call(self.app, params)
39
39
40 expected = ['changeset_comment', 'comment_pull_request', 'edit_comment',
40 expected = [
41 'get_pull_request_comments', 'comment_commit', 'get_repo_comments']
41 'changeset_comment', 'comment_pull_request', 'get_pull_request_comments',
42 'comment_commit', 'edit_comment', 'get_comment', 'get_repo_comments'
43 ]
42 assert_ok(id_, expected, given=response.body)
44 assert_ok(id_, expected, given=response.body)
43
45
44 def test_get_methods_on_single_match(self):
46 def test_get_methods_on_single_match(self):
45 id_, params = build_data(self.apikey, 'get_method',
47 id_, params = build_data(self.apikey, 'get_method',
46 pattern='*comment_commit*')
48 pattern='*comment_commit*')
47 response = api_call(self.app, params)
49 response = api_call(self.app, params)
48
50
49 expected = ['comment_commit',
51 expected = ['comment_commit',
50 {'apiuser': '<RequiredType>',
52 {'apiuser': '<RequiredType>',
51 'comment_type': "<Optional:u'note'>",
53 'comment_type': "<Optional:u'note'>",
52 'commit_id': '<RequiredType>',
54 'commit_id': '<RequiredType>',
53 'extra_recipients': '<Optional:[]>',
55 'extra_recipients': '<Optional:[]>',
54 'message': '<RequiredType>',
56 'message': '<RequiredType>',
55 'repoid': '<RequiredType>',
57 'repoid': '<RequiredType>',
56 'request': '<RequiredType>',
58 'request': '<RequiredType>',
57 'resolves_comment_id': '<Optional:None>',
59 'resolves_comment_id': '<Optional:None>',
58 'status': '<Optional:None>',
60 'status': '<Optional:None>',
59 'userid': '<Optional:<OptionalAttr:apiuser>>',
61 'userid': '<Optional:<OptionalAttr:apiuser>>',
60 'send_email': '<Optional:True>'}]
62 'send_email': '<Optional:True>'}]
61 assert_ok(id_, expected, given=response.body)
63 assert_ok(id_, expected, given=response.body)
@@ -1,86 +1,87 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23 import urlobject
23 import urlobject
24
24
25 from rhodecode.api.tests.utils import (
25 from rhodecode.api.tests.utils import (
26 build_data, api_call, assert_error, assert_ok)
26 build_data, api_call, assert_error, assert_ok)
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.utils2 import safe_unicode
28 from rhodecode.lib.utils2 import safe_unicode
29
29
30 pytestmark = pytest.mark.backends("git", "hg")
30 pytestmark = pytest.mark.backends("git", "hg")
31
31
32
32
33 @pytest.mark.usefixtures("testuser_api", "app")
33 @pytest.mark.usefixtures("testuser_api", "app")
34 class TestGetPullRequestComments(object):
34 class TestGetPullRequestComments(object):
35
35
36 def test_api_get_pull_request_comments(self, pr_util, http_host_only_stub):
36 def test_api_get_pull_request_comments(self, pr_util, http_host_only_stub):
37 from rhodecode.model.pull_request import PullRequestModel
37 from rhodecode.model.pull_request import PullRequestModel
38
38
39 pull_request = pr_util.create_pull_request(mergeable=True)
39 pull_request = pr_util.create_pull_request(mergeable=True)
40 id_, params = build_data(
40 id_, params = build_data(
41 self.apikey, 'get_pull_request_comments',
41 self.apikey, 'get_pull_request_comments',
42 pullrequestid=pull_request.pull_request_id)
42 pullrequestid=pull_request.pull_request_id)
43
43
44 response = api_call(self.app, params)
44 response = api_call(self.app, params)
45
45
46 assert response.status == '200 OK'
46 assert response.status == '200 OK'
47 resp_date = response.json['result'][0]['comment_created_on']
47 resp_date = response.json['result'][0]['comment_created_on']
48 resp_comment_id = response.json['result'][0]['comment_id']
48 resp_comment_id = response.json['result'][0]['comment_id']
49
49
50 expected = [
50 expected = [
51 {'comment_author': {'active': True,
51 {'comment_author': {'active': True,
52 'full_name_or_username': 'RhodeCode Admin',
52 'full_name_or_username': 'RhodeCode Admin',
53 'username': 'test_admin'},
53 'username': 'test_admin'},
54 'comment_created_on': resp_date,
54 'comment_created_on': resp_date,
55 'comment_f_path': None,
55 'comment_f_path': None,
56 'comment_id': resp_comment_id,
56 'comment_id': resp_comment_id,
57 'comment_lineno': None,
57 'comment_lineno': None,
58 'comment_status': {'status': 'under_review',
58 'comment_status': {'status': 'under_review',
59 'status_lbl': 'Under Review'},
59 'status_lbl': 'Under Review'},
60 'comment_text': 'Auto status change to |new_status|\n\n.. |new_status| replace:: *"Under Review"*',
60 'comment_text': 'Auto status change to |new_status|\n\n.. |new_status| replace:: *"Under Review"*',
61 'comment_type': 'note',
61 'comment_type': 'note',
62 'comment_resolved_by': None,
62 'comment_resolved_by': None,
63 'pull_request_version': None,
63 'pull_request_version': None,
64 'comment_last_version': 0,
64 'comment_commit_id': None,
65 'comment_commit_id': None,
65 'comment_pull_request_id': pull_request.pull_request_id
66 'comment_pull_request_id': pull_request.pull_request_id
66 }
67 }
67 ]
68 ]
68 assert_ok(id_, expected, response.body)
69 assert_ok(id_, expected, response.body)
69
70
70 def test_api_get_pull_request_comments_repo_error(self, pr_util):
71 def test_api_get_pull_request_comments_repo_error(self, pr_util):
71 pull_request = pr_util.create_pull_request()
72 pull_request = pr_util.create_pull_request()
72 id_, params = build_data(
73 id_, params = build_data(
73 self.apikey, 'get_pull_request_comments',
74 self.apikey, 'get_pull_request_comments',
74 repoid=666, pullrequestid=pull_request.pull_request_id)
75 repoid=666, pullrequestid=pull_request.pull_request_id)
75 response = api_call(self.app, params)
76 response = api_call(self.app, params)
76
77
77 expected = 'repository `666` does not exist'
78 expected = 'repository `666` does not exist'
78 assert_error(id_, expected, given=response.body)
79 assert_error(id_, expected, given=response.body)
79
80
80 def test_api_get_pull_request_comments_pull_request_error(self):
81 def test_api_get_pull_request_comments_pull_request_error(self):
81 id_, params = build_data(
82 id_, params = build_data(
82 self.apikey, 'get_pull_request_comments', pullrequestid=666)
83 self.apikey, 'get_pull_request_comments', pullrequestid=666)
83 response = api_call(self.app, params)
84 response = api_call(self.app, params)
84
85
85 expected = 'pull request `666` does not exist'
86 expected = 'pull request `666` does not exist'
86 assert_error(id_, expected, given=response.body)
87 assert_error(id_, expected, given=response.body)
@@ -1,110 +1,142 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.db import User, ChangesetComment
24 from rhodecode.model.db import User, ChangesetComment
25 from rhodecode.model.meta import Session
25 from rhodecode.model.meta import Session
26 from rhodecode.model.comment import CommentsModel
26 from rhodecode.model.comment import CommentsModel
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_error, assert_call_ok)
28 build_data, api_call, assert_error, assert_call_ok)
29
29
30
30
31 @pytest.fixture()
31 @pytest.fixture()
32 def make_repo_comments_factory(request):
32 def make_repo_comments_factory(request):
33
33
34 class Make(object):
34 class Make(object):
35
35
36 def make_comments(self, repo):
36 def make_comments(self, repo):
37 user = User.get_first_super_admin()
37 user = User.get_first_super_admin()
38 commit = repo.scm_instance()[0]
38 commit = repo.scm_instance()[0]
39
39
40 commit_id = commit.raw_id
40 commit_id = commit.raw_id
41 file_0 = commit.affected_files[0]
41 file_0 = commit.affected_files[0]
42 comments = []
42 comments = []
43
43
44 # general
44 # general
45 CommentsModel().create(
45 comment = CommentsModel().create(
46 text='General Comment', repo=repo, user=user, commit_id=commit_id,
46 text='General Comment', repo=repo, user=user, commit_id=commit_id,
47 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
47 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
48 comments.append(comment)
48
49
49 # inline
50 # inline
50 CommentsModel().create(
51 comment = CommentsModel().create(
51 text='Inline Comment', repo=repo, user=user, commit_id=commit_id,
52 text='Inline Comment', repo=repo, user=user, commit_id=commit_id,
52 f_path=file_0, line_no='n1',
53 f_path=file_0, line_no='n1',
53 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
54 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
55 comments.append(comment)
54
56
55 # todo
57 # todo
56 CommentsModel().create(
58 comment = CommentsModel().create(
57 text='INLINE TODO Comment', repo=repo, user=user, commit_id=commit_id,
59 text='INLINE TODO Comment', repo=repo, user=user, commit_id=commit_id,
58 f_path=file_0, line_no='n1',
60 f_path=file_0, line_no='n1',
59 comment_type=ChangesetComment.COMMENT_TYPE_TODO, send_email=False)
61 comment_type=ChangesetComment.COMMENT_TYPE_TODO, send_email=False)
62 comments.append(comment)
60
63
61 @request.addfinalizer
64 return comments
62 def cleanup():
65
63 for comment in comments:
64 Session().delete(comment)
65 return Make()
66 return Make()
66
67
67
68
68 @pytest.mark.usefixtures("testuser_api", "app")
69 @pytest.mark.usefixtures("testuser_api", "app")
69 class TestGetRepo(object):
70 class TestGetRepo(object):
70
71
71 @pytest.mark.parametrize('filters, expected_count', [
72 @pytest.mark.parametrize('filters, expected_count', [
72 ({}, 3),
73 ({}, 3),
73 ({'comment_type': ChangesetComment.COMMENT_TYPE_NOTE}, 2),
74 ({'comment_type': ChangesetComment.COMMENT_TYPE_NOTE}, 2),
74 ({'comment_type': ChangesetComment.COMMENT_TYPE_TODO}, 1),
75 ({'comment_type': ChangesetComment.COMMENT_TYPE_TODO}, 1),
75 ({'commit_id': 'FILLED DYNAMIC'}, 3),
76 ({'commit_id': 'FILLED DYNAMIC'}, 3),
76 ])
77 ])
77 def test_api_get_repo_comments(self, backend, user_util,
78 def test_api_get_repo_comments(self, backend, user_util,
78 make_repo_comments_factory, filters, expected_count):
79 make_repo_comments_factory, filters, expected_count):
79 commits = [{'message': 'A'}, {'message': 'B'}]
80 commits = [{'message': 'A'}, {'message': 'B'}]
80 repo = backend.create_repo(commits=commits)
81 repo = backend.create_repo(commits=commits)
81 make_repo_comments_factory.make_comments(repo)
82 make_repo_comments_factory.make_comments(repo)
82
83
83 api_call_params = {'repoid': repo.repo_name,}
84 api_call_params = {'repoid': repo.repo_name,}
84 api_call_params.update(filters)
85 api_call_params.update(filters)
85
86
86 if 'commit_id' in api_call_params:
87 if 'commit_id' in api_call_params:
87 commit = repo.scm_instance()[0]
88 commit = repo.scm_instance()[0]
88 commit_id = commit.raw_id
89 commit_id = commit.raw_id
89 api_call_params['commit_id'] = commit_id
90 api_call_params['commit_id'] = commit_id
90
91
91 id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params)
92 id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params)
92 response = api_call(self.app, params)
93 response = api_call(self.app, params)
93 result = assert_call_ok(id_, given=response.body)
94 result = assert_call_ok(id_, given=response.body)
94
95
95 assert len(result) == expected_count
96 assert len(result) == expected_count
96
97
97 def test_api_get_repo_comments_wrong_comment_type(
98 def test_api_get_repo_comments_wrong_comment_type(
98 self, make_repo_comments_factory, backend_hg):
99 self, make_repo_comments_factory, backend_hg):
99 commits = [{'message': 'A'}, {'message': 'B'}]
100 commits = [{'message': 'A'}, {'message': 'B'}]
100 repo = backend_hg.create_repo(commits=commits)
101 repo = backend_hg.create_repo(commits=commits)
101 make_repo_comments_factory.make_comments(repo)
102 make_repo_comments_factory.make_comments(repo)
102
103
103 api_call_params = {'repoid': repo.repo_name}
104 api_call_params = {'repoid': repo.repo_name}
104 api_call_params.update({'comment_type': 'bogus'})
105 api_call_params.update({'comment_type': 'bogus'})
105
106
106 expected = 'comment_type must be one of `{}` got {}'.format(
107 expected = 'comment_type must be one of `{}` got {}'.format(
107 ChangesetComment.COMMENT_TYPES, 'bogus')
108 ChangesetComment.COMMENT_TYPES, 'bogus')
108 id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params)
109 id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params)
109 response = api_call(self.app, params)
110 response = api_call(self.app, params)
110 assert_error(id_, expected, given=response.body)
111 assert_error(id_, expected, given=response.body)
112
113 def test_api_get_comment(self, make_repo_comments_factory, backend_hg):
114 commits = [{'message': 'A'}, {'message': 'B'}]
115 repo = backend_hg.create_repo(commits=commits)
116
117 comments = make_repo_comments_factory.make_comments(repo)
118 comment_ids = [x.comment_id for x in comments]
119 Session().commit()
120
121 for comment_id in comment_ids:
122 id_, params = build_data(self.apikey, 'get_comment',
123 **{'comment_id': comment_id})
124 response = api_call(self.app, params)
125 result = assert_call_ok(id_, given=response.body)
126 assert result['comment_id'] == comment_id
127
128 def test_api_get_comment_no_access(self, make_repo_comments_factory, backend_hg, user_util):
129 commits = [{'message': 'A'}, {'message': 'B'}]
130 repo = backend_hg.create_repo(commits=commits)
131 comments = make_repo_comments_factory.make_comments(repo)
132 comment_id = comments[0].comment_id
133
134 test_user = user_util.create_user()
135 user_util.grant_user_permission_to_repo(repo, test_user, 'repository.none')
136
137 id_, params = build_data(test_user.api_key, 'get_comment',
138 **{'comment_id': comment_id})
139 response = api_call(self.app, params)
140 assert_error(id_,
141 expected='comment `{}` does not exist'.format(comment_id),
142 given=response.body)
@@ -1,1092 +1,1018 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.exceptions import CommentVersionMismatch
31 from rhodecode.lib.base import vcs_operation_context
30 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
32 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
37 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
38 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
40 ReviewerListSchema)
41
39
42 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
43
41
44
42
45 @jsonrpc_method()
43 @jsonrpc_method()
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
44 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
47 merge_state=Optional(False)):
45 merge_state=Optional(False)):
48 """
46 """
49 Get a pull request based on the given ID.
47 Get a pull request based on the given ID.
50
48
51 :param apiuser: This is filled automatically from the |authtoken|.
49 :param apiuser: This is filled automatically from the |authtoken|.
52 :type apiuser: AuthUser
50 :type apiuser: AuthUser
53 :param repoid: Optional, repository name or repository ID from where
51 :param repoid: Optional, repository name or repository ID from where
54 the pull request was opened.
52 the pull request was opened.
55 :type repoid: str or int
53 :type repoid: str or int
56 :param pullrequestid: ID of the requested pull request.
54 :param pullrequestid: ID of the requested pull request.
57 :type pullrequestid: int
55 :type pullrequestid: int
58 :param merge_state: Optional calculate merge state for each repository.
56 :param merge_state: Optional calculate merge state for each repository.
59 This could result in longer time to fetch the data
57 This could result in longer time to fetch the data
60 :type merge_state: bool
58 :type merge_state: bool
61
59
62 Example output:
60 Example output:
63
61
64 .. code-block:: bash
62 .. code-block:: bash
65
63
66 "id": <id_given_in_input>,
64 "id": <id_given_in_input>,
67 "result":
65 "result":
68 {
66 {
69 "pull_request_id": "<pull_request_id>",
67 "pull_request_id": "<pull_request_id>",
70 "url": "<url>",
68 "url": "<url>",
71 "title": "<title>",
69 "title": "<title>",
72 "description": "<description>",
70 "description": "<description>",
73 "status" : "<status>",
71 "status" : "<status>",
74 "created_on": "<date_time_created>",
72 "created_on": "<date_time_created>",
75 "updated_on": "<date_time_updated>",
73 "updated_on": "<date_time_updated>",
76 "versions": "<number_or_versions_of_pr>",
74 "versions": "<number_or_versions_of_pr>",
77 "commit_ids": [
75 "commit_ids": [
78 ...
76 ...
79 "<commit_id>",
77 "<commit_id>",
80 "<commit_id>",
78 "<commit_id>",
81 ...
79 ...
82 ],
80 ],
83 "review_status": "<review_status>",
81 "review_status": "<review_status>",
84 "mergeable": {
82 "mergeable": {
85 "status": "<bool>",
83 "status": "<bool>",
86 "message": "<message>",
84 "message": "<message>",
87 },
85 },
88 "source": {
86 "source": {
89 "clone_url": "<clone_url>",
87 "clone_url": "<clone_url>",
90 "repository": "<repository_name>",
88 "repository": "<repository_name>",
91 "reference":
89 "reference":
92 {
90 {
93 "name": "<name>",
91 "name": "<name>",
94 "type": "<type>",
92 "type": "<type>",
95 "commit_id": "<commit_id>",
93 "commit_id": "<commit_id>",
96 }
94 }
97 },
95 },
98 "target": {
96 "target": {
99 "clone_url": "<clone_url>",
97 "clone_url": "<clone_url>",
100 "repository": "<repository_name>",
98 "repository": "<repository_name>",
101 "reference":
99 "reference":
102 {
100 {
103 "name": "<name>",
101 "name": "<name>",
104 "type": "<type>",
102 "type": "<type>",
105 "commit_id": "<commit_id>",
103 "commit_id": "<commit_id>",
106 }
104 }
107 },
105 },
108 "merge": {
106 "merge": {
109 "clone_url": "<clone_url>",
107 "clone_url": "<clone_url>",
110 "reference":
108 "reference":
111 {
109 {
112 "name": "<name>",
110 "name": "<name>",
113 "type": "<type>",
111 "type": "<type>",
114 "commit_id": "<commit_id>",
112 "commit_id": "<commit_id>",
115 }
113 }
116 },
114 },
117 "author": <user_obj>,
115 "author": <user_obj>,
118 "reviewers": [
116 "reviewers": [
119 ...
117 ...
120 {
118 {
121 "user": "<user_obj>",
119 "user": "<user_obj>",
122 "review_status": "<review_status>",
120 "review_status": "<review_status>",
123 }
121 }
124 ...
122 ...
125 ]
123 ]
126 },
124 },
127 "error": null
125 "error": null
128 """
126 """
129
127
130 pull_request = get_pull_request_or_error(pullrequestid)
128 pull_request = get_pull_request_or_error(pullrequestid)
131 if Optional.extract(repoid):
129 if Optional.extract(repoid):
132 repo = get_repo_or_error(repoid)
130 repo = get_repo_or_error(repoid)
133 else:
131 else:
134 repo = pull_request.target_repo
132 repo = pull_request.target_repo
135
133
136 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
134 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
137 raise JSONRPCError('repository `%s` or pull request `%s` '
135 raise JSONRPCError('repository `%s` or pull request `%s` '
138 'does not exist' % (repoid, pullrequestid))
136 'does not exist' % (repoid, pullrequestid))
139
137
140 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
138 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
141 # otherwise we can lock the repo on calculation of merge state while update/merge
139 # otherwise we can lock the repo on calculation of merge state while update/merge
142 # is happening.
140 # is happening.
143 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
141 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
144 merge_state = Optional.extract(merge_state, binary=True) and pr_created
142 merge_state = Optional.extract(merge_state, binary=True) and pr_created
145 data = pull_request.get_api_data(with_merge_state=merge_state)
143 data = pull_request.get_api_data(with_merge_state=merge_state)
146 return data
144 return data
147
145
148
146
149 @jsonrpc_method()
147 @jsonrpc_method()
150 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
148 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
151 merge_state=Optional(False)):
149 merge_state=Optional(False)):
152 """
150 """
153 Get all pull requests from the repository specified in `repoid`.
151 Get all pull requests from the repository specified in `repoid`.
154
152
155 :param apiuser: This is filled automatically from the |authtoken|.
153 :param apiuser: This is filled automatically from the |authtoken|.
156 :type apiuser: AuthUser
154 :type apiuser: AuthUser
157 :param repoid: Optional repository name or repository ID.
155 :param repoid: Optional repository name or repository ID.
158 :type repoid: str or int
156 :type repoid: str or int
159 :param status: Only return pull requests with the specified status.
157 :param status: Only return pull requests with the specified status.
160 Valid options are.
158 Valid options are.
161 * ``new`` (default)
159 * ``new`` (default)
162 * ``open``
160 * ``open``
163 * ``closed``
161 * ``closed``
164 :type status: str
162 :type status: str
165 :param merge_state: Optional calculate merge state for each repository.
163 :param merge_state: Optional calculate merge state for each repository.
166 This could result in longer time to fetch the data
164 This could result in longer time to fetch the data
167 :type merge_state: bool
165 :type merge_state: bool
168
166
169 Example output:
167 Example output:
170
168
171 .. code-block:: bash
169 .. code-block:: bash
172
170
173 "id": <id_given_in_input>,
171 "id": <id_given_in_input>,
174 "result":
172 "result":
175 [
173 [
176 ...
174 ...
177 {
175 {
178 "pull_request_id": "<pull_request_id>",
176 "pull_request_id": "<pull_request_id>",
179 "url": "<url>",
177 "url": "<url>",
180 "title" : "<title>",
178 "title" : "<title>",
181 "description": "<description>",
179 "description": "<description>",
182 "status": "<status>",
180 "status": "<status>",
183 "created_on": "<date_time_created>",
181 "created_on": "<date_time_created>",
184 "updated_on": "<date_time_updated>",
182 "updated_on": "<date_time_updated>",
185 "commit_ids": [
183 "commit_ids": [
186 ...
184 ...
187 "<commit_id>",
185 "<commit_id>",
188 "<commit_id>",
186 "<commit_id>",
189 ...
187 ...
190 ],
188 ],
191 "review_status": "<review_status>",
189 "review_status": "<review_status>",
192 "mergeable": {
190 "mergeable": {
193 "status": "<bool>",
191 "status": "<bool>",
194 "message: "<message>",
192 "message: "<message>",
195 },
193 },
196 "source": {
194 "source": {
197 "clone_url": "<clone_url>",
195 "clone_url": "<clone_url>",
198 "reference":
196 "reference":
199 {
197 {
200 "name": "<name>",
198 "name": "<name>",
201 "type": "<type>",
199 "type": "<type>",
202 "commit_id": "<commit_id>",
200 "commit_id": "<commit_id>",
203 }
201 }
204 },
202 },
205 "target": {
203 "target": {
206 "clone_url": "<clone_url>",
204 "clone_url": "<clone_url>",
207 "reference":
205 "reference":
208 {
206 {
209 "name": "<name>",
207 "name": "<name>",
210 "type": "<type>",
208 "type": "<type>",
211 "commit_id": "<commit_id>",
209 "commit_id": "<commit_id>",
212 }
210 }
213 },
211 },
214 "merge": {
212 "merge": {
215 "clone_url": "<clone_url>",
213 "clone_url": "<clone_url>",
216 "reference":
214 "reference":
217 {
215 {
218 "name": "<name>",
216 "name": "<name>",
219 "type": "<type>",
217 "type": "<type>",
220 "commit_id": "<commit_id>",
218 "commit_id": "<commit_id>",
221 }
219 }
222 },
220 },
223 "author": <user_obj>,
221 "author": <user_obj>,
224 "reviewers": [
222 "reviewers": [
225 ...
223 ...
226 {
224 {
227 "user": "<user_obj>",
225 "user": "<user_obj>",
228 "review_status": "<review_status>",
226 "review_status": "<review_status>",
229 }
227 }
230 ...
228 ...
231 ]
229 ]
232 }
230 }
233 ...
231 ...
234 ],
232 ],
235 "error": null
233 "error": null
236
234
237 """
235 """
238 repo = get_repo_or_error(repoid)
236 repo = get_repo_or_error(repoid)
239 if not has_superadmin_permission(apiuser):
237 if not has_superadmin_permission(apiuser):
240 _perms = (
238 _perms = (
241 'repository.admin', 'repository.write', 'repository.read',)
239 'repository.admin', 'repository.write', 'repository.read',)
242 validate_repo_permissions(apiuser, repoid, repo, _perms)
240 validate_repo_permissions(apiuser, repoid, repo, _perms)
243
241
244 status = Optional.extract(status)
242 status = Optional.extract(status)
245 merge_state = Optional.extract(merge_state, binary=True)
243 merge_state = Optional.extract(merge_state, binary=True)
246 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
244 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
247 order_by='id', order_dir='desc')
245 order_by='id', order_dir='desc')
248 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
246 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
249 return data
247 return data
250
248
251
249
252 @jsonrpc_method()
250 @jsonrpc_method()
253 def merge_pull_request(
251 def merge_pull_request(
254 request, apiuser, pullrequestid, repoid=Optional(None),
252 request, apiuser, pullrequestid, repoid=Optional(None),
255 userid=Optional(OAttr('apiuser'))):
253 userid=Optional(OAttr('apiuser'))):
256 """
254 """
257 Merge the pull request specified by `pullrequestid` into its target
255 Merge the pull request specified by `pullrequestid` into its target
258 repository.
256 repository.
259
257
260 :param apiuser: This is filled automatically from the |authtoken|.
258 :param apiuser: This is filled automatically from the |authtoken|.
261 :type apiuser: AuthUser
259 :type apiuser: AuthUser
262 :param repoid: Optional, repository name or repository ID of the
260 :param repoid: Optional, repository name or repository ID of the
263 target repository to which the |pr| is to be merged.
261 target repository to which the |pr| is to be merged.
264 :type repoid: str or int
262 :type repoid: str or int
265 :param pullrequestid: ID of the pull request which shall be merged.
263 :param pullrequestid: ID of the pull request which shall be merged.
266 :type pullrequestid: int
264 :type pullrequestid: int
267 :param userid: Merge the pull request as this user.
265 :param userid: Merge the pull request as this user.
268 :type userid: Optional(str or int)
266 :type userid: Optional(str or int)
269
267
270 Example output:
268 Example output:
271
269
272 .. code-block:: bash
270 .. code-block:: bash
273
271
274 "id": <id_given_in_input>,
272 "id": <id_given_in_input>,
275 "result": {
273 "result": {
276 "executed": "<bool>",
274 "executed": "<bool>",
277 "failure_reason": "<int>",
275 "failure_reason": "<int>",
278 "merge_status_message": "<str>",
276 "merge_status_message": "<str>",
279 "merge_commit_id": "<merge_commit_id>",
277 "merge_commit_id": "<merge_commit_id>",
280 "possible": "<bool>",
278 "possible": "<bool>",
281 "merge_ref": {
279 "merge_ref": {
282 "commit_id": "<commit_id>",
280 "commit_id": "<commit_id>",
283 "type": "<type>",
281 "type": "<type>",
284 "name": "<name>"
282 "name": "<name>"
285 }
283 }
286 },
284 },
287 "error": null
285 "error": null
288 """
286 """
289 pull_request = get_pull_request_or_error(pullrequestid)
287 pull_request = get_pull_request_or_error(pullrequestid)
290 if Optional.extract(repoid):
288 if Optional.extract(repoid):
291 repo = get_repo_or_error(repoid)
289 repo = get_repo_or_error(repoid)
292 else:
290 else:
293 repo = pull_request.target_repo
291 repo = pull_request.target_repo
294 auth_user = apiuser
292 auth_user = apiuser
295
293
296 if not isinstance(userid, Optional):
294 if not isinstance(userid, Optional):
297 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
295 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
298 user=apiuser, repo_name=repo.repo_name)
296 user=apiuser, repo_name=repo.repo_name)
299 if has_superadmin_permission(apiuser) or is_repo_admin:
297 if has_superadmin_permission(apiuser) or is_repo_admin:
300 apiuser = get_user_or_error(userid)
298 apiuser = get_user_or_error(userid)
301 auth_user = apiuser.AuthUser()
299 auth_user = apiuser.AuthUser()
302 else:
300 else:
303 raise JSONRPCError('userid is not the same as your user')
301 raise JSONRPCError('userid is not the same as your user')
304
302
305 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
303 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
306 raise JSONRPCError(
304 raise JSONRPCError(
307 'Operation forbidden because pull request is in state {}, '
305 'Operation forbidden because pull request is in state {}, '
308 'only state {} is allowed.'.format(
306 'only state {} is allowed.'.format(
309 pull_request.pull_request_state, PullRequest.STATE_CREATED))
307 pull_request.pull_request_state, PullRequest.STATE_CREATED))
310
308
311 with pull_request.set_state(PullRequest.STATE_UPDATING):
309 with pull_request.set_state(PullRequest.STATE_UPDATING):
312 check = MergeCheck.validate(pull_request, auth_user=auth_user,
310 check = MergeCheck.validate(pull_request, auth_user=auth_user,
313 translator=request.translate)
311 translator=request.translate)
314 merge_possible = not check.failed
312 merge_possible = not check.failed
315
313
316 if not merge_possible:
314 if not merge_possible:
317 error_messages = []
315 error_messages = []
318 for err_type, error_msg in check.errors:
316 for err_type, error_msg in check.errors:
319 error_msg = request.translate(error_msg)
317 error_msg = request.translate(error_msg)
320 error_messages.append(error_msg)
318 error_messages.append(error_msg)
321
319
322 reasons = ','.join(error_messages)
320 reasons = ','.join(error_messages)
323 raise JSONRPCError(
321 raise JSONRPCError(
324 'merge not possible for following reasons: {}'.format(reasons))
322 'merge not possible for following reasons: {}'.format(reasons))
325
323
326 target_repo = pull_request.target_repo
324 target_repo = pull_request.target_repo
327 extras = vcs_operation_context(
325 extras = vcs_operation_context(
328 request.environ, repo_name=target_repo.repo_name,
326 request.environ, repo_name=target_repo.repo_name,
329 username=auth_user.username, action='push',
327 username=auth_user.username, action='push',
330 scm=target_repo.repo_type)
328 scm=target_repo.repo_type)
331 with pull_request.set_state(PullRequest.STATE_UPDATING):
329 with pull_request.set_state(PullRequest.STATE_UPDATING):
332 merge_response = PullRequestModel().merge_repo(
330 merge_response = PullRequestModel().merge_repo(
333 pull_request, apiuser, extras=extras)
331 pull_request, apiuser, extras=extras)
334 if merge_response.executed:
332 if merge_response.executed:
335 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
333 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
336
334
337 Session().commit()
335 Session().commit()
338
336
339 # In previous versions the merge response directly contained the merge
337 # In previous versions the merge response directly contained the merge
340 # commit id. It is now contained in the merge reference object. To be
338 # commit id. It is now contained in the merge reference object. To be
341 # backwards compatible we have to extract it again.
339 # backwards compatible we have to extract it again.
342 merge_response = merge_response.asdict()
340 merge_response = merge_response.asdict()
343 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
341 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
344
342
345 return merge_response
343 return merge_response
346
344
347
345
348 @jsonrpc_method()
346 @jsonrpc_method()
349 def get_pull_request_comments(
347 def get_pull_request_comments(
350 request, apiuser, pullrequestid, repoid=Optional(None)):
348 request, apiuser, pullrequestid, repoid=Optional(None)):
351 """
349 """
352 Get all comments of pull request specified with the `pullrequestid`
350 Get all comments of pull request specified with the `pullrequestid`
353
351
354 :param apiuser: This is filled automatically from the |authtoken|.
352 :param apiuser: This is filled automatically from the |authtoken|.
355 :type apiuser: AuthUser
353 :type apiuser: AuthUser
356 :param repoid: Optional repository name or repository ID.
354 :param repoid: Optional repository name or repository ID.
357 :type repoid: str or int
355 :type repoid: str or int
358 :param pullrequestid: The pull request ID.
356 :param pullrequestid: The pull request ID.
359 :type pullrequestid: int
357 :type pullrequestid: int
360
358
361 Example output:
359 Example output:
362
360
363 .. code-block:: bash
361 .. code-block:: bash
364
362
365 id : <id_given_in_input>
363 id : <id_given_in_input>
366 result : [
364 result : [
367 {
365 {
368 "comment_author": {
366 "comment_author": {
369 "active": true,
367 "active": true,
370 "full_name_or_username": "Tom Gore",
368 "full_name_or_username": "Tom Gore",
371 "username": "admin"
369 "username": "admin"
372 },
370 },
373 "comment_created_on": "2017-01-02T18:43:45.533",
371 "comment_created_on": "2017-01-02T18:43:45.533",
374 "comment_f_path": null,
372 "comment_f_path": null,
375 "comment_id": 25,
373 "comment_id": 25,
376 "comment_lineno": null,
374 "comment_lineno": null,
377 "comment_status": {
375 "comment_status": {
378 "status": "under_review",
376 "status": "under_review",
379 "status_lbl": "Under Review"
377 "status_lbl": "Under Review"
380 },
378 },
381 "comment_text": "Example text",
379 "comment_text": "Example text",
382 "comment_type": null,
380 "comment_type": null,
381 "comment_last_version: 0,
383 "pull_request_version": null,
382 "pull_request_version": null,
384 "comment_commit_id": None,
383 "comment_commit_id": None,
385 "comment_pull_request_id": <pull_request_id>
384 "comment_pull_request_id": <pull_request_id>
386 }
385 }
387 ],
386 ],
388 error : null
387 error : null
389 """
388 """
390
389
391 pull_request = get_pull_request_or_error(pullrequestid)
390 pull_request = get_pull_request_or_error(pullrequestid)
392 if Optional.extract(repoid):
391 if Optional.extract(repoid):
393 repo = get_repo_or_error(repoid)
392 repo = get_repo_or_error(repoid)
394 else:
393 else:
395 repo = pull_request.target_repo
394 repo = pull_request.target_repo
396
395
397 if not PullRequestModel().check_user_read(
396 if not PullRequestModel().check_user_read(
398 pull_request, apiuser, api=True):
397 pull_request, apiuser, api=True):
399 raise JSONRPCError('repository `%s` or pull request `%s` '
398 raise JSONRPCError('repository `%s` or pull request `%s` '
400 'does not exist' % (repoid, pullrequestid))
399 'does not exist' % (repoid, pullrequestid))
401
400
402 (pull_request_latest,
401 (pull_request_latest,
403 pull_request_at_ver,
402 pull_request_at_ver,
404 pull_request_display_obj,
403 pull_request_display_obj,
405 at_version) = PullRequestModel().get_pr_version(
404 at_version) = PullRequestModel().get_pr_version(
406 pull_request.pull_request_id, version=None)
405 pull_request.pull_request_id, version=None)
407
406
408 versions = pull_request_display_obj.versions()
407 versions = pull_request_display_obj.versions()
409 ver_map = {
408 ver_map = {
410 ver.pull_request_version_id: cnt
409 ver.pull_request_version_id: cnt
411 for cnt, ver in enumerate(versions, 1)
410 for cnt, ver in enumerate(versions, 1)
412 }
411 }
413
412
414 # GENERAL COMMENTS with versions #
413 # GENERAL COMMENTS with versions #
415 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
414 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
416 q = q.order_by(ChangesetComment.comment_id.asc())
415 q = q.order_by(ChangesetComment.comment_id.asc())
417 general_comments = q.all()
416 general_comments = q.all()
418
417
419 # INLINE COMMENTS with versions #
418 # INLINE COMMENTS with versions #
420 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
419 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
421 q = q.order_by(ChangesetComment.comment_id.asc())
420 q = q.order_by(ChangesetComment.comment_id.asc())
422 inline_comments = q.all()
421 inline_comments = q.all()
423
422
424 data = []
423 data = []
425 for comment in inline_comments + general_comments:
424 for comment in inline_comments + general_comments:
426 full_data = comment.get_api_data()
425 full_data = comment.get_api_data()
427 pr_version_id = None
426 pr_version_id = None
428 if comment.pull_request_version_id:
427 if comment.pull_request_version_id:
429 pr_version_id = 'v{}'.format(
428 pr_version_id = 'v{}'.format(
430 ver_map[comment.pull_request_version_id])
429 ver_map[comment.pull_request_version_id])
431
430
432 # sanitize some entries
431 # sanitize some entries
433
432
434 full_data['pull_request_version'] = pr_version_id
433 full_data['pull_request_version'] = pr_version_id
435 full_data['comment_author'] = {
434 full_data['comment_author'] = {
436 'username': full_data['comment_author'].username,
435 'username': full_data['comment_author'].username,
437 'full_name_or_username': full_data['comment_author'].full_name_or_username,
436 'full_name_or_username': full_data['comment_author'].full_name_or_username,
438 'active': full_data['comment_author'].active,
437 'active': full_data['comment_author'].active,
439 }
438 }
440
439
441 if full_data['comment_status']:
440 if full_data['comment_status']:
442 full_data['comment_status'] = {
441 full_data['comment_status'] = {
443 'status': full_data['comment_status'][0].status,
442 'status': full_data['comment_status'][0].status,
444 'status_lbl': full_data['comment_status'][0].status_lbl,
443 'status_lbl': full_data['comment_status'][0].status_lbl,
445 }
444 }
446 else:
445 else:
447 full_data['comment_status'] = {}
446 full_data['comment_status'] = {}
448
447
449 data.append(full_data)
448 data.append(full_data)
450 return data
449 return data
451
450
452
451
453 @jsonrpc_method()
452 @jsonrpc_method()
454 def comment_pull_request(
453 def comment_pull_request(
455 request, apiuser, pullrequestid, repoid=Optional(None),
454 request, apiuser, pullrequestid, repoid=Optional(None),
456 message=Optional(None), commit_id=Optional(None), status=Optional(None),
455 message=Optional(None), commit_id=Optional(None), status=Optional(None),
457 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
456 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
458 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
457 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
459 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
458 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
460 """
459 """
461 Comment on the pull request specified with the `pullrequestid`,
460 Comment on the pull request specified with the `pullrequestid`,
462 in the |repo| specified by the `repoid`, and optionally change the
461 in the |repo| specified by the `repoid`, and optionally change the
463 review status.
462 review status.
464
463
465 :param apiuser: This is filled automatically from the |authtoken|.
464 :param apiuser: This is filled automatically from the |authtoken|.
466 :type apiuser: AuthUser
465 :type apiuser: AuthUser
467 :param repoid: Optional repository name or repository ID.
466 :param repoid: Optional repository name or repository ID.
468 :type repoid: str or int
467 :type repoid: str or int
469 :param pullrequestid: The pull request ID.
468 :param pullrequestid: The pull request ID.
470 :type pullrequestid: int
469 :type pullrequestid: int
471 :param commit_id: Specify the commit_id for which to set a comment. If
470 :param commit_id: Specify the commit_id for which to set a comment. If
472 given commit_id is different than latest in the PR status
471 given commit_id is different than latest in the PR status
473 change won't be performed.
472 change won't be performed.
474 :type commit_id: str
473 :type commit_id: str
475 :param message: The text content of the comment.
474 :param message: The text content of the comment.
476 :type message: str
475 :type message: str
477 :param status: (**Optional**) Set the approval status of the pull
476 :param status: (**Optional**) Set the approval status of the pull
478 request. One of: 'not_reviewed', 'approved', 'rejected',
477 request. One of: 'not_reviewed', 'approved', 'rejected',
479 'under_review'
478 'under_review'
480 :type status: str
479 :type status: str
481 :param comment_type: Comment type, one of: 'note', 'todo'
480 :param comment_type: Comment type, one of: 'note', 'todo'
482 :type comment_type: Optional(str), default: 'note'
481 :type comment_type: Optional(str), default: 'note'
483 :param resolves_comment_id: id of comment which this one will resolve
482 :param resolves_comment_id: id of comment which this one will resolve
484 :type resolves_comment_id: Optional(int)
483 :type resolves_comment_id: Optional(int)
485 :param extra_recipients: list of user ids or usernames to add
484 :param extra_recipients: list of user ids or usernames to add
486 notifications for this comment. Acts like a CC for notification
485 notifications for this comment. Acts like a CC for notification
487 :type extra_recipients: Optional(list)
486 :type extra_recipients: Optional(list)
488 :param userid: Comment on the pull request as this user
487 :param userid: Comment on the pull request as this user
489 :type userid: Optional(str or int)
488 :type userid: Optional(str or int)
490 :param send_email: Define if this comment should also send email notification
489 :param send_email: Define if this comment should also send email notification
491 :type send_email: Optional(bool)
490 :type send_email: Optional(bool)
492
491
493 Example output:
492 Example output:
494
493
495 .. code-block:: bash
494 .. code-block:: bash
496
495
497 id : <id_given_in_input>
496 id : <id_given_in_input>
498 result : {
497 result : {
499 "pull_request_id": "<Integer>",
498 "pull_request_id": "<Integer>",
500 "comment_id": "<Integer>",
499 "comment_id": "<Integer>",
501 "status": {"given": <given_status>,
500 "status": {"given": <given_status>,
502 "was_changed": <bool status_was_actually_changed> },
501 "was_changed": <bool status_was_actually_changed> },
503 },
502 },
504 error : null
503 error : null
505 """
504 """
506 pull_request = get_pull_request_or_error(pullrequestid)
505 pull_request = get_pull_request_or_error(pullrequestid)
507 if Optional.extract(repoid):
506 if Optional.extract(repoid):
508 repo = get_repo_or_error(repoid)
507 repo = get_repo_or_error(repoid)
509 else:
508 else:
510 repo = pull_request.target_repo
509 repo = pull_request.target_repo
511
510
512 auth_user = apiuser
511 auth_user = apiuser
513 if not isinstance(userid, Optional):
512 if not isinstance(userid, Optional):
514 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
513 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
515 user=apiuser, repo_name=repo.repo_name)
514 user=apiuser, repo_name=repo.repo_name)
516 if has_superadmin_permission(apiuser) or is_repo_admin:
515 if has_superadmin_permission(apiuser) or is_repo_admin:
517 apiuser = get_user_or_error(userid)
516 apiuser = get_user_or_error(userid)
518 auth_user = apiuser.AuthUser()
517 auth_user = apiuser.AuthUser()
519 else:
518 else:
520 raise JSONRPCError('userid is not the same as your user')
519 raise JSONRPCError('userid is not the same as your user')
521
520
522 if pull_request.is_closed():
521 if pull_request.is_closed():
523 raise JSONRPCError(
522 raise JSONRPCError(
524 'pull request `%s` comment failed, pull request is closed' % (
523 'pull request `%s` comment failed, pull request is closed' % (
525 pullrequestid,))
524 pullrequestid,))
526
525
527 if not PullRequestModel().check_user_read(
526 if not PullRequestModel().check_user_read(
528 pull_request, apiuser, api=True):
527 pull_request, apiuser, api=True):
529 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
528 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
530 message = Optional.extract(message)
529 message = Optional.extract(message)
531 status = Optional.extract(status)
530 status = Optional.extract(status)
532 commit_id = Optional.extract(commit_id)
531 commit_id = Optional.extract(commit_id)
533 comment_type = Optional.extract(comment_type)
532 comment_type = Optional.extract(comment_type)
534 resolves_comment_id = Optional.extract(resolves_comment_id)
533 resolves_comment_id = Optional.extract(resolves_comment_id)
535 extra_recipients = Optional.extract(extra_recipients)
534 extra_recipients = Optional.extract(extra_recipients)
536 send_email = Optional.extract(send_email, binary=True)
535 send_email = Optional.extract(send_email, binary=True)
537
536
538 if not message and not status:
537 if not message and not status:
539 raise JSONRPCError(
538 raise JSONRPCError(
540 'Both message and status parameters are missing. '
539 'Both message and status parameters are missing. '
541 'At least one is required.')
540 'At least one is required.')
542
541
543 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
542 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
544 status is not None):
543 status is not None):
545 raise JSONRPCError('Unknown comment status: `%s`' % status)
544 raise JSONRPCError('Unknown comment status: `%s`' % status)
546
545
547 if commit_id and commit_id not in pull_request.revisions:
546 if commit_id and commit_id not in pull_request.revisions:
548 raise JSONRPCError(
547 raise JSONRPCError(
549 'Invalid commit_id `%s` for this pull request.' % commit_id)
548 'Invalid commit_id `%s` for this pull request.' % commit_id)
550
549
551 allowed_to_change_status = PullRequestModel().check_user_change_status(
550 allowed_to_change_status = PullRequestModel().check_user_change_status(
552 pull_request, apiuser)
551 pull_request, apiuser)
553
552
554 # if commit_id is passed re-validated if user is allowed to change status
553 # if commit_id is passed re-validated if user is allowed to change status
555 # based on latest commit_id from the PR
554 # based on latest commit_id from the PR
556 if commit_id:
555 if commit_id:
557 commit_idx = pull_request.revisions.index(commit_id)
556 commit_idx = pull_request.revisions.index(commit_id)
558 if commit_idx != 0:
557 if commit_idx != 0:
559 allowed_to_change_status = False
558 allowed_to_change_status = False
560
559
561 if resolves_comment_id:
560 if resolves_comment_id:
562 comment = ChangesetComment.get(resolves_comment_id)
561 comment = ChangesetComment.get(resolves_comment_id)
563 if not comment:
562 if not comment:
564 raise JSONRPCError(
563 raise JSONRPCError(
565 'Invalid resolves_comment_id `%s` for this pull request.'
564 'Invalid resolves_comment_id `%s` for this pull request.'
566 % resolves_comment_id)
565 % resolves_comment_id)
567 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
566 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
568 raise JSONRPCError(
567 raise JSONRPCError(
569 'Comment `%s` is wrong type for setting status to resolved.'
568 'Comment `%s` is wrong type for setting status to resolved.'
570 % resolves_comment_id)
569 % resolves_comment_id)
571
570
572 text = message
571 text = message
573 status_label = ChangesetStatus.get_status_lbl(status)
572 status_label = ChangesetStatus.get_status_lbl(status)
574 if status and allowed_to_change_status:
573 if status and allowed_to_change_status:
575 st_message = ('Status change %(transition_icon)s %(status)s'
574 st_message = ('Status change %(transition_icon)s %(status)s'
576 % {'transition_icon': '>', 'status': status_label})
575 % {'transition_icon': '>', 'status': status_label})
577 text = message or st_message
576 text = message or st_message
578
577
579 rc_config = SettingsModel().get_all_settings()
578 rc_config = SettingsModel().get_all_settings()
580 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
579 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
581
580
582 status_change = status and allowed_to_change_status
581 status_change = status and allowed_to_change_status
583 comment = CommentsModel().create(
582 comment = CommentsModel().create(
584 text=text,
583 text=text,
585 repo=pull_request.target_repo.repo_id,
584 repo=pull_request.target_repo.repo_id,
586 user=apiuser.user_id,
585 user=apiuser.user_id,
587 pull_request=pull_request.pull_request_id,
586 pull_request=pull_request.pull_request_id,
588 f_path=None,
587 f_path=None,
589 line_no=None,
588 line_no=None,
590 status_change=(status_label if status_change else None),
589 status_change=(status_label if status_change else None),
591 status_change_type=(status if status_change else None),
590 status_change_type=(status if status_change else None),
592 closing_pr=False,
591 closing_pr=False,
593 renderer=renderer,
592 renderer=renderer,
594 comment_type=comment_type,
593 comment_type=comment_type,
595 resolves_comment_id=resolves_comment_id,
594 resolves_comment_id=resolves_comment_id,
596 auth_user=auth_user,
595 auth_user=auth_user,
597 extra_recipients=extra_recipients,
596 extra_recipients=extra_recipients,
598 send_email=send_email
597 send_email=send_email
599 )
598 )
600
599
601 if allowed_to_change_status and status:
600 if allowed_to_change_status and status:
602 old_calculated_status = pull_request.calculated_review_status()
601 old_calculated_status = pull_request.calculated_review_status()
603 ChangesetStatusModel().set_status(
602 ChangesetStatusModel().set_status(
604 pull_request.target_repo.repo_id,
603 pull_request.target_repo.repo_id,
605 status,
604 status,
606 apiuser.user_id,
605 apiuser.user_id,
607 comment,
606 comment,
608 pull_request=pull_request.pull_request_id
607 pull_request=pull_request.pull_request_id
609 )
608 )
610 Session().flush()
609 Session().flush()
611
610
612 Session().commit()
611 Session().commit()
613
612
614 PullRequestModel().trigger_pull_request_hook(
613 PullRequestModel().trigger_pull_request_hook(
615 pull_request, apiuser, 'comment',
614 pull_request, apiuser, 'comment',
616 data={'comment': comment})
615 data={'comment': comment})
617
616
618 if allowed_to_change_status and status:
617 if allowed_to_change_status and status:
619 # we now calculate the status of pull request, and based on that
618 # we now calculate the status of pull request, and based on that
620 # calculation we set the commits status
619 # calculation we set the commits status
621 calculated_status = pull_request.calculated_review_status()
620 calculated_status = pull_request.calculated_review_status()
622 if old_calculated_status != calculated_status:
621 if old_calculated_status != calculated_status:
623 PullRequestModel().trigger_pull_request_hook(
622 PullRequestModel().trigger_pull_request_hook(
624 pull_request, apiuser, 'review_status_change',
623 pull_request, apiuser, 'review_status_change',
625 data={'status': calculated_status})
624 data={'status': calculated_status})
626
625
627 data = {
626 data = {
628 'pull_request_id': pull_request.pull_request_id,
627 'pull_request_id': pull_request.pull_request_id,
629 'comment_id': comment.comment_id if comment else None,
628 'comment_id': comment.comment_id if comment else None,
630 'status': {'given': status, 'was_changed': status_change},
629 'status': {'given': status, 'was_changed': status_change},
631 }
630 }
632 return data
631 return data
633
632
634
633
635 @jsonrpc_method()
634 @jsonrpc_method()
636 def edit_comment(
637 request, apiuser, message, comment_id, version,
638 userid=Optional(OAttr('apiuser')),
639 ):
640 """
641 Edit comment on the pull request or commit,
642 specified by the `comment_id` and version. Initially version should be 0
643
644 :param apiuser: This is filled automatically from the |authtoken|.
645 :type apiuser: AuthUser
646 :param comment_id: Specify the comment_id for editing
647 :type comment_id: int
648 :param version: version of the comment that will be created, starts from 0
649 :type version: int
650 :param message: The text content of the comment.
651 :type message: str
652 :param userid: Comment on the pull request as this user
653 :type userid: Optional(str or int)
654
655 Example output:
656
657 .. code-block:: bash
658
659 id : <id_given_in_input>
660 result : {
661 "comment_history_id": "<Integer>",
662 "version": "<Integer>",
663 },
664 error : null
665 """
666
667 auth_user = apiuser
668 comment = ChangesetComment.get(comment_id)
669
670 is_super_admin = has_superadmin_permission(apiuser)
671 is_repo_admin = HasRepoPermissionAnyApi('repository.admin') \
672 (user=apiuser, repo_name=comment.repo.repo_name)
673
674 if not isinstance(userid, Optional):
675 if is_super_admin or is_repo_admin:
676 apiuser = get_user_or_error(userid)
677 auth_user = apiuser.AuthUser()
678 else:
679 raise JSONRPCError('userid is not the same as your user')
680
681 comment_author = comment.author.user_id == auth_user.user_id
682 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
683 raise JSONRPCError("you don't have access to edit this comment")
684
685 try:
686 comment_history = CommentsModel().edit(
687 comment_id=comment_id,
688 text=message,
689 auth_user=auth_user,
690 version=version,
691 )
692 Session().commit()
693 except CommentVersionMismatch:
694 raise JSONRPCError(
695 'comment ({}) version ({}) mismatch'.format(comment_id, version)
696 )
697 if not comment_history and not message:
698 raise JSONRPCError(
699 "comment ({}) can't be changed with empty string".format(comment_id)
700 )
701 data = {
702 'comment_history_id': comment_history.comment_history_id if comment_history else None,
703 'version': comment_history.version if comment_history else None,
704 }
705 return data
706
707
708 @jsonrpc_method()
709 def create_pull_request(
635 def create_pull_request(
710 request, apiuser, source_repo, target_repo, source_ref, target_ref,
636 request, apiuser, source_repo, target_repo, source_ref, target_ref,
711 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
637 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
712 description_renderer=Optional(''), reviewers=Optional(None)):
638 description_renderer=Optional(''), reviewers=Optional(None)):
713 """
639 """
714 Creates a new pull request.
640 Creates a new pull request.
715
641
716 Accepts refs in the following formats:
642 Accepts refs in the following formats:
717
643
718 * branch:<branch_name>:<sha>
644 * branch:<branch_name>:<sha>
719 * branch:<branch_name>
645 * branch:<branch_name>
720 * bookmark:<bookmark_name>:<sha> (Mercurial only)
646 * bookmark:<bookmark_name>:<sha> (Mercurial only)
721 * bookmark:<bookmark_name> (Mercurial only)
647 * bookmark:<bookmark_name> (Mercurial only)
722
648
723 :param apiuser: This is filled automatically from the |authtoken|.
649 :param apiuser: This is filled automatically from the |authtoken|.
724 :type apiuser: AuthUser
650 :type apiuser: AuthUser
725 :param source_repo: Set the source repository name.
651 :param source_repo: Set the source repository name.
726 :type source_repo: str
652 :type source_repo: str
727 :param target_repo: Set the target repository name.
653 :param target_repo: Set the target repository name.
728 :type target_repo: str
654 :type target_repo: str
729 :param source_ref: Set the source ref name.
655 :param source_ref: Set the source ref name.
730 :type source_ref: str
656 :type source_ref: str
731 :param target_ref: Set the target ref name.
657 :param target_ref: Set the target ref name.
732 :type target_ref: str
658 :type target_ref: str
733 :param owner: user_id or username
659 :param owner: user_id or username
734 :type owner: Optional(str)
660 :type owner: Optional(str)
735 :param title: Optionally Set the pull request title, it's generated otherwise
661 :param title: Optionally Set the pull request title, it's generated otherwise
736 :type title: str
662 :type title: str
737 :param description: Set the pull request description.
663 :param description: Set the pull request description.
738 :type description: Optional(str)
664 :type description: Optional(str)
739 :type description_renderer: Optional(str)
665 :type description_renderer: Optional(str)
740 :param description_renderer: Set pull request renderer for the description.
666 :param description_renderer: Set pull request renderer for the description.
741 It should be 'rst', 'markdown' or 'plain'. If not give default
667 It should be 'rst', 'markdown' or 'plain'. If not give default
742 system renderer will be used
668 system renderer will be used
743 :param reviewers: Set the new pull request reviewers list.
669 :param reviewers: Set the new pull request reviewers list.
744 Reviewer defined by review rules will be added automatically to the
670 Reviewer defined by review rules will be added automatically to the
745 defined list.
671 defined list.
746 :type reviewers: Optional(list)
672 :type reviewers: Optional(list)
747 Accepts username strings or objects of the format:
673 Accepts username strings or objects of the format:
748
674
749 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
675 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
750 """
676 """
751
677
752 source_db_repo = get_repo_or_error(source_repo)
678 source_db_repo = get_repo_or_error(source_repo)
753 target_db_repo = get_repo_or_error(target_repo)
679 target_db_repo = get_repo_or_error(target_repo)
754 if not has_superadmin_permission(apiuser):
680 if not has_superadmin_permission(apiuser):
755 _perms = ('repository.admin', 'repository.write', 'repository.read',)
681 _perms = ('repository.admin', 'repository.write', 'repository.read',)
756 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
682 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
757
683
758 owner = validate_set_owner_permissions(apiuser, owner)
684 owner = validate_set_owner_permissions(apiuser, owner)
759
685
760 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
686 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
761 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
687 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
762
688
763 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
689 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
764 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
690 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
765
691
766 reviewer_objects = Optional.extract(reviewers) or []
692 reviewer_objects = Optional.extract(reviewers) or []
767
693
768 # serialize and validate passed in given reviewers
694 # serialize and validate passed in given reviewers
769 if reviewer_objects:
695 if reviewer_objects:
770 schema = ReviewerListSchema()
696 schema = ReviewerListSchema()
771 try:
697 try:
772 reviewer_objects = schema.deserialize(reviewer_objects)
698 reviewer_objects = schema.deserialize(reviewer_objects)
773 except Invalid as err:
699 except Invalid as err:
774 raise JSONRPCValidationError(colander_exc=err)
700 raise JSONRPCValidationError(colander_exc=err)
775
701
776 # validate users
702 # validate users
777 for reviewer_object in reviewer_objects:
703 for reviewer_object in reviewer_objects:
778 user = get_user_or_error(reviewer_object['username'])
704 user = get_user_or_error(reviewer_object['username'])
779 reviewer_object['user_id'] = user.user_id
705 reviewer_object['user_id'] = user.user_id
780
706
781 get_default_reviewers_data, validate_default_reviewers = \
707 get_default_reviewers_data, validate_default_reviewers = \
782 PullRequestModel().get_reviewer_functions()
708 PullRequestModel().get_reviewer_functions()
783
709
784 # recalculate reviewers logic, to make sure we can validate this
710 # recalculate reviewers logic, to make sure we can validate this
785 default_reviewers_data = get_default_reviewers_data(
711 default_reviewers_data = get_default_reviewers_data(
786 owner, source_db_repo,
712 owner, source_db_repo,
787 source_commit, target_db_repo, target_commit)
713 source_commit, target_db_repo, target_commit)
788
714
789 # now MERGE our given with the calculated
715 # now MERGE our given with the calculated
790 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
716 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
791
717
792 try:
718 try:
793 reviewers = validate_default_reviewers(
719 reviewers = validate_default_reviewers(
794 reviewer_objects, default_reviewers_data)
720 reviewer_objects, default_reviewers_data)
795 except ValueError as e:
721 except ValueError as e:
796 raise JSONRPCError('Reviewers Validation: {}'.format(e))
722 raise JSONRPCError('Reviewers Validation: {}'.format(e))
797
723
798 title = Optional.extract(title)
724 title = Optional.extract(title)
799 if not title:
725 if not title:
800 title_source_ref = source_ref.split(':', 2)[1]
726 title_source_ref = source_ref.split(':', 2)[1]
801 title = PullRequestModel().generate_pullrequest_title(
727 title = PullRequestModel().generate_pullrequest_title(
802 source=source_repo,
728 source=source_repo,
803 source_ref=title_source_ref,
729 source_ref=title_source_ref,
804 target=target_repo
730 target=target_repo
805 )
731 )
806
732
807 diff_info = default_reviewers_data['diff_info']
733 diff_info = default_reviewers_data['diff_info']
808 common_ancestor_id = diff_info['ancestor']
734 common_ancestor_id = diff_info['ancestor']
809 commits = diff_info['commits']
735 commits = diff_info['commits']
810
736
811 if not common_ancestor_id:
737 if not common_ancestor_id:
812 raise JSONRPCError('no common ancestor found')
738 raise JSONRPCError('no common ancestor found')
813
739
814 if not commits:
740 if not commits:
815 raise JSONRPCError('no commits found')
741 raise JSONRPCError('no commits found')
816
742
817 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
743 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
818 revisions = [commit.raw_id for commit in reversed(commits)]
744 revisions = [commit.raw_id for commit in reversed(commits)]
819
745
820 # recalculate target ref based on ancestor
746 # recalculate target ref based on ancestor
821 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
747 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
822 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
748 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
823
749
824 # fetch renderer, if set fallback to plain in case of PR
750 # fetch renderer, if set fallback to plain in case of PR
825 rc_config = SettingsModel().get_all_settings()
751 rc_config = SettingsModel().get_all_settings()
826 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
752 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
827 description = Optional.extract(description)
753 description = Optional.extract(description)
828 description_renderer = Optional.extract(description_renderer) or default_system_renderer
754 description_renderer = Optional.extract(description_renderer) or default_system_renderer
829
755
830 pull_request = PullRequestModel().create(
756 pull_request = PullRequestModel().create(
831 created_by=owner.user_id,
757 created_by=owner.user_id,
832 source_repo=source_repo,
758 source_repo=source_repo,
833 source_ref=full_source_ref,
759 source_ref=full_source_ref,
834 target_repo=target_repo,
760 target_repo=target_repo,
835 target_ref=full_target_ref,
761 target_ref=full_target_ref,
836 common_ancestor_id=common_ancestor_id,
762 common_ancestor_id=common_ancestor_id,
837 revisions=revisions,
763 revisions=revisions,
838 reviewers=reviewers,
764 reviewers=reviewers,
839 title=title,
765 title=title,
840 description=description,
766 description=description,
841 description_renderer=description_renderer,
767 description_renderer=description_renderer,
842 reviewer_data=default_reviewers_data,
768 reviewer_data=default_reviewers_data,
843 auth_user=apiuser
769 auth_user=apiuser
844 )
770 )
845
771
846 Session().commit()
772 Session().commit()
847 data = {
773 data = {
848 'msg': 'Created new pull request `{}`'.format(title),
774 'msg': 'Created new pull request `{}`'.format(title),
849 'pull_request_id': pull_request.pull_request_id,
775 'pull_request_id': pull_request.pull_request_id,
850 }
776 }
851 return data
777 return data
852
778
853
779
854 @jsonrpc_method()
780 @jsonrpc_method()
855 def update_pull_request(
781 def update_pull_request(
856 request, apiuser, pullrequestid, repoid=Optional(None),
782 request, apiuser, pullrequestid, repoid=Optional(None),
857 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
783 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
858 reviewers=Optional(None), update_commits=Optional(None)):
784 reviewers=Optional(None), update_commits=Optional(None)):
859 """
785 """
860 Updates a pull request.
786 Updates a pull request.
861
787
862 :param apiuser: This is filled automatically from the |authtoken|.
788 :param apiuser: This is filled automatically from the |authtoken|.
863 :type apiuser: AuthUser
789 :type apiuser: AuthUser
864 :param repoid: Optional repository name or repository ID.
790 :param repoid: Optional repository name or repository ID.
865 :type repoid: str or int
791 :type repoid: str or int
866 :param pullrequestid: The pull request ID.
792 :param pullrequestid: The pull request ID.
867 :type pullrequestid: int
793 :type pullrequestid: int
868 :param title: Set the pull request title.
794 :param title: Set the pull request title.
869 :type title: str
795 :type title: str
870 :param description: Update pull request description.
796 :param description: Update pull request description.
871 :type description: Optional(str)
797 :type description: Optional(str)
872 :type description_renderer: Optional(str)
798 :type description_renderer: Optional(str)
873 :param description_renderer: Update pull request renderer for the description.
799 :param description_renderer: Update pull request renderer for the description.
874 It should be 'rst', 'markdown' or 'plain'
800 It should be 'rst', 'markdown' or 'plain'
875 :param reviewers: Update pull request reviewers list with new value.
801 :param reviewers: Update pull request reviewers list with new value.
876 :type reviewers: Optional(list)
802 :type reviewers: Optional(list)
877 Accepts username strings or objects of the format:
803 Accepts username strings or objects of the format:
878
804
879 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
805 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
880
806
881 :param update_commits: Trigger update of commits for this pull request
807 :param update_commits: Trigger update of commits for this pull request
882 :type: update_commits: Optional(bool)
808 :type: update_commits: Optional(bool)
883
809
884 Example output:
810 Example output:
885
811
886 .. code-block:: bash
812 .. code-block:: bash
887
813
888 id : <id_given_in_input>
814 id : <id_given_in_input>
889 result : {
815 result : {
890 "msg": "Updated pull request `63`",
816 "msg": "Updated pull request `63`",
891 "pull_request": <pull_request_object>,
817 "pull_request": <pull_request_object>,
892 "updated_reviewers": {
818 "updated_reviewers": {
893 "added": [
819 "added": [
894 "username"
820 "username"
895 ],
821 ],
896 "removed": []
822 "removed": []
897 },
823 },
898 "updated_commits": {
824 "updated_commits": {
899 "added": [
825 "added": [
900 "<sha1_hash>"
826 "<sha1_hash>"
901 ],
827 ],
902 "common": [
828 "common": [
903 "<sha1_hash>",
829 "<sha1_hash>",
904 "<sha1_hash>",
830 "<sha1_hash>",
905 ],
831 ],
906 "removed": []
832 "removed": []
907 }
833 }
908 }
834 }
909 error : null
835 error : null
910 """
836 """
911
837
912 pull_request = get_pull_request_or_error(pullrequestid)
838 pull_request = get_pull_request_or_error(pullrequestid)
913 if Optional.extract(repoid):
839 if Optional.extract(repoid):
914 repo = get_repo_or_error(repoid)
840 repo = get_repo_or_error(repoid)
915 else:
841 else:
916 repo = pull_request.target_repo
842 repo = pull_request.target_repo
917
843
918 if not PullRequestModel().check_user_update(
844 if not PullRequestModel().check_user_update(
919 pull_request, apiuser, api=True):
845 pull_request, apiuser, api=True):
920 raise JSONRPCError(
846 raise JSONRPCError(
921 'pull request `%s` update failed, no permission to update.' % (
847 'pull request `%s` update failed, no permission to update.' % (
922 pullrequestid,))
848 pullrequestid,))
923 if pull_request.is_closed():
849 if pull_request.is_closed():
924 raise JSONRPCError(
850 raise JSONRPCError(
925 'pull request `%s` update failed, pull request is closed' % (
851 'pull request `%s` update failed, pull request is closed' % (
926 pullrequestid,))
852 pullrequestid,))
927
853
928 reviewer_objects = Optional.extract(reviewers) or []
854 reviewer_objects = Optional.extract(reviewers) or []
929
855
930 if reviewer_objects:
856 if reviewer_objects:
931 schema = ReviewerListSchema()
857 schema = ReviewerListSchema()
932 try:
858 try:
933 reviewer_objects = schema.deserialize(reviewer_objects)
859 reviewer_objects = schema.deserialize(reviewer_objects)
934 except Invalid as err:
860 except Invalid as err:
935 raise JSONRPCValidationError(colander_exc=err)
861 raise JSONRPCValidationError(colander_exc=err)
936
862
937 # validate users
863 # validate users
938 for reviewer_object in reviewer_objects:
864 for reviewer_object in reviewer_objects:
939 user = get_user_or_error(reviewer_object['username'])
865 user = get_user_or_error(reviewer_object['username'])
940 reviewer_object['user_id'] = user.user_id
866 reviewer_object['user_id'] = user.user_id
941
867
942 get_default_reviewers_data, get_validated_reviewers = \
868 get_default_reviewers_data, get_validated_reviewers = \
943 PullRequestModel().get_reviewer_functions()
869 PullRequestModel().get_reviewer_functions()
944
870
945 # re-use stored rules
871 # re-use stored rules
946 reviewer_rules = pull_request.reviewer_data
872 reviewer_rules = pull_request.reviewer_data
947 try:
873 try:
948 reviewers = get_validated_reviewers(
874 reviewers = get_validated_reviewers(
949 reviewer_objects, reviewer_rules)
875 reviewer_objects, reviewer_rules)
950 except ValueError as e:
876 except ValueError as e:
951 raise JSONRPCError('Reviewers Validation: {}'.format(e))
877 raise JSONRPCError('Reviewers Validation: {}'.format(e))
952 else:
878 else:
953 reviewers = []
879 reviewers = []
954
880
955 title = Optional.extract(title)
881 title = Optional.extract(title)
956 description = Optional.extract(description)
882 description = Optional.extract(description)
957 description_renderer = Optional.extract(description_renderer)
883 description_renderer = Optional.extract(description_renderer)
958
884
959 if title or description:
885 if title or description:
960 PullRequestModel().edit(
886 PullRequestModel().edit(
961 pull_request,
887 pull_request,
962 title or pull_request.title,
888 title or pull_request.title,
963 description or pull_request.description,
889 description or pull_request.description,
964 description_renderer or pull_request.description_renderer,
890 description_renderer or pull_request.description_renderer,
965 apiuser)
891 apiuser)
966 Session().commit()
892 Session().commit()
967
893
968 commit_changes = {"added": [], "common": [], "removed": []}
894 commit_changes = {"added": [], "common": [], "removed": []}
969 if str2bool(Optional.extract(update_commits)):
895 if str2bool(Optional.extract(update_commits)):
970
896
971 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
897 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
972 raise JSONRPCError(
898 raise JSONRPCError(
973 'Operation forbidden because pull request is in state {}, '
899 'Operation forbidden because pull request is in state {}, '
974 'only state {} is allowed.'.format(
900 'only state {} is allowed.'.format(
975 pull_request.pull_request_state, PullRequest.STATE_CREATED))
901 pull_request.pull_request_state, PullRequest.STATE_CREATED))
976
902
977 with pull_request.set_state(PullRequest.STATE_UPDATING):
903 with pull_request.set_state(PullRequest.STATE_UPDATING):
978 if PullRequestModel().has_valid_update_type(pull_request):
904 if PullRequestModel().has_valid_update_type(pull_request):
979 db_user = apiuser.get_instance()
905 db_user = apiuser.get_instance()
980 update_response = PullRequestModel().update_commits(
906 update_response = PullRequestModel().update_commits(
981 pull_request, db_user)
907 pull_request, db_user)
982 commit_changes = update_response.changes or commit_changes
908 commit_changes = update_response.changes or commit_changes
983 Session().commit()
909 Session().commit()
984
910
985 reviewers_changes = {"added": [], "removed": []}
911 reviewers_changes = {"added": [], "removed": []}
986 if reviewers:
912 if reviewers:
987 old_calculated_status = pull_request.calculated_review_status()
913 old_calculated_status = pull_request.calculated_review_status()
988 added_reviewers, removed_reviewers = \
914 added_reviewers, removed_reviewers = \
989 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
915 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
990
916
991 reviewers_changes['added'] = sorted(
917 reviewers_changes['added'] = sorted(
992 [get_user_or_error(n).username for n in added_reviewers])
918 [get_user_or_error(n).username for n in added_reviewers])
993 reviewers_changes['removed'] = sorted(
919 reviewers_changes['removed'] = sorted(
994 [get_user_or_error(n).username for n in removed_reviewers])
920 [get_user_or_error(n).username for n in removed_reviewers])
995 Session().commit()
921 Session().commit()
996
922
997 # trigger status changed if change in reviewers changes the status
923 # trigger status changed if change in reviewers changes the status
998 calculated_status = pull_request.calculated_review_status()
924 calculated_status = pull_request.calculated_review_status()
999 if old_calculated_status != calculated_status:
925 if old_calculated_status != calculated_status:
1000 PullRequestModel().trigger_pull_request_hook(
926 PullRequestModel().trigger_pull_request_hook(
1001 pull_request, apiuser, 'review_status_change',
927 pull_request, apiuser, 'review_status_change',
1002 data={'status': calculated_status})
928 data={'status': calculated_status})
1003
929
1004 data = {
930 data = {
1005 'msg': 'Updated pull request `{}`'.format(
931 'msg': 'Updated pull request `{}`'.format(
1006 pull_request.pull_request_id),
932 pull_request.pull_request_id),
1007 'pull_request': pull_request.get_api_data(),
933 'pull_request': pull_request.get_api_data(),
1008 'updated_commits': commit_changes,
934 'updated_commits': commit_changes,
1009 'updated_reviewers': reviewers_changes
935 'updated_reviewers': reviewers_changes
1010 }
936 }
1011
937
1012 return data
938 return data
1013
939
1014
940
1015 @jsonrpc_method()
941 @jsonrpc_method()
1016 def close_pull_request(
942 def close_pull_request(
1017 request, apiuser, pullrequestid, repoid=Optional(None),
943 request, apiuser, pullrequestid, repoid=Optional(None),
1018 userid=Optional(OAttr('apiuser')), message=Optional('')):
944 userid=Optional(OAttr('apiuser')), message=Optional('')):
1019 """
945 """
1020 Close the pull request specified by `pullrequestid`.
946 Close the pull request specified by `pullrequestid`.
1021
947
1022 :param apiuser: This is filled automatically from the |authtoken|.
948 :param apiuser: This is filled automatically from the |authtoken|.
1023 :type apiuser: AuthUser
949 :type apiuser: AuthUser
1024 :param repoid: Repository name or repository ID to which the pull
950 :param repoid: Repository name or repository ID to which the pull
1025 request belongs.
951 request belongs.
1026 :type repoid: str or int
952 :type repoid: str or int
1027 :param pullrequestid: ID of the pull request to be closed.
953 :param pullrequestid: ID of the pull request to be closed.
1028 :type pullrequestid: int
954 :type pullrequestid: int
1029 :param userid: Close the pull request as this user.
955 :param userid: Close the pull request as this user.
1030 :type userid: Optional(str or int)
956 :type userid: Optional(str or int)
1031 :param message: Optional message to close the Pull Request with. If not
957 :param message: Optional message to close the Pull Request with. If not
1032 specified it will be generated automatically.
958 specified it will be generated automatically.
1033 :type message: Optional(str)
959 :type message: Optional(str)
1034
960
1035 Example output:
961 Example output:
1036
962
1037 .. code-block:: bash
963 .. code-block:: bash
1038
964
1039 "id": <id_given_in_input>,
965 "id": <id_given_in_input>,
1040 "result": {
966 "result": {
1041 "pull_request_id": "<int>",
967 "pull_request_id": "<int>",
1042 "close_status": "<str:status_lbl>,
968 "close_status": "<str:status_lbl>,
1043 "closed": "<bool>"
969 "closed": "<bool>"
1044 },
970 },
1045 "error": null
971 "error": null
1046
972
1047 """
973 """
1048 _ = request.translate
974 _ = request.translate
1049
975
1050 pull_request = get_pull_request_or_error(pullrequestid)
976 pull_request = get_pull_request_or_error(pullrequestid)
1051 if Optional.extract(repoid):
977 if Optional.extract(repoid):
1052 repo = get_repo_or_error(repoid)
978 repo = get_repo_or_error(repoid)
1053 else:
979 else:
1054 repo = pull_request.target_repo
980 repo = pull_request.target_repo
1055
981
1056 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
982 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
1057 user=apiuser, repo_name=repo.repo_name)
983 user=apiuser, repo_name=repo.repo_name)
1058 if not isinstance(userid, Optional):
984 if not isinstance(userid, Optional):
1059 if has_superadmin_permission(apiuser) or is_repo_admin:
985 if has_superadmin_permission(apiuser) or is_repo_admin:
1060 apiuser = get_user_or_error(userid)
986 apiuser = get_user_or_error(userid)
1061 else:
987 else:
1062 raise JSONRPCError('userid is not the same as your user')
988 raise JSONRPCError('userid is not the same as your user')
1063
989
1064 if pull_request.is_closed():
990 if pull_request.is_closed():
1065 raise JSONRPCError(
991 raise JSONRPCError(
1066 'pull request `%s` is already closed' % (pullrequestid,))
992 'pull request `%s` is already closed' % (pullrequestid,))
1067
993
1068 # only owner or admin or person with write permissions
994 # only owner or admin or person with write permissions
1069 allowed_to_close = PullRequestModel().check_user_update(
995 allowed_to_close = PullRequestModel().check_user_update(
1070 pull_request, apiuser, api=True)
996 pull_request, apiuser, api=True)
1071
997
1072 if not allowed_to_close:
998 if not allowed_to_close:
1073 raise JSONRPCError(
999 raise JSONRPCError(
1074 'pull request `%s` close failed, no permission to close.' % (
1000 'pull request `%s` close failed, no permission to close.' % (
1075 pullrequestid,))
1001 pullrequestid,))
1076
1002
1077 # message we're using to close the PR, else it's automatically generated
1003 # message we're using to close the PR, else it's automatically generated
1078 message = Optional.extract(message)
1004 message = Optional.extract(message)
1079
1005
1080 # finally close the PR, with proper message comment
1006 # finally close the PR, with proper message comment
1081 comment, status = PullRequestModel().close_pull_request_with_comment(
1007 comment, status = PullRequestModel().close_pull_request_with_comment(
1082 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1008 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1083 status_lbl = ChangesetStatus.get_status_lbl(status)
1009 status_lbl = ChangesetStatus.get_status_lbl(status)
1084
1010
1085 Session().commit()
1011 Session().commit()
1086
1012
1087 data = {
1013 data = {
1088 'pull_request_id': pull_request.pull_request_id,
1014 'pull_request_id': pull_request.pull_request_id,
1089 'close_status': status_lbl,
1015 'close_status': status_lbl,
1090 'closed': True,
1016 'closed': True,
1091 }
1017 }
1092 return data
1018 return data
@@ -1,2349 +1,2491 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import time
22 import time
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.api import (
25 from rhodecode.api import (
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 from rhodecode.api.utils import (
27 from rhodecode.api.utils import (
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 validate_set_owner_permissions)
31 validate_set_owner_permissions)
32 from rhodecode.lib import audit_logger, rc_cache
32 from rhodecode.lib import audit_logger, rc_cache
33 from rhodecode.lib import repo_maintenance
33 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
34 from rhodecode.lib.auth import (
35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
36 HasRepoPermissionAnyApi)
35 from rhodecode.lib.celerylib.utils import get_task_id
37 from rhodecode.lib.celerylib.utils import get_task_id
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int, safe_unicode
38 from rhodecode.lib.utils2 import (
39 str2bool, time_to_datetime, safe_str, safe_int, safe_unicode)
37 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 from rhodecode.lib.exceptions import (
42 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
39 from rhodecode.lib.vcs import RepositoryError
43 from rhodecode.lib.vcs import RepositoryError
40 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
44 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
41 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
42 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.db import (
47 from rhodecode.model.db import (
44 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
48 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
45 ChangesetComment)
49 ChangesetComment)
46 from rhodecode.model.permission import PermissionModel
50 from rhodecode.model.permission import PermissionModel
47 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
48 from rhodecode.model.scm import ScmModel, RepoList
52 from rhodecode.model.scm import ScmModel, RepoList
49 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
53 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
50 from rhodecode.model import validation_schema
54 from rhodecode.model import validation_schema
51 from rhodecode.model.validation_schema.schemas import repo_schema
55 from rhodecode.model.validation_schema.schemas import repo_schema
52
56
53 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
54
58
55
59
56 @jsonrpc_method()
60 @jsonrpc_method()
57 def get_repo(request, apiuser, repoid, cache=Optional(True)):
61 def get_repo(request, apiuser, repoid, cache=Optional(True)):
58 """
62 """
59 Gets an existing repository by its name or repository_id.
63 Gets an existing repository by its name or repository_id.
60
64
61 The members section so the output returns users groups or users
65 The members section so the output returns users groups or users
62 associated with that repository.
66 associated with that repository.
63
67
64 This command can only be run using an |authtoken| with admin rights,
68 This command can only be run using an |authtoken| with admin rights,
65 or users with at least read rights to the |repo|.
69 or users with at least read rights to the |repo|.
66
70
67 :param apiuser: This is filled automatically from the |authtoken|.
71 :param apiuser: This is filled automatically from the |authtoken|.
68 :type apiuser: AuthUser
72 :type apiuser: AuthUser
69 :param repoid: The repository name or repository id.
73 :param repoid: The repository name or repository id.
70 :type repoid: str or int
74 :type repoid: str or int
71 :param cache: use the cached value for last changeset
75 :param cache: use the cached value for last changeset
72 :type: cache: Optional(bool)
76 :type: cache: Optional(bool)
73
77
74 Example output:
78 Example output:
75
79
76 .. code-block:: bash
80 .. code-block:: bash
77
81
78 {
82 {
79 "error": null,
83 "error": null,
80 "id": <repo_id>,
84 "id": <repo_id>,
81 "result": {
85 "result": {
82 "clone_uri": null,
86 "clone_uri": null,
83 "created_on": "timestamp",
87 "created_on": "timestamp",
84 "description": "repo description",
88 "description": "repo description",
85 "enable_downloads": false,
89 "enable_downloads": false,
86 "enable_locking": false,
90 "enable_locking": false,
87 "enable_statistics": false,
91 "enable_statistics": false,
88 "followers": [
92 "followers": [
89 {
93 {
90 "active": true,
94 "active": true,
91 "admin": false,
95 "admin": false,
92 "api_key": "****************************************",
96 "api_key": "****************************************",
93 "api_keys": [
97 "api_keys": [
94 "****************************************"
98 "****************************************"
95 ],
99 ],
96 "email": "user@example.com",
100 "email": "user@example.com",
97 "emails": [
101 "emails": [
98 "user@example.com"
102 "user@example.com"
99 ],
103 ],
100 "extern_name": "rhodecode",
104 "extern_name": "rhodecode",
101 "extern_type": "rhodecode",
105 "extern_type": "rhodecode",
102 "firstname": "username",
106 "firstname": "username",
103 "ip_addresses": [],
107 "ip_addresses": [],
104 "language": null,
108 "language": null,
105 "last_login": "2015-09-16T17:16:35.854",
109 "last_login": "2015-09-16T17:16:35.854",
106 "lastname": "surname",
110 "lastname": "surname",
107 "user_id": <user_id>,
111 "user_id": <user_id>,
108 "username": "name"
112 "username": "name"
109 }
113 }
110 ],
114 ],
111 "fork_of": "parent-repo",
115 "fork_of": "parent-repo",
112 "landing_rev": [
116 "landing_rev": [
113 "rev",
117 "rev",
114 "tip"
118 "tip"
115 ],
119 ],
116 "last_changeset": {
120 "last_changeset": {
117 "author": "User <user@example.com>",
121 "author": "User <user@example.com>",
118 "branch": "default",
122 "branch": "default",
119 "date": "timestamp",
123 "date": "timestamp",
120 "message": "last commit message",
124 "message": "last commit message",
121 "parents": [
125 "parents": [
122 {
126 {
123 "raw_id": "commit-id"
127 "raw_id": "commit-id"
124 }
128 }
125 ],
129 ],
126 "raw_id": "commit-id",
130 "raw_id": "commit-id",
127 "revision": <revision number>,
131 "revision": <revision number>,
128 "short_id": "short id"
132 "short_id": "short id"
129 },
133 },
130 "lock_reason": null,
134 "lock_reason": null,
131 "locked_by": null,
135 "locked_by": null,
132 "locked_date": null,
136 "locked_date": null,
133 "owner": "owner-name",
137 "owner": "owner-name",
134 "permissions": [
138 "permissions": [
135 {
139 {
136 "name": "super-admin-name",
140 "name": "super-admin-name",
137 "origin": "super-admin",
141 "origin": "super-admin",
138 "permission": "repository.admin",
142 "permission": "repository.admin",
139 "type": "user"
143 "type": "user"
140 },
144 },
141 {
145 {
142 "name": "owner-name",
146 "name": "owner-name",
143 "origin": "owner",
147 "origin": "owner",
144 "permission": "repository.admin",
148 "permission": "repository.admin",
145 "type": "user"
149 "type": "user"
146 },
150 },
147 {
151 {
148 "name": "user-group-name",
152 "name": "user-group-name",
149 "origin": "permission",
153 "origin": "permission",
150 "permission": "repository.write",
154 "permission": "repository.write",
151 "type": "user_group"
155 "type": "user_group"
152 }
156 }
153 ],
157 ],
154 "private": true,
158 "private": true,
155 "repo_id": 676,
159 "repo_id": 676,
156 "repo_name": "user-group/repo-name",
160 "repo_name": "user-group/repo-name",
157 "repo_type": "hg"
161 "repo_type": "hg"
158 }
162 }
159 }
163 }
160 """
164 """
161
165
162 repo = get_repo_or_error(repoid)
166 repo = get_repo_or_error(repoid)
163 cache = Optional.extract(cache)
167 cache = Optional.extract(cache)
164
168
165 include_secrets = False
169 include_secrets = False
166 if has_superadmin_permission(apiuser):
170 if has_superadmin_permission(apiuser):
167 include_secrets = True
171 include_secrets = True
168 else:
172 else:
169 # check if we have at least read permission for this repo !
173 # check if we have at least read permission for this repo !
170 _perms = (
174 _perms = (
171 'repository.admin', 'repository.write', 'repository.read',)
175 'repository.admin', 'repository.write', 'repository.read',)
172 validate_repo_permissions(apiuser, repoid, repo, _perms)
176 validate_repo_permissions(apiuser, repoid, repo, _perms)
173
177
174 permissions = []
178 permissions = []
175 for _user in repo.permissions():
179 for _user in repo.permissions():
176 user_data = {
180 user_data = {
177 'name': _user.username,
181 'name': _user.username,
178 'permission': _user.permission,
182 'permission': _user.permission,
179 'origin': get_origin(_user),
183 'origin': get_origin(_user),
180 'type': "user",
184 'type': "user",
181 }
185 }
182 permissions.append(user_data)
186 permissions.append(user_data)
183
187
184 for _user_group in repo.permission_user_groups():
188 for _user_group in repo.permission_user_groups():
185 user_group_data = {
189 user_group_data = {
186 'name': _user_group.users_group_name,
190 'name': _user_group.users_group_name,
187 'permission': _user_group.permission,
191 'permission': _user_group.permission,
188 'origin': get_origin(_user_group),
192 'origin': get_origin(_user_group),
189 'type': "user_group",
193 'type': "user_group",
190 }
194 }
191 permissions.append(user_group_data)
195 permissions.append(user_group_data)
192
196
193 following_users = [
197 following_users = [
194 user.user.get_api_data(include_secrets=include_secrets)
198 user.user.get_api_data(include_secrets=include_secrets)
195 for user in repo.followers]
199 for user in repo.followers]
196
200
197 if not cache:
201 if not cache:
198 repo.update_commit_cache()
202 repo.update_commit_cache()
199 data = repo.get_api_data(include_secrets=include_secrets)
203 data = repo.get_api_data(include_secrets=include_secrets)
200 data['permissions'] = permissions
204 data['permissions'] = permissions
201 data['followers'] = following_users
205 data['followers'] = following_users
202 return data
206 return data
203
207
204
208
205 @jsonrpc_method()
209 @jsonrpc_method()
206 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
210 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
207 """
211 """
208 Lists all existing repositories.
212 Lists all existing repositories.
209
213
210 This command can only be run using an |authtoken| with admin rights,
214 This command can only be run using an |authtoken| with admin rights,
211 or users with at least read rights to |repos|.
215 or users with at least read rights to |repos|.
212
216
213 :param apiuser: This is filled automatically from the |authtoken|.
217 :param apiuser: This is filled automatically from the |authtoken|.
214 :type apiuser: AuthUser
218 :type apiuser: AuthUser
215 :param root: specify root repository group to fetch repositories.
219 :param root: specify root repository group to fetch repositories.
216 filters the returned repositories to be members of given root group.
220 filters the returned repositories to be members of given root group.
217 :type root: Optional(None)
221 :type root: Optional(None)
218 :param traverse: traverse given root into subrepositories. With this flag
222 :param traverse: traverse given root into subrepositories. With this flag
219 set to False, it will only return top-level repositories from `root`.
223 set to False, it will only return top-level repositories from `root`.
220 if root is empty it will return just top-level repositories.
224 if root is empty it will return just top-level repositories.
221 :type traverse: Optional(True)
225 :type traverse: Optional(True)
222
226
223
227
224 Example output:
228 Example output:
225
229
226 .. code-block:: bash
230 .. code-block:: bash
227
231
228 id : <id_given_in_input>
232 id : <id_given_in_input>
229 result: [
233 result: [
230 {
234 {
231 "repo_id" : "<repo_id>",
235 "repo_id" : "<repo_id>",
232 "repo_name" : "<reponame>"
236 "repo_name" : "<reponame>"
233 "repo_type" : "<repo_type>",
237 "repo_type" : "<repo_type>",
234 "clone_uri" : "<clone_uri>",
238 "clone_uri" : "<clone_uri>",
235 "private": : "<bool>",
239 "private": : "<bool>",
236 "created_on" : "<datetimecreated>",
240 "created_on" : "<datetimecreated>",
237 "description" : "<description>",
241 "description" : "<description>",
238 "landing_rev": "<landing_rev>",
242 "landing_rev": "<landing_rev>",
239 "owner": "<repo_owner>",
243 "owner": "<repo_owner>",
240 "fork_of": "<name_of_fork_parent>",
244 "fork_of": "<name_of_fork_parent>",
241 "enable_downloads": "<bool>",
245 "enable_downloads": "<bool>",
242 "enable_locking": "<bool>",
246 "enable_locking": "<bool>",
243 "enable_statistics": "<bool>",
247 "enable_statistics": "<bool>",
244 },
248 },
245 ...
249 ...
246 ]
250 ]
247 error: null
251 error: null
248 """
252 """
249
253
250 include_secrets = has_superadmin_permission(apiuser)
254 include_secrets = has_superadmin_permission(apiuser)
251 _perms = ('repository.read', 'repository.write', 'repository.admin',)
255 _perms = ('repository.read', 'repository.write', 'repository.admin',)
252 extras = {'user': apiuser}
256 extras = {'user': apiuser}
253
257
254 root = Optional.extract(root)
258 root = Optional.extract(root)
255 traverse = Optional.extract(traverse, binary=True)
259 traverse = Optional.extract(traverse, binary=True)
256
260
257 if root:
261 if root:
258 # verify parent existance, if it's empty return an error
262 # verify parent existance, if it's empty return an error
259 parent = RepoGroup.get_by_group_name(root)
263 parent = RepoGroup.get_by_group_name(root)
260 if not parent:
264 if not parent:
261 raise JSONRPCError(
265 raise JSONRPCError(
262 'Root repository group `{}` does not exist'.format(root))
266 'Root repository group `{}` does not exist'.format(root))
263
267
264 if traverse:
268 if traverse:
265 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
269 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
266 else:
270 else:
267 repos = RepoModel().get_repos_for_root(root=parent)
271 repos = RepoModel().get_repos_for_root(root=parent)
268 else:
272 else:
269 if traverse:
273 if traverse:
270 repos = RepoModel().get_all()
274 repos = RepoModel().get_all()
271 else:
275 else:
272 # return just top-level
276 # return just top-level
273 repos = RepoModel().get_repos_for_root(root=None)
277 repos = RepoModel().get_repos_for_root(root=None)
274
278
275 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
279 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
276 return [repo.get_api_data(include_secrets=include_secrets)
280 return [repo.get_api_data(include_secrets=include_secrets)
277 for repo in repo_list]
281 for repo in repo_list]
278
282
279
283
280 @jsonrpc_method()
284 @jsonrpc_method()
281 def get_repo_changeset(request, apiuser, repoid, revision,
285 def get_repo_changeset(request, apiuser, repoid, revision,
282 details=Optional('basic')):
286 details=Optional('basic')):
283 """
287 """
284 Returns information about a changeset.
288 Returns information about a changeset.
285
289
286 Additionally parameters define the amount of details returned by
290 Additionally parameters define the amount of details returned by
287 this function.
291 this function.
288
292
289 This command can only be run using an |authtoken| with admin rights,
293 This command can only be run using an |authtoken| with admin rights,
290 or users with at least read rights to the |repo|.
294 or users with at least read rights to the |repo|.
291
295
292 :param apiuser: This is filled automatically from the |authtoken|.
296 :param apiuser: This is filled automatically from the |authtoken|.
293 :type apiuser: AuthUser
297 :type apiuser: AuthUser
294 :param repoid: The repository name or repository id
298 :param repoid: The repository name or repository id
295 :type repoid: str or int
299 :type repoid: str or int
296 :param revision: revision for which listing should be done
300 :param revision: revision for which listing should be done
297 :type revision: str
301 :type revision: str
298 :param details: details can be 'basic|extended|full' full gives diff
302 :param details: details can be 'basic|extended|full' full gives diff
299 info details like the diff itself, and number of changed files etc.
303 info details like the diff itself, and number of changed files etc.
300 :type details: Optional(str)
304 :type details: Optional(str)
301
305
302 """
306 """
303 repo = get_repo_or_error(repoid)
307 repo = get_repo_or_error(repoid)
304 if not has_superadmin_permission(apiuser):
308 if not has_superadmin_permission(apiuser):
305 _perms = (
309 _perms = (
306 'repository.admin', 'repository.write', 'repository.read',)
310 'repository.admin', 'repository.write', 'repository.read',)
307 validate_repo_permissions(apiuser, repoid, repo, _perms)
311 validate_repo_permissions(apiuser, repoid, repo, _perms)
308
312
309 changes_details = Optional.extract(details)
313 changes_details = Optional.extract(details)
310 _changes_details_types = ['basic', 'extended', 'full']
314 _changes_details_types = ['basic', 'extended', 'full']
311 if changes_details not in _changes_details_types:
315 if changes_details not in _changes_details_types:
312 raise JSONRPCError(
316 raise JSONRPCError(
313 'ret_type must be one of %s' % (
317 'ret_type must be one of %s' % (
314 ','.join(_changes_details_types)))
318 ','.join(_changes_details_types)))
315
319
316 pre_load = ['author', 'branch', 'date', 'message', 'parents',
320 pre_load = ['author', 'branch', 'date', 'message', 'parents',
317 'status', '_commit', '_file_paths']
321 'status', '_commit', '_file_paths']
318
322
319 try:
323 try:
320 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
324 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
321 except TypeError as e:
325 except TypeError as e:
322 raise JSONRPCError(safe_str(e))
326 raise JSONRPCError(safe_str(e))
323 _cs_json = cs.__json__()
327 _cs_json = cs.__json__()
324 _cs_json['diff'] = build_commit_data(cs, changes_details)
328 _cs_json['diff'] = build_commit_data(cs, changes_details)
325 if changes_details == 'full':
329 if changes_details == 'full':
326 _cs_json['refs'] = cs._get_refs()
330 _cs_json['refs'] = cs._get_refs()
327 return _cs_json
331 return _cs_json
328
332
329
333
330 @jsonrpc_method()
334 @jsonrpc_method()
331 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
335 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
332 details=Optional('basic')):
336 details=Optional('basic')):
333 """
337 """
334 Returns a set of commits limited by the number starting
338 Returns a set of commits limited by the number starting
335 from the `start_rev` option.
339 from the `start_rev` option.
336
340
337 Additional parameters define the amount of details returned by this
341 Additional parameters define the amount of details returned by this
338 function.
342 function.
339
343
340 This command can only be run using an |authtoken| with admin rights,
344 This command can only be run using an |authtoken| with admin rights,
341 or users with at least read rights to |repos|.
345 or users with at least read rights to |repos|.
342
346
343 :param apiuser: This is filled automatically from the |authtoken|.
347 :param apiuser: This is filled automatically from the |authtoken|.
344 :type apiuser: AuthUser
348 :type apiuser: AuthUser
345 :param repoid: The repository name or repository ID.
349 :param repoid: The repository name or repository ID.
346 :type repoid: str or int
350 :type repoid: str or int
347 :param start_rev: The starting revision from where to get changesets.
351 :param start_rev: The starting revision from where to get changesets.
348 :type start_rev: str
352 :type start_rev: str
349 :param limit: Limit the number of commits to this amount
353 :param limit: Limit the number of commits to this amount
350 :type limit: str or int
354 :type limit: str or int
351 :param details: Set the level of detail returned. Valid option are:
355 :param details: Set the level of detail returned. Valid option are:
352 ``basic``, ``extended`` and ``full``.
356 ``basic``, ``extended`` and ``full``.
353 :type details: Optional(str)
357 :type details: Optional(str)
354
358
355 .. note::
359 .. note::
356
360
357 Setting the parameter `details` to the value ``full`` is extensive
361 Setting the parameter `details` to the value ``full`` is extensive
358 and returns details like the diff itself, and the number
362 and returns details like the diff itself, and the number
359 of changed files.
363 of changed files.
360
364
361 """
365 """
362 repo = get_repo_or_error(repoid)
366 repo = get_repo_or_error(repoid)
363 if not has_superadmin_permission(apiuser):
367 if not has_superadmin_permission(apiuser):
364 _perms = (
368 _perms = (
365 'repository.admin', 'repository.write', 'repository.read',)
369 'repository.admin', 'repository.write', 'repository.read',)
366 validate_repo_permissions(apiuser, repoid, repo, _perms)
370 validate_repo_permissions(apiuser, repoid, repo, _perms)
367
371
368 changes_details = Optional.extract(details)
372 changes_details = Optional.extract(details)
369 _changes_details_types = ['basic', 'extended', 'full']
373 _changes_details_types = ['basic', 'extended', 'full']
370 if changes_details not in _changes_details_types:
374 if changes_details not in _changes_details_types:
371 raise JSONRPCError(
375 raise JSONRPCError(
372 'ret_type must be one of %s' % (
376 'ret_type must be one of %s' % (
373 ','.join(_changes_details_types)))
377 ','.join(_changes_details_types)))
374
378
375 limit = int(limit)
379 limit = int(limit)
376 pre_load = ['author', 'branch', 'date', 'message', 'parents',
380 pre_load = ['author', 'branch', 'date', 'message', 'parents',
377 'status', '_commit', '_file_paths']
381 'status', '_commit', '_file_paths']
378
382
379 vcs_repo = repo.scm_instance()
383 vcs_repo = repo.scm_instance()
380 # SVN needs a special case to distinguish its index and commit id
384 # SVN needs a special case to distinguish its index and commit id
381 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
385 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
382 start_rev = vcs_repo.commit_ids[0]
386 start_rev = vcs_repo.commit_ids[0]
383
387
384 try:
388 try:
385 commits = vcs_repo.get_commits(
389 commits = vcs_repo.get_commits(
386 start_id=start_rev, pre_load=pre_load, translate_tags=False)
390 start_id=start_rev, pre_load=pre_load, translate_tags=False)
387 except TypeError as e:
391 except TypeError as e:
388 raise JSONRPCError(safe_str(e))
392 raise JSONRPCError(safe_str(e))
389 except Exception:
393 except Exception:
390 log.exception('Fetching of commits failed')
394 log.exception('Fetching of commits failed')
391 raise JSONRPCError('Error occurred during commit fetching')
395 raise JSONRPCError('Error occurred during commit fetching')
392
396
393 ret = []
397 ret = []
394 for cnt, commit in enumerate(commits):
398 for cnt, commit in enumerate(commits):
395 if cnt >= limit != -1:
399 if cnt >= limit != -1:
396 break
400 break
397 _cs_json = commit.__json__()
401 _cs_json = commit.__json__()
398 _cs_json['diff'] = build_commit_data(commit, changes_details)
402 _cs_json['diff'] = build_commit_data(commit, changes_details)
399 if changes_details == 'full':
403 if changes_details == 'full':
400 _cs_json['refs'] = {
404 _cs_json['refs'] = {
401 'branches': [commit.branch],
405 'branches': [commit.branch],
402 'bookmarks': getattr(commit, 'bookmarks', []),
406 'bookmarks': getattr(commit, 'bookmarks', []),
403 'tags': commit.tags
407 'tags': commit.tags
404 }
408 }
405 ret.append(_cs_json)
409 ret.append(_cs_json)
406 return ret
410 return ret
407
411
408
412
409 @jsonrpc_method()
413 @jsonrpc_method()
410 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
414 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
411 ret_type=Optional('all'), details=Optional('basic'),
415 ret_type=Optional('all'), details=Optional('basic'),
412 max_file_bytes=Optional(None)):
416 max_file_bytes=Optional(None)):
413 """
417 """
414 Returns a list of nodes and children in a flat list for a given
418 Returns a list of nodes and children in a flat list for a given
415 path at given revision.
419 path at given revision.
416
420
417 It's possible to specify ret_type to show only `files` or `dirs`.
421 It's possible to specify ret_type to show only `files` or `dirs`.
418
422
419 This command can only be run using an |authtoken| with admin rights,
423 This command can only be run using an |authtoken| with admin rights,
420 or users with at least read rights to |repos|.
424 or users with at least read rights to |repos|.
421
425
422 :param apiuser: This is filled automatically from the |authtoken|.
426 :param apiuser: This is filled automatically from the |authtoken|.
423 :type apiuser: AuthUser
427 :type apiuser: AuthUser
424 :param repoid: The repository name or repository ID.
428 :param repoid: The repository name or repository ID.
425 :type repoid: str or int
429 :type repoid: str or int
426 :param revision: The revision for which listing should be done.
430 :param revision: The revision for which listing should be done.
427 :type revision: str
431 :type revision: str
428 :param root_path: The path from which to start displaying.
432 :param root_path: The path from which to start displaying.
429 :type root_path: str
433 :type root_path: str
430 :param ret_type: Set the return type. Valid options are
434 :param ret_type: Set the return type. Valid options are
431 ``all`` (default), ``files`` and ``dirs``.
435 ``all`` (default), ``files`` and ``dirs``.
432 :type ret_type: Optional(str)
436 :type ret_type: Optional(str)
433 :param details: Returns extended information about nodes, such as
437 :param details: Returns extended information about nodes, such as
434 md5, binary, and or content.
438 md5, binary, and or content.
435 The valid options are ``basic`` and ``full``.
439 The valid options are ``basic`` and ``full``.
436 :type details: Optional(str)
440 :type details: Optional(str)
437 :param max_file_bytes: Only return file content under this file size bytes
441 :param max_file_bytes: Only return file content under this file size bytes
438 :type details: Optional(int)
442 :type details: Optional(int)
439
443
440 Example output:
444 Example output:
441
445
442 .. code-block:: bash
446 .. code-block:: bash
443
447
444 id : <id_given_in_input>
448 id : <id_given_in_input>
445 result: [
449 result: [
446 {
450 {
447 "binary": false,
451 "binary": false,
448 "content": "File line",
452 "content": "File line",
449 "extension": "md",
453 "extension": "md",
450 "lines": 2,
454 "lines": 2,
451 "md5": "059fa5d29b19c0657e384749480f6422",
455 "md5": "059fa5d29b19c0657e384749480f6422",
452 "mimetype": "text/x-minidsrc",
456 "mimetype": "text/x-minidsrc",
453 "name": "file.md",
457 "name": "file.md",
454 "size": 580,
458 "size": 580,
455 "type": "file"
459 "type": "file"
456 },
460 },
457 ...
461 ...
458 ]
462 ]
459 error: null
463 error: null
460 """
464 """
461
465
462 repo = get_repo_or_error(repoid)
466 repo = get_repo_or_error(repoid)
463 if not has_superadmin_permission(apiuser):
467 if not has_superadmin_permission(apiuser):
464 _perms = ('repository.admin', 'repository.write', 'repository.read',)
468 _perms = ('repository.admin', 'repository.write', 'repository.read',)
465 validate_repo_permissions(apiuser, repoid, repo, _perms)
469 validate_repo_permissions(apiuser, repoid, repo, _perms)
466
470
467 ret_type = Optional.extract(ret_type)
471 ret_type = Optional.extract(ret_type)
468 details = Optional.extract(details)
472 details = Optional.extract(details)
469 _extended_types = ['basic', 'full']
473 _extended_types = ['basic', 'full']
470 if details not in _extended_types:
474 if details not in _extended_types:
471 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
475 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
472 extended_info = False
476 extended_info = False
473 content = False
477 content = False
474 if details == 'basic':
478 if details == 'basic':
475 extended_info = True
479 extended_info = True
476
480
477 if details == 'full':
481 if details == 'full':
478 extended_info = content = True
482 extended_info = content = True
479
483
480 _map = {}
484 _map = {}
481 try:
485 try:
482 # check if repo is not empty by any chance, skip quicker if it is.
486 # check if repo is not empty by any chance, skip quicker if it is.
483 _scm = repo.scm_instance()
487 _scm = repo.scm_instance()
484 if _scm.is_empty():
488 if _scm.is_empty():
485 return []
489 return []
486
490
487 _d, _f = ScmModel().get_nodes(
491 _d, _f = ScmModel().get_nodes(
488 repo, revision, root_path, flat=False,
492 repo, revision, root_path, flat=False,
489 extended_info=extended_info, content=content,
493 extended_info=extended_info, content=content,
490 max_file_bytes=max_file_bytes)
494 max_file_bytes=max_file_bytes)
491 _map = {
495 _map = {
492 'all': _d + _f,
496 'all': _d + _f,
493 'files': _f,
497 'files': _f,
494 'dirs': _d,
498 'dirs': _d,
495 }
499 }
496 return _map[ret_type]
500 return _map[ret_type]
497 except KeyError:
501 except KeyError:
498 raise JSONRPCError(
502 raise JSONRPCError(
499 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
503 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
500 except Exception:
504 except Exception:
501 log.exception("Exception occurred while trying to get repo nodes")
505 log.exception("Exception occurred while trying to get repo nodes")
502 raise JSONRPCError(
506 raise JSONRPCError(
503 'failed to get repo: `%s` nodes' % repo.repo_name
507 'failed to get repo: `%s` nodes' % repo.repo_name
504 )
508 )
505
509
506
510
507 @jsonrpc_method()
511 @jsonrpc_method()
508 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
512 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
509 max_file_bytes=Optional(None), details=Optional('basic'),
513 max_file_bytes=Optional(None), details=Optional('basic'),
510 cache=Optional(True)):
514 cache=Optional(True)):
511 """
515 """
512 Returns a single file from repository at given revision.
516 Returns a single file from repository at given revision.
513
517
514 This command can only be run using an |authtoken| with admin rights,
518 This command can only be run using an |authtoken| with admin rights,
515 or users with at least read rights to |repos|.
519 or users with at least read rights to |repos|.
516
520
517 :param apiuser: This is filled automatically from the |authtoken|.
521 :param apiuser: This is filled automatically from the |authtoken|.
518 :type apiuser: AuthUser
522 :type apiuser: AuthUser
519 :param repoid: The repository name or repository ID.
523 :param repoid: The repository name or repository ID.
520 :type repoid: str or int
524 :type repoid: str or int
521 :param commit_id: The revision for which listing should be done.
525 :param commit_id: The revision for which listing should be done.
522 :type commit_id: str
526 :type commit_id: str
523 :param file_path: The path from which to start displaying.
527 :param file_path: The path from which to start displaying.
524 :type file_path: str
528 :type file_path: str
525 :param details: Returns different set of information about nodes.
529 :param details: Returns different set of information about nodes.
526 The valid options are ``minimal`` ``basic`` and ``full``.
530 The valid options are ``minimal`` ``basic`` and ``full``.
527 :type details: Optional(str)
531 :type details: Optional(str)
528 :param max_file_bytes: Only return file content under this file size bytes
532 :param max_file_bytes: Only return file content under this file size bytes
529 :type max_file_bytes: Optional(int)
533 :type max_file_bytes: Optional(int)
530 :param cache: Use internal caches for fetching files. If disabled fetching
534 :param cache: Use internal caches for fetching files. If disabled fetching
531 files is slower but more memory efficient
535 files is slower but more memory efficient
532 :type cache: Optional(bool)
536 :type cache: Optional(bool)
533
537
534 Example output:
538 Example output:
535
539
536 .. code-block:: bash
540 .. code-block:: bash
537
541
538 id : <id_given_in_input>
542 id : <id_given_in_input>
539 result: {
543 result: {
540 "binary": false,
544 "binary": false,
541 "extension": "py",
545 "extension": "py",
542 "lines": 35,
546 "lines": 35,
543 "content": "....",
547 "content": "....",
544 "md5": "76318336366b0f17ee249e11b0c99c41",
548 "md5": "76318336366b0f17ee249e11b0c99c41",
545 "mimetype": "text/x-python",
549 "mimetype": "text/x-python",
546 "name": "python.py",
550 "name": "python.py",
547 "size": 817,
551 "size": 817,
548 "type": "file",
552 "type": "file",
549 }
553 }
550 error: null
554 error: null
551 """
555 """
552
556
553 repo = get_repo_or_error(repoid)
557 repo = get_repo_or_error(repoid)
554 if not has_superadmin_permission(apiuser):
558 if not has_superadmin_permission(apiuser):
555 _perms = ('repository.admin', 'repository.write', 'repository.read',)
559 _perms = ('repository.admin', 'repository.write', 'repository.read',)
556 validate_repo_permissions(apiuser, repoid, repo, _perms)
560 validate_repo_permissions(apiuser, repoid, repo, _perms)
557
561
558 cache = Optional.extract(cache, binary=True)
562 cache = Optional.extract(cache, binary=True)
559 details = Optional.extract(details)
563 details = Optional.extract(details)
560 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
564 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
561 if details not in _extended_types:
565 if details not in _extended_types:
562 raise JSONRPCError(
566 raise JSONRPCError(
563 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
567 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
564 extended_info = False
568 extended_info = False
565 content = False
569 content = False
566
570
567 if details == 'minimal':
571 if details == 'minimal':
568 extended_info = False
572 extended_info = False
569
573
570 elif details == 'basic':
574 elif details == 'basic':
571 extended_info = True
575 extended_info = True
572
576
573 elif details == 'full':
577 elif details == 'full':
574 extended_info = content = True
578 extended_info = content = True
575
579
576 file_path = safe_unicode(file_path)
580 file_path = safe_unicode(file_path)
577 try:
581 try:
578 # check if repo is not empty by any chance, skip quicker if it is.
582 # check if repo is not empty by any chance, skip quicker if it is.
579 _scm = repo.scm_instance()
583 _scm = repo.scm_instance()
580 if _scm.is_empty():
584 if _scm.is_empty():
581 return None
585 return None
582
586
583 node = ScmModel().get_node(
587 node = ScmModel().get_node(
584 repo, commit_id, file_path, extended_info=extended_info,
588 repo, commit_id, file_path, extended_info=extended_info,
585 content=content, max_file_bytes=max_file_bytes, cache=cache)
589 content=content, max_file_bytes=max_file_bytes, cache=cache)
586 except NodeDoesNotExistError:
590 except NodeDoesNotExistError:
587 raise JSONRPCError(u'There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
591 raise JSONRPCError(u'There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
588 repo.repo_name, file_path, commit_id))
592 repo.repo_name, file_path, commit_id))
589 except Exception:
593 except Exception:
590 log.exception(u"Exception occurred while trying to get repo %s file",
594 log.exception(u"Exception occurred while trying to get repo %s file",
591 repo.repo_name)
595 repo.repo_name)
592 raise JSONRPCError(u'failed to get repo: `{}` file at path {}'.format(
596 raise JSONRPCError(u'failed to get repo: `{}` file at path {}'.format(
593 repo.repo_name, file_path))
597 repo.repo_name, file_path))
594
598
595 return node
599 return node
596
600
597
601
598 @jsonrpc_method()
602 @jsonrpc_method()
599 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
603 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
600 """
604 """
601 Returns a list of tree nodes for path at given revision. This api is built
605 Returns a list of tree nodes for path at given revision. This api is built
602 strictly for usage in full text search building, and shouldn't be consumed
606 strictly for usage in full text search building, and shouldn't be consumed
603
607
604 This command can only be run using an |authtoken| with admin rights,
608 This command can only be run using an |authtoken| with admin rights,
605 or users with at least read rights to |repos|.
609 or users with at least read rights to |repos|.
606
610
607 """
611 """
608
612
609 repo = get_repo_or_error(repoid)
613 repo = get_repo_or_error(repoid)
610 if not has_superadmin_permission(apiuser):
614 if not has_superadmin_permission(apiuser):
611 _perms = ('repository.admin', 'repository.write', 'repository.read',)
615 _perms = ('repository.admin', 'repository.write', 'repository.read',)
612 validate_repo_permissions(apiuser, repoid, repo, _perms)
616 validate_repo_permissions(apiuser, repoid, repo, _perms)
613
617
614 repo_id = repo.repo_id
618 repo_id = repo.repo_id
615 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
619 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
616 cache_on = cache_seconds > 0
620 cache_on = cache_seconds > 0
617
621
618 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
622 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
619 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
623 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
620
624
621 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
625 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
622 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
626 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
623
627
624 try:
628 try:
625 # check if repo is not empty by any chance, skip quicker if it is.
629 # check if repo is not empty by any chance, skip quicker if it is.
626 _scm = repo.scm_instance()
630 _scm = repo.scm_instance()
627 if _scm.is_empty():
631 if _scm.is_empty():
628 return []
632 return []
629 except RepositoryError:
633 except RepositoryError:
630 log.exception("Exception occurred while trying to get repo nodes")
634 log.exception("Exception occurred while trying to get repo nodes")
631 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
635 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
632
636
633 try:
637 try:
634 # we need to resolve commit_id to a FULL sha for cache to work correctly.
638 # we need to resolve commit_id to a FULL sha for cache to work correctly.
635 # sending 'master' is a pointer that needs to be translated to current commit.
639 # sending 'master' is a pointer that needs to be translated to current commit.
636 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
640 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
637 log.debug(
641 log.debug(
638 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
642 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
639 'with caching: %s[TTL: %ss]' % (
643 'with caching: %s[TTL: %ss]' % (
640 repo_id, commit_id, cache_on, cache_seconds or 0))
644 repo_id, commit_id, cache_on, cache_seconds or 0))
641
645
642 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
646 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
643 return tree_files
647 return tree_files
644
648
645 except Exception:
649 except Exception:
646 log.exception("Exception occurred while trying to get repo nodes")
650 log.exception("Exception occurred while trying to get repo nodes")
647 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
651 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
648
652
649
653
650 @jsonrpc_method()
654 @jsonrpc_method()
651 def get_repo_refs(request, apiuser, repoid):
655 def get_repo_refs(request, apiuser, repoid):
652 """
656 """
653 Returns a dictionary of current references. It returns
657 Returns a dictionary of current references. It returns
654 bookmarks, branches, closed_branches, and tags for given repository
658 bookmarks, branches, closed_branches, and tags for given repository
655
659
656 It's possible to specify ret_type to show only `files` or `dirs`.
660 It's possible to specify ret_type to show only `files` or `dirs`.
657
661
658 This command can only be run using an |authtoken| with admin rights,
662 This command can only be run using an |authtoken| with admin rights,
659 or users with at least read rights to |repos|.
663 or users with at least read rights to |repos|.
660
664
661 :param apiuser: This is filled automatically from the |authtoken|.
665 :param apiuser: This is filled automatically from the |authtoken|.
662 :type apiuser: AuthUser
666 :type apiuser: AuthUser
663 :param repoid: The repository name or repository ID.
667 :param repoid: The repository name or repository ID.
664 :type repoid: str or int
668 :type repoid: str or int
665
669
666 Example output:
670 Example output:
667
671
668 .. code-block:: bash
672 .. code-block:: bash
669
673
670 id : <id_given_in_input>
674 id : <id_given_in_input>
671 "result": {
675 "result": {
672 "bookmarks": {
676 "bookmarks": {
673 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
677 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
674 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
678 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
675 },
679 },
676 "branches": {
680 "branches": {
677 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
681 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
678 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
682 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
679 },
683 },
680 "branches_closed": {},
684 "branches_closed": {},
681 "tags": {
685 "tags": {
682 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
686 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
683 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
687 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
684 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
688 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
685 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
689 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
686 }
690 }
687 }
691 }
688 error: null
692 error: null
689 """
693 """
690
694
691 repo = get_repo_or_error(repoid)
695 repo = get_repo_or_error(repoid)
692 if not has_superadmin_permission(apiuser):
696 if not has_superadmin_permission(apiuser):
693 _perms = ('repository.admin', 'repository.write', 'repository.read',)
697 _perms = ('repository.admin', 'repository.write', 'repository.read',)
694 validate_repo_permissions(apiuser, repoid, repo, _perms)
698 validate_repo_permissions(apiuser, repoid, repo, _perms)
695
699
696 try:
700 try:
697 # check if repo is not empty by any chance, skip quicker if it is.
701 # check if repo is not empty by any chance, skip quicker if it is.
698 vcs_instance = repo.scm_instance()
702 vcs_instance = repo.scm_instance()
699 refs = vcs_instance.refs()
703 refs = vcs_instance.refs()
700 return refs
704 return refs
701 except Exception:
705 except Exception:
702 log.exception("Exception occurred while trying to get repo refs")
706 log.exception("Exception occurred while trying to get repo refs")
703 raise JSONRPCError(
707 raise JSONRPCError(
704 'failed to get repo: `%s` references' % repo.repo_name
708 'failed to get repo: `%s` references' % repo.repo_name
705 )
709 )
706
710
707
711
708 @jsonrpc_method()
712 @jsonrpc_method()
709 def create_repo(
713 def create_repo(
710 request, apiuser, repo_name, repo_type,
714 request, apiuser, repo_name, repo_type,
711 owner=Optional(OAttr('apiuser')),
715 owner=Optional(OAttr('apiuser')),
712 description=Optional(''),
716 description=Optional(''),
713 private=Optional(False),
717 private=Optional(False),
714 clone_uri=Optional(None),
718 clone_uri=Optional(None),
715 push_uri=Optional(None),
719 push_uri=Optional(None),
716 landing_rev=Optional(None),
720 landing_rev=Optional(None),
717 enable_statistics=Optional(False),
721 enable_statistics=Optional(False),
718 enable_locking=Optional(False),
722 enable_locking=Optional(False),
719 enable_downloads=Optional(False),
723 enable_downloads=Optional(False),
720 copy_permissions=Optional(False)):
724 copy_permissions=Optional(False)):
721 """
725 """
722 Creates a repository.
726 Creates a repository.
723
727
724 * If the repository name contains "/", repository will be created inside
728 * If the repository name contains "/", repository will be created inside
725 a repository group or nested repository groups
729 a repository group or nested repository groups
726
730
727 For example "foo/bar/repo1" will create |repo| called "repo1" inside
731 For example "foo/bar/repo1" will create |repo| called "repo1" inside
728 group "foo/bar". You have to have permissions to access and write to
732 group "foo/bar". You have to have permissions to access and write to
729 the last repository group ("bar" in this example)
733 the last repository group ("bar" in this example)
730
734
731 This command can only be run using an |authtoken| with at least
735 This command can only be run using an |authtoken| with at least
732 permissions to create repositories, or write permissions to
736 permissions to create repositories, or write permissions to
733 parent repository groups.
737 parent repository groups.
734
738
735 :param apiuser: This is filled automatically from the |authtoken|.
739 :param apiuser: This is filled automatically from the |authtoken|.
736 :type apiuser: AuthUser
740 :type apiuser: AuthUser
737 :param repo_name: Set the repository name.
741 :param repo_name: Set the repository name.
738 :type repo_name: str
742 :type repo_name: str
739 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
743 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
740 :type repo_type: str
744 :type repo_type: str
741 :param owner: user_id or username
745 :param owner: user_id or username
742 :type owner: Optional(str)
746 :type owner: Optional(str)
743 :param description: Set the repository description.
747 :param description: Set the repository description.
744 :type description: Optional(str)
748 :type description: Optional(str)
745 :param private: set repository as private
749 :param private: set repository as private
746 :type private: bool
750 :type private: bool
747 :param clone_uri: set clone_uri
751 :param clone_uri: set clone_uri
748 :type clone_uri: str
752 :type clone_uri: str
749 :param push_uri: set push_uri
753 :param push_uri: set push_uri
750 :type push_uri: str
754 :type push_uri: str
751 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
755 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
752 :type landing_rev: str
756 :type landing_rev: str
753 :param enable_locking:
757 :param enable_locking:
754 :type enable_locking: bool
758 :type enable_locking: bool
755 :param enable_downloads:
759 :param enable_downloads:
756 :type enable_downloads: bool
760 :type enable_downloads: bool
757 :param enable_statistics:
761 :param enable_statistics:
758 :type enable_statistics: bool
762 :type enable_statistics: bool
759 :param copy_permissions: Copy permission from group in which the
763 :param copy_permissions: Copy permission from group in which the
760 repository is being created.
764 repository is being created.
761 :type copy_permissions: bool
765 :type copy_permissions: bool
762
766
763
767
764 Example output:
768 Example output:
765
769
766 .. code-block:: bash
770 .. code-block:: bash
767
771
768 id : <id_given_in_input>
772 id : <id_given_in_input>
769 result: {
773 result: {
770 "msg": "Created new repository `<reponame>`",
774 "msg": "Created new repository `<reponame>`",
771 "success": true,
775 "success": true,
772 "task": "<celery task id or None if done sync>"
776 "task": "<celery task id or None if done sync>"
773 }
777 }
774 error: null
778 error: null
775
779
776
780
777 Example error output:
781 Example error output:
778
782
779 .. code-block:: bash
783 .. code-block:: bash
780
784
781 id : <id_given_in_input>
785 id : <id_given_in_input>
782 result : null
786 result : null
783 error : {
787 error : {
784 'failed to create repository `<repo_name>`'
788 'failed to create repository `<repo_name>`'
785 }
789 }
786
790
787 """
791 """
788
792
789 owner = validate_set_owner_permissions(apiuser, owner)
793 owner = validate_set_owner_permissions(apiuser, owner)
790
794
791 description = Optional.extract(description)
795 description = Optional.extract(description)
792 copy_permissions = Optional.extract(copy_permissions)
796 copy_permissions = Optional.extract(copy_permissions)
793 clone_uri = Optional.extract(clone_uri)
797 clone_uri = Optional.extract(clone_uri)
794 push_uri = Optional.extract(push_uri)
798 push_uri = Optional.extract(push_uri)
795
799
796 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
800 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
797 if isinstance(private, Optional):
801 if isinstance(private, Optional):
798 private = defs.get('repo_private') or Optional.extract(private)
802 private = defs.get('repo_private') or Optional.extract(private)
799 if isinstance(repo_type, Optional):
803 if isinstance(repo_type, Optional):
800 repo_type = defs.get('repo_type')
804 repo_type = defs.get('repo_type')
801 if isinstance(enable_statistics, Optional):
805 if isinstance(enable_statistics, Optional):
802 enable_statistics = defs.get('repo_enable_statistics')
806 enable_statistics = defs.get('repo_enable_statistics')
803 if isinstance(enable_locking, Optional):
807 if isinstance(enable_locking, Optional):
804 enable_locking = defs.get('repo_enable_locking')
808 enable_locking = defs.get('repo_enable_locking')
805 if isinstance(enable_downloads, Optional):
809 if isinstance(enable_downloads, Optional):
806 enable_downloads = defs.get('repo_enable_downloads')
810 enable_downloads = defs.get('repo_enable_downloads')
807
811
808 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
812 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
809 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
813 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
810 ref_choices = list(set(ref_choices + [landing_ref]))
814 ref_choices = list(set(ref_choices + [landing_ref]))
811
815
812 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
816 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
813
817
814 schema = repo_schema.RepoSchema().bind(
818 schema = repo_schema.RepoSchema().bind(
815 repo_type_options=rhodecode.BACKENDS.keys(),
819 repo_type_options=rhodecode.BACKENDS.keys(),
816 repo_ref_options=ref_choices,
820 repo_ref_options=ref_choices,
817 repo_type=repo_type,
821 repo_type=repo_type,
818 # user caller
822 # user caller
819 user=apiuser)
823 user=apiuser)
820
824
821 try:
825 try:
822 schema_data = schema.deserialize(dict(
826 schema_data = schema.deserialize(dict(
823 repo_name=repo_name,
827 repo_name=repo_name,
824 repo_type=repo_type,
828 repo_type=repo_type,
825 repo_owner=owner.username,
829 repo_owner=owner.username,
826 repo_description=description,
830 repo_description=description,
827 repo_landing_commit_ref=landing_commit_ref,
831 repo_landing_commit_ref=landing_commit_ref,
828 repo_clone_uri=clone_uri,
832 repo_clone_uri=clone_uri,
829 repo_push_uri=push_uri,
833 repo_push_uri=push_uri,
830 repo_private=private,
834 repo_private=private,
831 repo_copy_permissions=copy_permissions,
835 repo_copy_permissions=copy_permissions,
832 repo_enable_statistics=enable_statistics,
836 repo_enable_statistics=enable_statistics,
833 repo_enable_downloads=enable_downloads,
837 repo_enable_downloads=enable_downloads,
834 repo_enable_locking=enable_locking))
838 repo_enable_locking=enable_locking))
835 except validation_schema.Invalid as err:
839 except validation_schema.Invalid as err:
836 raise JSONRPCValidationError(colander_exc=err)
840 raise JSONRPCValidationError(colander_exc=err)
837
841
838 try:
842 try:
839 data = {
843 data = {
840 'owner': owner,
844 'owner': owner,
841 'repo_name': schema_data['repo_group']['repo_name_without_group'],
845 'repo_name': schema_data['repo_group']['repo_name_without_group'],
842 'repo_name_full': schema_data['repo_name'],
846 'repo_name_full': schema_data['repo_name'],
843 'repo_group': schema_data['repo_group']['repo_group_id'],
847 'repo_group': schema_data['repo_group']['repo_group_id'],
844 'repo_type': schema_data['repo_type'],
848 'repo_type': schema_data['repo_type'],
845 'repo_description': schema_data['repo_description'],
849 'repo_description': schema_data['repo_description'],
846 'repo_private': schema_data['repo_private'],
850 'repo_private': schema_data['repo_private'],
847 'clone_uri': schema_data['repo_clone_uri'],
851 'clone_uri': schema_data['repo_clone_uri'],
848 'push_uri': schema_data['repo_push_uri'],
852 'push_uri': schema_data['repo_push_uri'],
849 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
853 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
850 'enable_statistics': schema_data['repo_enable_statistics'],
854 'enable_statistics': schema_data['repo_enable_statistics'],
851 'enable_locking': schema_data['repo_enable_locking'],
855 'enable_locking': schema_data['repo_enable_locking'],
852 'enable_downloads': schema_data['repo_enable_downloads'],
856 'enable_downloads': schema_data['repo_enable_downloads'],
853 'repo_copy_permissions': schema_data['repo_copy_permissions'],
857 'repo_copy_permissions': schema_data['repo_copy_permissions'],
854 }
858 }
855
859
856 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
860 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
857 task_id = get_task_id(task)
861 task_id = get_task_id(task)
858 # no commit, it's done in RepoModel, or async via celery
862 # no commit, it's done in RepoModel, or async via celery
859 return {
863 return {
860 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
864 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
861 'success': True, # cannot return the repo data here since fork
865 'success': True, # cannot return the repo data here since fork
862 # can be done async
866 # can be done async
863 'task': task_id
867 'task': task_id
864 }
868 }
865 except Exception:
869 except Exception:
866 log.exception(
870 log.exception(
867 u"Exception while trying to create the repository %s",
871 u"Exception while trying to create the repository %s",
868 schema_data['repo_name'])
872 schema_data['repo_name'])
869 raise JSONRPCError(
873 raise JSONRPCError(
870 'failed to create repository `%s`' % (schema_data['repo_name'],))
874 'failed to create repository `%s`' % (schema_data['repo_name'],))
871
875
872
876
873 @jsonrpc_method()
877 @jsonrpc_method()
874 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
878 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
875 description=Optional('')):
879 description=Optional('')):
876 """
880 """
877 Adds an extra field to a repository.
881 Adds an extra field to a repository.
878
882
879 This command can only be run using an |authtoken| with at least
883 This command can only be run using an |authtoken| with at least
880 write permissions to the |repo|.
884 write permissions to the |repo|.
881
885
882 :param apiuser: This is filled automatically from the |authtoken|.
886 :param apiuser: This is filled automatically from the |authtoken|.
883 :type apiuser: AuthUser
887 :type apiuser: AuthUser
884 :param repoid: Set the repository name or repository id.
888 :param repoid: Set the repository name or repository id.
885 :type repoid: str or int
889 :type repoid: str or int
886 :param key: Create a unique field key for this repository.
890 :param key: Create a unique field key for this repository.
887 :type key: str
891 :type key: str
888 :param label:
892 :param label:
889 :type label: Optional(str)
893 :type label: Optional(str)
890 :param description:
894 :param description:
891 :type description: Optional(str)
895 :type description: Optional(str)
892 """
896 """
893 repo = get_repo_or_error(repoid)
897 repo = get_repo_or_error(repoid)
894 if not has_superadmin_permission(apiuser):
898 if not has_superadmin_permission(apiuser):
895 _perms = ('repository.admin',)
899 _perms = ('repository.admin',)
896 validate_repo_permissions(apiuser, repoid, repo, _perms)
900 validate_repo_permissions(apiuser, repoid, repo, _perms)
897
901
898 label = Optional.extract(label) or key
902 label = Optional.extract(label) or key
899 description = Optional.extract(description)
903 description = Optional.extract(description)
900
904
901 field = RepositoryField.get_by_key_name(key, repo)
905 field = RepositoryField.get_by_key_name(key, repo)
902 if field:
906 if field:
903 raise JSONRPCError('Field with key '
907 raise JSONRPCError('Field with key '
904 '`%s` exists for repo `%s`' % (key, repoid))
908 '`%s` exists for repo `%s`' % (key, repoid))
905
909
906 try:
910 try:
907 RepoModel().add_repo_field(repo, key, field_label=label,
911 RepoModel().add_repo_field(repo, key, field_label=label,
908 field_desc=description)
912 field_desc=description)
909 Session().commit()
913 Session().commit()
910 return {
914 return {
911 'msg': "Added new repository field `%s`" % (key,),
915 'msg': "Added new repository field `%s`" % (key,),
912 'success': True,
916 'success': True,
913 }
917 }
914 except Exception:
918 except Exception:
915 log.exception("Exception occurred while trying to add field to repo")
919 log.exception("Exception occurred while trying to add field to repo")
916 raise JSONRPCError(
920 raise JSONRPCError(
917 'failed to create new field for repository `%s`' % (repoid,))
921 'failed to create new field for repository `%s`' % (repoid,))
918
922
919
923
920 @jsonrpc_method()
924 @jsonrpc_method()
921 def remove_field_from_repo(request, apiuser, repoid, key):
925 def remove_field_from_repo(request, apiuser, repoid, key):
922 """
926 """
923 Removes an extra field from a repository.
927 Removes an extra field from a repository.
924
928
925 This command can only be run using an |authtoken| with at least
929 This command can only be run using an |authtoken| with at least
926 write permissions to the |repo|.
930 write permissions to the |repo|.
927
931
928 :param apiuser: This is filled automatically from the |authtoken|.
932 :param apiuser: This is filled automatically from the |authtoken|.
929 :type apiuser: AuthUser
933 :type apiuser: AuthUser
930 :param repoid: Set the repository name or repository ID.
934 :param repoid: Set the repository name or repository ID.
931 :type repoid: str or int
935 :type repoid: str or int
932 :param key: Set the unique field key for this repository.
936 :param key: Set the unique field key for this repository.
933 :type key: str
937 :type key: str
934 """
938 """
935
939
936 repo = get_repo_or_error(repoid)
940 repo = get_repo_or_error(repoid)
937 if not has_superadmin_permission(apiuser):
941 if not has_superadmin_permission(apiuser):
938 _perms = ('repository.admin',)
942 _perms = ('repository.admin',)
939 validate_repo_permissions(apiuser, repoid, repo, _perms)
943 validate_repo_permissions(apiuser, repoid, repo, _perms)
940
944
941 field = RepositoryField.get_by_key_name(key, repo)
945 field = RepositoryField.get_by_key_name(key, repo)
942 if not field:
946 if not field:
943 raise JSONRPCError('Field with key `%s` does not '
947 raise JSONRPCError('Field with key `%s` does not '
944 'exists for repo `%s`' % (key, repoid))
948 'exists for repo `%s`' % (key, repoid))
945
949
946 try:
950 try:
947 RepoModel().delete_repo_field(repo, field_key=key)
951 RepoModel().delete_repo_field(repo, field_key=key)
948 Session().commit()
952 Session().commit()
949 return {
953 return {
950 'msg': "Deleted repository field `%s`" % (key,),
954 'msg': "Deleted repository field `%s`" % (key,),
951 'success': True,
955 'success': True,
952 }
956 }
953 except Exception:
957 except Exception:
954 log.exception(
958 log.exception(
955 "Exception occurred while trying to delete field from repo")
959 "Exception occurred while trying to delete field from repo")
956 raise JSONRPCError(
960 raise JSONRPCError(
957 'failed to delete field for repository `%s`' % (repoid,))
961 'failed to delete field for repository `%s`' % (repoid,))
958
962
959
963
960 @jsonrpc_method()
964 @jsonrpc_method()
961 def update_repo(
965 def update_repo(
962 request, apiuser, repoid, repo_name=Optional(None),
966 request, apiuser, repoid, repo_name=Optional(None),
963 owner=Optional(OAttr('apiuser')), description=Optional(''),
967 owner=Optional(OAttr('apiuser')), description=Optional(''),
964 private=Optional(False),
968 private=Optional(False),
965 clone_uri=Optional(None), push_uri=Optional(None),
969 clone_uri=Optional(None), push_uri=Optional(None),
966 landing_rev=Optional(None), fork_of=Optional(None),
970 landing_rev=Optional(None), fork_of=Optional(None),
967 enable_statistics=Optional(False),
971 enable_statistics=Optional(False),
968 enable_locking=Optional(False),
972 enable_locking=Optional(False),
969 enable_downloads=Optional(False), fields=Optional('')):
973 enable_downloads=Optional(False), fields=Optional('')):
970 """
974 """
971 Updates a repository with the given information.
975 Updates a repository with the given information.
972
976
973 This command can only be run using an |authtoken| with at least
977 This command can only be run using an |authtoken| with at least
974 admin permissions to the |repo|.
978 admin permissions to the |repo|.
975
979
976 * If the repository name contains "/", repository will be updated
980 * If the repository name contains "/", repository will be updated
977 accordingly with a repository group or nested repository groups
981 accordingly with a repository group or nested repository groups
978
982
979 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
983 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
980 called "repo-test" and place it inside group "foo/bar".
984 called "repo-test" and place it inside group "foo/bar".
981 You have to have permissions to access and write to the last repository
985 You have to have permissions to access and write to the last repository
982 group ("bar" in this example)
986 group ("bar" in this example)
983
987
984 :param apiuser: This is filled automatically from the |authtoken|.
988 :param apiuser: This is filled automatically from the |authtoken|.
985 :type apiuser: AuthUser
989 :type apiuser: AuthUser
986 :param repoid: repository name or repository ID.
990 :param repoid: repository name or repository ID.
987 :type repoid: str or int
991 :type repoid: str or int
988 :param repo_name: Update the |repo| name, including the
992 :param repo_name: Update the |repo| name, including the
989 repository group it's in.
993 repository group it's in.
990 :type repo_name: str
994 :type repo_name: str
991 :param owner: Set the |repo| owner.
995 :param owner: Set the |repo| owner.
992 :type owner: str
996 :type owner: str
993 :param fork_of: Set the |repo| as fork of another |repo|.
997 :param fork_of: Set the |repo| as fork of another |repo|.
994 :type fork_of: str
998 :type fork_of: str
995 :param description: Update the |repo| description.
999 :param description: Update the |repo| description.
996 :type description: str
1000 :type description: str
997 :param private: Set the |repo| as private. (True | False)
1001 :param private: Set the |repo| as private. (True | False)
998 :type private: bool
1002 :type private: bool
999 :param clone_uri: Update the |repo| clone URI.
1003 :param clone_uri: Update the |repo| clone URI.
1000 :type clone_uri: str
1004 :type clone_uri: str
1001 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1005 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1002 :type landing_rev: str
1006 :type landing_rev: str
1003 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1007 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1004 :type enable_statistics: bool
1008 :type enable_statistics: bool
1005 :param enable_locking: Enable |repo| locking.
1009 :param enable_locking: Enable |repo| locking.
1006 :type enable_locking: bool
1010 :type enable_locking: bool
1007 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1011 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1008 :type enable_downloads: bool
1012 :type enable_downloads: bool
1009 :param fields: Add extra fields to the |repo|. Use the following
1013 :param fields: Add extra fields to the |repo|. Use the following
1010 example format: ``field_key=field_val,field_key2=fieldval2``.
1014 example format: ``field_key=field_val,field_key2=fieldval2``.
1011 Escape ', ' with \,
1015 Escape ', ' with \,
1012 :type fields: str
1016 :type fields: str
1013 """
1017 """
1014
1018
1015 repo = get_repo_or_error(repoid)
1019 repo = get_repo_or_error(repoid)
1016
1020
1017 include_secrets = False
1021 include_secrets = False
1018 if not has_superadmin_permission(apiuser):
1022 if not has_superadmin_permission(apiuser):
1019 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
1023 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
1020 else:
1024 else:
1021 include_secrets = True
1025 include_secrets = True
1022
1026
1023 updates = dict(
1027 updates = dict(
1024 repo_name=repo_name
1028 repo_name=repo_name
1025 if not isinstance(repo_name, Optional) else repo.repo_name,
1029 if not isinstance(repo_name, Optional) else repo.repo_name,
1026
1030
1027 fork_id=fork_of
1031 fork_id=fork_of
1028 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1032 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1029
1033
1030 user=owner
1034 user=owner
1031 if not isinstance(owner, Optional) else repo.user.username,
1035 if not isinstance(owner, Optional) else repo.user.username,
1032
1036
1033 repo_description=description
1037 repo_description=description
1034 if not isinstance(description, Optional) else repo.description,
1038 if not isinstance(description, Optional) else repo.description,
1035
1039
1036 repo_private=private
1040 repo_private=private
1037 if not isinstance(private, Optional) else repo.private,
1041 if not isinstance(private, Optional) else repo.private,
1038
1042
1039 clone_uri=clone_uri
1043 clone_uri=clone_uri
1040 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1044 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1041
1045
1042 push_uri=push_uri
1046 push_uri=push_uri
1043 if not isinstance(push_uri, Optional) else repo.push_uri,
1047 if not isinstance(push_uri, Optional) else repo.push_uri,
1044
1048
1045 repo_landing_rev=landing_rev
1049 repo_landing_rev=landing_rev
1046 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1050 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1047
1051
1048 repo_enable_statistics=enable_statistics
1052 repo_enable_statistics=enable_statistics
1049 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1053 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1050
1054
1051 repo_enable_locking=enable_locking
1055 repo_enable_locking=enable_locking
1052 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1056 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1053
1057
1054 repo_enable_downloads=enable_downloads
1058 repo_enable_downloads=enable_downloads
1055 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1059 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1056
1060
1057 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1061 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1058 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1062 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1059 request.translate, repo=repo)
1063 request.translate, repo=repo)
1060 ref_choices = list(set(ref_choices + [landing_ref]))
1064 ref_choices = list(set(ref_choices + [landing_ref]))
1061
1065
1062 old_values = repo.get_api_data()
1066 old_values = repo.get_api_data()
1063 repo_type = repo.repo_type
1067 repo_type = repo.repo_type
1064 schema = repo_schema.RepoSchema().bind(
1068 schema = repo_schema.RepoSchema().bind(
1065 repo_type_options=rhodecode.BACKENDS.keys(),
1069 repo_type_options=rhodecode.BACKENDS.keys(),
1066 repo_ref_options=ref_choices,
1070 repo_ref_options=ref_choices,
1067 repo_type=repo_type,
1071 repo_type=repo_type,
1068 # user caller
1072 # user caller
1069 user=apiuser,
1073 user=apiuser,
1070 old_values=old_values)
1074 old_values=old_values)
1071 try:
1075 try:
1072 schema_data = schema.deserialize(dict(
1076 schema_data = schema.deserialize(dict(
1073 # we save old value, users cannot change type
1077 # we save old value, users cannot change type
1074 repo_type=repo_type,
1078 repo_type=repo_type,
1075
1079
1076 repo_name=updates['repo_name'],
1080 repo_name=updates['repo_name'],
1077 repo_owner=updates['user'],
1081 repo_owner=updates['user'],
1078 repo_description=updates['repo_description'],
1082 repo_description=updates['repo_description'],
1079 repo_clone_uri=updates['clone_uri'],
1083 repo_clone_uri=updates['clone_uri'],
1080 repo_push_uri=updates['push_uri'],
1084 repo_push_uri=updates['push_uri'],
1081 repo_fork_of=updates['fork_id'],
1085 repo_fork_of=updates['fork_id'],
1082 repo_private=updates['repo_private'],
1086 repo_private=updates['repo_private'],
1083 repo_landing_commit_ref=updates['repo_landing_rev'],
1087 repo_landing_commit_ref=updates['repo_landing_rev'],
1084 repo_enable_statistics=updates['repo_enable_statistics'],
1088 repo_enable_statistics=updates['repo_enable_statistics'],
1085 repo_enable_downloads=updates['repo_enable_downloads'],
1089 repo_enable_downloads=updates['repo_enable_downloads'],
1086 repo_enable_locking=updates['repo_enable_locking']))
1090 repo_enable_locking=updates['repo_enable_locking']))
1087 except validation_schema.Invalid as err:
1091 except validation_schema.Invalid as err:
1088 raise JSONRPCValidationError(colander_exc=err)
1092 raise JSONRPCValidationError(colander_exc=err)
1089
1093
1090 # save validated data back into the updates dict
1094 # save validated data back into the updates dict
1091 validated_updates = dict(
1095 validated_updates = dict(
1092 repo_name=schema_data['repo_group']['repo_name_without_group'],
1096 repo_name=schema_data['repo_group']['repo_name_without_group'],
1093 repo_group=schema_data['repo_group']['repo_group_id'],
1097 repo_group=schema_data['repo_group']['repo_group_id'],
1094
1098
1095 user=schema_data['repo_owner'],
1099 user=schema_data['repo_owner'],
1096 repo_description=schema_data['repo_description'],
1100 repo_description=schema_data['repo_description'],
1097 repo_private=schema_data['repo_private'],
1101 repo_private=schema_data['repo_private'],
1098 clone_uri=schema_data['repo_clone_uri'],
1102 clone_uri=schema_data['repo_clone_uri'],
1099 push_uri=schema_data['repo_push_uri'],
1103 push_uri=schema_data['repo_push_uri'],
1100 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1104 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1101 repo_enable_statistics=schema_data['repo_enable_statistics'],
1105 repo_enable_statistics=schema_data['repo_enable_statistics'],
1102 repo_enable_locking=schema_data['repo_enable_locking'],
1106 repo_enable_locking=schema_data['repo_enable_locking'],
1103 repo_enable_downloads=schema_data['repo_enable_downloads'],
1107 repo_enable_downloads=schema_data['repo_enable_downloads'],
1104 )
1108 )
1105
1109
1106 if schema_data['repo_fork_of']:
1110 if schema_data['repo_fork_of']:
1107 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1111 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1108 validated_updates['fork_id'] = fork_repo.repo_id
1112 validated_updates['fork_id'] = fork_repo.repo_id
1109
1113
1110 # extra fields
1114 # extra fields
1111 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1115 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1112 if fields:
1116 if fields:
1113 validated_updates.update(fields)
1117 validated_updates.update(fields)
1114
1118
1115 try:
1119 try:
1116 RepoModel().update(repo, **validated_updates)
1120 RepoModel().update(repo, **validated_updates)
1117 audit_logger.store_api(
1121 audit_logger.store_api(
1118 'repo.edit', action_data={'old_data': old_values},
1122 'repo.edit', action_data={'old_data': old_values},
1119 user=apiuser, repo=repo)
1123 user=apiuser, repo=repo)
1120 Session().commit()
1124 Session().commit()
1121 return {
1125 return {
1122 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1126 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1123 'repository': repo.get_api_data(include_secrets=include_secrets)
1127 'repository': repo.get_api_data(include_secrets=include_secrets)
1124 }
1128 }
1125 except Exception:
1129 except Exception:
1126 log.exception(
1130 log.exception(
1127 u"Exception while trying to update the repository %s",
1131 u"Exception while trying to update the repository %s",
1128 repoid)
1132 repoid)
1129 raise JSONRPCError('failed to update repo `%s`' % repoid)
1133 raise JSONRPCError('failed to update repo `%s`' % repoid)
1130
1134
1131
1135
1132 @jsonrpc_method()
1136 @jsonrpc_method()
1133 def fork_repo(request, apiuser, repoid, fork_name,
1137 def fork_repo(request, apiuser, repoid, fork_name,
1134 owner=Optional(OAttr('apiuser')),
1138 owner=Optional(OAttr('apiuser')),
1135 description=Optional(''),
1139 description=Optional(''),
1136 private=Optional(False),
1140 private=Optional(False),
1137 clone_uri=Optional(None),
1141 clone_uri=Optional(None),
1138 landing_rev=Optional(None),
1142 landing_rev=Optional(None),
1139 copy_permissions=Optional(False)):
1143 copy_permissions=Optional(False)):
1140 """
1144 """
1141 Creates a fork of the specified |repo|.
1145 Creates a fork of the specified |repo|.
1142
1146
1143 * If the fork_name contains "/", fork will be created inside
1147 * If the fork_name contains "/", fork will be created inside
1144 a repository group or nested repository groups
1148 a repository group or nested repository groups
1145
1149
1146 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1150 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1147 inside group "foo/bar". You have to have permissions to access and
1151 inside group "foo/bar". You have to have permissions to access and
1148 write to the last repository group ("bar" in this example)
1152 write to the last repository group ("bar" in this example)
1149
1153
1150 This command can only be run using an |authtoken| with minimum
1154 This command can only be run using an |authtoken| with minimum
1151 read permissions of the forked repo, create fork permissions for an user.
1155 read permissions of the forked repo, create fork permissions for an user.
1152
1156
1153 :param apiuser: This is filled automatically from the |authtoken|.
1157 :param apiuser: This is filled automatically from the |authtoken|.
1154 :type apiuser: AuthUser
1158 :type apiuser: AuthUser
1155 :param repoid: Set repository name or repository ID.
1159 :param repoid: Set repository name or repository ID.
1156 :type repoid: str or int
1160 :type repoid: str or int
1157 :param fork_name: Set the fork name, including it's repository group membership.
1161 :param fork_name: Set the fork name, including it's repository group membership.
1158 :type fork_name: str
1162 :type fork_name: str
1159 :param owner: Set the fork owner.
1163 :param owner: Set the fork owner.
1160 :type owner: str
1164 :type owner: str
1161 :param description: Set the fork description.
1165 :param description: Set the fork description.
1162 :type description: str
1166 :type description: str
1163 :param copy_permissions: Copy permissions from parent |repo|. The
1167 :param copy_permissions: Copy permissions from parent |repo|. The
1164 default is False.
1168 default is False.
1165 :type copy_permissions: bool
1169 :type copy_permissions: bool
1166 :param private: Make the fork private. The default is False.
1170 :param private: Make the fork private. The default is False.
1167 :type private: bool
1171 :type private: bool
1168 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1172 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1169
1173
1170 Example output:
1174 Example output:
1171
1175
1172 .. code-block:: bash
1176 .. code-block:: bash
1173
1177
1174 id : <id_for_response>
1178 id : <id_for_response>
1175 api_key : "<api_key>"
1179 api_key : "<api_key>"
1176 args: {
1180 args: {
1177 "repoid" : "<reponame or repo_id>",
1181 "repoid" : "<reponame or repo_id>",
1178 "fork_name": "<forkname>",
1182 "fork_name": "<forkname>",
1179 "owner": "<username or user_id = Optional(=apiuser)>",
1183 "owner": "<username or user_id = Optional(=apiuser)>",
1180 "description": "<description>",
1184 "description": "<description>",
1181 "copy_permissions": "<bool>",
1185 "copy_permissions": "<bool>",
1182 "private": "<bool>",
1186 "private": "<bool>",
1183 "landing_rev": "<landing_rev>"
1187 "landing_rev": "<landing_rev>"
1184 }
1188 }
1185
1189
1186 Example error output:
1190 Example error output:
1187
1191
1188 .. code-block:: bash
1192 .. code-block:: bash
1189
1193
1190 id : <id_given_in_input>
1194 id : <id_given_in_input>
1191 result: {
1195 result: {
1192 "msg": "Created fork of `<reponame>` as `<forkname>`",
1196 "msg": "Created fork of `<reponame>` as `<forkname>`",
1193 "success": true,
1197 "success": true,
1194 "task": "<celery task id or None if done sync>"
1198 "task": "<celery task id or None if done sync>"
1195 }
1199 }
1196 error: null
1200 error: null
1197
1201
1198 """
1202 """
1199
1203
1200 repo = get_repo_or_error(repoid)
1204 repo = get_repo_or_error(repoid)
1201 repo_name = repo.repo_name
1205 repo_name = repo.repo_name
1202
1206
1203 if not has_superadmin_permission(apiuser):
1207 if not has_superadmin_permission(apiuser):
1204 # check if we have at least read permission for
1208 # check if we have at least read permission for
1205 # this repo that we fork !
1209 # this repo that we fork !
1206 _perms = (
1210 _perms = (
1207 'repository.admin', 'repository.write', 'repository.read')
1211 'repository.admin', 'repository.write', 'repository.read')
1208 validate_repo_permissions(apiuser, repoid, repo, _perms)
1212 validate_repo_permissions(apiuser, repoid, repo, _perms)
1209
1213
1210 # check if the regular user has at least fork permissions as well
1214 # check if the regular user has at least fork permissions as well
1211 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1215 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1212 raise JSONRPCForbidden()
1216 raise JSONRPCForbidden()
1213
1217
1214 # check if user can set owner parameter
1218 # check if user can set owner parameter
1215 owner = validate_set_owner_permissions(apiuser, owner)
1219 owner = validate_set_owner_permissions(apiuser, owner)
1216
1220
1217 description = Optional.extract(description)
1221 description = Optional.extract(description)
1218 copy_permissions = Optional.extract(copy_permissions)
1222 copy_permissions = Optional.extract(copy_permissions)
1219 clone_uri = Optional.extract(clone_uri)
1223 clone_uri = Optional.extract(clone_uri)
1220
1224
1221 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1225 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1222 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1226 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1223 ref_choices = list(set(ref_choices + [landing_ref]))
1227 ref_choices = list(set(ref_choices + [landing_ref]))
1224 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1228 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1225
1229
1226 private = Optional.extract(private)
1230 private = Optional.extract(private)
1227
1231
1228 schema = repo_schema.RepoSchema().bind(
1232 schema = repo_schema.RepoSchema().bind(
1229 repo_type_options=rhodecode.BACKENDS.keys(),
1233 repo_type_options=rhodecode.BACKENDS.keys(),
1230 repo_ref_options=ref_choices,
1234 repo_ref_options=ref_choices,
1231 repo_type=repo.repo_type,
1235 repo_type=repo.repo_type,
1232 # user caller
1236 # user caller
1233 user=apiuser)
1237 user=apiuser)
1234
1238
1235 try:
1239 try:
1236 schema_data = schema.deserialize(dict(
1240 schema_data = schema.deserialize(dict(
1237 repo_name=fork_name,
1241 repo_name=fork_name,
1238 repo_type=repo.repo_type,
1242 repo_type=repo.repo_type,
1239 repo_owner=owner.username,
1243 repo_owner=owner.username,
1240 repo_description=description,
1244 repo_description=description,
1241 repo_landing_commit_ref=landing_commit_ref,
1245 repo_landing_commit_ref=landing_commit_ref,
1242 repo_clone_uri=clone_uri,
1246 repo_clone_uri=clone_uri,
1243 repo_private=private,
1247 repo_private=private,
1244 repo_copy_permissions=copy_permissions))
1248 repo_copy_permissions=copy_permissions))
1245 except validation_schema.Invalid as err:
1249 except validation_schema.Invalid as err:
1246 raise JSONRPCValidationError(colander_exc=err)
1250 raise JSONRPCValidationError(colander_exc=err)
1247
1251
1248 try:
1252 try:
1249 data = {
1253 data = {
1250 'fork_parent_id': repo.repo_id,
1254 'fork_parent_id': repo.repo_id,
1251
1255
1252 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1256 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1253 'repo_name_full': schema_data['repo_name'],
1257 'repo_name_full': schema_data['repo_name'],
1254 'repo_group': schema_data['repo_group']['repo_group_id'],
1258 'repo_group': schema_data['repo_group']['repo_group_id'],
1255 'repo_type': schema_data['repo_type'],
1259 'repo_type': schema_data['repo_type'],
1256 'description': schema_data['repo_description'],
1260 'description': schema_data['repo_description'],
1257 'private': schema_data['repo_private'],
1261 'private': schema_data['repo_private'],
1258 'copy_permissions': schema_data['repo_copy_permissions'],
1262 'copy_permissions': schema_data['repo_copy_permissions'],
1259 'landing_rev': schema_data['repo_landing_commit_ref'],
1263 'landing_rev': schema_data['repo_landing_commit_ref'],
1260 }
1264 }
1261
1265
1262 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1266 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1263 # no commit, it's done in RepoModel, or async via celery
1267 # no commit, it's done in RepoModel, or async via celery
1264 task_id = get_task_id(task)
1268 task_id = get_task_id(task)
1265
1269
1266 return {
1270 return {
1267 'msg': 'Created fork of `%s` as `%s`' % (
1271 'msg': 'Created fork of `%s` as `%s`' % (
1268 repo.repo_name, schema_data['repo_name']),
1272 repo.repo_name, schema_data['repo_name']),
1269 'success': True, # cannot return the repo data here since fork
1273 'success': True, # cannot return the repo data here since fork
1270 # can be done async
1274 # can be done async
1271 'task': task_id
1275 'task': task_id
1272 }
1276 }
1273 except Exception:
1277 except Exception:
1274 log.exception(
1278 log.exception(
1275 u"Exception while trying to create fork %s",
1279 u"Exception while trying to create fork %s",
1276 schema_data['repo_name'])
1280 schema_data['repo_name'])
1277 raise JSONRPCError(
1281 raise JSONRPCError(
1278 'failed to fork repository `%s` as `%s`' % (
1282 'failed to fork repository `%s` as `%s`' % (
1279 repo_name, schema_data['repo_name']))
1283 repo_name, schema_data['repo_name']))
1280
1284
1281
1285
1282 @jsonrpc_method()
1286 @jsonrpc_method()
1283 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1287 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1284 """
1288 """
1285 Deletes a repository.
1289 Deletes a repository.
1286
1290
1287 * When the `forks` parameter is set it's possible to detach or delete
1291 * When the `forks` parameter is set it's possible to detach or delete
1288 forks of deleted repository.
1292 forks of deleted repository.
1289
1293
1290 This command can only be run using an |authtoken| with admin
1294 This command can only be run using an |authtoken| with admin
1291 permissions on the |repo|.
1295 permissions on the |repo|.
1292
1296
1293 :param apiuser: This is filled automatically from the |authtoken|.
1297 :param apiuser: This is filled automatically from the |authtoken|.
1294 :type apiuser: AuthUser
1298 :type apiuser: AuthUser
1295 :param repoid: Set the repository name or repository ID.
1299 :param repoid: Set the repository name or repository ID.
1296 :type repoid: str or int
1300 :type repoid: str or int
1297 :param forks: Set to `detach` or `delete` forks from the |repo|.
1301 :param forks: Set to `detach` or `delete` forks from the |repo|.
1298 :type forks: Optional(str)
1302 :type forks: Optional(str)
1299
1303
1300 Example error output:
1304 Example error output:
1301
1305
1302 .. code-block:: bash
1306 .. code-block:: bash
1303
1307
1304 id : <id_given_in_input>
1308 id : <id_given_in_input>
1305 result: {
1309 result: {
1306 "msg": "Deleted repository `<reponame>`",
1310 "msg": "Deleted repository `<reponame>`",
1307 "success": true
1311 "success": true
1308 }
1312 }
1309 error: null
1313 error: null
1310 """
1314 """
1311
1315
1312 repo = get_repo_or_error(repoid)
1316 repo = get_repo_or_error(repoid)
1313 repo_name = repo.repo_name
1317 repo_name = repo.repo_name
1314 if not has_superadmin_permission(apiuser):
1318 if not has_superadmin_permission(apiuser):
1315 _perms = ('repository.admin',)
1319 _perms = ('repository.admin',)
1316 validate_repo_permissions(apiuser, repoid, repo, _perms)
1320 validate_repo_permissions(apiuser, repoid, repo, _perms)
1317
1321
1318 try:
1322 try:
1319 handle_forks = Optional.extract(forks)
1323 handle_forks = Optional.extract(forks)
1320 _forks_msg = ''
1324 _forks_msg = ''
1321 _forks = [f for f in repo.forks]
1325 _forks = [f for f in repo.forks]
1322 if handle_forks == 'detach':
1326 if handle_forks == 'detach':
1323 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1327 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1324 elif handle_forks == 'delete':
1328 elif handle_forks == 'delete':
1325 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1329 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1326 elif _forks:
1330 elif _forks:
1327 raise JSONRPCError(
1331 raise JSONRPCError(
1328 'Cannot delete `%s` it still contains attached forks' %
1332 'Cannot delete `%s` it still contains attached forks' %
1329 (repo.repo_name,)
1333 (repo.repo_name,)
1330 )
1334 )
1331 old_data = repo.get_api_data()
1335 old_data = repo.get_api_data()
1332 RepoModel().delete(repo, forks=forks)
1336 RepoModel().delete(repo, forks=forks)
1333
1337
1334 repo = audit_logger.RepoWrap(repo_id=None,
1338 repo = audit_logger.RepoWrap(repo_id=None,
1335 repo_name=repo.repo_name)
1339 repo_name=repo.repo_name)
1336
1340
1337 audit_logger.store_api(
1341 audit_logger.store_api(
1338 'repo.delete', action_data={'old_data': old_data},
1342 'repo.delete', action_data={'old_data': old_data},
1339 user=apiuser, repo=repo)
1343 user=apiuser, repo=repo)
1340
1344
1341 ScmModel().mark_for_invalidation(repo_name, delete=True)
1345 ScmModel().mark_for_invalidation(repo_name, delete=True)
1342 Session().commit()
1346 Session().commit()
1343 return {
1347 return {
1344 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1348 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1345 'success': True
1349 'success': True
1346 }
1350 }
1347 except Exception:
1351 except Exception:
1348 log.exception("Exception occurred while trying to delete repo")
1352 log.exception("Exception occurred while trying to delete repo")
1349 raise JSONRPCError(
1353 raise JSONRPCError(
1350 'failed to delete repository `%s`' % (repo_name,)
1354 'failed to delete repository `%s`' % (repo_name,)
1351 )
1355 )
1352
1356
1353
1357
1354 #TODO: marcink, change name ?
1358 #TODO: marcink, change name ?
1355 @jsonrpc_method()
1359 @jsonrpc_method()
1356 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1360 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1357 """
1361 """
1358 Invalidates the cache for the specified repository.
1362 Invalidates the cache for the specified repository.
1359
1363
1360 This command can only be run using an |authtoken| with admin rights to
1364 This command can only be run using an |authtoken| with admin rights to
1361 the specified repository.
1365 the specified repository.
1362
1366
1363 This command takes the following options:
1367 This command takes the following options:
1364
1368
1365 :param apiuser: This is filled automatically from |authtoken|.
1369 :param apiuser: This is filled automatically from |authtoken|.
1366 :type apiuser: AuthUser
1370 :type apiuser: AuthUser
1367 :param repoid: Sets the repository name or repository ID.
1371 :param repoid: Sets the repository name or repository ID.
1368 :type repoid: str or int
1372 :type repoid: str or int
1369 :param delete_keys: This deletes the invalidated keys instead of
1373 :param delete_keys: This deletes the invalidated keys instead of
1370 just flagging them.
1374 just flagging them.
1371 :type delete_keys: Optional(``True`` | ``False``)
1375 :type delete_keys: Optional(``True`` | ``False``)
1372
1376
1373 Example output:
1377 Example output:
1374
1378
1375 .. code-block:: bash
1379 .. code-block:: bash
1376
1380
1377 id : <id_given_in_input>
1381 id : <id_given_in_input>
1378 result : {
1382 result : {
1379 'msg': Cache for repository `<repository name>` was invalidated,
1383 'msg': Cache for repository `<repository name>` was invalidated,
1380 'repository': <repository name>
1384 'repository': <repository name>
1381 }
1385 }
1382 error : null
1386 error : null
1383
1387
1384 Example error output:
1388 Example error output:
1385
1389
1386 .. code-block:: bash
1390 .. code-block:: bash
1387
1391
1388 id : <id_given_in_input>
1392 id : <id_given_in_input>
1389 result : null
1393 result : null
1390 error : {
1394 error : {
1391 'Error occurred during cache invalidation action'
1395 'Error occurred during cache invalidation action'
1392 }
1396 }
1393
1397
1394 """
1398 """
1395
1399
1396 repo = get_repo_or_error(repoid)
1400 repo = get_repo_or_error(repoid)
1397 if not has_superadmin_permission(apiuser):
1401 if not has_superadmin_permission(apiuser):
1398 _perms = ('repository.admin', 'repository.write',)
1402 _perms = ('repository.admin', 'repository.write',)
1399 validate_repo_permissions(apiuser, repoid, repo, _perms)
1403 validate_repo_permissions(apiuser, repoid, repo, _perms)
1400
1404
1401 delete = Optional.extract(delete_keys)
1405 delete = Optional.extract(delete_keys)
1402 try:
1406 try:
1403 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1407 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1404 return {
1408 return {
1405 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1409 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1406 'repository': repo.repo_name
1410 'repository': repo.repo_name
1407 }
1411 }
1408 except Exception:
1412 except Exception:
1409 log.exception(
1413 log.exception(
1410 "Exception occurred while trying to invalidate repo cache")
1414 "Exception occurred while trying to invalidate repo cache")
1411 raise JSONRPCError(
1415 raise JSONRPCError(
1412 'Error occurred during cache invalidation action'
1416 'Error occurred during cache invalidation action'
1413 )
1417 )
1414
1418
1415
1419
1416 #TODO: marcink, change name ?
1420 #TODO: marcink, change name ?
1417 @jsonrpc_method()
1421 @jsonrpc_method()
1418 def lock(request, apiuser, repoid, locked=Optional(None),
1422 def lock(request, apiuser, repoid, locked=Optional(None),
1419 userid=Optional(OAttr('apiuser'))):
1423 userid=Optional(OAttr('apiuser'))):
1420 """
1424 """
1421 Sets the lock state of the specified |repo| by the given user.
1425 Sets the lock state of the specified |repo| by the given user.
1422 From more information, see :ref:`repo-locking`.
1426 From more information, see :ref:`repo-locking`.
1423
1427
1424 * If the ``userid`` option is not set, the repository is locked to the
1428 * If the ``userid`` option is not set, the repository is locked to the
1425 user who called the method.
1429 user who called the method.
1426 * If the ``locked`` parameter is not set, the current lock state of the
1430 * If the ``locked`` parameter is not set, the current lock state of the
1427 repository is displayed.
1431 repository is displayed.
1428
1432
1429 This command can only be run using an |authtoken| with admin rights to
1433 This command can only be run using an |authtoken| with admin rights to
1430 the specified repository.
1434 the specified repository.
1431
1435
1432 This command takes the following options:
1436 This command takes the following options:
1433
1437
1434 :param apiuser: This is filled automatically from the |authtoken|.
1438 :param apiuser: This is filled automatically from the |authtoken|.
1435 :type apiuser: AuthUser
1439 :type apiuser: AuthUser
1436 :param repoid: Sets the repository name or repository ID.
1440 :param repoid: Sets the repository name or repository ID.
1437 :type repoid: str or int
1441 :type repoid: str or int
1438 :param locked: Sets the lock state.
1442 :param locked: Sets the lock state.
1439 :type locked: Optional(``True`` | ``False``)
1443 :type locked: Optional(``True`` | ``False``)
1440 :param userid: Set the repository lock to this user.
1444 :param userid: Set the repository lock to this user.
1441 :type userid: Optional(str or int)
1445 :type userid: Optional(str or int)
1442
1446
1443 Example error output:
1447 Example error output:
1444
1448
1445 .. code-block:: bash
1449 .. code-block:: bash
1446
1450
1447 id : <id_given_in_input>
1451 id : <id_given_in_input>
1448 result : {
1452 result : {
1449 'repo': '<reponame>',
1453 'repo': '<reponame>',
1450 'locked': <bool: lock state>,
1454 'locked': <bool: lock state>,
1451 'locked_since': <int: lock timestamp>,
1455 'locked_since': <int: lock timestamp>,
1452 'locked_by': <username of person who made the lock>,
1456 'locked_by': <username of person who made the lock>,
1453 'lock_reason': <str: reason for locking>,
1457 'lock_reason': <str: reason for locking>,
1454 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1458 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1455 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1459 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1456 or
1460 or
1457 'msg': 'Repo `<repository name>` not locked.'
1461 'msg': 'Repo `<repository name>` not locked.'
1458 or
1462 or
1459 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1463 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1460 }
1464 }
1461 error : null
1465 error : null
1462
1466
1463 Example error output:
1467 Example error output:
1464
1468
1465 .. code-block:: bash
1469 .. code-block:: bash
1466
1470
1467 id : <id_given_in_input>
1471 id : <id_given_in_input>
1468 result : null
1472 result : null
1469 error : {
1473 error : {
1470 'Error occurred locking repository `<reponame>`'
1474 'Error occurred locking repository `<reponame>`'
1471 }
1475 }
1472 """
1476 """
1473
1477
1474 repo = get_repo_or_error(repoid)
1478 repo = get_repo_or_error(repoid)
1475 if not has_superadmin_permission(apiuser):
1479 if not has_superadmin_permission(apiuser):
1476 # check if we have at least write permission for this repo !
1480 # check if we have at least write permission for this repo !
1477 _perms = ('repository.admin', 'repository.write',)
1481 _perms = ('repository.admin', 'repository.write',)
1478 validate_repo_permissions(apiuser, repoid, repo, _perms)
1482 validate_repo_permissions(apiuser, repoid, repo, _perms)
1479
1483
1480 # make sure normal user does not pass someone else userid,
1484 # make sure normal user does not pass someone else userid,
1481 # he is not allowed to do that
1485 # he is not allowed to do that
1482 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1486 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1483 raise JSONRPCError('userid is not the same as your user')
1487 raise JSONRPCError('userid is not the same as your user')
1484
1488
1485 if isinstance(userid, Optional):
1489 if isinstance(userid, Optional):
1486 userid = apiuser.user_id
1490 userid = apiuser.user_id
1487
1491
1488 user = get_user_or_error(userid)
1492 user = get_user_or_error(userid)
1489
1493
1490 if isinstance(locked, Optional):
1494 if isinstance(locked, Optional):
1491 lockobj = repo.locked
1495 lockobj = repo.locked
1492
1496
1493 if lockobj[0] is None:
1497 if lockobj[0] is None:
1494 _d = {
1498 _d = {
1495 'repo': repo.repo_name,
1499 'repo': repo.repo_name,
1496 'locked': False,
1500 'locked': False,
1497 'locked_since': None,
1501 'locked_since': None,
1498 'locked_by': None,
1502 'locked_by': None,
1499 'lock_reason': None,
1503 'lock_reason': None,
1500 'lock_state_changed': False,
1504 'lock_state_changed': False,
1501 'msg': 'Repo `%s` not locked.' % repo.repo_name
1505 'msg': 'Repo `%s` not locked.' % repo.repo_name
1502 }
1506 }
1503 return _d
1507 return _d
1504 else:
1508 else:
1505 _user_id, _time, _reason = lockobj
1509 _user_id, _time, _reason = lockobj
1506 lock_user = get_user_or_error(userid)
1510 lock_user = get_user_or_error(userid)
1507 _d = {
1511 _d = {
1508 'repo': repo.repo_name,
1512 'repo': repo.repo_name,
1509 'locked': True,
1513 'locked': True,
1510 'locked_since': _time,
1514 'locked_since': _time,
1511 'locked_by': lock_user.username,
1515 'locked_by': lock_user.username,
1512 'lock_reason': _reason,
1516 'lock_reason': _reason,
1513 'lock_state_changed': False,
1517 'lock_state_changed': False,
1514 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1518 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1515 % (repo.repo_name, lock_user.username,
1519 % (repo.repo_name, lock_user.username,
1516 json.dumps(time_to_datetime(_time))))
1520 json.dumps(time_to_datetime(_time))))
1517 }
1521 }
1518 return _d
1522 return _d
1519
1523
1520 # force locked state through a flag
1524 # force locked state through a flag
1521 else:
1525 else:
1522 locked = str2bool(locked)
1526 locked = str2bool(locked)
1523 lock_reason = Repository.LOCK_API
1527 lock_reason = Repository.LOCK_API
1524 try:
1528 try:
1525 if locked:
1529 if locked:
1526 lock_time = time.time()
1530 lock_time = time.time()
1527 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1531 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1528 else:
1532 else:
1529 lock_time = None
1533 lock_time = None
1530 Repository.unlock(repo)
1534 Repository.unlock(repo)
1531 _d = {
1535 _d = {
1532 'repo': repo.repo_name,
1536 'repo': repo.repo_name,
1533 'locked': locked,
1537 'locked': locked,
1534 'locked_since': lock_time,
1538 'locked_since': lock_time,
1535 'locked_by': user.username,
1539 'locked_by': user.username,
1536 'lock_reason': lock_reason,
1540 'lock_reason': lock_reason,
1537 'lock_state_changed': True,
1541 'lock_state_changed': True,
1538 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1542 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1539 % (user.username, repo.repo_name, locked))
1543 % (user.username, repo.repo_name, locked))
1540 }
1544 }
1541 return _d
1545 return _d
1542 except Exception:
1546 except Exception:
1543 log.exception(
1547 log.exception(
1544 "Exception occurred while trying to lock repository")
1548 "Exception occurred while trying to lock repository")
1545 raise JSONRPCError(
1549 raise JSONRPCError(
1546 'Error occurred locking repository `%s`' % repo.repo_name
1550 'Error occurred locking repository `%s`' % repo.repo_name
1547 )
1551 )
1548
1552
1549
1553
1550 @jsonrpc_method()
1554 @jsonrpc_method()
1551 def comment_commit(
1555 def comment_commit(
1552 request, apiuser, repoid, commit_id, message, status=Optional(None),
1556 request, apiuser, repoid, commit_id, message, status=Optional(None),
1553 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1557 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1554 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1558 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1555 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1559 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1556 """
1560 """
1557 Set a commit comment, and optionally change the status of the commit.
1561 Set a commit comment, and optionally change the status of the commit.
1558
1562
1559 :param apiuser: This is filled automatically from the |authtoken|.
1563 :param apiuser: This is filled automatically from the |authtoken|.
1560 :type apiuser: AuthUser
1564 :type apiuser: AuthUser
1561 :param repoid: Set the repository name or repository ID.
1565 :param repoid: Set the repository name or repository ID.
1562 :type repoid: str or int
1566 :type repoid: str or int
1563 :param commit_id: Specify the commit_id for which to set a comment.
1567 :param commit_id: Specify the commit_id for which to set a comment.
1564 :type commit_id: str
1568 :type commit_id: str
1565 :param message: The comment text.
1569 :param message: The comment text.
1566 :type message: str
1570 :type message: str
1567 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1571 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1568 'approved', 'rejected', 'under_review'
1572 'approved', 'rejected', 'under_review'
1569 :type status: str
1573 :type status: str
1570 :param comment_type: Comment type, one of: 'note', 'todo'
1574 :param comment_type: Comment type, one of: 'note', 'todo'
1571 :type comment_type: Optional(str), default: 'note'
1575 :type comment_type: Optional(str), default: 'note'
1572 :param resolves_comment_id: id of comment which this one will resolve
1576 :param resolves_comment_id: id of comment which this one will resolve
1573 :type resolves_comment_id: Optional(int)
1577 :type resolves_comment_id: Optional(int)
1574 :param extra_recipients: list of user ids or usernames to add
1578 :param extra_recipients: list of user ids or usernames to add
1575 notifications for this comment. Acts like a CC for notification
1579 notifications for this comment. Acts like a CC for notification
1576 :type extra_recipients: Optional(list)
1580 :type extra_recipients: Optional(list)
1577 :param userid: Set the user name of the comment creator.
1581 :param userid: Set the user name of the comment creator.
1578 :type userid: Optional(str or int)
1582 :type userid: Optional(str or int)
1579 :param send_email: Define if this comment should also send email notification
1583 :param send_email: Define if this comment should also send email notification
1580 :type send_email: Optional(bool)
1584 :type send_email: Optional(bool)
1581
1585
1582 Example error output:
1586 Example error output:
1583
1587
1584 .. code-block:: bash
1588 .. code-block:: bash
1585
1589
1586 {
1590 {
1587 "id" : <id_given_in_input>,
1591 "id" : <id_given_in_input>,
1588 "result" : {
1592 "result" : {
1589 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1593 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1590 "status_change": null or <status>,
1594 "status_change": null or <status>,
1591 "success": true
1595 "success": true
1592 },
1596 },
1593 "error" : null
1597 "error" : null
1594 }
1598 }
1595
1599
1596 """
1600 """
1597 repo = get_repo_or_error(repoid)
1601 repo = get_repo_or_error(repoid)
1598 if not has_superadmin_permission(apiuser):
1602 if not has_superadmin_permission(apiuser):
1599 _perms = ('repository.read', 'repository.write', 'repository.admin')
1603 _perms = ('repository.read', 'repository.write', 'repository.admin')
1600 validate_repo_permissions(apiuser, repoid, repo, _perms)
1604 validate_repo_permissions(apiuser, repoid, repo, _perms)
1601
1605
1602 try:
1606 try:
1603 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1607 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1604 commit_id = commit.raw_id
1608 commit_id = commit.raw_id
1605 except Exception as e:
1609 except Exception as e:
1606 log.exception('Failed to fetch commit')
1610 log.exception('Failed to fetch commit')
1607 raise JSONRPCError(safe_str(e))
1611 raise JSONRPCError(safe_str(e))
1608
1612
1609 if isinstance(userid, Optional):
1613 if isinstance(userid, Optional):
1610 userid = apiuser.user_id
1614 userid = apiuser.user_id
1611
1615
1612 user = get_user_or_error(userid)
1616 user = get_user_or_error(userid)
1613 status = Optional.extract(status)
1617 status = Optional.extract(status)
1614 comment_type = Optional.extract(comment_type)
1618 comment_type = Optional.extract(comment_type)
1615 resolves_comment_id = Optional.extract(resolves_comment_id)
1619 resolves_comment_id = Optional.extract(resolves_comment_id)
1616 extra_recipients = Optional.extract(extra_recipients)
1620 extra_recipients = Optional.extract(extra_recipients)
1617 send_email = Optional.extract(send_email, binary=True)
1621 send_email = Optional.extract(send_email, binary=True)
1618
1622
1619 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1623 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1620 if status and status not in allowed_statuses:
1624 if status and status not in allowed_statuses:
1621 raise JSONRPCError('Bad status, must be on '
1625 raise JSONRPCError('Bad status, must be on '
1622 'of %s got %s' % (allowed_statuses, status,))
1626 'of %s got %s' % (allowed_statuses, status,))
1623
1627
1624 if resolves_comment_id:
1628 if resolves_comment_id:
1625 comment = ChangesetComment.get(resolves_comment_id)
1629 comment = ChangesetComment.get(resolves_comment_id)
1626 if not comment:
1630 if not comment:
1627 raise JSONRPCError(
1631 raise JSONRPCError(
1628 'Invalid resolves_comment_id `%s` for this commit.'
1632 'Invalid resolves_comment_id `%s` for this commit.'
1629 % resolves_comment_id)
1633 % resolves_comment_id)
1630 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1634 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1631 raise JSONRPCError(
1635 raise JSONRPCError(
1632 'Comment `%s` is wrong type for setting status to resolved.'
1636 'Comment `%s` is wrong type for setting status to resolved.'
1633 % resolves_comment_id)
1637 % resolves_comment_id)
1634
1638
1635 try:
1639 try:
1636 rc_config = SettingsModel().get_all_settings()
1640 rc_config = SettingsModel().get_all_settings()
1637 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1641 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1638 status_change_label = ChangesetStatus.get_status_lbl(status)
1642 status_change_label = ChangesetStatus.get_status_lbl(status)
1639 comment = CommentsModel().create(
1643 comment = CommentsModel().create(
1640 message, repo, user, commit_id=commit_id,
1644 message, repo, user, commit_id=commit_id,
1641 status_change=status_change_label,
1645 status_change=status_change_label,
1642 status_change_type=status,
1646 status_change_type=status,
1643 renderer=renderer,
1647 renderer=renderer,
1644 comment_type=comment_type,
1648 comment_type=comment_type,
1645 resolves_comment_id=resolves_comment_id,
1649 resolves_comment_id=resolves_comment_id,
1646 auth_user=apiuser,
1650 auth_user=apiuser,
1647 extra_recipients=extra_recipients,
1651 extra_recipients=extra_recipients,
1648 send_email=send_email
1652 send_email=send_email
1649 )
1653 )
1650 if status:
1654 if status:
1651 # also do a status change
1655 # also do a status change
1652 try:
1656 try:
1653 ChangesetStatusModel().set_status(
1657 ChangesetStatusModel().set_status(
1654 repo, status, user, comment, revision=commit_id,
1658 repo, status, user, comment, revision=commit_id,
1655 dont_allow_on_closed_pull_request=True
1659 dont_allow_on_closed_pull_request=True
1656 )
1660 )
1657 except StatusChangeOnClosedPullRequestError:
1661 except StatusChangeOnClosedPullRequestError:
1658 log.exception(
1662 log.exception(
1659 "Exception occurred while trying to change repo commit status")
1663 "Exception occurred while trying to change repo commit status")
1660 msg = ('Changing status on a commit associated with '
1664 msg = ('Changing status on a commit associated with '
1661 'a closed pull request is not allowed')
1665 'a closed pull request is not allowed')
1662 raise JSONRPCError(msg)
1666 raise JSONRPCError(msg)
1663
1667
1664 CommentsModel().trigger_commit_comment_hook(
1668 CommentsModel().trigger_commit_comment_hook(
1665 repo, apiuser, 'create',
1669 repo, apiuser, 'create',
1666 data={'comment': comment, 'commit': commit})
1670 data={'comment': comment, 'commit': commit})
1667
1671
1668 Session().commit()
1672 Session().commit()
1669 return {
1673 return {
1670 'msg': (
1674 'msg': (
1671 'Commented on commit `%s` for repository `%s`' % (
1675 'Commented on commit `%s` for repository `%s`' % (
1672 comment.revision, repo.repo_name)),
1676 comment.revision, repo.repo_name)),
1673 'status_change': status,
1677 'status_change': status,
1674 'success': True,
1678 'success': True,
1675 }
1679 }
1676 except JSONRPCError:
1680 except JSONRPCError:
1677 # catch any inside errors, and re-raise them to prevent from
1681 # catch any inside errors, and re-raise them to prevent from
1678 # below global catch to silence them
1682 # below global catch to silence them
1679 raise
1683 raise
1680 except Exception:
1684 except Exception:
1681 log.exception("Exception occurred while trying to comment on commit")
1685 log.exception("Exception occurred while trying to comment on commit")
1682 raise JSONRPCError(
1686 raise JSONRPCError(
1683 'failed to set comment on repository `%s`' % (repo.repo_name,)
1687 'failed to set comment on repository `%s`' % (repo.repo_name,)
1684 )
1688 )
1685
1689
1686
1690
1687 @jsonrpc_method()
1691 @jsonrpc_method()
1688 def get_repo_comments(request, apiuser, repoid,
1692 def get_repo_comments(request, apiuser, repoid,
1689 commit_id=Optional(None), comment_type=Optional(None),
1693 commit_id=Optional(None), comment_type=Optional(None),
1690 userid=Optional(None)):
1694 userid=Optional(None)):
1691 """
1695 """
1692 Get all comments for a repository
1696 Get all comments for a repository
1693
1697
1694 :param apiuser: This is filled automatically from the |authtoken|.
1698 :param apiuser: This is filled automatically from the |authtoken|.
1695 :type apiuser: AuthUser
1699 :type apiuser: AuthUser
1696 :param repoid: Set the repository name or repository ID.
1700 :param repoid: Set the repository name or repository ID.
1697 :type repoid: str or int
1701 :type repoid: str or int
1698 :param commit_id: Optionally filter the comments by the commit_id
1702 :param commit_id: Optionally filter the comments by the commit_id
1699 :type commit_id: Optional(str), default: None
1703 :type commit_id: Optional(str), default: None
1700 :param comment_type: Optionally filter the comments by the comment_type
1704 :param comment_type: Optionally filter the comments by the comment_type
1701 one of: 'note', 'todo'
1705 one of: 'note', 'todo'
1702 :type comment_type: Optional(str), default: None
1706 :type comment_type: Optional(str), default: None
1703 :param userid: Optionally filter the comments by the author of comment
1707 :param userid: Optionally filter the comments by the author of comment
1704 :type userid: Optional(str or int), Default: None
1708 :type userid: Optional(str or int), Default: None
1705
1709
1706 Example error output:
1710 Example error output:
1707
1711
1708 .. code-block:: bash
1712 .. code-block:: bash
1709
1713
1710 {
1714 {
1711 "id" : <id_given_in_input>,
1715 "id" : <id_given_in_input>,
1712 "result" : [
1716 "result" : [
1713 {
1717 {
1714 "comment_author": <USER_DETAILS>,
1718 "comment_author": <USER_DETAILS>,
1715 "comment_created_on": "2017-02-01T14:38:16.309",
1719 "comment_created_on": "2017-02-01T14:38:16.309",
1716 "comment_f_path": "file.txt",
1720 "comment_f_path": "file.txt",
1717 "comment_id": 282,
1721 "comment_id": 282,
1718 "comment_lineno": "n1",
1722 "comment_lineno": "n1",
1719 "comment_resolved_by": null,
1723 "comment_resolved_by": null,
1720 "comment_status": [],
1724 "comment_status": [],
1721 "comment_text": "This file needs a header",
1725 "comment_text": "This file needs a header",
1722 "comment_type": "todo"
1726 "comment_type": "todo",
1727 "comment_last_version: 0
1723 }
1728 }
1724 ],
1729 ],
1725 "error" : null
1730 "error" : null
1726 }
1731 }
1727
1732
1728 """
1733 """
1729 repo = get_repo_or_error(repoid)
1734 repo = get_repo_or_error(repoid)
1730 if not has_superadmin_permission(apiuser):
1735 if not has_superadmin_permission(apiuser):
1731 _perms = ('repository.read', 'repository.write', 'repository.admin')
1736 _perms = ('repository.read', 'repository.write', 'repository.admin')
1732 validate_repo_permissions(apiuser, repoid, repo, _perms)
1737 validate_repo_permissions(apiuser, repoid, repo, _perms)
1733
1738
1734 commit_id = Optional.extract(commit_id)
1739 commit_id = Optional.extract(commit_id)
1735
1740
1736 userid = Optional.extract(userid)
1741 userid = Optional.extract(userid)
1737 if userid:
1742 if userid:
1738 user = get_user_or_error(userid)
1743 user = get_user_or_error(userid)
1739 else:
1744 else:
1740 user = None
1745 user = None
1741
1746
1742 comment_type = Optional.extract(comment_type)
1747 comment_type = Optional.extract(comment_type)
1743 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1748 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1744 raise JSONRPCError(
1749 raise JSONRPCError(
1745 'comment_type must be one of `{}` got {}'.format(
1750 'comment_type must be one of `{}` got {}'.format(
1746 ChangesetComment.COMMENT_TYPES, comment_type)
1751 ChangesetComment.COMMENT_TYPES, comment_type)
1747 )
1752 )
1748
1753
1749 comments = CommentsModel().get_repository_comments(
1754 comments = CommentsModel().get_repository_comments(
1750 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1755 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1751 return comments
1756 return comments
1752
1757
1753
1758
1754 @jsonrpc_method()
1759 @jsonrpc_method()
1760 def get_comment(request, apiuser, comment_id):
1761 """
1762 Get single comment from repository or pull_request
1763
1764 :param apiuser: This is filled automatically from the |authtoken|.
1765 :type apiuser: AuthUser
1766 :param comment_id: comment id found in the URL of comment
1767 :type comment_id: str or int
1768
1769 Example error output:
1770
1771 .. code-block:: bash
1772
1773 {
1774 "id" : <id_given_in_input>,
1775 "result" : {
1776 "comment_author": <USER_DETAILS>,
1777 "comment_created_on": "2017-02-01T14:38:16.309",
1778 "comment_f_path": "file.txt",
1779 "comment_id": 282,
1780 "comment_lineno": "n1",
1781 "comment_resolved_by": null,
1782 "comment_status": [],
1783 "comment_text": "This file needs a header",
1784 "comment_type": "todo",
1785 "comment_last_version: 0
1786 },
1787 "error" : null
1788 }
1789
1790 """
1791
1792 comment = ChangesetComment.get(comment_id)
1793 if not comment:
1794 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1795
1796 perms = ('repository.read', 'repository.write', 'repository.admin')
1797 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1798 (user=apiuser, repo_name=comment.repo.repo_name)
1799
1800 if not has_comment_perm:
1801 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1802
1803 return comment
1804
1805
1806 @jsonrpc_method()
1807 def edit_comment(request, apiuser, message, comment_id, version,
1808 userid=Optional(OAttr('apiuser'))):
1809 """
1810 Edit comment on the pull request or commit,
1811 specified by the `comment_id` and version. Initially version should be 0
1812
1813 :param apiuser: This is filled automatically from the |authtoken|.
1814 :type apiuser: AuthUser
1815 :param comment_id: Specify the comment_id for editing
1816 :type comment_id: int
1817 :param version: version of the comment that will be created, starts from 0
1818 :type version: int
1819 :param message: The text content of the comment.
1820 :type message: str
1821 :param userid: Comment on the pull request as this user
1822 :type userid: Optional(str or int)
1823
1824 Example output:
1825
1826 .. code-block:: bash
1827
1828 id : <id_given_in_input>
1829 result : {
1830 "comment": "<comment data>",
1831 "version": "<Integer>",
1832 },
1833 error : null
1834 """
1835
1836 auth_user = apiuser
1837 comment = ChangesetComment.get(comment_id)
1838 if not comment:
1839 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1840
1841 is_super_admin = has_superadmin_permission(apiuser)
1842 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1843 (user=apiuser, repo_name=comment.repo.repo_name)
1844
1845 if not isinstance(userid, Optional):
1846 if is_super_admin or is_repo_admin:
1847 apiuser = get_user_or_error(userid)
1848 auth_user = apiuser.AuthUser()
1849 else:
1850 raise JSONRPCError('userid is not the same as your user')
1851
1852 comment_author = comment.author.user_id == auth_user.user_id
1853 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1854 raise JSONRPCError("you don't have access to edit this comment")
1855
1856 try:
1857 comment_history = CommentsModel().edit(
1858 comment_id=comment_id,
1859 text=message,
1860 auth_user=auth_user,
1861 version=version,
1862 )
1863 Session().commit()
1864 except CommentVersionMismatch:
1865 raise JSONRPCError(
1866 'comment ({}) version ({}) mismatch'.format(comment_id, version)
1867 )
1868 if not comment_history and not message:
1869 raise JSONRPCError(
1870 "comment ({}) can't be changed with empty string".format(comment_id)
1871 )
1872 data = {
1873 'comment': comment,
1874 'version': comment_history.version if comment_history else None,
1875 }
1876 return data
1877
1878
1879 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1880 # @jsonrpc_method()
1881 # def delete_comment(request, apiuser, comment_id):
1882 # auth_user = apiuser
1883 #
1884 # comment = ChangesetComment.get(comment_id)
1885 # if not comment:
1886 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1887 #
1888 # is_super_admin = has_superadmin_permission(apiuser)
1889 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1890 # (user=apiuser, repo_name=comment.repo.repo_name)
1891 #
1892 # comment_author = comment.author.user_id == auth_user.user_id
1893 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1894 # raise JSONRPCError("you don't have access to edit this comment")
1895
1896 @jsonrpc_method()
1755 def grant_user_permission(request, apiuser, repoid, userid, perm):
1897 def grant_user_permission(request, apiuser, repoid, userid, perm):
1756 """
1898 """
1757 Grant permissions for the specified user on the given repository,
1899 Grant permissions for the specified user on the given repository,
1758 or update existing permissions if found.
1900 or update existing permissions if found.
1759
1901
1760 This command can only be run using an |authtoken| with admin
1902 This command can only be run using an |authtoken| with admin
1761 permissions on the |repo|.
1903 permissions on the |repo|.
1762
1904
1763 :param apiuser: This is filled automatically from the |authtoken|.
1905 :param apiuser: This is filled automatically from the |authtoken|.
1764 :type apiuser: AuthUser
1906 :type apiuser: AuthUser
1765 :param repoid: Set the repository name or repository ID.
1907 :param repoid: Set the repository name or repository ID.
1766 :type repoid: str or int
1908 :type repoid: str or int
1767 :param userid: Set the user name.
1909 :param userid: Set the user name.
1768 :type userid: str
1910 :type userid: str
1769 :param perm: Set the user permissions, using the following format
1911 :param perm: Set the user permissions, using the following format
1770 ``(repository.(none|read|write|admin))``
1912 ``(repository.(none|read|write|admin))``
1771 :type perm: str
1913 :type perm: str
1772
1914
1773 Example output:
1915 Example output:
1774
1916
1775 .. code-block:: bash
1917 .. code-block:: bash
1776
1918
1777 id : <id_given_in_input>
1919 id : <id_given_in_input>
1778 result: {
1920 result: {
1779 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1921 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1780 "success": true
1922 "success": true
1781 }
1923 }
1782 error: null
1924 error: null
1783 """
1925 """
1784
1926
1785 repo = get_repo_or_error(repoid)
1927 repo = get_repo_or_error(repoid)
1786 user = get_user_or_error(userid)
1928 user = get_user_or_error(userid)
1787 perm = get_perm_or_error(perm)
1929 perm = get_perm_or_error(perm)
1788 if not has_superadmin_permission(apiuser):
1930 if not has_superadmin_permission(apiuser):
1789 _perms = ('repository.admin',)
1931 _perms = ('repository.admin',)
1790 validate_repo_permissions(apiuser, repoid, repo, _perms)
1932 validate_repo_permissions(apiuser, repoid, repo, _perms)
1791
1933
1792 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1934 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1793 try:
1935 try:
1794 changes = RepoModel().update_permissions(
1936 changes = RepoModel().update_permissions(
1795 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1937 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1796
1938
1797 action_data = {
1939 action_data = {
1798 'added': changes['added'],
1940 'added': changes['added'],
1799 'updated': changes['updated'],
1941 'updated': changes['updated'],
1800 'deleted': changes['deleted'],
1942 'deleted': changes['deleted'],
1801 }
1943 }
1802 audit_logger.store_api(
1944 audit_logger.store_api(
1803 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1945 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1804 Session().commit()
1946 Session().commit()
1805 PermissionModel().flush_user_permission_caches(changes)
1947 PermissionModel().flush_user_permission_caches(changes)
1806
1948
1807 return {
1949 return {
1808 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1950 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1809 perm.permission_name, user.username, repo.repo_name
1951 perm.permission_name, user.username, repo.repo_name
1810 ),
1952 ),
1811 'success': True
1953 'success': True
1812 }
1954 }
1813 except Exception:
1955 except Exception:
1814 log.exception("Exception occurred while trying edit permissions for repo")
1956 log.exception("Exception occurred while trying edit permissions for repo")
1815 raise JSONRPCError(
1957 raise JSONRPCError(
1816 'failed to edit permission for user: `%s` in repo: `%s`' % (
1958 'failed to edit permission for user: `%s` in repo: `%s`' % (
1817 userid, repoid
1959 userid, repoid
1818 )
1960 )
1819 )
1961 )
1820
1962
1821
1963
1822 @jsonrpc_method()
1964 @jsonrpc_method()
1823 def revoke_user_permission(request, apiuser, repoid, userid):
1965 def revoke_user_permission(request, apiuser, repoid, userid):
1824 """
1966 """
1825 Revoke permission for a user on the specified repository.
1967 Revoke permission for a user on the specified repository.
1826
1968
1827 This command can only be run using an |authtoken| with admin
1969 This command can only be run using an |authtoken| with admin
1828 permissions on the |repo|.
1970 permissions on the |repo|.
1829
1971
1830 :param apiuser: This is filled automatically from the |authtoken|.
1972 :param apiuser: This is filled automatically from the |authtoken|.
1831 :type apiuser: AuthUser
1973 :type apiuser: AuthUser
1832 :param repoid: Set the repository name or repository ID.
1974 :param repoid: Set the repository name or repository ID.
1833 :type repoid: str or int
1975 :type repoid: str or int
1834 :param userid: Set the user name of revoked user.
1976 :param userid: Set the user name of revoked user.
1835 :type userid: str or int
1977 :type userid: str or int
1836
1978
1837 Example error output:
1979 Example error output:
1838
1980
1839 .. code-block:: bash
1981 .. code-block:: bash
1840
1982
1841 id : <id_given_in_input>
1983 id : <id_given_in_input>
1842 result: {
1984 result: {
1843 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1985 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1844 "success": true
1986 "success": true
1845 }
1987 }
1846 error: null
1988 error: null
1847 """
1989 """
1848
1990
1849 repo = get_repo_or_error(repoid)
1991 repo = get_repo_or_error(repoid)
1850 user = get_user_or_error(userid)
1992 user = get_user_or_error(userid)
1851 if not has_superadmin_permission(apiuser):
1993 if not has_superadmin_permission(apiuser):
1852 _perms = ('repository.admin',)
1994 _perms = ('repository.admin',)
1853 validate_repo_permissions(apiuser, repoid, repo, _perms)
1995 validate_repo_permissions(apiuser, repoid, repo, _perms)
1854
1996
1855 perm_deletions = [[user.user_id, None, "user"]]
1997 perm_deletions = [[user.user_id, None, "user"]]
1856 try:
1998 try:
1857 changes = RepoModel().update_permissions(
1999 changes = RepoModel().update_permissions(
1858 repo=repo, perm_deletions=perm_deletions, cur_user=user)
2000 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1859
2001
1860 action_data = {
2002 action_data = {
1861 'added': changes['added'],
2003 'added': changes['added'],
1862 'updated': changes['updated'],
2004 'updated': changes['updated'],
1863 'deleted': changes['deleted'],
2005 'deleted': changes['deleted'],
1864 }
2006 }
1865 audit_logger.store_api(
2007 audit_logger.store_api(
1866 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2008 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1867 Session().commit()
2009 Session().commit()
1868 PermissionModel().flush_user_permission_caches(changes)
2010 PermissionModel().flush_user_permission_caches(changes)
1869
2011
1870 return {
2012 return {
1871 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
2013 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1872 user.username, repo.repo_name
2014 user.username, repo.repo_name
1873 ),
2015 ),
1874 'success': True
2016 'success': True
1875 }
2017 }
1876 except Exception:
2018 except Exception:
1877 log.exception("Exception occurred while trying revoke permissions to repo")
2019 log.exception("Exception occurred while trying revoke permissions to repo")
1878 raise JSONRPCError(
2020 raise JSONRPCError(
1879 'failed to edit permission for user: `%s` in repo: `%s`' % (
2021 'failed to edit permission for user: `%s` in repo: `%s`' % (
1880 userid, repoid
2022 userid, repoid
1881 )
2023 )
1882 )
2024 )
1883
2025
1884
2026
1885 @jsonrpc_method()
2027 @jsonrpc_method()
1886 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
2028 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1887 """
2029 """
1888 Grant permission for a user group on the specified repository,
2030 Grant permission for a user group on the specified repository,
1889 or update existing permissions.
2031 or update existing permissions.
1890
2032
1891 This command can only be run using an |authtoken| with admin
2033 This command can only be run using an |authtoken| with admin
1892 permissions on the |repo|.
2034 permissions on the |repo|.
1893
2035
1894 :param apiuser: This is filled automatically from the |authtoken|.
2036 :param apiuser: This is filled automatically from the |authtoken|.
1895 :type apiuser: AuthUser
2037 :type apiuser: AuthUser
1896 :param repoid: Set the repository name or repository ID.
2038 :param repoid: Set the repository name or repository ID.
1897 :type repoid: str or int
2039 :type repoid: str or int
1898 :param usergroupid: Specify the ID of the user group.
2040 :param usergroupid: Specify the ID of the user group.
1899 :type usergroupid: str or int
2041 :type usergroupid: str or int
1900 :param perm: Set the user group permissions using the following
2042 :param perm: Set the user group permissions using the following
1901 format: (repository.(none|read|write|admin))
2043 format: (repository.(none|read|write|admin))
1902 :type perm: str
2044 :type perm: str
1903
2045
1904 Example output:
2046 Example output:
1905
2047
1906 .. code-block:: bash
2048 .. code-block:: bash
1907
2049
1908 id : <id_given_in_input>
2050 id : <id_given_in_input>
1909 result : {
2051 result : {
1910 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
2052 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1911 "success": true
2053 "success": true
1912
2054
1913 }
2055 }
1914 error : null
2056 error : null
1915
2057
1916 Example error output:
2058 Example error output:
1917
2059
1918 .. code-block:: bash
2060 .. code-block:: bash
1919
2061
1920 id : <id_given_in_input>
2062 id : <id_given_in_input>
1921 result : null
2063 result : null
1922 error : {
2064 error : {
1923 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
2065 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1924 }
2066 }
1925
2067
1926 """
2068 """
1927
2069
1928 repo = get_repo_or_error(repoid)
2070 repo = get_repo_or_error(repoid)
1929 perm = get_perm_or_error(perm)
2071 perm = get_perm_or_error(perm)
1930 if not has_superadmin_permission(apiuser):
2072 if not has_superadmin_permission(apiuser):
1931 _perms = ('repository.admin',)
2073 _perms = ('repository.admin',)
1932 validate_repo_permissions(apiuser, repoid, repo, _perms)
2074 validate_repo_permissions(apiuser, repoid, repo, _perms)
1933
2075
1934 user_group = get_user_group_or_error(usergroupid)
2076 user_group = get_user_group_or_error(usergroupid)
1935 if not has_superadmin_permission(apiuser):
2077 if not has_superadmin_permission(apiuser):
1936 # check if we have at least read permission for this user group !
2078 # check if we have at least read permission for this user group !
1937 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2079 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1938 if not HasUserGroupPermissionAnyApi(*_perms)(
2080 if not HasUserGroupPermissionAnyApi(*_perms)(
1939 user=apiuser, user_group_name=user_group.users_group_name):
2081 user=apiuser, user_group_name=user_group.users_group_name):
1940 raise JSONRPCError(
2082 raise JSONRPCError(
1941 'user group `%s` does not exist' % (usergroupid,))
2083 'user group `%s` does not exist' % (usergroupid,))
1942
2084
1943 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
2085 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1944 try:
2086 try:
1945 changes = RepoModel().update_permissions(
2087 changes = RepoModel().update_permissions(
1946 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
2088 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1947 action_data = {
2089 action_data = {
1948 'added': changes['added'],
2090 'added': changes['added'],
1949 'updated': changes['updated'],
2091 'updated': changes['updated'],
1950 'deleted': changes['deleted'],
2092 'deleted': changes['deleted'],
1951 }
2093 }
1952 audit_logger.store_api(
2094 audit_logger.store_api(
1953 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2095 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1954 Session().commit()
2096 Session().commit()
1955 PermissionModel().flush_user_permission_caches(changes)
2097 PermissionModel().flush_user_permission_caches(changes)
1956
2098
1957 return {
2099 return {
1958 'msg': 'Granted perm: `%s` for user group: `%s` in '
2100 'msg': 'Granted perm: `%s` for user group: `%s` in '
1959 'repo: `%s`' % (
2101 'repo: `%s`' % (
1960 perm.permission_name, user_group.users_group_name,
2102 perm.permission_name, user_group.users_group_name,
1961 repo.repo_name
2103 repo.repo_name
1962 ),
2104 ),
1963 'success': True
2105 'success': True
1964 }
2106 }
1965 except Exception:
2107 except Exception:
1966 log.exception(
2108 log.exception(
1967 "Exception occurred while trying change permission on repo")
2109 "Exception occurred while trying change permission on repo")
1968 raise JSONRPCError(
2110 raise JSONRPCError(
1969 'failed to edit permission for user group: `%s` in '
2111 'failed to edit permission for user group: `%s` in '
1970 'repo: `%s`' % (
2112 'repo: `%s`' % (
1971 usergroupid, repo.repo_name
2113 usergroupid, repo.repo_name
1972 )
2114 )
1973 )
2115 )
1974
2116
1975
2117
1976 @jsonrpc_method()
2118 @jsonrpc_method()
1977 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
2119 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1978 """
2120 """
1979 Revoke the permissions of a user group on a given repository.
2121 Revoke the permissions of a user group on a given repository.
1980
2122
1981 This command can only be run using an |authtoken| with admin
2123 This command can only be run using an |authtoken| with admin
1982 permissions on the |repo|.
2124 permissions on the |repo|.
1983
2125
1984 :param apiuser: This is filled automatically from the |authtoken|.
2126 :param apiuser: This is filled automatically from the |authtoken|.
1985 :type apiuser: AuthUser
2127 :type apiuser: AuthUser
1986 :param repoid: Set the repository name or repository ID.
2128 :param repoid: Set the repository name or repository ID.
1987 :type repoid: str or int
2129 :type repoid: str or int
1988 :param usergroupid: Specify the user group ID.
2130 :param usergroupid: Specify the user group ID.
1989 :type usergroupid: str or int
2131 :type usergroupid: str or int
1990
2132
1991 Example output:
2133 Example output:
1992
2134
1993 .. code-block:: bash
2135 .. code-block:: bash
1994
2136
1995 id : <id_given_in_input>
2137 id : <id_given_in_input>
1996 result: {
2138 result: {
1997 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
2139 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1998 "success": true
2140 "success": true
1999 }
2141 }
2000 error: null
2142 error: null
2001 """
2143 """
2002
2144
2003 repo = get_repo_or_error(repoid)
2145 repo = get_repo_or_error(repoid)
2004 if not has_superadmin_permission(apiuser):
2146 if not has_superadmin_permission(apiuser):
2005 _perms = ('repository.admin',)
2147 _perms = ('repository.admin',)
2006 validate_repo_permissions(apiuser, repoid, repo, _perms)
2148 validate_repo_permissions(apiuser, repoid, repo, _perms)
2007
2149
2008 user_group = get_user_group_or_error(usergroupid)
2150 user_group = get_user_group_or_error(usergroupid)
2009 if not has_superadmin_permission(apiuser):
2151 if not has_superadmin_permission(apiuser):
2010 # check if we have at least read permission for this user group !
2152 # check if we have at least read permission for this user group !
2011 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2153 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2012 if not HasUserGroupPermissionAnyApi(*_perms)(
2154 if not HasUserGroupPermissionAnyApi(*_perms)(
2013 user=apiuser, user_group_name=user_group.users_group_name):
2155 user=apiuser, user_group_name=user_group.users_group_name):
2014 raise JSONRPCError(
2156 raise JSONRPCError(
2015 'user group `%s` does not exist' % (usergroupid,))
2157 'user group `%s` does not exist' % (usergroupid,))
2016
2158
2017 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2159 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2018 try:
2160 try:
2019 changes = RepoModel().update_permissions(
2161 changes = RepoModel().update_permissions(
2020 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2162 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2021 action_data = {
2163 action_data = {
2022 'added': changes['added'],
2164 'added': changes['added'],
2023 'updated': changes['updated'],
2165 'updated': changes['updated'],
2024 'deleted': changes['deleted'],
2166 'deleted': changes['deleted'],
2025 }
2167 }
2026 audit_logger.store_api(
2168 audit_logger.store_api(
2027 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2169 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2028 Session().commit()
2170 Session().commit()
2029 PermissionModel().flush_user_permission_caches(changes)
2171 PermissionModel().flush_user_permission_caches(changes)
2030
2172
2031 return {
2173 return {
2032 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2174 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2033 user_group.users_group_name, repo.repo_name
2175 user_group.users_group_name, repo.repo_name
2034 ),
2176 ),
2035 'success': True
2177 'success': True
2036 }
2178 }
2037 except Exception:
2179 except Exception:
2038 log.exception("Exception occurred while trying revoke "
2180 log.exception("Exception occurred while trying revoke "
2039 "user group permission on repo")
2181 "user group permission on repo")
2040 raise JSONRPCError(
2182 raise JSONRPCError(
2041 'failed to edit permission for user group: `%s` in '
2183 'failed to edit permission for user group: `%s` in '
2042 'repo: `%s`' % (
2184 'repo: `%s`' % (
2043 user_group.users_group_name, repo.repo_name
2185 user_group.users_group_name, repo.repo_name
2044 )
2186 )
2045 )
2187 )
2046
2188
2047
2189
2048 @jsonrpc_method()
2190 @jsonrpc_method()
2049 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2191 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2050 """
2192 """
2051 Triggers a pull on the given repository from a remote location. You
2193 Triggers a pull on the given repository from a remote location. You
2052 can use this to keep remote repositories up-to-date.
2194 can use this to keep remote repositories up-to-date.
2053
2195
2054 This command can only be run using an |authtoken| with admin
2196 This command can only be run using an |authtoken| with admin
2055 rights to the specified repository. For more information,
2197 rights to the specified repository. For more information,
2056 see :ref:`config-token-ref`.
2198 see :ref:`config-token-ref`.
2057
2199
2058 This command takes the following options:
2200 This command takes the following options:
2059
2201
2060 :param apiuser: This is filled automatically from the |authtoken|.
2202 :param apiuser: This is filled automatically from the |authtoken|.
2061 :type apiuser: AuthUser
2203 :type apiuser: AuthUser
2062 :param repoid: The repository name or repository ID.
2204 :param repoid: The repository name or repository ID.
2063 :type repoid: str or int
2205 :type repoid: str or int
2064 :param remote_uri: Optional remote URI to pass in for pull
2206 :param remote_uri: Optional remote URI to pass in for pull
2065 :type remote_uri: str
2207 :type remote_uri: str
2066
2208
2067 Example output:
2209 Example output:
2068
2210
2069 .. code-block:: bash
2211 .. code-block:: bash
2070
2212
2071 id : <id_given_in_input>
2213 id : <id_given_in_input>
2072 result : {
2214 result : {
2073 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2215 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2074 "repository": "<repository name>"
2216 "repository": "<repository name>"
2075 }
2217 }
2076 error : null
2218 error : null
2077
2219
2078 Example error output:
2220 Example error output:
2079
2221
2080 .. code-block:: bash
2222 .. code-block:: bash
2081
2223
2082 id : <id_given_in_input>
2224 id : <id_given_in_input>
2083 result : null
2225 result : null
2084 error : {
2226 error : {
2085 "Unable to push changes from `<remote_url>`"
2227 "Unable to push changes from `<remote_url>`"
2086 }
2228 }
2087
2229
2088 """
2230 """
2089
2231
2090 repo = get_repo_or_error(repoid)
2232 repo = get_repo_or_error(repoid)
2091 remote_uri = Optional.extract(remote_uri)
2233 remote_uri = Optional.extract(remote_uri)
2092 remote_uri_display = remote_uri or repo.clone_uri_hidden
2234 remote_uri_display = remote_uri or repo.clone_uri_hidden
2093 if not has_superadmin_permission(apiuser):
2235 if not has_superadmin_permission(apiuser):
2094 _perms = ('repository.admin',)
2236 _perms = ('repository.admin',)
2095 validate_repo_permissions(apiuser, repoid, repo, _perms)
2237 validate_repo_permissions(apiuser, repoid, repo, _perms)
2096
2238
2097 try:
2239 try:
2098 ScmModel().pull_changes(
2240 ScmModel().pull_changes(
2099 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2241 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2100 return {
2242 return {
2101 'msg': 'Pulled from url `%s` on repo `%s`' % (
2243 'msg': 'Pulled from url `%s` on repo `%s`' % (
2102 remote_uri_display, repo.repo_name),
2244 remote_uri_display, repo.repo_name),
2103 'repository': repo.repo_name
2245 'repository': repo.repo_name
2104 }
2246 }
2105 except Exception:
2247 except Exception:
2106 log.exception("Exception occurred while trying to "
2248 log.exception("Exception occurred while trying to "
2107 "pull changes from remote location")
2249 "pull changes from remote location")
2108 raise JSONRPCError(
2250 raise JSONRPCError(
2109 'Unable to pull changes from `%s`' % remote_uri_display
2251 'Unable to pull changes from `%s`' % remote_uri_display
2110 )
2252 )
2111
2253
2112
2254
2113 @jsonrpc_method()
2255 @jsonrpc_method()
2114 def strip(request, apiuser, repoid, revision, branch):
2256 def strip(request, apiuser, repoid, revision, branch):
2115 """
2257 """
2116 Strips the given revision from the specified repository.
2258 Strips the given revision from the specified repository.
2117
2259
2118 * This will remove the revision and all of its decendants.
2260 * This will remove the revision and all of its decendants.
2119
2261
2120 This command can only be run using an |authtoken| with admin rights to
2262 This command can only be run using an |authtoken| with admin rights to
2121 the specified repository.
2263 the specified repository.
2122
2264
2123 This command takes the following options:
2265 This command takes the following options:
2124
2266
2125 :param apiuser: This is filled automatically from the |authtoken|.
2267 :param apiuser: This is filled automatically from the |authtoken|.
2126 :type apiuser: AuthUser
2268 :type apiuser: AuthUser
2127 :param repoid: The repository name or repository ID.
2269 :param repoid: The repository name or repository ID.
2128 :type repoid: str or int
2270 :type repoid: str or int
2129 :param revision: The revision you wish to strip.
2271 :param revision: The revision you wish to strip.
2130 :type revision: str
2272 :type revision: str
2131 :param branch: The branch from which to strip the revision.
2273 :param branch: The branch from which to strip the revision.
2132 :type branch: str
2274 :type branch: str
2133
2275
2134 Example output:
2276 Example output:
2135
2277
2136 .. code-block:: bash
2278 .. code-block:: bash
2137
2279
2138 id : <id_given_in_input>
2280 id : <id_given_in_input>
2139 result : {
2281 result : {
2140 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2282 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2141 "repository": "<repository name>"
2283 "repository": "<repository name>"
2142 }
2284 }
2143 error : null
2285 error : null
2144
2286
2145 Example error output:
2287 Example error output:
2146
2288
2147 .. code-block:: bash
2289 .. code-block:: bash
2148
2290
2149 id : <id_given_in_input>
2291 id : <id_given_in_input>
2150 result : null
2292 result : null
2151 error : {
2293 error : {
2152 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2294 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2153 }
2295 }
2154
2296
2155 """
2297 """
2156
2298
2157 repo = get_repo_or_error(repoid)
2299 repo = get_repo_or_error(repoid)
2158 if not has_superadmin_permission(apiuser):
2300 if not has_superadmin_permission(apiuser):
2159 _perms = ('repository.admin',)
2301 _perms = ('repository.admin',)
2160 validate_repo_permissions(apiuser, repoid, repo, _perms)
2302 validate_repo_permissions(apiuser, repoid, repo, _perms)
2161
2303
2162 try:
2304 try:
2163 ScmModel().strip(repo, revision, branch)
2305 ScmModel().strip(repo, revision, branch)
2164 audit_logger.store_api(
2306 audit_logger.store_api(
2165 'repo.commit.strip', action_data={'commit_id': revision},
2307 'repo.commit.strip', action_data={'commit_id': revision},
2166 repo=repo,
2308 repo=repo,
2167 user=apiuser, commit=True)
2309 user=apiuser, commit=True)
2168
2310
2169 return {
2311 return {
2170 'msg': 'Stripped commit %s from repo `%s`' % (
2312 'msg': 'Stripped commit %s from repo `%s`' % (
2171 revision, repo.repo_name),
2313 revision, repo.repo_name),
2172 'repository': repo.repo_name
2314 'repository': repo.repo_name
2173 }
2315 }
2174 except Exception:
2316 except Exception:
2175 log.exception("Exception while trying to strip")
2317 log.exception("Exception while trying to strip")
2176 raise JSONRPCError(
2318 raise JSONRPCError(
2177 'Unable to strip commit %s from repo `%s`' % (
2319 'Unable to strip commit %s from repo `%s`' % (
2178 revision, repo.repo_name)
2320 revision, repo.repo_name)
2179 )
2321 )
2180
2322
2181
2323
2182 @jsonrpc_method()
2324 @jsonrpc_method()
2183 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2325 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2184 """
2326 """
2185 Returns all settings for a repository. If key is given it only returns the
2327 Returns all settings for a repository. If key is given it only returns the
2186 setting identified by the key or null.
2328 setting identified by the key or null.
2187
2329
2188 :param apiuser: This is filled automatically from the |authtoken|.
2330 :param apiuser: This is filled automatically from the |authtoken|.
2189 :type apiuser: AuthUser
2331 :type apiuser: AuthUser
2190 :param repoid: The repository name or repository id.
2332 :param repoid: The repository name or repository id.
2191 :type repoid: str or int
2333 :type repoid: str or int
2192 :param key: Key of the setting to return.
2334 :param key: Key of the setting to return.
2193 :type: key: Optional(str)
2335 :type: key: Optional(str)
2194
2336
2195 Example output:
2337 Example output:
2196
2338
2197 .. code-block:: bash
2339 .. code-block:: bash
2198
2340
2199 {
2341 {
2200 "error": null,
2342 "error": null,
2201 "id": 237,
2343 "id": 237,
2202 "result": {
2344 "result": {
2203 "extensions_largefiles": true,
2345 "extensions_largefiles": true,
2204 "extensions_evolve": true,
2346 "extensions_evolve": true,
2205 "hooks_changegroup_push_logger": true,
2347 "hooks_changegroup_push_logger": true,
2206 "hooks_changegroup_repo_size": false,
2348 "hooks_changegroup_repo_size": false,
2207 "hooks_outgoing_pull_logger": true,
2349 "hooks_outgoing_pull_logger": true,
2208 "phases_publish": "True",
2350 "phases_publish": "True",
2209 "rhodecode_hg_use_rebase_for_merging": true,
2351 "rhodecode_hg_use_rebase_for_merging": true,
2210 "rhodecode_pr_merge_enabled": true,
2352 "rhodecode_pr_merge_enabled": true,
2211 "rhodecode_use_outdated_comments": true
2353 "rhodecode_use_outdated_comments": true
2212 }
2354 }
2213 }
2355 }
2214 """
2356 """
2215
2357
2216 # Restrict access to this api method to admins only.
2358 # Restrict access to this api method to admins only.
2217 if not has_superadmin_permission(apiuser):
2359 if not has_superadmin_permission(apiuser):
2218 raise JSONRPCForbidden()
2360 raise JSONRPCForbidden()
2219
2361
2220 try:
2362 try:
2221 repo = get_repo_or_error(repoid)
2363 repo = get_repo_or_error(repoid)
2222 settings_model = VcsSettingsModel(repo=repo)
2364 settings_model = VcsSettingsModel(repo=repo)
2223 settings = settings_model.get_global_settings()
2365 settings = settings_model.get_global_settings()
2224 settings.update(settings_model.get_repo_settings())
2366 settings.update(settings_model.get_repo_settings())
2225
2367
2226 # If only a single setting is requested fetch it from all settings.
2368 # If only a single setting is requested fetch it from all settings.
2227 key = Optional.extract(key)
2369 key = Optional.extract(key)
2228 if key is not None:
2370 if key is not None:
2229 settings = settings.get(key, None)
2371 settings = settings.get(key, None)
2230 except Exception:
2372 except Exception:
2231 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2373 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2232 log.exception(msg)
2374 log.exception(msg)
2233 raise JSONRPCError(msg)
2375 raise JSONRPCError(msg)
2234
2376
2235 return settings
2377 return settings
2236
2378
2237
2379
2238 @jsonrpc_method()
2380 @jsonrpc_method()
2239 def set_repo_settings(request, apiuser, repoid, settings):
2381 def set_repo_settings(request, apiuser, repoid, settings):
2240 """
2382 """
2241 Update repository settings. Returns true on success.
2383 Update repository settings. Returns true on success.
2242
2384
2243 :param apiuser: This is filled automatically from the |authtoken|.
2385 :param apiuser: This is filled automatically from the |authtoken|.
2244 :type apiuser: AuthUser
2386 :type apiuser: AuthUser
2245 :param repoid: The repository name or repository id.
2387 :param repoid: The repository name or repository id.
2246 :type repoid: str or int
2388 :type repoid: str or int
2247 :param settings: The new settings for the repository.
2389 :param settings: The new settings for the repository.
2248 :type: settings: dict
2390 :type: settings: dict
2249
2391
2250 Example output:
2392 Example output:
2251
2393
2252 .. code-block:: bash
2394 .. code-block:: bash
2253
2395
2254 {
2396 {
2255 "error": null,
2397 "error": null,
2256 "id": 237,
2398 "id": 237,
2257 "result": true
2399 "result": true
2258 }
2400 }
2259 """
2401 """
2260 # Restrict access to this api method to admins only.
2402 # Restrict access to this api method to admins only.
2261 if not has_superadmin_permission(apiuser):
2403 if not has_superadmin_permission(apiuser):
2262 raise JSONRPCForbidden()
2404 raise JSONRPCForbidden()
2263
2405
2264 if type(settings) is not dict:
2406 if type(settings) is not dict:
2265 raise JSONRPCError('Settings have to be a JSON Object.')
2407 raise JSONRPCError('Settings have to be a JSON Object.')
2266
2408
2267 try:
2409 try:
2268 settings_model = VcsSettingsModel(repo=repoid)
2410 settings_model = VcsSettingsModel(repo=repoid)
2269
2411
2270 # Merge global, repo and incoming settings.
2412 # Merge global, repo and incoming settings.
2271 new_settings = settings_model.get_global_settings()
2413 new_settings = settings_model.get_global_settings()
2272 new_settings.update(settings_model.get_repo_settings())
2414 new_settings.update(settings_model.get_repo_settings())
2273 new_settings.update(settings)
2415 new_settings.update(settings)
2274
2416
2275 # Update the settings.
2417 # Update the settings.
2276 inherit_global_settings = new_settings.get(
2418 inherit_global_settings = new_settings.get(
2277 'inherit_global_settings', False)
2419 'inherit_global_settings', False)
2278 settings_model.create_or_update_repo_settings(
2420 settings_model.create_or_update_repo_settings(
2279 new_settings, inherit_global_settings=inherit_global_settings)
2421 new_settings, inherit_global_settings=inherit_global_settings)
2280 Session().commit()
2422 Session().commit()
2281 except Exception:
2423 except Exception:
2282 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2424 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2283 log.exception(msg)
2425 log.exception(msg)
2284 raise JSONRPCError(msg)
2426 raise JSONRPCError(msg)
2285
2427
2286 # Indicate success.
2428 # Indicate success.
2287 return True
2429 return True
2288
2430
2289
2431
2290 @jsonrpc_method()
2432 @jsonrpc_method()
2291 def maintenance(request, apiuser, repoid):
2433 def maintenance(request, apiuser, repoid):
2292 """
2434 """
2293 Triggers a maintenance on the given repository.
2435 Triggers a maintenance on the given repository.
2294
2436
2295 This command can only be run using an |authtoken| with admin
2437 This command can only be run using an |authtoken| with admin
2296 rights to the specified repository. For more information,
2438 rights to the specified repository. For more information,
2297 see :ref:`config-token-ref`.
2439 see :ref:`config-token-ref`.
2298
2440
2299 This command takes the following options:
2441 This command takes the following options:
2300
2442
2301 :param apiuser: This is filled automatically from the |authtoken|.
2443 :param apiuser: This is filled automatically from the |authtoken|.
2302 :type apiuser: AuthUser
2444 :type apiuser: AuthUser
2303 :param repoid: The repository name or repository ID.
2445 :param repoid: The repository name or repository ID.
2304 :type repoid: str or int
2446 :type repoid: str or int
2305
2447
2306 Example output:
2448 Example output:
2307
2449
2308 .. code-block:: bash
2450 .. code-block:: bash
2309
2451
2310 id : <id_given_in_input>
2452 id : <id_given_in_input>
2311 result : {
2453 result : {
2312 "msg": "executed maintenance command",
2454 "msg": "executed maintenance command",
2313 "executed_actions": [
2455 "executed_actions": [
2314 <action_message>, <action_message2>...
2456 <action_message>, <action_message2>...
2315 ],
2457 ],
2316 "repository": "<repository name>"
2458 "repository": "<repository name>"
2317 }
2459 }
2318 error : null
2460 error : null
2319
2461
2320 Example error output:
2462 Example error output:
2321
2463
2322 .. code-block:: bash
2464 .. code-block:: bash
2323
2465
2324 id : <id_given_in_input>
2466 id : <id_given_in_input>
2325 result : null
2467 result : null
2326 error : {
2468 error : {
2327 "Unable to execute maintenance on `<reponame>`"
2469 "Unable to execute maintenance on `<reponame>`"
2328 }
2470 }
2329
2471
2330 """
2472 """
2331
2473
2332 repo = get_repo_or_error(repoid)
2474 repo = get_repo_or_error(repoid)
2333 if not has_superadmin_permission(apiuser):
2475 if not has_superadmin_permission(apiuser):
2334 _perms = ('repository.admin',)
2476 _perms = ('repository.admin',)
2335 validate_repo_permissions(apiuser, repoid, repo, _perms)
2477 validate_repo_permissions(apiuser, repoid, repo, _perms)
2336
2478
2337 try:
2479 try:
2338 maintenance = repo_maintenance.RepoMaintenance()
2480 maintenance = repo_maintenance.RepoMaintenance()
2339 executed_actions = maintenance.execute(repo)
2481 executed_actions = maintenance.execute(repo)
2340
2482
2341 return {
2483 return {
2342 'msg': 'executed maintenance command',
2484 'msg': 'executed maintenance command',
2343 'executed_actions': executed_actions,
2485 'executed_actions': executed_actions,
2344 'repository': repo.repo_name
2486 'repository': repo.repo_name
2345 }
2487 }
2346 except Exception:
2488 except Exception:
2347 log.exception("Exception occurred while trying to run maintenance")
2489 log.exception("Exception occurred while trying to run maintenance")
2348 raise JSONRPCError(
2490 raise JSONRPCError(
2349 'Unable to execute maintenance on `%s`' % repo.repo_name)
2491 'Unable to execute maintenance on `%s`' % repo.repo_name)
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now