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