##// END OF EJS Templates
comment-history: fixes/ui changes...
marcink -
r4408:1349565d default
parent child Browse files
Show More
@@ -1,507 +1,507 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.tests import TestController
23 from rhodecode.tests import TestController
24
24
25 from rhodecode.model.db import ChangesetComment, Notification
25 from rhodecode.model.db import ChangesetComment, Notification
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28
28
29
29
30 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
31 import urllib
31 import urllib
32
32
33 base_url = {
33 base_url = {
34 'repo_commit': '/{repo_name}/changeset/{commit_id}',
34 'repo_commit': '/{repo_name}/changeset/{commit_id}',
35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
37 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
38 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
38 'repo_commit_comment_edit': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/edit',
39 }[name].format(**kwargs)
39 }[name].format(**kwargs)
40
40
41 if params:
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
43 return base_url
43 return base_url
44
44
45
45
46 @pytest.mark.backends("git", "hg", "svn")
46 @pytest.mark.backends("git", "hg", "svn")
47 class TestRepoCommitCommentsView(TestController):
47 class TestRepoCommitCommentsView(TestController):
48
48
49 @pytest.fixture(autouse=True)
49 @pytest.fixture(autouse=True)
50 def prepare(self, request, baseapp):
50 def prepare(self, request, baseapp):
51 for x in ChangesetComment.query().all():
51 for x in ChangesetComment.query().all():
52 Session().delete(x)
52 Session().delete(x)
53 Session().commit()
53 Session().commit()
54
54
55 for x in Notification.query().all():
55 for x in Notification.query().all():
56 Session().delete(x)
56 Session().delete(x)
57 Session().commit()
57 Session().commit()
58
58
59 request.addfinalizer(self.cleanup)
59 request.addfinalizer(self.cleanup)
60
60
61 def cleanup(self):
61 def cleanup(self):
62 for x in ChangesetComment.query().all():
62 for x in ChangesetComment.query().all():
63 Session().delete(x)
63 Session().delete(x)
64 Session().commit()
64 Session().commit()
65
65
66 for x in Notification.query().all():
66 for x in Notification.query().all():
67 Session().delete(x)
67 Session().delete(x)
68 Session().commit()
68 Session().commit()
69
69
70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 def test_create(self, comment_type, backend):
71 def test_create(self, comment_type, backend):
72 self.log_user()
72 self.log_user()
73 commit = backend.repo.get_commit('300')
73 commit = backend.repo.get_commit('300')
74 commit_id = commit.raw_id
74 commit_id = commit.raw_id
75 text = u'CommentOnCommit'
75 text = u'CommentOnCommit'
76
76
77 params = {'text': text, 'csrf_token': self.csrf_token,
77 params = {'text': text, 'csrf_token': self.csrf_token,
78 'comment_type': comment_type}
78 'comment_type': comment_type}
79 self.app.post(
79 self.app.post(
80 route_path('repo_commit_comment_create',
80 route_path('repo_commit_comment_create',
81 repo_name=backend.repo_name, commit_id=commit_id),
81 repo_name=backend.repo_name, commit_id=commit_id),
82 params=params)
82 params=params)
83
83
84 response = self.app.get(
84 response = self.app.get(
85 route_path('repo_commit',
85 route_path('repo_commit',
86 repo_name=backend.repo_name, commit_id=commit_id))
86 repo_name=backend.repo_name, commit_id=commit_id))
87
87
88 # test DB
88 # test DB
89 assert ChangesetComment.query().count() == 1
89 assert ChangesetComment.query().count() == 1
90 assert_comment_links(response, ChangesetComment.query().count(), 0)
90 assert_comment_links(response, ChangesetComment.query().count(), 0)
91
91
92 assert Notification.query().count() == 1
92 assert Notification.query().count() == 1
93 assert ChangesetComment.query().count() == 1
93 assert ChangesetComment.query().count() == 1
94
94
95 notification = Notification.query().all()[0]
95 notification = Notification.query().all()[0]
96
96
97 comment_id = ChangesetComment.query().first().comment_id
97 comment_id = ChangesetComment.query().first().comment_id
98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99
99
100 author = notification.created_by_user.username_and_name
100 author = notification.created_by_user.username_and_name
101 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
101 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
102 author, comment_type, h.show_id(commit), backend.repo_name)
102 author, comment_type, h.show_id(commit), backend.repo_name)
103 assert sbj == notification.subject
103 assert sbj == notification.subject
104
104
105 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
105 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
106 backend.repo_name, commit_id, comment_id))
106 backend.repo_name, commit_id, comment_id))
107 assert lnk in notification.body
107 assert lnk in notification.body
108
108
109 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
109 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 def test_create_inline(self, comment_type, backend):
110 def test_create_inline(self, comment_type, backend):
111 self.log_user()
111 self.log_user()
112 commit = backend.repo.get_commit('300')
112 commit = backend.repo.get_commit('300')
113 commit_id = commit.raw_id
113 commit_id = commit.raw_id
114 text = u'CommentOnCommit'
114 text = u'CommentOnCommit'
115 f_path = 'vcs/web/simplevcs/views/repository.py'
115 f_path = 'vcs/web/simplevcs/views/repository.py'
116 line = 'n1'
116 line = 'n1'
117
117
118 params = {'text': text, 'f_path': f_path, 'line': line,
118 params = {'text': text, 'f_path': f_path, 'line': line,
119 'comment_type': comment_type,
119 'comment_type': comment_type,
120 'csrf_token': self.csrf_token}
120 'csrf_token': self.csrf_token}
121
121
122 self.app.post(
122 self.app.post(
123 route_path('repo_commit_comment_create',
123 route_path('repo_commit_comment_create',
124 repo_name=backend.repo_name, commit_id=commit_id),
124 repo_name=backend.repo_name, commit_id=commit_id),
125 params=params)
125 params=params)
126
126
127 response = self.app.get(
127 response = self.app.get(
128 route_path('repo_commit',
128 route_path('repo_commit',
129 repo_name=backend.repo_name, commit_id=commit_id))
129 repo_name=backend.repo_name, commit_id=commit_id))
130
130
131 # test DB
131 # test DB
132 assert ChangesetComment.query().count() == 1
132 assert ChangesetComment.query().count() == 1
133 assert_comment_links(response, 0, ChangesetComment.query().count())
133 assert_comment_links(response, 0, ChangesetComment.query().count())
134
134
135 if backend.alias == 'svn':
135 if backend.alias == 'svn':
136 response.mustcontain(
136 response.mustcontain(
137 '''data-f-path="vcs/commands/summary.py" '''
137 '''data-f-path="vcs/commands/summary.py" '''
138 '''data-anchor-id="c-300-ad05457a43f8"'''
138 '''data-anchor-id="c-300-ad05457a43f8"'''
139 )
139 )
140 if backend.alias == 'git':
140 if backend.alias == 'git':
141 response.mustcontain(
141 response.mustcontain(
142 '''data-f-path="vcs/backends/hg.py" '''
142 '''data-f-path="vcs/backends/hg.py" '''
143 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
143 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 )
144 )
145
145
146 if backend.alias == 'hg':
146 if backend.alias == 'hg':
147 response.mustcontain(
147 response.mustcontain(
148 '''data-f-path="vcs/backends/hg.py" '''
148 '''data-f-path="vcs/backends/hg.py" '''
149 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
149 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 )
150 )
151
151
152 assert Notification.query().count() == 1
152 assert Notification.query().count() == 1
153 assert ChangesetComment.query().count() == 1
153 assert ChangesetComment.query().count() == 1
154
154
155 notification = Notification.query().all()[0]
155 notification = Notification.query().all()[0]
156 comment = ChangesetComment.query().first()
156 comment = ChangesetComment.query().first()
157 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
157 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158
158
159 assert comment.revision == commit_id
159 assert comment.revision == commit_id
160
160
161 author = notification.created_by_user.username_and_name
161 author = notification.created_by_user.username_and_name
162 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
162 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
163 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
163 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164
164
165 assert sbj == notification.subject
165 assert sbj == notification.subject
166
166
167 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
167 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
168 backend.repo_name, commit_id, comment.comment_id))
168 backend.repo_name, commit_id, comment.comment_id))
169 assert lnk in notification.body
169 assert lnk in notification.body
170 assert 'on line n1' in notification.body
170 assert 'on line n1' in notification.body
171
171
172 def test_create_with_mention(self, backend):
172 def test_create_with_mention(self, backend):
173 self.log_user()
173 self.log_user()
174
174
175 commit_id = backend.repo.get_commit('300').raw_id
175 commit_id = backend.repo.get_commit('300').raw_id
176 text = u'@test_regular check CommentOnCommit'
176 text = u'@test_regular check CommentOnCommit'
177
177
178 params = {'text': text, 'csrf_token': self.csrf_token}
178 params = {'text': text, 'csrf_token': self.csrf_token}
179 self.app.post(
179 self.app.post(
180 route_path('repo_commit_comment_create',
180 route_path('repo_commit_comment_create',
181 repo_name=backend.repo_name, commit_id=commit_id),
181 repo_name=backend.repo_name, commit_id=commit_id),
182 params=params)
182 params=params)
183
183
184 response = self.app.get(
184 response = self.app.get(
185 route_path('repo_commit',
185 route_path('repo_commit',
186 repo_name=backend.repo_name, commit_id=commit_id))
186 repo_name=backend.repo_name, commit_id=commit_id))
187 # test DB
187 # test DB
188 assert ChangesetComment.query().count() == 1
188 assert ChangesetComment.query().count() == 1
189 assert_comment_links(response, ChangesetComment.query().count(), 0)
189 assert_comment_links(response, ChangesetComment.query().count(), 0)
190
190
191 notification = Notification.query().one()
191 notification = Notification.query().one()
192
192
193 assert len(notification.recipients) == 2
193 assert len(notification.recipients) == 2
194 users = [x.username for x in notification.recipients]
194 users = [x.username for x in notification.recipients]
195
195
196 # test_regular gets notification by @mention
196 # test_regular gets notification by @mention
197 assert sorted(users) == [u'test_admin', u'test_regular']
197 assert sorted(users) == [u'test_admin', u'test_regular']
198
198
199 def test_create_with_status_change(self, backend):
199 def test_create_with_status_change(self, backend):
200 self.log_user()
200 self.log_user()
201 commit = backend.repo.get_commit('300')
201 commit = backend.repo.get_commit('300')
202 commit_id = commit.raw_id
202 commit_id = commit.raw_id
203 text = u'CommentOnCommit'
203 text = u'CommentOnCommit'
204 f_path = 'vcs/web/simplevcs/views/repository.py'
204 f_path = 'vcs/web/simplevcs/views/repository.py'
205 line = 'n1'
205 line = 'n1'
206
206
207 params = {'text': text, 'changeset_status': 'approved',
207 params = {'text': text, 'changeset_status': 'approved',
208 'csrf_token': self.csrf_token}
208 'csrf_token': self.csrf_token}
209
209
210 self.app.post(
210 self.app.post(
211 route_path(
211 route_path(
212 'repo_commit_comment_create',
212 'repo_commit_comment_create',
213 repo_name=backend.repo_name, commit_id=commit_id),
213 repo_name=backend.repo_name, commit_id=commit_id),
214 params=params)
214 params=params)
215
215
216 response = self.app.get(
216 response = self.app.get(
217 route_path('repo_commit',
217 route_path('repo_commit',
218 repo_name=backend.repo_name, commit_id=commit_id))
218 repo_name=backend.repo_name, commit_id=commit_id))
219
219
220 # test DB
220 # test DB
221 assert ChangesetComment.query().count() == 1
221 assert ChangesetComment.query().count() == 1
222 assert_comment_links(response, ChangesetComment.query().count(), 0)
222 assert_comment_links(response, ChangesetComment.query().count(), 0)
223
223
224 assert Notification.query().count() == 1
224 assert Notification.query().count() == 1
225 assert ChangesetComment.query().count() == 1
225 assert ChangesetComment.query().count() == 1
226
226
227 notification = Notification.query().all()[0]
227 notification = Notification.query().all()[0]
228
228
229 comment_id = ChangesetComment.query().first().comment_id
229 comment_id = ChangesetComment.query().first().comment_id
230 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
230 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231
231
232 author = notification.created_by_user.username_and_name
232 author = notification.created_by_user.username_and_name
233 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
233 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
234 author, h.show_id(commit), backend.repo_name)
234 author, h.show_id(commit), backend.repo_name)
235 assert sbj == notification.subject
235 assert sbj == notification.subject
236
236
237 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
237 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
238 backend.repo_name, commit_id, comment_id))
238 backend.repo_name, commit_id, comment_id))
239 assert lnk in notification.body
239 assert lnk in notification.body
240
240
241 def test_delete(self, backend):
241 def test_delete(self, backend):
242 self.log_user()
242 self.log_user()
243 commit_id = backend.repo.get_commit('300').raw_id
243 commit_id = backend.repo.get_commit('300').raw_id
244 text = u'CommentOnCommit'
244 text = u'CommentOnCommit'
245
245
246 params = {'text': text, 'csrf_token': self.csrf_token}
246 params = {'text': text, 'csrf_token': self.csrf_token}
247 self.app.post(
247 self.app.post(
248 route_path(
248 route_path(
249 'repo_commit_comment_create',
249 'repo_commit_comment_create',
250 repo_name=backend.repo_name, commit_id=commit_id),
250 repo_name=backend.repo_name, commit_id=commit_id),
251 params=params)
251 params=params)
252
252
253 comments = ChangesetComment.query().all()
253 comments = ChangesetComment.query().all()
254 assert len(comments) == 1
254 assert len(comments) == 1
255 comment_id = comments[0].comment_id
255 comment_id = comments[0].comment_id
256
256
257 self.app.post(
257 self.app.post(
258 route_path('repo_commit_comment_delete',
258 route_path('repo_commit_comment_delete',
259 repo_name=backend.repo_name,
259 repo_name=backend.repo_name,
260 commit_id=commit_id,
260 commit_id=commit_id,
261 comment_id=comment_id),
261 comment_id=comment_id),
262 params={'csrf_token': self.csrf_token})
262 params={'csrf_token': self.csrf_token})
263
263
264 comments = ChangesetComment.query().all()
264 comments = ChangesetComment.query().all()
265 assert len(comments) == 0
265 assert len(comments) == 0
266
266
267 response = self.app.get(
267 response = self.app.get(
268 route_path('repo_commit',
268 route_path('repo_commit',
269 repo_name=backend.repo_name, commit_id=commit_id))
269 repo_name=backend.repo_name, commit_id=commit_id))
270 assert_comment_links(response, 0, 0)
270 assert_comment_links(response, 0, 0)
271
271
272 def test_edit(self, backend):
272 def test_edit(self, backend):
273 self.log_user()
273 self.log_user()
274 commit_id = backend.repo.get_commit('300').raw_id
274 commit_id = backend.repo.get_commit('300').raw_id
275 text = u'CommentOnCommit'
275 text = u'CommentOnCommit'
276
276
277 params = {'text': text, 'csrf_token': self.csrf_token}
277 params = {'text': text, 'csrf_token': self.csrf_token}
278 self.app.post(
278 self.app.post(
279 route_path(
279 route_path(
280 'repo_commit_comment_create',
280 'repo_commit_comment_create',
281 repo_name=backend.repo_name, commit_id=commit_id),
281 repo_name=backend.repo_name, commit_id=commit_id),
282 params=params)
282 params=params)
283
283
284 comments = ChangesetComment.query().all()
284 comments = ChangesetComment.query().all()
285 assert len(comments) == 1
285 assert len(comments) == 1
286 comment_id = comments[0].comment_id
286 comment_id = comments[0].comment_id
287 test_text = 'test_text'
287 test_text = 'test_text'
288 self.app.post(
288 self.app.post(
289 route_path(
289 route_path(
290 'repo_commit_comment_edit',
290 'repo_commit_comment_edit',
291 repo_name=backend.repo_name,
291 repo_name=backend.repo_name,
292 commit_id=commit_id,
292 commit_id=commit_id,
293 comment_id=comment_id,
293 comment_id=comment_id,
294 ),
294 ),
295 params={
295 params={
296 'csrf_token': self.csrf_token,
296 'csrf_token': self.csrf_token,
297 'text': test_text,
297 'text': test_text,
298 'version': '0',
298 'version': '0',
299 })
299 })
300
300
301 text_form_db = ChangesetComment.query().filter(
301 text_form_db = ChangesetComment.query().filter(
302 ChangesetComment.comment_id == comment_id).first().text
302 ChangesetComment.comment_id == comment_id).first().text
303 assert test_text == text_form_db
303 assert test_text == text_form_db
304
304
305 def test_edit_without_change(self, backend):
305 def test_edit_without_change(self, backend):
306 self.log_user()
306 self.log_user()
307 commit_id = backend.repo.get_commit('300').raw_id
307 commit_id = backend.repo.get_commit('300').raw_id
308 text = u'CommentOnCommit'
308 text = u'CommentOnCommit'
309
309
310 params = {'text': text, 'csrf_token': self.csrf_token}
310 params = {'text': text, 'csrf_token': self.csrf_token}
311 self.app.post(
311 self.app.post(
312 route_path(
312 route_path(
313 'repo_commit_comment_create',
313 'repo_commit_comment_create',
314 repo_name=backend.repo_name, commit_id=commit_id),
314 repo_name=backend.repo_name, commit_id=commit_id),
315 params=params)
315 params=params)
316
316
317 comments = ChangesetComment.query().all()
317 comments = ChangesetComment.query().all()
318 assert len(comments) == 1
318 assert len(comments) == 1
319 comment_id = comments[0].comment_id
319 comment_id = comments[0].comment_id
320
320
321 response = self.app.post(
321 response = self.app.post(
322 route_path(
322 route_path(
323 'repo_commit_comment_edit',
323 'repo_commit_comment_edit',
324 repo_name=backend.repo_name,
324 repo_name=backend.repo_name,
325 commit_id=commit_id,
325 commit_id=commit_id,
326 comment_id=comment_id,
326 comment_id=comment_id,
327 ),
327 ),
328 params={
328 params={
329 'csrf_token': self.csrf_token,
329 'csrf_token': self.csrf_token,
330 'text': text,
330 'text': text,
331 'version': '0',
331 'version': '0',
332 },
332 },
333 status=404,
333 status=404,
334 )
334 )
335 assert response.status_int == 404
335 assert response.status_int == 404
336
336
337 def test_edit_try_edit_already_edited(self, backend):
337 def test_edit_try_edit_already_edited(self, backend):
338 self.log_user()
338 self.log_user()
339 commit_id = backend.repo.get_commit('300').raw_id
339 commit_id = backend.repo.get_commit('300').raw_id
340 text = u'CommentOnCommit'
340 text = u'CommentOnCommit'
341
341
342 params = {'text': text, 'csrf_token': self.csrf_token}
342 params = {'text': text, 'csrf_token': self.csrf_token}
343 self.app.post(
343 self.app.post(
344 route_path(
344 route_path(
345 'repo_commit_comment_create',
345 'repo_commit_comment_create',
346 repo_name=backend.repo_name, commit_id=commit_id
346 repo_name=backend.repo_name, commit_id=commit_id
347 ),
347 ),
348 params=params,
348 params=params,
349 )
349 )
350
350
351 comments = ChangesetComment.query().all()
351 comments = ChangesetComment.query().all()
352 assert len(comments) == 1
352 assert len(comments) == 1
353 comment_id = comments[0].comment_id
353 comment_id = comments[0].comment_id
354 test_text = 'test_text'
354 test_text = 'test_text'
355 self.app.post(
355 self.app.post(
356 route_path(
356 route_path(
357 'repo_commit_comment_edit',
357 'repo_commit_comment_edit',
358 repo_name=backend.repo_name,
358 repo_name=backend.repo_name,
359 commit_id=commit_id,
359 commit_id=commit_id,
360 comment_id=comment_id,
360 comment_id=comment_id,
361 ),
361 ),
362 params={
362 params={
363 'csrf_token': self.csrf_token,
363 'csrf_token': self.csrf_token,
364 'text': test_text,
364 'text': test_text,
365 'version': '0',
365 'version': '0',
366 }
366 }
367 )
367 )
368 test_text_v2 = 'test_v2'
368 test_text_v2 = 'test_v2'
369 response = self.app.post(
369 response = self.app.post(
370 route_path(
370 route_path(
371 'repo_commit_comment_edit',
371 'repo_commit_comment_edit',
372 repo_name=backend.repo_name,
372 repo_name=backend.repo_name,
373 commit_id=commit_id,
373 commit_id=commit_id,
374 comment_id=comment_id,
374 comment_id=comment_id,
375 ),
375 ),
376 params={
376 params={
377 'csrf_token': self.csrf_token,
377 'csrf_token': self.csrf_token,
378 'text': test_text_v2,
378 'text': test_text_v2,
379 'version': '0',
379 'version': '0',
380 },
380 },
381 status=404,
381 status=409,
382 )
382 )
383 assert response.status_int == 404
383 assert response.status_int == 409
384
384
385 text_form_db = ChangesetComment.query().filter(
385 text_form_db = ChangesetComment.query().filter(
386 ChangesetComment.comment_id == comment_id).first().text
386 ChangesetComment.comment_id == comment_id).first().text
387
387
388 assert test_text == text_form_db
388 assert test_text == text_form_db
389 assert test_text_v2 != text_form_db
389 assert test_text_v2 != text_form_db
390
390
391 def test_edit_forbidden_for_immutable_comments(self, backend):
391 def test_edit_forbidden_for_immutable_comments(self, backend):
392 self.log_user()
392 self.log_user()
393 commit_id = backend.repo.get_commit('300').raw_id
393 commit_id = backend.repo.get_commit('300').raw_id
394 text = u'CommentOnCommit'
394 text = u'CommentOnCommit'
395
395
396 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
396 params = {'text': text, 'csrf_token': self.csrf_token, 'version': '0'}
397 self.app.post(
397 self.app.post(
398 route_path(
398 route_path(
399 'repo_commit_comment_create',
399 'repo_commit_comment_create',
400 repo_name=backend.repo_name,
400 repo_name=backend.repo_name,
401 commit_id=commit_id,
401 commit_id=commit_id,
402 ),
402 ),
403 params=params
403 params=params
404 )
404 )
405
405
406 comments = ChangesetComment.query().all()
406 comments = ChangesetComment.query().all()
407 assert len(comments) == 1
407 assert len(comments) == 1
408 comment_id = comments[0].comment_id
408 comment_id = comments[0].comment_id
409
409
410 comment = ChangesetComment.get(comment_id)
410 comment = ChangesetComment.get(comment_id)
411 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
411 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
412 Session().add(comment)
412 Session().add(comment)
413 Session().commit()
413 Session().commit()
414
414
415 response = self.app.post(
415 response = self.app.post(
416 route_path(
416 route_path(
417 'repo_commit_comment_edit',
417 'repo_commit_comment_edit',
418 repo_name=backend.repo_name,
418 repo_name=backend.repo_name,
419 commit_id=commit_id,
419 commit_id=commit_id,
420 comment_id=comment_id,
420 comment_id=comment_id,
421 ),
421 ),
422 params={
422 params={
423 'csrf_token': self.csrf_token,
423 'csrf_token': self.csrf_token,
424 'text': 'test_text',
424 'text': 'test_text',
425 },
425 },
426 status=403,
426 status=403,
427 )
427 )
428 assert response.status_int == 403
428 assert response.status_int == 403
429
429
430 def test_delete_forbidden_for_immutable_comments(self, backend):
430 def test_delete_forbidden_for_immutable_comments(self, backend):
431 self.log_user()
431 self.log_user()
432 commit_id = backend.repo.get_commit('300').raw_id
432 commit_id = backend.repo.get_commit('300').raw_id
433 text = u'CommentOnCommit'
433 text = u'CommentOnCommit'
434
434
435 params = {'text': text, 'csrf_token': self.csrf_token}
435 params = {'text': text, 'csrf_token': self.csrf_token}
436 self.app.post(
436 self.app.post(
437 route_path(
437 route_path(
438 'repo_commit_comment_create',
438 'repo_commit_comment_create',
439 repo_name=backend.repo_name, commit_id=commit_id),
439 repo_name=backend.repo_name, commit_id=commit_id),
440 params=params)
440 params=params)
441
441
442 comments = ChangesetComment.query().all()
442 comments = ChangesetComment.query().all()
443 assert len(comments) == 1
443 assert len(comments) == 1
444 comment_id = comments[0].comment_id
444 comment_id = comments[0].comment_id
445
445
446 comment = ChangesetComment.get(comment_id)
446 comment = ChangesetComment.get(comment_id)
447 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
447 comment.immutable_state = ChangesetComment.OP_IMMUTABLE
448 Session().add(comment)
448 Session().add(comment)
449 Session().commit()
449 Session().commit()
450
450
451 self.app.post(
451 self.app.post(
452 route_path('repo_commit_comment_delete',
452 route_path('repo_commit_comment_delete',
453 repo_name=backend.repo_name,
453 repo_name=backend.repo_name,
454 commit_id=commit_id,
454 commit_id=commit_id,
455 comment_id=comment_id),
455 comment_id=comment_id),
456 params={'csrf_token': self.csrf_token},
456 params={'csrf_token': self.csrf_token},
457 status=403)
457 status=403)
458
458
459 @pytest.mark.parametrize('renderer, text_input, output', [
459 @pytest.mark.parametrize('renderer, text_input, output', [
460 ('rst', 'plain text', '<p>plain text</p>'),
460 ('rst', 'plain text', '<p>plain text</p>'),
461 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
461 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
462 ('rst', '*italics*', '<em>italics</em>'),
462 ('rst', '*italics*', '<em>italics</em>'),
463 ('rst', '**bold**', '<strong>bold</strong>'),
463 ('rst', '**bold**', '<strong>bold</strong>'),
464 ('markdown', 'plain text', '<p>plain text</p>'),
464 ('markdown', 'plain text', '<p>plain text</p>'),
465 ('markdown', '# header', '<h1>header</h1>'),
465 ('markdown', '# header', '<h1>header</h1>'),
466 ('markdown', '*italics*', '<em>italics</em>'),
466 ('markdown', '*italics*', '<em>italics</em>'),
467 ('markdown', '**bold**', '<strong>bold</strong>'),
467 ('markdown', '**bold**', '<strong>bold</strong>'),
468 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
468 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
469 'md-header', 'md-italics', 'md-bold', ])
469 'md-header', 'md-italics', 'md-bold', ])
470 def test_preview(self, renderer, text_input, output, backend, xhr_header):
470 def test_preview(self, renderer, text_input, output, backend, xhr_header):
471 self.log_user()
471 self.log_user()
472 params = {
472 params = {
473 'renderer': renderer,
473 'renderer': renderer,
474 'text': text_input,
474 'text': text_input,
475 'csrf_token': self.csrf_token
475 'csrf_token': self.csrf_token
476 }
476 }
477 commit_id = '0' * 16 # fake this for tests
477 commit_id = '0' * 16 # fake this for tests
478 response = self.app.post(
478 response = self.app.post(
479 route_path('repo_commit_comment_preview',
479 route_path('repo_commit_comment_preview',
480 repo_name=backend.repo_name, commit_id=commit_id,),
480 repo_name=backend.repo_name, commit_id=commit_id,),
481 params=params,
481 params=params,
482 extra_environ=xhr_header)
482 extra_environ=xhr_header)
483
483
484 response.mustcontain(output)
484 response.mustcontain(output)
485
485
486
486
487 def assert_comment_links(response, comments, inline_comments):
487 def assert_comment_links(response, comments, inline_comments):
488 if comments == 1:
488 if comments == 1:
489 comments_text = "%d General" % comments
489 comments_text = "%d General" % comments
490 else:
490 else:
491 comments_text = "%d General" % comments
491 comments_text = "%d General" % comments
492
492
493 if inline_comments == 1:
493 if inline_comments == 1:
494 inline_comments_text = "%d Inline" % inline_comments
494 inline_comments_text = "%d Inline" % inline_comments
495 else:
495 else:
496 inline_comments_text = "%d Inline" % inline_comments
496 inline_comments_text = "%d Inline" % inline_comments
497
497
498 if comments:
498 if comments:
499 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
499 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
500 else:
500 else:
501 response.mustcontain(comments_text)
501 response.mustcontain(comments_text)
502
502
503 if inline_comments:
503 if inline_comments:
504 response.mustcontain(
504 response.mustcontain(
505 'id="inline-comments-counter">%s' % inline_comments_text)
505 'id="inline-comments-counter">%s' % inline_comments_text)
506 else:
506 else:
507 response.mustcontain(inline_comments_text)
507 response.mustcontain(inline_comments_text)
@@ -1,1427 +1,1426 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.tests import (
34 from rhodecode.tests import (
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36
36
37
37
38 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
39 import urllib
39 import urllib
40
40
41 base_url = {
41 base_url = {
42 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'repo_commits': '/{repo_name}/commits',
44 'repo_commits': '/{repo_name}/commits',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
59 }[name].format(**kwargs)
59 }[name].format(**kwargs)
60
60
61 if params:
61 if params:
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
63 return base_url
63 return base_url
64
64
65
65
66 @pytest.mark.usefixtures('app', 'autologin_user')
66 @pytest.mark.usefixtures('app', 'autologin_user')
67 @pytest.mark.backends("git", "hg")
67 @pytest.mark.backends("git", "hg")
68 class TestPullrequestsView(object):
68 class TestPullrequestsView(object):
69
69
70 def test_index(self, backend):
70 def test_index(self, backend):
71 self.app.get(route_path(
71 self.app.get(route_path(
72 'pullrequest_new',
72 'pullrequest_new',
73 repo_name=backend.repo_name))
73 repo_name=backend.repo_name))
74
74
75 def test_option_menu_create_pull_request_exists(self, backend):
75 def test_option_menu_create_pull_request_exists(self, backend):
76 repo_name = backend.repo_name
76 repo_name = backend.repo_name
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
78
78
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
80 'pullrequest_new', repo_name=repo_name)
80 'pullrequest_new', repo_name=repo_name)
81 response.mustcontain(create_pr_link)
81 response.mustcontain(create_pr_link)
82
82
83 def test_create_pr_form_with_raw_commit_id(self, backend):
83 def test_create_pr_form_with_raw_commit_id(self, backend):
84 repo = backend.repo
84 repo = backend.repo
85
85
86 self.app.get(
86 self.app.get(
87 route_path('pullrequest_new', repo_name=repo.repo_name,
87 route_path('pullrequest_new', repo_name=repo.repo_name,
88 commit=repo.get_commit().raw_id),
88 commit=repo.get_commit().raw_id),
89 status=200)
89 status=200)
90
90
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
94 pull_request = pr_util.create_pull_request(
94 pull_request = pr_util.create_pull_request(
95 mergeable=pr_merge_enabled, enable_notifications=False)
95 mergeable=pr_merge_enabled, enable_notifications=False)
96
96
97 response = self.app.get(route_path(
97 response = self.app.get(route_path(
98 'pullrequest_show',
98 'pullrequest_show',
99 repo_name=pull_request.target_repo.scm_instance().name,
99 repo_name=pull_request.target_repo.scm_instance().name,
100 pull_request_id=pull_request.pull_request_id,
100 pull_request_id=pull_request.pull_request_id,
101 params={'range-diff': range_diff}))
101 params={'range-diff': range_diff}))
102
102
103 for commit_id in pull_request.revisions:
103 for commit_id in pull_request.revisions:
104 response.mustcontain(commit_id)
104 response.mustcontain(commit_id)
105
105
106 response.mustcontain(pull_request.target_ref_parts.type)
106 response.mustcontain(pull_request.target_ref_parts.type)
107 response.mustcontain(pull_request.target_ref_parts.name)
107 response.mustcontain(pull_request.target_ref_parts.name)
108
108
109 response.mustcontain('class="pull-request-merge"')
109 response.mustcontain('class="pull-request-merge"')
110
110
111 if pr_merge_enabled:
111 if pr_merge_enabled:
112 response.mustcontain('Pull request reviewer approval is pending')
112 response.mustcontain('Pull request reviewer approval is pending')
113 else:
113 else:
114 response.mustcontain('Server-side pull request merging is disabled.')
114 response.mustcontain('Server-side pull request merging is disabled.')
115
115
116 if range_diff == "1":
116 if range_diff == "1":
117 response.mustcontain('Turn off: Show the diff as commit range')
117 response.mustcontain('Turn off: Show the diff as commit range')
118
118
119 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
119 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
120 # Logout
120 # Logout
121 response = self.app.post(
121 response = self.app.post(
122 h.route_path('logout'),
122 h.route_path('logout'),
123 params={'csrf_token': csrf_token})
123 params={'csrf_token': csrf_token})
124 # Login as regular user
124 # Login as regular user
125 response = self.app.post(h.route_path('login'),
125 response = self.app.post(h.route_path('login'),
126 {'username': TEST_USER_REGULAR_LOGIN,
126 {'username': TEST_USER_REGULAR_LOGIN,
127 'password': 'test12'})
127 'password': 'test12'})
128
128
129 pull_request = pr_util.create_pull_request(
129 pull_request = pr_util.create_pull_request(
130 author=TEST_USER_REGULAR_LOGIN)
130 author=TEST_USER_REGULAR_LOGIN)
131
131
132 response = self.app.get(route_path(
132 response = self.app.get(route_path(
133 'pullrequest_show',
133 'pullrequest_show',
134 repo_name=pull_request.target_repo.scm_instance().name,
134 repo_name=pull_request.target_repo.scm_instance().name,
135 pull_request_id=pull_request.pull_request_id))
135 pull_request_id=pull_request.pull_request_id))
136
136
137 response.mustcontain('Server-side pull request merging is disabled.')
137 response.mustcontain('Server-side pull request merging is disabled.')
138
138
139 assert_response = response.assert_response()
139 assert_response = response.assert_response()
140 # for regular user without a merge permissions, we don't see it
140 # for regular user without a merge permissions, we don't see it
141 assert_response.no_element_exists('#close-pull-request-action')
141 assert_response.no_element_exists('#close-pull-request-action')
142
142
143 user_util.grant_user_permission_to_repo(
143 user_util.grant_user_permission_to_repo(
144 pull_request.target_repo,
144 pull_request.target_repo,
145 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
145 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
146 'repository.write')
146 'repository.write')
147 response = self.app.get(route_path(
147 response = self.app.get(route_path(
148 'pullrequest_show',
148 'pullrequest_show',
149 repo_name=pull_request.target_repo.scm_instance().name,
149 repo_name=pull_request.target_repo.scm_instance().name,
150 pull_request_id=pull_request.pull_request_id))
150 pull_request_id=pull_request.pull_request_id))
151
151
152 response.mustcontain('Server-side pull request merging is disabled.')
152 response.mustcontain('Server-side pull request merging is disabled.')
153
153
154 assert_response = response.assert_response()
154 assert_response = response.assert_response()
155 # now regular user has a merge permissions, we have CLOSE button
155 # now regular user has a merge permissions, we have CLOSE button
156 assert_response.one_element_exists('#close-pull-request-action')
156 assert_response.one_element_exists('#close-pull-request-action')
157
157
158 def test_show_invalid_commit_id(self, pr_util):
158 def test_show_invalid_commit_id(self, pr_util):
159 # Simulating invalid revisions which will cause a lookup error
159 # Simulating invalid revisions which will cause a lookup error
160 pull_request = pr_util.create_pull_request()
160 pull_request = pr_util.create_pull_request()
161 pull_request.revisions = ['invalid']
161 pull_request.revisions = ['invalid']
162 Session().add(pull_request)
162 Session().add(pull_request)
163 Session().commit()
163 Session().commit()
164
164
165 response = self.app.get(route_path(
165 response = self.app.get(route_path(
166 'pullrequest_show',
166 'pullrequest_show',
167 repo_name=pull_request.target_repo.scm_instance().name,
167 repo_name=pull_request.target_repo.scm_instance().name,
168 pull_request_id=pull_request.pull_request_id))
168 pull_request_id=pull_request.pull_request_id))
169
169
170 for commit_id in pull_request.revisions:
170 for commit_id in pull_request.revisions:
171 response.mustcontain(commit_id)
171 response.mustcontain(commit_id)
172
172
173 def test_show_invalid_source_reference(self, pr_util):
173 def test_show_invalid_source_reference(self, pr_util):
174 pull_request = pr_util.create_pull_request()
174 pull_request = pr_util.create_pull_request()
175 pull_request.source_ref = 'branch:b:invalid'
175 pull_request.source_ref = 'branch:b:invalid'
176 Session().add(pull_request)
176 Session().add(pull_request)
177 Session().commit()
177 Session().commit()
178
178
179 self.app.get(route_path(
179 self.app.get(route_path(
180 'pullrequest_show',
180 'pullrequest_show',
181 repo_name=pull_request.target_repo.scm_instance().name,
181 repo_name=pull_request.target_repo.scm_instance().name,
182 pull_request_id=pull_request.pull_request_id))
182 pull_request_id=pull_request.pull_request_id))
183
183
184 def test_edit_title_description(self, pr_util, csrf_token):
184 def test_edit_title_description(self, pr_util, csrf_token):
185 pull_request = pr_util.create_pull_request()
185 pull_request = pr_util.create_pull_request()
186 pull_request_id = pull_request.pull_request_id
186 pull_request_id = pull_request.pull_request_id
187
187
188 response = self.app.post(
188 response = self.app.post(
189 route_path('pullrequest_update',
189 route_path('pullrequest_update',
190 repo_name=pull_request.target_repo.repo_name,
190 repo_name=pull_request.target_repo.repo_name,
191 pull_request_id=pull_request_id),
191 pull_request_id=pull_request_id),
192 params={
192 params={
193 'edit_pull_request': 'true',
193 'edit_pull_request': 'true',
194 'title': 'New title',
194 'title': 'New title',
195 'description': 'New description',
195 'description': 'New description',
196 'csrf_token': csrf_token})
196 'csrf_token': csrf_token})
197
197
198 assert_session_flash(
198 assert_session_flash(
199 response, u'Pull request title & description updated.',
199 response, u'Pull request title & description updated.',
200 category='success')
200 category='success')
201
201
202 pull_request = PullRequest.get(pull_request_id)
202 pull_request = PullRequest.get(pull_request_id)
203 assert pull_request.title == 'New title'
203 assert pull_request.title == 'New title'
204 assert pull_request.description == 'New description'
204 assert pull_request.description == 'New description'
205
205
206 def test_edit_title_description_closed(self, pr_util, csrf_token):
206 def test_edit_title_description_closed(self, pr_util, csrf_token):
207 pull_request = pr_util.create_pull_request()
207 pull_request = pr_util.create_pull_request()
208 pull_request_id = pull_request.pull_request_id
208 pull_request_id = pull_request.pull_request_id
209 repo_name = pull_request.target_repo.repo_name
209 repo_name = pull_request.target_repo.repo_name
210 pr_util.close()
210 pr_util.close()
211
211
212 response = self.app.post(
212 response = self.app.post(
213 route_path('pullrequest_update',
213 route_path('pullrequest_update',
214 repo_name=repo_name, pull_request_id=pull_request_id),
214 repo_name=repo_name, pull_request_id=pull_request_id),
215 params={
215 params={
216 'edit_pull_request': 'true',
216 'edit_pull_request': 'true',
217 'title': 'New title',
217 'title': 'New title',
218 'description': 'New description',
218 'description': 'New description',
219 'csrf_token': csrf_token}, status=200)
219 'csrf_token': csrf_token}, status=200)
220 assert_session_flash(
220 assert_session_flash(
221 response, u'Cannot update closed pull requests.',
221 response, u'Cannot update closed pull requests.',
222 category='error')
222 category='error')
223
223
224 def test_update_invalid_source_reference(self, pr_util, csrf_token):
224 def test_update_invalid_source_reference(self, pr_util, csrf_token):
225 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
225 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
226
226
227 pull_request = pr_util.create_pull_request()
227 pull_request = pr_util.create_pull_request()
228 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
228 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
229 Session().add(pull_request)
229 Session().add(pull_request)
230 Session().commit()
230 Session().commit()
231
231
232 pull_request_id = pull_request.pull_request_id
232 pull_request_id = pull_request.pull_request_id
233
233
234 response = self.app.post(
234 response = self.app.post(
235 route_path('pullrequest_update',
235 route_path('pullrequest_update',
236 repo_name=pull_request.target_repo.repo_name,
236 repo_name=pull_request.target_repo.repo_name,
237 pull_request_id=pull_request_id),
237 pull_request_id=pull_request_id),
238 params={'update_commits': 'true', 'csrf_token': csrf_token})
238 params={'update_commits': 'true', 'csrf_token': csrf_token})
239
239
240 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
240 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
241 UpdateFailureReason.MISSING_SOURCE_REF])
241 UpdateFailureReason.MISSING_SOURCE_REF])
242 assert_session_flash(response, expected_msg, category='error')
242 assert_session_flash(response, expected_msg, category='error')
243
243
244 def test_missing_target_reference(self, pr_util, csrf_token):
244 def test_missing_target_reference(self, pr_util, csrf_token):
245 from rhodecode.lib.vcs.backends.base import MergeFailureReason
245 from rhodecode.lib.vcs.backends.base import MergeFailureReason
246 pull_request = pr_util.create_pull_request(
246 pull_request = pr_util.create_pull_request(
247 approved=True, mergeable=True)
247 approved=True, mergeable=True)
248 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
248 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
249 pull_request.target_ref = unicode_reference
249 pull_request.target_ref = unicode_reference
250 Session().add(pull_request)
250 Session().add(pull_request)
251 Session().commit()
251 Session().commit()
252
252
253 pull_request_id = pull_request.pull_request_id
253 pull_request_id = pull_request.pull_request_id
254 pull_request_url = route_path(
254 pull_request_url = route_path(
255 'pullrequest_show',
255 'pullrequest_show',
256 repo_name=pull_request.target_repo.repo_name,
256 repo_name=pull_request.target_repo.repo_name,
257 pull_request_id=pull_request_id)
257 pull_request_id=pull_request_id)
258
258
259 response = self.app.get(pull_request_url)
259 response = self.app.get(pull_request_url)
260 target_ref_id = 'invalid-branch'
260 target_ref_id = 'invalid-branch'
261 merge_resp = MergeResponse(
261 merge_resp = MergeResponse(
262 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
262 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
263 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
263 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
264 response.assert_response().element_contains(
264 response.assert_response().element_contains(
265 'div[data-role="merge-message"]', merge_resp.merge_status_message)
265 'div[data-role="merge-message"]', merge_resp.merge_status_message)
266
266
267 def test_comment_and_close_pull_request_custom_message_approved(
267 def test_comment_and_close_pull_request_custom_message_approved(
268 self, pr_util, csrf_token, xhr_header):
268 self, pr_util, csrf_token, xhr_header):
269
269
270 pull_request = pr_util.create_pull_request(approved=True)
270 pull_request = pr_util.create_pull_request(approved=True)
271 pull_request_id = pull_request.pull_request_id
271 pull_request_id = pull_request.pull_request_id
272 author = pull_request.user_id
272 author = pull_request.user_id
273 repo = pull_request.target_repo.repo_id
273 repo = pull_request.target_repo.repo_id
274
274
275 self.app.post(
275 self.app.post(
276 route_path('pullrequest_comment_create',
276 route_path('pullrequest_comment_create',
277 repo_name=pull_request.target_repo.scm_instance().name,
277 repo_name=pull_request.target_repo.scm_instance().name,
278 pull_request_id=pull_request_id),
278 pull_request_id=pull_request_id),
279 params={
279 params={
280 'close_pull_request': '1',
280 'close_pull_request': '1',
281 'text': 'Closing a PR',
281 'text': 'Closing a PR',
282 'csrf_token': csrf_token},
282 'csrf_token': csrf_token},
283 extra_environ=xhr_header,)
283 extra_environ=xhr_header,)
284
284
285 journal = UserLog.query()\
285 journal = UserLog.query()\
286 .filter(UserLog.user_id == author)\
286 .filter(UserLog.user_id == author)\
287 .filter(UserLog.repository_id == repo) \
287 .filter(UserLog.repository_id == repo) \
288 .order_by(UserLog.user_log_id.asc()) \
288 .order_by(UserLog.user_log_id.asc()) \
289 .all()
289 .all()
290 assert journal[-1].action == 'repo.pull_request.close'
290 assert journal[-1].action == 'repo.pull_request.close'
291
291
292 pull_request = PullRequest.get(pull_request_id)
292 pull_request = PullRequest.get(pull_request_id)
293 assert pull_request.is_closed()
293 assert pull_request.is_closed()
294
294
295 status = ChangesetStatusModel().get_status(
295 status = ChangesetStatusModel().get_status(
296 pull_request.source_repo, pull_request=pull_request)
296 pull_request.source_repo, pull_request=pull_request)
297 assert status == ChangesetStatus.STATUS_APPROVED
297 assert status == ChangesetStatus.STATUS_APPROVED
298 comments = ChangesetComment().query() \
298 comments = ChangesetComment().query() \
299 .filter(ChangesetComment.pull_request == pull_request) \
299 .filter(ChangesetComment.pull_request == pull_request) \
300 .order_by(ChangesetComment.comment_id.asc())\
300 .order_by(ChangesetComment.comment_id.asc())\
301 .all()
301 .all()
302 assert comments[-1].text == 'Closing a PR'
302 assert comments[-1].text == 'Closing a PR'
303
303
304 def test_comment_force_close_pull_request_rejected(
304 def test_comment_force_close_pull_request_rejected(
305 self, pr_util, csrf_token, xhr_header):
305 self, pr_util, csrf_token, xhr_header):
306 pull_request = pr_util.create_pull_request()
306 pull_request = pr_util.create_pull_request()
307 pull_request_id = pull_request.pull_request_id
307 pull_request_id = pull_request.pull_request_id
308 PullRequestModel().update_reviewers(
308 PullRequestModel().update_reviewers(
309 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
309 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
310 pull_request.author)
310 pull_request.author)
311 author = pull_request.user_id
311 author = pull_request.user_id
312 repo = pull_request.target_repo.repo_id
312 repo = pull_request.target_repo.repo_id
313
313
314 self.app.post(
314 self.app.post(
315 route_path('pullrequest_comment_create',
315 route_path('pullrequest_comment_create',
316 repo_name=pull_request.target_repo.scm_instance().name,
316 repo_name=pull_request.target_repo.scm_instance().name,
317 pull_request_id=pull_request_id),
317 pull_request_id=pull_request_id),
318 params={
318 params={
319 'close_pull_request': '1',
319 'close_pull_request': '1',
320 'csrf_token': csrf_token},
320 'csrf_token': csrf_token},
321 extra_environ=xhr_header)
321 extra_environ=xhr_header)
322
322
323 pull_request = PullRequest.get(pull_request_id)
323 pull_request = PullRequest.get(pull_request_id)
324
324
325 journal = UserLog.query()\
325 journal = UserLog.query()\
326 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
326 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
327 .order_by(UserLog.user_log_id.asc()) \
327 .order_by(UserLog.user_log_id.asc()) \
328 .all()
328 .all()
329 assert journal[-1].action == 'repo.pull_request.close'
329 assert journal[-1].action == 'repo.pull_request.close'
330
330
331 # check only the latest status, not the review status
331 # check only the latest status, not the review status
332 status = ChangesetStatusModel().get_status(
332 status = ChangesetStatusModel().get_status(
333 pull_request.source_repo, pull_request=pull_request)
333 pull_request.source_repo, pull_request=pull_request)
334 assert status == ChangesetStatus.STATUS_REJECTED
334 assert status == ChangesetStatus.STATUS_REJECTED
335
335
336 def test_comment_and_close_pull_request(
336 def test_comment_and_close_pull_request(
337 self, pr_util, csrf_token, xhr_header):
337 self, pr_util, csrf_token, xhr_header):
338 pull_request = pr_util.create_pull_request()
338 pull_request = pr_util.create_pull_request()
339 pull_request_id = pull_request.pull_request_id
339 pull_request_id = pull_request.pull_request_id
340
340
341 response = self.app.post(
341 response = self.app.post(
342 route_path('pullrequest_comment_create',
342 route_path('pullrequest_comment_create',
343 repo_name=pull_request.target_repo.scm_instance().name,
343 repo_name=pull_request.target_repo.scm_instance().name,
344 pull_request_id=pull_request.pull_request_id),
344 pull_request_id=pull_request.pull_request_id),
345 params={
345 params={
346 'close_pull_request': 'true',
346 'close_pull_request': 'true',
347 'csrf_token': csrf_token},
347 'csrf_token': csrf_token},
348 extra_environ=xhr_header)
348 extra_environ=xhr_header)
349
349
350 assert response.json
350 assert response.json
351
351
352 pull_request = PullRequest.get(pull_request_id)
352 pull_request = PullRequest.get(pull_request_id)
353 assert pull_request.is_closed()
353 assert pull_request.is_closed()
354
354
355 # check only the latest status, not the review status
355 # check only the latest status, not the review status
356 status = ChangesetStatusModel().get_status(
356 status = ChangesetStatusModel().get_status(
357 pull_request.source_repo, pull_request=pull_request)
357 pull_request.source_repo, pull_request=pull_request)
358 assert status == ChangesetStatus.STATUS_REJECTED
358 assert status == ChangesetStatus.STATUS_REJECTED
359
359
360 def test_comment_and_close_pull_request_try_edit_comment(
360 def test_comment_and_close_pull_request_try_edit_comment(
361 self, pr_util, csrf_token, xhr_header
361 self, pr_util, csrf_token, xhr_header
362 ):
362 ):
363 pull_request = pr_util.create_pull_request()
363 pull_request = pr_util.create_pull_request()
364 pull_request_id = pull_request.pull_request_id
364 pull_request_id = pull_request.pull_request_id
365
365
366 response = self.app.post(
366 response = self.app.post(
367 route_path(
367 route_path(
368 'pullrequest_comment_create',
368 'pullrequest_comment_create',
369 repo_name=pull_request.target_repo.scm_instance().name,
369 repo_name=pull_request.target_repo.scm_instance().name,
370 pull_request_id=pull_request.pull_request_id,
370 pull_request_id=pull_request.pull_request_id,
371 ),
371 ),
372 params={
372 params={
373 'close_pull_request': 'true',
373 'close_pull_request': 'true',
374 'csrf_token': csrf_token,
374 'csrf_token': csrf_token,
375 },
375 },
376 extra_environ=xhr_header)
376 extra_environ=xhr_header)
377
377
378 assert response.json
378 assert response.json
379
379
380 pull_request = PullRequest.get(pull_request_id)
380 pull_request = PullRequest.get(pull_request_id)
381 assert pull_request.is_closed()
381 assert pull_request.is_closed()
382
382
383 # check only the latest status, not the review status
383 # check only the latest status, not the review status
384 status = ChangesetStatusModel().get_status(
384 status = ChangesetStatusModel().get_status(
385 pull_request.source_repo, pull_request=pull_request)
385 pull_request.source_repo, pull_request=pull_request)
386 assert status == ChangesetStatus.STATUS_REJECTED
386 assert status == ChangesetStatus.STATUS_REJECTED
387
387
388 comment_id = response.json.get('comment_id', None)
388 comment_id = response.json.get('comment_id', None)
389 test_text = 'test'
389 test_text = 'test'
390 response = self.app.post(
390 response = self.app.post(
391 route_path(
391 route_path(
392 'pullrequest_comment_edit',
392 'pullrequest_comment_edit',
393 repo_name=pull_request.target_repo.scm_instance().name,
393 repo_name=pull_request.target_repo.scm_instance().name,
394 pull_request_id=pull_request.pull_request_id,
394 pull_request_id=pull_request.pull_request_id,
395 comment_id=comment_id,
395 comment_id=comment_id,
396 ),
396 ),
397 extra_environ=xhr_header,
397 extra_environ=xhr_header,
398 params={
398 params={
399 'csrf_token': csrf_token,
399 'csrf_token': csrf_token,
400 'text': test_text,
400 'text': test_text,
401 },
401 },
402 status=403,
402 status=403,
403 )
403 )
404 assert response.status_int == 403
404 assert response.status_int == 403
405
405
406 def test_comment_and_comment_edit(
406 def test_comment_and_comment_edit(
407 self, pr_util, csrf_token, xhr_header
407 self, pr_util, csrf_token, xhr_header
408 ):
408 ):
409 pull_request = pr_util.create_pull_request()
409 pull_request = pr_util.create_pull_request()
410 response = self.app.post(
410 response = self.app.post(
411 route_path(
411 route_path(
412 'pullrequest_comment_create',
412 'pullrequest_comment_create',
413 repo_name=pull_request.target_repo.scm_instance().name,
413 repo_name=pull_request.target_repo.scm_instance().name,
414 pull_request_id=pull_request.pull_request_id),
414 pull_request_id=pull_request.pull_request_id),
415 params={
415 params={
416 'csrf_token': csrf_token,
416 'csrf_token': csrf_token,
417 'text': 'init',
417 'text': 'init',
418 },
418 },
419 extra_environ=xhr_header,
419 extra_environ=xhr_header,
420 )
420 )
421 assert response.json
421 assert response.json
422
422
423 comment_id = response.json.get('comment_id', None)
423 comment_id = response.json.get('comment_id', None)
424 assert comment_id
424 assert comment_id
425 test_text = 'test'
425 test_text = 'test'
426 self.app.post(
426 self.app.post(
427 route_path(
427 route_path(
428 'pullrequest_comment_edit',
428 'pullrequest_comment_edit',
429 repo_name=pull_request.target_repo.scm_instance().name,
429 repo_name=pull_request.target_repo.scm_instance().name,
430 pull_request_id=pull_request.pull_request_id,
430 pull_request_id=pull_request.pull_request_id,
431 comment_id=comment_id,
431 comment_id=comment_id,
432 ),
432 ),
433 extra_environ=xhr_header,
433 extra_environ=xhr_header,
434 params={
434 params={
435 'csrf_token': csrf_token,
435 'csrf_token': csrf_token,
436 'text': test_text,
436 'text': test_text,
437 'version': '0',
437 'version': '0',
438 },
438 },
439
439
440 )
440 )
441 text_form_db = ChangesetComment.query().filter(
441 text_form_db = ChangesetComment.query().filter(
442 ChangesetComment.comment_id == comment_id).first().text
442 ChangesetComment.comment_id == comment_id).first().text
443 assert test_text == text_form_db
443 assert test_text == text_form_db
444
444
445 def test_comment_and_comment_edit(
445 def test_comment_and_comment_edit(
446 self, pr_util, csrf_token, xhr_header
446 self, pr_util, csrf_token, xhr_header
447 ):
447 ):
448 pull_request = pr_util.create_pull_request()
448 pull_request = pr_util.create_pull_request()
449 response = self.app.post(
449 response = self.app.post(
450 route_path(
450 route_path(
451 'pullrequest_comment_create',
451 'pullrequest_comment_create',
452 repo_name=pull_request.target_repo.scm_instance().name,
452 repo_name=pull_request.target_repo.scm_instance().name,
453 pull_request_id=pull_request.pull_request_id),
453 pull_request_id=pull_request.pull_request_id),
454 params={
454 params={
455 'csrf_token': csrf_token,
455 'csrf_token': csrf_token,
456 'text': 'init',
456 'text': 'init',
457 },
457 },
458 extra_environ=xhr_header,
458 extra_environ=xhr_header,
459 )
459 )
460 assert response.json
460 assert response.json
461
461
462 comment_id = response.json.get('comment_id', None)
462 comment_id = response.json.get('comment_id', None)
463 assert comment_id
463 assert comment_id
464 test_text = 'init'
464 test_text = 'init'
465 response = self.app.post(
465 response = self.app.post(
466 route_path(
466 route_path(
467 'pullrequest_comment_edit',
467 'pullrequest_comment_edit',
468 repo_name=pull_request.target_repo.scm_instance().name,
468 repo_name=pull_request.target_repo.scm_instance().name,
469 pull_request_id=pull_request.pull_request_id,
469 pull_request_id=pull_request.pull_request_id,
470 comment_id=comment_id,
470 comment_id=comment_id,
471 ),
471 ),
472 extra_environ=xhr_header,
472 extra_environ=xhr_header,
473 params={
473 params={
474 'csrf_token': csrf_token,
474 'csrf_token': csrf_token,
475 'text': test_text,
475 'text': test_text,
476 'version': '0',
476 'version': '0',
477 },
477 },
478 status=404,
478 status=404,
479
479
480 )
480 )
481 assert response.status_int == 404
481 assert response.status_int == 404
482
482
483 def test_comment_and_try_edit_already_edited(
483 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
484 self, pr_util, csrf_token, xhr_header
485 ):
486 pull_request = pr_util.create_pull_request()
484 pull_request = pr_util.create_pull_request()
487 response = self.app.post(
485 response = self.app.post(
488 route_path(
486 route_path(
489 'pullrequest_comment_create',
487 'pullrequest_comment_create',
490 repo_name=pull_request.target_repo.scm_instance().name,
488 repo_name=pull_request.target_repo.scm_instance().name,
491 pull_request_id=pull_request.pull_request_id),
489 pull_request_id=pull_request.pull_request_id),
492 params={
490 params={
493 'csrf_token': csrf_token,
491 'csrf_token': csrf_token,
494 'text': 'init',
492 'text': 'init',
495 },
493 },
496 extra_environ=xhr_header,
494 extra_environ=xhr_header,
497 )
495 )
498 assert response.json
496 assert response.json
499 comment_id = response.json.get('comment_id', None)
497 comment_id = response.json.get('comment_id', None)
500 assert comment_id
498 assert comment_id
499
501 test_text = 'test'
500 test_text = 'test'
502 response = self.app.post(
501 self.app.post(
503 route_path(
502 route_path(
504 'pullrequest_comment_edit',
503 'pullrequest_comment_edit',
505 repo_name=pull_request.target_repo.scm_instance().name,
504 repo_name=pull_request.target_repo.scm_instance().name,
506 pull_request_id=pull_request.pull_request_id,
505 pull_request_id=pull_request.pull_request_id,
507 comment_id=comment_id,
506 comment_id=comment_id,
508 ),
507 ),
509 extra_environ=xhr_header,
508 extra_environ=xhr_header,
510 params={
509 params={
511 'csrf_token': csrf_token,
510 'csrf_token': csrf_token,
512 'text': test_text,
511 'text': test_text,
513 'version': '0',
512 'version': '0',
514 },
513 },
515
514
516 )
515 )
517 test_text_v2 = 'test_v2'
516 test_text_v2 = 'test_v2'
518 response = self.app.post(
517 response = self.app.post(
519 route_path(
518 route_path(
520 'pullrequest_comment_edit',
519 'pullrequest_comment_edit',
521 repo_name=pull_request.target_repo.scm_instance().name,
520 repo_name=pull_request.target_repo.scm_instance().name,
522 pull_request_id=pull_request.pull_request_id,
521 pull_request_id=pull_request.pull_request_id,
523 comment_id=comment_id,
522 comment_id=comment_id,
524 ),
523 ),
525 extra_environ=xhr_header,
524 extra_environ=xhr_header,
526 params={
525 params={
527 'csrf_token': csrf_token,
526 'csrf_token': csrf_token,
528 'text': test_text_v2,
527 'text': test_text_v2,
529 'version': '0',
528 'version': '0',
530 },
529 },
531 status=404,
530 status=409,
532 )
531 )
533 assert response.status_int == 404
532 assert response.status_int == 409
534
533
535 text_form_db = ChangesetComment.query().filter(
534 text_form_db = ChangesetComment.query().filter(
536 ChangesetComment.comment_id == comment_id).first().text
535 ChangesetComment.comment_id == comment_id).first().text
537
536
538 assert test_text == text_form_db
537 assert test_text == text_form_db
539 assert test_text_v2 != text_form_db
538 assert test_text_v2 != text_form_db
540
539
541 def test_comment_and_comment_edit_permissions_forbidden(
540 def test_comment_and_comment_edit_permissions_forbidden(
542 self, autologin_regular_user, user_regular, user_admin, pr_util,
541 self, autologin_regular_user, user_regular, user_admin, pr_util,
543 csrf_token, xhr_header):
542 csrf_token, xhr_header):
544 pull_request = pr_util.create_pull_request(
543 pull_request = pr_util.create_pull_request(
545 author=user_admin.username, enable_notifications=False)
544 author=user_admin.username, enable_notifications=False)
546 comment = CommentsModel().create(
545 comment = CommentsModel().create(
547 text='test',
546 text='test',
548 repo=pull_request.target_repo.scm_instance().name,
547 repo=pull_request.target_repo.scm_instance().name,
549 user=user_admin,
548 user=user_admin,
550 pull_request=pull_request,
549 pull_request=pull_request,
551 )
550 )
552 response = self.app.post(
551 response = self.app.post(
553 route_path(
552 route_path(
554 'pullrequest_comment_edit',
553 'pullrequest_comment_edit',
555 repo_name=pull_request.target_repo.scm_instance().name,
554 repo_name=pull_request.target_repo.scm_instance().name,
556 pull_request_id=pull_request.pull_request_id,
555 pull_request_id=pull_request.pull_request_id,
557 comment_id=comment.comment_id,
556 comment_id=comment.comment_id,
558 ),
557 ),
559 extra_environ=xhr_header,
558 extra_environ=xhr_header,
560 params={
559 params={
561 'csrf_token': csrf_token,
560 'csrf_token': csrf_token,
562 'text': 'test_text',
561 'text': 'test_text',
563 },
562 },
564 status=403,
563 status=403,
565 )
564 )
566 assert response.status_int == 403
565 assert response.status_int == 403
567
566
568 def test_create_pull_request(self, backend, csrf_token):
567 def test_create_pull_request(self, backend, csrf_token):
569 commits = [
568 commits = [
570 {'message': 'ancestor'},
569 {'message': 'ancestor'},
571 {'message': 'change'},
570 {'message': 'change'},
572 {'message': 'change2'},
571 {'message': 'change2'},
573 ]
572 ]
574 commit_ids = backend.create_master_repo(commits)
573 commit_ids = backend.create_master_repo(commits)
575 target = backend.create_repo(heads=['ancestor'])
574 target = backend.create_repo(heads=['ancestor'])
576 source = backend.create_repo(heads=['change2'])
575 source = backend.create_repo(heads=['change2'])
577
576
578 response = self.app.post(
577 response = self.app.post(
579 route_path('pullrequest_create', repo_name=source.repo_name),
578 route_path('pullrequest_create', repo_name=source.repo_name),
580 [
579 [
581 ('source_repo', source.repo_name),
580 ('source_repo', source.repo_name),
582 ('source_ref', 'branch:default:' + commit_ids['change2']),
581 ('source_ref', 'branch:default:' + commit_ids['change2']),
583 ('target_repo', target.repo_name),
582 ('target_repo', target.repo_name),
584 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
583 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
585 ('common_ancestor', commit_ids['ancestor']),
584 ('common_ancestor', commit_ids['ancestor']),
586 ('pullrequest_title', 'Title'),
585 ('pullrequest_title', 'Title'),
587 ('pullrequest_desc', 'Description'),
586 ('pullrequest_desc', 'Description'),
588 ('description_renderer', 'markdown'),
587 ('description_renderer', 'markdown'),
589 ('__start__', 'review_members:sequence'),
588 ('__start__', 'review_members:sequence'),
590 ('__start__', 'reviewer:mapping'),
589 ('__start__', 'reviewer:mapping'),
591 ('user_id', '1'),
590 ('user_id', '1'),
592 ('__start__', 'reasons:sequence'),
591 ('__start__', 'reasons:sequence'),
593 ('reason', 'Some reason'),
592 ('reason', 'Some reason'),
594 ('__end__', 'reasons:sequence'),
593 ('__end__', 'reasons:sequence'),
595 ('__start__', 'rules:sequence'),
594 ('__start__', 'rules:sequence'),
596 ('__end__', 'rules:sequence'),
595 ('__end__', 'rules:sequence'),
597 ('mandatory', 'False'),
596 ('mandatory', 'False'),
598 ('__end__', 'reviewer:mapping'),
597 ('__end__', 'reviewer:mapping'),
599 ('__end__', 'review_members:sequence'),
598 ('__end__', 'review_members:sequence'),
600 ('__start__', 'revisions:sequence'),
599 ('__start__', 'revisions:sequence'),
601 ('revisions', commit_ids['change']),
600 ('revisions', commit_ids['change']),
602 ('revisions', commit_ids['change2']),
601 ('revisions', commit_ids['change2']),
603 ('__end__', 'revisions:sequence'),
602 ('__end__', 'revisions:sequence'),
604 ('user', ''),
603 ('user', ''),
605 ('csrf_token', csrf_token),
604 ('csrf_token', csrf_token),
606 ],
605 ],
607 status=302)
606 status=302)
608
607
609 location = response.headers['Location']
608 location = response.headers['Location']
610 pull_request_id = location.rsplit('/', 1)[1]
609 pull_request_id = location.rsplit('/', 1)[1]
611 assert pull_request_id != 'new'
610 assert pull_request_id != 'new'
612 pull_request = PullRequest.get(int(pull_request_id))
611 pull_request = PullRequest.get(int(pull_request_id))
613
612
614 # check that we have now both revisions
613 # check that we have now both revisions
615 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
614 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
616 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
615 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
617 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
616 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
618 assert pull_request.target_ref == expected_target_ref
617 assert pull_request.target_ref == expected_target_ref
619
618
620 def test_reviewer_notifications(self, backend, csrf_token):
619 def test_reviewer_notifications(self, backend, csrf_token):
621 # We have to use the app.post for this test so it will create the
620 # We have to use the app.post for this test so it will create the
622 # notifications properly with the new PR
621 # notifications properly with the new PR
623 commits = [
622 commits = [
624 {'message': 'ancestor',
623 {'message': 'ancestor',
625 'added': [FileNode('file_A', content='content_of_ancestor')]},
624 'added': [FileNode('file_A', content='content_of_ancestor')]},
626 {'message': 'change',
625 {'message': 'change',
627 'added': [FileNode('file_a', content='content_of_change')]},
626 'added': [FileNode('file_a', content='content_of_change')]},
628 {'message': 'change-child'},
627 {'message': 'change-child'},
629 {'message': 'ancestor-child', 'parents': ['ancestor'],
628 {'message': 'ancestor-child', 'parents': ['ancestor'],
630 'added': [
629 'added': [
631 FileNode('file_B', content='content_of_ancestor_child')]},
630 FileNode('file_B', content='content_of_ancestor_child')]},
632 {'message': 'ancestor-child-2'},
631 {'message': 'ancestor-child-2'},
633 ]
632 ]
634 commit_ids = backend.create_master_repo(commits)
633 commit_ids = backend.create_master_repo(commits)
635 target = backend.create_repo(heads=['ancestor-child'])
634 target = backend.create_repo(heads=['ancestor-child'])
636 source = backend.create_repo(heads=['change'])
635 source = backend.create_repo(heads=['change'])
637
636
638 response = self.app.post(
637 response = self.app.post(
639 route_path('pullrequest_create', repo_name=source.repo_name),
638 route_path('pullrequest_create', repo_name=source.repo_name),
640 [
639 [
641 ('source_repo', source.repo_name),
640 ('source_repo', source.repo_name),
642 ('source_ref', 'branch:default:' + commit_ids['change']),
641 ('source_ref', 'branch:default:' + commit_ids['change']),
643 ('target_repo', target.repo_name),
642 ('target_repo', target.repo_name),
644 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
643 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
645 ('common_ancestor', commit_ids['ancestor']),
644 ('common_ancestor', commit_ids['ancestor']),
646 ('pullrequest_title', 'Title'),
645 ('pullrequest_title', 'Title'),
647 ('pullrequest_desc', 'Description'),
646 ('pullrequest_desc', 'Description'),
648 ('description_renderer', 'markdown'),
647 ('description_renderer', 'markdown'),
649 ('__start__', 'review_members:sequence'),
648 ('__start__', 'review_members:sequence'),
650 ('__start__', 'reviewer:mapping'),
649 ('__start__', 'reviewer:mapping'),
651 ('user_id', '2'),
650 ('user_id', '2'),
652 ('__start__', 'reasons:sequence'),
651 ('__start__', 'reasons:sequence'),
653 ('reason', 'Some reason'),
652 ('reason', 'Some reason'),
654 ('__end__', 'reasons:sequence'),
653 ('__end__', 'reasons:sequence'),
655 ('__start__', 'rules:sequence'),
654 ('__start__', 'rules:sequence'),
656 ('__end__', 'rules:sequence'),
655 ('__end__', 'rules:sequence'),
657 ('mandatory', 'False'),
656 ('mandatory', 'False'),
658 ('__end__', 'reviewer:mapping'),
657 ('__end__', 'reviewer:mapping'),
659 ('__end__', 'review_members:sequence'),
658 ('__end__', 'review_members:sequence'),
660 ('__start__', 'revisions:sequence'),
659 ('__start__', 'revisions:sequence'),
661 ('revisions', commit_ids['change']),
660 ('revisions', commit_ids['change']),
662 ('__end__', 'revisions:sequence'),
661 ('__end__', 'revisions:sequence'),
663 ('user', ''),
662 ('user', ''),
664 ('csrf_token', csrf_token),
663 ('csrf_token', csrf_token),
665 ],
664 ],
666 status=302)
665 status=302)
667
666
668 location = response.headers['Location']
667 location = response.headers['Location']
669
668
670 pull_request_id = location.rsplit('/', 1)[1]
669 pull_request_id = location.rsplit('/', 1)[1]
671 assert pull_request_id != 'new'
670 assert pull_request_id != 'new'
672 pull_request = PullRequest.get(int(pull_request_id))
671 pull_request = PullRequest.get(int(pull_request_id))
673
672
674 # Check that a notification was made
673 # Check that a notification was made
675 notifications = Notification.query()\
674 notifications = Notification.query()\
676 .filter(Notification.created_by == pull_request.author.user_id,
675 .filter(Notification.created_by == pull_request.author.user_id,
677 Notification.type_ == Notification.TYPE_PULL_REQUEST,
676 Notification.type_ == Notification.TYPE_PULL_REQUEST,
678 Notification.subject.contains(
677 Notification.subject.contains(
679 "requested a pull request review. !%s" % pull_request_id))
678 "requested a pull request review. !%s" % pull_request_id))
680 assert len(notifications.all()) == 1
679 assert len(notifications.all()) == 1
681
680
682 # Change reviewers and check that a notification was made
681 # Change reviewers and check that a notification was made
683 PullRequestModel().update_reviewers(
682 PullRequestModel().update_reviewers(
684 pull_request.pull_request_id, [(1, [], False, [])],
683 pull_request.pull_request_id, [(1, [], False, [])],
685 pull_request.author)
684 pull_request.author)
686 assert len(notifications.all()) == 2
685 assert len(notifications.all()) == 2
687
686
688 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
687 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
689 csrf_token):
688 csrf_token):
690 commits = [
689 commits = [
691 {'message': 'ancestor',
690 {'message': 'ancestor',
692 'added': [FileNode('file_A', content='content_of_ancestor')]},
691 'added': [FileNode('file_A', content='content_of_ancestor')]},
693 {'message': 'change',
692 {'message': 'change',
694 'added': [FileNode('file_a', content='content_of_change')]},
693 'added': [FileNode('file_a', content='content_of_change')]},
695 {'message': 'change-child'},
694 {'message': 'change-child'},
696 {'message': 'ancestor-child', 'parents': ['ancestor'],
695 {'message': 'ancestor-child', 'parents': ['ancestor'],
697 'added': [
696 'added': [
698 FileNode('file_B', content='content_of_ancestor_child')]},
697 FileNode('file_B', content='content_of_ancestor_child')]},
699 {'message': 'ancestor-child-2'},
698 {'message': 'ancestor-child-2'},
700 ]
699 ]
701 commit_ids = backend.create_master_repo(commits)
700 commit_ids = backend.create_master_repo(commits)
702 target = backend.create_repo(heads=['ancestor-child'])
701 target = backend.create_repo(heads=['ancestor-child'])
703 source = backend.create_repo(heads=['change'])
702 source = backend.create_repo(heads=['change'])
704
703
705 response = self.app.post(
704 response = self.app.post(
706 route_path('pullrequest_create', repo_name=source.repo_name),
705 route_path('pullrequest_create', repo_name=source.repo_name),
707 [
706 [
708 ('source_repo', source.repo_name),
707 ('source_repo', source.repo_name),
709 ('source_ref', 'branch:default:' + commit_ids['change']),
708 ('source_ref', 'branch:default:' + commit_ids['change']),
710 ('target_repo', target.repo_name),
709 ('target_repo', target.repo_name),
711 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
710 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
712 ('common_ancestor', commit_ids['ancestor']),
711 ('common_ancestor', commit_ids['ancestor']),
713 ('pullrequest_title', 'Title'),
712 ('pullrequest_title', 'Title'),
714 ('pullrequest_desc', 'Description'),
713 ('pullrequest_desc', 'Description'),
715 ('description_renderer', 'markdown'),
714 ('description_renderer', 'markdown'),
716 ('__start__', 'review_members:sequence'),
715 ('__start__', 'review_members:sequence'),
717 ('__start__', 'reviewer:mapping'),
716 ('__start__', 'reviewer:mapping'),
718 ('user_id', '1'),
717 ('user_id', '1'),
719 ('__start__', 'reasons:sequence'),
718 ('__start__', 'reasons:sequence'),
720 ('reason', 'Some reason'),
719 ('reason', 'Some reason'),
721 ('__end__', 'reasons:sequence'),
720 ('__end__', 'reasons:sequence'),
722 ('__start__', 'rules:sequence'),
721 ('__start__', 'rules:sequence'),
723 ('__end__', 'rules:sequence'),
722 ('__end__', 'rules:sequence'),
724 ('mandatory', 'False'),
723 ('mandatory', 'False'),
725 ('__end__', 'reviewer:mapping'),
724 ('__end__', 'reviewer:mapping'),
726 ('__end__', 'review_members:sequence'),
725 ('__end__', 'review_members:sequence'),
727 ('__start__', 'revisions:sequence'),
726 ('__start__', 'revisions:sequence'),
728 ('revisions', commit_ids['change']),
727 ('revisions', commit_ids['change']),
729 ('__end__', 'revisions:sequence'),
728 ('__end__', 'revisions:sequence'),
730 ('user', ''),
729 ('user', ''),
731 ('csrf_token', csrf_token),
730 ('csrf_token', csrf_token),
732 ],
731 ],
733 status=302)
732 status=302)
734
733
735 location = response.headers['Location']
734 location = response.headers['Location']
736
735
737 pull_request_id = location.rsplit('/', 1)[1]
736 pull_request_id = location.rsplit('/', 1)[1]
738 assert pull_request_id != 'new'
737 assert pull_request_id != 'new'
739 pull_request = PullRequest.get(int(pull_request_id))
738 pull_request = PullRequest.get(int(pull_request_id))
740
739
741 # target_ref has to point to the ancestor's commit_id in order to
740 # target_ref has to point to the ancestor's commit_id in order to
742 # show the correct diff
741 # show the correct diff
743 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
742 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
744 assert pull_request.target_ref == expected_target_ref
743 assert pull_request.target_ref == expected_target_ref
745
744
746 # Check generated diff contents
745 # Check generated diff contents
747 response = response.follow()
746 response = response.follow()
748 response.mustcontain(no=['content_of_ancestor'])
747 response.mustcontain(no=['content_of_ancestor'])
749 response.mustcontain(no=['content_of_ancestor-child'])
748 response.mustcontain(no=['content_of_ancestor-child'])
750 response.mustcontain('content_of_change')
749 response.mustcontain('content_of_change')
751
750
752 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
751 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
753 # Clear any previous calls to rcextensions
752 # Clear any previous calls to rcextensions
754 rhodecode.EXTENSIONS.calls.clear()
753 rhodecode.EXTENSIONS.calls.clear()
755
754
756 pull_request = pr_util.create_pull_request(
755 pull_request = pr_util.create_pull_request(
757 approved=True, mergeable=True)
756 approved=True, mergeable=True)
758 pull_request_id = pull_request.pull_request_id
757 pull_request_id = pull_request.pull_request_id
759 repo_name = pull_request.target_repo.scm_instance().name,
758 repo_name = pull_request.target_repo.scm_instance().name,
760
759
761 url = route_path('pullrequest_merge',
760 url = route_path('pullrequest_merge',
762 repo_name=str(repo_name[0]),
761 repo_name=str(repo_name[0]),
763 pull_request_id=pull_request_id)
762 pull_request_id=pull_request_id)
764 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
763 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
765
764
766 pull_request = PullRequest.get(pull_request_id)
765 pull_request = PullRequest.get(pull_request_id)
767
766
768 assert response.status_int == 200
767 assert response.status_int == 200
769 assert pull_request.is_closed()
768 assert pull_request.is_closed()
770 assert_pull_request_status(
769 assert_pull_request_status(
771 pull_request, ChangesetStatus.STATUS_APPROVED)
770 pull_request, ChangesetStatus.STATUS_APPROVED)
772
771
773 # Check the relevant log entries were added
772 # Check the relevant log entries were added
774 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
773 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
775 actions = [log.action for log in user_logs]
774 actions = [log.action for log in user_logs]
776 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
775 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
777 expected_actions = [
776 expected_actions = [
778 u'repo.pull_request.close',
777 u'repo.pull_request.close',
779 u'repo.pull_request.merge',
778 u'repo.pull_request.merge',
780 u'repo.pull_request.comment.create'
779 u'repo.pull_request.comment.create'
781 ]
780 ]
782 assert actions == expected_actions
781 assert actions == expected_actions
783
782
784 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
783 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
785 actions = [log for log in user_logs]
784 actions = [log for log in user_logs]
786 assert actions[-1].action == 'user.push'
785 assert actions[-1].action == 'user.push'
787 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
786 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
788
787
789 # Check post_push rcextension was really executed
788 # Check post_push rcextension was really executed
790 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
789 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
791 assert len(push_calls) == 1
790 assert len(push_calls) == 1
792 unused_last_call_args, last_call_kwargs = push_calls[0]
791 unused_last_call_args, last_call_kwargs = push_calls[0]
793 assert last_call_kwargs['action'] == 'push'
792 assert last_call_kwargs['action'] == 'push'
794 assert last_call_kwargs['commit_ids'] == pr_commit_ids
793 assert last_call_kwargs['commit_ids'] == pr_commit_ids
795
794
796 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
795 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
797 pull_request = pr_util.create_pull_request(mergeable=False)
796 pull_request = pr_util.create_pull_request(mergeable=False)
798 pull_request_id = pull_request.pull_request_id
797 pull_request_id = pull_request.pull_request_id
799 pull_request = PullRequest.get(pull_request_id)
798 pull_request = PullRequest.get(pull_request_id)
800
799
801 response = self.app.post(
800 response = self.app.post(
802 route_path('pullrequest_merge',
801 route_path('pullrequest_merge',
803 repo_name=pull_request.target_repo.scm_instance().name,
802 repo_name=pull_request.target_repo.scm_instance().name,
804 pull_request_id=pull_request.pull_request_id),
803 pull_request_id=pull_request.pull_request_id),
805 params={'csrf_token': csrf_token}).follow()
804 params={'csrf_token': csrf_token}).follow()
806
805
807 assert response.status_int == 200
806 assert response.status_int == 200
808 response.mustcontain(
807 response.mustcontain(
809 'Merge is not currently possible because of below failed checks.')
808 'Merge is not currently possible because of below failed checks.')
810 response.mustcontain('Server-side pull request merging is disabled.')
809 response.mustcontain('Server-side pull request merging is disabled.')
811
810
812 @pytest.mark.skip_backends('svn')
811 @pytest.mark.skip_backends('svn')
813 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
812 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
814 pull_request = pr_util.create_pull_request(mergeable=True)
813 pull_request = pr_util.create_pull_request(mergeable=True)
815 pull_request_id = pull_request.pull_request_id
814 pull_request_id = pull_request.pull_request_id
816 repo_name = pull_request.target_repo.scm_instance().name
815 repo_name = pull_request.target_repo.scm_instance().name
817
816
818 response = self.app.post(
817 response = self.app.post(
819 route_path('pullrequest_merge',
818 route_path('pullrequest_merge',
820 repo_name=repo_name, pull_request_id=pull_request_id),
819 repo_name=repo_name, pull_request_id=pull_request_id),
821 params={'csrf_token': csrf_token}).follow()
820 params={'csrf_token': csrf_token}).follow()
822
821
823 assert response.status_int == 200
822 assert response.status_int == 200
824
823
825 response.mustcontain(
824 response.mustcontain(
826 'Merge is not currently possible because of below failed checks.')
825 'Merge is not currently possible because of below failed checks.')
827 response.mustcontain('Pull request reviewer approval is pending.')
826 response.mustcontain('Pull request reviewer approval is pending.')
828
827
829 def test_merge_pull_request_renders_failure_reason(
828 def test_merge_pull_request_renders_failure_reason(
830 self, user_regular, csrf_token, pr_util):
829 self, user_regular, csrf_token, pr_util):
831 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
830 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
832 pull_request_id = pull_request.pull_request_id
831 pull_request_id = pull_request.pull_request_id
833 repo_name = pull_request.target_repo.scm_instance().name
832 repo_name = pull_request.target_repo.scm_instance().name
834
833
835 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
834 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
836 MergeFailureReason.PUSH_FAILED,
835 MergeFailureReason.PUSH_FAILED,
837 metadata={'target': 'shadow repo',
836 metadata={'target': 'shadow repo',
838 'merge_commit': 'xxx'})
837 'merge_commit': 'xxx'})
839 model_patcher = mock.patch.multiple(
838 model_patcher = mock.patch.multiple(
840 PullRequestModel,
839 PullRequestModel,
841 merge_repo=mock.Mock(return_value=merge_resp),
840 merge_repo=mock.Mock(return_value=merge_resp),
842 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
841 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
843
842
844 with model_patcher:
843 with model_patcher:
845 response = self.app.post(
844 response = self.app.post(
846 route_path('pullrequest_merge',
845 route_path('pullrequest_merge',
847 repo_name=repo_name,
846 repo_name=repo_name,
848 pull_request_id=pull_request_id),
847 pull_request_id=pull_request_id),
849 params={'csrf_token': csrf_token}, status=302)
848 params={'csrf_token': csrf_token}, status=302)
850
849
851 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
850 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
852 metadata={'target': 'shadow repo',
851 metadata={'target': 'shadow repo',
853 'merge_commit': 'xxx'})
852 'merge_commit': 'xxx'})
854 assert_session_flash(response, merge_resp.merge_status_message)
853 assert_session_flash(response, merge_resp.merge_status_message)
855
854
856 def test_update_source_revision(self, backend, csrf_token):
855 def test_update_source_revision(self, backend, csrf_token):
857 commits = [
856 commits = [
858 {'message': 'ancestor'},
857 {'message': 'ancestor'},
859 {'message': 'change'},
858 {'message': 'change'},
860 {'message': 'change-2'},
859 {'message': 'change-2'},
861 ]
860 ]
862 commit_ids = backend.create_master_repo(commits)
861 commit_ids = backend.create_master_repo(commits)
863 target = backend.create_repo(heads=['ancestor'])
862 target = backend.create_repo(heads=['ancestor'])
864 source = backend.create_repo(heads=['change'])
863 source = backend.create_repo(heads=['change'])
865
864
866 # create pr from a in source to A in target
865 # create pr from a in source to A in target
867 pull_request = PullRequest()
866 pull_request = PullRequest()
868
867
869 pull_request.source_repo = source
868 pull_request.source_repo = source
870 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
869 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
871 branch=backend.default_branch_name, commit_id=commit_ids['change'])
870 branch=backend.default_branch_name, commit_id=commit_ids['change'])
872
871
873 pull_request.target_repo = target
872 pull_request.target_repo = target
874 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
873 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
875 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
874 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
876
875
877 pull_request.revisions = [commit_ids['change']]
876 pull_request.revisions = [commit_ids['change']]
878 pull_request.title = u"Test"
877 pull_request.title = u"Test"
879 pull_request.description = u"Description"
878 pull_request.description = u"Description"
880 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
879 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
881 pull_request.pull_request_state = PullRequest.STATE_CREATED
880 pull_request.pull_request_state = PullRequest.STATE_CREATED
882 Session().add(pull_request)
881 Session().add(pull_request)
883 Session().commit()
882 Session().commit()
884 pull_request_id = pull_request.pull_request_id
883 pull_request_id = pull_request.pull_request_id
885
884
886 # source has ancestor - change - change-2
885 # source has ancestor - change - change-2
887 backend.pull_heads(source, heads=['change-2'])
886 backend.pull_heads(source, heads=['change-2'])
888
887
889 # update PR
888 # update PR
890 self.app.post(
889 self.app.post(
891 route_path('pullrequest_update',
890 route_path('pullrequest_update',
892 repo_name=target.repo_name, pull_request_id=pull_request_id),
891 repo_name=target.repo_name, pull_request_id=pull_request_id),
893 params={'update_commits': 'true', 'csrf_token': csrf_token})
892 params={'update_commits': 'true', 'csrf_token': csrf_token})
894
893
895 response = self.app.get(
894 response = self.app.get(
896 route_path('pullrequest_show',
895 route_path('pullrequest_show',
897 repo_name=target.repo_name,
896 repo_name=target.repo_name,
898 pull_request_id=pull_request.pull_request_id))
897 pull_request_id=pull_request.pull_request_id))
899
898
900 assert response.status_int == 200
899 assert response.status_int == 200
901 response.mustcontain('Pull request updated to')
900 response.mustcontain('Pull request updated to')
902 response.mustcontain('with 1 added, 0 removed commits.')
901 response.mustcontain('with 1 added, 0 removed commits.')
903
902
904 # check that we have now both revisions
903 # check that we have now both revisions
905 pull_request = PullRequest.get(pull_request_id)
904 pull_request = PullRequest.get(pull_request_id)
906 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
905 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
907
906
908 def test_update_target_revision(self, backend, csrf_token):
907 def test_update_target_revision(self, backend, csrf_token):
909 commits = [
908 commits = [
910 {'message': 'ancestor'},
909 {'message': 'ancestor'},
911 {'message': 'change'},
910 {'message': 'change'},
912 {'message': 'ancestor-new', 'parents': ['ancestor']},
911 {'message': 'ancestor-new', 'parents': ['ancestor']},
913 {'message': 'change-rebased'},
912 {'message': 'change-rebased'},
914 ]
913 ]
915 commit_ids = backend.create_master_repo(commits)
914 commit_ids = backend.create_master_repo(commits)
916 target = backend.create_repo(heads=['ancestor'])
915 target = backend.create_repo(heads=['ancestor'])
917 source = backend.create_repo(heads=['change'])
916 source = backend.create_repo(heads=['change'])
918
917
919 # create pr from a in source to A in target
918 # create pr from a in source to A in target
920 pull_request = PullRequest()
919 pull_request = PullRequest()
921
920
922 pull_request.source_repo = source
921 pull_request.source_repo = source
923 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
922 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
924 branch=backend.default_branch_name, commit_id=commit_ids['change'])
923 branch=backend.default_branch_name, commit_id=commit_ids['change'])
925
924
926 pull_request.target_repo = target
925 pull_request.target_repo = target
927 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
926 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
928 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
927 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
929
928
930 pull_request.revisions = [commit_ids['change']]
929 pull_request.revisions = [commit_ids['change']]
931 pull_request.title = u"Test"
930 pull_request.title = u"Test"
932 pull_request.description = u"Description"
931 pull_request.description = u"Description"
933 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
932 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
934 pull_request.pull_request_state = PullRequest.STATE_CREATED
933 pull_request.pull_request_state = PullRequest.STATE_CREATED
935
934
936 Session().add(pull_request)
935 Session().add(pull_request)
937 Session().commit()
936 Session().commit()
938 pull_request_id = pull_request.pull_request_id
937 pull_request_id = pull_request.pull_request_id
939
938
940 # target has ancestor - ancestor-new
939 # target has ancestor - ancestor-new
941 # source has ancestor - ancestor-new - change-rebased
940 # source has ancestor - ancestor-new - change-rebased
942 backend.pull_heads(target, heads=['ancestor-new'])
941 backend.pull_heads(target, heads=['ancestor-new'])
943 backend.pull_heads(source, heads=['change-rebased'])
942 backend.pull_heads(source, heads=['change-rebased'])
944
943
945 # update PR
944 # update PR
946 url = route_path('pullrequest_update',
945 url = route_path('pullrequest_update',
947 repo_name=target.repo_name,
946 repo_name=target.repo_name,
948 pull_request_id=pull_request_id)
947 pull_request_id=pull_request_id)
949 self.app.post(url,
948 self.app.post(url,
950 params={'update_commits': 'true', 'csrf_token': csrf_token},
949 params={'update_commits': 'true', 'csrf_token': csrf_token},
951 status=200)
950 status=200)
952
951
953 # check that we have now both revisions
952 # check that we have now both revisions
954 pull_request = PullRequest.get(pull_request_id)
953 pull_request = PullRequest.get(pull_request_id)
955 assert pull_request.revisions == [commit_ids['change-rebased']]
954 assert pull_request.revisions == [commit_ids['change-rebased']]
956 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
955 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
957 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
956 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
958
957
959 response = self.app.get(
958 response = self.app.get(
960 route_path('pullrequest_show',
959 route_path('pullrequest_show',
961 repo_name=target.repo_name,
960 repo_name=target.repo_name,
962 pull_request_id=pull_request.pull_request_id))
961 pull_request_id=pull_request.pull_request_id))
963 assert response.status_int == 200
962 assert response.status_int == 200
964 response.mustcontain('Pull request updated to')
963 response.mustcontain('Pull request updated to')
965 response.mustcontain('with 1 added, 1 removed commits.')
964 response.mustcontain('with 1 added, 1 removed commits.')
966
965
967 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
966 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
968 backend = backend_git
967 backend = backend_git
969 commits = [
968 commits = [
970 {'message': 'master-commit-1'},
969 {'message': 'master-commit-1'},
971 {'message': 'master-commit-2-change-1'},
970 {'message': 'master-commit-2-change-1'},
972 {'message': 'master-commit-3-change-2'},
971 {'message': 'master-commit-3-change-2'},
973
972
974 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
973 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
975 {'message': 'feat-commit-2'},
974 {'message': 'feat-commit-2'},
976 ]
975 ]
977 commit_ids = backend.create_master_repo(commits)
976 commit_ids = backend.create_master_repo(commits)
978 target = backend.create_repo(heads=['master-commit-3-change-2'])
977 target = backend.create_repo(heads=['master-commit-3-change-2'])
979 source = backend.create_repo(heads=['feat-commit-2'])
978 source = backend.create_repo(heads=['feat-commit-2'])
980
979
981 # create pr from a in source to A in target
980 # create pr from a in source to A in target
982 pull_request = PullRequest()
981 pull_request = PullRequest()
983 pull_request.source_repo = source
982 pull_request.source_repo = source
984
983
985 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
984 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
986 branch=backend.default_branch_name,
985 branch=backend.default_branch_name,
987 commit_id=commit_ids['master-commit-3-change-2'])
986 commit_id=commit_ids['master-commit-3-change-2'])
988
987
989 pull_request.target_repo = target
988 pull_request.target_repo = target
990 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
989 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
991 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
990 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
992
991
993 pull_request.revisions = [
992 pull_request.revisions = [
994 commit_ids['feat-commit-1'],
993 commit_ids['feat-commit-1'],
995 commit_ids['feat-commit-2']
994 commit_ids['feat-commit-2']
996 ]
995 ]
997 pull_request.title = u"Test"
996 pull_request.title = u"Test"
998 pull_request.description = u"Description"
997 pull_request.description = u"Description"
999 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
998 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1000 pull_request.pull_request_state = PullRequest.STATE_CREATED
999 pull_request.pull_request_state = PullRequest.STATE_CREATED
1001 Session().add(pull_request)
1000 Session().add(pull_request)
1002 Session().commit()
1001 Session().commit()
1003 pull_request_id = pull_request.pull_request_id
1002 pull_request_id = pull_request.pull_request_id
1004
1003
1005 # PR is created, now we simulate a force-push into target,
1004 # PR is created, now we simulate a force-push into target,
1006 # that drops a 2 last commits
1005 # that drops a 2 last commits
1007 vcsrepo = target.scm_instance()
1006 vcsrepo = target.scm_instance()
1008 vcsrepo.config.clear_section('hooks')
1007 vcsrepo.config.clear_section('hooks')
1009 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1008 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1010
1009
1011 # update PR
1010 # update PR
1012 url = route_path('pullrequest_update',
1011 url = route_path('pullrequest_update',
1013 repo_name=target.repo_name,
1012 repo_name=target.repo_name,
1014 pull_request_id=pull_request_id)
1013 pull_request_id=pull_request_id)
1015 self.app.post(url,
1014 self.app.post(url,
1016 params={'update_commits': 'true', 'csrf_token': csrf_token},
1015 params={'update_commits': 'true', 'csrf_token': csrf_token},
1017 status=200)
1016 status=200)
1018
1017
1019 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
1018 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
1020 assert response.status_int == 200
1019 assert response.status_int == 200
1021 response.mustcontain('Pull request updated to')
1020 response.mustcontain('Pull request updated to')
1022 response.mustcontain('with 0 added, 0 removed commits.')
1021 response.mustcontain('with 0 added, 0 removed commits.')
1023
1022
1024 def test_update_of_ancestor_reference(self, backend, csrf_token):
1023 def test_update_of_ancestor_reference(self, backend, csrf_token):
1025 commits = [
1024 commits = [
1026 {'message': 'ancestor'},
1025 {'message': 'ancestor'},
1027 {'message': 'change'},
1026 {'message': 'change'},
1028 {'message': 'change-2'},
1027 {'message': 'change-2'},
1029 {'message': 'ancestor-new', 'parents': ['ancestor']},
1028 {'message': 'ancestor-new', 'parents': ['ancestor']},
1030 {'message': 'change-rebased'},
1029 {'message': 'change-rebased'},
1031 ]
1030 ]
1032 commit_ids = backend.create_master_repo(commits)
1031 commit_ids = backend.create_master_repo(commits)
1033 target = backend.create_repo(heads=['ancestor'])
1032 target = backend.create_repo(heads=['ancestor'])
1034 source = backend.create_repo(heads=['change'])
1033 source = backend.create_repo(heads=['change'])
1035
1034
1036 # create pr from a in source to A in target
1035 # create pr from a in source to A in target
1037 pull_request = PullRequest()
1036 pull_request = PullRequest()
1038 pull_request.source_repo = source
1037 pull_request.source_repo = source
1039
1038
1040 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1039 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1041 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1040 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1042 pull_request.target_repo = target
1041 pull_request.target_repo = target
1043 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1042 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1044 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1043 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1045 pull_request.revisions = [commit_ids['change']]
1044 pull_request.revisions = [commit_ids['change']]
1046 pull_request.title = u"Test"
1045 pull_request.title = u"Test"
1047 pull_request.description = u"Description"
1046 pull_request.description = u"Description"
1048 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1047 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1049 pull_request.pull_request_state = PullRequest.STATE_CREATED
1048 pull_request.pull_request_state = PullRequest.STATE_CREATED
1050 Session().add(pull_request)
1049 Session().add(pull_request)
1051 Session().commit()
1050 Session().commit()
1052 pull_request_id = pull_request.pull_request_id
1051 pull_request_id = pull_request.pull_request_id
1053
1052
1054 # target has ancestor - ancestor-new
1053 # target has ancestor - ancestor-new
1055 # source has ancestor - ancestor-new - change-rebased
1054 # source has ancestor - ancestor-new - change-rebased
1056 backend.pull_heads(target, heads=['ancestor-new'])
1055 backend.pull_heads(target, heads=['ancestor-new'])
1057 backend.pull_heads(source, heads=['change-rebased'])
1056 backend.pull_heads(source, heads=['change-rebased'])
1058
1057
1059 # update PR
1058 # update PR
1060 self.app.post(
1059 self.app.post(
1061 route_path('pullrequest_update',
1060 route_path('pullrequest_update',
1062 repo_name=target.repo_name, pull_request_id=pull_request_id),
1061 repo_name=target.repo_name, pull_request_id=pull_request_id),
1063 params={'update_commits': 'true', 'csrf_token': csrf_token},
1062 params={'update_commits': 'true', 'csrf_token': csrf_token},
1064 status=200)
1063 status=200)
1065
1064
1066 # Expect the target reference to be updated correctly
1065 # Expect the target reference to be updated correctly
1067 pull_request = PullRequest.get(pull_request_id)
1066 pull_request = PullRequest.get(pull_request_id)
1068 assert pull_request.revisions == [commit_ids['change-rebased']]
1067 assert pull_request.revisions == [commit_ids['change-rebased']]
1069 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1068 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1070 branch=backend.default_branch_name,
1069 branch=backend.default_branch_name,
1071 commit_id=commit_ids['ancestor-new'])
1070 commit_id=commit_ids['ancestor-new'])
1072 assert pull_request.target_ref == expected_target_ref
1071 assert pull_request.target_ref == expected_target_ref
1073
1072
1074 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1073 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1075 branch_name = 'development'
1074 branch_name = 'development'
1076 commits = [
1075 commits = [
1077 {'message': 'initial-commit'},
1076 {'message': 'initial-commit'},
1078 {'message': 'old-feature'},
1077 {'message': 'old-feature'},
1079 {'message': 'new-feature', 'branch': branch_name},
1078 {'message': 'new-feature', 'branch': branch_name},
1080 ]
1079 ]
1081 repo = backend_git.create_repo(commits)
1080 repo = backend_git.create_repo(commits)
1082 repo_name = repo.repo_name
1081 repo_name = repo.repo_name
1083 commit_ids = backend_git.commit_ids
1082 commit_ids = backend_git.commit_ids
1084
1083
1085 pull_request = PullRequest()
1084 pull_request = PullRequest()
1086 pull_request.source_repo = repo
1085 pull_request.source_repo = repo
1087 pull_request.target_repo = repo
1086 pull_request.target_repo = repo
1088 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1087 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1089 branch=branch_name, commit_id=commit_ids['new-feature'])
1088 branch=branch_name, commit_id=commit_ids['new-feature'])
1090 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1089 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1091 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1090 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1092 pull_request.revisions = [commit_ids['new-feature']]
1091 pull_request.revisions = [commit_ids['new-feature']]
1093 pull_request.title = u"Test"
1092 pull_request.title = u"Test"
1094 pull_request.description = u"Description"
1093 pull_request.description = u"Description"
1095 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1094 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1096 pull_request.pull_request_state = PullRequest.STATE_CREATED
1095 pull_request.pull_request_state = PullRequest.STATE_CREATED
1097 Session().add(pull_request)
1096 Session().add(pull_request)
1098 Session().commit()
1097 Session().commit()
1099
1098
1100 pull_request_id = pull_request.pull_request_id
1099 pull_request_id = pull_request.pull_request_id
1101
1100
1102 vcs = repo.scm_instance()
1101 vcs = repo.scm_instance()
1103 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1102 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1104 # NOTE(marcink): run GC to ensure the commits are gone
1103 # NOTE(marcink): run GC to ensure the commits are gone
1105 vcs.run_gc()
1104 vcs.run_gc()
1106
1105
1107 response = self.app.get(route_path(
1106 response = self.app.get(route_path(
1108 'pullrequest_show',
1107 'pullrequest_show',
1109 repo_name=repo_name,
1108 repo_name=repo_name,
1110 pull_request_id=pull_request_id))
1109 pull_request_id=pull_request_id))
1111
1110
1112 assert response.status_int == 200
1111 assert response.status_int == 200
1113
1112
1114 response.assert_response().element_contains(
1113 response.assert_response().element_contains(
1115 '#changeset_compare_view_content .alert strong',
1114 '#changeset_compare_view_content .alert strong',
1116 'Missing commits')
1115 'Missing commits')
1117 response.assert_response().element_contains(
1116 response.assert_response().element_contains(
1118 '#changeset_compare_view_content .alert',
1117 '#changeset_compare_view_content .alert',
1119 'This pull request cannot be displayed, because one or more'
1118 'This pull request cannot be displayed, because one or more'
1120 ' commits no longer exist in the source repository.')
1119 ' commits no longer exist in the source repository.')
1121
1120
1122 def test_strip_commits_from_pull_request(
1121 def test_strip_commits_from_pull_request(
1123 self, backend, pr_util, csrf_token):
1122 self, backend, pr_util, csrf_token):
1124 commits = [
1123 commits = [
1125 {'message': 'initial-commit'},
1124 {'message': 'initial-commit'},
1126 {'message': 'old-feature'},
1125 {'message': 'old-feature'},
1127 {'message': 'new-feature', 'parents': ['initial-commit']},
1126 {'message': 'new-feature', 'parents': ['initial-commit']},
1128 ]
1127 ]
1129 pull_request = pr_util.create_pull_request(
1128 pull_request = pr_util.create_pull_request(
1130 commits, target_head='initial-commit', source_head='new-feature',
1129 commits, target_head='initial-commit', source_head='new-feature',
1131 revisions=['new-feature'])
1130 revisions=['new-feature'])
1132
1131
1133 vcs = pr_util.source_repository.scm_instance()
1132 vcs = pr_util.source_repository.scm_instance()
1134 if backend.alias == 'git':
1133 if backend.alias == 'git':
1135 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1134 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1136 else:
1135 else:
1137 vcs.strip(pr_util.commit_ids['new-feature'])
1136 vcs.strip(pr_util.commit_ids['new-feature'])
1138
1137
1139 response = self.app.get(route_path(
1138 response = self.app.get(route_path(
1140 'pullrequest_show',
1139 'pullrequest_show',
1141 repo_name=pr_util.target_repository.repo_name,
1140 repo_name=pr_util.target_repository.repo_name,
1142 pull_request_id=pull_request.pull_request_id))
1141 pull_request_id=pull_request.pull_request_id))
1143
1142
1144 assert response.status_int == 200
1143 assert response.status_int == 200
1145
1144
1146 response.assert_response().element_contains(
1145 response.assert_response().element_contains(
1147 '#changeset_compare_view_content .alert strong',
1146 '#changeset_compare_view_content .alert strong',
1148 'Missing commits')
1147 'Missing commits')
1149 response.assert_response().element_contains(
1148 response.assert_response().element_contains(
1150 '#changeset_compare_view_content .alert',
1149 '#changeset_compare_view_content .alert',
1151 'This pull request cannot be displayed, because one or more'
1150 'This pull request cannot be displayed, because one or more'
1152 ' commits no longer exist in the source repository.')
1151 ' commits no longer exist in the source repository.')
1153 response.assert_response().element_contains(
1152 response.assert_response().element_contains(
1154 '#update_commits',
1153 '#update_commits',
1155 'Update commits')
1154 'Update commits')
1156
1155
1157 def test_strip_commits_and_update(
1156 def test_strip_commits_and_update(
1158 self, backend, pr_util, csrf_token):
1157 self, backend, pr_util, csrf_token):
1159 commits = [
1158 commits = [
1160 {'message': 'initial-commit'},
1159 {'message': 'initial-commit'},
1161 {'message': 'old-feature'},
1160 {'message': 'old-feature'},
1162 {'message': 'new-feature', 'parents': ['old-feature']},
1161 {'message': 'new-feature', 'parents': ['old-feature']},
1163 ]
1162 ]
1164 pull_request = pr_util.create_pull_request(
1163 pull_request = pr_util.create_pull_request(
1165 commits, target_head='old-feature', source_head='new-feature',
1164 commits, target_head='old-feature', source_head='new-feature',
1166 revisions=['new-feature'], mergeable=True)
1165 revisions=['new-feature'], mergeable=True)
1167
1166
1168 vcs = pr_util.source_repository.scm_instance()
1167 vcs = pr_util.source_repository.scm_instance()
1169 if backend.alias == 'git':
1168 if backend.alias == 'git':
1170 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1169 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1171 else:
1170 else:
1172 vcs.strip(pr_util.commit_ids['new-feature'])
1171 vcs.strip(pr_util.commit_ids['new-feature'])
1173
1172
1174 url = route_path('pullrequest_update',
1173 url = route_path('pullrequest_update',
1175 repo_name=pull_request.target_repo.repo_name,
1174 repo_name=pull_request.target_repo.repo_name,
1176 pull_request_id=pull_request.pull_request_id)
1175 pull_request_id=pull_request.pull_request_id)
1177 response = self.app.post(url,
1176 response = self.app.post(url,
1178 params={'update_commits': 'true',
1177 params={'update_commits': 'true',
1179 'csrf_token': csrf_token})
1178 'csrf_token': csrf_token})
1180
1179
1181 assert response.status_int == 200
1180 assert response.status_int == 200
1182 assert response.body == '{"response": true, "redirect_url": null}'
1181 assert response.body == '{"response": true, "redirect_url": null}'
1183
1182
1184 # Make sure that after update, it won't raise 500 errors
1183 # Make sure that after update, it won't raise 500 errors
1185 response = self.app.get(route_path(
1184 response = self.app.get(route_path(
1186 'pullrequest_show',
1185 'pullrequest_show',
1187 repo_name=pr_util.target_repository.repo_name,
1186 repo_name=pr_util.target_repository.repo_name,
1188 pull_request_id=pull_request.pull_request_id))
1187 pull_request_id=pull_request.pull_request_id))
1189
1188
1190 assert response.status_int == 200
1189 assert response.status_int == 200
1191 response.assert_response().element_contains(
1190 response.assert_response().element_contains(
1192 '#changeset_compare_view_content .alert strong',
1191 '#changeset_compare_view_content .alert strong',
1193 'Missing commits')
1192 'Missing commits')
1194
1193
1195 def test_branch_is_a_link(self, pr_util):
1194 def test_branch_is_a_link(self, pr_util):
1196 pull_request = pr_util.create_pull_request()
1195 pull_request = pr_util.create_pull_request()
1197 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1196 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1198 pull_request.target_ref = 'branch:target:abcdef1234567890'
1197 pull_request.target_ref = 'branch:target:abcdef1234567890'
1199 Session().add(pull_request)
1198 Session().add(pull_request)
1200 Session().commit()
1199 Session().commit()
1201
1200
1202 response = self.app.get(route_path(
1201 response = self.app.get(route_path(
1203 'pullrequest_show',
1202 'pullrequest_show',
1204 repo_name=pull_request.target_repo.scm_instance().name,
1203 repo_name=pull_request.target_repo.scm_instance().name,
1205 pull_request_id=pull_request.pull_request_id))
1204 pull_request_id=pull_request.pull_request_id))
1206 assert response.status_int == 200
1205 assert response.status_int == 200
1207
1206
1208 source = response.assert_response().get_element('.pr-source-info')
1207 source = response.assert_response().get_element('.pr-source-info')
1209 source_parent = source.getparent()
1208 source_parent = source.getparent()
1210 assert len(source_parent) == 1
1209 assert len(source_parent) == 1
1211
1210
1212 target = response.assert_response().get_element('.pr-target-info')
1211 target = response.assert_response().get_element('.pr-target-info')
1213 target_parent = target.getparent()
1212 target_parent = target.getparent()
1214 assert len(target_parent) == 1
1213 assert len(target_parent) == 1
1215
1214
1216 expected_origin_link = route_path(
1215 expected_origin_link = route_path(
1217 'repo_commits',
1216 'repo_commits',
1218 repo_name=pull_request.source_repo.scm_instance().name,
1217 repo_name=pull_request.source_repo.scm_instance().name,
1219 params=dict(branch='origin'))
1218 params=dict(branch='origin'))
1220 expected_target_link = route_path(
1219 expected_target_link = route_path(
1221 'repo_commits',
1220 'repo_commits',
1222 repo_name=pull_request.target_repo.scm_instance().name,
1221 repo_name=pull_request.target_repo.scm_instance().name,
1223 params=dict(branch='target'))
1222 params=dict(branch='target'))
1224 assert source_parent.attrib['href'] == expected_origin_link
1223 assert source_parent.attrib['href'] == expected_origin_link
1225 assert target_parent.attrib['href'] == expected_target_link
1224 assert target_parent.attrib['href'] == expected_target_link
1226
1225
1227 def test_bookmark_is_not_a_link(self, pr_util):
1226 def test_bookmark_is_not_a_link(self, pr_util):
1228 pull_request = pr_util.create_pull_request()
1227 pull_request = pr_util.create_pull_request()
1229 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1228 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1230 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1229 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1231 Session().add(pull_request)
1230 Session().add(pull_request)
1232 Session().commit()
1231 Session().commit()
1233
1232
1234 response = self.app.get(route_path(
1233 response = self.app.get(route_path(
1235 'pullrequest_show',
1234 'pullrequest_show',
1236 repo_name=pull_request.target_repo.scm_instance().name,
1235 repo_name=pull_request.target_repo.scm_instance().name,
1237 pull_request_id=pull_request.pull_request_id))
1236 pull_request_id=pull_request.pull_request_id))
1238 assert response.status_int == 200
1237 assert response.status_int == 200
1239
1238
1240 source = response.assert_response().get_element('.pr-source-info')
1239 source = response.assert_response().get_element('.pr-source-info')
1241 assert source.text.strip() == 'bookmark:origin'
1240 assert source.text.strip() == 'bookmark:origin'
1242 assert source.getparent().attrib.get('href') is None
1241 assert source.getparent().attrib.get('href') is None
1243
1242
1244 target = response.assert_response().get_element('.pr-target-info')
1243 target = response.assert_response().get_element('.pr-target-info')
1245 assert target.text.strip() == 'bookmark:target'
1244 assert target.text.strip() == 'bookmark:target'
1246 assert target.getparent().attrib.get('href') is None
1245 assert target.getparent().attrib.get('href') is None
1247
1246
1248 def test_tag_is_not_a_link(self, pr_util):
1247 def test_tag_is_not_a_link(self, pr_util):
1249 pull_request = pr_util.create_pull_request()
1248 pull_request = pr_util.create_pull_request()
1250 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1249 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1251 pull_request.target_ref = 'tag:target:abcdef1234567890'
1250 pull_request.target_ref = 'tag:target:abcdef1234567890'
1252 Session().add(pull_request)
1251 Session().add(pull_request)
1253 Session().commit()
1252 Session().commit()
1254
1253
1255 response = self.app.get(route_path(
1254 response = self.app.get(route_path(
1256 'pullrequest_show',
1255 'pullrequest_show',
1257 repo_name=pull_request.target_repo.scm_instance().name,
1256 repo_name=pull_request.target_repo.scm_instance().name,
1258 pull_request_id=pull_request.pull_request_id))
1257 pull_request_id=pull_request.pull_request_id))
1259 assert response.status_int == 200
1258 assert response.status_int == 200
1260
1259
1261 source = response.assert_response().get_element('.pr-source-info')
1260 source = response.assert_response().get_element('.pr-source-info')
1262 assert source.text.strip() == 'tag:origin'
1261 assert source.text.strip() == 'tag:origin'
1263 assert source.getparent().attrib.get('href') is None
1262 assert source.getparent().attrib.get('href') is None
1264
1263
1265 target = response.assert_response().get_element('.pr-target-info')
1264 target = response.assert_response().get_element('.pr-target-info')
1266 assert target.text.strip() == 'tag:target'
1265 assert target.text.strip() == 'tag:target'
1267 assert target.getparent().attrib.get('href') is None
1266 assert target.getparent().attrib.get('href') is None
1268
1267
1269 @pytest.mark.parametrize('mergeable', [True, False])
1268 @pytest.mark.parametrize('mergeable', [True, False])
1270 def test_shadow_repository_link(
1269 def test_shadow_repository_link(
1271 self, mergeable, pr_util, http_host_only_stub):
1270 self, mergeable, pr_util, http_host_only_stub):
1272 """
1271 """
1273 Check that the pull request summary page displays a link to the shadow
1272 Check that the pull request summary page displays a link to the shadow
1274 repository if the pull request is mergeable. If it is not mergeable
1273 repository if the pull request is mergeable. If it is not mergeable
1275 the link should not be displayed.
1274 the link should not be displayed.
1276 """
1275 """
1277 pull_request = pr_util.create_pull_request(
1276 pull_request = pr_util.create_pull_request(
1278 mergeable=mergeable, enable_notifications=False)
1277 mergeable=mergeable, enable_notifications=False)
1279 target_repo = pull_request.target_repo.scm_instance()
1278 target_repo = pull_request.target_repo.scm_instance()
1280 pr_id = pull_request.pull_request_id
1279 pr_id = pull_request.pull_request_id
1281 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1280 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1282 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1281 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1283
1282
1284 response = self.app.get(route_path(
1283 response = self.app.get(route_path(
1285 'pullrequest_show',
1284 'pullrequest_show',
1286 repo_name=target_repo.name,
1285 repo_name=target_repo.name,
1287 pull_request_id=pr_id))
1286 pull_request_id=pr_id))
1288
1287
1289 if mergeable:
1288 if mergeable:
1290 response.assert_response().element_value_contains(
1289 response.assert_response().element_value_contains(
1291 'input.pr-mergeinfo', shadow_url)
1290 'input.pr-mergeinfo', shadow_url)
1292 response.assert_response().element_value_contains(
1291 response.assert_response().element_value_contains(
1293 'input.pr-mergeinfo ', 'pr-merge')
1292 'input.pr-mergeinfo ', 'pr-merge')
1294 else:
1293 else:
1295 response.assert_response().no_element_exists('.pr-mergeinfo')
1294 response.assert_response().no_element_exists('.pr-mergeinfo')
1296
1295
1297
1296
1298 @pytest.mark.usefixtures('app')
1297 @pytest.mark.usefixtures('app')
1299 @pytest.mark.backends("git", "hg")
1298 @pytest.mark.backends("git", "hg")
1300 class TestPullrequestsControllerDelete(object):
1299 class TestPullrequestsControllerDelete(object):
1301 def test_pull_request_delete_button_permissions_admin(
1300 def test_pull_request_delete_button_permissions_admin(
1302 self, autologin_user, user_admin, pr_util):
1301 self, autologin_user, user_admin, pr_util):
1303 pull_request = pr_util.create_pull_request(
1302 pull_request = pr_util.create_pull_request(
1304 author=user_admin.username, enable_notifications=False)
1303 author=user_admin.username, enable_notifications=False)
1305
1304
1306 response = self.app.get(route_path(
1305 response = self.app.get(route_path(
1307 'pullrequest_show',
1306 'pullrequest_show',
1308 repo_name=pull_request.target_repo.scm_instance().name,
1307 repo_name=pull_request.target_repo.scm_instance().name,
1309 pull_request_id=pull_request.pull_request_id))
1308 pull_request_id=pull_request.pull_request_id))
1310
1309
1311 response.mustcontain('id="delete_pullrequest"')
1310 response.mustcontain('id="delete_pullrequest"')
1312 response.mustcontain('Confirm to delete this pull request')
1311 response.mustcontain('Confirm to delete this pull request')
1313
1312
1314 def test_pull_request_delete_button_permissions_owner(
1313 def test_pull_request_delete_button_permissions_owner(
1315 self, autologin_regular_user, user_regular, pr_util):
1314 self, autologin_regular_user, user_regular, pr_util):
1316 pull_request = pr_util.create_pull_request(
1315 pull_request = pr_util.create_pull_request(
1317 author=user_regular.username, enable_notifications=False)
1316 author=user_regular.username, enable_notifications=False)
1318
1317
1319 response = self.app.get(route_path(
1318 response = self.app.get(route_path(
1320 'pullrequest_show',
1319 'pullrequest_show',
1321 repo_name=pull_request.target_repo.scm_instance().name,
1320 repo_name=pull_request.target_repo.scm_instance().name,
1322 pull_request_id=pull_request.pull_request_id))
1321 pull_request_id=pull_request.pull_request_id))
1323
1322
1324 response.mustcontain('id="delete_pullrequest"')
1323 response.mustcontain('id="delete_pullrequest"')
1325 response.mustcontain('Confirm to delete this pull request')
1324 response.mustcontain('Confirm to delete this pull request')
1326
1325
1327 def test_pull_request_delete_button_permissions_forbidden(
1326 def test_pull_request_delete_button_permissions_forbidden(
1328 self, autologin_regular_user, user_regular, user_admin, pr_util):
1327 self, autologin_regular_user, user_regular, user_admin, pr_util):
1329 pull_request = pr_util.create_pull_request(
1328 pull_request = pr_util.create_pull_request(
1330 author=user_admin.username, enable_notifications=False)
1329 author=user_admin.username, enable_notifications=False)
1331
1330
1332 response = self.app.get(route_path(
1331 response = self.app.get(route_path(
1333 'pullrequest_show',
1332 'pullrequest_show',
1334 repo_name=pull_request.target_repo.scm_instance().name,
1333 repo_name=pull_request.target_repo.scm_instance().name,
1335 pull_request_id=pull_request.pull_request_id))
1334 pull_request_id=pull_request.pull_request_id))
1336 response.mustcontain(no=['id="delete_pullrequest"'])
1335 response.mustcontain(no=['id="delete_pullrequest"'])
1337 response.mustcontain(no=['Confirm to delete this pull request'])
1336 response.mustcontain(no=['Confirm to delete this pull request'])
1338
1337
1339 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1338 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1340 self, autologin_regular_user, user_regular, user_admin, pr_util,
1339 self, autologin_regular_user, user_regular, user_admin, pr_util,
1341 user_util):
1340 user_util):
1342
1341
1343 pull_request = pr_util.create_pull_request(
1342 pull_request = pr_util.create_pull_request(
1344 author=user_admin.username, enable_notifications=False)
1343 author=user_admin.username, enable_notifications=False)
1345
1344
1346 user_util.grant_user_permission_to_repo(
1345 user_util.grant_user_permission_to_repo(
1347 pull_request.target_repo, user_regular,
1346 pull_request.target_repo, user_regular,
1348 'repository.write')
1347 'repository.write')
1349
1348
1350 response = self.app.get(route_path(
1349 response = self.app.get(route_path(
1351 'pullrequest_show',
1350 'pullrequest_show',
1352 repo_name=pull_request.target_repo.scm_instance().name,
1351 repo_name=pull_request.target_repo.scm_instance().name,
1353 pull_request_id=pull_request.pull_request_id))
1352 pull_request_id=pull_request.pull_request_id))
1354
1353
1355 response.mustcontain('id="open_edit_pullrequest"')
1354 response.mustcontain('id="open_edit_pullrequest"')
1356 response.mustcontain('id="delete_pullrequest"')
1355 response.mustcontain('id="delete_pullrequest"')
1357 response.mustcontain(no=['Confirm to delete this pull request'])
1356 response.mustcontain(no=['Confirm to delete this pull request'])
1358
1357
1359 def test_delete_comment_returns_404_if_comment_does_not_exist(
1358 def test_delete_comment_returns_404_if_comment_does_not_exist(
1360 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1359 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1361
1360
1362 pull_request = pr_util.create_pull_request(
1361 pull_request = pr_util.create_pull_request(
1363 author=user_admin.username, enable_notifications=False)
1362 author=user_admin.username, enable_notifications=False)
1364
1363
1365 self.app.post(
1364 self.app.post(
1366 route_path(
1365 route_path(
1367 'pullrequest_comment_delete',
1366 'pullrequest_comment_delete',
1368 repo_name=pull_request.target_repo.scm_instance().name,
1367 repo_name=pull_request.target_repo.scm_instance().name,
1369 pull_request_id=pull_request.pull_request_id,
1368 pull_request_id=pull_request.pull_request_id,
1370 comment_id=1024404),
1369 comment_id=1024404),
1371 extra_environ=xhr_header,
1370 extra_environ=xhr_header,
1372 params={'csrf_token': csrf_token},
1371 params={'csrf_token': csrf_token},
1373 status=404
1372 status=404
1374 )
1373 )
1375
1374
1376 def test_delete_comment(
1375 def test_delete_comment(
1377 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1376 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1378
1377
1379 pull_request = pr_util.create_pull_request(
1378 pull_request = pr_util.create_pull_request(
1380 author=user_admin.username, enable_notifications=False)
1379 author=user_admin.username, enable_notifications=False)
1381 comment = pr_util.create_comment()
1380 comment = pr_util.create_comment()
1382 comment_id = comment.comment_id
1381 comment_id = comment.comment_id
1383
1382
1384 response = self.app.post(
1383 response = self.app.post(
1385 route_path(
1384 route_path(
1386 'pullrequest_comment_delete',
1385 'pullrequest_comment_delete',
1387 repo_name=pull_request.target_repo.scm_instance().name,
1386 repo_name=pull_request.target_repo.scm_instance().name,
1388 pull_request_id=pull_request.pull_request_id,
1387 pull_request_id=pull_request.pull_request_id,
1389 comment_id=comment_id),
1388 comment_id=comment_id),
1390 extra_environ=xhr_header,
1389 extra_environ=xhr_header,
1391 params={'csrf_token': csrf_token},
1390 params={'csrf_token': csrf_token},
1392 status=200
1391 status=200
1393 )
1392 )
1394 assert response.body == 'true'
1393 assert response.body == 'true'
1395
1394
1396 @pytest.mark.parametrize('url_type', [
1395 @pytest.mark.parametrize('url_type', [
1397 'pullrequest_new',
1396 'pullrequest_new',
1398 'pullrequest_create',
1397 'pullrequest_create',
1399 'pullrequest_update',
1398 'pullrequest_update',
1400 'pullrequest_merge',
1399 'pullrequest_merge',
1401 ])
1400 ])
1402 def test_pull_request_is_forbidden_on_archived_repo(
1401 def test_pull_request_is_forbidden_on_archived_repo(
1403 self, autologin_user, backend, xhr_header, user_util, url_type):
1402 self, autologin_user, backend, xhr_header, user_util, url_type):
1404
1403
1405 # create a temporary repo
1404 # create a temporary repo
1406 source = user_util.create_repo(repo_type=backend.alias)
1405 source = user_util.create_repo(repo_type=backend.alias)
1407 repo_name = source.repo_name
1406 repo_name = source.repo_name
1408 repo = Repository.get_by_repo_name(repo_name)
1407 repo = Repository.get_by_repo_name(repo_name)
1409 repo.archived = True
1408 repo.archived = True
1410 Session().commit()
1409 Session().commit()
1411
1410
1412 response = self.app.get(
1411 response = self.app.get(
1413 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1412 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1414
1413
1415 msg = 'Action not supported for archived repository.'
1414 msg = 'Action not supported for archived repository.'
1416 assert_session_flash(response, msg)
1415 assert_session_flash(response, msg)
1417
1416
1418
1417
1419 def assert_pull_request_status(pull_request, expected_status):
1418 def assert_pull_request_status(pull_request, expected_status):
1420 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1419 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1421 assert status == expected_status
1420 assert status == expected_status
1422
1421
1423
1422
1424 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1423 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1425 @pytest.mark.usefixtures("autologin_user")
1424 @pytest.mark.usefixtures("autologin_user")
1426 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1425 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1427 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
1426 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,705 +1,717 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 import collections
24
23
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37
37
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 import rhodecode.lib.helpers as h
43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 ChangesetCommentHistory
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def _update_with_GET(params, request):
58 def _update_with_GET(params, request):
59 for k in ['diff1', 'diff2', 'diff']:
59 for k in ['diff1', 'diff2', 'diff']:
60 params[k] += request.GET.getall(k)
60 params[k] += request.GET.getall(k)
61
61
62
62
63 class RepoCommitsView(RepoAppView):
63 class RepoCommitsView(RepoAppView):
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 c.rhodecode_repo = self.rhodecode_vcs_repo
66 c.rhodecode_repo = self.rhodecode_vcs_repo
67
67
68 return c
68 return c
69
69
70 def _is_diff_cache_enabled(self, target_repo):
70 def _is_diff_cache_enabled(self, target_repo):
71 caching_enabled = self._get_general_setting(
71 caching_enabled = self._get_general_setting(
72 target_repo, 'rhodecode_diff_cache')
72 target_repo, 'rhodecode_diff_cache')
73 log.debug('Diff caching enabled: %s', caching_enabled)
73 log.debug('Diff caching enabled: %s', caching_enabled)
74 return caching_enabled
74 return caching_enabled
75
75
76 def _commit(self, commit_id_range, method):
76 def _commit(self, commit_id_range, method):
77 _ = self.request.translate
77 _ = self.request.translate
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
80
80
81 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
82 diff_context = get_diff_context(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84
84
85 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
86 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
87 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
88 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
89
89
90 # get ranges of commit ids if preset
90 # get ranges of commit ids if preset
91 commit_range = commit_id_range.split('...')[:2]
91 commit_range = commit_id_range.split('...')[:2]
92
92
93 try:
93 try:
94 pre_load = ['affected_files', 'author', 'branch', 'date',
94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 'message', 'parents']
95 'message', 'parents']
96 if self.rhodecode_vcs_repo.alias == 'hg':
96 if self.rhodecode_vcs_repo.alias == 'hg':
97 pre_load += ['hidden', 'obsolete', 'phase']
97 pre_load += ['hidden', 'obsolete', 'phase']
98
98
99 if len(commit_range) == 2:
99 if len(commit_range) == 2:
100 commits = self.rhodecode_vcs_repo.get_commits(
100 commits = self.rhodecode_vcs_repo.get_commits(
101 start_id=commit_range[0], end_id=commit_range[1],
101 start_id=commit_range[0], end_id=commit_range[1],
102 pre_load=pre_load, translate_tags=False)
102 pre_load=pre_load, translate_tags=False)
103 commits = list(commits)
103 commits = list(commits)
104 else:
104 else:
105 commits = [self.rhodecode_vcs_repo.get_commit(
105 commits = [self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id_range, pre_load=pre_load)]
106 commit_id=commit_id_range, pre_load=pre_load)]
107
107
108 c.commit_ranges = commits
108 c.commit_ranges = commits
109 if not c.commit_ranges:
109 if not c.commit_ranges:
110 raise RepositoryError('The commit range returned an empty result')
110 raise RepositoryError('The commit range returned an empty result')
111 except CommitDoesNotExistError as e:
111 except CommitDoesNotExistError as e:
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 h.flash(msg, category='error')
113 h.flash(msg, category='error')
114 raise HTTPNotFound()
114 raise HTTPNotFound()
115 except Exception:
115 except Exception:
116 log.exception("General failure")
116 log.exception("General failure")
117 raise HTTPNotFound()
117 raise HTTPNotFound()
118
118
119 c.changes = OrderedDict()
119 c.changes = OrderedDict()
120 c.lines_added = 0
120 c.lines_added = 0
121 c.lines_deleted = 0
121 c.lines_deleted = 0
122
122
123 # auto collapse if we have more than limit
123 # auto collapse if we have more than limit
124 collapse_limit = diffs.DiffProcessor._collapse_commits_over
124 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
125 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126
126
127 c.commit_statuses = ChangesetStatus.STATUSES
127 c.commit_statuses = ChangesetStatus.STATUSES
128 c.inline_comments = []
128 c.inline_comments = []
129 c.files = []
129 c.files = []
130
130
131 c.statuses = []
131 c.statuses = []
132 c.comments = []
132 c.comments = []
133 c.unresolved_comments = []
133 c.unresolved_comments = []
134 c.resolved_comments = []
134 c.resolved_comments = []
135 if len(c.commit_ranges) == 1:
135 if len(c.commit_ranges) == 1:
136 commit = c.commit_ranges[0]
136 commit = c.commit_ranges[0]
137 c.comments = CommentsModel().get_comments(
137 c.comments = CommentsModel().get_comments(
138 self.db_repo.repo_id,
138 self.db_repo.repo_id,
139 revision=commit.raw_id)
139 revision=commit.raw_id)
140 c.statuses.append(ChangesetStatusModel().get_status(
140 c.statuses.append(ChangesetStatusModel().get_status(
141 self.db_repo.repo_id, commit.raw_id))
141 self.db_repo.repo_id, commit.raw_id))
142 # comments from PR
142 # comments from PR
143 statuses = ChangesetStatusModel().get_statuses(
143 statuses = ChangesetStatusModel().get_statuses(
144 self.db_repo.repo_id, commit.raw_id,
144 self.db_repo.repo_id, commit.raw_id,
145 with_revisions=True)
145 with_revisions=True)
146 prs = set(st.pull_request for st in statuses
146 prs = set(st.pull_request for st in statuses
147 if st.pull_request is not None)
147 if st.pull_request is not None)
148 # from associated statuses, check the pull requests, and
148 # from associated statuses, check the pull requests, and
149 # show comments from them
149 # show comments from them
150 for pr in prs:
150 for pr in prs:
151 c.comments.extend(pr.comments)
151 c.comments.extend(pr.comments)
152
152
153 c.unresolved_comments = CommentsModel()\
153 c.unresolved_comments = CommentsModel()\
154 .get_commit_unresolved_todos(commit.raw_id)
154 .get_commit_unresolved_todos(commit.raw_id)
155 c.resolved_comments = CommentsModel()\
155 c.resolved_comments = CommentsModel()\
156 .get_commit_resolved_todos(commit.raw_id)
156 .get_commit_resolved_todos(commit.raw_id)
157
157
158 diff = None
158 diff = None
159 # Iterate over ranges (default commit view is always one commit)
159 # Iterate over ranges (default commit view is always one commit)
160 for commit in c.commit_ranges:
160 for commit in c.commit_ranges:
161 c.changes[commit.raw_id] = []
161 c.changes[commit.raw_id] = []
162
162
163 commit2 = commit
163 commit2 = commit
164 commit1 = commit.first_parent
164 commit1 = commit.first_parent
165
165
166 if method == 'show':
166 if method == 'show':
167 inline_comments = CommentsModel().get_inline_comments(
167 inline_comments = CommentsModel().get_inline_comments(
168 self.db_repo.repo_id, revision=commit.raw_id)
168 self.db_repo.repo_id, revision=commit.raw_id)
169 c.inline_cnt = CommentsModel().get_inline_comments_count(
169 c.inline_cnt = CommentsModel().get_inline_comments_count(
170 inline_comments)
170 inline_comments)
171 c.inline_comments = inline_comments
171 c.inline_comments = inline_comments
172
172
173 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
173 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
174 self.db_repo)
174 self.db_repo)
175 cache_file_path = diff_cache_exist(
175 cache_file_path = diff_cache_exist(
176 cache_path, 'diff', commit.raw_id,
176 cache_path, 'diff', commit.raw_id,
177 hide_whitespace_changes, diff_context, c.fulldiff)
177 hide_whitespace_changes, diff_context, c.fulldiff)
178
178
179 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
179 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
180 force_recache = str2bool(self.request.GET.get('force_recache'))
180 force_recache = str2bool(self.request.GET.get('force_recache'))
181
181
182 cached_diff = None
182 cached_diff = None
183 if caching_enabled:
183 if caching_enabled:
184 cached_diff = load_cached_diff(cache_file_path)
184 cached_diff = load_cached_diff(cache_file_path)
185
185
186 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
186 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
187 if not force_recache and has_proper_diff_cache:
187 if not force_recache and has_proper_diff_cache:
188 diffset = cached_diff['diff']
188 diffset = cached_diff['diff']
189 else:
189 else:
190 vcs_diff = self.rhodecode_vcs_repo.get_diff(
190 vcs_diff = self.rhodecode_vcs_repo.get_diff(
191 commit1, commit2,
191 commit1, commit2,
192 ignore_whitespace=hide_whitespace_changes,
192 ignore_whitespace=hide_whitespace_changes,
193 context=diff_context)
193 context=diff_context)
194
194
195 diff_processor = diffs.DiffProcessor(
195 diff_processor = diffs.DiffProcessor(
196 vcs_diff, format='newdiff', diff_limit=diff_limit,
196 vcs_diff, format='newdiff', diff_limit=diff_limit,
197 file_limit=file_limit, show_full_diff=c.fulldiff)
197 file_limit=file_limit, show_full_diff=c.fulldiff)
198
198
199 _parsed = diff_processor.prepare()
199 _parsed = diff_processor.prepare()
200
200
201 diffset = codeblocks.DiffSet(
201 diffset = codeblocks.DiffSet(
202 repo_name=self.db_repo_name,
202 repo_name=self.db_repo_name,
203 source_node_getter=codeblocks.diffset_node_getter(commit1),
203 source_node_getter=codeblocks.diffset_node_getter(commit1),
204 target_node_getter=codeblocks.diffset_node_getter(commit2))
204 target_node_getter=codeblocks.diffset_node_getter(commit2))
205
205
206 diffset = self.path_filter.render_patchset_filtered(
206 diffset = self.path_filter.render_patchset_filtered(
207 diffset, _parsed, commit1.raw_id, commit2.raw_id)
207 diffset, _parsed, commit1.raw_id, commit2.raw_id)
208
208
209 # save cached diff
209 # save cached diff
210 if caching_enabled:
210 if caching_enabled:
211 cache_diff(cache_file_path, diffset, None)
211 cache_diff(cache_file_path, diffset, None)
212
212
213 c.limited_diff = diffset.limited_diff
213 c.limited_diff = diffset.limited_diff
214 c.changes[commit.raw_id] = diffset
214 c.changes[commit.raw_id] = diffset
215 else:
215 else:
216 # TODO(marcink): no cache usage here...
216 # TODO(marcink): no cache usage here...
217 _diff = self.rhodecode_vcs_repo.get_diff(
217 _diff = self.rhodecode_vcs_repo.get_diff(
218 commit1, commit2,
218 commit1, commit2,
219 ignore_whitespace=hide_whitespace_changes, context=diff_context)
219 ignore_whitespace=hide_whitespace_changes, context=diff_context)
220 diff_processor = diffs.DiffProcessor(
220 diff_processor = diffs.DiffProcessor(
221 _diff, format='newdiff', diff_limit=diff_limit,
221 _diff, format='newdiff', diff_limit=diff_limit,
222 file_limit=file_limit, show_full_diff=c.fulldiff)
222 file_limit=file_limit, show_full_diff=c.fulldiff)
223 # downloads/raw we only need RAW diff nothing else
223 # downloads/raw we only need RAW diff nothing else
224 diff = self.path_filter.get_raw_patch(diff_processor)
224 diff = self.path_filter.get_raw_patch(diff_processor)
225 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
225 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
226
226
227 # sort comments by how they were generated
227 # sort comments by how they were generated
228 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
228 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
229
229
230 if len(c.commit_ranges) == 1:
230 if len(c.commit_ranges) == 1:
231 c.commit = c.commit_ranges[0]
231 c.commit = c.commit_ranges[0]
232 c.parent_tmpl = ''.join(
232 c.parent_tmpl = ''.join(
233 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
233 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
234
234
235 if method == 'download':
235 if method == 'download':
236 response = Response(diff)
236 response = Response(diff)
237 response.content_type = 'text/plain'
237 response.content_type = 'text/plain'
238 response.content_disposition = (
238 response.content_disposition = (
239 'attachment; filename=%s.diff' % commit_id_range[:12])
239 'attachment; filename=%s.diff' % commit_id_range[:12])
240 return response
240 return response
241 elif method == 'patch':
241 elif method == 'patch':
242 c.diff = safe_unicode(diff)
242 c.diff = safe_unicode(diff)
243 patch = render(
243 patch = render(
244 'rhodecode:templates/changeset/patch_changeset.mako',
244 'rhodecode:templates/changeset/patch_changeset.mako',
245 self._get_template_context(c), self.request)
245 self._get_template_context(c), self.request)
246 response = Response(patch)
246 response = Response(patch)
247 response.content_type = 'text/plain'
247 response.content_type = 'text/plain'
248 return response
248 return response
249 elif method == 'raw':
249 elif method == 'raw':
250 response = Response(diff)
250 response = Response(diff)
251 response.content_type = 'text/plain'
251 response.content_type = 'text/plain'
252 return response
252 return response
253 elif method == 'show':
253 elif method == 'show':
254 if len(c.commit_ranges) == 1:
254 if len(c.commit_ranges) == 1:
255 html = render(
255 html = render(
256 'rhodecode:templates/changeset/changeset.mako',
256 'rhodecode:templates/changeset/changeset.mako',
257 self._get_template_context(c), self.request)
257 self._get_template_context(c), self.request)
258 return Response(html)
258 return Response(html)
259 else:
259 else:
260 c.ancestor = None
260 c.ancestor = None
261 c.target_repo = self.db_repo
261 c.target_repo = self.db_repo
262 html = render(
262 html = render(
263 'rhodecode:templates/changeset/changeset_range.mako',
263 'rhodecode:templates/changeset/changeset_range.mako',
264 self._get_template_context(c), self.request)
264 self._get_template_context(c), self.request)
265 return Response(html)
265 return Response(html)
266
266
267 raise HTTPBadRequest()
267 raise HTTPBadRequest()
268
268
269 @LoginRequired()
269 @LoginRequired()
270 @HasRepoPermissionAnyDecorator(
270 @HasRepoPermissionAnyDecorator(
271 'repository.read', 'repository.write', 'repository.admin')
271 'repository.read', 'repository.write', 'repository.admin')
272 @view_config(
272 @view_config(
273 route_name='repo_commit', request_method='GET',
273 route_name='repo_commit', request_method='GET',
274 renderer=None)
274 renderer=None)
275 def repo_commit_show(self):
275 def repo_commit_show(self):
276 commit_id = self.request.matchdict['commit_id']
276 commit_id = self.request.matchdict['commit_id']
277 return self._commit(commit_id, method='show')
277 return self._commit(commit_id, method='show')
278
278
279 @LoginRequired()
279 @LoginRequired()
280 @HasRepoPermissionAnyDecorator(
280 @HasRepoPermissionAnyDecorator(
281 'repository.read', 'repository.write', 'repository.admin')
281 'repository.read', 'repository.write', 'repository.admin')
282 @view_config(
282 @view_config(
283 route_name='repo_commit_raw', request_method='GET',
283 route_name='repo_commit_raw', request_method='GET',
284 renderer=None)
284 renderer=None)
285 @view_config(
285 @view_config(
286 route_name='repo_commit_raw_deprecated', request_method='GET',
286 route_name='repo_commit_raw_deprecated', request_method='GET',
287 renderer=None)
287 renderer=None)
288 def repo_commit_raw(self):
288 def repo_commit_raw(self):
289 commit_id = self.request.matchdict['commit_id']
289 commit_id = self.request.matchdict['commit_id']
290 return self._commit(commit_id, method='raw')
290 return self._commit(commit_id, method='raw')
291
291
292 @LoginRequired()
292 @LoginRequired()
293 @HasRepoPermissionAnyDecorator(
293 @HasRepoPermissionAnyDecorator(
294 'repository.read', 'repository.write', 'repository.admin')
294 'repository.read', 'repository.write', 'repository.admin')
295 @view_config(
295 @view_config(
296 route_name='repo_commit_patch', request_method='GET',
296 route_name='repo_commit_patch', request_method='GET',
297 renderer=None)
297 renderer=None)
298 def repo_commit_patch(self):
298 def repo_commit_patch(self):
299 commit_id = self.request.matchdict['commit_id']
299 commit_id = self.request.matchdict['commit_id']
300 return self._commit(commit_id, method='patch')
300 return self._commit(commit_id, method='patch')
301
301
302 @LoginRequired()
302 @LoginRequired()
303 @HasRepoPermissionAnyDecorator(
303 @HasRepoPermissionAnyDecorator(
304 'repository.read', 'repository.write', 'repository.admin')
304 'repository.read', 'repository.write', 'repository.admin')
305 @view_config(
305 @view_config(
306 route_name='repo_commit_download', request_method='GET',
306 route_name='repo_commit_download', request_method='GET',
307 renderer=None)
307 renderer=None)
308 def repo_commit_download(self):
308 def repo_commit_download(self):
309 commit_id = self.request.matchdict['commit_id']
309 commit_id = self.request.matchdict['commit_id']
310 return self._commit(commit_id, method='download')
310 return self._commit(commit_id, method='download')
311
311
312 @LoginRequired()
312 @LoginRequired()
313 @NotAnonymous()
313 @NotAnonymous()
314 @HasRepoPermissionAnyDecorator(
314 @HasRepoPermissionAnyDecorator(
315 'repository.read', 'repository.write', 'repository.admin')
315 'repository.read', 'repository.write', 'repository.admin')
316 @CSRFRequired()
316 @CSRFRequired()
317 @view_config(
317 @view_config(
318 route_name='repo_commit_comment_create', request_method='POST',
318 route_name='repo_commit_comment_create', request_method='POST',
319 renderer='json_ext')
319 renderer='json_ext')
320 def repo_commit_comment_create(self):
320 def repo_commit_comment_create(self):
321 _ = self.request.translate
321 _ = self.request.translate
322 commit_id = self.request.matchdict['commit_id']
322 commit_id = self.request.matchdict['commit_id']
323
323
324 c = self.load_default_context()
324 c = self.load_default_context()
325 status = self.request.POST.get('changeset_status', None)
325 status = self.request.POST.get('changeset_status', None)
326 text = self.request.POST.get('text')
326 text = self.request.POST.get('text')
327 comment_type = self.request.POST.get('comment_type')
327 comment_type = self.request.POST.get('comment_type')
328 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
328 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
329
329
330 if status:
330 if status:
331 text = text or (_('Status change %(transition_icon)s %(status)s')
331 text = text or (_('Status change %(transition_icon)s %(status)s')
332 % {'transition_icon': '>',
332 % {'transition_icon': '>',
333 'status': ChangesetStatus.get_status_lbl(status)})
333 'status': ChangesetStatus.get_status_lbl(status)})
334
334
335 multi_commit_ids = []
335 multi_commit_ids = []
336 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
336 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
337 if _commit_id not in ['', None, EmptyCommit.raw_id]:
337 if _commit_id not in ['', None, EmptyCommit.raw_id]:
338 if _commit_id not in multi_commit_ids:
338 if _commit_id not in multi_commit_ids:
339 multi_commit_ids.append(_commit_id)
339 multi_commit_ids.append(_commit_id)
340
340
341 commit_ids = multi_commit_ids or [commit_id]
341 commit_ids = multi_commit_ids or [commit_id]
342
342
343 comment = None
343 comment = None
344 for current_id in filter(None, commit_ids):
344 for current_id in filter(None, commit_ids):
345 comment = CommentsModel().create(
345 comment = CommentsModel().create(
346 text=text,
346 text=text,
347 repo=self.db_repo.repo_id,
347 repo=self.db_repo.repo_id,
348 user=self._rhodecode_db_user.user_id,
348 user=self._rhodecode_db_user.user_id,
349 commit_id=current_id,
349 commit_id=current_id,
350 f_path=self.request.POST.get('f_path'),
350 f_path=self.request.POST.get('f_path'),
351 line_no=self.request.POST.get('line'),
351 line_no=self.request.POST.get('line'),
352 status_change=(ChangesetStatus.get_status_lbl(status)
352 status_change=(ChangesetStatus.get_status_lbl(status)
353 if status else None),
353 if status else None),
354 status_change_type=status,
354 status_change_type=status,
355 comment_type=comment_type,
355 comment_type=comment_type,
356 resolves_comment_id=resolves_comment_id,
356 resolves_comment_id=resolves_comment_id,
357 auth_user=self._rhodecode_user
357 auth_user=self._rhodecode_user
358 )
358 )
359
359
360 # get status if set !
360 # get status if set !
361 if status:
361 if status:
362 # if latest status was from pull request and it's closed
362 # if latest status was from pull request and it's closed
363 # disallow changing status !
363 # disallow changing status !
364 # dont_allow_on_closed_pull_request = True !
364 # dont_allow_on_closed_pull_request = True !
365
365
366 try:
366 try:
367 ChangesetStatusModel().set_status(
367 ChangesetStatusModel().set_status(
368 self.db_repo.repo_id,
368 self.db_repo.repo_id,
369 status,
369 status,
370 self._rhodecode_db_user.user_id,
370 self._rhodecode_db_user.user_id,
371 comment,
371 comment,
372 revision=current_id,
372 revision=current_id,
373 dont_allow_on_closed_pull_request=True
373 dont_allow_on_closed_pull_request=True
374 )
374 )
375 except StatusChangeOnClosedPullRequestError:
375 except StatusChangeOnClosedPullRequestError:
376 msg = _('Changing the status of a commit associated with '
376 msg = _('Changing the status of a commit associated with '
377 'a closed pull request is not allowed')
377 'a closed pull request is not allowed')
378 log.exception(msg)
378 log.exception(msg)
379 h.flash(msg, category='warning')
379 h.flash(msg, category='warning')
380 raise HTTPFound(h.route_path(
380 raise HTTPFound(h.route_path(
381 'repo_commit', repo_name=self.db_repo_name,
381 'repo_commit', repo_name=self.db_repo_name,
382 commit_id=current_id))
382 commit_id=current_id))
383
383
384 commit = self.db_repo.get_commit(current_id)
384 commit = self.db_repo.get_commit(current_id)
385 CommentsModel().trigger_commit_comment_hook(
385 CommentsModel().trigger_commit_comment_hook(
386 self.db_repo, self._rhodecode_user, 'create',
386 self.db_repo, self._rhodecode_user, 'create',
387 data={'comment': comment, 'commit': commit})
387 data={'comment': comment, 'commit': commit})
388
388
389 # finalize, commit and redirect
389 # finalize, commit and redirect
390 Session().commit()
390 Session().commit()
391
391
392 data = {
392 data = {
393 'target_id': h.safeid(h.safe_unicode(
393 'target_id': h.safeid(h.safe_unicode(
394 self.request.POST.get('f_path'))),
394 self.request.POST.get('f_path'))),
395 }
395 }
396 if comment:
396 if comment:
397 c.co = comment
397 c.co = comment
398 rendered_comment = render(
398 rendered_comment = render(
399 'rhodecode:templates/changeset/changeset_comment_block.mako',
399 'rhodecode:templates/changeset/changeset_comment_block.mako',
400 self._get_template_context(c), self.request)
400 self._get_template_context(c), self.request)
401
401
402 data.update(comment.get_dict())
402 data.update(comment.get_dict())
403 data.update({'rendered_text': rendered_comment})
403 data.update({'rendered_text': rendered_comment})
404
404
405 return data
405 return data
406
406
407 @LoginRequired()
407 @LoginRequired()
408 @NotAnonymous()
408 @NotAnonymous()
409 @HasRepoPermissionAnyDecorator(
409 @HasRepoPermissionAnyDecorator(
410 'repository.read', 'repository.write', 'repository.admin')
410 'repository.read', 'repository.write', 'repository.admin')
411 @CSRFRequired()
411 @CSRFRequired()
412 @view_config(
412 @view_config(
413 route_name='repo_commit_comment_preview', request_method='POST',
413 route_name='repo_commit_comment_preview', request_method='POST',
414 renderer='string', xhr=True)
414 renderer='string', xhr=True)
415 def repo_commit_comment_preview(self):
415 def repo_commit_comment_preview(self):
416 # Technically a CSRF token is not needed as no state changes with this
416 # Technically a CSRF token is not needed as no state changes with this
417 # call. However, as this is a POST is better to have it, so automated
417 # call. However, as this is a POST is better to have it, so automated
418 # tools don't flag it as potential CSRF.
418 # tools don't flag it as potential CSRF.
419 # Post is required because the payload could be bigger than the maximum
419 # Post is required because the payload could be bigger than the maximum
420 # allowed by GET.
420 # allowed by GET.
421
421
422 text = self.request.POST.get('text')
422 text = self.request.POST.get('text')
423 renderer = self.request.POST.get('renderer') or 'rst'
423 renderer = self.request.POST.get('renderer') or 'rst'
424 if text:
424 if text:
425 return h.render(text, renderer=renderer, mentions=True,
425 return h.render(text, renderer=renderer, mentions=True,
426 repo_name=self.db_repo_name)
426 repo_name=self.db_repo_name)
427 return ''
427 return ''
428
428
429 @LoginRequired()
429 @LoginRequired()
430 @NotAnonymous()
430 @NotAnonymous()
431 @HasRepoPermissionAnyDecorator(
431 @HasRepoPermissionAnyDecorator(
432 'repository.read', 'repository.write', 'repository.admin')
432 'repository.read', 'repository.write', 'repository.admin')
433 @CSRFRequired()
433 @CSRFRequired()
434 @view_config(
434 @view_config(
435 route_name='repo_commit_comment_history_view', request_method='POST',
435 route_name='repo_commit_comment_history_view', request_method='POST',
436 renderer='string', xhr=True)
436 renderer='string', xhr=True)
437 def repo_commit_comment_history_view(self):
437 def repo_commit_comment_history_view(self):
438 c = self.load_default_context()
438 c = self.load_default_context()
439
439
440 comment_history_id = self.request.matchdict['comment_history_id']
440 comment_history_id = self.request.matchdict['comment_history_id']
441 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
441 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
442 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
442 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
443
443
444 if is_repo_comment:
444 if is_repo_comment:
445 c.comment_history = comment_history
445 c.comment_history = comment_history
446
446
447 rendered_comment = render(
447 rendered_comment = render(
448 'rhodecode:templates/changeset/comment_history.mako',
448 'rhodecode:templates/changeset/comment_history.mako',
449 self._get_template_context(c)
449 self._get_template_context(c)
450 , self.request)
450 , self.request)
451 return rendered_comment
451 return rendered_comment
452 else:
452 else:
453 log.warning('No permissions for user %s to show comment_history_id: %s',
453 log.warning('No permissions for user %s to show comment_history_id: %s',
454 self._rhodecode_db_user, comment_history_id)
454 self._rhodecode_db_user, comment_history_id)
455 raise HTTPNotFound()
455 raise HTTPNotFound()
456
456
457 @LoginRequired()
457 @LoginRequired()
458 @NotAnonymous()
458 @NotAnonymous()
459 @HasRepoPermissionAnyDecorator(
459 @HasRepoPermissionAnyDecorator(
460 'repository.read', 'repository.write', 'repository.admin')
460 'repository.read', 'repository.write', 'repository.admin')
461 @CSRFRequired()
461 @CSRFRequired()
462 @view_config(
462 @view_config(
463 route_name='repo_commit_comment_attachment_upload', request_method='POST',
463 route_name='repo_commit_comment_attachment_upload', request_method='POST',
464 renderer='json_ext', xhr=True)
464 renderer='json_ext', xhr=True)
465 def repo_commit_comment_attachment_upload(self):
465 def repo_commit_comment_attachment_upload(self):
466 c = self.load_default_context()
466 c = self.load_default_context()
467 upload_key = 'attachment'
467 upload_key = 'attachment'
468
468
469 file_obj = self.request.POST.get(upload_key)
469 file_obj = self.request.POST.get(upload_key)
470
470
471 if file_obj is None:
471 if file_obj is None:
472 self.request.response.status = 400
472 self.request.response.status = 400
473 return {'store_fid': None,
473 return {'store_fid': None,
474 'access_path': None,
474 'access_path': None,
475 'error': '{} data field is missing'.format(upload_key)}
475 'error': '{} data field is missing'.format(upload_key)}
476
476
477 if not hasattr(file_obj, 'filename'):
477 if not hasattr(file_obj, 'filename'):
478 self.request.response.status = 400
478 self.request.response.status = 400
479 return {'store_fid': None,
479 return {'store_fid': None,
480 'access_path': None,
480 'access_path': None,
481 'error': 'filename cannot be read from the data field'}
481 'error': 'filename cannot be read from the data field'}
482
482
483 filename = file_obj.filename
483 filename = file_obj.filename
484 file_display_name = filename
484 file_display_name = filename
485
485
486 metadata = {
486 metadata = {
487 'user_uploaded': {'username': self._rhodecode_user.username,
487 'user_uploaded': {'username': self._rhodecode_user.username,
488 'user_id': self._rhodecode_user.user_id,
488 'user_id': self._rhodecode_user.user_id,
489 'ip': self._rhodecode_user.ip_addr}}
489 'ip': self._rhodecode_user.ip_addr}}
490
490
491 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
491 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
492 allowed_extensions = [
492 allowed_extensions = [
493 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
493 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
494 '.pptx', '.txt', '.xlsx', '.zip']
494 '.pptx', '.txt', '.xlsx', '.zip']
495 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
495 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
496
496
497 try:
497 try:
498 storage = store_utils.get_file_storage(self.request.registry.settings)
498 storage = store_utils.get_file_storage(self.request.registry.settings)
499 store_uid, metadata = storage.save_file(
499 store_uid, metadata = storage.save_file(
500 file_obj.file, filename, extra_metadata=metadata,
500 file_obj.file, filename, extra_metadata=metadata,
501 extensions=allowed_extensions, max_filesize=max_file_size)
501 extensions=allowed_extensions, max_filesize=max_file_size)
502 except FileNotAllowedException:
502 except FileNotAllowedException:
503 self.request.response.status = 400
503 self.request.response.status = 400
504 permitted_extensions = ', '.join(allowed_extensions)
504 permitted_extensions = ', '.join(allowed_extensions)
505 error_msg = 'File `{}` is not allowed. ' \
505 error_msg = 'File `{}` is not allowed. ' \
506 'Only following extensions are permitted: {}'.format(
506 'Only following extensions are permitted: {}'.format(
507 filename, permitted_extensions)
507 filename, permitted_extensions)
508 return {'store_fid': None,
508 return {'store_fid': None,
509 'access_path': None,
509 'access_path': None,
510 'error': error_msg}
510 'error': error_msg}
511 except FileOverSizeException:
511 except FileOverSizeException:
512 self.request.response.status = 400
512 self.request.response.status = 400
513 limit_mb = h.format_byte_size_binary(max_file_size)
513 limit_mb = h.format_byte_size_binary(max_file_size)
514 return {'store_fid': None,
514 return {'store_fid': None,
515 'access_path': None,
515 'access_path': None,
516 'error': 'File {} is exceeding allowed limit of {}.'.format(
516 'error': 'File {} is exceeding allowed limit of {}.'.format(
517 filename, limit_mb)}
517 filename, limit_mb)}
518
518
519 try:
519 try:
520 entry = FileStore.create(
520 entry = FileStore.create(
521 file_uid=store_uid, filename=metadata["filename"],
521 file_uid=store_uid, filename=metadata["filename"],
522 file_hash=metadata["sha256"], file_size=metadata["size"],
522 file_hash=metadata["sha256"], file_size=metadata["size"],
523 file_display_name=file_display_name,
523 file_display_name=file_display_name,
524 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
524 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
525 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
525 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
526 scope_repo_id=self.db_repo.repo_id
526 scope_repo_id=self.db_repo.repo_id
527 )
527 )
528 Session().add(entry)
528 Session().add(entry)
529 Session().commit()
529 Session().commit()
530 log.debug('Stored upload in DB as %s', entry)
530 log.debug('Stored upload in DB as %s', entry)
531 except Exception:
531 except Exception:
532 log.exception('Failed to store file %s', filename)
532 log.exception('Failed to store file %s', filename)
533 self.request.response.status = 400
533 self.request.response.status = 400
534 return {'store_fid': None,
534 return {'store_fid': None,
535 'access_path': None,
535 'access_path': None,
536 'error': 'File {} failed to store in DB.'.format(filename)}
536 'error': 'File {} failed to store in DB.'.format(filename)}
537
537
538 Session().commit()
538 Session().commit()
539
539
540 return {
540 return {
541 'store_fid': store_uid,
541 'store_fid': store_uid,
542 'access_path': h.route_path(
542 'access_path': h.route_path(
543 'download_file', fid=store_uid),
543 'download_file', fid=store_uid),
544 'fqn_access_path': h.route_url(
544 'fqn_access_path': h.route_url(
545 'download_file', fid=store_uid),
545 'download_file', fid=store_uid),
546 'repo_access_path': h.route_path(
546 'repo_access_path': h.route_path(
547 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
547 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
548 'repo_fqn_access_path': h.route_url(
548 'repo_fqn_access_path': h.route_url(
549 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
549 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
550 }
550 }
551
551
552 @LoginRequired()
552 @LoginRequired()
553 @NotAnonymous()
553 @NotAnonymous()
554 @HasRepoPermissionAnyDecorator(
554 @HasRepoPermissionAnyDecorator(
555 'repository.read', 'repository.write', 'repository.admin')
555 'repository.read', 'repository.write', 'repository.admin')
556 @CSRFRequired()
556 @CSRFRequired()
557 @view_config(
557 @view_config(
558 route_name='repo_commit_comment_delete', request_method='POST',
558 route_name='repo_commit_comment_delete', request_method='POST',
559 renderer='json_ext')
559 renderer='json_ext')
560 def repo_commit_comment_delete(self):
560 def repo_commit_comment_delete(self):
561 commit_id = self.request.matchdict['commit_id']
561 commit_id = self.request.matchdict['commit_id']
562 comment_id = self.request.matchdict['comment_id']
562 comment_id = self.request.matchdict['comment_id']
563
563
564 comment = ChangesetComment.get_or_404(comment_id)
564 comment = ChangesetComment.get_or_404(comment_id)
565 if not comment:
565 if not comment:
566 log.debug('Comment with id:%s not found, skipping', comment_id)
566 log.debug('Comment with id:%s not found, skipping', comment_id)
567 # comment already deleted in another call probably
567 # comment already deleted in another call probably
568 return True
568 return True
569
569
570 if comment.immutable:
570 if comment.immutable:
571 # don't allow deleting comments that are immutable
571 # don't allow deleting comments that are immutable
572 raise HTTPForbidden()
572 raise HTTPForbidden()
573
573
574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
575 super_admin = h.HasPermissionAny('hg.admin')()
575 super_admin = h.HasPermissionAny('hg.admin')()
576 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
576 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
577 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
577 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
578 comment_repo_admin = is_repo_admin and is_repo_comment
578 comment_repo_admin = is_repo_admin and is_repo_comment
579
579
580 if super_admin or comment_owner or comment_repo_admin:
580 if super_admin or comment_owner or comment_repo_admin:
581 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
581 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
582 Session().commit()
582 Session().commit()
583 return True
583 return True
584 else:
584 else:
585 log.warning('No permissions for user %s to delete comment_id: %s',
585 log.warning('No permissions for user %s to delete comment_id: %s',
586 self._rhodecode_db_user, comment_id)
586 self._rhodecode_db_user, comment_id)
587 raise HTTPNotFound()
587 raise HTTPNotFound()
588
588
589 @LoginRequired()
589 @LoginRequired()
590 @NotAnonymous()
590 @NotAnonymous()
591 @HasRepoPermissionAnyDecorator(
591 @HasRepoPermissionAnyDecorator(
592 'repository.read', 'repository.write', 'repository.admin')
592 'repository.read', 'repository.write', 'repository.admin')
593 @CSRFRequired()
593 @CSRFRequired()
594 @view_config(
594 @view_config(
595 route_name='repo_commit_comment_edit', request_method='POST',
595 route_name='repo_commit_comment_edit', request_method='POST',
596 renderer='json_ext')
596 renderer='json_ext')
597 def repo_commit_comment_edit(self):
597 def repo_commit_comment_edit(self):
598 self.load_default_context()
599
598 comment_id = self.request.matchdict['comment_id']
600 comment_id = self.request.matchdict['comment_id']
599 comment = ChangesetComment.get_or_404(comment_id)
601 comment = ChangesetComment.get_or_404(comment_id)
600
602
601 if comment.immutable:
603 if comment.immutable:
602 # don't allow deleting comments that are immutable
604 # don't allow deleting comments that are immutable
603 raise HTTPForbidden()
605 raise HTTPForbidden()
604
606
605 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
607 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
606 super_admin = h.HasPermissionAny('hg.admin')()
608 super_admin = h.HasPermissionAny('hg.admin')()
607 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
609 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
608 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
610 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
609 comment_repo_admin = is_repo_admin and is_repo_comment
611 comment_repo_admin = is_repo_admin and is_repo_comment
610
612
611 if super_admin or comment_owner or comment_repo_admin:
613 if super_admin or comment_owner or comment_repo_admin:
612 text = self.request.POST.get('text')
614 text = self.request.POST.get('text')
613 version = self.request.POST.get('version')
615 version = self.request.POST.get('version')
614 if text == comment.text:
616 if text == comment.text:
615 log.warning(
617 log.warning(
616 'Comment(repo): '
618 'Comment(repo): '
617 'Trying to create new version '
619 'Trying to create new version '
618 'of existing comment {}'.format(
620 'with the same comment body {}'.format(
619 comment_id,
621 comment_id,
620 )
622 )
621 )
623 )
622 raise HTTPNotFound()
624 raise HTTPNotFound()
625
623 if version.isdigit():
626 if version.isdigit():
624 version = int(version)
627 version = int(version)
625 else:
628 else:
626 log.warning(
629 log.warning(
627 'Comment(repo): Wrong version type {} {} '
630 'Comment(repo): Wrong version type {} {} '
628 'for comment {}'.format(
631 'for comment {}'.format(
629 version,
632 version,
630 type(version),
633 type(version),
631 comment_id,
634 comment_id,
632 )
635 )
633 )
636 )
634 raise HTTPNotFound()
637 raise HTTPNotFound()
635
638
639 try:
636 comment_history = CommentsModel().edit(
640 comment_history = CommentsModel().edit(
637 comment_id=comment_id,
641 comment_id=comment_id,
638 text=text,
642 text=text,
639 auth_user=self._rhodecode_user,
643 auth_user=self._rhodecode_user,
640 version=version,
644 version=version,
641 )
645 )
646 except CommentVersionMismatch:
647 raise HTTPConflict()
648
642 if not comment_history:
649 if not comment_history:
643 raise HTTPNotFound()
650 raise HTTPNotFound()
651
644 Session().commit()
652 Session().commit()
645 return {
653 return {
646 'comment_history_id': comment_history.comment_history_id,
654 'comment_history_id': comment_history.comment_history_id,
647 'comment_id': comment.comment_id,
655 'comment_id': comment.comment_id,
648 'comment_version': comment_history.version,
656 'comment_version': comment_history.version,
657 'comment_author_username': comment_history.author.username,
658 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
659 'comment_created_on': h.age_component(comment_history.created_on,
660 time_is_local=True),
649 }
661 }
650 else:
662 else:
651 log.warning('No permissions for user %s to edit comment_id: %s',
663 log.warning('No permissions for user %s to edit comment_id: %s',
652 self._rhodecode_db_user, comment_id)
664 self._rhodecode_db_user, comment_id)
653 raise HTTPNotFound()
665 raise HTTPNotFound()
654
666
655 @LoginRequired()
667 @LoginRequired()
656 @HasRepoPermissionAnyDecorator(
668 @HasRepoPermissionAnyDecorator(
657 'repository.read', 'repository.write', 'repository.admin')
669 'repository.read', 'repository.write', 'repository.admin')
658 @view_config(
670 @view_config(
659 route_name='repo_commit_data', request_method='GET',
671 route_name='repo_commit_data', request_method='GET',
660 renderer='json_ext', xhr=True)
672 renderer='json_ext', xhr=True)
661 def repo_commit_data(self):
673 def repo_commit_data(self):
662 commit_id = self.request.matchdict['commit_id']
674 commit_id = self.request.matchdict['commit_id']
663 self.load_default_context()
675 self.load_default_context()
664
676
665 try:
677 try:
666 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
678 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
667 except CommitDoesNotExistError as e:
679 except CommitDoesNotExistError as e:
668 return EmptyCommit(message=str(e))
680 return EmptyCommit(message=str(e))
669
681
670 @LoginRequired()
682 @LoginRequired()
671 @HasRepoPermissionAnyDecorator(
683 @HasRepoPermissionAnyDecorator(
672 'repository.read', 'repository.write', 'repository.admin')
684 'repository.read', 'repository.write', 'repository.admin')
673 @view_config(
685 @view_config(
674 route_name='repo_commit_children', request_method='GET',
686 route_name='repo_commit_children', request_method='GET',
675 renderer='json_ext', xhr=True)
687 renderer='json_ext', xhr=True)
676 def repo_commit_children(self):
688 def repo_commit_children(self):
677 commit_id = self.request.matchdict['commit_id']
689 commit_id = self.request.matchdict['commit_id']
678 self.load_default_context()
690 self.load_default_context()
679
691
680 try:
692 try:
681 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
693 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
682 children = commit.children
694 children = commit.children
683 except CommitDoesNotExistError:
695 except CommitDoesNotExistError:
684 children = []
696 children = []
685
697
686 result = {"results": children}
698 result = {"results": children}
687 return result
699 return result
688
700
689 @LoginRequired()
701 @LoginRequired()
690 @HasRepoPermissionAnyDecorator(
702 @HasRepoPermissionAnyDecorator(
691 'repository.read', 'repository.write', 'repository.admin')
703 'repository.read', 'repository.write', 'repository.admin')
692 @view_config(
704 @view_config(
693 route_name='repo_commit_parents', request_method='GET',
705 route_name='repo_commit_parents', request_method='GET',
694 renderer='json_ext')
706 renderer='json_ext')
695 def repo_commit_parents(self):
707 def repo_commit_parents(self):
696 commit_id = self.request.matchdict['commit_id']
708 commit_id = self.request.matchdict['commit_id']
697 self.load_default_context()
709 self.load_default_context()
698
710
699 try:
711 try:
700 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
712 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
701 parents = commit.parents
713 parents = commit.parents
702 except CommitDoesNotExistError:
714 except CommitDoesNotExistError:
703 parents = []
715 parents = []
704 result = {"results": parents}
716 result = {"results": parents}
705 return result
717 return result
@@ -1,1607 +1,1617 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
44 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (
48 from rhodecode.model.db import (
48 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
53
54
54 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
55
56
56
57
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
59
59 def load_default_context(self):
60 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 # backward compat., we use for OLD PRs a plain renderer
64 # backward compat., we use for OLD PRs a plain renderer
64 c.renderer = 'plain'
65 c.renderer = 'plain'
65 return c
66 return c
66
67
67 def _get_pull_requests_list(
68 def _get_pull_requests_list(
68 self, repo_name, source, filter_type, opened_by, statuses):
69 self, repo_name, source, filter_type, opened_by, statuses):
69
70
70 draw, start, limit = self._extract_chunk(self.request)
71 draw, start, limit = self._extract_chunk(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 _render = self.request.get_partial_renderer(
73 _render = self.request.get_partial_renderer(
73 'rhodecode:templates/data_table/_dt_elements.mako')
74 'rhodecode:templates/data_table/_dt_elements.mako')
74
75
75 # pagination
76 # pagination
76
77
77 if filter_type == 'awaiting_review':
78 if filter_type == 'awaiting_review':
78 pull_requests = PullRequestModel().get_awaiting_review(
79 pull_requests = PullRequestModel().get_awaiting_review(
79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 statuses=statuses, offset=start, length=limit,
81 statuses=statuses, offset=start, length=limit,
81 order_by=order_by, order_dir=order_dir)
82 order_by=order_by, order_dir=order_dir)
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 repo_name, search_q=search_q, source=source, statuses=statuses,
84 repo_name, search_q=search_q, source=source, statuses=statuses,
84 opened_by=opened_by)
85 opened_by=opened_by)
85 elif filter_type == 'awaiting_my_review':
86 elif filter_type == 'awaiting_my_review':
86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 pull_requests = PullRequestModel().get_awaiting_my_review(
87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 offset=start, length=limit, order_by=order_by,
90 offset=start, length=limit, order_by=order_by,
90 order_dir=order_dir)
91 order_dir=order_dir)
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 statuses=statuses, opened_by=opened_by)
94 statuses=statuses, opened_by=opened_by)
94 else:
95 else:
95 pull_requests = PullRequestModel().get_all(
96 pull_requests = PullRequestModel().get_all(
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 statuses=statuses, offset=start, length=limit,
98 statuses=statuses, offset=start, length=limit,
98 order_by=order_by, order_dir=order_dir)
99 order_by=order_by, order_dir=order_dir)
99 pull_requests_total_count = PullRequestModel().count_all(
100 pull_requests_total_count = PullRequestModel().count_all(
100 repo_name, search_q=search_q, source=source, statuses=statuses,
101 repo_name, search_q=search_q, source=source, statuses=statuses,
101 opened_by=opened_by)
102 opened_by=opened_by)
102
103
103 data = []
104 data = []
104 comments_model = CommentsModel()
105 comments_model = CommentsModel()
105 for pr in pull_requests:
106 for pr in pull_requests:
106 comments = comments_model.get_all_comments(
107 comments = comments_model.get_all_comments(
107 self.db_repo.repo_id, pull_request=pr)
108 self.db_repo.repo_id, pull_request=pr)
108
109
109 data.append({
110 data.append({
110 'name': _render('pullrequest_name',
111 'name': _render('pullrequest_name',
111 pr.pull_request_id, pr.pull_request_state,
112 pr.pull_request_id, pr.pull_request_state,
112 pr.work_in_progress, pr.target_repo.repo_name),
113 pr.work_in_progress, pr.target_repo.repo_name),
113 'name_raw': pr.pull_request_id,
114 'name_raw': pr.pull_request_id,
114 'status': _render('pullrequest_status',
115 'status': _render('pullrequest_status',
115 pr.calculated_review_status()),
116 pr.calculated_review_status()),
116 'title': _render('pullrequest_title', pr.title, pr.description),
117 'title': _render('pullrequest_title', pr.title, pr.description),
117 'description': h.escape(pr.description),
118 'description': h.escape(pr.description),
118 'updated_on': _render('pullrequest_updated_on',
119 'updated_on': _render('pullrequest_updated_on',
119 h.datetime_to_time(pr.updated_on)),
120 h.datetime_to_time(pr.updated_on)),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'created_on': _render('pullrequest_updated_on',
122 'created_on': _render('pullrequest_updated_on',
122 h.datetime_to_time(pr.created_on)),
123 h.datetime_to_time(pr.created_on)),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'state': pr.pull_request_state,
125 'state': pr.pull_request_state,
125 'author': _render('pullrequest_author',
126 'author': _render('pullrequest_author',
126 pr.author.full_contact, ),
127 pr.author.full_contact, ),
127 'author_raw': pr.author.full_name,
128 'author_raw': pr.author.full_name,
128 'comments': _render('pullrequest_comments', len(comments)),
129 'comments': _render('pullrequest_comments', len(comments)),
129 'comments_raw': len(comments),
130 'comments_raw': len(comments),
130 'closed': pr.is_closed(),
131 'closed': pr.is_closed(),
131 })
132 })
132
133
133 data = ({
134 data = ({
134 'draw': draw,
135 'draw': draw,
135 'data': data,
136 'data': data,
136 'recordsTotal': pull_requests_total_count,
137 'recordsTotal': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
138 'recordsFiltered': pull_requests_total_count,
138 })
139 })
139 return data
140 return data
140
141
141 @LoginRequired()
142 @LoginRequired()
142 @HasRepoPermissionAnyDecorator(
143 @HasRepoPermissionAnyDecorator(
143 'repository.read', 'repository.write', 'repository.admin')
144 'repository.read', 'repository.write', 'repository.admin')
144 @view_config(
145 @view_config(
145 route_name='pullrequest_show_all', request_method='GET',
146 route_name='pullrequest_show_all', request_method='GET',
146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 def pull_request_list(self):
148 def pull_request_list(self):
148 c = self.load_default_context()
149 c = self.load_default_context()
149
150
150 req_get = self.request.GET
151 req_get = self.request.GET
151 c.source = str2bool(req_get.get('source'))
152 c.source = str2bool(req_get.get('source'))
152 c.closed = str2bool(req_get.get('closed'))
153 c.closed = str2bool(req_get.get('closed'))
153 c.my = str2bool(req_get.get('my'))
154 c.my = str2bool(req_get.get('my'))
154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156
157
157 c.active = 'open'
158 c.active = 'open'
158 if c.my:
159 if c.my:
159 c.active = 'my'
160 c.active = 'my'
160 if c.closed:
161 if c.closed:
161 c.active = 'closed'
162 c.active = 'closed'
162 if c.awaiting_review and not c.source:
163 if c.awaiting_review and not c.source:
163 c.active = 'awaiting'
164 c.active = 'awaiting'
164 if c.source and not c.awaiting_review:
165 if c.source and not c.awaiting_review:
165 c.active = 'source'
166 c.active = 'source'
166 if c.awaiting_my_review:
167 if c.awaiting_my_review:
167 c.active = 'awaiting_my'
168 c.active = 'awaiting_my'
168
169
169 return self._get_template_context(c)
170 return self._get_template_context(c)
170
171
171 @LoginRequired()
172 @LoginRequired()
172 @HasRepoPermissionAnyDecorator(
173 @HasRepoPermissionAnyDecorator(
173 'repository.read', 'repository.write', 'repository.admin')
174 'repository.read', 'repository.write', 'repository.admin')
174 @view_config(
175 @view_config(
175 route_name='pullrequest_show_all_data', request_method='GET',
176 route_name='pullrequest_show_all_data', request_method='GET',
176 renderer='json_ext', xhr=True)
177 renderer='json_ext', xhr=True)
177 def pull_request_list_data(self):
178 def pull_request_list_data(self):
178 self.load_default_context()
179 self.load_default_context()
179
180
180 # additional filters
181 # additional filters
181 req_get = self.request.GET
182 req_get = self.request.GET
182 source = str2bool(req_get.get('source'))
183 source = str2bool(req_get.get('source'))
183 closed = str2bool(req_get.get('closed'))
184 closed = str2bool(req_get.get('closed'))
184 my = str2bool(req_get.get('my'))
185 my = str2bool(req_get.get('my'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187
188
188 filter_type = 'awaiting_review' if awaiting_review \
189 filter_type = 'awaiting_review' if awaiting_review \
189 else 'awaiting_my_review' if awaiting_my_review \
190 else 'awaiting_my_review' if awaiting_my_review \
190 else None
191 else None
191
192
192 opened_by = None
193 opened_by = None
193 if my:
194 if my:
194 opened_by = [self._rhodecode_user.user_id]
195 opened_by = [self._rhodecode_user.user_id]
195
196
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 if closed:
198 if closed:
198 statuses = [PullRequest.STATUS_CLOSED]
199 statuses = [PullRequest.STATUS_CLOSED]
199
200
200 data = self._get_pull_requests_list(
201 data = self._get_pull_requests_list(
201 repo_name=self.db_repo_name, source=source,
202 repo_name=self.db_repo_name, source=source,
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203
204
204 return data
205 return data
205
206
206 def _is_diff_cache_enabled(self, target_repo):
207 def _is_diff_cache_enabled(self, target_repo):
207 caching_enabled = self._get_general_setting(
208 caching_enabled = self._get_general_setting(
208 target_repo, 'rhodecode_diff_cache')
209 target_repo, 'rhodecode_diff_cache')
209 log.debug('Diff caching enabled: %s', caching_enabled)
210 log.debug('Diff caching enabled: %s', caching_enabled)
210 return caching_enabled
211 return caching_enabled
211
212
212 def _get_diffset(self, source_repo_name, source_repo,
213 def _get_diffset(self, source_repo_name, source_repo,
213 ancestor_commit,
214 ancestor_commit,
214 source_ref_id, target_ref_id,
215 source_ref_id, target_ref_id,
215 target_commit, source_commit, diff_limit, file_limit,
216 target_commit, source_commit, diff_limit, file_limit,
216 fulldiff, hide_whitespace_changes, diff_context):
217 fulldiff, hide_whitespace_changes, diff_context):
217
218
218 target_ref_id = ancestor_commit.raw_id
219 target_ref_id = ancestor_commit.raw_id
219 vcs_diff = PullRequestModel().get_diff(
220 vcs_diff = PullRequestModel().get_diff(
220 source_repo, source_ref_id, target_ref_id,
221 source_repo, source_ref_id, target_ref_id,
221 hide_whitespace_changes, diff_context)
222 hide_whitespace_changes, diff_context)
222
223
223 diff_processor = diffs.DiffProcessor(
224 diff_processor = diffs.DiffProcessor(
224 vcs_diff, format='newdiff', diff_limit=diff_limit,
225 vcs_diff, format='newdiff', diff_limit=diff_limit,
225 file_limit=file_limit, show_full_diff=fulldiff)
226 file_limit=file_limit, show_full_diff=fulldiff)
226
227
227 _parsed = diff_processor.prepare()
228 _parsed = diff_processor.prepare()
228
229
229 diffset = codeblocks.DiffSet(
230 diffset = codeblocks.DiffSet(
230 repo_name=self.db_repo_name,
231 repo_name=self.db_repo_name,
231 source_repo_name=source_repo_name,
232 source_repo_name=source_repo_name,
232 source_node_getter=codeblocks.diffset_node_getter(target_commit),
233 source_node_getter=codeblocks.diffset_node_getter(target_commit),
233 target_node_getter=codeblocks.diffset_node_getter(source_commit),
234 target_node_getter=codeblocks.diffset_node_getter(source_commit),
234 )
235 )
235 diffset = self.path_filter.render_patchset_filtered(
236 diffset = self.path_filter.render_patchset_filtered(
236 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
237 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
237
238
238 return diffset
239 return diffset
239
240
240 def _get_range_diffset(self, source_scm, source_repo,
241 def _get_range_diffset(self, source_scm, source_repo,
241 commit1, commit2, diff_limit, file_limit,
242 commit1, commit2, diff_limit, file_limit,
242 fulldiff, hide_whitespace_changes, diff_context):
243 fulldiff, hide_whitespace_changes, diff_context):
243 vcs_diff = source_scm.get_diff(
244 vcs_diff = source_scm.get_diff(
244 commit1, commit2,
245 commit1, commit2,
245 ignore_whitespace=hide_whitespace_changes,
246 ignore_whitespace=hide_whitespace_changes,
246 context=diff_context)
247 context=diff_context)
247
248
248 diff_processor = diffs.DiffProcessor(
249 diff_processor = diffs.DiffProcessor(
249 vcs_diff, format='newdiff', diff_limit=diff_limit,
250 vcs_diff, format='newdiff', diff_limit=diff_limit,
250 file_limit=file_limit, show_full_diff=fulldiff)
251 file_limit=file_limit, show_full_diff=fulldiff)
251
252
252 _parsed = diff_processor.prepare()
253 _parsed = diff_processor.prepare()
253
254
254 diffset = codeblocks.DiffSet(
255 diffset = codeblocks.DiffSet(
255 repo_name=source_repo.repo_name,
256 repo_name=source_repo.repo_name,
256 source_node_getter=codeblocks.diffset_node_getter(commit1),
257 source_node_getter=codeblocks.diffset_node_getter(commit1),
257 target_node_getter=codeblocks.diffset_node_getter(commit2))
258 target_node_getter=codeblocks.diffset_node_getter(commit2))
258
259
259 diffset = self.path_filter.render_patchset_filtered(
260 diffset = self.path_filter.render_patchset_filtered(
260 diffset, _parsed, commit1.raw_id, commit2.raw_id)
261 diffset, _parsed, commit1.raw_id, commit2.raw_id)
261
262
262 return diffset
263 return diffset
263
264
264 @LoginRequired()
265 @LoginRequired()
265 @HasRepoPermissionAnyDecorator(
266 @HasRepoPermissionAnyDecorator(
266 'repository.read', 'repository.write', 'repository.admin')
267 'repository.read', 'repository.write', 'repository.admin')
267 @view_config(
268 @view_config(
268 route_name='pullrequest_show', request_method='GET',
269 route_name='pullrequest_show', request_method='GET',
269 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
270 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
270 def pull_request_show(self):
271 def pull_request_show(self):
271 _ = self.request.translate
272 _ = self.request.translate
272 c = self.load_default_context()
273 c = self.load_default_context()
273
274
274 pull_request = PullRequest.get_or_404(
275 pull_request = PullRequest.get_or_404(
275 self.request.matchdict['pull_request_id'])
276 self.request.matchdict['pull_request_id'])
276 pull_request_id = pull_request.pull_request_id
277 pull_request_id = pull_request.pull_request_id
277
278
278 c.state_progressing = pull_request.is_state_changing()
279 c.state_progressing = pull_request.is_state_changing()
279
280
280 _new_state = {
281 _new_state = {
281 'created': PullRequest.STATE_CREATED,
282 'created': PullRequest.STATE_CREATED,
282 }.get(self.request.GET.get('force_state'))
283 }.get(self.request.GET.get('force_state'))
283
284
284 if c.is_super_admin and _new_state:
285 if c.is_super_admin and _new_state:
285 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
286 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
286 h.flash(
287 h.flash(
287 _('Pull Request state was force changed to `{}`').format(_new_state),
288 _('Pull Request state was force changed to `{}`').format(_new_state),
288 category='success')
289 category='success')
289 Session().commit()
290 Session().commit()
290
291
291 raise HTTPFound(h.route_path(
292 raise HTTPFound(h.route_path(
292 'pullrequest_show', repo_name=self.db_repo_name,
293 'pullrequest_show', repo_name=self.db_repo_name,
293 pull_request_id=pull_request_id))
294 pull_request_id=pull_request_id))
294
295
295 version = self.request.GET.get('version')
296 version = self.request.GET.get('version')
296 from_version = self.request.GET.get('from_version') or version
297 from_version = self.request.GET.get('from_version') or version
297 merge_checks = self.request.GET.get('merge_checks')
298 merge_checks = self.request.GET.get('merge_checks')
298 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
299 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
299
300
300 # fetch global flags of ignore ws or context lines
301 # fetch global flags of ignore ws or context lines
301 diff_context = diffs.get_diff_context(self.request)
302 diff_context = diffs.get_diff_context(self.request)
302 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
303 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
303
304
304 force_refresh = str2bool(self.request.GET.get('force_refresh'))
305 force_refresh = str2bool(self.request.GET.get('force_refresh'))
305
306
306 (pull_request_latest,
307 (pull_request_latest,
307 pull_request_at_ver,
308 pull_request_at_ver,
308 pull_request_display_obj,
309 pull_request_display_obj,
309 at_version) = PullRequestModel().get_pr_version(
310 at_version) = PullRequestModel().get_pr_version(
310 pull_request_id, version=version)
311 pull_request_id, version=version)
311 pr_closed = pull_request_latest.is_closed()
312 pr_closed = pull_request_latest.is_closed()
312
313
313 if pr_closed and (version or from_version):
314 if pr_closed and (version or from_version):
314 # not allow to browse versions
315 # not allow to browse versions
315 raise HTTPFound(h.route_path(
316 raise HTTPFound(h.route_path(
316 'pullrequest_show', repo_name=self.db_repo_name,
317 'pullrequest_show', repo_name=self.db_repo_name,
317 pull_request_id=pull_request_id))
318 pull_request_id=pull_request_id))
318
319
319 versions = pull_request_display_obj.versions()
320 versions = pull_request_display_obj.versions()
320 # used to store per-commit range diffs
321 # used to store per-commit range diffs
321 c.changes = collections.OrderedDict()
322 c.changes = collections.OrderedDict()
322 c.range_diff_on = self.request.GET.get('range-diff') == "1"
323 c.range_diff_on = self.request.GET.get('range-diff') == "1"
323
324
324 c.at_version = at_version
325 c.at_version = at_version
325 c.at_version_num = (at_version
326 c.at_version_num = (at_version
326 if at_version and at_version != 'latest'
327 if at_version and at_version != 'latest'
327 else None)
328 else None)
328 c.at_version_pos = ChangesetComment.get_index_from_version(
329 c.at_version_pos = ChangesetComment.get_index_from_version(
329 c.at_version_num, versions)
330 c.at_version_num, versions)
330
331
331 (prev_pull_request_latest,
332 (prev_pull_request_latest,
332 prev_pull_request_at_ver,
333 prev_pull_request_at_ver,
333 prev_pull_request_display_obj,
334 prev_pull_request_display_obj,
334 prev_at_version) = PullRequestModel().get_pr_version(
335 prev_at_version) = PullRequestModel().get_pr_version(
335 pull_request_id, version=from_version)
336 pull_request_id, version=from_version)
336
337
337 c.from_version = prev_at_version
338 c.from_version = prev_at_version
338 c.from_version_num = (prev_at_version
339 c.from_version_num = (prev_at_version
339 if prev_at_version and prev_at_version != 'latest'
340 if prev_at_version and prev_at_version != 'latest'
340 else None)
341 else None)
341 c.from_version_pos = ChangesetComment.get_index_from_version(
342 c.from_version_pos = ChangesetComment.get_index_from_version(
342 c.from_version_num, versions)
343 c.from_version_num, versions)
343
344
344 # define if we're in COMPARE mode or VIEW at version mode
345 # define if we're in COMPARE mode or VIEW at version mode
345 compare = at_version != prev_at_version
346 compare = at_version != prev_at_version
346
347
347 # pull_requests repo_name we opened it against
348 # pull_requests repo_name we opened it against
348 # ie. target_repo must match
349 # ie. target_repo must match
349 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
350 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
350 raise HTTPNotFound()
351 raise HTTPNotFound()
351
352
352 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
353 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
353 pull_request_at_ver)
354 pull_request_at_ver)
354
355
355 c.pull_request = pull_request_display_obj
356 c.pull_request = pull_request_display_obj
356 c.renderer = pull_request_at_ver.description_renderer or c.renderer
357 c.renderer = pull_request_at_ver.description_renderer or c.renderer
357 c.pull_request_latest = pull_request_latest
358 c.pull_request_latest = pull_request_latest
358
359
359 if compare or (at_version and not at_version == 'latest'):
360 if compare or (at_version and not at_version == 'latest'):
360 c.allowed_to_change_status = False
361 c.allowed_to_change_status = False
361 c.allowed_to_update = False
362 c.allowed_to_update = False
362 c.allowed_to_merge = False
363 c.allowed_to_merge = False
363 c.allowed_to_delete = False
364 c.allowed_to_delete = False
364 c.allowed_to_comment = False
365 c.allowed_to_comment = False
365 c.allowed_to_close = False
366 c.allowed_to_close = False
366 else:
367 else:
367 can_change_status = PullRequestModel().check_user_change_status(
368 can_change_status = PullRequestModel().check_user_change_status(
368 pull_request_at_ver, self._rhodecode_user)
369 pull_request_at_ver, self._rhodecode_user)
369 c.allowed_to_change_status = can_change_status and not pr_closed
370 c.allowed_to_change_status = can_change_status and not pr_closed
370
371
371 c.allowed_to_update = PullRequestModel().check_user_update(
372 c.allowed_to_update = PullRequestModel().check_user_update(
372 pull_request_latest, self._rhodecode_user) and not pr_closed
373 pull_request_latest, self._rhodecode_user) and not pr_closed
373 c.allowed_to_merge = PullRequestModel().check_user_merge(
374 c.allowed_to_merge = PullRequestModel().check_user_merge(
374 pull_request_latest, self._rhodecode_user) and not pr_closed
375 pull_request_latest, self._rhodecode_user) and not pr_closed
375 c.allowed_to_delete = PullRequestModel().check_user_delete(
376 c.allowed_to_delete = PullRequestModel().check_user_delete(
376 pull_request_latest, self._rhodecode_user) and not pr_closed
377 pull_request_latest, self._rhodecode_user) and not pr_closed
377 c.allowed_to_comment = not pr_closed
378 c.allowed_to_comment = not pr_closed
378 c.allowed_to_close = c.allowed_to_merge and not pr_closed
379 c.allowed_to_close = c.allowed_to_merge and not pr_closed
379
380
380 c.forbid_adding_reviewers = False
381 c.forbid_adding_reviewers = False
381 c.forbid_author_to_review = False
382 c.forbid_author_to_review = False
382 c.forbid_commit_author_to_review = False
383 c.forbid_commit_author_to_review = False
383
384
384 if pull_request_latest.reviewer_data and \
385 if pull_request_latest.reviewer_data and \
385 'rules' in pull_request_latest.reviewer_data:
386 'rules' in pull_request_latest.reviewer_data:
386 rules = pull_request_latest.reviewer_data['rules'] or {}
387 rules = pull_request_latest.reviewer_data['rules'] or {}
387 try:
388 try:
388 c.forbid_adding_reviewers = rules.get(
389 c.forbid_adding_reviewers = rules.get(
389 'forbid_adding_reviewers')
390 'forbid_adding_reviewers')
390 c.forbid_author_to_review = rules.get(
391 c.forbid_author_to_review = rules.get(
391 'forbid_author_to_review')
392 'forbid_author_to_review')
392 c.forbid_commit_author_to_review = rules.get(
393 c.forbid_commit_author_to_review = rules.get(
393 'forbid_commit_author_to_review')
394 'forbid_commit_author_to_review')
394 except Exception:
395 except Exception:
395 pass
396 pass
396
397
397 # check merge capabilities
398 # check merge capabilities
398 _merge_check = MergeCheck.validate(
399 _merge_check = MergeCheck.validate(
399 pull_request_latest, auth_user=self._rhodecode_user,
400 pull_request_latest, auth_user=self._rhodecode_user,
400 translator=self.request.translate,
401 translator=self.request.translate,
401 force_shadow_repo_refresh=force_refresh)
402 force_shadow_repo_refresh=force_refresh)
402
403
403 c.pr_merge_errors = _merge_check.error_details
404 c.pr_merge_errors = _merge_check.error_details
404 c.pr_merge_possible = not _merge_check.failed
405 c.pr_merge_possible = not _merge_check.failed
405 c.pr_merge_message = _merge_check.merge_msg
406 c.pr_merge_message = _merge_check.merge_msg
406 c.pr_merge_source_commit = _merge_check.source_commit
407 c.pr_merge_source_commit = _merge_check.source_commit
407 c.pr_merge_target_commit = _merge_check.target_commit
408 c.pr_merge_target_commit = _merge_check.target_commit
408
409
409 c.pr_merge_info = MergeCheck.get_merge_conditions(
410 c.pr_merge_info = MergeCheck.get_merge_conditions(
410 pull_request_latest, translator=self.request.translate)
411 pull_request_latest, translator=self.request.translate)
411
412
412 c.pull_request_review_status = _merge_check.review_status
413 c.pull_request_review_status = _merge_check.review_status
413 if merge_checks:
414 if merge_checks:
414 self.request.override_renderer = \
415 self.request.override_renderer = \
415 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
416 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
416 return self._get_template_context(c)
417 return self._get_template_context(c)
417
418
418 comments_model = CommentsModel()
419 comments_model = CommentsModel()
419
420
420 # reviewers and statuses
421 # reviewers and statuses
421 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
422 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
422 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
423 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
423
424
424 # GENERAL COMMENTS with versions #
425 # GENERAL COMMENTS with versions #
425 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
426 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
426 q = q.order_by(ChangesetComment.comment_id.asc())
427 q = q.order_by(ChangesetComment.comment_id.asc())
427 general_comments = q
428 general_comments = q
428
429
429 # pick comments we want to render at current version
430 # pick comments we want to render at current version
430 c.comment_versions = comments_model.aggregate_comments(
431 c.comment_versions = comments_model.aggregate_comments(
431 general_comments, versions, c.at_version_num)
432 general_comments, versions, c.at_version_num)
432 c.comments = c.comment_versions[c.at_version_num]['until']
433 c.comments = c.comment_versions[c.at_version_num]['until']
433
434
434 # INLINE COMMENTS with versions #
435 # INLINE COMMENTS with versions #
435 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
436 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
436 q = q.order_by(ChangesetComment.comment_id.asc())
437 q = q.order_by(ChangesetComment.comment_id.asc())
437 inline_comments = q
438 inline_comments = q
438
439
439 c.inline_versions = comments_model.aggregate_comments(
440 c.inline_versions = comments_model.aggregate_comments(
440 inline_comments, versions, c.at_version_num, inline=True)
441 inline_comments, versions, c.at_version_num, inline=True)
441
442
442 # TODOs
443 # TODOs
443 c.unresolved_comments = CommentsModel() \
444 c.unresolved_comments = CommentsModel() \
444 .get_pull_request_unresolved_todos(pull_request)
445 .get_pull_request_unresolved_todos(pull_request)
445 c.resolved_comments = CommentsModel() \
446 c.resolved_comments = CommentsModel() \
446 .get_pull_request_resolved_todos(pull_request)
447 .get_pull_request_resolved_todos(pull_request)
447
448
448 # inject latest version
449 # inject latest version
449 latest_ver = PullRequest.get_pr_display_object(
450 latest_ver = PullRequest.get_pr_display_object(
450 pull_request_latest, pull_request_latest)
451 pull_request_latest, pull_request_latest)
451
452
452 c.versions = versions + [latest_ver]
453 c.versions = versions + [latest_ver]
453
454
454 # if we use version, then do not show later comments
455 # if we use version, then do not show later comments
455 # than current version
456 # than current version
456 display_inline_comments = collections.defaultdict(
457 display_inline_comments = collections.defaultdict(
457 lambda: collections.defaultdict(list))
458 lambda: collections.defaultdict(list))
458 for co in inline_comments:
459 for co in inline_comments:
459 if c.at_version_num:
460 if c.at_version_num:
460 # pick comments that are at least UPTO given version, so we
461 # pick comments that are at least UPTO given version, so we
461 # don't render comments for higher version
462 # don't render comments for higher version
462 should_render = co.pull_request_version_id and \
463 should_render = co.pull_request_version_id and \
463 co.pull_request_version_id <= c.at_version_num
464 co.pull_request_version_id <= c.at_version_num
464 else:
465 else:
465 # showing all, for 'latest'
466 # showing all, for 'latest'
466 should_render = True
467 should_render = True
467
468
468 if should_render:
469 if should_render:
469 display_inline_comments[co.f_path][co.line_no].append(co)
470 display_inline_comments[co.f_path][co.line_no].append(co)
470
471
471 # load diff data into template context, if we use compare mode then
472 # load diff data into template context, if we use compare mode then
472 # diff is calculated based on changes between versions of PR
473 # diff is calculated based on changes between versions of PR
473
474
474 source_repo = pull_request_at_ver.source_repo
475 source_repo = pull_request_at_ver.source_repo
475 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
476 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
476
477
477 target_repo = pull_request_at_ver.target_repo
478 target_repo = pull_request_at_ver.target_repo
478 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
479 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
479
480
480 if compare:
481 if compare:
481 # in compare switch the diff base to latest commit from prev version
482 # in compare switch the diff base to latest commit from prev version
482 target_ref_id = prev_pull_request_display_obj.revisions[0]
483 target_ref_id = prev_pull_request_display_obj.revisions[0]
483
484
484 # despite opening commits for bookmarks/branches/tags, we always
485 # despite opening commits for bookmarks/branches/tags, we always
485 # convert this to rev to prevent changes after bookmark or branch change
486 # convert this to rev to prevent changes after bookmark or branch change
486 c.source_ref_type = 'rev'
487 c.source_ref_type = 'rev'
487 c.source_ref = source_ref_id
488 c.source_ref = source_ref_id
488
489
489 c.target_ref_type = 'rev'
490 c.target_ref_type = 'rev'
490 c.target_ref = target_ref_id
491 c.target_ref = target_ref_id
491
492
492 c.source_repo = source_repo
493 c.source_repo = source_repo
493 c.target_repo = target_repo
494 c.target_repo = target_repo
494
495
495 c.commit_ranges = []
496 c.commit_ranges = []
496 source_commit = EmptyCommit()
497 source_commit = EmptyCommit()
497 target_commit = EmptyCommit()
498 target_commit = EmptyCommit()
498 c.missing_requirements = False
499 c.missing_requirements = False
499
500
500 source_scm = source_repo.scm_instance()
501 source_scm = source_repo.scm_instance()
501 target_scm = target_repo.scm_instance()
502 target_scm = target_repo.scm_instance()
502
503
503 shadow_scm = None
504 shadow_scm = None
504 try:
505 try:
505 shadow_scm = pull_request_latest.get_shadow_repo()
506 shadow_scm = pull_request_latest.get_shadow_repo()
506 except Exception:
507 except Exception:
507 log.debug('Failed to get shadow repo', exc_info=True)
508 log.debug('Failed to get shadow repo', exc_info=True)
508 # try first the existing source_repo, and then shadow
509 # try first the existing source_repo, and then shadow
509 # repo if we can obtain one
510 # repo if we can obtain one
510 commits_source_repo = source_scm
511 commits_source_repo = source_scm
511 if shadow_scm:
512 if shadow_scm:
512 commits_source_repo = shadow_scm
513 commits_source_repo = shadow_scm
513
514
514 c.commits_source_repo = commits_source_repo
515 c.commits_source_repo = commits_source_repo
515 c.ancestor = None # set it to None, to hide it from PR view
516 c.ancestor = None # set it to None, to hide it from PR view
516
517
517 # empty version means latest, so we keep this to prevent
518 # empty version means latest, so we keep this to prevent
518 # double caching
519 # double caching
519 version_normalized = version or 'latest'
520 version_normalized = version or 'latest'
520 from_version_normalized = from_version or 'latest'
521 from_version_normalized = from_version or 'latest'
521
522
522 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
523 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
523 cache_file_path = diff_cache_exist(
524 cache_file_path = diff_cache_exist(
524 cache_path, 'pull_request', pull_request_id, version_normalized,
525 cache_path, 'pull_request', pull_request_id, version_normalized,
525 from_version_normalized, source_ref_id, target_ref_id,
526 from_version_normalized, source_ref_id, target_ref_id,
526 hide_whitespace_changes, diff_context, c.fulldiff)
527 hide_whitespace_changes, diff_context, c.fulldiff)
527
528
528 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
529 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
529 force_recache = self.get_recache_flag()
530 force_recache = self.get_recache_flag()
530
531
531 cached_diff = None
532 cached_diff = None
532 if caching_enabled:
533 if caching_enabled:
533 cached_diff = load_cached_diff(cache_file_path)
534 cached_diff = load_cached_diff(cache_file_path)
534
535
535 has_proper_commit_cache = (
536 has_proper_commit_cache = (
536 cached_diff and cached_diff.get('commits')
537 cached_diff and cached_diff.get('commits')
537 and len(cached_diff.get('commits', [])) == 5
538 and len(cached_diff.get('commits', [])) == 5
538 and cached_diff.get('commits')[0]
539 and cached_diff.get('commits')[0]
539 and cached_diff.get('commits')[3])
540 and cached_diff.get('commits')[3])
540
541
541 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
542 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
542 diff_commit_cache = \
543 diff_commit_cache = \
543 (ancestor_commit, commit_cache, missing_requirements,
544 (ancestor_commit, commit_cache, missing_requirements,
544 source_commit, target_commit) = cached_diff['commits']
545 source_commit, target_commit) = cached_diff['commits']
545 else:
546 else:
546 # NOTE(marcink): we reach potentially unreachable errors when a PR has
547 # NOTE(marcink): we reach potentially unreachable errors when a PR has
547 # merge errors resulting in potentially hidden commits in the shadow repo.
548 # merge errors resulting in potentially hidden commits in the shadow repo.
548 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
549 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
549 and _merge_check.merge_response
550 and _merge_check.merge_response
550 maybe_unreachable = maybe_unreachable \
551 maybe_unreachable = maybe_unreachable \
551 and _merge_check.merge_response.metadata.get('unresolved_files')
552 and _merge_check.merge_response.metadata.get('unresolved_files')
552 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
553 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
553 diff_commit_cache = \
554 diff_commit_cache = \
554 (ancestor_commit, commit_cache, missing_requirements,
555 (ancestor_commit, commit_cache, missing_requirements,
555 source_commit, target_commit) = self.get_commits(
556 source_commit, target_commit) = self.get_commits(
556 commits_source_repo,
557 commits_source_repo,
557 pull_request_at_ver,
558 pull_request_at_ver,
558 source_commit,
559 source_commit,
559 source_ref_id,
560 source_ref_id,
560 source_scm,
561 source_scm,
561 target_commit,
562 target_commit,
562 target_ref_id,
563 target_ref_id,
563 target_scm,
564 target_scm,
564 maybe_unreachable=maybe_unreachable)
565 maybe_unreachable=maybe_unreachable)
565
566
566 # register our commit range
567 # register our commit range
567 for comm in commit_cache.values():
568 for comm in commit_cache.values():
568 c.commit_ranges.append(comm)
569 c.commit_ranges.append(comm)
569
570
570 c.missing_requirements = missing_requirements
571 c.missing_requirements = missing_requirements
571 c.ancestor_commit = ancestor_commit
572 c.ancestor_commit = ancestor_commit
572 c.statuses = source_repo.statuses(
573 c.statuses = source_repo.statuses(
573 [x.raw_id for x in c.commit_ranges])
574 [x.raw_id for x in c.commit_ranges])
574
575
575 # auto collapse if we have more than limit
576 # auto collapse if we have more than limit
576 collapse_limit = diffs.DiffProcessor._collapse_commits_over
577 collapse_limit = diffs.DiffProcessor._collapse_commits_over
577 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
578 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
578 c.compare_mode = compare
579 c.compare_mode = compare
579
580
580 # diff_limit is the old behavior, will cut off the whole diff
581 # diff_limit is the old behavior, will cut off the whole diff
581 # if the limit is applied otherwise will just hide the
582 # if the limit is applied otherwise will just hide the
582 # big files from the front-end
583 # big files from the front-end
583 diff_limit = c.visual.cut_off_limit_diff
584 diff_limit = c.visual.cut_off_limit_diff
584 file_limit = c.visual.cut_off_limit_file
585 file_limit = c.visual.cut_off_limit_file
585
586
586 c.missing_commits = False
587 c.missing_commits = False
587 if (c.missing_requirements
588 if (c.missing_requirements
588 or isinstance(source_commit, EmptyCommit)
589 or isinstance(source_commit, EmptyCommit)
589 or source_commit == target_commit):
590 or source_commit == target_commit):
590
591
591 c.missing_commits = True
592 c.missing_commits = True
592 else:
593 else:
593 c.inline_comments = display_inline_comments
594 c.inline_comments = display_inline_comments
594
595
595 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
596 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
596 if not force_recache and has_proper_diff_cache:
597 if not force_recache and has_proper_diff_cache:
597 c.diffset = cached_diff['diff']
598 c.diffset = cached_diff['diff']
598 else:
599 else:
599 c.diffset = self._get_diffset(
600 c.diffset = self._get_diffset(
600 c.source_repo.repo_name, commits_source_repo,
601 c.source_repo.repo_name, commits_source_repo,
601 c.ancestor_commit,
602 c.ancestor_commit,
602 source_ref_id, target_ref_id,
603 source_ref_id, target_ref_id,
603 target_commit, source_commit,
604 target_commit, source_commit,
604 diff_limit, file_limit, c.fulldiff,
605 diff_limit, file_limit, c.fulldiff,
605 hide_whitespace_changes, diff_context)
606 hide_whitespace_changes, diff_context)
606
607
607 # save cached diff
608 # save cached diff
608 if caching_enabled:
609 if caching_enabled:
609 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
610 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
610
611
611 c.limited_diff = c.diffset.limited_diff
612 c.limited_diff = c.diffset.limited_diff
612
613
613 # calculate removed files that are bound to comments
614 # calculate removed files that are bound to comments
614 comment_deleted_files = [
615 comment_deleted_files = [
615 fname for fname in display_inline_comments
616 fname for fname in display_inline_comments
616 if fname not in c.diffset.file_stats]
617 if fname not in c.diffset.file_stats]
617
618
618 c.deleted_files_comments = collections.defaultdict(dict)
619 c.deleted_files_comments = collections.defaultdict(dict)
619 for fname, per_line_comments in display_inline_comments.items():
620 for fname, per_line_comments in display_inline_comments.items():
620 if fname in comment_deleted_files:
621 if fname in comment_deleted_files:
621 c.deleted_files_comments[fname]['stats'] = 0
622 c.deleted_files_comments[fname]['stats'] = 0
622 c.deleted_files_comments[fname]['comments'] = list()
623 c.deleted_files_comments[fname]['comments'] = list()
623 for lno, comments in per_line_comments.items():
624 for lno, comments in per_line_comments.items():
624 c.deleted_files_comments[fname]['comments'].extend(comments)
625 c.deleted_files_comments[fname]['comments'].extend(comments)
625
626
626 # maybe calculate the range diff
627 # maybe calculate the range diff
627 if c.range_diff_on:
628 if c.range_diff_on:
628 # TODO(marcink): set whitespace/context
629 # TODO(marcink): set whitespace/context
629 context_lcl = 3
630 context_lcl = 3
630 ign_whitespace_lcl = False
631 ign_whitespace_lcl = False
631
632
632 for commit in c.commit_ranges:
633 for commit in c.commit_ranges:
633 commit2 = commit
634 commit2 = commit
634 commit1 = commit.first_parent
635 commit1 = commit.first_parent
635
636
636 range_diff_cache_file_path = diff_cache_exist(
637 range_diff_cache_file_path = diff_cache_exist(
637 cache_path, 'diff', commit.raw_id,
638 cache_path, 'diff', commit.raw_id,
638 ign_whitespace_lcl, context_lcl, c.fulldiff)
639 ign_whitespace_lcl, context_lcl, c.fulldiff)
639
640
640 cached_diff = None
641 cached_diff = None
641 if caching_enabled:
642 if caching_enabled:
642 cached_diff = load_cached_diff(range_diff_cache_file_path)
643 cached_diff = load_cached_diff(range_diff_cache_file_path)
643
644
644 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
645 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
645 if not force_recache and has_proper_diff_cache:
646 if not force_recache and has_proper_diff_cache:
646 diffset = cached_diff['diff']
647 diffset = cached_diff['diff']
647 else:
648 else:
648 diffset = self._get_range_diffset(
649 diffset = self._get_range_diffset(
649 commits_source_repo, source_repo,
650 commits_source_repo, source_repo,
650 commit1, commit2, diff_limit, file_limit,
651 commit1, commit2, diff_limit, file_limit,
651 c.fulldiff, ign_whitespace_lcl, context_lcl
652 c.fulldiff, ign_whitespace_lcl, context_lcl
652 )
653 )
653
654
654 # save cached diff
655 # save cached diff
655 if caching_enabled:
656 if caching_enabled:
656 cache_diff(range_diff_cache_file_path, diffset, None)
657 cache_diff(range_diff_cache_file_path, diffset, None)
657
658
658 c.changes[commit.raw_id] = diffset
659 c.changes[commit.raw_id] = diffset
659
660
660 # this is a hack to properly display links, when creating PR, the
661 # this is a hack to properly display links, when creating PR, the
661 # compare view and others uses different notation, and
662 # compare view and others uses different notation, and
662 # compare_commits.mako renders links based on the target_repo.
663 # compare_commits.mako renders links based on the target_repo.
663 # We need to swap that here to generate it properly on the html side
664 # We need to swap that here to generate it properly on the html side
664 c.target_repo = c.source_repo
665 c.target_repo = c.source_repo
665
666
666 c.commit_statuses = ChangesetStatus.STATUSES
667 c.commit_statuses = ChangesetStatus.STATUSES
667
668
668 c.show_version_changes = not pr_closed
669 c.show_version_changes = not pr_closed
669 if c.show_version_changes:
670 if c.show_version_changes:
670 cur_obj = pull_request_at_ver
671 cur_obj = pull_request_at_ver
671 prev_obj = prev_pull_request_at_ver
672 prev_obj = prev_pull_request_at_ver
672
673
673 old_commit_ids = prev_obj.revisions
674 old_commit_ids = prev_obj.revisions
674 new_commit_ids = cur_obj.revisions
675 new_commit_ids = cur_obj.revisions
675 commit_changes = PullRequestModel()._calculate_commit_id_changes(
676 commit_changes = PullRequestModel()._calculate_commit_id_changes(
676 old_commit_ids, new_commit_ids)
677 old_commit_ids, new_commit_ids)
677 c.commit_changes_summary = commit_changes
678 c.commit_changes_summary = commit_changes
678
679
679 # calculate the diff for commits between versions
680 # calculate the diff for commits between versions
680 c.commit_changes = []
681 c.commit_changes = []
681
682
682 def mark(cs, fw):
683 def mark(cs, fw):
683 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
684 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
684
685
685 for c_type, raw_id in mark(commit_changes.added, 'a') \
686 for c_type, raw_id in mark(commit_changes.added, 'a') \
686 + mark(commit_changes.removed, 'r') \
687 + mark(commit_changes.removed, 'r') \
687 + mark(commit_changes.common, 'c'):
688 + mark(commit_changes.common, 'c'):
688
689
689 if raw_id in commit_cache:
690 if raw_id in commit_cache:
690 commit = commit_cache[raw_id]
691 commit = commit_cache[raw_id]
691 else:
692 else:
692 try:
693 try:
693 commit = commits_source_repo.get_commit(raw_id)
694 commit = commits_source_repo.get_commit(raw_id)
694 except CommitDoesNotExistError:
695 except CommitDoesNotExistError:
695 # in case we fail extracting still use "dummy" commit
696 # in case we fail extracting still use "dummy" commit
696 # for display in commit diff
697 # for display in commit diff
697 commit = h.AttributeDict(
698 commit = h.AttributeDict(
698 {'raw_id': raw_id,
699 {'raw_id': raw_id,
699 'message': 'EMPTY or MISSING COMMIT'})
700 'message': 'EMPTY or MISSING COMMIT'})
700 c.commit_changes.append([c_type, commit])
701 c.commit_changes.append([c_type, commit])
701
702
702 # current user review statuses for each version
703 # current user review statuses for each version
703 c.review_versions = {}
704 c.review_versions = {}
704 if self._rhodecode_user.user_id in allowed_reviewers:
705 if self._rhodecode_user.user_id in allowed_reviewers:
705 for co in general_comments:
706 for co in general_comments:
706 if co.author.user_id == self._rhodecode_user.user_id:
707 if co.author.user_id == self._rhodecode_user.user_id:
707 status = co.status_change
708 status = co.status_change
708 if status:
709 if status:
709 _ver_pr = status[0].comment.pull_request_version_id
710 _ver_pr = status[0].comment.pull_request_version_id
710 c.review_versions[_ver_pr] = status[0]
711 c.review_versions[_ver_pr] = status[0]
711
712
712 return self._get_template_context(c)
713 return self._get_template_context(c)
713
714
714 def get_commits(
715 def get_commits(
715 self, commits_source_repo, pull_request_at_ver, source_commit,
716 self, commits_source_repo, pull_request_at_ver, source_commit,
716 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
717 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
717 maybe_unreachable=False):
718 maybe_unreachable=False):
718
719
719 commit_cache = collections.OrderedDict()
720 commit_cache = collections.OrderedDict()
720 missing_requirements = False
721 missing_requirements = False
721
722
722 try:
723 try:
723 pre_load = ["author", "date", "message", "branch", "parents"]
724 pre_load = ["author", "date", "message", "branch", "parents"]
724
725
725 pull_request_commits = pull_request_at_ver.revisions
726 pull_request_commits = pull_request_at_ver.revisions
726 log.debug('Loading %s commits from %s',
727 log.debug('Loading %s commits from %s',
727 len(pull_request_commits), commits_source_repo)
728 len(pull_request_commits), commits_source_repo)
728
729
729 for rev in pull_request_commits:
730 for rev in pull_request_commits:
730 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
731 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
731 maybe_unreachable=maybe_unreachable)
732 maybe_unreachable=maybe_unreachable)
732 commit_cache[comm.raw_id] = comm
733 commit_cache[comm.raw_id] = comm
733
734
734 # Order here matters, we first need to get target, and then
735 # Order here matters, we first need to get target, and then
735 # the source
736 # the source
736 target_commit = commits_source_repo.get_commit(
737 target_commit = commits_source_repo.get_commit(
737 commit_id=safe_str(target_ref_id))
738 commit_id=safe_str(target_ref_id))
738
739
739 source_commit = commits_source_repo.get_commit(
740 source_commit = commits_source_repo.get_commit(
740 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
741 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
741 except CommitDoesNotExistError:
742 except CommitDoesNotExistError:
742 log.warning('Failed to get commit from `{}` repo'.format(
743 log.warning('Failed to get commit from `{}` repo'.format(
743 commits_source_repo), exc_info=True)
744 commits_source_repo), exc_info=True)
744 except RepositoryRequirementError:
745 except RepositoryRequirementError:
745 log.warning('Failed to get all required data from repo', exc_info=True)
746 log.warning('Failed to get all required data from repo', exc_info=True)
746 missing_requirements = True
747 missing_requirements = True
747
748
748 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
749 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
749
750
750 try:
751 try:
751 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
752 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
752 except Exception:
753 except Exception:
753 ancestor_commit = None
754 ancestor_commit = None
754
755
755 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
756 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
756
757
757 def assure_not_empty_repo(self):
758 def assure_not_empty_repo(self):
758 _ = self.request.translate
759 _ = self.request.translate
759
760
760 try:
761 try:
761 self.db_repo.scm_instance().get_commit()
762 self.db_repo.scm_instance().get_commit()
762 except EmptyRepositoryError:
763 except EmptyRepositoryError:
763 h.flash(h.literal(_('There are no commits yet')),
764 h.flash(h.literal(_('There are no commits yet')),
764 category='warning')
765 category='warning')
765 raise HTTPFound(
766 raise HTTPFound(
766 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
767 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
767
768
768 @LoginRequired()
769 @LoginRequired()
769 @NotAnonymous()
770 @NotAnonymous()
770 @HasRepoPermissionAnyDecorator(
771 @HasRepoPermissionAnyDecorator(
771 'repository.read', 'repository.write', 'repository.admin')
772 'repository.read', 'repository.write', 'repository.admin')
772 @view_config(
773 @view_config(
773 route_name='pullrequest_new', request_method='GET',
774 route_name='pullrequest_new', request_method='GET',
774 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
775 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
775 def pull_request_new(self):
776 def pull_request_new(self):
776 _ = self.request.translate
777 _ = self.request.translate
777 c = self.load_default_context()
778 c = self.load_default_context()
778
779
779 self.assure_not_empty_repo()
780 self.assure_not_empty_repo()
780 source_repo = self.db_repo
781 source_repo = self.db_repo
781
782
782 commit_id = self.request.GET.get('commit')
783 commit_id = self.request.GET.get('commit')
783 branch_ref = self.request.GET.get('branch')
784 branch_ref = self.request.GET.get('branch')
784 bookmark_ref = self.request.GET.get('bookmark')
785 bookmark_ref = self.request.GET.get('bookmark')
785
786
786 try:
787 try:
787 source_repo_data = PullRequestModel().generate_repo_data(
788 source_repo_data = PullRequestModel().generate_repo_data(
788 source_repo, commit_id=commit_id,
789 source_repo, commit_id=commit_id,
789 branch=branch_ref, bookmark=bookmark_ref,
790 branch=branch_ref, bookmark=bookmark_ref,
790 translator=self.request.translate)
791 translator=self.request.translate)
791 except CommitDoesNotExistError as e:
792 except CommitDoesNotExistError as e:
792 log.exception(e)
793 log.exception(e)
793 h.flash(_('Commit does not exist'), 'error')
794 h.flash(_('Commit does not exist'), 'error')
794 raise HTTPFound(
795 raise HTTPFound(
795 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
796 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
796
797
797 default_target_repo = source_repo
798 default_target_repo = source_repo
798
799
799 if source_repo.parent and c.has_origin_repo_read_perm:
800 if source_repo.parent and c.has_origin_repo_read_perm:
800 parent_vcs_obj = source_repo.parent.scm_instance()
801 parent_vcs_obj = source_repo.parent.scm_instance()
801 if parent_vcs_obj and not parent_vcs_obj.is_empty():
802 if parent_vcs_obj and not parent_vcs_obj.is_empty():
802 # change default if we have a parent repo
803 # change default if we have a parent repo
803 default_target_repo = source_repo.parent
804 default_target_repo = source_repo.parent
804
805
805 target_repo_data = PullRequestModel().generate_repo_data(
806 target_repo_data = PullRequestModel().generate_repo_data(
806 default_target_repo, translator=self.request.translate)
807 default_target_repo, translator=self.request.translate)
807
808
808 selected_source_ref = source_repo_data['refs']['selected_ref']
809 selected_source_ref = source_repo_data['refs']['selected_ref']
809 title_source_ref = ''
810 title_source_ref = ''
810 if selected_source_ref:
811 if selected_source_ref:
811 title_source_ref = selected_source_ref.split(':', 2)[1]
812 title_source_ref = selected_source_ref.split(':', 2)[1]
812 c.default_title = PullRequestModel().generate_pullrequest_title(
813 c.default_title = PullRequestModel().generate_pullrequest_title(
813 source=source_repo.repo_name,
814 source=source_repo.repo_name,
814 source_ref=title_source_ref,
815 source_ref=title_source_ref,
815 target=default_target_repo.repo_name
816 target=default_target_repo.repo_name
816 )
817 )
817
818
818 c.default_repo_data = {
819 c.default_repo_data = {
819 'source_repo_name': source_repo.repo_name,
820 'source_repo_name': source_repo.repo_name,
820 'source_refs_json': json.dumps(source_repo_data),
821 'source_refs_json': json.dumps(source_repo_data),
821 'target_repo_name': default_target_repo.repo_name,
822 'target_repo_name': default_target_repo.repo_name,
822 'target_refs_json': json.dumps(target_repo_data),
823 'target_refs_json': json.dumps(target_repo_data),
823 }
824 }
824 c.default_source_ref = selected_source_ref
825 c.default_source_ref = selected_source_ref
825
826
826 return self._get_template_context(c)
827 return self._get_template_context(c)
827
828
828 @LoginRequired()
829 @LoginRequired()
829 @NotAnonymous()
830 @NotAnonymous()
830 @HasRepoPermissionAnyDecorator(
831 @HasRepoPermissionAnyDecorator(
831 'repository.read', 'repository.write', 'repository.admin')
832 'repository.read', 'repository.write', 'repository.admin')
832 @view_config(
833 @view_config(
833 route_name='pullrequest_repo_refs', request_method='GET',
834 route_name='pullrequest_repo_refs', request_method='GET',
834 renderer='json_ext', xhr=True)
835 renderer='json_ext', xhr=True)
835 def pull_request_repo_refs(self):
836 def pull_request_repo_refs(self):
836 self.load_default_context()
837 self.load_default_context()
837 target_repo_name = self.request.matchdict['target_repo_name']
838 target_repo_name = self.request.matchdict['target_repo_name']
838 repo = Repository.get_by_repo_name(target_repo_name)
839 repo = Repository.get_by_repo_name(target_repo_name)
839 if not repo:
840 if not repo:
840 raise HTTPNotFound()
841 raise HTTPNotFound()
841
842
842 target_perm = HasRepoPermissionAny(
843 target_perm = HasRepoPermissionAny(
843 'repository.read', 'repository.write', 'repository.admin')(
844 'repository.read', 'repository.write', 'repository.admin')(
844 target_repo_name)
845 target_repo_name)
845 if not target_perm:
846 if not target_perm:
846 raise HTTPNotFound()
847 raise HTTPNotFound()
847
848
848 return PullRequestModel().generate_repo_data(
849 return PullRequestModel().generate_repo_data(
849 repo, translator=self.request.translate)
850 repo, translator=self.request.translate)
850
851
851 @LoginRequired()
852 @LoginRequired()
852 @NotAnonymous()
853 @NotAnonymous()
853 @HasRepoPermissionAnyDecorator(
854 @HasRepoPermissionAnyDecorator(
854 'repository.read', 'repository.write', 'repository.admin')
855 'repository.read', 'repository.write', 'repository.admin')
855 @view_config(
856 @view_config(
856 route_name='pullrequest_repo_targets', request_method='GET',
857 route_name='pullrequest_repo_targets', request_method='GET',
857 renderer='json_ext', xhr=True)
858 renderer='json_ext', xhr=True)
858 def pullrequest_repo_targets(self):
859 def pullrequest_repo_targets(self):
859 _ = self.request.translate
860 _ = self.request.translate
860 filter_query = self.request.GET.get('query')
861 filter_query = self.request.GET.get('query')
861
862
862 # get the parents
863 # get the parents
863 parent_target_repos = []
864 parent_target_repos = []
864 if self.db_repo.parent:
865 if self.db_repo.parent:
865 parents_query = Repository.query() \
866 parents_query = Repository.query() \
866 .order_by(func.length(Repository.repo_name)) \
867 .order_by(func.length(Repository.repo_name)) \
867 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
868 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
868
869
869 if filter_query:
870 if filter_query:
870 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
871 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
871 parents_query = parents_query.filter(
872 parents_query = parents_query.filter(
872 Repository.repo_name.ilike(ilike_expression))
873 Repository.repo_name.ilike(ilike_expression))
873 parents = parents_query.limit(20).all()
874 parents = parents_query.limit(20).all()
874
875
875 for parent in parents:
876 for parent in parents:
876 parent_vcs_obj = parent.scm_instance()
877 parent_vcs_obj = parent.scm_instance()
877 if parent_vcs_obj and not parent_vcs_obj.is_empty():
878 if parent_vcs_obj and not parent_vcs_obj.is_empty():
878 parent_target_repos.append(parent)
879 parent_target_repos.append(parent)
879
880
880 # get other forks, and repo itself
881 # get other forks, and repo itself
881 query = Repository.query() \
882 query = Repository.query() \
882 .order_by(func.length(Repository.repo_name)) \
883 .order_by(func.length(Repository.repo_name)) \
883 .filter(
884 .filter(
884 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
885 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
885 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
886 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
886 ) \
887 ) \
887 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
888 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
888
889
889 if filter_query:
890 if filter_query:
890 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
891 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
891 query = query.filter(Repository.repo_name.ilike(ilike_expression))
892 query = query.filter(Repository.repo_name.ilike(ilike_expression))
892
893
893 limit = max(20 - len(parent_target_repos), 5) # not less then 5
894 limit = max(20 - len(parent_target_repos), 5) # not less then 5
894 target_repos = query.limit(limit).all()
895 target_repos = query.limit(limit).all()
895
896
896 all_target_repos = target_repos + parent_target_repos
897 all_target_repos = target_repos + parent_target_repos
897
898
898 repos = []
899 repos = []
899 # This checks permissions to the repositories
900 # This checks permissions to the repositories
900 for obj in ScmModel().get_repos(all_target_repos):
901 for obj in ScmModel().get_repos(all_target_repos):
901 repos.append({
902 repos.append({
902 'id': obj['name'],
903 'id': obj['name'],
903 'text': obj['name'],
904 'text': obj['name'],
904 'type': 'repo',
905 'type': 'repo',
905 'repo_id': obj['dbrepo']['repo_id'],
906 'repo_id': obj['dbrepo']['repo_id'],
906 'repo_type': obj['dbrepo']['repo_type'],
907 'repo_type': obj['dbrepo']['repo_type'],
907 'private': obj['dbrepo']['private'],
908 'private': obj['dbrepo']['private'],
908
909
909 })
910 })
910
911
911 data = {
912 data = {
912 'more': False,
913 'more': False,
913 'results': [{
914 'results': [{
914 'text': _('Repositories'),
915 'text': _('Repositories'),
915 'children': repos
916 'children': repos
916 }] if repos else []
917 }] if repos else []
917 }
918 }
918 return data
919 return data
919
920
920 @LoginRequired()
921 @LoginRequired()
921 @NotAnonymous()
922 @NotAnonymous()
922 @HasRepoPermissionAnyDecorator(
923 @HasRepoPermissionAnyDecorator(
923 'repository.read', 'repository.write', 'repository.admin')
924 'repository.read', 'repository.write', 'repository.admin')
924 @CSRFRequired()
925 @CSRFRequired()
925 @view_config(
926 @view_config(
926 route_name='pullrequest_create', request_method='POST',
927 route_name='pullrequest_create', request_method='POST',
927 renderer=None)
928 renderer=None)
928 def pull_request_create(self):
929 def pull_request_create(self):
929 _ = self.request.translate
930 _ = self.request.translate
930 self.assure_not_empty_repo()
931 self.assure_not_empty_repo()
931 self.load_default_context()
932 self.load_default_context()
932
933
933 controls = peppercorn.parse(self.request.POST.items())
934 controls = peppercorn.parse(self.request.POST.items())
934
935
935 try:
936 try:
936 form = PullRequestForm(
937 form = PullRequestForm(
937 self.request.translate, self.db_repo.repo_id)()
938 self.request.translate, self.db_repo.repo_id)()
938 _form = form.to_python(controls)
939 _form = form.to_python(controls)
939 except formencode.Invalid as errors:
940 except formencode.Invalid as errors:
940 if errors.error_dict.get('revisions'):
941 if errors.error_dict.get('revisions'):
941 msg = 'Revisions: %s' % errors.error_dict['revisions']
942 msg = 'Revisions: %s' % errors.error_dict['revisions']
942 elif errors.error_dict.get('pullrequest_title'):
943 elif errors.error_dict.get('pullrequest_title'):
943 msg = errors.error_dict.get('pullrequest_title')
944 msg = errors.error_dict.get('pullrequest_title')
944 else:
945 else:
945 msg = _('Error creating pull request: {}').format(errors)
946 msg = _('Error creating pull request: {}').format(errors)
946 log.exception(msg)
947 log.exception(msg)
947 h.flash(msg, 'error')
948 h.flash(msg, 'error')
948
949
949 # would rather just go back to form ...
950 # would rather just go back to form ...
950 raise HTTPFound(
951 raise HTTPFound(
951 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
952 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
952
953
953 source_repo = _form['source_repo']
954 source_repo = _form['source_repo']
954 source_ref = _form['source_ref']
955 source_ref = _form['source_ref']
955 target_repo = _form['target_repo']
956 target_repo = _form['target_repo']
956 target_ref = _form['target_ref']
957 target_ref = _form['target_ref']
957 commit_ids = _form['revisions'][::-1]
958 commit_ids = _form['revisions'][::-1]
958 common_ancestor_id = _form['common_ancestor']
959 common_ancestor_id = _form['common_ancestor']
959
960
960 # find the ancestor for this pr
961 # find the ancestor for this pr
961 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
962 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
962 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
963 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
963
964
964 if not (source_db_repo or target_db_repo):
965 if not (source_db_repo or target_db_repo):
965 h.flash(_('source_repo or target repo not found'), category='error')
966 h.flash(_('source_repo or target repo not found'), category='error')
966 raise HTTPFound(
967 raise HTTPFound(
967 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
968 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
968
969
969 # re-check permissions again here
970 # re-check permissions again here
970 # source_repo we must have read permissions
971 # source_repo we must have read permissions
971
972
972 source_perm = HasRepoPermissionAny(
973 source_perm = HasRepoPermissionAny(
973 'repository.read', 'repository.write', 'repository.admin')(
974 'repository.read', 'repository.write', 'repository.admin')(
974 source_db_repo.repo_name)
975 source_db_repo.repo_name)
975 if not source_perm:
976 if not source_perm:
976 msg = _('Not Enough permissions to source repo `{}`.'.format(
977 msg = _('Not Enough permissions to source repo `{}`.'.format(
977 source_db_repo.repo_name))
978 source_db_repo.repo_name))
978 h.flash(msg, category='error')
979 h.flash(msg, category='error')
979 # copy the args back to redirect
980 # copy the args back to redirect
980 org_query = self.request.GET.mixed()
981 org_query = self.request.GET.mixed()
981 raise HTTPFound(
982 raise HTTPFound(
982 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
983 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
983 _query=org_query))
984 _query=org_query))
984
985
985 # target repo we must have read permissions, and also later on
986 # target repo we must have read permissions, and also later on
986 # we want to check branch permissions here
987 # we want to check branch permissions here
987 target_perm = HasRepoPermissionAny(
988 target_perm = HasRepoPermissionAny(
988 'repository.read', 'repository.write', 'repository.admin')(
989 'repository.read', 'repository.write', 'repository.admin')(
989 target_db_repo.repo_name)
990 target_db_repo.repo_name)
990 if not target_perm:
991 if not target_perm:
991 msg = _('Not Enough permissions to target repo `{}`.'.format(
992 msg = _('Not Enough permissions to target repo `{}`.'.format(
992 target_db_repo.repo_name))
993 target_db_repo.repo_name))
993 h.flash(msg, category='error')
994 h.flash(msg, category='error')
994 # copy the args back to redirect
995 # copy the args back to redirect
995 org_query = self.request.GET.mixed()
996 org_query = self.request.GET.mixed()
996 raise HTTPFound(
997 raise HTTPFound(
997 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
998 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
998 _query=org_query))
999 _query=org_query))
999
1000
1000 source_scm = source_db_repo.scm_instance()
1001 source_scm = source_db_repo.scm_instance()
1001 target_scm = target_db_repo.scm_instance()
1002 target_scm = target_db_repo.scm_instance()
1002
1003
1003 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1004 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1004 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1005 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1005
1006
1006 ancestor = source_scm.get_common_ancestor(
1007 ancestor = source_scm.get_common_ancestor(
1007 source_commit.raw_id, target_commit.raw_id, target_scm)
1008 source_commit.raw_id, target_commit.raw_id, target_scm)
1008
1009
1009 # recalculate target ref based on ancestor
1010 # recalculate target ref based on ancestor
1010 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1011 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1011 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1012 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1012
1013
1013 get_default_reviewers_data, validate_default_reviewers = \
1014 get_default_reviewers_data, validate_default_reviewers = \
1014 PullRequestModel().get_reviewer_functions()
1015 PullRequestModel().get_reviewer_functions()
1015
1016
1016 # recalculate reviewers logic, to make sure we can validate this
1017 # recalculate reviewers logic, to make sure we can validate this
1017 reviewer_rules = get_default_reviewers_data(
1018 reviewer_rules = get_default_reviewers_data(
1018 self._rhodecode_db_user, source_db_repo,
1019 self._rhodecode_db_user, source_db_repo,
1019 source_commit, target_db_repo, target_commit)
1020 source_commit, target_db_repo, target_commit)
1020
1021
1021 given_reviewers = _form['review_members']
1022 given_reviewers = _form['review_members']
1022 reviewers = validate_default_reviewers(
1023 reviewers = validate_default_reviewers(
1023 given_reviewers, reviewer_rules)
1024 given_reviewers, reviewer_rules)
1024
1025
1025 pullrequest_title = _form['pullrequest_title']
1026 pullrequest_title = _form['pullrequest_title']
1026 title_source_ref = source_ref.split(':', 2)[1]
1027 title_source_ref = source_ref.split(':', 2)[1]
1027 if not pullrequest_title:
1028 if not pullrequest_title:
1028 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1029 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1029 source=source_repo,
1030 source=source_repo,
1030 source_ref=title_source_ref,
1031 source_ref=title_source_ref,
1031 target=target_repo
1032 target=target_repo
1032 )
1033 )
1033
1034
1034 description = _form['pullrequest_desc']
1035 description = _form['pullrequest_desc']
1035 description_renderer = _form['description_renderer']
1036 description_renderer = _form['description_renderer']
1036
1037
1037 try:
1038 try:
1038 pull_request = PullRequestModel().create(
1039 pull_request = PullRequestModel().create(
1039 created_by=self._rhodecode_user.user_id,
1040 created_by=self._rhodecode_user.user_id,
1040 source_repo=source_repo,
1041 source_repo=source_repo,
1041 source_ref=source_ref,
1042 source_ref=source_ref,
1042 target_repo=target_repo,
1043 target_repo=target_repo,
1043 target_ref=target_ref,
1044 target_ref=target_ref,
1044 revisions=commit_ids,
1045 revisions=commit_ids,
1045 common_ancestor_id=common_ancestor_id,
1046 common_ancestor_id=common_ancestor_id,
1046 reviewers=reviewers,
1047 reviewers=reviewers,
1047 title=pullrequest_title,
1048 title=pullrequest_title,
1048 description=description,
1049 description=description,
1049 description_renderer=description_renderer,
1050 description_renderer=description_renderer,
1050 reviewer_data=reviewer_rules,
1051 reviewer_data=reviewer_rules,
1051 auth_user=self._rhodecode_user
1052 auth_user=self._rhodecode_user
1052 )
1053 )
1053 Session().commit()
1054 Session().commit()
1054
1055
1055 h.flash(_('Successfully opened new pull request'),
1056 h.flash(_('Successfully opened new pull request'),
1056 category='success')
1057 category='success')
1057 except Exception:
1058 except Exception:
1058 msg = _('Error occurred during creation of this pull request.')
1059 msg = _('Error occurred during creation of this pull request.')
1059 log.exception(msg)
1060 log.exception(msg)
1060 h.flash(msg, category='error')
1061 h.flash(msg, category='error')
1061
1062
1062 # copy the args back to redirect
1063 # copy the args back to redirect
1063 org_query = self.request.GET.mixed()
1064 org_query = self.request.GET.mixed()
1064 raise HTTPFound(
1065 raise HTTPFound(
1065 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1066 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1066 _query=org_query))
1067 _query=org_query))
1067
1068
1068 raise HTTPFound(
1069 raise HTTPFound(
1069 h.route_path('pullrequest_show', repo_name=target_repo,
1070 h.route_path('pullrequest_show', repo_name=target_repo,
1070 pull_request_id=pull_request.pull_request_id))
1071 pull_request_id=pull_request.pull_request_id))
1071
1072
1072 @LoginRequired()
1073 @LoginRequired()
1073 @NotAnonymous()
1074 @NotAnonymous()
1074 @HasRepoPermissionAnyDecorator(
1075 @HasRepoPermissionAnyDecorator(
1075 'repository.read', 'repository.write', 'repository.admin')
1076 'repository.read', 'repository.write', 'repository.admin')
1076 @CSRFRequired()
1077 @CSRFRequired()
1077 @view_config(
1078 @view_config(
1078 route_name='pullrequest_update', request_method='POST',
1079 route_name='pullrequest_update', request_method='POST',
1079 renderer='json_ext')
1080 renderer='json_ext')
1080 def pull_request_update(self):
1081 def pull_request_update(self):
1081 pull_request = PullRequest.get_or_404(
1082 pull_request = PullRequest.get_or_404(
1082 self.request.matchdict['pull_request_id'])
1083 self.request.matchdict['pull_request_id'])
1083 _ = self.request.translate
1084 _ = self.request.translate
1084
1085
1085 self.load_default_context()
1086 self.load_default_context()
1086 redirect_url = None
1087 redirect_url = None
1087
1088
1088 if pull_request.is_closed():
1089 if pull_request.is_closed():
1089 log.debug('update: forbidden because pull request is closed')
1090 log.debug('update: forbidden because pull request is closed')
1090 msg = _(u'Cannot update closed pull requests.')
1091 msg = _(u'Cannot update closed pull requests.')
1091 h.flash(msg, category='error')
1092 h.flash(msg, category='error')
1092 return {'response': True,
1093 return {'response': True,
1093 'redirect_url': redirect_url}
1094 'redirect_url': redirect_url}
1094
1095
1095 is_state_changing = pull_request.is_state_changing()
1096 is_state_changing = pull_request.is_state_changing()
1096
1097
1097 # only owner or admin can update it
1098 # only owner or admin can update it
1098 allowed_to_update = PullRequestModel().check_user_update(
1099 allowed_to_update = PullRequestModel().check_user_update(
1099 pull_request, self._rhodecode_user)
1100 pull_request, self._rhodecode_user)
1100 if allowed_to_update:
1101 if allowed_to_update:
1101 controls = peppercorn.parse(self.request.POST.items())
1102 controls = peppercorn.parse(self.request.POST.items())
1102 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1103 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1103
1104
1104 if 'review_members' in controls:
1105 if 'review_members' in controls:
1105 self._update_reviewers(
1106 self._update_reviewers(
1106 pull_request, controls['review_members'],
1107 pull_request, controls['review_members'],
1107 pull_request.reviewer_data)
1108 pull_request.reviewer_data)
1108 elif str2bool(self.request.POST.get('update_commits', 'false')):
1109 elif str2bool(self.request.POST.get('update_commits', 'false')):
1109 if is_state_changing:
1110 if is_state_changing:
1110 log.debug('commits update: forbidden because pull request is in state %s',
1111 log.debug('commits update: forbidden because pull request is in state %s',
1111 pull_request.pull_request_state)
1112 pull_request.pull_request_state)
1112 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1113 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1113 u'Current state is: `{}`').format(
1114 u'Current state is: `{}`').format(
1114 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1115 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1115 h.flash(msg, category='error')
1116 h.flash(msg, category='error')
1116 return {'response': True,
1117 return {'response': True,
1117 'redirect_url': redirect_url}
1118 'redirect_url': redirect_url}
1118
1119
1119 self._update_commits(pull_request)
1120 self._update_commits(pull_request)
1120 if force_refresh:
1121 if force_refresh:
1121 redirect_url = h.route_path(
1122 redirect_url = h.route_path(
1122 'pullrequest_show', repo_name=self.db_repo_name,
1123 'pullrequest_show', repo_name=self.db_repo_name,
1123 pull_request_id=pull_request.pull_request_id,
1124 pull_request_id=pull_request.pull_request_id,
1124 _query={"force_refresh": 1})
1125 _query={"force_refresh": 1})
1125 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1126 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1126 self._edit_pull_request(pull_request)
1127 self._edit_pull_request(pull_request)
1127 else:
1128 else:
1128 raise HTTPBadRequest()
1129 raise HTTPBadRequest()
1129
1130
1130 return {'response': True,
1131 return {'response': True,
1131 'redirect_url': redirect_url}
1132 'redirect_url': redirect_url}
1132 raise HTTPForbidden()
1133 raise HTTPForbidden()
1133
1134
1134 def _edit_pull_request(self, pull_request):
1135 def _edit_pull_request(self, pull_request):
1135 _ = self.request.translate
1136 _ = self.request.translate
1136
1137
1137 try:
1138 try:
1138 PullRequestModel().edit(
1139 PullRequestModel().edit(
1139 pull_request,
1140 pull_request,
1140 self.request.POST.get('title'),
1141 self.request.POST.get('title'),
1141 self.request.POST.get('description'),
1142 self.request.POST.get('description'),
1142 self.request.POST.get('description_renderer'),
1143 self.request.POST.get('description_renderer'),
1143 self._rhodecode_user)
1144 self._rhodecode_user)
1144 except ValueError:
1145 except ValueError:
1145 msg = _(u'Cannot update closed pull requests.')
1146 msg = _(u'Cannot update closed pull requests.')
1146 h.flash(msg, category='error')
1147 h.flash(msg, category='error')
1147 return
1148 return
1148 else:
1149 else:
1149 Session().commit()
1150 Session().commit()
1150
1151
1151 msg = _(u'Pull request title & description updated.')
1152 msg = _(u'Pull request title & description updated.')
1152 h.flash(msg, category='success')
1153 h.flash(msg, category='success')
1153 return
1154 return
1154
1155
1155 def _update_commits(self, pull_request):
1156 def _update_commits(self, pull_request):
1156 _ = self.request.translate
1157 _ = self.request.translate
1157
1158
1158 with pull_request.set_state(PullRequest.STATE_UPDATING):
1159 with pull_request.set_state(PullRequest.STATE_UPDATING):
1159 resp = PullRequestModel().update_commits(
1160 resp = PullRequestModel().update_commits(
1160 pull_request, self._rhodecode_db_user)
1161 pull_request, self._rhodecode_db_user)
1161
1162
1162 if resp.executed:
1163 if resp.executed:
1163
1164
1164 if resp.target_changed and resp.source_changed:
1165 if resp.target_changed and resp.source_changed:
1165 changed = 'target and source repositories'
1166 changed = 'target and source repositories'
1166 elif resp.target_changed and not resp.source_changed:
1167 elif resp.target_changed and not resp.source_changed:
1167 changed = 'target repository'
1168 changed = 'target repository'
1168 elif not resp.target_changed and resp.source_changed:
1169 elif not resp.target_changed and resp.source_changed:
1169 changed = 'source repository'
1170 changed = 'source repository'
1170 else:
1171 else:
1171 changed = 'nothing'
1172 changed = 'nothing'
1172
1173
1173 msg = _(u'Pull request updated to "{source_commit_id}" with '
1174 msg = _(u'Pull request updated to "{source_commit_id}" with '
1174 u'{count_added} added, {count_removed} removed commits. '
1175 u'{count_added} added, {count_removed} removed commits. '
1175 u'Source of changes: {change_source}')
1176 u'Source of changes: {change_source}')
1176 msg = msg.format(
1177 msg = msg.format(
1177 source_commit_id=pull_request.source_ref_parts.commit_id,
1178 source_commit_id=pull_request.source_ref_parts.commit_id,
1178 count_added=len(resp.changes.added),
1179 count_added=len(resp.changes.added),
1179 count_removed=len(resp.changes.removed),
1180 count_removed=len(resp.changes.removed),
1180 change_source=changed)
1181 change_source=changed)
1181 h.flash(msg, category='success')
1182 h.flash(msg, category='success')
1182
1183
1183 channel = '/repo${}$/pr/{}'.format(
1184 channel = '/repo${}$/pr/{}'.format(
1184 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1185 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1185 message = msg + (
1186 message = msg + (
1186 ' - <a onclick="window.location.reload()">'
1187 ' - <a onclick="window.location.reload()">'
1187 '<strong>{}</strong></a>'.format(_('Reload page')))
1188 '<strong>{}</strong></a>'.format(_('Reload page')))
1188 channelstream.post_message(
1189 channelstream.post_message(
1189 channel, message, self._rhodecode_user.username,
1190 channel, message, self._rhodecode_user.username,
1190 registry=self.request.registry)
1191 registry=self.request.registry)
1191 else:
1192 else:
1192 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1193 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1193 warning_reasons = [
1194 warning_reasons = [
1194 UpdateFailureReason.NO_CHANGE,
1195 UpdateFailureReason.NO_CHANGE,
1195 UpdateFailureReason.WRONG_REF_TYPE,
1196 UpdateFailureReason.WRONG_REF_TYPE,
1196 ]
1197 ]
1197 category = 'warning' if resp.reason in warning_reasons else 'error'
1198 category = 'warning' if resp.reason in warning_reasons else 'error'
1198 h.flash(msg, category=category)
1199 h.flash(msg, category=category)
1199
1200
1200 @LoginRequired()
1201 @LoginRequired()
1201 @NotAnonymous()
1202 @NotAnonymous()
1202 @HasRepoPermissionAnyDecorator(
1203 @HasRepoPermissionAnyDecorator(
1203 'repository.read', 'repository.write', 'repository.admin')
1204 'repository.read', 'repository.write', 'repository.admin')
1204 @CSRFRequired()
1205 @CSRFRequired()
1205 @view_config(
1206 @view_config(
1206 route_name='pullrequest_merge', request_method='POST',
1207 route_name='pullrequest_merge', request_method='POST',
1207 renderer='json_ext')
1208 renderer='json_ext')
1208 def pull_request_merge(self):
1209 def pull_request_merge(self):
1209 """
1210 """
1210 Merge will perform a server-side merge of the specified
1211 Merge will perform a server-side merge of the specified
1211 pull request, if the pull request is approved and mergeable.
1212 pull request, if the pull request is approved and mergeable.
1212 After successful merging, the pull request is automatically
1213 After successful merging, the pull request is automatically
1213 closed, with a relevant comment.
1214 closed, with a relevant comment.
1214 """
1215 """
1215 pull_request = PullRequest.get_or_404(
1216 pull_request = PullRequest.get_or_404(
1216 self.request.matchdict['pull_request_id'])
1217 self.request.matchdict['pull_request_id'])
1217 _ = self.request.translate
1218 _ = self.request.translate
1218
1219
1219 if pull_request.is_state_changing():
1220 if pull_request.is_state_changing():
1220 log.debug('show: forbidden because pull request is in state %s',
1221 log.debug('show: forbidden because pull request is in state %s',
1221 pull_request.pull_request_state)
1222 pull_request.pull_request_state)
1222 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1223 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1223 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1224 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1224 pull_request.pull_request_state)
1225 pull_request.pull_request_state)
1225 h.flash(msg, category='error')
1226 h.flash(msg, category='error')
1226 raise HTTPFound(
1227 raise HTTPFound(
1227 h.route_path('pullrequest_show',
1228 h.route_path('pullrequest_show',
1228 repo_name=pull_request.target_repo.repo_name,
1229 repo_name=pull_request.target_repo.repo_name,
1229 pull_request_id=pull_request.pull_request_id))
1230 pull_request_id=pull_request.pull_request_id))
1230
1231
1231 self.load_default_context()
1232 self.load_default_context()
1232
1233
1233 with pull_request.set_state(PullRequest.STATE_UPDATING):
1234 with pull_request.set_state(PullRequest.STATE_UPDATING):
1234 check = MergeCheck.validate(
1235 check = MergeCheck.validate(
1235 pull_request, auth_user=self._rhodecode_user,
1236 pull_request, auth_user=self._rhodecode_user,
1236 translator=self.request.translate)
1237 translator=self.request.translate)
1237 merge_possible = not check.failed
1238 merge_possible = not check.failed
1238
1239
1239 for err_type, error_msg in check.errors:
1240 for err_type, error_msg in check.errors:
1240 h.flash(error_msg, category=err_type)
1241 h.flash(error_msg, category=err_type)
1241
1242
1242 if merge_possible:
1243 if merge_possible:
1243 log.debug("Pre-conditions checked, trying to merge.")
1244 log.debug("Pre-conditions checked, trying to merge.")
1244 extras = vcs_operation_context(
1245 extras = vcs_operation_context(
1245 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1246 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1246 username=self._rhodecode_db_user.username, action='push',
1247 username=self._rhodecode_db_user.username, action='push',
1247 scm=pull_request.target_repo.repo_type)
1248 scm=pull_request.target_repo.repo_type)
1248 with pull_request.set_state(PullRequest.STATE_UPDATING):
1249 with pull_request.set_state(PullRequest.STATE_UPDATING):
1249 self._merge_pull_request(
1250 self._merge_pull_request(
1250 pull_request, self._rhodecode_db_user, extras)
1251 pull_request, self._rhodecode_db_user, extras)
1251 else:
1252 else:
1252 log.debug("Pre-conditions failed, NOT merging.")
1253 log.debug("Pre-conditions failed, NOT merging.")
1253
1254
1254 raise HTTPFound(
1255 raise HTTPFound(
1255 h.route_path('pullrequest_show',
1256 h.route_path('pullrequest_show',
1256 repo_name=pull_request.target_repo.repo_name,
1257 repo_name=pull_request.target_repo.repo_name,
1257 pull_request_id=pull_request.pull_request_id))
1258 pull_request_id=pull_request.pull_request_id))
1258
1259
1259 def _merge_pull_request(self, pull_request, user, extras):
1260 def _merge_pull_request(self, pull_request, user, extras):
1260 _ = self.request.translate
1261 _ = self.request.translate
1261 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1262 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1262
1263
1263 if merge_resp.executed:
1264 if merge_resp.executed:
1264 log.debug("The merge was successful, closing the pull request.")
1265 log.debug("The merge was successful, closing the pull request.")
1265 PullRequestModel().close_pull_request(
1266 PullRequestModel().close_pull_request(
1266 pull_request.pull_request_id, user)
1267 pull_request.pull_request_id, user)
1267 Session().commit()
1268 Session().commit()
1268 msg = _('Pull request was successfully merged and closed.')
1269 msg = _('Pull request was successfully merged and closed.')
1269 h.flash(msg, category='success')
1270 h.flash(msg, category='success')
1270 else:
1271 else:
1271 log.debug(
1272 log.debug(
1272 "The merge was not successful. Merge response: %s", merge_resp)
1273 "The merge was not successful. Merge response: %s", merge_resp)
1273 msg = merge_resp.merge_status_message
1274 msg = merge_resp.merge_status_message
1274 h.flash(msg, category='error')
1275 h.flash(msg, category='error')
1275
1276
1276 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1277 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1277 _ = self.request.translate
1278 _ = self.request.translate
1278
1279
1279 get_default_reviewers_data, validate_default_reviewers = \
1280 get_default_reviewers_data, validate_default_reviewers = \
1280 PullRequestModel().get_reviewer_functions()
1281 PullRequestModel().get_reviewer_functions()
1281
1282
1282 try:
1283 try:
1283 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1284 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1284 except ValueError as e:
1285 except ValueError as e:
1285 log.error('Reviewers Validation: {}'.format(e))
1286 log.error('Reviewers Validation: {}'.format(e))
1286 h.flash(e, category='error')
1287 h.flash(e, category='error')
1287 return
1288 return
1288
1289
1289 old_calculated_status = pull_request.calculated_review_status()
1290 old_calculated_status = pull_request.calculated_review_status()
1290 PullRequestModel().update_reviewers(
1291 PullRequestModel().update_reviewers(
1291 pull_request, reviewers, self._rhodecode_user)
1292 pull_request, reviewers, self._rhodecode_user)
1292 h.flash(_('Pull request reviewers updated.'), category='success')
1293 h.flash(_('Pull request reviewers updated.'), category='success')
1293 Session().commit()
1294 Session().commit()
1294
1295
1295 # trigger status changed if change in reviewers changes the status
1296 # trigger status changed if change in reviewers changes the status
1296 calculated_status = pull_request.calculated_review_status()
1297 calculated_status = pull_request.calculated_review_status()
1297 if old_calculated_status != calculated_status:
1298 if old_calculated_status != calculated_status:
1298 PullRequestModel().trigger_pull_request_hook(
1299 PullRequestModel().trigger_pull_request_hook(
1299 pull_request, self._rhodecode_user, 'review_status_change',
1300 pull_request, self._rhodecode_user, 'review_status_change',
1300 data={'status': calculated_status})
1301 data={'status': calculated_status})
1301
1302
1302 @LoginRequired()
1303 @LoginRequired()
1303 @NotAnonymous()
1304 @NotAnonymous()
1304 @HasRepoPermissionAnyDecorator(
1305 @HasRepoPermissionAnyDecorator(
1305 'repository.read', 'repository.write', 'repository.admin')
1306 'repository.read', 'repository.write', 'repository.admin')
1306 @CSRFRequired()
1307 @CSRFRequired()
1307 @view_config(
1308 @view_config(
1308 route_name='pullrequest_delete', request_method='POST',
1309 route_name='pullrequest_delete', request_method='POST',
1309 renderer='json_ext')
1310 renderer='json_ext')
1310 def pull_request_delete(self):
1311 def pull_request_delete(self):
1311 _ = self.request.translate
1312 _ = self.request.translate
1312
1313
1313 pull_request = PullRequest.get_or_404(
1314 pull_request = PullRequest.get_or_404(
1314 self.request.matchdict['pull_request_id'])
1315 self.request.matchdict['pull_request_id'])
1315 self.load_default_context()
1316 self.load_default_context()
1316
1317
1317 pr_closed = pull_request.is_closed()
1318 pr_closed = pull_request.is_closed()
1318 allowed_to_delete = PullRequestModel().check_user_delete(
1319 allowed_to_delete = PullRequestModel().check_user_delete(
1319 pull_request, self._rhodecode_user) and not pr_closed
1320 pull_request, self._rhodecode_user) and not pr_closed
1320
1321
1321 # only owner can delete it !
1322 # only owner can delete it !
1322 if allowed_to_delete:
1323 if allowed_to_delete:
1323 PullRequestModel().delete(pull_request, self._rhodecode_user)
1324 PullRequestModel().delete(pull_request, self._rhodecode_user)
1324 Session().commit()
1325 Session().commit()
1325 h.flash(_('Successfully deleted pull request'),
1326 h.flash(_('Successfully deleted pull request'),
1326 category='success')
1327 category='success')
1327 raise HTTPFound(h.route_path('pullrequest_show_all',
1328 raise HTTPFound(h.route_path('pullrequest_show_all',
1328 repo_name=self.db_repo_name))
1329 repo_name=self.db_repo_name))
1329
1330
1330 log.warning('user %s tried to delete pull request without access',
1331 log.warning('user %s tried to delete pull request without access',
1331 self._rhodecode_user)
1332 self._rhodecode_user)
1332 raise HTTPNotFound()
1333 raise HTTPNotFound()
1333
1334
1334 @LoginRequired()
1335 @LoginRequired()
1335 @NotAnonymous()
1336 @NotAnonymous()
1336 @HasRepoPermissionAnyDecorator(
1337 @HasRepoPermissionAnyDecorator(
1337 'repository.read', 'repository.write', 'repository.admin')
1338 'repository.read', 'repository.write', 'repository.admin')
1338 @CSRFRequired()
1339 @CSRFRequired()
1339 @view_config(
1340 @view_config(
1340 route_name='pullrequest_comment_create', request_method='POST',
1341 route_name='pullrequest_comment_create', request_method='POST',
1341 renderer='json_ext')
1342 renderer='json_ext')
1342 def pull_request_comment_create(self):
1343 def pull_request_comment_create(self):
1343 _ = self.request.translate
1344 _ = self.request.translate
1344
1345
1345 pull_request = PullRequest.get_or_404(
1346 pull_request = PullRequest.get_or_404(
1346 self.request.matchdict['pull_request_id'])
1347 self.request.matchdict['pull_request_id'])
1347 pull_request_id = pull_request.pull_request_id
1348 pull_request_id = pull_request.pull_request_id
1348
1349
1349 if pull_request.is_closed():
1350 if pull_request.is_closed():
1350 log.debug('comment: forbidden because pull request is closed')
1351 log.debug('comment: forbidden because pull request is closed')
1351 raise HTTPForbidden()
1352 raise HTTPForbidden()
1352
1353
1353 allowed_to_comment = PullRequestModel().check_user_comment(
1354 allowed_to_comment = PullRequestModel().check_user_comment(
1354 pull_request, self._rhodecode_user)
1355 pull_request, self._rhodecode_user)
1355 if not allowed_to_comment:
1356 if not allowed_to_comment:
1356 log.debug(
1357 log.debug(
1357 'comment: forbidden because pull request is from forbidden repo')
1358 'comment: forbidden because pull request is from forbidden repo')
1358 raise HTTPForbidden()
1359 raise HTTPForbidden()
1359
1360
1360 c = self.load_default_context()
1361 c = self.load_default_context()
1361
1362
1362 status = self.request.POST.get('changeset_status', None)
1363 status = self.request.POST.get('changeset_status', None)
1363 text = self.request.POST.get('text')
1364 text = self.request.POST.get('text')
1364 comment_type = self.request.POST.get('comment_type')
1365 comment_type = self.request.POST.get('comment_type')
1365 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1366 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1366 close_pull_request = self.request.POST.get('close_pull_request')
1367 close_pull_request = self.request.POST.get('close_pull_request')
1367
1368
1368 # the logic here should work like following, if we submit close
1369 # the logic here should work like following, if we submit close
1369 # pr comment, use `close_pull_request_with_comment` function
1370 # pr comment, use `close_pull_request_with_comment` function
1370 # else handle regular comment logic
1371 # else handle regular comment logic
1371
1372
1372 if close_pull_request:
1373 if close_pull_request:
1373 # only owner or admin or person with write permissions
1374 # only owner or admin or person with write permissions
1374 allowed_to_close = PullRequestModel().check_user_update(
1375 allowed_to_close = PullRequestModel().check_user_update(
1375 pull_request, self._rhodecode_user)
1376 pull_request, self._rhodecode_user)
1376 if not allowed_to_close:
1377 if not allowed_to_close:
1377 log.debug('comment: forbidden because not allowed to close '
1378 log.debug('comment: forbidden because not allowed to close '
1378 'pull request %s', pull_request_id)
1379 'pull request %s', pull_request_id)
1379 raise HTTPForbidden()
1380 raise HTTPForbidden()
1380
1381
1381 # This also triggers `review_status_change`
1382 # This also triggers `review_status_change`
1382 comment, status = PullRequestModel().close_pull_request_with_comment(
1383 comment, status = PullRequestModel().close_pull_request_with_comment(
1383 pull_request, self._rhodecode_user, self.db_repo, message=text,
1384 pull_request, self._rhodecode_user, self.db_repo, message=text,
1384 auth_user=self._rhodecode_user)
1385 auth_user=self._rhodecode_user)
1385 Session().flush()
1386 Session().flush()
1386
1387
1387 PullRequestModel().trigger_pull_request_hook(
1388 PullRequestModel().trigger_pull_request_hook(
1388 pull_request, self._rhodecode_user, 'comment',
1389 pull_request, self._rhodecode_user, 'comment',
1389 data={'comment': comment})
1390 data={'comment': comment})
1390
1391
1391 else:
1392 else:
1392 # regular comment case, could be inline, or one with status.
1393 # regular comment case, could be inline, or one with status.
1393 # for that one we check also permissions
1394 # for that one we check also permissions
1394
1395
1395 allowed_to_change_status = PullRequestModel().check_user_change_status(
1396 allowed_to_change_status = PullRequestModel().check_user_change_status(
1396 pull_request, self._rhodecode_user)
1397 pull_request, self._rhodecode_user)
1397
1398
1398 if status and allowed_to_change_status:
1399 if status and allowed_to_change_status:
1399 message = (_('Status change %(transition_icon)s %(status)s')
1400 message = (_('Status change %(transition_icon)s %(status)s')
1400 % {'transition_icon': '>',
1401 % {'transition_icon': '>',
1401 'status': ChangesetStatus.get_status_lbl(status)})
1402 'status': ChangesetStatus.get_status_lbl(status)})
1402 text = text or message
1403 text = text or message
1403
1404
1404 comment = CommentsModel().create(
1405 comment = CommentsModel().create(
1405 text=text,
1406 text=text,
1406 repo=self.db_repo.repo_id,
1407 repo=self.db_repo.repo_id,
1407 user=self._rhodecode_user.user_id,
1408 user=self._rhodecode_user.user_id,
1408 pull_request=pull_request,
1409 pull_request=pull_request,
1409 f_path=self.request.POST.get('f_path'),
1410 f_path=self.request.POST.get('f_path'),
1410 line_no=self.request.POST.get('line'),
1411 line_no=self.request.POST.get('line'),
1411 status_change=(ChangesetStatus.get_status_lbl(status)
1412 status_change=(ChangesetStatus.get_status_lbl(status)
1412 if status and allowed_to_change_status else None),
1413 if status and allowed_to_change_status else None),
1413 status_change_type=(status
1414 status_change_type=(status
1414 if status and allowed_to_change_status else None),
1415 if status and allowed_to_change_status else None),
1415 comment_type=comment_type,
1416 comment_type=comment_type,
1416 resolves_comment_id=resolves_comment_id,
1417 resolves_comment_id=resolves_comment_id,
1417 auth_user=self._rhodecode_user
1418 auth_user=self._rhodecode_user
1418 )
1419 )
1419
1420
1420 if allowed_to_change_status:
1421 if allowed_to_change_status:
1421 # calculate old status before we change it
1422 # calculate old status before we change it
1422 old_calculated_status = pull_request.calculated_review_status()
1423 old_calculated_status = pull_request.calculated_review_status()
1423
1424
1424 # get status if set !
1425 # get status if set !
1425 if status:
1426 if status:
1426 ChangesetStatusModel().set_status(
1427 ChangesetStatusModel().set_status(
1427 self.db_repo.repo_id,
1428 self.db_repo.repo_id,
1428 status,
1429 status,
1429 self._rhodecode_user.user_id,
1430 self._rhodecode_user.user_id,
1430 comment,
1431 comment,
1431 pull_request=pull_request
1432 pull_request=pull_request
1432 )
1433 )
1433
1434
1434 Session().flush()
1435 Session().flush()
1435 # this is somehow required to get access to some relationship
1436 # this is somehow required to get access to some relationship
1436 # loaded on comment
1437 # loaded on comment
1437 Session().refresh(comment)
1438 Session().refresh(comment)
1438
1439
1439 PullRequestModel().trigger_pull_request_hook(
1440 PullRequestModel().trigger_pull_request_hook(
1440 pull_request, self._rhodecode_user, 'comment',
1441 pull_request, self._rhodecode_user, 'comment',
1441 data={'comment': comment})
1442 data={'comment': comment})
1442
1443
1443 # we now calculate the status of pull request, and based on that
1444 # we now calculate the status of pull request, and based on that
1444 # calculation we set the commits status
1445 # calculation we set the commits status
1445 calculated_status = pull_request.calculated_review_status()
1446 calculated_status = pull_request.calculated_review_status()
1446 if old_calculated_status != calculated_status:
1447 if old_calculated_status != calculated_status:
1447 PullRequestModel().trigger_pull_request_hook(
1448 PullRequestModel().trigger_pull_request_hook(
1448 pull_request, self._rhodecode_user, 'review_status_change',
1449 pull_request, self._rhodecode_user, 'review_status_change',
1449 data={'status': calculated_status})
1450 data={'status': calculated_status})
1450
1451
1451 Session().commit()
1452 Session().commit()
1452
1453
1453 data = {
1454 data = {
1454 'target_id': h.safeid(h.safe_unicode(
1455 'target_id': h.safeid(h.safe_unicode(
1455 self.request.POST.get('f_path'))),
1456 self.request.POST.get('f_path'))),
1456 }
1457 }
1457 if comment:
1458 if comment:
1458 c.co = comment
1459 c.co = comment
1459 rendered_comment = render(
1460 rendered_comment = render(
1460 'rhodecode:templates/changeset/changeset_comment_block.mako',
1461 'rhodecode:templates/changeset/changeset_comment_block.mako',
1461 self._get_template_context(c), self.request)
1462 self._get_template_context(c), self.request)
1462
1463
1463 data.update(comment.get_dict())
1464 data.update(comment.get_dict())
1464 data.update({'rendered_text': rendered_comment})
1465 data.update({'rendered_text': rendered_comment})
1465
1466
1466 return data
1467 return data
1467
1468
1468 @LoginRequired()
1469 @LoginRequired()
1469 @NotAnonymous()
1470 @NotAnonymous()
1470 @HasRepoPermissionAnyDecorator(
1471 @HasRepoPermissionAnyDecorator(
1471 'repository.read', 'repository.write', 'repository.admin')
1472 'repository.read', 'repository.write', 'repository.admin')
1472 @CSRFRequired()
1473 @CSRFRequired()
1473 @view_config(
1474 @view_config(
1474 route_name='pullrequest_comment_delete', request_method='POST',
1475 route_name='pullrequest_comment_delete', request_method='POST',
1475 renderer='json_ext')
1476 renderer='json_ext')
1476 def pull_request_comment_delete(self):
1477 def pull_request_comment_delete(self):
1477 pull_request = PullRequest.get_or_404(
1478 pull_request = PullRequest.get_or_404(
1478 self.request.matchdict['pull_request_id'])
1479 self.request.matchdict['pull_request_id'])
1479
1480
1480 comment = ChangesetComment.get_or_404(
1481 comment = ChangesetComment.get_or_404(
1481 self.request.matchdict['comment_id'])
1482 self.request.matchdict['comment_id'])
1482 comment_id = comment.comment_id
1483 comment_id = comment.comment_id
1483
1484
1484 if comment.immutable:
1485 if comment.immutable:
1485 # don't allow deleting comments that are immutable
1486 # don't allow deleting comments that are immutable
1486 raise HTTPForbidden()
1487 raise HTTPForbidden()
1487
1488
1488 if pull_request.is_closed():
1489 if pull_request.is_closed():
1489 log.debug('comment: forbidden because pull request is closed')
1490 log.debug('comment: forbidden because pull request is closed')
1490 raise HTTPForbidden()
1491 raise HTTPForbidden()
1491
1492
1492 if not comment:
1493 if not comment:
1493 log.debug('Comment with id:%s not found, skipping', comment_id)
1494 log.debug('Comment with id:%s not found, skipping', comment_id)
1494 # comment already deleted in another call probably
1495 # comment already deleted in another call probably
1495 return True
1496 return True
1496
1497
1497 if comment.pull_request.is_closed():
1498 if comment.pull_request.is_closed():
1498 # don't allow deleting comments on closed pull request
1499 # don't allow deleting comments on closed pull request
1499 raise HTTPForbidden()
1500 raise HTTPForbidden()
1500
1501
1501 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1502 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1502 super_admin = h.HasPermissionAny('hg.admin')()
1503 super_admin = h.HasPermissionAny('hg.admin')()
1503 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1504 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1504 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1505 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1505 comment_repo_admin = is_repo_admin and is_repo_comment
1506 comment_repo_admin = is_repo_admin and is_repo_comment
1506
1507
1507 if super_admin or comment_owner or comment_repo_admin:
1508 if super_admin or comment_owner or comment_repo_admin:
1508 old_calculated_status = comment.pull_request.calculated_review_status()
1509 old_calculated_status = comment.pull_request.calculated_review_status()
1509 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1510 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1510 Session().commit()
1511 Session().commit()
1511 calculated_status = comment.pull_request.calculated_review_status()
1512 calculated_status = comment.pull_request.calculated_review_status()
1512 if old_calculated_status != calculated_status:
1513 if old_calculated_status != calculated_status:
1513 PullRequestModel().trigger_pull_request_hook(
1514 PullRequestModel().trigger_pull_request_hook(
1514 comment.pull_request, self._rhodecode_user, 'review_status_change',
1515 comment.pull_request, self._rhodecode_user, 'review_status_change',
1515 data={'status': calculated_status})
1516 data={'status': calculated_status})
1516 return True
1517 return True
1517 else:
1518 else:
1518 log.warning('No permissions for user %s to delete comment_id: %s',
1519 log.warning('No permissions for user %s to delete comment_id: %s',
1519 self._rhodecode_db_user, comment_id)
1520 self._rhodecode_db_user, comment_id)
1520 raise HTTPNotFound()
1521 raise HTTPNotFound()
1521
1522
1522 @LoginRequired()
1523 @LoginRequired()
1523 @NotAnonymous()
1524 @NotAnonymous()
1524 @HasRepoPermissionAnyDecorator(
1525 @HasRepoPermissionAnyDecorator(
1525 'repository.read', 'repository.write', 'repository.admin')
1526 'repository.read', 'repository.write', 'repository.admin')
1526 @CSRFRequired()
1527 @CSRFRequired()
1527 @view_config(
1528 @view_config(
1528 route_name='pullrequest_comment_edit', request_method='POST',
1529 route_name='pullrequest_comment_edit', request_method='POST',
1529 renderer='json_ext')
1530 renderer='json_ext')
1530 def pull_request_comment_edit(self):
1531 def pull_request_comment_edit(self):
1532 self.load_default_context()
1533
1531 pull_request = PullRequest.get_or_404(
1534 pull_request = PullRequest.get_or_404(
1532 self.request.matchdict['pull_request_id']
1535 self.request.matchdict['pull_request_id']
1533 )
1536 )
1534 comment = ChangesetComment.get_or_404(
1537 comment = ChangesetComment.get_or_404(
1535 self.request.matchdict['comment_id']
1538 self.request.matchdict['comment_id']
1536 )
1539 )
1537 comment_id = comment.comment_id
1540 comment_id = comment.comment_id
1538
1541
1539 if comment.immutable:
1542 if comment.immutable:
1540 # don't allow deleting comments that are immutable
1543 # don't allow deleting comments that are immutable
1541 raise HTTPForbidden()
1544 raise HTTPForbidden()
1542
1545
1543 if pull_request.is_closed():
1546 if pull_request.is_closed():
1544 log.debug('comment: forbidden because pull request is closed')
1547 log.debug('comment: forbidden because pull request is closed')
1545 raise HTTPForbidden()
1548 raise HTTPForbidden()
1546
1549
1547 if not comment:
1550 if not comment:
1548 log.debug('Comment with id:%s not found, skipping', comment_id)
1551 log.debug('Comment with id:%s not found, skipping', comment_id)
1549 # comment already deleted in another call probably
1552 # comment already deleted in another call probably
1550 return True
1553 return True
1551
1554
1552 if comment.pull_request.is_closed():
1555 if comment.pull_request.is_closed():
1553 # don't allow deleting comments on closed pull request
1556 # don't allow deleting comments on closed pull request
1554 raise HTTPForbidden()
1557 raise HTTPForbidden()
1555
1558
1556 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1559 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1557 super_admin = h.HasPermissionAny('hg.admin')()
1560 super_admin = h.HasPermissionAny('hg.admin')()
1558 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1561 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1559 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1562 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1560 comment_repo_admin = is_repo_admin and is_repo_comment
1563 comment_repo_admin = is_repo_admin and is_repo_comment
1561
1564
1562 if super_admin or comment_owner or comment_repo_admin:
1565 if super_admin or comment_owner or comment_repo_admin:
1563 text = self.request.POST.get('text')
1566 text = self.request.POST.get('text')
1564 version = self.request.POST.get('version')
1567 version = self.request.POST.get('version')
1565 if text == comment.text:
1568 if text == comment.text:
1566 log.warning(
1569 log.warning(
1567 'Comment(PR): '
1570 'Comment(PR): '
1568 'Trying to create new version '
1571 'Trying to create new version '
1569 'of existing comment {}'.format(
1572 'with the same comment body {}'.format(
1570 comment_id,
1573 comment_id,
1571 )
1574 )
1572 )
1575 )
1573 raise HTTPNotFound()
1576 raise HTTPNotFound()
1577
1574 if version.isdigit():
1578 if version.isdigit():
1575 version = int(version)
1579 version = int(version)
1576 else:
1580 else:
1577 log.warning(
1581 log.warning(
1578 'Comment(PR): Wrong version type {} {} '
1582 'Comment(PR): Wrong version type {} {} '
1579 'for comment {}'.format(
1583 'for comment {}'.format(
1580 version,
1584 version,
1581 type(version),
1585 type(version),
1582 comment_id,
1586 comment_id,
1583 )
1587 )
1584 )
1588 )
1585 raise HTTPNotFound()
1589 raise HTTPNotFound()
1586
1590
1591 try:
1587 comment_history = CommentsModel().edit(
1592 comment_history = CommentsModel().edit(
1588 comment_id=comment_id,
1593 comment_id=comment_id,
1589 text=text,
1594 text=text,
1590 auth_user=self._rhodecode_user,
1595 auth_user=self._rhodecode_user,
1591 version=version,
1596 version=version,
1592 )
1597 )
1598 except CommentVersionMismatch:
1599 raise HTTPConflict()
1600
1593 if not comment_history:
1601 if not comment_history:
1594 raise HTTPNotFound()
1602 raise HTTPNotFound()
1603
1595 Session().commit()
1604 Session().commit()
1596 return {
1605 return {
1597 'comment_history_id': comment_history.comment_history_id,
1606 'comment_history_id': comment_history.comment_history_id,
1598 'comment_id': comment.comment_id,
1607 'comment_id': comment.comment_id,
1599 'comment_version': comment_history.version,
1608 'comment_version': comment_history.version,
1609 'comment_author_username': comment_history.author.username,
1610 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1611 'comment_created_on': h.age_component(comment_history.created_on,
1612 time_is_local=True),
1600 }
1613 }
1601 else:
1614 else:
1602 log.warning(
1615 log.warning('No permissions for user %s to edit comment_id: %s',
1603 'No permissions for user {} to edit comment_id: {}'.format(
1616 self._rhodecode_db_user, comment_id)
1604 self._rhodecode_db_user, comment_id
1605 )
1606 )
1607 raise HTTPNotFound()
1617 raise HTTPNotFound()
@@ -1,179 +1,183 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Set of custom exceptions used in RhodeCode
22 Set of custom exceptions used in RhodeCode
23 """
23 """
24
24
25 from webob.exc import HTTPClientError
25 from webob.exc import HTTPClientError
26 from pyramid.httpexceptions import HTTPBadGateway
26 from pyramid.httpexceptions import HTTPBadGateway
27
27
28
28
29 class LdapUsernameError(Exception):
29 class LdapUsernameError(Exception):
30 pass
30 pass
31
31
32
32
33 class LdapPasswordError(Exception):
33 class LdapPasswordError(Exception):
34 pass
34 pass
35
35
36
36
37 class LdapConnectionError(Exception):
37 class LdapConnectionError(Exception):
38 pass
38 pass
39
39
40
40
41 class LdapImportError(Exception):
41 class LdapImportError(Exception):
42 pass
42 pass
43
43
44
44
45 class DefaultUserException(Exception):
45 class DefaultUserException(Exception):
46 pass
46 pass
47
47
48
48
49 class UserOwnsReposException(Exception):
49 class UserOwnsReposException(Exception):
50 pass
50 pass
51
51
52
52
53 class UserOwnsRepoGroupsException(Exception):
53 class UserOwnsRepoGroupsException(Exception):
54 pass
54 pass
55
55
56
56
57 class UserOwnsUserGroupsException(Exception):
57 class UserOwnsUserGroupsException(Exception):
58 pass
58 pass
59
59
60
60
61 class UserOwnsPullRequestsException(Exception):
61 class UserOwnsPullRequestsException(Exception):
62 pass
62 pass
63
63
64
64
65 class UserOwnsArtifactsException(Exception):
65 class UserOwnsArtifactsException(Exception):
66 pass
66 pass
67
67
68
68
69 class UserGroupAssignedException(Exception):
69 class UserGroupAssignedException(Exception):
70 pass
70 pass
71
71
72
72
73 class StatusChangeOnClosedPullRequestError(Exception):
73 class StatusChangeOnClosedPullRequestError(Exception):
74 pass
74 pass
75
75
76
76
77 class AttachedForksError(Exception):
77 class AttachedForksError(Exception):
78 pass
78 pass
79
79
80
80
81 class AttachedPullRequestsError(Exception):
81 class AttachedPullRequestsError(Exception):
82 pass
82 pass
83
83
84
84
85 class RepoGroupAssignmentError(Exception):
85 class RepoGroupAssignmentError(Exception):
86 pass
86 pass
87
87
88
88
89 class NonRelativePathError(Exception):
89 class NonRelativePathError(Exception):
90 pass
90 pass
91
91
92
92
93 class HTTPRequirementError(HTTPClientError):
93 class HTTPRequirementError(HTTPClientError):
94 title = explanation = 'Repository Requirement Missing'
94 title = explanation = 'Repository Requirement Missing'
95 reason = None
95 reason = None
96
96
97 def __init__(self, message, *args, **kwargs):
97 def __init__(self, message, *args, **kwargs):
98 self.title = self.explanation = message
98 self.title = self.explanation = message
99 super(HTTPRequirementError, self).__init__(*args, **kwargs)
99 super(HTTPRequirementError, self).__init__(*args, **kwargs)
100 self.args = (message, )
100 self.args = (message, )
101
101
102
102
103 class HTTPLockedRC(HTTPClientError):
103 class HTTPLockedRC(HTTPClientError):
104 """
104 """
105 Special Exception For locked Repos in RhodeCode, the return code can
105 Special Exception For locked Repos in RhodeCode, the return code can
106 be overwritten by _code keyword argument passed into constructors
106 be overwritten by _code keyword argument passed into constructors
107 """
107 """
108 code = 423
108 code = 423
109 title = explanation = 'Repository Locked'
109 title = explanation = 'Repository Locked'
110 reason = None
110 reason = None
111
111
112 def __init__(self, message, *args, **kwargs):
112 def __init__(self, message, *args, **kwargs):
113 from rhodecode import CONFIG
113 from rhodecode import CONFIG
114 from rhodecode.lib.utils2 import safe_int
114 from rhodecode.lib.utils2 import safe_int
115 _code = CONFIG.get('lock_ret_code')
115 _code = CONFIG.get('lock_ret_code')
116 self.code = safe_int(_code, self.code)
116 self.code = safe_int(_code, self.code)
117 self.title = self.explanation = message
117 self.title = self.explanation = message
118 super(HTTPLockedRC, self).__init__(*args, **kwargs)
118 super(HTTPLockedRC, self).__init__(*args, **kwargs)
119 self.args = (message, )
119 self.args = (message, )
120
120
121
121
122 class HTTPBranchProtected(HTTPClientError):
122 class HTTPBranchProtected(HTTPClientError):
123 """
123 """
124 Special Exception For Indicating that branch is protected in RhodeCode, the
124 Special Exception For Indicating that branch is protected in RhodeCode, the
125 return code can be overwritten by _code keyword argument passed into constructors
125 return code can be overwritten by _code keyword argument passed into constructors
126 """
126 """
127 code = 403
127 code = 403
128 title = explanation = 'Branch Protected'
128 title = explanation = 'Branch Protected'
129 reason = None
129 reason = None
130
130
131 def __init__(self, message, *args, **kwargs):
131 def __init__(self, message, *args, **kwargs):
132 self.title = self.explanation = message
132 self.title = self.explanation = message
133 super(HTTPBranchProtected, self).__init__(*args, **kwargs)
133 super(HTTPBranchProtected, self).__init__(*args, **kwargs)
134 self.args = (message, )
134 self.args = (message, )
135
135
136
136
137 class IMCCommitError(Exception):
137 class IMCCommitError(Exception):
138 pass
138 pass
139
139
140
140
141 class UserCreationError(Exception):
141 class UserCreationError(Exception):
142 pass
142 pass
143
143
144
144
145 class NotAllowedToCreateUserError(Exception):
145 class NotAllowedToCreateUserError(Exception):
146 pass
146 pass
147
147
148
148
149 class RepositoryCreationError(Exception):
149 class RepositoryCreationError(Exception):
150 pass
150 pass
151
151
152
152
153 class VCSServerUnavailable(HTTPBadGateway):
153 class VCSServerUnavailable(HTTPBadGateway):
154 """ HTTP Exception class for VCS Server errors """
154 """ HTTP Exception class for VCS Server errors """
155 code = 502
155 code = 502
156 title = 'VCS Server Error'
156 title = 'VCS Server Error'
157 causes = [
157 causes = [
158 'VCS Server is not running',
158 'VCS Server is not running',
159 'Incorrect vcs.server=host:port',
159 'Incorrect vcs.server=host:port',
160 'Incorrect vcs.server.protocol',
160 'Incorrect vcs.server.protocol',
161 ]
161 ]
162
162
163 def __init__(self, message=''):
163 def __init__(self, message=''):
164 self.explanation = 'Could not connect to VCS Server'
164 self.explanation = 'Could not connect to VCS Server'
165 if message:
165 if message:
166 self.explanation += ': ' + message
166 self.explanation += ': ' + message
167 super(VCSServerUnavailable, self).__init__()
167 super(VCSServerUnavailable, self).__init__()
168
168
169
169
170 class ArtifactMetadataDuplicate(ValueError):
170 class ArtifactMetadataDuplicate(ValueError):
171
171
172 def __init__(self, *args, **kwargs):
172 def __init__(self, *args, **kwargs):
173 self.err_section = kwargs.pop('err_section', None)
173 self.err_section = kwargs.pop('err_section', None)
174 self.err_key = kwargs.pop('err_key', None)
174 self.err_key = kwargs.pop('err_key', None)
175 super(ArtifactMetadataDuplicate, self).__init__(*args, **kwargs)
175 super(ArtifactMetadataDuplicate, self).__init__(*args, **kwargs)
176
176
177
177
178 class ArtifactMetadataBadValueType(ValueError):
178 class ArtifactMetadataBadValueType(ValueError):
179 pass
179 pass
180
181
182 class CommentVersionMismatch(ValueError):
183 pass
@@ -1,2040 +1,2041 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27 import base64
27
28
28 import os
29 import os
29 import random
30 import random
30 import hashlib
31 import hashlib
31 import StringIO
32 import StringIO
32 import textwrap
33 import textwrap
33 import urllib
34 import urllib
34 import math
35 import math
35 import logging
36 import logging
36 import re
37 import re
37 import time
38 import time
38 import string
39 import string
39 import hashlib
40 import hashlib
40 from collections import OrderedDict
41 from collections import OrderedDict
41
42
42 import pygments
43 import pygments
43 import itertools
44 import itertools
44 import fnmatch
45 import fnmatch
45 import bleach
46 import bleach
46
47
47 from pyramid import compat
48 from pyramid import compat
48 from datetime import datetime
49 from datetime import datetime
49 from functools import partial
50 from functools import partial
50 from pygments.formatters.html import HtmlFormatter
51 from pygments.formatters.html import HtmlFormatter
51 from pygments.lexers import (
52 from pygments.lexers import (
52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53
54
54 from pyramid.threadlocal import get_current_request
55 from pyramid.threadlocal import get_current_request
55
56
56 from webhelpers2.html import literal, HTML, escape
57 from webhelpers2.html import literal, HTML, escape
57 from webhelpers2.html._autolink import _auto_link_urls
58 from webhelpers2.html._autolink import _auto_link_urls
58 from webhelpers2.html.tools import (
59 from webhelpers2.html.tools import (
59 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60
61
61 from webhelpers2.text import (
62 from webhelpers2.text import (
62 chop_at, collapse, convert_accented_entities,
63 chop_at, collapse, convert_accented_entities,
63 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 from webhelpers2.date import time_ago_in_words
66 from webhelpers2.date import time_ago_in_words
66
67
67 from webhelpers2.html.tags import (
68 from webhelpers2.html.tags import (
68 _input, NotGiven, _make_safe_id_component as safeid,
69 _input, NotGiven, _make_safe_id_component as safeid,
69 form as insecure_form,
70 form as insecure_form,
70 auto_discovery_link, checkbox, end_form, file,
71 auto_discovery_link, checkbox, end_form, file,
71 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 select as raw_select, stylesheet_link, submit, text, password, textarea,
73 select as raw_select, stylesheet_link, submit, text, password, textarea,
73 ul, radio, Options)
74 ul, radio, Options)
74
75
75 from webhelpers2.number import format_byte_size
76 from webhelpers2.number import format_byte_size
76
77
77 from rhodecode.lib.action_parser import action_parser
78 from rhodecode.lib.action_parser import action_parser
78 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
79 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
79 from rhodecode.lib.ext_json import json
80 from rhodecode.lib.ext_json import json
80 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
81 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
81 from rhodecode.lib.utils2 import (
82 from rhodecode.lib.utils2 import (
82 str2bool, safe_unicode, safe_str,
83 str2bool, safe_unicode, safe_str,
83 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
84 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
84 AttributeDict, safe_int, md5, md5_safe, get_host_info)
85 AttributeDict, safe_int, md5, md5_safe, get_host_info)
85 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
88 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
88 from rhodecode.lib.index.search_utils import get_matching_line_offsets
89 from rhodecode.lib.index.search_utils import get_matching_line_offsets
89 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
90 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
90 from rhodecode.model.changeset_status import ChangesetStatusModel
91 from rhodecode.model.changeset_status import ChangesetStatusModel
91 from rhodecode.model.db import Permission, User, Repository
92 from rhodecode.model.db import Permission, User, Repository
92 from rhodecode.model.repo_group import RepoGroupModel
93 from rhodecode.model.repo_group import RepoGroupModel
93 from rhodecode.model.settings import IssueTrackerSettingsModel
94 from rhodecode.model.settings import IssueTrackerSettingsModel
94
95
95
96
96 log = logging.getLogger(__name__)
97 log = logging.getLogger(__name__)
97
98
98
99
99 DEFAULT_USER = User.DEFAULT_USER
100 DEFAULT_USER = User.DEFAULT_USER
100 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
101 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
101
102
102
103
103 def asset(path, ver=None, **kwargs):
104 def asset(path, ver=None, **kwargs):
104 """
105 """
105 Helper to generate a static asset file path for rhodecode assets
106 Helper to generate a static asset file path for rhodecode assets
106
107
107 eg. h.asset('images/image.png', ver='3923')
108 eg. h.asset('images/image.png', ver='3923')
108
109
109 :param path: path of asset
110 :param path: path of asset
110 :param ver: optional version query param to append as ?ver=
111 :param ver: optional version query param to append as ?ver=
111 """
112 """
112 request = get_current_request()
113 request = get_current_request()
113 query = {}
114 query = {}
114 query.update(kwargs)
115 query.update(kwargs)
115 if ver:
116 if ver:
116 query = {'ver': ver}
117 query = {'ver': ver}
117 return request.static_path(
118 return request.static_path(
118 'rhodecode:public/{}'.format(path), _query=query)
119 'rhodecode:public/{}'.format(path), _query=query)
119
120
120
121
121 default_html_escape_table = {
122 default_html_escape_table = {
122 ord('&'): u'&amp;',
123 ord('&'): u'&amp;',
123 ord('<'): u'&lt;',
124 ord('<'): u'&lt;',
124 ord('>'): u'&gt;',
125 ord('>'): u'&gt;',
125 ord('"'): u'&quot;',
126 ord('"'): u'&quot;',
126 ord("'"): u'&#39;',
127 ord("'"): u'&#39;',
127 }
128 }
128
129
129
130
130 def html_escape(text, html_escape_table=default_html_escape_table):
131 def html_escape(text, html_escape_table=default_html_escape_table):
131 """Produce entities within text."""
132 """Produce entities within text."""
132 return text.translate(html_escape_table)
133 return text.translate(html_escape_table)
133
134
134
135
135 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
136 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
136 """
137 """
137 Truncate string ``s`` at the first occurrence of ``sub``.
138 Truncate string ``s`` at the first occurrence of ``sub``.
138
139
139 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
140 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
140 """
141 """
141 suffix_if_chopped = suffix_if_chopped or ''
142 suffix_if_chopped = suffix_if_chopped or ''
142 pos = s.find(sub)
143 pos = s.find(sub)
143 if pos == -1:
144 if pos == -1:
144 return s
145 return s
145
146
146 if inclusive:
147 if inclusive:
147 pos += len(sub)
148 pos += len(sub)
148
149
149 chopped = s[:pos]
150 chopped = s[:pos]
150 left = s[pos:].strip()
151 left = s[pos:].strip()
151
152
152 if left and suffix_if_chopped:
153 if left and suffix_if_chopped:
153 chopped += suffix_if_chopped
154 chopped += suffix_if_chopped
154
155
155 return chopped
156 return chopped
156
157
157
158
158 def shorter(text, size=20, prefix=False):
159 def shorter(text, size=20, prefix=False):
159 postfix = '...'
160 postfix = '...'
160 if len(text) > size:
161 if len(text) > size:
161 if prefix:
162 if prefix:
162 # shorten in front
163 # shorten in front
163 return postfix + text[-(size - len(postfix)):]
164 return postfix + text[-(size - len(postfix)):]
164 else:
165 else:
165 return text[:size - len(postfix)] + postfix
166 return text[:size - len(postfix)] + postfix
166 return text
167 return text
167
168
168
169
169 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
170 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
170 """
171 """
171 Reset button
172 Reset button
172 """
173 """
173 return _input(type, name, value, id, attrs)
174 return _input(type, name, value, id, attrs)
174
175
175
176
176 def select(name, selected_values, options, id=NotGiven, **attrs):
177 def select(name, selected_values, options, id=NotGiven, **attrs):
177
178
178 if isinstance(options, (list, tuple)):
179 if isinstance(options, (list, tuple)):
179 options_iter = options
180 options_iter = options
180 # Handle old value,label lists ... where value also can be value,label lists
181 # Handle old value,label lists ... where value also can be value,label lists
181 options = Options()
182 options = Options()
182 for opt in options_iter:
183 for opt in options_iter:
183 if isinstance(opt, tuple) and len(opt) == 2:
184 if isinstance(opt, tuple) and len(opt) == 2:
184 value, label = opt
185 value, label = opt
185 elif isinstance(opt, basestring):
186 elif isinstance(opt, basestring):
186 value = label = opt
187 value = label = opt
187 else:
188 else:
188 raise ValueError('invalid select option type %r' % type(opt))
189 raise ValueError('invalid select option type %r' % type(opt))
189
190
190 if isinstance(value, (list, tuple)):
191 if isinstance(value, (list, tuple)):
191 option_group = options.add_optgroup(label)
192 option_group = options.add_optgroup(label)
192 for opt2 in value:
193 for opt2 in value:
193 if isinstance(opt2, tuple) and len(opt2) == 2:
194 if isinstance(opt2, tuple) and len(opt2) == 2:
194 group_value, group_label = opt2
195 group_value, group_label = opt2
195 elif isinstance(opt2, basestring):
196 elif isinstance(opt2, basestring):
196 group_value = group_label = opt2
197 group_value = group_label = opt2
197 else:
198 else:
198 raise ValueError('invalid select option type %r' % type(opt2))
199 raise ValueError('invalid select option type %r' % type(opt2))
199
200
200 option_group.add_option(group_label, group_value)
201 option_group.add_option(group_label, group_value)
201 else:
202 else:
202 options.add_option(label, value)
203 options.add_option(label, value)
203
204
204 return raw_select(name, selected_values, options, id=id, **attrs)
205 return raw_select(name, selected_values, options, id=id, **attrs)
205
206
206
207
207 def branding(name, length=40):
208 def branding(name, length=40):
208 return truncate(name, length, indicator="")
209 return truncate(name, length, indicator="")
209
210
210
211
211 def FID(raw_id, path):
212 def FID(raw_id, path):
212 """
213 """
213 Creates a unique ID for filenode based on it's hash of path and commit
214 Creates a unique ID for filenode based on it's hash of path and commit
214 it's safe to use in urls
215 it's safe to use in urls
215
216
216 :param raw_id:
217 :param raw_id:
217 :param path:
218 :param path:
218 """
219 """
219
220
220 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
221 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
221
222
222
223
223 class _GetError(object):
224 class _GetError(object):
224 """Get error from form_errors, and represent it as span wrapped error
225 """Get error from form_errors, and represent it as span wrapped error
225 message
226 message
226
227
227 :param field_name: field to fetch errors for
228 :param field_name: field to fetch errors for
228 :param form_errors: form errors dict
229 :param form_errors: form errors dict
229 """
230 """
230
231
231 def __call__(self, field_name, form_errors):
232 def __call__(self, field_name, form_errors):
232 tmpl = """<span class="error_msg">%s</span>"""
233 tmpl = """<span class="error_msg">%s</span>"""
233 if form_errors and field_name in form_errors:
234 if form_errors and field_name in form_errors:
234 return literal(tmpl % form_errors.get(field_name))
235 return literal(tmpl % form_errors.get(field_name))
235
236
236
237
237 get_error = _GetError()
238 get_error = _GetError()
238
239
239
240
240 class _ToolTip(object):
241 class _ToolTip(object):
241
242
242 def __call__(self, tooltip_title, trim_at=50):
243 def __call__(self, tooltip_title, trim_at=50):
243 """
244 """
244 Special function just to wrap our text into nice formatted
245 Special function just to wrap our text into nice formatted
245 autowrapped text
246 autowrapped text
246
247
247 :param tooltip_title:
248 :param tooltip_title:
248 """
249 """
249 tooltip_title = escape(tooltip_title)
250 tooltip_title = escape(tooltip_title)
250 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
251 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
251 return tooltip_title
252 return tooltip_title
252
253
253
254
254 tooltip = _ToolTip()
255 tooltip = _ToolTip()
255
256
256 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
257 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
257
258
258
259
259 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
260 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
260 limit_items=False, linkify_last_item=False, hide_last_item=False,
261 limit_items=False, linkify_last_item=False, hide_last_item=False,
261 copy_path_icon=True):
262 copy_path_icon=True):
262 if isinstance(file_path, str):
263 if isinstance(file_path, str):
263 file_path = safe_unicode(file_path)
264 file_path = safe_unicode(file_path)
264
265
265 if at_ref:
266 if at_ref:
266 route_qry = {'at': at_ref}
267 route_qry = {'at': at_ref}
267 default_landing_ref = at_ref or landing_ref_name or commit_id
268 default_landing_ref = at_ref or landing_ref_name or commit_id
268 else:
269 else:
269 route_qry = None
270 route_qry = None
270 default_landing_ref = commit_id
271 default_landing_ref = commit_id
271
272
272 # first segment is a `HOME` link to repo files root location
273 # first segment is a `HOME` link to repo files root location
273 root_name = literal(u'<i class="icon-home"></i>')
274 root_name = literal(u'<i class="icon-home"></i>')
274
275
275 url_segments = [
276 url_segments = [
276 link_to(
277 link_to(
277 root_name,
278 root_name,
278 repo_files_by_ref_url(
279 repo_files_by_ref_url(
279 repo_name,
280 repo_name,
280 repo_type,
281 repo_type,
281 f_path=None, # None here is a special case for SVN repos,
282 f_path=None, # None here is a special case for SVN repos,
282 # that won't prefix with a ref
283 # that won't prefix with a ref
283 ref_name=default_landing_ref,
284 ref_name=default_landing_ref,
284 commit_id=commit_id,
285 commit_id=commit_id,
285 query=route_qry
286 query=route_qry
286 )
287 )
287 )]
288 )]
288
289
289 path_segments = file_path.split('/')
290 path_segments = file_path.split('/')
290 last_cnt = len(path_segments) - 1
291 last_cnt = len(path_segments) - 1
291 for cnt, segment in enumerate(path_segments):
292 for cnt, segment in enumerate(path_segments):
292 if not segment:
293 if not segment:
293 continue
294 continue
294 segment_html = escape(segment)
295 segment_html = escape(segment)
295
296
296 last_item = cnt == last_cnt
297 last_item = cnt == last_cnt
297
298
298 if last_item and hide_last_item:
299 if last_item and hide_last_item:
299 # iterate over and hide last element
300 # iterate over and hide last element
300 continue
301 continue
301
302
302 if last_item and linkify_last_item is False:
303 if last_item and linkify_last_item is False:
303 # plain version
304 # plain version
304 url_segments.append(segment_html)
305 url_segments.append(segment_html)
305 else:
306 else:
306 url_segments.append(
307 url_segments.append(
307 link_to(
308 link_to(
308 segment_html,
309 segment_html,
309 repo_files_by_ref_url(
310 repo_files_by_ref_url(
310 repo_name,
311 repo_name,
311 repo_type,
312 repo_type,
312 f_path='/'.join(path_segments[:cnt + 1]),
313 f_path='/'.join(path_segments[:cnt + 1]),
313 ref_name=default_landing_ref,
314 ref_name=default_landing_ref,
314 commit_id=commit_id,
315 commit_id=commit_id,
315 query=route_qry
316 query=route_qry
316 ),
317 ),
317 ))
318 ))
318
319
319 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
320 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
320 if limit_items and len(limited_url_segments) < len(url_segments):
321 if limit_items and len(limited_url_segments) < len(url_segments):
321 url_segments = limited_url_segments
322 url_segments = limited_url_segments
322
323
323 full_path = file_path
324 full_path = file_path
324 if copy_path_icon:
325 if copy_path_icon:
325 icon = files_icon.format(escape(full_path))
326 icon = files_icon.format(escape(full_path))
326 else:
327 else:
327 icon = ''
328 icon = ''
328
329
329 if file_path == '':
330 if file_path == '':
330 return root_name
331 return root_name
331 else:
332 else:
332 return literal(' / '.join(url_segments) + icon)
333 return literal(' / '.join(url_segments) + icon)
333
334
334
335
335 def files_url_data(request):
336 def files_url_data(request):
336 matchdict = request.matchdict
337 matchdict = request.matchdict
337
338
338 if 'f_path' not in matchdict:
339 if 'f_path' not in matchdict:
339 matchdict['f_path'] = ''
340 matchdict['f_path'] = ''
340
341
341 if 'commit_id' not in matchdict:
342 if 'commit_id' not in matchdict:
342 matchdict['commit_id'] = 'tip'
343 matchdict['commit_id'] = 'tip'
343
344
344 return json.dumps(matchdict)
345 return json.dumps(matchdict)
345
346
346
347
347 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
348 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
348 _is_svn = is_svn(db_repo_type)
349 _is_svn = is_svn(db_repo_type)
349 final_f_path = f_path
350 final_f_path = f_path
350
351
351 if _is_svn:
352 if _is_svn:
352 """
353 """
353 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
354 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
354 actually commit_id followed by the ref_name. This should be done only in case
355 actually commit_id followed by the ref_name. This should be done only in case
355 This is a initial landing url, without additional paths.
356 This is a initial landing url, without additional paths.
356
357
357 like: /1000/tags/1.0.0/?at=tags/1.0.0
358 like: /1000/tags/1.0.0/?at=tags/1.0.0
358 """
359 """
359
360
360 if ref_name and ref_name != 'tip':
361 if ref_name and ref_name != 'tip':
361 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
362 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
362 # for SVN we only do this magic prefix if it's root, .eg landing revision
363 # for SVN we only do this magic prefix if it's root, .eg landing revision
363 # of files link. If we are in the tree we don't need this since we traverse the url
364 # of files link. If we are in the tree we don't need this since we traverse the url
364 # that has everything stored
365 # that has everything stored
365 if f_path in ['', '/']:
366 if f_path in ['', '/']:
366 final_f_path = '/'.join([ref_name, f_path])
367 final_f_path = '/'.join([ref_name, f_path])
367
368
368 # SVN always needs a commit_id explicitly, without a named REF
369 # SVN always needs a commit_id explicitly, without a named REF
369 default_commit_id = commit_id
370 default_commit_id = commit_id
370 else:
371 else:
371 """
372 """
372 For git and mercurial we construct a new URL using the names instead of commit_id
373 For git and mercurial we construct a new URL using the names instead of commit_id
373 like: /master/some_path?at=master
374 like: /master/some_path?at=master
374 """
375 """
375 # We currently do not support branches with slashes
376 # We currently do not support branches with slashes
376 if '/' in ref_name:
377 if '/' in ref_name:
377 default_commit_id = commit_id
378 default_commit_id = commit_id
378 else:
379 else:
379 default_commit_id = ref_name
380 default_commit_id = ref_name
380
381
381 # sometimes we pass f_path as None, to indicate explicit no prefix,
382 # sometimes we pass f_path as None, to indicate explicit no prefix,
382 # we translate it to string to not have None
383 # we translate it to string to not have None
383 final_f_path = final_f_path or ''
384 final_f_path = final_f_path or ''
384
385
385 files_url = route_path(
386 files_url = route_path(
386 'repo_files',
387 'repo_files',
387 repo_name=db_repo_name,
388 repo_name=db_repo_name,
388 commit_id=default_commit_id,
389 commit_id=default_commit_id,
389 f_path=final_f_path,
390 f_path=final_f_path,
390 _query=query
391 _query=query
391 )
392 )
392 return files_url
393 return files_url
393
394
394
395
395 def code_highlight(code, lexer, formatter, use_hl_filter=False):
396 def code_highlight(code, lexer, formatter, use_hl_filter=False):
396 """
397 """
397 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
398 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
398
399
399 If ``outfile`` is given and a valid file object (an object
400 If ``outfile`` is given and a valid file object (an object
400 with a ``write`` method), the result will be written to it, otherwise
401 with a ``write`` method), the result will be written to it, otherwise
401 it is returned as a string.
402 it is returned as a string.
402 """
403 """
403 if use_hl_filter:
404 if use_hl_filter:
404 # add HL filter
405 # add HL filter
405 from rhodecode.lib.index import search_utils
406 from rhodecode.lib.index import search_utils
406 lexer.add_filter(search_utils.ElasticSearchHLFilter())
407 lexer.add_filter(search_utils.ElasticSearchHLFilter())
407 return pygments.format(pygments.lex(code, lexer), formatter)
408 return pygments.format(pygments.lex(code, lexer), formatter)
408
409
409
410
410 class CodeHtmlFormatter(HtmlFormatter):
411 class CodeHtmlFormatter(HtmlFormatter):
411 """
412 """
412 My code Html Formatter for source codes
413 My code Html Formatter for source codes
413 """
414 """
414
415
415 def wrap(self, source, outfile):
416 def wrap(self, source, outfile):
416 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
417 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
417
418
418 def _wrap_code(self, source):
419 def _wrap_code(self, source):
419 for cnt, it in enumerate(source):
420 for cnt, it in enumerate(source):
420 i, t = it
421 i, t = it
421 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
422 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
422 yield i, t
423 yield i, t
423
424
424 def _wrap_tablelinenos(self, inner):
425 def _wrap_tablelinenos(self, inner):
425 dummyoutfile = StringIO.StringIO()
426 dummyoutfile = StringIO.StringIO()
426 lncount = 0
427 lncount = 0
427 for t, line in inner:
428 for t, line in inner:
428 if t:
429 if t:
429 lncount += 1
430 lncount += 1
430 dummyoutfile.write(line)
431 dummyoutfile.write(line)
431
432
432 fl = self.linenostart
433 fl = self.linenostart
433 mw = len(str(lncount + fl - 1))
434 mw = len(str(lncount + fl - 1))
434 sp = self.linenospecial
435 sp = self.linenospecial
435 st = self.linenostep
436 st = self.linenostep
436 la = self.lineanchors
437 la = self.lineanchors
437 aln = self.anchorlinenos
438 aln = self.anchorlinenos
438 nocls = self.noclasses
439 nocls = self.noclasses
439 if sp:
440 if sp:
440 lines = []
441 lines = []
441
442
442 for i in range(fl, fl + lncount):
443 for i in range(fl, fl + lncount):
443 if i % st == 0:
444 if i % st == 0:
444 if i % sp == 0:
445 if i % sp == 0:
445 if aln:
446 if aln:
446 lines.append('<a href="#%s%d" class="special">%*d</a>' %
447 lines.append('<a href="#%s%d" class="special">%*d</a>' %
447 (la, i, mw, i))
448 (la, i, mw, i))
448 else:
449 else:
449 lines.append('<span class="special">%*d</span>' % (mw, i))
450 lines.append('<span class="special">%*d</span>' % (mw, i))
450 else:
451 else:
451 if aln:
452 if aln:
452 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
453 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
453 else:
454 else:
454 lines.append('%*d' % (mw, i))
455 lines.append('%*d' % (mw, i))
455 else:
456 else:
456 lines.append('')
457 lines.append('')
457 ls = '\n'.join(lines)
458 ls = '\n'.join(lines)
458 else:
459 else:
459 lines = []
460 lines = []
460 for i in range(fl, fl + lncount):
461 for i in range(fl, fl + lncount):
461 if i % st == 0:
462 if i % st == 0:
462 if aln:
463 if aln:
463 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
464 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
464 else:
465 else:
465 lines.append('%*d' % (mw, i))
466 lines.append('%*d' % (mw, i))
466 else:
467 else:
467 lines.append('')
468 lines.append('')
468 ls = '\n'.join(lines)
469 ls = '\n'.join(lines)
469
470
470 # in case you wonder about the seemingly redundant <div> here: since the
471 # in case you wonder about the seemingly redundant <div> here: since the
471 # content in the other cell also is wrapped in a div, some browsers in
472 # content in the other cell also is wrapped in a div, some browsers in
472 # some configurations seem to mess up the formatting...
473 # some configurations seem to mess up the formatting...
473 if nocls:
474 if nocls:
474 yield 0, ('<table class="%stable">' % self.cssclass +
475 yield 0, ('<table class="%stable">' % self.cssclass +
475 '<tr><td><div class="linenodiv" '
476 '<tr><td><div class="linenodiv" '
476 'style="background-color: #f0f0f0; padding-right: 10px">'
477 'style="background-color: #f0f0f0; padding-right: 10px">'
477 '<pre style="line-height: 125%">' +
478 '<pre style="line-height: 125%">' +
478 ls + '</pre></div></td><td id="hlcode" class="code">')
479 ls + '</pre></div></td><td id="hlcode" class="code">')
479 else:
480 else:
480 yield 0, ('<table class="%stable">' % self.cssclass +
481 yield 0, ('<table class="%stable">' % self.cssclass +
481 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
482 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
482 ls + '</pre></div></td><td id="hlcode" class="code">')
483 ls + '</pre></div></td><td id="hlcode" class="code">')
483 yield 0, dummyoutfile.getvalue()
484 yield 0, dummyoutfile.getvalue()
484 yield 0, '</td></tr></table>'
485 yield 0, '</td></tr></table>'
485
486
486
487
487 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
488 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
488 def __init__(self, **kw):
489 def __init__(self, **kw):
489 # only show these line numbers if set
490 # only show these line numbers if set
490 self.only_lines = kw.pop('only_line_numbers', [])
491 self.only_lines = kw.pop('only_line_numbers', [])
491 self.query_terms = kw.pop('query_terms', [])
492 self.query_terms = kw.pop('query_terms', [])
492 self.max_lines = kw.pop('max_lines', 5)
493 self.max_lines = kw.pop('max_lines', 5)
493 self.line_context = kw.pop('line_context', 3)
494 self.line_context = kw.pop('line_context', 3)
494 self.url = kw.pop('url', None)
495 self.url = kw.pop('url', None)
495
496
496 super(CodeHtmlFormatter, self).__init__(**kw)
497 super(CodeHtmlFormatter, self).__init__(**kw)
497
498
498 def _wrap_code(self, source):
499 def _wrap_code(self, source):
499 for cnt, it in enumerate(source):
500 for cnt, it in enumerate(source):
500 i, t = it
501 i, t = it
501 t = '<pre>%s</pre>' % t
502 t = '<pre>%s</pre>' % t
502 yield i, t
503 yield i, t
503
504
504 def _wrap_tablelinenos(self, inner):
505 def _wrap_tablelinenos(self, inner):
505 yield 0, '<table class="code-highlight %stable">' % self.cssclass
506 yield 0, '<table class="code-highlight %stable">' % self.cssclass
506
507
507 last_shown_line_number = 0
508 last_shown_line_number = 0
508 current_line_number = 1
509 current_line_number = 1
509
510
510 for t, line in inner:
511 for t, line in inner:
511 if not t:
512 if not t:
512 yield t, line
513 yield t, line
513 continue
514 continue
514
515
515 if current_line_number in self.only_lines:
516 if current_line_number in self.only_lines:
516 if last_shown_line_number + 1 != current_line_number:
517 if last_shown_line_number + 1 != current_line_number:
517 yield 0, '<tr>'
518 yield 0, '<tr>'
518 yield 0, '<td class="line">...</td>'
519 yield 0, '<td class="line">...</td>'
519 yield 0, '<td id="hlcode" class="code"></td>'
520 yield 0, '<td id="hlcode" class="code"></td>'
520 yield 0, '</tr>'
521 yield 0, '</tr>'
521
522
522 yield 0, '<tr>'
523 yield 0, '<tr>'
523 if self.url:
524 if self.url:
524 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
525 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
525 self.url, current_line_number, current_line_number)
526 self.url, current_line_number, current_line_number)
526 else:
527 else:
527 yield 0, '<td class="line"><a href="">%i</a></td>' % (
528 yield 0, '<td class="line"><a href="">%i</a></td>' % (
528 current_line_number)
529 current_line_number)
529 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
530 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
530 yield 0, '</tr>'
531 yield 0, '</tr>'
531
532
532 last_shown_line_number = current_line_number
533 last_shown_line_number = current_line_number
533
534
534 current_line_number += 1
535 current_line_number += 1
535
536
536 yield 0, '</table>'
537 yield 0, '</table>'
537
538
538
539
539 def hsv_to_rgb(h, s, v):
540 def hsv_to_rgb(h, s, v):
540 """ Convert hsv color values to rgb """
541 """ Convert hsv color values to rgb """
541
542
542 if s == 0.0:
543 if s == 0.0:
543 return v, v, v
544 return v, v, v
544 i = int(h * 6.0) # XXX assume int() truncates!
545 i = int(h * 6.0) # XXX assume int() truncates!
545 f = (h * 6.0) - i
546 f = (h * 6.0) - i
546 p = v * (1.0 - s)
547 p = v * (1.0 - s)
547 q = v * (1.0 - s * f)
548 q = v * (1.0 - s * f)
548 t = v * (1.0 - s * (1.0 - f))
549 t = v * (1.0 - s * (1.0 - f))
549 i = i % 6
550 i = i % 6
550 if i == 0:
551 if i == 0:
551 return v, t, p
552 return v, t, p
552 if i == 1:
553 if i == 1:
553 return q, v, p
554 return q, v, p
554 if i == 2:
555 if i == 2:
555 return p, v, t
556 return p, v, t
556 if i == 3:
557 if i == 3:
557 return p, q, v
558 return p, q, v
558 if i == 4:
559 if i == 4:
559 return t, p, v
560 return t, p, v
560 if i == 5:
561 if i == 5:
561 return v, p, q
562 return v, p, q
562
563
563
564
564 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
565 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
565 """
566 """
566 Generator for getting n of evenly distributed colors using
567 Generator for getting n of evenly distributed colors using
567 hsv color and golden ratio. It always return same order of colors
568 hsv color and golden ratio. It always return same order of colors
568
569
569 :param n: number of colors to generate
570 :param n: number of colors to generate
570 :param saturation: saturation of returned colors
571 :param saturation: saturation of returned colors
571 :param lightness: lightness of returned colors
572 :param lightness: lightness of returned colors
572 :returns: RGB tuple
573 :returns: RGB tuple
573 """
574 """
574
575
575 golden_ratio = 0.618033988749895
576 golden_ratio = 0.618033988749895
576 h = 0.22717784590367374
577 h = 0.22717784590367374
577
578
578 for _ in xrange(n):
579 for _ in xrange(n):
579 h += golden_ratio
580 h += golden_ratio
580 h %= 1
581 h %= 1
581 HSV_tuple = [h, saturation, lightness]
582 HSV_tuple = [h, saturation, lightness]
582 RGB_tuple = hsv_to_rgb(*HSV_tuple)
583 RGB_tuple = hsv_to_rgb(*HSV_tuple)
583 yield map(lambda x: str(int(x * 256)), RGB_tuple)
584 yield map(lambda x: str(int(x * 256)), RGB_tuple)
584
585
585
586
586 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
587 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
587 """
588 """
588 Returns a function which when called with an argument returns a unique
589 Returns a function which when called with an argument returns a unique
589 color for that argument, eg.
590 color for that argument, eg.
590
591
591 :param n: number of colors to generate
592 :param n: number of colors to generate
592 :param saturation: saturation of returned colors
593 :param saturation: saturation of returned colors
593 :param lightness: lightness of returned colors
594 :param lightness: lightness of returned colors
594 :returns: css RGB string
595 :returns: css RGB string
595
596
596 >>> color_hash = color_hasher()
597 >>> color_hash = color_hasher()
597 >>> color_hash('hello')
598 >>> color_hash('hello')
598 'rgb(34, 12, 59)'
599 'rgb(34, 12, 59)'
599 >>> color_hash('hello')
600 >>> color_hash('hello')
600 'rgb(34, 12, 59)'
601 'rgb(34, 12, 59)'
601 >>> color_hash('other')
602 >>> color_hash('other')
602 'rgb(90, 224, 159)'
603 'rgb(90, 224, 159)'
603 """
604 """
604
605
605 color_dict = {}
606 color_dict = {}
606 cgenerator = unique_color_generator(
607 cgenerator = unique_color_generator(
607 saturation=saturation, lightness=lightness)
608 saturation=saturation, lightness=lightness)
608
609
609 def get_color_string(thing):
610 def get_color_string(thing):
610 if thing in color_dict:
611 if thing in color_dict:
611 col = color_dict[thing]
612 col = color_dict[thing]
612 else:
613 else:
613 col = color_dict[thing] = cgenerator.next()
614 col = color_dict[thing] = cgenerator.next()
614 return "rgb(%s)" % (', '.join(col))
615 return "rgb(%s)" % (', '.join(col))
615
616
616 return get_color_string
617 return get_color_string
617
618
618
619
619 def get_lexer_safe(mimetype=None, filepath=None):
620 def get_lexer_safe(mimetype=None, filepath=None):
620 """
621 """
621 Tries to return a relevant pygments lexer using mimetype/filepath name,
622 Tries to return a relevant pygments lexer using mimetype/filepath name,
622 defaulting to plain text if none could be found
623 defaulting to plain text if none could be found
623 """
624 """
624 lexer = None
625 lexer = None
625 try:
626 try:
626 if mimetype:
627 if mimetype:
627 lexer = get_lexer_for_mimetype(mimetype)
628 lexer = get_lexer_for_mimetype(mimetype)
628 if not lexer:
629 if not lexer:
629 lexer = get_lexer_for_filename(filepath)
630 lexer = get_lexer_for_filename(filepath)
630 except pygments.util.ClassNotFound:
631 except pygments.util.ClassNotFound:
631 pass
632 pass
632
633
633 if not lexer:
634 if not lexer:
634 lexer = get_lexer_by_name('text')
635 lexer = get_lexer_by_name('text')
635
636
636 return lexer
637 return lexer
637
638
638
639
639 def get_lexer_for_filenode(filenode):
640 def get_lexer_for_filenode(filenode):
640 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
641 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
641 return lexer
642 return lexer
642
643
643
644
644 def pygmentize(filenode, **kwargs):
645 def pygmentize(filenode, **kwargs):
645 """
646 """
646 pygmentize function using pygments
647 pygmentize function using pygments
647
648
648 :param filenode:
649 :param filenode:
649 """
650 """
650 lexer = get_lexer_for_filenode(filenode)
651 lexer = get_lexer_for_filenode(filenode)
651 return literal(code_highlight(filenode.content, lexer,
652 return literal(code_highlight(filenode.content, lexer,
652 CodeHtmlFormatter(**kwargs)))
653 CodeHtmlFormatter(**kwargs)))
653
654
654
655
655 def is_following_repo(repo_name, user_id):
656 def is_following_repo(repo_name, user_id):
656 from rhodecode.model.scm import ScmModel
657 from rhodecode.model.scm import ScmModel
657 return ScmModel().is_following_repo(repo_name, user_id)
658 return ScmModel().is_following_repo(repo_name, user_id)
658
659
659
660
660 class _Message(object):
661 class _Message(object):
661 """A message returned by ``Flash.pop_messages()``.
662 """A message returned by ``Flash.pop_messages()``.
662
663
663 Converting the message to a string returns the message text. Instances
664 Converting the message to a string returns the message text. Instances
664 also have the following attributes:
665 also have the following attributes:
665
666
666 * ``message``: the message text.
667 * ``message``: the message text.
667 * ``category``: the category specified when the message was created.
668 * ``category``: the category specified when the message was created.
668 """
669 """
669
670
670 def __init__(self, category, message, sub_data=None):
671 def __init__(self, category, message, sub_data=None):
671 self.category = category
672 self.category = category
672 self.message = message
673 self.message = message
673 self.sub_data = sub_data or {}
674 self.sub_data = sub_data or {}
674
675
675 def __str__(self):
676 def __str__(self):
676 return self.message
677 return self.message
677
678
678 __unicode__ = __str__
679 __unicode__ = __str__
679
680
680 def __html__(self):
681 def __html__(self):
681 return escape(safe_unicode(self.message))
682 return escape(safe_unicode(self.message))
682
683
683
684
684 class Flash(object):
685 class Flash(object):
685 # List of allowed categories. If None, allow any category.
686 # List of allowed categories. If None, allow any category.
686 categories = ["warning", "notice", "error", "success"]
687 categories = ["warning", "notice", "error", "success"]
687
688
688 # Default category if none is specified.
689 # Default category if none is specified.
689 default_category = "notice"
690 default_category = "notice"
690
691
691 def __init__(self, session_key="flash", categories=None,
692 def __init__(self, session_key="flash", categories=None,
692 default_category=None):
693 default_category=None):
693 """
694 """
694 Instantiate a ``Flash`` object.
695 Instantiate a ``Flash`` object.
695
696
696 ``session_key`` is the key to save the messages under in the user's
697 ``session_key`` is the key to save the messages under in the user's
697 session.
698 session.
698
699
699 ``categories`` is an optional list which overrides the default list
700 ``categories`` is an optional list which overrides the default list
700 of categories.
701 of categories.
701
702
702 ``default_category`` overrides the default category used for messages
703 ``default_category`` overrides the default category used for messages
703 when none is specified.
704 when none is specified.
704 """
705 """
705 self.session_key = session_key
706 self.session_key = session_key
706 if categories is not None:
707 if categories is not None:
707 self.categories = categories
708 self.categories = categories
708 if default_category is not None:
709 if default_category is not None:
709 self.default_category = default_category
710 self.default_category = default_category
710 if self.categories and self.default_category not in self.categories:
711 if self.categories and self.default_category not in self.categories:
711 raise ValueError(
712 raise ValueError(
712 "unrecognized default category %r" % (self.default_category,))
713 "unrecognized default category %r" % (self.default_category,))
713
714
714 def pop_messages(self, session=None, request=None):
715 def pop_messages(self, session=None, request=None):
715 """
716 """
716 Return all accumulated messages and delete them from the session.
717 Return all accumulated messages and delete them from the session.
717
718
718 The return value is a list of ``Message`` objects.
719 The return value is a list of ``Message`` objects.
719 """
720 """
720 messages = []
721 messages = []
721
722
722 if not session:
723 if not session:
723 if not request:
724 if not request:
724 request = get_current_request()
725 request = get_current_request()
725 session = request.session
726 session = request.session
726
727
727 # Pop the 'old' pylons flash messages. They are tuples of the form
728 # Pop the 'old' pylons flash messages. They are tuples of the form
728 # (category, message)
729 # (category, message)
729 for cat, msg in session.pop(self.session_key, []):
730 for cat, msg in session.pop(self.session_key, []):
730 messages.append(_Message(cat, msg))
731 messages.append(_Message(cat, msg))
731
732
732 # Pop the 'new' pyramid flash messages for each category as list
733 # Pop the 'new' pyramid flash messages for each category as list
733 # of strings.
734 # of strings.
734 for cat in self.categories:
735 for cat in self.categories:
735 for msg in session.pop_flash(queue=cat):
736 for msg in session.pop_flash(queue=cat):
736 sub_data = {}
737 sub_data = {}
737 if hasattr(msg, 'rsplit'):
738 if hasattr(msg, 'rsplit'):
738 flash_data = msg.rsplit('|DELIM|', 1)
739 flash_data = msg.rsplit('|DELIM|', 1)
739 org_message = flash_data[0]
740 org_message = flash_data[0]
740 if len(flash_data) > 1:
741 if len(flash_data) > 1:
741 sub_data = json.loads(flash_data[1])
742 sub_data = json.loads(flash_data[1])
742 else:
743 else:
743 org_message = msg
744 org_message = msg
744
745
745 messages.append(_Message(cat, org_message, sub_data=sub_data))
746 messages.append(_Message(cat, org_message, sub_data=sub_data))
746
747
747 # Map messages from the default queue to the 'notice' category.
748 # Map messages from the default queue to the 'notice' category.
748 for msg in session.pop_flash():
749 for msg in session.pop_flash():
749 messages.append(_Message('notice', msg))
750 messages.append(_Message('notice', msg))
750
751
751 session.save()
752 session.save()
752 return messages
753 return messages
753
754
754 def json_alerts(self, session=None, request=None):
755 def json_alerts(self, session=None, request=None):
755 payloads = []
756 payloads = []
756 messages = flash.pop_messages(session=session, request=request) or []
757 messages = flash.pop_messages(session=session, request=request) or []
757 for message in messages:
758 for message in messages:
758 payloads.append({
759 payloads.append({
759 'message': {
760 'message': {
760 'message': u'{}'.format(message.message),
761 'message': u'{}'.format(message.message),
761 'level': message.category,
762 'level': message.category,
762 'force': True,
763 'force': True,
763 'subdata': message.sub_data
764 'subdata': message.sub_data
764 }
765 }
765 })
766 })
766 return json.dumps(payloads)
767 return json.dumps(payloads)
767
768
768 def __call__(self, message, category=None, ignore_duplicate=True,
769 def __call__(self, message, category=None, ignore_duplicate=True,
769 session=None, request=None):
770 session=None, request=None):
770
771
771 if not session:
772 if not session:
772 if not request:
773 if not request:
773 request = get_current_request()
774 request = get_current_request()
774 session = request.session
775 session = request.session
775
776
776 session.flash(
777 session.flash(
777 message, queue=category, allow_duplicate=not ignore_duplicate)
778 message, queue=category, allow_duplicate=not ignore_duplicate)
778
779
779
780
780 flash = Flash()
781 flash = Flash()
781
782
782 #==============================================================================
783 #==============================================================================
783 # SCM FILTERS available via h.
784 # SCM FILTERS available via h.
784 #==============================================================================
785 #==============================================================================
785 from rhodecode.lib.vcs.utils import author_name, author_email
786 from rhodecode.lib.vcs.utils import author_name, author_email
786 from rhodecode.lib.utils2 import age, age_from_seconds
787 from rhodecode.lib.utils2 import age, age_from_seconds
787 from rhodecode.model.db import User, ChangesetStatus
788 from rhodecode.model.db import User, ChangesetStatus
788
789
789
790
790 email = author_email
791 email = author_email
791
792
792
793
793 def capitalize(raw_text):
794 def capitalize(raw_text):
794 return raw_text.capitalize()
795 return raw_text.capitalize()
795
796
796
797
797 def short_id(long_id):
798 def short_id(long_id):
798 return long_id[:12]
799 return long_id[:12]
799
800
800
801
801 def hide_credentials(url):
802 def hide_credentials(url):
802 from rhodecode.lib.utils2 import credentials_filter
803 from rhodecode.lib.utils2 import credentials_filter
803 return credentials_filter(url)
804 return credentials_filter(url)
804
805
805
806
806 import pytz
807 import pytz
807 import tzlocal
808 import tzlocal
808 local_timezone = tzlocal.get_localzone()
809 local_timezone = tzlocal.get_localzone()
809
810
810
811
811 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
812 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
812 title = value or format_date(datetime_iso)
813 title = value or format_date(datetime_iso)
813 tzinfo = '+00:00'
814 tzinfo = '+00:00'
814
815
815 # detect if we have a timezone info, otherwise, add it
816 # detect if we have a timezone info, otherwise, add it
816 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
817 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
817 force_timezone = os.environ.get('RC_TIMEZONE', '')
818 force_timezone = os.environ.get('RC_TIMEZONE', '')
818 if force_timezone:
819 if force_timezone:
819 force_timezone = pytz.timezone(force_timezone)
820 force_timezone = pytz.timezone(force_timezone)
820 timezone = force_timezone or local_timezone
821 timezone = force_timezone or local_timezone
821 offset = timezone.localize(datetime_iso).strftime('%z')
822 offset = timezone.localize(datetime_iso).strftime('%z')
822 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
823 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
823
824
824 return literal(
825 return literal(
825 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
826 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
826 cls='tooltip' if tooltip else '',
827 cls='tooltip' if tooltip else '',
827 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
828 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
828 title=title, dt=datetime_iso, tzinfo=tzinfo
829 title=title, dt=datetime_iso, tzinfo=tzinfo
829 ))
830 ))
830
831
831
832
832 def _shorten_commit_id(commit_id, commit_len=None):
833 def _shorten_commit_id(commit_id, commit_len=None):
833 if commit_len is None:
834 if commit_len is None:
834 request = get_current_request()
835 request = get_current_request()
835 commit_len = request.call_context.visual.show_sha_length
836 commit_len = request.call_context.visual.show_sha_length
836 return commit_id[:commit_len]
837 return commit_id[:commit_len]
837
838
838
839
839 def show_id(commit, show_idx=None, commit_len=None):
840 def show_id(commit, show_idx=None, commit_len=None):
840 """
841 """
841 Configurable function that shows ID
842 Configurable function that shows ID
842 by default it's r123:fffeeefffeee
843 by default it's r123:fffeeefffeee
843
844
844 :param commit: commit instance
845 :param commit: commit instance
845 """
846 """
846 if show_idx is None:
847 if show_idx is None:
847 request = get_current_request()
848 request = get_current_request()
848 show_idx = request.call_context.visual.show_revision_number
849 show_idx = request.call_context.visual.show_revision_number
849
850
850 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
851 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
851 if show_idx:
852 if show_idx:
852 return 'r%s:%s' % (commit.idx, raw_id)
853 return 'r%s:%s' % (commit.idx, raw_id)
853 else:
854 else:
854 return '%s' % (raw_id, )
855 return '%s' % (raw_id, )
855
856
856
857
857 def format_date(date):
858 def format_date(date):
858 """
859 """
859 use a standardized formatting for dates used in RhodeCode
860 use a standardized formatting for dates used in RhodeCode
860
861
861 :param date: date/datetime object
862 :param date: date/datetime object
862 :return: formatted date
863 :return: formatted date
863 """
864 """
864
865
865 if date:
866 if date:
866 _fmt = "%a, %d %b %Y %H:%M:%S"
867 _fmt = "%a, %d %b %Y %H:%M:%S"
867 return safe_unicode(date.strftime(_fmt))
868 return safe_unicode(date.strftime(_fmt))
868
869
869 return u""
870 return u""
870
871
871
872
872 class _RepoChecker(object):
873 class _RepoChecker(object):
873
874
874 def __init__(self, backend_alias):
875 def __init__(self, backend_alias):
875 self._backend_alias = backend_alias
876 self._backend_alias = backend_alias
876
877
877 def __call__(self, repository):
878 def __call__(self, repository):
878 if hasattr(repository, 'alias'):
879 if hasattr(repository, 'alias'):
879 _type = repository.alias
880 _type = repository.alias
880 elif hasattr(repository, 'repo_type'):
881 elif hasattr(repository, 'repo_type'):
881 _type = repository.repo_type
882 _type = repository.repo_type
882 else:
883 else:
883 _type = repository
884 _type = repository
884 return _type == self._backend_alias
885 return _type == self._backend_alias
885
886
886
887
887 is_git = _RepoChecker('git')
888 is_git = _RepoChecker('git')
888 is_hg = _RepoChecker('hg')
889 is_hg = _RepoChecker('hg')
889 is_svn = _RepoChecker('svn')
890 is_svn = _RepoChecker('svn')
890
891
891
892
892 def get_repo_type_by_name(repo_name):
893 def get_repo_type_by_name(repo_name):
893 repo = Repository.get_by_repo_name(repo_name)
894 repo = Repository.get_by_repo_name(repo_name)
894 if repo:
895 if repo:
895 return repo.repo_type
896 return repo.repo_type
896
897
897
898
898 def is_svn_without_proxy(repository):
899 def is_svn_without_proxy(repository):
899 if is_svn(repository):
900 if is_svn(repository):
900 from rhodecode.model.settings import VcsSettingsModel
901 from rhodecode.model.settings import VcsSettingsModel
901 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
902 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
902 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
903 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
903 return False
904 return False
904
905
905
906
906 def discover_user(author):
907 def discover_user(author):
907 """
908 """
908 Tries to discover RhodeCode User based on the author string. Author string
909 Tries to discover RhodeCode User based on the author string. Author string
909 is typically `FirstName LastName <email@address.com>`
910 is typically `FirstName LastName <email@address.com>`
910 """
911 """
911
912
912 # if author is already an instance use it for extraction
913 # if author is already an instance use it for extraction
913 if isinstance(author, User):
914 if isinstance(author, User):
914 return author
915 return author
915
916
916 # Valid email in the attribute passed, see if they're in the system
917 # Valid email in the attribute passed, see if they're in the system
917 _email = author_email(author)
918 _email = author_email(author)
918 if _email != '':
919 if _email != '':
919 user = User.get_by_email(_email, case_insensitive=True, cache=True)
920 user = User.get_by_email(_email, case_insensitive=True, cache=True)
920 if user is not None:
921 if user is not None:
921 return user
922 return user
922
923
923 # Maybe it's a username, we try to extract it and fetch by username ?
924 # Maybe it's a username, we try to extract it and fetch by username ?
924 _author = author_name(author)
925 _author = author_name(author)
925 user = User.get_by_username(_author, case_insensitive=True, cache=True)
926 user = User.get_by_username(_author, case_insensitive=True, cache=True)
926 if user is not None:
927 if user is not None:
927 return user
928 return user
928
929
929 return None
930 return None
930
931
931
932
932 def email_or_none(author):
933 def email_or_none(author):
933 # extract email from the commit string
934 # extract email from the commit string
934 _email = author_email(author)
935 _email = author_email(author)
935
936
936 # If we have an email, use it, otherwise
937 # If we have an email, use it, otherwise
937 # see if it contains a username we can get an email from
938 # see if it contains a username we can get an email from
938 if _email != '':
939 if _email != '':
939 return _email
940 return _email
940 else:
941 else:
941 user = User.get_by_username(
942 user = User.get_by_username(
942 author_name(author), case_insensitive=True, cache=True)
943 author_name(author), case_insensitive=True, cache=True)
943
944
944 if user is not None:
945 if user is not None:
945 return user.email
946 return user.email
946
947
947 # No valid email, not a valid user in the system, none!
948 # No valid email, not a valid user in the system, none!
948 return None
949 return None
949
950
950
951
951 def link_to_user(author, length=0, **kwargs):
952 def link_to_user(author, length=0, **kwargs):
952 user = discover_user(author)
953 user = discover_user(author)
953 # user can be None, but if we have it already it means we can re-use it
954 # user can be None, but if we have it already it means we can re-use it
954 # in the person() function, so we save 1 intensive-query
955 # in the person() function, so we save 1 intensive-query
955 if user:
956 if user:
956 author = user
957 author = user
957
958
958 display_person = person(author, 'username_or_name_or_email')
959 display_person = person(author, 'username_or_name_or_email')
959 if length:
960 if length:
960 display_person = shorter(display_person, length)
961 display_person = shorter(display_person, length)
961
962
962 if user:
963 if user:
963 return link_to(
964 return link_to(
964 escape(display_person),
965 escape(display_person),
965 route_path('user_profile', username=user.username),
966 route_path('user_profile', username=user.username),
966 **kwargs)
967 **kwargs)
967 else:
968 else:
968 return escape(display_person)
969 return escape(display_person)
969
970
970
971
971 def link_to_group(users_group_name, **kwargs):
972 def link_to_group(users_group_name, **kwargs):
972 return link_to(
973 return link_to(
973 escape(users_group_name),
974 escape(users_group_name),
974 route_path('user_group_profile', user_group_name=users_group_name),
975 route_path('user_group_profile', user_group_name=users_group_name),
975 **kwargs)
976 **kwargs)
976
977
977
978
978 def person(author, show_attr="username_and_name"):
979 def person(author, show_attr="username_and_name"):
979 user = discover_user(author)
980 user = discover_user(author)
980 if user:
981 if user:
981 return getattr(user, show_attr)
982 return getattr(user, show_attr)
982 else:
983 else:
983 _author = author_name(author)
984 _author = author_name(author)
984 _email = email(author)
985 _email = email(author)
985 return _author or _email
986 return _author or _email
986
987
987
988
988 def author_string(email):
989 def author_string(email):
989 if email:
990 if email:
990 user = User.get_by_email(email, case_insensitive=True, cache=True)
991 user = User.get_by_email(email, case_insensitive=True, cache=True)
991 if user:
992 if user:
992 if user.first_name or user.last_name:
993 if user.first_name or user.last_name:
993 return '%s %s &lt;%s&gt;' % (
994 return '%s %s &lt;%s&gt;' % (
994 user.first_name, user.last_name, email)
995 user.first_name, user.last_name, email)
995 else:
996 else:
996 return email
997 return email
997 else:
998 else:
998 return email
999 return email
999 else:
1000 else:
1000 return None
1001 return None
1001
1002
1002
1003
1003 def person_by_id(id_, show_attr="username_and_name"):
1004 def person_by_id(id_, show_attr="username_and_name"):
1004 # attr to return from fetched user
1005 # attr to return from fetched user
1005 person_getter = lambda usr: getattr(usr, show_attr)
1006 person_getter = lambda usr: getattr(usr, show_attr)
1006
1007
1007 #maybe it's an ID ?
1008 #maybe it's an ID ?
1008 if str(id_).isdigit() or isinstance(id_, int):
1009 if str(id_).isdigit() or isinstance(id_, int):
1009 id_ = int(id_)
1010 id_ = int(id_)
1010 user = User.get(id_)
1011 user = User.get(id_)
1011 if user is not None:
1012 if user is not None:
1012 return person_getter(user)
1013 return person_getter(user)
1013 return id_
1014 return id_
1014
1015
1015
1016
1016 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1017 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1017 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1018 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1018 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1019 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1019
1020
1020
1021
1021 tags_paterns = OrderedDict((
1022 tags_paterns = OrderedDict((
1022 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1023 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1023 '<div class="metatag" tag="lang">\\2</div>')),
1024 '<div class="metatag" tag="lang">\\2</div>')),
1024
1025
1025 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1026 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1026 '<div class="metatag" tag="see">see: \\1 </div>')),
1027 '<div class="metatag" tag="see">see: \\1 </div>')),
1027
1028
1028 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1029 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1029 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1030 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1030
1031
1031 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1032 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1032 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1033 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1033
1034
1034 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1035 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1035 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1036 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1036
1037
1037 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1038 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1038 '<div class="metatag" tag="state \\1">\\1</div>')),
1039 '<div class="metatag" tag="state \\1">\\1</div>')),
1039
1040
1040 # label in grey
1041 # label in grey
1041 ('label', (re.compile(r'\[([a-z]+)\]'),
1042 ('label', (re.compile(r'\[([a-z]+)\]'),
1042 '<div class="metatag" tag="label">\\1</div>')),
1043 '<div class="metatag" tag="label">\\1</div>')),
1043
1044
1044 # generic catch all in grey
1045 # generic catch all in grey
1045 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1046 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1046 '<div class="metatag" tag="generic">\\1</div>')),
1047 '<div class="metatag" tag="generic">\\1</div>')),
1047 ))
1048 ))
1048
1049
1049
1050
1050 def extract_metatags(value):
1051 def extract_metatags(value):
1051 """
1052 """
1052 Extract supported meta-tags from given text value
1053 Extract supported meta-tags from given text value
1053 """
1054 """
1054 tags = []
1055 tags = []
1055 if not value:
1056 if not value:
1056 return tags, ''
1057 return tags, ''
1057
1058
1058 for key, val in tags_paterns.items():
1059 for key, val in tags_paterns.items():
1059 pat, replace_html = val
1060 pat, replace_html = val
1060 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1061 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1061 value = pat.sub('', value)
1062 value = pat.sub('', value)
1062
1063
1063 return tags, value
1064 return tags, value
1064
1065
1065
1066
1066 def style_metatag(tag_type, value):
1067 def style_metatag(tag_type, value):
1067 """
1068 """
1068 converts tags from value into html equivalent
1069 converts tags from value into html equivalent
1069 """
1070 """
1070 if not value:
1071 if not value:
1071 return ''
1072 return ''
1072
1073
1073 html_value = value
1074 html_value = value
1074 tag_data = tags_paterns.get(tag_type)
1075 tag_data = tags_paterns.get(tag_type)
1075 if tag_data:
1076 if tag_data:
1076 pat, replace_html = tag_data
1077 pat, replace_html = tag_data
1077 # convert to plain `unicode` instead of a markup tag to be used in
1078 # convert to plain `unicode` instead of a markup tag to be used in
1078 # regex expressions. safe_unicode doesn't work here
1079 # regex expressions. safe_unicode doesn't work here
1079 html_value = pat.sub(replace_html, unicode(value))
1080 html_value = pat.sub(replace_html, unicode(value))
1080
1081
1081 return html_value
1082 return html_value
1082
1083
1083
1084
1084 def bool2icon(value, show_at_false=True):
1085 def bool2icon(value, show_at_false=True):
1085 """
1086 """
1086 Returns boolean value of a given value, represented as html element with
1087 Returns boolean value of a given value, represented as html element with
1087 classes that will represent icons
1088 classes that will represent icons
1088
1089
1089 :param value: given value to convert to html node
1090 :param value: given value to convert to html node
1090 """
1091 """
1091
1092
1092 if value: # does bool conversion
1093 if value: # does bool conversion
1093 return HTML.tag('i', class_="icon-true", title='True')
1094 return HTML.tag('i', class_="icon-true", title='True')
1094 else: # not true as bool
1095 else: # not true as bool
1095 if show_at_false:
1096 if show_at_false:
1096 return HTML.tag('i', class_="icon-false", title='False')
1097 return HTML.tag('i', class_="icon-false", title='False')
1097 return HTML.tag('i')
1098 return HTML.tag('i')
1098
1099
1099 #==============================================================================
1100 #==============================================================================
1100 # PERMS
1101 # PERMS
1101 #==============================================================================
1102 #==============================================================================
1102 from rhodecode.lib.auth import (
1103 from rhodecode.lib.auth import (
1103 HasPermissionAny, HasPermissionAll,
1104 HasPermissionAny, HasPermissionAll,
1104 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1105 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1105 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1106 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1106 csrf_token_key, AuthUser)
1107 csrf_token_key, AuthUser)
1107
1108
1108
1109
1109 #==============================================================================
1110 #==============================================================================
1110 # GRAVATAR URL
1111 # GRAVATAR URL
1111 #==============================================================================
1112 #==============================================================================
1112 class InitialsGravatar(object):
1113 class InitialsGravatar(object):
1113 def __init__(self, email_address, first_name, last_name, size=30,
1114 def __init__(self, email_address, first_name, last_name, size=30,
1114 background=None, text_color='#fff'):
1115 background=None, text_color='#fff'):
1115 self.size = size
1116 self.size = size
1116 self.first_name = first_name
1117 self.first_name = first_name
1117 self.last_name = last_name
1118 self.last_name = last_name
1118 self.email_address = email_address
1119 self.email_address = email_address
1119 self.background = background or self.str2color(email_address)
1120 self.background = background or self.str2color(email_address)
1120 self.text_color = text_color
1121 self.text_color = text_color
1121
1122
1122 def get_color_bank(self):
1123 def get_color_bank(self):
1123 """
1124 """
1124 returns a predefined list of colors that gravatars can use.
1125 returns a predefined list of colors that gravatars can use.
1125 Those are randomized distinct colors that guarantee readability and
1126 Those are randomized distinct colors that guarantee readability and
1126 uniqueness.
1127 uniqueness.
1127
1128
1128 generated with: http://phrogz.net/css/distinct-colors.html
1129 generated with: http://phrogz.net/css/distinct-colors.html
1129 """
1130 """
1130 return [
1131 return [
1131 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1132 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1132 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1133 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1133 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1134 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1134 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1135 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1135 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1136 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1136 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1137 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1137 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1138 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1138 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1139 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1139 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1140 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1140 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1141 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1141 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1142 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1142 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1143 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1143 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1144 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1144 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1145 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1145 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1146 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1146 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1147 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1147 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1148 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1148 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1149 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1149 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1150 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1150 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1151 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1151 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1152 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1152 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1153 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1153 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1154 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1154 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1155 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1155 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1156 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1156 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1157 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1157 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1158 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1158 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1159 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1159 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1160 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1160 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1161 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1161 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1162 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1162 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1163 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1163 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1164 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1164 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1165 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1165 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1166 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1166 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1167 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1167 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1168 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1168 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1169 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1169 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1170 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1170 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1171 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1171 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1172 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1172 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1173 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1173 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1174 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1174 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1175 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1175 '#4f8c46', '#368dd9', '#5c0073'
1176 '#4f8c46', '#368dd9', '#5c0073'
1176 ]
1177 ]
1177
1178
1178 def rgb_to_hex_color(self, rgb_tuple):
1179 def rgb_to_hex_color(self, rgb_tuple):
1179 """
1180 """
1180 Converts an rgb_tuple passed to an hex color.
1181 Converts an rgb_tuple passed to an hex color.
1181
1182
1182 :param rgb_tuple: tuple with 3 ints represents rgb color space
1183 :param rgb_tuple: tuple with 3 ints represents rgb color space
1183 """
1184 """
1184 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1185 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1185
1186
1186 def email_to_int_list(self, email_str):
1187 def email_to_int_list(self, email_str):
1187 """
1188 """
1188 Get every byte of the hex digest value of email and turn it to integer.
1189 Get every byte of the hex digest value of email and turn it to integer.
1189 It's going to be always between 0-255
1190 It's going to be always between 0-255
1190 """
1191 """
1191 digest = md5_safe(email_str.lower())
1192 digest = md5_safe(email_str.lower())
1192 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1193 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1193
1194
1194 def pick_color_bank_index(self, email_str, color_bank):
1195 def pick_color_bank_index(self, email_str, color_bank):
1195 return self.email_to_int_list(email_str)[0] % len(color_bank)
1196 return self.email_to_int_list(email_str)[0] % len(color_bank)
1196
1197
1197 def str2color(self, email_str):
1198 def str2color(self, email_str):
1198 """
1199 """
1199 Tries to map in a stable algorithm an email to color
1200 Tries to map in a stable algorithm an email to color
1200
1201
1201 :param email_str:
1202 :param email_str:
1202 """
1203 """
1203 color_bank = self.get_color_bank()
1204 color_bank = self.get_color_bank()
1204 # pick position (module it's length so we always find it in the
1205 # pick position (module it's length so we always find it in the
1205 # bank even if it's smaller than 256 values
1206 # bank even if it's smaller than 256 values
1206 pos = self.pick_color_bank_index(email_str, color_bank)
1207 pos = self.pick_color_bank_index(email_str, color_bank)
1207 return color_bank[pos]
1208 return color_bank[pos]
1208
1209
1209 def normalize_email(self, email_address):
1210 def normalize_email(self, email_address):
1210 import unicodedata
1211 import unicodedata
1211 # default host used to fill in the fake/missing email
1212 # default host used to fill in the fake/missing email
1212 default_host = u'localhost'
1213 default_host = u'localhost'
1213
1214
1214 if not email_address:
1215 if not email_address:
1215 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1216 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1216
1217
1217 email_address = safe_unicode(email_address)
1218 email_address = safe_unicode(email_address)
1218
1219
1219 if u'@' not in email_address:
1220 if u'@' not in email_address:
1220 email_address = u'%s@%s' % (email_address, default_host)
1221 email_address = u'%s@%s' % (email_address, default_host)
1221
1222
1222 if email_address.endswith(u'@'):
1223 if email_address.endswith(u'@'):
1223 email_address = u'%s%s' % (email_address, default_host)
1224 email_address = u'%s%s' % (email_address, default_host)
1224
1225
1225 email_address = unicodedata.normalize('NFKD', email_address)\
1226 email_address = unicodedata.normalize('NFKD', email_address)\
1226 .encode('ascii', 'ignore')
1227 .encode('ascii', 'ignore')
1227 return email_address
1228 return email_address
1228
1229
1229 def get_initials(self):
1230 def get_initials(self):
1230 """
1231 """
1231 Returns 2 letter initials calculated based on the input.
1232 Returns 2 letter initials calculated based on the input.
1232 The algorithm picks first given email address, and takes first letter
1233 The algorithm picks first given email address, and takes first letter
1233 of part before @, and then the first letter of server name. In case
1234 of part before @, and then the first letter of server name. In case
1234 the part before @ is in a format of `somestring.somestring2` it replaces
1235 the part before @ is in a format of `somestring.somestring2` it replaces
1235 the server letter with first letter of somestring2
1236 the server letter with first letter of somestring2
1236
1237
1237 In case function was initialized with both first and lastname, this
1238 In case function was initialized with both first and lastname, this
1238 overrides the extraction from email by first letter of the first and
1239 overrides the extraction from email by first letter of the first and
1239 last name. We add special logic to that functionality, In case Full name
1240 last name. We add special logic to that functionality, In case Full name
1240 is compound, like Guido Von Rossum, we use last part of the last name
1241 is compound, like Guido Von Rossum, we use last part of the last name
1241 (Von Rossum) picking `R`.
1242 (Von Rossum) picking `R`.
1242
1243
1243 Function also normalizes the non-ascii characters to they ascii
1244 Function also normalizes the non-ascii characters to they ascii
1244 representation, eg Δ„ => A
1245 representation, eg Δ„ => A
1245 """
1246 """
1246 import unicodedata
1247 import unicodedata
1247 # replace non-ascii to ascii
1248 # replace non-ascii to ascii
1248 first_name = unicodedata.normalize(
1249 first_name = unicodedata.normalize(
1249 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1250 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1250 last_name = unicodedata.normalize(
1251 last_name = unicodedata.normalize(
1251 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1252 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1252
1253
1253 # do NFKD encoding, and also make sure email has proper format
1254 # do NFKD encoding, and also make sure email has proper format
1254 email_address = self.normalize_email(self.email_address)
1255 email_address = self.normalize_email(self.email_address)
1255
1256
1256 # first push the email initials
1257 # first push the email initials
1257 prefix, server = email_address.split('@', 1)
1258 prefix, server = email_address.split('@', 1)
1258
1259
1259 # check if prefix is maybe a 'first_name.last_name' syntax
1260 # check if prefix is maybe a 'first_name.last_name' syntax
1260 _dot_split = prefix.rsplit('.', 1)
1261 _dot_split = prefix.rsplit('.', 1)
1261 if len(_dot_split) == 2 and _dot_split[1]:
1262 if len(_dot_split) == 2 and _dot_split[1]:
1262 initials = [_dot_split[0][0], _dot_split[1][0]]
1263 initials = [_dot_split[0][0], _dot_split[1][0]]
1263 else:
1264 else:
1264 initials = [prefix[0], server[0]]
1265 initials = [prefix[0], server[0]]
1265
1266
1266 # then try to replace either first_name or last_name
1267 # then try to replace either first_name or last_name
1267 fn_letter = (first_name or " ")[0].strip()
1268 fn_letter = (first_name or " ")[0].strip()
1268 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1269 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1269
1270
1270 if fn_letter:
1271 if fn_letter:
1271 initials[0] = fn_letter
1272 initials[0] = fn_letter
1272
1273
1273 if ln_letter:
1274 if ln_letter:
1274 initials[1] = ln_letter
1275 initials[1] = ln_letter
1275
1276
1276 return ''.join(initials).upper()
1277 return ''.join(initials).upper()
1277
1278
1278 def get_img_data_by_type(self, font_family, img_type):
1279 def get_img_data_by_type(self, font_family, img_type):
1279 default_user = """
1280 default_user = """
1280 <svg xmlns="http://www.w3.org/2000/svg"
1281 <svg xmlns="http://www.w3.org/2000/svg"
1281 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1282 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1282 viewBox="-15 -10 439.165 429.164"
1283 viewBox="-15 -10 439.165 429.164"
1283
1284
1284 xml:space="preserve"
1285 xml:space="preserve"
1285 style="background:{background};" >
1286 style="background:{background};" >
1286
1287
1287 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1288 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1288 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1289 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1289 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1290 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1290 168.596,153.916,216.671,
1291 168.596,153.916,216.671,
1291 204.583,216.671z" fill="{text_color}"/>
1292 204.583,216.671z" fill="{text_color}"/>
1292 <path d="M407.164,374.717L360.88,
1293 <path d="M407.164,374.717L360.88,
1293 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1294 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1294 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1295 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1295 15.366-44.203,23.488-69.076,23.488c-24.877,
1296 15.366-44.203,23.488-69.076,23.488c-24.877,
1296 0-48.762-8.122-69.078-23.488
1297 0-48.762-8.122-69.078-23.488
1297 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1298 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1298 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1299 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1299 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1300 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1300 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1301 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1301 19.402-10.527 C409.699,390.129,
1302 19.402-10.527 C409.699,390.129,
1302 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1303 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1303 </svg>""".format(
1304 </svg>""".format(
1304 size=self.size,
1305 size=self.size,
1305 background='#979797', # @grey4
1306 background='#979797', # @grey4
1306 text_color=self.text_color,
1307 text_color=self.text_color,
1307 font_family=font_family)
1308 font_family=font_family)
1308
1309
1309 return {
1310 return {
1310 "default_user": default_user
1311 "default_user": default_user
1311 }[img_type]
1312 }[img_type]
1312
1313
1313 def get_img_data(self, svg_type=None):
1314 def get_img_data(self, svg_type=None):
1314 """
1315 """
1315 generates the svg metadata for image
1316 generates the svg metadata for image
1316 """
1317 """
1317 fonts = [
1318 fonts = [
1318 '-apple-system',
1319 '-apple-system',
1319 'BlinkMacSystemFont',
1320 'BlinkMacSystemFont',
1320 'Segoe UI',
1321 'Segoe UI',
1321 'Roboto',
1322 'Roboto',
1322 'Oxygen-Sans',
1323 'Oxygen-Sans',
1323 'Ubuntu',
1324 'Ubuntu',
1324 'Cantarell',
1325 'Cantarell',
1325 'Helvetica Neue',
1326 'Helvetica Neue',
1326 'sans-serif'
1327 'sans-serif'
1327 ]
1328 ]
1328 font_family = ','.join(fonts)
1329 font_family = ','.join(fonts)
1329 if svg_type:
1330 if svg_type:
1330 return self.get_img_data_by_type(font_family, svg_type)
1331 return self.get_img_data_by_type(font_family, svg_type)
1331
1332
1332 initials = self.get_initials()
1333 initials = self.get_initials()
1333 img_data = """
1334 img_data = """
1334 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1335 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1335 width="{size}" height="{size}"
1336 width="{size}" height="{size}"
1336 style="width: 100%; height: 100%; background-color: {background}"
1337 style="width: 100%; height: 100%; background-color: {background}"
1337 viewBox="0 0 {size} {size}">
1338 viewBox="0 0 {size} {size}">
1338 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1339 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1339 pointer-events="auto" fill="{text_color}"
1340 pointer-events="auto" fill="{text_color}"
1340 font-family="{font_family}"
1341 font-family="{font_family}"
1341 style="font-weight: 400; font-size: {f_size}px;">{text}
1342 style="font-weight: 400; font-size: {f_size}px;">{text}
1342 </text>
1343 </text>
1343 </svg>""".format(
1344 </svg>""".format(
1344 size=self.size,
1345 size=self.size,
1345 f_size=self.size/2.05, # scale the text inside the box nicely
1346 f_size=self.size/2.05, # scale the text inside the box nicely
1346 background=self.background,
1347 background=self.background,
1347 text_color=self.text_color,
1348 text_color=self.text_color,
1348 text=initials.upper(),
1349 text=initials.upper(),
1349 font_family=font_family)
1350 font_family=font_family)
1350
1351
1351 return img_data
1352 return img_data
1352
1353
1353 def generate_svg(self, svg_type=None):
1354 def generate_svg(self, svg_type=None):
1354 img_data = self.get_img_data(svg_type)
1355 img_data = self.get_img_data(svg_type)
1355 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1356 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1356
1357
1357
1358
1358 def initials_gravatar(email_address, first_name, last_name, size=30):
1359 def initials_gravatar(email_address, first_name, last_name, size=30):
1359 svg_type = None
1360 svg_type = None
1360 if email_address == User.DEFAULT_USER_EMAIL:
1361 if email_address == User.DEFAULT_USER_EMAIL:
1361 svg_type = 'default_user'
1362 svg_type = 'default_user'
1362 klass = InitialsGravatar(email_address, first_name, last_name, size)
1363 klass = InitialsGravatar(email_address, first_name, last_name, size)
1363 return klass.generate_svg(svg_type=svg_type)
1364 return klass.generate_svg(svg_type=svg_type)
1364
1365
1365
1366
1366 def gravatar_url(email_address, size=30, request=None):
1367 def gravatar_url(email_address, size=30, request=None):
1367 request = get_current_request()
1368 request = get_current_request()
1368 _use_gravatar = request.call_context.visual.use_gravatar
1369 _use_gravatar = request.call_context.visual.use_gravatar
1369 _gravatar_url = request.call_context.visual.gravatar_url
1370 _gravatar_url = request.call_context.visual.gravatar_url
1370
1371
1371 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1372 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1372
1373
1373 email_address = email_address or User.DEFAULT_USER_EMAIL
1374 email_address = email_address or User.DEFAULT_USER_EMAIL
1374 if isinstance(email_address, unicode):
1375 if isinstance(email_address, unicode):
1375 # hashlib crashes on unicode items
1376 # hashlib crashes on unicode items
1376 email_address = safe_str(email_address)
1377 email_address = safe_str(email_address)
1377
1378
1378 # empty email or default user
1379 # empty email or default user
1379 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1380 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1380 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1381 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1381
1382
1382 if _use_gravatar:
1383 if _use_gravatar:
1383 # TODO: Disuse pyramid thread locals. Think about another solution to
1384 # TODO: Disuse pyramid thread locals. Think about another solution to
1384 # get the host and schema here.
1385 # get the host and schema here.
1385 request = get_current_request()
1386 request = get_current_request()
1386 tmpl = safe_str(_gravatar_url)
1387 tmpl = safe_str(_gravatar_url)
1387 tmpl = tmpl.replace('{email}', email_address)\
1388 tmpl = tmpl.replace('{email}', email_address)\
1388 .replace('{md5email}', md5_safe(email_address.lower())) \
1389 .replace('{md5email}', md5_safe(email_address.lower())) \
1389 .replace('{netloc}', request.host)\
1390 .replace('{netloc}', request.host)\
1390 .replace('{scheme}', request.scheme)\
1391 .replace('{scheme}', request.scheme)\
1391 .replace('{size}', safe_str(size))
1392 .replace('{size}', safe_str(size))
1392 return tmpl
1393 return tmpl
1393 else:
1394 else:
1394 return initials_gravatar(email_address, '', '', size=size)
1395 return initials_gravatar(email_address, '', '', size=size)
1395
1396
1396
1397
1397 def breadcrumb_repo_link(repo):
1398 def breadcrumb_repo_link(repo):
1398 """
1399 """
1399 Makes a breadcrumbs path link to repo
1400 Makes a breadcrumbs path link to repo
1400
1401
1401 ex::
1402 ex::
1402 group >> subgroup >> repo
1403 group >> subgroup >> repo
1403
1404
1404 :param repo: a Repository instance
1405 :param repo: a Repository instance
1405 """
1406 """
1406
1407
1407 path = [
1408 path = [
1408 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1409 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1409 title='last change:{}'.format(format_date(group.last_commit_change)))
1410 title='last change:{}'.format(format_date(group.last_commit_change)))
1410 for group in repo.groups_with_parents
1411 for group in repo.groups_with_parents
1411 ] + [
1412 ] + [
1412 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1413 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1413 title='last change:{}'.format(format_date(repo.last_commit_change)))
1414 title='last change:{}'.format(format_date(repo.last_commit_change)))
1414 ]
1415 ]
1415
1416
1416 return literal(' &raquo; '.join(path))
1417 return literal(' &raquo; '.join(path))
1417
1418
1418
1419
1419 def breadcrumb_repo_group_link(repo_group):
1420 def breadcrumb_repo_group_link(repo_group):
1420 """
1421 """
1421 Makes a breadcrumbs path link to repo
1422 Makes a breadcrumbs path link to repo
1422
1423
1423 ex::
1424 ex::
1424 group >> subgroup
1425 group >> subgroup
1425
1426
1426 :param repo_group: a Repository Group instance
1427 :param repo_group: a Repository Group instance
1427 """
1428 """
1428
1429
1429 path = [
1430 path = [
1430 link_to(group.name,
1431 link_to(group.name,
1431 route_path('repo_group_home', repo_group_name=group.group_name),
1432 route_path('repo_group_home', repo_group_name=group.group_name),
1432 title='last change:{}'.format(format_date(group.last_commit_change)))
1433 title='last change:{}'.format(format_date(group.last_commit_change)))
1433 for group in repo_group.parents
1434 for group in repo_group.parents
1434 ] + [
1435 ] + [
1435 link_to(repo_group.name,
1436 link_to(repo_group.name,
1436 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1437 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1437 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1438 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1438 ]
1439 ]
1439
1440
1440 return literal(' &raquo; '.join(path))
1441 return literal(' &raquo; '.join(path))
1441
1442
1442
1443
1443 def format_byte_size_binary(file_size):
1444 def format_byte_size_binary(file_size):
1444 """
1445 """
1445 Formats file/folder sizes to standard.
1446 Formats file/folder sizes to standard.
1446 """
1447 """
1447 if file_size is None:
1448 if file_size is None:
1448 file_size = 0
1449 file_size = 0
1449
1450
1450 formatted_size = format_byte_size(file_size, binary=True)
1451 formatted_size = format_byte_size(file_size, binary=True)
1451 return formatted_size
1452 return formatted_size
1452
1453
1453
1454
1454 def urlify_text(text_, safe=True, **href_attrs):
1455 def urlify_text(text_, safe=True, **href_attrs):
1455 """
1456 """
1456 Extract urls from text and make html links out of them
1457 Extract urls from text and make html links out of them
1457 """
1458 """
1458
1459
1459 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1460 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1460 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1461 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1461
1462
1462 def url_func(match_obj):
1463 def url_func(match_obj):
1463 url_full = match_obj.groups()[0]
1464 url_full = match_obj.groups()[0]
1464 a_options = dict(href_attrs)
1465 a_options = dict(href_attrs)
1465 a_options['href'] = url_full
1466 a_options['href'] = url_full
1466 a_text = url_full
1467 a_text = url_full
1467 return HTML.tag("a", a_text, **a_options)
1468 return HTML.tag("a", a_text, **a_options)
1468
1469
1469 _new_text = url_pat.sub(url_func, text_)
1470 _new_text = url_pat.sub(url_func, text_)
1470
1471
1471 if safe:
1472 if safe:
1472 return literal(_new_text)
1473 return literal(_new_text)
1473 return _new_text
1474 return _new_text
1474
1475
1475
1476
1476 def urlify_commits(text_, repo_name):
1477 def urlify_commits(text_, repo_name):
1477 """
1478 """
1478 Extract commit ids from text and make link from them
1479 Extract commit ids from text and make link from them
1479
1480
1480 :param text_:
1481 :param text_:
1481 :param repo_name: repo name to build the URL with
1482 :param repo_name: repo name to build the URL with
1482 """
1483 """
1483
1484
1484 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1485 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1485
1486
1486 def url_func(match_obj):
1487 def url_func(match_obj):
1487 commit_id = match_obj.groups()[1]
1488 commit_id = match_obj.groups()[1]
1488 pref = match_obj.groups()[0]
1489 pref = match_obj.groups()[0]
1489 suf = match_obj.groups()[2]
1490 suf = match_obj.groups()[2]
1490
1491
1491 tmpl = (
1492 tmpl = (
1492 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1493 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1493 '%(commit_id)s</a>%(suf)s'
1494 '%(commit_id)s</a>%(suf)s'
1494 )
1495 )
1495 return tmpl % {
1496 return tmpl % {
1496 'pref': pref,
1497 'pref': pref,
1497 'cls': 'revision-link',
1498 'cls': 'revision-link',
1498 'url': route_url(
1499 'url': route_url(
1499 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1500 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1500 'commit_id': commit_id,
1501 'commit_id': commit_id,
1501 'suf': suf,
1502 'suf': suf,
1502 'hovercard_alt': 'Commit: {}'.format(commit_id),
1503 'hovercard_alt': 'Commit: {}'.format(commit_id),
1503 'hovercard_url': route_url(
1504 'hovercard_url': route_url(
1504 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1505 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1505 }
1506 }
1506
1507
1507 new_text = url_pat.sub(url_func, text_)
1508 new_text = url_pat.sub(url_func, text_)
1508
1509
1509 return new_text
1510 return new_text
1510
1511
1511
1512
1512 def _process_url_func(match_obj, repo_name, uid, entry,
1513 def _process_url_func(match_obj, repo_name, uid, entry,
1513 return_raw_data=False, link_format='html'):
1514 return_raw_data=False, link_format='html'):
1514 pref = ''
1515 pref = ''
1515 if match_obj.group().startswith(' '):
1516 if match_obj.group().startswith(' '):
1516 pref = ' '
1517 pref = ' '
1517
1518
1518 issue_id = ''.join(match_obj.groups())
1519 issue_id = ''.join(match_obj.groups())
1519
1520
1520 if link_format == 'html':
1521 if link_format == 'html':
1521 tmpl = (
1522 tmpl = (
1522 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1523 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1523 '%(issue-prefix)s%(id-repr)s'
1524 '%(issue-prefix)s%(id-repr)s'
1524 '</a>')
1525 '</a>')
1525 elif link_format == 'html+hovercard':
1526 elif link_format == 'html+hovercard':
1526 tmpl = (
1527 tmpl = (
1527 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1528 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1528 '%(issue-prefix)s%(id-repr)s'
1529 '%(issue-prefix)s%(id-repr)s'
1529 '</a>')
1530 '</a>')
1530 elif link_format in ['rst', 'rst+hovercard']:
1531 elif link_format in ['rst', 'rst+hovercard']:
1531 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1532 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1532 elif link_format in ['markdown', 'markdown+hovercard']:
1533 elif link_format in ['markdown', 'markdown+hovercard']:
1533 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1534 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1534 else:
1535 else:
1535 raise ValueError('Bad link_format:{}'.format(link_format))
1536 raise ValueError('Bad link_format:{}'.format(link_format))
1536
1537
1537 (repo_name_cleaned,
1538 (repo_name_cleaned,
1538 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1539 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1539
1540
1540 # variables replacement
1541 # variables replacement
1541 named_vars = {
1542 named_vars = {
1542 'id': issue_id,
1543 'id': issue_id,
1543 'repo': repo_name,
1544 'repo': repo_name,
1544 'repo_name': repo_name_cleaned,
1545 'repo_name': repo_name_cleaned,
1545 'group_name': parent_group_name,
1546 'group_name': parent_group_name,
1546 # set dummy keys so we always have them
1547 # set dummy keys so we always have them
1547 'hostname': '',
1548 'hostname': '',
1548 'netloc': '',
1549 'netloc': '',
1549 'scheme': ''
1550 'scheme': ''
1550 }
1551 }
1551
1552
1552 request = get_current_request()
1553 request = get_current_request()
1553 if request:
1554 if request:
1554 # exposes, hostname, netloc, scheme
1555 # exposes, hostname, netloc, scheme
1555 host_data = get_host_info(request)
1556 host_data = get_host_info(request)
1556 named_vars.update(host_data)
1557 named_vars.update(host_data)
1557
1558
1558 # named regex variables
1559 # named regex variables
1559 named_vars.update(match_obj.groupdict())
1560 named_vars.update(match_obj.groupdict())
1560 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1561 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1561 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1562 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1562 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1563 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1563
1564
1564 def quote_cleaner(input_str):
1565 def quote_cleaner(input_str):
1565 """Remove quotes as it's HTML"""
1566 """Remove quotes as it's HTML"""
1566 return input_str.replace('"', '')
1567 return input_str.replace('"', '')
1567
1568
1568 data = {
1569 data = {
1569 'pref': pref,
1570 'pref': pref,
1570 'cls': quote_cleaner('issue-tracker-link'),
1571 'cls': quote_cleaner('issue-tracker-link'),
1571 'url': quote_cleaner(_url),
1572 'url': quote_cleaner(_url),
1572 'id-repr': issue_id,
1573 'id-repr': issue_id,
1573 'issue-prefix': entry['pref'],
1574 'issue-prefix': entry['pref'],
1574 'serv': entry['url'],
1575 'serv': entry['url'],
1575 'title': bleach.clean(desc, strip=True),
1576 'title': bleach.clean(desc, strip=True),
1576 'hovercard_url': hovercard_url
1577 'hovercard_url': hovercard_url
1577 }
1578 }
1578
1579
1579 if return_raw_data:
1580 if return_raw_data:
1580 return {
1581 return {
1581 'id': issue_id,
1582 'id': issue_id,
1582 'url': _url
1583 'url': _url
1583 }
1584 }
1584 return tmpl % data
1585 return tmpl % data
1585
1586
1586
1587
1587 def get_active_pattern_entries(repo_name):
1588 def get_active_pattern_entries(repo_name):
1588 repo = None
1589 repo = None
1589 if repo_name:
1590 if repo_name:
1590 # Retrieving repo_name to avoid invalid repo_name to explode on
1591 # Retrieving repo_name to avoid invalid repo_name to explode on
1591 # IssueTrackerSettingsModel but still passing invalid name further down
1592 # IssueTrackerSettingsModel but still passing invalid name further down
1592 repo = Repository.get_by_repo_name(repo_name, cache=True)
1593 repo = Repository.get_by_repo_name(repo_name, cache=True)
1593
1594
1594 settings_model = IssueTrackerSettingsModel(repo=repo)
1595 settings_model = IssueTrackerSettingsModel(repo=repo)
1595 active_entries = settings_model.get_settings(cache=True)
1596 active_entries = settings_model.get_settings(cache=True)
1596 return active_entries
1597 return active_entries
1597
1598
1598
1599
1599 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1600 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1600
1601
1601
1602
1602 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1603 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1603
1604
1604 allowed_formats = ['html', 'rst', 'markdown',
1605 allowed_formats = ['html', 'rst', 'markdown',
1605 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1606 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1606 if link_format not in allowed_formats:
1607 if link_format not in allowed_formats:
1607 raise ValueError('Link format can be only one of:{} got {}'.format(
1608 raise ValueError('Link format can be only one of:{} got {}'.format(
1608 allowed_formats, link_format))
1609 allowed_formats, link_format))
1609
1610
1610 if active_entries is None:
1611 if active_entries is None:
1611 log.debug('Fetch active patterns for repo: %s', repo_name)
1612 log.debug('Fetch active patterns for repo: %s', repo_name)
1612 active_entries = get_active_pattern_entries(repo_name)
1613 active_entries = get_active_pattern_entries(repo_name)
1613
1614
1614 issues_data = []
1615 issues_data = []
1615 new_text = text_string
1616 new_text = text_string
1616
1617
1617 log.debug('Got %s entries to process', len(active_entries))
1618 log.debug('Got %s entries to process', len(active_entries))
1618 for uid, entry in active_entries.items():
1619 for uid, entry in active_entries.items():
1619 log.debug('found issue tracker entry with uid %s', uid)
1620 log.debug('found issue tracker entry with uid %s', uid)
1620
1621
1621 if not (entry['pat'] and entry['url']):
1622 if not (entry['pat'] and entry['url']):
1622 log.debug('skipping due to missing data')
1623 log.debug('skipping due to missing data')
1623 continue
1624 continue
1624
1625
1625 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1626 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1626 uid, entry['pat'], entry['url'], entry['pref'])
1627 uid, entry['pat'], entry['url'], entry['pref'])
1627
1628
1628 if entry.get('pat_compiled'):
1629 if entry.get('pat_compiled'):
1629 pattern = entry['pat_compiled']
1630 pattern = entry['pat_compiled']
1630 else:
1631 else:
1631 try:
1632 try:
1632 pattern = re.compile(r'%s' % entry['pat'])
1633 pattern = re.compile(r'%s' % entry['pat'])
1633 except re.error:
1634 except re.error:
1634 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1635 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1635 continue
1636 continue
1636
1637
1637 data_func = partial(
1638 data_func = partial(
1638 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1639 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1639 return_raw_data=True)
1640 return_raw_data=True)
1640
1641
1641 for match_obj in pattern.finditer(text_string):
1642 for match_obj in pattern.finditer(text_string):
1642 issues_data.append(data_func(match_obj))
1643 issues_data.append(data_func(match_obj))
1643
1644
1644 url_func = partial(
1645 url_func = partial(
1645 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1646 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1646 link_format=link_format)
1647 link_format=link_format)
1647
1648
1648 new_text = pattern.sub(url_func, new_text)
1649 new_text = pattern.sub(url_func, new_text)
1649 log.debug('processed prefix:uid `%s`', uid)
1650 log.debug('processed prefix:uid `%s`', uid)
1650
1651
1651 # finally use global replace, eg !123 -> pr-link, those will not catch
1652 # finally use global replace, eg !123 -> pr-link, those will not catch
1652 # if already similar pattern exists
1653 # if already similar pattern exists
1653 server_url = '${scheme}://${netloc}'
1654 server_url = '${scheme}://${netloc}'
1654 pr_entry = {
1655 pr_entry = {
1655 'pref': '!',
1656 'pref': '!',
1656 'url': server_url + '/_admin/pull-requests/${id}',
1657 'url': server_url + '/_admin/pull-requests/${id}',
1657 'desc': 'Pull Request !${id}',
1658 'desc': 'Pull Request !${id}',
1658 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1659 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1659 }
1660 }
1660 pr_url_func = partial(
1661 pr_url_func = partial(
1661 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1662 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1662 link_format=link_format+'+hovercard')
1663 link_format=link_format+'+hovercard')
1663 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1664 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1664 log.debug('processed !pr pattern')
1665 log.debug('processed !pr pattern')
1665
1666
1666 return new_text, issues_data
1667 return new_text, issues_data
1667
1668
1668
1669
1669 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1670 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1670 """
1671 """
1671 Parses given text message and makes proper links.
1672 Parses given text message and makes proper links.
1672 issues are linked to given issue-server, and rest is a commit link
1673 issues are linked to given issue-server, and rest is a commit link
1673 """
1674 """
1674
1675
1675 def escaper(_text):
1676 def escaper(_text):
1676 return _text.replace('<', '&lt;').replace('>', '&gt;')
1677 return _text.replace('<', '&lt;').replace('>', '&gt;')
1677
1678
1678 new_text = escaper(commit_text)
1679 new_text = escaper(commit_text)
1679
1680
1680 # extract http/https links and make them real urls
1681 # extract http/https links and make them real urls
1681 new_text = urlify_text(new_text, safe=False)
1682 new_text = urlify_text(new_text, safe=False)
1682
1683
1683 # urlify commits - extract commit ids and make link out of them, if we have
1684 # urlify commits - extract commit ids and make link out of them, if we have
1684 # the scope of repository present.
1685 # the scope of repository present.
1685 if repository:
1686 if repository:
1686 new_text = urlify_commits(new_text, repository)
1687 new_text = urlify_commits(new_text, repository)
1687
1688
1688 # process issue tracker patterns
1689 # process issue tracker patterns
1689 new_text, issues = process_patterns(new_text, repository or '',
1690 new_text, issues = process_patterns(new_text, repository or '',
1690 active_entries=active_pattern_entries)
1691 active_entries=active_pattern_entries)
1691
1692
1692 return literal(new_text)
1693 return literal(new_text)
1693
1694
1694
1695
1695 def render_binary(repo_name, file_obj):
1696 def render_binary(repo_name, file_obj):
1696 """
1697 """
1697 Choose how to render a binary file
1698 Choose how to render a binary file
1698 """
1699 """
1699
1700
1700 # unicode
1701 # unicode
1701 filename = file_obj.name
1702 filename = file_obj.name
1702
1703
1703 # images
1704 # images
1704 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1705 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1705 if fnmatch.fnmatch(filename, pat=ext):
1706 if fnmatch.fnmatch(filename, pat=ext):
1706 src = route_path(
1707 src = route_path(
1707 'repo_file_raw', repo_name=repo_name,
1708 'repo_file_raw', repo_name=repo_name,
1708 commit_id=file_obj.commit.raw_id,
1709 commit_id=file_obj.commit.raw_id,
1709 f_path=file_obj.path)
1710 f_path=file_obj.path)
1710
1711
1711 return literal(
1712 return literal(
1712 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1713 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1713
1714
1714
1715
1715 def renderer_from_filename(filename, exclude=None):
1716 def renderer_from_filename(filename, exclude=None):
1716 """
1717 """
1717 choose a renderer based on filename, this works only for text based files
1718 choose a renderer based on filename, this works only for text based files
1718 """
1719 """
1719
1720
1720 # ipython
1721 # ipython
1721 for ext in ['*.ipynb']:
1722 for ext in ['*.ipynb']:
1722 if fnmatch.fnmatch(filename, pat=ext):
1723 if fnmatch.fnmatch(filename, pat=ext):
1723 return 'jupyter'
1724 return 'jupyter'
1724
1725
1725 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1726 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1726 if is_markup:
1727 if is_markup:
1727 return is_markup
1728 return is_markup
1728 return None
1729 return None
1729
1730
1730
1731
1731 def render(source, renderer='rst', mentions=False, relative_urls=None,
1732 def render(source, renderer='rst', mentions=False, relative_urls=None,
1732 repo_name=None, active_pattern_entries=None):
1733 repo_name=None, active_pattern_entries=None):
1733
1734
1734 def maybe_convert_relative_links(html_source):
1735 def maybe_convert_relative_links(html_source):
1735 if relative_urls:
1736 if relative_urls:
1736 return relative_links(html_source, relative_urls)
1737 return relative_links(html_source, relative_urls)
1737 return html_source
1738 return html_source
1738
1739
1739 if renderer == 'plain':
1740 if renderer == 'plain':
1740 return literal(
1741 return literal(
1741 MarkupRenderer.plain(source, leading_newline=False))
1742 MarkupRenderer.plain(source, leading_newline=False))
1742
1743
1743 elif renderer == 'rst':
1744 elif renderer == 'rst':
1744 if repo_name:
1745 if repo_name:
1745 # process patterns on comments if we pass in repo name
1746 # process patterns on comments if we pass in repo name
1746 source, issues = process_patterns(
1747 source, issues = process_patterns(
1747 source, repo_name, link_format='rst',
1748 source, repo_name, link_format='rst',
1748 active_entries=active_pattern_entries)
1749 active_entries=active_pattern_entries)
1749
1750
1750 return literal(
1751 return literal(
1751 '<div class="rst-block">%s</div>' %
1752 '<div class="rst-block">%s</div>' %
1752 maybe_convert_relative_links(
1753 maybe_convert_relative_links(
1753 MarkupRenderer.rst(source, mentions=mentions)))
1754 MarkupRenderer.rst(source, mentions=mentions)))
1754
1755
1755 elif renderer == 'markdown':
1756 elif renderer == 'markdown':
1756 if repo_name:
1757 if repo_name:
1757 # process patterns on comments if we pass in repo name
1758 # process patterns on comments if we pass in repo name
1758 source, issues = process_patterns(
1759 source, issues = process_patterns(
1759 source, repo_name, link_format='markdown',
1760 source, repo_name, link_format='markdown',
1760 active_entries=active_pattern_entries)
1761 active_entries=active_pattern_entries)
1761
1762
1762 return literal(
1763 return literal(
1763 '<div class="markdown-block">%s</div>' %
1764 '<div class="markdown-block">%s</div>' %
1764 maybe_convert_relative_links(
1765 maybe_convert_relative_links(
1765 MarkupRenderer.markdown(source, flavored=True,
1766 MarkupRenderer.markdown(source, flavored=True,
1766 mentions=mentions)))
1767 mentions=mentions)))
1767
1768
1768 elif renderer == 'jupyter':
1769 elif renderer == 'jupyter':
1769 return literal(
1770 return literal(
1770 '<div class="ipynb">%s</div>' %
1771 '<div class="ipynb">%s</div>' %
1771 maybe_convert_relative_links(
1772 maybe_convert_relative_links(
1772 MarkupRenderer.jupyter(source)))
1773 MarkupRenderer.jupyter(source)))
1773
1774
1774 # None means just show the file-source
1775 # None means just show the file-source
1775 return None
1776 return None
1776
1777
1777
1778
1778 def commit_status(repo, commit_id):
1779 def commit_status(repo, commit_id):
1779 return ChangesetStatusModel().get_status(repo, commit_id)
1780 return ChangesetStatusModel().get_status(repo, commit_id)
1780
1781
1781
1782
1782 def commit_status_lbl(commit_status):
1783 def commit_status_lbl(commit_status):
1783 return dict(ChangesetStatus.STATUSES).get(commit_status)
1784 return dict(ChangesetStatus.STATUSES).get(commit_status)
1784
1785
1785
1786
1786 def commit_time(repo_name, commit_id):
1787 def commit_time(repo_name, commit_id):
1787 repo = Repository.get_by_repo_name(repo_name)
1788 repo = Repository.get_by_repo_name(repo_name)
1788 commit = repo.get_commit(commit_id=commit_id)
1789 commit = repo.get_commit(commit_id=commit_id)
1789 return commit.date
1790 return commit.date
1790
1791
1791
1792
1792 def get_permission_name(key):
1793 def get_permission_name(key):
1793 return dict(Permission.PERMS).get(key)
1794 return dict(Permission.PERMS).get(key)
1794
1795
1795
1796
1796 def journal_filter_help(request):
1797 def journal_filter_help(request):
1797 _ = request.translate
1798 _ = request.translate
1798 from rhodecode.lib.audit_logger import ACTIONS
1799 from rhodecode.lib.audit_logger import ACTIONS
1799 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1800 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1800
1801
1801 return _(
1802 return _(
1802 'Example filter terms:\n' +
1803 'Example filter terms:\n' +
1803 ' repository:vcs\n' +
1804 ' repository:vcs\n' +
1804 ' username:marcin\n' +
1805 ' username:marcin\n' +
1805 ' username:(NOT marcin)\n' +
1806 ' username:(NOT marcin)\n' +
1806 ' action:*push*\n' +
1807 ' action:*push*\n' +
1807 ' ip:127.0.0.1\n' +
1808 ' ip:127.0.0.1\n' +
1808 ' date:20120101\n' +
1809 ' date:20120101\n' +
1809 ' date:[20120101100000 TO 20120102]\n' +
1810 ' date:[20120101100000 TO 20120102]\n' +
1810 '\n' +
1811 '\n' +
1811 'Actions: {actions}\n' +
1812 'Actions: {actions}\n' +
1812 '\n' +
1813 '\n' +
1813 'Generate wildcards using \'*\' character:\n' +
1814 'Generate wildcards using \'*\' character:\n' +
1814 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1815 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1815 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1816 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1816 '\n' +
1817 '\n' +
1817 'Optional AND / OR operators in queries\n' +
1818 'Optional AND / OR operators in queries\n' +
1818 ' "repository:vcs OR repository:test"\n' +
1819 ' "repository:vcs OR repository:test"\n' +
1819 ' "username:test AND repository:test*"\n'
1820 ' "username:test AND repository:test*"\n'
1820 ).format(actions=actions)
1821 ).format(actions=actions)
1821
1822
1822
1823
1823 def not_mapped_error(repo_name):
1824 def not_mapped_error(repo_name):
1824 from rhodecode.translation import _
1825 from rhodecode.translation import _
1825 flash(_('%s repository is not mapped to db perhaps'
1826 flash(_('%s repository is not mapped to db perhaps'
1826 ' it was created or renamed from the filesystem'
1827 ' it was created or renamed from the filesystem'
1827 ' please run the application again'
1828 ' please run the application again'
1828 ' in order to rescan repositories') % repo_name, category='error')
1829 ' in order to rescan repositories') % repo_name, category='error')
1829
1830
1830
1831
1831 def ip_range(ip_addr):
1832 def ip_range(ip_addr):
1832 from rhodecode.model.db import UserIpMap
1833 from rhodecode.model.db import UserIpMap
1833 s, e = UserIpMap._get_ip_range(ip_addr)
1834 s, e = UserIpMap._get_ip_range(ip_addr)
1834 return '%s - %s' % (s, e)
1835 return '%s - %s' % (s, e)
1835
1836
1836
1837
1837 def form(url, method='post', needs_csrf_token=True, **attrs):
1838 def form(url, method='post', needs_csrf_token=True, **attrs):
1838 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1839 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1839 if method.lower() != 'get' and needs_csrf_token:
1840 if method.lower() != 'get' and needs_csrf_token:
1840 raise Exception(
1841 raise Exception(
1841 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1842 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1842 'CSRF token. If the endpoint does not require such token you can ' +
1843 'CSRF token. If the endpoint does not require such token you can ' +
1843 'explicitly set the parameter needs_csrf_token to false.')
1844 'explicitly set the parameter needs_csrf_token to false.')
1844
1845
1845 return insecure_form(url, method=method, **attrs)
1846 return insecure_form(url, method=method, **attrs)
1846
1847
1847
1848
1848 def secure_form(form_url, method="POST", multipart=False, **attrs):
1849 def secure_form(form_url, method="POST", multipart=False, **attrs):
1849 """Start a form tag that points the action to an url. This
1850 """Start a form tag that points the action to an url. This
1850 form tag will also include the hidden field containing
1851 form tag will also include the hidden field containing
1851 the auth token.
1852 the auth token.
1852
1853
1853 The url options should be given either as a string, or as a
1854 The url options should be given either as a string, or as a
1854 ``url()`` function. The method for the form defaults to POST.
1855 ``url()`` function. The method for the form defaults to POST.
1855
1856
1856 Options:
1857 Options:
1857
1858
1858 ``multipart``
1859 ``multipart``
1859 If set to True, the enctype is set to "multipart/form-data".
1860 If set to True, the enctype is set to "multipart/form-data".
1860 ``method``
1861 ``method``
1861 The method to use when submitting the form, usually either
1862 The method to use when submitting the form, usually either
1862 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1863 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1863 hidden input with name _method is added to simulate the verb
1864 hidden input with name _method is added to simulate the verb
1864 over POST.
1865 over POST.
1865
1866
1866 """
1867 """
1867
1868
1868 if 'request' in attrs:
1869 if 'request' in attrs:
1869 session = attrs['request'].session
1870 session = attrs['request'].session
1870 del attrs['request']
1871 del attrs['request']
1871 else:
1872 else:
1872 raise ValueError(
1873 raise ValueError(
1873 'Calling this form requires request= to be passed as argument')
1874 'Calling this form requires request= to be passed as argument')
1874
1875
1875 _form = insecure_form(form_url, method, multipart, **attrs)
1876 _form = insecure_form(form_url, method, multipart, **attrs)
1876 token = literal(
1877 token = literal(
1877 '<input type="hidden" name="{}" value="{}">'.format(
1878 '<input type="hidden" name="{}" value="{}">'.format(
1878 csrf_token_key, get_csrf_token(session)))
1879 csrf_token_key, get_csrf_token(session)))
1879
1880
1880 return literal("%s\n%s" % (_form, token))
1881 return literal("%s\n%s" % (_form, token))
1881
1882
1882
1883
1883 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1884 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1884 select_html = select(name, selected, options, **attrs)
1885 select_html = select(name, selected, options, **attrs)
1885
1886
1886 select2 = """
1887 select2 = """
1887 <script>
1888 <script>
1888 $(document).ready(function() {
1889 $(document).ready(function() {
1889 $('#%s').select2({
1890 $('#%s').select2({
1890 containerCssClass: 'drop-menu %s',
1891 containerCssClass: 'drop-menu %s',
1891 dropdownCssClass: 'drop-menu-dropdown',
1892 dropdownCssClass: 'drop-menu-dropdown',
1892 dropdownAutoWidth: true%s
1893 dropdownAutoWidth: true%s
1893 });
1894 });
1894 });
1895 });
1895 </script>
1896 </script>
1896 """
1897 """
1897
1898
1898 filter_option = """,
1899 filter_option = """,
1899 minimumResultsForSearch: -1
1900 minimumResultsForSearch: -1
1900 """
1901 """
1901 input_id = attrs.get('id') or name
1902 input_id = attrs.get('id') or name
1902 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1903 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1903 filter_enabled = "" if enable_filter else filter_option
1904 filter_enabled = "" if enable_filter else filter_option
1904 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1905 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1905
1906
1906 return literal(select_html+select_script)
1907 return literal(select_html+select_script)
1907
1908
1908
1909
1909 def get_visual_attr(tmpl_context_var, attr_name):
1910 def get_visual_attr(tmpl_context_var, attr_name):
1910 """
1911 """
1911 A safe way to get a variable from visual variable of template context
1912 A safe way to get a variable from visual variable of template context
1912
1913
1913 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1914 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1914 :param attr_name: name of the attribute we fetch from the c.visual
1915 :param attr_name: name of the attribute we fetch from the c.visual
1915 """
1916 """
1916 visual = getattr(tmpl_context_var, 'visual', None)
1917 visual = getattr(tmpl_context_var, 'visual', None)
1917 if not visual:
1918 if not visual:
1918 return
1919 return
1919 else:
1920 else:
1920 return getattr(visual, attr_name, None)
1921 return getattr(visual, attr_name, None)
1921
1922
1922
1923
1923 def get_last_path_part(file_node):
1924 def get_last_path_part(file_node):
1924 if not file_node.path:
1925 if not file_node.path:
1925 return u'/'
1926 return u'/'
1926
1927
1927 path = safe_unicode(file_node.path.split('/')[-1])
1928 path = safe_unicode(file_node.path.split('/')[-1])
1928 return u'../' + path
1929 return u'../' + path
1929
1930
1930
1931
1931 def route_url(*args, **kwargs):
1932 def route_url(*args, **kwargs):
1932 """
1933 """
1933 Wrapper around pyramids `route_url` (fully qualified url) function.
1934 Wrapper around pyramids `route_url` (fully qualified url) function.
1934 """
1935 """
1935 req = get_current_request()
1936 req = get_current_request()
1936 return req.route_url(*args, **kwargs)
1937 return req.route_url(*args, **kwargs)
1937
1938
1938
1939
1939 def route_path(*args, **kwargs):
1940 def route_path(*args, **kwargs):
1940 """
1941 """
1941 Wrapper around pyramids `route_path` function.
1942 Wrapper around pyramids `route_path` function.
1942 """
1943 """
1943 req = get_current_request()
1944 req = get_current_request()
1944 return req.route_path(*args, **kwargs)
1945 return req.route_path(*args, **kwargs)
1945
1946
1946
1947
1947 def route_path_or_none(*args, **kwargs):
1948 def route_path_or_none(*args, **kwargs):
1948 try:
1949 try:
1949 return route_path(*args, **kwargs)
1950 return route_path(*args, **kwargs)
1950 except KeyError:
1951 except KeyError:
1951 return None
1952 return None
1952
1953
1953
1954
1954 def current_route_path(request, **kw):
1955 def current_route_path(request, **kw):
1955 new_args = request.GET.mixed()
1956 new_args = request.GET.mixed()
1956 new_args.update(kw)
1957 new_args.update(kw)
1957 return request.current_route_path(_query=new_args)
1958 return request.current_route_path(_query=new_args)
1958
1959
1959
1960
1960 def curl_api_example(method, args):
1961 def curl_api_example(method, args):
1961 args_json = json.dumps(OrderedDict([
1962 args_json = json.dumps(OrderedDict([
1962 ('id', 1),
1963 ('id', 1),
1963 ('auth_token', 'SECRET'),
1964 ('auth_token', 'SECRET'),
1964 ('method', method),
1965 ('method', method),
1965 ('args', args)
1966 ('args', args)
1966 ]))
1967 ]))
1967
1968
1968 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1969 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1969 api_url=route_url('apiv2'),
1970 api_url=route_url('apiv2'),
1970 args_json=args_json
1971 args_json=args_json
1971 )
1972 )
1972
1973
1973
1974
1974 def api_call_example(method, args):
1975 def api_call_example(method, args):
1975 """
1976 """
1976 Generates an API call example via CURL
1977 Generates an API call example via CURL
1977 """
1978 """
1978 curl_call = curl_api_example(method, args)
1979 curl_call = curl_api_example(method, args)
1979
1980
1980 return literal(
1981 return literal(
1981 curl_call +
1982 curl_call +
1982 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1983 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1983 "and needs to be of `api calls` role."
1984 "and needs to be of `api calls` role."
1984 .format(token_url=route_url('my_account_auth_tokens')))
1985 .format(token_url=route_url('my_account_auth_tokens')))
1985
1986
1986
1987
1987 def notification_description(notification, request):
1988 def notification_description(notification, request):
1988 """
1989 """
1989 Generate notification human readable description based on notification type
1990 Generate notification human readable description based on notification type
1990 """
1991 """
1991 from rhodecode.model.notification import NotificationModel
1992 from rhodecode.model.notification import NotificationModel
1992 return NotificationModel().make_description(
1993 return NotificationModel().make_description(
1993 notification, translate=request.translate)
1994 notification, translate=request.translate)
1994
1995
1995
1996
1996 def go_import_header(request, db_repo=None):
1997 def go_import_header(request, db_repo=None):
1997 """
1998 """
1998 Creates a header for go-import functionality in Go Lang
1999 Creates a header for go-import functionality in Go Lang
1999 """
2000 """
2000
2001
2001 if not db_repo:
2002 if not db_repo:
2002 return
2003 return
2003 if 'go-get' not in request.GET:
2004 if 'go-get' not in request.GET:
2004 return
2005 return
2005
2006
2006 clone_url = db_repo.clone_url()
2007 clone_url = db_repo.clone_url()
2007 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2008 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2008 # we have a repo and go-get flag,
2009 # we have a repo and go-get flag,
2009 return literal('<meta name="go-import" content="{} {} {}">'.format(
2010 return literal('<meta name="go-import" content="{} {} {}">'.format(
2010 prefix, db_repo.repo_type, clone_url))
2011 prefix, db_repo.repo_type, clone_url))
2011
2012
2012
2013
2013 def reviewer_as_json(*args, **kwargs):
2014 def reviewer_as_json(*args, **kwargs):
2014 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2015 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2015 return _reviewer_as_json(*args, **kwargs)
2016 return _reviewer_as_json(*args, **kwargs)
2016
2017
2017
2018
2018 def get_repo_view_type(request):
2019 def get_repo_view_type(request):
2019 route_name = request.matched_route.name
2020 route_name = request.matched_route.name
2020 route_to_view_type = {
2021 route_to_view_type = {
2021 'repo_changelog': 'commits',
2022 'repo_changelog': 'commits',
2022 'repo_commits': 'commits',
2023 'repo_commits': 'commits',
2023 'repo_files': 'files',
2024 'repo_files': 'files',
2024 'repo_summary': 'summary',
2025 'repo_summary': 'summary',
2025 'repo_commit': 'commit'
2026 'repo_commit': 'commit'
2026 }
2027 }
2027
2028
2028 return route_to_view_type.get(route_name)
2029 return route_to_view_type.get(route_name)
2029
2030
2030
2031
2031 def is_active(menu_entry, selected):
2032 def is_active(menu_entry, selected):
2032 """
2033 """
2033 Returns active class for selecting menus in templates
2034 Returns active class for selecting menus in templates
2034 <li class=${h.is_active('settings', current_active)}></li>
2035 <li class=${h.is_active('settings', current_active)}></li>
2035 """
2036 """
2036 if not isinstance(menu_entry, list):
2037 if not isinstance(menu_entry, list):
2037 menu_entry = [menu_entry]
2038 menu_entry = [menu_entry]
2038
2039
2039 if selected in menu_entry:
2040 if selected in menu_entry:
2040 return "active"
2041 return "active"
@@ -1,832 +1,833 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24 import datetime
24 import datetime
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28 import collections
28 import collections
29
29
30 from pyramid.threadlocal import get_current_registry, get_current_request
30 from pyramid.threadlocal import get_current_registry, get_current_request
31 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.expression import null
32 from sqlalchemy.sql.functions import coalesce
32 from sqlalchemy.sql.functions import coalesce
33
33
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
37 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
38 from rhodecode.model.db import (
39 from rhodecode.model.db import (
39 ChangesetComment,
40 ChangesetComment,
40 User,
41 User,
41 Notification,
42 Notification,
42 PullRequest,
43 PullRequest,
43 AttributeDict,
44 AttributeDict,
44 ChangesetCommentHistory,
45 ChangesetCommentHistory,
45 )
46 )
46 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
48 from rhodecode.model.settings import VcsSettingsModel
49 from rhodecode.model.settings import VcsSettingsModel
49 from rhodecode.model.notification import EmailNotificationModel
50 from rhodecode.model.notification import EmailNotificationModel
50 from rhodecode.model.validation_schema.schemas import comment_schema
51 from rhodecode.model.validation_schema.schemas import comment_schema
51
52
52
53
53 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
54
55
55
56
56 class CommentsModel(BaseModel):
57 class CommentsModel(BaseModel):
57
58
58 cls = ChangesetComment
59 cls = ChangesetComment
59
60
60 DIFF_CONTEXT_BEFORE = 3
61 DIFF_CONTEXT_BEFORE = 3
61 DIFF_CONTEXT_AFTER = 3
62 DIFF_CONTEXT_AFTER = 3
62
63
63 def __get_commit_comment(self, changeset_comment):
64 def __get_commit_comment(self, changeset_comment):
64 return self._get_instance(ChangesetComment, changeset_comment)
65 return self._get_instance(ChangesetComment, changeset_comment)
65
66
66 def __get_pull_request(self, pull_request):
67 def __get_pull_request(self, pull_request):
67 return self._get_instance(PullRequest, pull_request)
68 return self._get_instance(PullRequest, pull_request)
68
69
69 def _extract_mentions(self, s):
70 def _extract_mentions(self, s):
70 user_objects = []
71 user_objects = []
71 for username in extract_mentioned_users(s):
72 for username in extract_mentioned_users(s):
72 user_obj = User.get_by_username(username, case_insensitive=True)
73 user_obj = User.get_by_username(username, case_insensitive=True)
73 if user_obj:
74 if user_obj:
74 user_objects.append(user_obj)
75 user_objects.append(user_obj)
75 return user_objects
76 return user_objects
76
77
77 def _get_renderer(self, global_renderer='rst', request=None):
78 def _get_renderer(self, global_renderer='rst', request=None):
78 request = request or get_current_request()
79 request = request or get_current_request()
79
80
80 try:
81 try:
81 global_renderer = request.call_context.visual.default_renderer
82 global_renderer = request.call_context.visual.default_renderer
82 except AttributeError:
83 except AttributeError:
83 log.debug("Renderer not set, falling back "
84 log.debug("Renderer not set, falling back "
84 "to default renderer '%s'", global_renderer)
85 "to default renderer '%s'", global_renderer)
85 except Exception:
86 except Exception:
86 log.error(traceback.format_exc())
87 log.error(traceback.format_exc())
87 return global_renderer
88 return global_renderer
88
89
89 def aggregate_comments(self, comments, versions, show_version, inline=False):
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
90 # group by versions, and count until, and display objects
91 # group by versions, and count until, and display objects
91
92
92 comment_groups = collections.defaultdict(list)
93 comment_groups = collections.defaultdict(list)
93 [comment_groups[
94 [comment_groups[
94 _co.pull_request_version_id].append(_co) for _co in comments]
95 _co.pull_request_version_id].append(_co) for _co in comments]
95
96
96 def yield_comments(pos):
97 def yield_comments(pos):
97 for co in comment_groups[pos]:
98 for co in comment_groups[pos]:
98 yield co
99 yield co
99
100
100 comment_versions = collections.defaultdict(
101 comment_versions = collections.defaultdict(
101 lambda: collections.defaultdict(list))
102 lambda: collections.defaultdict(list))
102 prev_prvid = -1
103 prev_prvid = -1
103 # fake last entry with None, to aggregate on "latest" version which
104 # fake last entry with None, to aggregate on "latest" version which
104 # doesn't have an pull_request_version_id
105 # doesn't have an pull_request_version_id
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 prvid = ver.pull_request_version_id
107 prvid = ver.pull_request_version_id
107 if prev_prvid == -1:
108 if prev_prvid == -1:
108 prev_prvid = prvid
109 prev_prvid = prvid
109
110
110 for co in yield_comments(prvid):
111 for co in yield_comments(prvid):
111 comment_versions[prvid]['at'].append(co)
112 comment_versions[prvid]['at'].append(co)
112
113
113 # save until
114 # save until
114 current = comment_versions[prvid]['at']
115 current = comment_versions[prvid]['at']
115 prev_until = comment_versions[prev_prvid]['until']
116 prev_until = comment_versions[prev_prvid]['until']
116 cur_until = prev_until + current
117 cur_until = prev_until + current
117 comment_versions[prvid]['until'].extend(cur_until)
118 comment_versions[prvid]['until'].extend(cur_until)
118
119
119 # save outdated
120 # save outdated
120 if inline:
121 if inline:
121 outdated = [x for x in cur_until
122 outdated = [x for x in cur_until
122 if x.outdated_at_version(show_version)]
123 if x.outdated_at_version(show_version)]
123 else:
124 else:
124 outdated = [x for x in cur_until
125 outdated = [x for x in cur_until
125 if x.older_than_version(show_version)]
126 if x.older_than_version(show_version)]
126 display = [x for x in cur_until if x not in outdated]
127 display = [x for x in cur_until if x not in outdated]
127
128
128 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['display'] = display
130 comment_versions[prvid]['display'] = display
130
131
131 prev_prvid = prvid
132 prev_prvid = prvid
132
133
133 return comment_versions
134 return comment_versions
134
135
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 qry = Session().query(ChangesetComment) \
137 qry = Session().query(ChangesetComment) \
137 .filter(ChangesetComment.repo == repo)
138 .filter(ChangesetComment.repo == repo)
138
139
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141
142
142 if user:
143 if user:
143 user = self._get_user(user)
144 user = self._get_user(user)
144 if user:
145 if user:
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146
147
147 if commit_id:
148 if commit_id:
148 qry = qry.filter(ChangesetComment.revision == commit_id)
149 qry = qry.filter(ChangesetComment.revision == commit_id)
149
150
150 qry = qry.order_by(ChangesetComment.created_on)
151 qry = qry.order_by(ChangesetComment.created_on)
151 return qry.all()
152 return qry.all()
152
153
153 def get_repository_unresolved_todos(self, repo):
154 def get_repository_unresolved_todos(self, repo):
154 todos = Session().query(ChangesetComment) \
155 todos = Session().query(ChangesetComment) \
155 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.comment_type
158 .filter(ChangesetComment.comment_type
158 == ChangesetComment.COMMENT_TYPE_TODO)
159 == ChangesetComment.COMMENT_TYPE_TODO)
159 todos = todos.all()
160 todos = todos.all()
160
161
161 return todos
162 return todos
162
163
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
164
165
165 todos = Session().query(ChangesetComment) \
166 todos = Session().query(ChangesetComment) \
166 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.comment_type
169 .filter(ChangesetComment.comment_type
169 == ChangesetComment.COMMENT_TYPE_TODO)
170 == ChangesetComment.COMMENT_TYPE_TODO)
170
171
171 if not show_outdated:
172 if not show_outdated:
172 todos = todos.filter(
173 todos = todos.filter(
173 coalesce(ChangesetComment.display_state, '') !=
174 coalesce(ChangesetComment.display_state, '') !=
174 ChangesetComment.COMMENT_OUTDATED)
175 ChangesetComment.COMMENT_OUTDATED)
175
176
176 todos = todos.all()
177 todos = todos.all()
177
178
178 return todos
179 return todos
179
180
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
181 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
181
182
182 todos = Session().query(ChangesetComment) \
183 todos = Session().query(ChangesetComment) \
183 .filter(ChangesetComment.pull_request == pull_request) \
184 .filter(ChangesetComment.pull_request == pull_request) \
184 .filter(ChangesetComment.resolved_by != None) \
185 .filter(ChangesetComment.resolved_by != None) \
185 .filter(ChangesetComment.comment_type
186 .filter(ChangesetComment.comment_type
186 == ChangesetComment.COMMENT_TYPE_TODO)
187 == ChangesetComment.COMMENT_TYPE_TODO)
187
188
188 if not show_outdated:
189 if not show_outdated:
189 todos = todos.filter(
190 todos = todos.filter(
190 coalesce(ChangesetComment.display_state, '') !=
191 coalesce(ChangesetComment.display_state, '') !=
191 ChangesetComment.COMMENT_OUTDATED)
192 ChangesetComment.COMMENT_OUTDATED)
192
193
193 todos = todos.all()
194 todos = todos.all()
194
195
195 return todos
196 return todos
196
197
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
198 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
198
199
199 todos = Session().query(ChangesetComment) \
200 todos = Session().query(ChangesetComment) \
200 .filter(ChangesetComment.revision == commit_id) \
201 .filter(ChangesetComment.revision == commit_id) \
201 .filter(ChangesetComment.resolved_by == None) \
202 .filter(ChangesetComment.resolved_by == None) \
202 .filter(ChangesetComment.comment_type
203 .filter(ChangesetComment.comment_type
203 == ChangesetComment.COMMENT_TYPE_TODO)
204 == ChangesetComment.COMMENT_TYPE_TODO)
204
205
205 if not show_outdated:
206 if not show_outdated:
206 todos = todos.filter(
207 todos = todos.filter(
207 coalesce(ChangesetComment.display_state, '') !=
208 coalesce(ChangesetComment.display_state, '') !=
208 ChangesetComment.COMMENT_OUTDATED)
209 ChangesetComment.COMMENT_OUTDATED)
209
210
210 todos = todos.all()
211 todos = todos.all()
211
212
212 return todos
213 return todos
213
214
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
215 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
215
216
216 todos = Session().query(ChangesetComment) \
217 todos = Session().query(ChangesetComment) \
217 .filter(ChangesetComment.revision == commit_id) \
218 .filter(ChangesetComment.revision == commit_id) \
218 .filter(ChangesetComment.resolved_by != None) \
219 .filter(ChangesetComment.resolved_by != None) \
219 .filter(ChangesetComment.comment_type
220 .filter(ChangesetComment.comment_type
220 == ChangesetComment.COMMENT_TYPE_TODO)
221 == ChangesetComment.COMMENT_TYPE_TODO)
221
222
222 if not show_outdated:
223 if not show_outdated:
223 todos = todos.filter(
224 todos = todos.filter(
224 coalesce(ChangesetComment.display_state, '') !=
225 coalesce(ChangesetComment.display_state, '') !=
225 ChangesetComment.COMMENT_OUTDATED)
226 ChangesetComment.COMMENT_OUTDATED)
226
227
227 todos = todos.all()
228 todos = todos.all()
228
229
229 return todos
230 return todos
230
231
231 def _log_audit_action(self, action, action_data, auth_user, comment):
232 def _log_audit_action(self, action, action_data, auth_user, comment):
232 audit_logger.store(
233 audit_logger.store(
233 action=action,
234 action=action,
234 action_data=action_data,
235 action_data=action_data,
235 user=auth_user,
236 user=auth_user,
236 repo=comment.repo)
237 repo=comment.repo)
237
238
238 def create(self, text, repo, user, commit_id=None, pull_request=None,
239 def create(self, text, repo, user, commit_id=None, pull_request=None,
239 f_path=None, line_no=None, status_change=None,
240 f_path=None, line_no=None, status_change=None,
240 status_change_type=None, comment_type=None,
241 status_change_type=None, comment_type=None,
241 resolves_comment_id=None, closing_pr=False, send_email=True,
242 resolves_comment_id=None, closing_pr=False, send_email=True,
242 renderer=None, auth_user=None, extra_recipients=None):
243 renderer=None, auth_user=None, extra_recipients=None):
243 """
244 """
244 Creates new comment for commit or pull request.
245 Creates new comment for commit or pull request.
245 IF status_change is not none this comment is associated with a
246 IF status_change is not none this comment is associated with a
246 status change of commit or commit associated with pull request
247 status change of commit or commit associated with pull request
247
248
248 :param text:
249 :param text:
249 :param repo:
250 :param repo:
250 :param user:
251 :param user:
251 :param commit_id:
252 :param commit_id:
252 :param pull_request:
253 :param pull_request:
253 :param f_path:
254 :param f_path:
254 :param line_no:
255 :param line_no:
255 :param status_change: Label for status change
256 :param status_change: Label for status change
256 :param comment_type: Type of comment
257 :param comment_type: Type of comment
257 :param resolves_comment_id: id of comment which this one will resolve
258 :param resolves_comment_id: id of comment which this one will resolve
258 :param status_change_type: type of status change
259 :param status_change_type: type of status change
259 :param closing_pr:
260 :param closing_pr:
260 :param send_email:
261 :param send_email:
261 :param renderer: pick renderer for this comment
262 :param renderer: pick renderer for this comment
262 :param auth_user: current authenticated user calling this method
263 :param auth_user: current authenticated user calling this method
263 :param extra_recipients: list of extra users to be added to recipients
264 :param extra_recipients: list of extra users to be added to recipients
264 """
265 """
265
266
266 if not text:
267 if not text:
267 log.warning('Missing text for comment, skipping...')
268 log.warning('Missing text for comment, skipping...')
268 return
269 return
269 request = get_current_request()
270 request = get_current_request()
270 _ = request.translate
271 _ = request.translate
271
272
272 if not renderer:
273 if not renderer:
273 renderer = self._get_renderer(request=request)
274 renderer = self._get_renderer(request=request)
274
275
275 repo = self._get_repo(repo)
276 repo = self._get_repo(repo)
276 user = self._get_user(user)
277 user = self._get_user(user)
277 auth_user = auth_user or user
278 auth_user = auth_user or user
278
279
279 schema = comment_schema.CommentSchema()
280 schema = comment_schema.CommentSchema()
280 validated_kwargs = schema.deserialize(dict(
281 validated_kwargs = schema.deserialize(dict(
281 comment_body=text,
282 comment_body=text,
282 comment_type=comment_type,
283 comment_type=comment_type,
283 comment_file=f_path,
284 comment_file=f_path,
284 comment_line=line_no,
285 comment_line=line_no,
285 renderer_type=renderer,
286 renderer_type=renderer,
286 status_change=status_change_type,
287 status_change=status_change_type,
287 resolves_comment_id=resolves_comment_id,
288 resolves_comment_id=resolves_comment_id,
288 repo=repo.repo_id,
289 repo=repo.repo_id,
289 user=user.user_id,
290 user=user.user_id,
290 ))
291 ))
291
292
292 comment = ChangesetComment()
293 comment = ChangesetComment()
293 comment.renderer = validated_kwargs['renderer_type']
294 comment.renderer = validated_kwargs['renderer_type']
294 comment.text = validated_kwargs['comment_body']
295 comment.text = validated_kwargs['comment_body']
295 comment.f_path = validated_kwargs['comment_file']
296 comment.f_path = validated_kwargs['comment_file']
296 comment.line_no = validated_kwargs['comment_line']
297 comment.line_no = validated_kwargs['comment_line']
297 comment.comment_type = validated_kwargs['comment_type']
298 comment.comment_type = validated_kwargs['comment_type']
298
299
299 comment.repo = repo
300 comment.repo = repo
300 comment.author = user
301 comment.author = user
301 resolved_comment = self.__get_commit_comment(
302 resolved_comment = self.__get_commit_comment(
302 validated_kwargs['resolves_comment_id'])
303 validated_kwargs['resolves_comment_id'])
303 # check if the comment actually belongs to this PR
304 # check if the comment actually belongs to this PR
304 if resolved_comment and resolved_comment.pull_request and \
305 if resolved_comment and resolved_comment.pull_request and \
305 resolved_comment.pull_request != pull_request:
306 resolved_comment.pull_request != pull_request:
306 log.warning('Comment tried to resolved unrelated todo comment: %s',
307 log.warning('Comment tried to resolved unrelated todo comment: %s',
307 resolved_comment)
308 resolved_comment)
308 # comment not bound to this pull request, forbid
309 # comment not bound to this pull request, forbid
309 resolved_comment = None
310 resolved_comment = None
310
311
311 elif resolved_comment and resolved_comment.repo and \
312 elif resolved_comment and resolved_comment.repo and \
312 resolved_comment.repo != repo:
313 resolved_comment.repo != repo:
313 log.warning('Comment tried to resolved unrelated todo comment: %s',
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
314 resolved_comment)
315 resolved_comment)
315 # comment not bound to this repo, forbid
316 # comment not bound to this repo, forbid
316 resolved_comment = None
317 resolved_comment = None
317
318
318 comment.resolved_comment = resolved_comment
319 comment.resolved_comment = resolved_comment
319
320
320 pull_request_id = pull_request
321 pull_request_id = pull_request
321
322
322 commit_obj = None
323 commit_obj = None
323 pull_request_obj = None
324 pull_request_obj = None
324
325
325 if commit_id:
326 if commit_id:
326 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
327 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
327 # do a lookup, so we don't pass something bad here
328 # do a lookup, so we don't pass something bad here
328 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
329 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
329 comment.revision = commit_obj.raw_id
330 comment.revision = commit_obj.raw_id
330
331
331 elif pull_request_id:
332 elif pull_request_id:
332 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
333 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
333 pull_request_obj = self.__get_pull_request(pull_request_id)
334 pull_request_obj = self.__get_pull_request(pull_request_id)
334 comment.pull_request = pull_request_obj
335 comment.pull_request = pull_request_obj
335 else:
336 else:
336 raise Exception('Please specify commit or pull_request_id')
337 raise Exception('Please specify commit or pull_request_id')
337
338
338 Session().add(comment)
339 Session().add(comment)
339 Session().flush()
340 Session().flush()
340 kwargs = {
341 kwargs = {
341 'user': user,
342 'user': user,
342 'renderer_type': renderer,
343 'renderer_type': renderer,
343 'repo_name': repo.repo_name,
344 'repo_name': repo.repo_name,
344 'status_change': status_change,
345 'status_change': status_change,
345 'status_change_type': status_change_type,
346 'status_change_type': status_change_type,
346 'comment_body': text,
347 'comment_body': text,
347 'comment_file': f_path,
348 'comment_file': f_path,
348 'comment_line': line_no,
349 'comment_line': line_no,
349 'comment_type': comment_type or 'note',
350 'comment_type': comment_type or 'note',
350 'comment_id': comment.comment_id
351 'comment_id': comment.comment_id
351 }
352 }
352
353
353 if commit_obj:
354 if commit_obj:
354 recipients = ChangesetComment.get_users(
355 recipients = ChangesetComment.get_users(
355 revision=commit_obj.raw_id)
356 revision=commit_obj.raw_id)
356 # add commit author if it's in RhodeCode system
357 # add commit author if it's in RhodeCode system
357 cs_author = User.get_from_cs_author(commit_obj.author)
358 cs_author = User.get_from_cs_author(commit_obj.author)
358 if not cs_author:
359 if not cs_author:
359 # use repo owner if we cannot extract the author correctly
360 # use repo owner if we cannot extract the author correctly
360 cs_author = repo.user
361 cs_author = repo.user
361 recipients += [cs_author]
362 recipients += [cs_author]
362
363
363 commit_comment_url = self.get_url(comment, request=request)
364 commit_comment_url = self.get_url(comment, request=request)
364 commit_comment_reply_url = self.get_url(
365 commit_comment_reply_url = self.get_url(
365 comment, request=request,
366 comment, request=request,
366 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
367 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
367
368
368 target_repo_url = h.link_to(
369 target_repo_url = h.link_to(
369 repo.repo_name,
370 repo.repo_name,
370 h.route_url('repo_summary', repo_name=repo.repo_name))
371 h.route_url('repo_summary', repo_name=repo.repo_name))
371
372
372 # commit specifics
373 # commit specifics
373 kwargs.update({
374 kwargs.update({
374 'commit': commit_obj,
375 'commit': commit_obj,
375 'commit_message': commit_obj.message,
376 'commit_message': commit_obj.message,
376 'commit_target_repo_url': target_repo_url,
377 'commit_target_repo_url': target_repo_url,
377 'commit_comment_url': commit_comment_url,
378 'commit_comment_url': commit_comment_url,
378 'commit_comment_reply_url': commit_comment_reply_url
379 'commit_comment_reply_url': commit_comment_reply_url
379 })
380 })
380
381
381 elif pull_request_obj:
382 elif pull_request_obj:
382 # get the current participants of this pull request
383 # get the current participants of this pull request
383 recipients = ChangesetComment.get_users(
384 recipients = ChangesetComment.get_users(
384 pull_request_id=pull_request_obj.pull_request_id)
385 pull_request_id=pull_request_obj.pull_request_id)
385 # add pull request author
386 # add pull request author
386 recipients += [pull_request_obj.author]
387 recipients += [pull_request_obj.author]
387
388
388 # add the reviewers to notification
389 # add the reviewers to notification
389 recipients += [x.user for x in pull_request_obj.reviewers]
390 recipients += [x.user for x in pull_request_obj.reviewers]
390
391
391 pr_target_repo = pull_request_obj.target_repo
392 pr_target_repo = pull_request_obj.target_repo
392 pr_source_repo = pull_request_obj.source_repo
393 pr_source_repo = pull_request_obj.source_repo
393
394
394 pr_comment_url = self.get_url(comment, request=request)
395 pr_comment_url = self.get_url(comment, request=request)
395 pr_comment_reply_url = self.get_url(
396 pr_comment_reply_url = self.get_url(
396 comment, request=request,
397 comment, request=request,
397 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
398 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
398
399
399 pr_url = h.route_url(
400 pr_url = h.route_url(
400 'pullrequest_show',
401 'pullrequest_show',
401 repo_name=pr_target_repo.repo_name,
402 repo_name=pr_target_repo.repo_name,
402 pull_request_id=pull_request_obj.pull_request_id, )
403 pull_request_id=pull_request_obj.pull_request_id, )
403
404
404 # set some variables for email notification
405 # set some variables for email notification
405 pr_target_repo_url = h.route_url(
406 pr_target_repo_url = h.route_url(
406 'repo_summary', repo_name=pr_target_repo.repo_name)
407 'repo_summary', repo_name=pr_target_repo.repo_name)
407
408
408 pr_source_repo_url = h.route_url(
409 pr_source_repo_url = h.route_url(
409 'repo_summary', repo_name=pr_source_repo.repo_name)
410 'repo_summary', repo_name=pr_source_repo.repo_name)
410
411
411 # pull request specifics
412 # pull request specifics
412 kwargs.update({
413 kwargs.update({
413 'pull_request': pull_request_obj,
414 'pull_request': pull_request_obj,
414 'pr_id': pull_request_obj.pull_request_id,
415 'pr_id': pull_request_obj.pull_request_id,
415 'pull_request_url': pr_url,
416 'pull_request_url': pr_url,
416 'pull_request_target_repo': pr_target_repo,
417 'pull_request_target_repo': pr_target_repo,
417 'pull_request_target_repo_url': pr_target_repo_url,
418 'pull_request_target_repo_url': pr_target_repo_url,
418 'pull_request_source_repo': pr_source_repo,
419 'pull_request_source_repo': pr_source_repo,
419 'pull_request_source_repo_url': pr_source_repo_url,
420 'pull_request_source_repo_url': pr_source_repo_url,
420 'pr_comment_url': pr_comment_url,
421 'pr_comment_url': pr_comment_url,
421 'pr_comment_reply_url': pr_comment_reply_url,
422 'pr_comment_reply_url': pr_comment_reply_url,
422 'pr_closing': closing_pr,
423 'pr_closing': closing_pr,
423 })
424 })
424
425
425 recipients += [self._get_user(u) for u in (extra_recipients or [])]
426 recipients += [self._get_user(u) for u in (extra_recipients or [])]
426
427
427 if send_email:
428 if send_email:
428 # pre-generate the subject for notification itself
429 # pre-generate the subject for notification itself
429 (subject,
430 (subject,
430 _h, _e, # we don't care about those
431 _h, _e, # we don't care about those
431 body_plaintext) = EmailNotificationModel().render_email(
432 body_plaintext) = EmailNotificationModel().render_email(
432 notification_type, **kwargs)
433 notification_type, **kwargs)
433
434
434 mention_recipients = set(
435 mention_recipients = set(
435 self._extract_mentions(text)).difference(recipients)
436 self._extract_mentions(text)).difference(recipients)
436
437
437 # create notification objects, and emails
438 # create notification objects, and emails
438 NotificationModel().create(
439 NotificationModel().create(
439 created_by=user,
440 created_by=user,
440 notification_subject=subject,
441 notification_subject=subject,
441 notification_body=body_plaintext,
442 notification_body=body_plaintext,
442 notification_type=notification_type,
443 notification_type=notification_type,
443 recipients=recipients,
444 recipients=recipients,
444 mention_recipients=mention_recipients,
445 mention_recipients=mention_recipients,
445 email_kwargs=kwargs,
446 email_kwargs=kwargs,
446 )
447 )
447
448
448 Session().flush()
449 Session().flush()
449 if comment.pull_request:
450 if comment.pull_request:
450 action = 'repo.pull_request.comment.create'
451 action = 'repo.pull_request.comment.create'
451 else:
452 else:
452 action = 'repo.commit.comment.create'
453 action = 'repo.commit.comment.create'
453
454
454 comment_data = comment.get_api_data()
455 comment_data = comment.get_api_data()
455 self._log_audit_action(
456 self._log_audit_action(
456 action, {'data': comment_data}, auth_user, comment)
457 action, {'data': comment_data}, auth_user, comment)
457
458
458 msg_url = ''
459 msg_url = ''
459 channel = None
460 channel = None
460 if commit_obj:
461 if commit_obj:
461 msg_url = commit_comment_url
462 msg_url = commit_comment_url
462 repo_name = repo.repo_name
463 repo_name = repo.repo_name
463 channel = u'/repo${}$/commit/{}'.format(
464 channel = u'/repo${}$/commit/{}'.format(
464 repo_name,
465 repo_name,
465 commit_obj.raw_id
466 commit_obj.raw_id
466 )
467 )
467 elif pull_request_obj:
468 elif pull_request_obj:
468 msg_url = pr_comment_url
469 msg_url = pr_comment_url
469 repo_name = pr_target_repo.repo_name
470 repo_name = pr_target_repo.repo_name
470 channel = u'/repo${}$/pr/{}'.format(
471 channel = u'/repo${}$/pr/{}'.format(
471 repo_name,
472 repo_name,
472 pull_request_id
473 pull_request_id
473 )
474 )
474
475
475 message = '<strong>{}</strong> {} - ' \
476 message = '<strong>{}</strong> {} - ' \
476 '<a onclick="window.location=\'{}\';' \
477 '<a onclick="window.location=\'{}\';' \
477 'window.location.reload()">' \
478 'window.location.reload()">' \
478 '<strong>{}</strong></a>'
479 '<strong>{}</strong></a>'
479 message = message.format(
480 message = message.format(
480 user.username, _('made a comment'), msg_url,
481 user.username, _('made a comment'), msg_url,
481 _('Show it now'))
482 _('Show it now'))
482
483
483 channelstream.post_message(
484 channelstream.post_message(
484 channel, message, user.username,
485 channel, message, user.username,
485 registry=get_current_registry())
486 registry=get_current_registry())
486
487
487 return comment
488 return comment
488
489
489 def edit(self, comment_id, text, auth_user, version):
490 def edit(self, comment_id, text, auth_user, version):
490 """
491 """
491 Change existing comment for commit or pull request.
492 Change existing comment for commit or pull request.
492
493
493 :param comment_id:
494 :param comment_id:
494 :param text:
495 :param text:
495 :param auth_user: current authenticated user calling this method
496 :param auth_user: current authenticated user calling this method
496 :param version: last comment version
497 :param version: last comment version
497 """
498 """
498 if not text:
499 if not text:
499 log.warning('Missing text for comment, skipping...')
500 log.warning('Missing text for comment, skipping...')
500 return
501 return
501
502
502 comment = ChangesetComment.get(comment_id)
503 comment = ChangesetComment.get(comment_id)
503 old_comment_text = comment.text
504 old_comment_text = comment.text
504 comment.text = text
505 comment.text = text
505 comment.modified_at = datetime.datetime.now()
506 comment.modified_at = datetime.datetime.now()
506
507
507 comment_version = ChangesetCommentHistory.get_version(comment_id)
508 comment_version = ChangesetCommentHistory.get_version(comment_id)
508 if (comment_version - version) != 1:
509 if (comment_version - version) != 1:
509 log.warning(
510 log.warning(
510 'Version mismatch, skipping... '
511 'Version mismatch comment_version {} submitted {}, skipping'.format(
511 'version {} but should be {}'.format(
512 (version - 1),
513 comment_version,
512 comment_version,
513 version
514 )
514 )
515 )
515 )
516 return
516 raise CommentVersionMismatch()
517
517 comment_history = ChangesetCommentHistory()
518 comment_history = ChangesetCommentHistory()
518 comment_history.comment_id = comment_id
519 comment_history.comment_id = comment_id
519 comment_history.version = comment_version
520 comment_history.version = comment_version
520 comment_history.created_by_user_id = auth_user.user_id
521 comment_history.created_by_user_id = auth_user.user_id
521 comment_history.text = old_comment_text
522 comment_history.text = old_comment_text
522 # TODO add email notification
523 # TODO add email notification
523 Session().add(comment_history)
524 Session().add(comment_history)
524 Session().add(comment)
525 Session().add(comment)
525 Session().flush()
526 Session().flush()
526
527
527 if comment.pull_request:
528 if comment.pull_request:
528 action = 'repo.pull_request.comment.edit'
529 action = 'repo.pull_request.comment.edit'
529 else:
530 else:
530 action = 'repo.commit.comment.edit'
531 action = 'repo.commit.comment.edit'
531
532
532 comment_data = comment.get_api_data()
533 comment_data = comment.get_api_data()
533 comment_data['old_comment_text'] = old_comment_text
534 comment_data['old_comment_text'] = old_comment_text
534 self._log_audit_action(
535 self._log_audit_action(
535 action, {'data': comment_data}, auth_user, comment)
536 action, {'data': comment_data}, auth_user, comment)
536
537
537 return comment_history
538 return comment_history
538
539
539 def delete(self, comment, auth_user):
540 def delete(self, comment, auth_user):
540 """
541 """
541 Deletes given comment
542 Deletes given comment
542 """
543 """
543 comment = self.__get_commit_comment(comment)
544 comment = self.__get_commit_comment(comment)
544 old_data = comment.get_api_data()
545 old_data = comment.get_api_data()
545 Session().delete(comment)
546 Session().delete(comment)
546
547
547 if comment.pull_request:
548 if comment.pull_request:
548 action = 'repo.pull_request.comment.delete'
549 action = 'repo.pull_request.comment.delete'
549 else:
550 else:
550 action = 'repo.commit.comment.delete'
551 action = 'repo.commit.comment.delete'
551
552
552 self._log_audit_action(
553 self._log_audit_action(
553 action, {'old_data': old_data}, auth_user, comment)
554 action, {'old_data': old_data}, auth_user, comment)
554
555
555 return comment
556 return comment
556
557
557 def get_all_comments(self, repo_id, revision=None, pull_request=None):
558 def get_all_comments(self, repo_id, revision=None, pull_request=None):
558 q = ChangesetComment.query()\
559 q = ChangesetComment.query()\
559 .filter(ChangesetComment.repo_id == repo_id)
560 .filter(ChangesetComment.repo_id == repo_id)
560 if revision:
561 if revision:
561 q = q.filter(ChangesetComment.revision == revision)
562 q = q.filter(ChangesetComment.revision == revision)
562 elif pull_request:
563 elif pull_request:
563 pull_request = self.__get_pull_request(pull_request)
564 pull_request = self.__get_pull_request(pull_request)
564 q = q.filter(ChangesetComment.pull_request == pull_request)
565 q = q.filter(ChangesetComment.pull_request == pull_request)
565 else:
566 else:
566 raise Exception('Please specify commit or pull_request')
567 raise Exception('Please specify commit or pull_request')
567 q = q.order_by(ChangesetComment.created_on)
568 q = q.order_by(ChangesetComment.created_on)
568 return q.all()
569 return q.all()
569
570
570 def get_url(self, comment, request=None, permalink=False, anchor=None):
571 def get_url(self, comment, request=None, permalink=False, anchor=None):
571 if not request:
572 if not request:
572 request = get_current_request()
573 request = get_current_request()
573
574
574 comment = self.__get_commit_comment(comment)
575 comment = self.__get_commit_comment(comment)
575 if anchor is None:
576 if anchor is None:
576 anchor = 'comment-{}'.format(comment.comment_id)
577 anchor = 'comment-{}'.format(comment.comment_id)
577
578
578 if comment.pull_request:
579 if comment.pull_request:
579 pull_request = comment.pull_request
580 pull_request = comment.pull_request
580 if permalink:
581 if permalink:
581 return request.route_url(
582 return request.route_url(
582 'pull_requests_global',
583 'pull_requests_global',
583 pull_request_id=pull_request.pull_request_id,
584 pull_request_id=pull_request.pull_request_id,
584 _anchor=anchor)
585 _anchor=anchor)
585 else:
586 else:
586 return request.route_url(
587 return request.route_url(
587 'pullrequest_show',
588 'pullrequest_show',
588 repo_name=safe_str(pull_request.target_repo.repo_name),
589 repo_name=safe_str(pull_request.target_repo.repo_name),
589 pull_request_id=pull_request.pull_request_id,
590 pull_request_id=pull_request.pull_request_id,
590 _anchor=anchor)
591 _anchor=anchor)
591
592
592 else:
593 else:
593 repo = comment.repo
594 repo = comment.repo
594 commit_id = comment.revision
595 commit_id = comment.revision
595
596
596 if permalink:
597 if permalink:
597 return request.route_url(
598 return request.route_url(
598 'repo_commit', repo_name=safe_str(repo.repo_id),
599 'repo_commit', repo_name=safe_str(repo.repo_id),
599 commit_id=commit_id,
600 commit_id=commit_id,
600 _anchor=anchor)
601 _anchor=anchor)
601
602
602 else:
603 else:
603 return request.route_url(
604 return request.route_url(
604 'repo_commit', repo_name=safe_str(repo.repo_name),
605 'repo_commit', repo_name=safe_str(repo.repo_name),
605 commit_id=commit_id,
606 commit_id=commit_id,
606 _anchor=anchor)
607 _anchor=anchor)
607
608
608 def get_comments(self, repo_id, revision=None, pull_request=None):
609 def get_comments(self, repo_id, revision=None, pull_request=None):
609 """
610 """
610 Gets main comments based on revision or pull_request_id
611 Gets main comments based on revision or pull_request_id
611
612
612 :param repo_id:
613 :param repo_id:
613 :param revision:
614 :param revision:
614 :param pull_request:
615 :param pull_request:
615 """
616 """
616
617
617 q = ChangesetComment.query()\
618 q = ChangesetComment.query()\
618 .filter(ChangesetComment.repo_id == repo_id)\
619 .filter(ChangesetComment.repo_id == repo_id)\
619 .filter(ChangesetComment.line_no == None)\
620 .filter(ChangesetComment.line_no == None)\
620 .filter(ChangesetComment.f_path == None)
621 .filter(ChangesetComment.f_path == None)
621 if revision:
622 if revision:
622 q = q.filter(ChangesetComment.revision == revision)
623 q = q.filter(ChangesetComment.revision == revision)
623 elif pull_request:
624 elif pull_request:
624 pull_request = self.__get_pull_request(pull_request)
625 pull_request = self.__get_pull_request(pull_request)
625 q = q.filter(ChangesetComment.pull_request == pull_request)
626 q = q.filter(ChangesetComment.pull_request == pull_request)
626 else:
627 else:
627 raise Exception('Please specify commit or pull_request')
628 raise Exception('Please specify commit or pull_request')
628 q = q.order_by(ChangesetComment.created_on)
629 q = q.order_by(ChangesetComment.created_on)
629 return q.all()
630 return q.all()
630
631
631 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
632 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
632 q = self._get_inline_comments_query(repo_id, revision, pull_request)
633 q = self._get_inline_comments_query(repo_id, revision, pull_request)
633 return self._group_comments_by_path_and_line_number(q)
634 return self._group_comments_by_path_and_line_number(q)
634
635
635 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
636 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
636 version=None):
637 version=None):
637 inline_cnt = 0
638 inline_cnt = 0
638 for fname, per_line_comments in inline_comments.iteritems():
639 for fname, per_line_comments in inline_comments.iteritems():
639 for lno, comments in per_line_comments.iteritems():
640 for lno, comments in per_line_comments.iteritems():
640 for comm in comments:
641 for comm in comments:
641 if not comm.outdated_at_version(version) and skip_outdated:
642 if not comm.outdated_at_version(version) and skip_outdated:
642 inline_cnt += 1
643 inline_cnt += 1
643
644
644 return inline_cnt
645 return inline_cnt
645
646
646 def get_outdated_comments(self, repo_id, pull_request):
647 def get_outdated_comments(self, repo_id, pull_request):
647 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
648 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
648 # of a pull request.
649 # of a pull request.
649 q = self._all_inline_comments_of_pull_request(pull_request)
650 q = self._all_inline_comments_of_pull_request(pull_request)
650 q = q.filter(
651 q = q.filter(
651 ChangesetComment.display_state ==
652 ChangesetComment.display_state ==
652 ChangesetComment.COMMENT_OUTDATED
653 ChangesetComment.COMMENT_OUTDATED
653 ).order_by(ChangesetComment.comment_id.asc())
654 ).order_by(ChangesetComment.comment_id.asc())
654
655
655 return self._group_comments_by_path_and_line_number(q)
656 return self._group_comments_by_path_and_line_number(q)
656
657
657 def _get_inline_comments_query(self, repo_id, revision, pull_request):
658 def _get_inline_comments_query(self, repo_id, revision, pull_request):
658 # TODO: johbo: Split this into two methods: One for PR and one for
659 # TODO: johbo: Split this into two methods: One for PR and one for
659 # commit.
660 # commit.
660 if revision:
661 if revision:
661 q = Session().query(ChangesetComment).filter(
662 q = Session().query(ChangesetComment).filter(
662 ChangesetComment.repo_id == repo_id,
663 ChangesetComment.repo_id == repo_id,
663 ChangesetComment.line_no != null(),
664 ChangesetComment.line_no != null(),
664 ChangesetComment.f_path != null(),
665 ChangesetComment.f_path != null(),
665 ChangesetComment.revision == revision)
666 ChangesetComment.revision == revision)
666
667
667 elif pull_request:
668 elif pull_request:
668 pull_request = self.__get_pull_request(pull_request)
669 pull_request = self.__get_pull_request(pull_request)
669 if not CommentsModel.use_outdated_comments(pull_request):
670 if not CommentsModel.use_outdated_comments(pull_request):
670 q = self._visible_inline_comments_of_pull_request(pull_request)
671 q = self._visible_inline_comments_of_pull_request(pull_request)
671 else:
672 else:
672 q = self._all_inline_comments_of_pull_request(pull_request)
673 q = self._all_inline_comments_of_pull_request(pull_request)
673
674
674 else:
675 else:
675 raise Exception('Please specify commit or pull_request_id')
676 raise Exception('Please specify commit or pull_request_id')
676 q = q.order_by(ChangesetComment.comment_id.asc())
677 q = q.order_by(ChangesetComment.comment_id.asc())
677 return q
678 return q
678
679
679 def _group_comments_by_path_and_line_number(self, q):
680 def _group_comments_by_path_and_line_number(self, q):
680 comments = q.all()
681 comments = q.all()
681 paths = collections.defaultdict(lambda: collections.defaultdict(list))
682 paths = collections.defaultdict(lambda: collections.defaultdict(list))
682 for co in comments:
683 for co in comments:
683 paths[co.f_path][co.line_no].append(co)
684 paths[co.f_path][co.line_no].append(co)
684 return paths
685 return paths
685
686
686 @classmethod
687 @classmethod
687 def needed_extra_diff_context(cls):
688 def needed_extra_diff_context(cls):
688 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
689 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
689
690
690 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
691 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
691 if not CommentsModel.use_outdated_comments(pull_request):
692 if not CommentsModel.use_outdated_comments(pull_request):
692 return
693 return
693
694
694 comments = self._visible_inline_comments_of_pull_request(pull_request)
695 comments = self._visible_inline_comments_of_pull_request(pull_request)
695 comments_to_outdate = comments.all()
696 comments_to_outdate = comments.all()
696
697
697 for comment in comments_to_outdate:
698 for comment in comments_to_outdate:
698 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
699 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
699
700
700 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
701 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
701 diff_line = _parse_comment_line_number(comment.line_no)
702 diff_line = _parse_comment_line_number(comment.line_no)
702
703
703 try:
704 try:
704 old_context = old_diff_proc.get_context_of_line(
705 old_context = old_diff_proc.get_context_of_line(
705 path=comment.f_path, diff_line=diff_line)
706 path=comment.f_path, diff_line=diff_line)
706 new_context = new_diff_proc.get_context_of_line(
707 new_context = new_diff_proc.get_context_of_line(
707 path=comment.f_path, diff_line=diff_line)
708 path=comment.f_path, diff_line=diff_line)
708 except (diffs.LineNotInDiffException,
709 except (diffs.LineNotInDiffException,
709 diffs.FileNotInDiffException):
710 diffs.FileNotInDiffException):
710 comment.display_state = ChangesetComment.COMMENT_OUTDATED
711 comment.display_state = ChangesetComment.COMMENT_OUTDATED
711 return
712 return
712
713
713 if old_context == new_context:
714 if old_context == new_context:
714 return
715 return
715
716
716 if self._should_relocate_diff_line(diff_line):
717 if self._should_relocate_diff_line(diff_line):
717 new_diff_lines = new_diff_proc.find_context(
718 new_diff_lines = new_diff_proc.find_context(
718 path=comment.f_path, context=old_context,
719 path=comment.f_path, context=old_context,
719 offset=self.DIFF_CONTEXT_BEFORE)
720 offset=self.DIFF_CONTEXT_BEFORE)
720 if not new_diff_lines:
721 if not new_diff_lines:
721 comment.display_state = ChangesetComment.COMMENT_OUTDATED
722 comment.display_state = ChangesetComment.COMMENT_OUTDATED
722 else:
723 else:
723 new_diff_line = self._choose_closest_diff_line(
724 new_diff_line = self._choose_closest_diff_line(
724 diff_line, new_diff_lines)
725 diff_line, new_diff_lines)
725 comment.line_no = _diff_to_comment_line_number(new_diff_line)
726 comment.line_no = _diff_to_comment_line_number(new_diff_line)
726 else:
727 else:
727 comment.display_state = ChangesetComment.COMMENT_OUTDATED
728 comment.display_state = ChangesetComment.COMMENT_OUTDATED
728
729
729 def _should_relocate_diff_line(self, diff_line):
730 def _should_relocate_diff_line(self, diff_line):
730 """
731 """
731 Checks if relocation shall be tried for the given `diff_line`.
732 Checks if relocation shall be tried for the given `diff_line`.
732
733
733 If a comment points into the first lines, then we can have a situation
734 If a comment points into the first lines, then we can have a situation
734 that after an update another line has been added on top. In this case
735 that after an update another line has been added on top. In this case
735 we would find the context still and move the comment around. This
736 we would find the context still and move the comment around. This
736 would be wrong.
737 would be wrong.
737 """
738 """
738 should_relocate = (
739 should_relocate = (
739 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
740 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
740 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
741 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
741 return should_relocate
742 return should_relocate
742
743
743 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
744 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
744 candidate = new_diff_lines[0]
745 candidate = new_diff_lines[0]
745 best_delta = _diff_line_delta(diff_line, candidate)
746 best_delta = _diff_line_delta(diff_line, candidate)
746 for new_diff_line in new_diff_lines[1:]:
747 for new_diff_line in new_diff_lines[1:]:
747 delta = _diff_line_delta(diff_line, new_diff_line)
748 delta = _diff_line_delta(diff_line, new_diff_line)
748 if delta < best_delta:
749 if delta < best_delta:
749 candidate = new_diff_line
750 candidate = new_diff_line
750 best_delta = delta
751 best_delta = delta
751 return candidate
752 return candidate
752
753
753 def _visible_inline_comments_of_pull_request(self, pull_request):
754 def _visible_inline_comments_of_pull_request(self, pull_request):
754 comments = self._all_inline_comments_of_pull_request(pull_request)
755 comments = self._all_inline_comments_of_pull_request(pull_request)
755 comments = comments.filter(
756 comments = comments.filter(
756 coalesce(ChangesetComment.display_state, '') !=
757 coalesce(ChangesetComment.display_state, '') !=
757 ChangesetComment.COMMENT_OUTDATED)
758 ChangesetComment.COMMENT_OUTDATED)
758 return comments
759 return comments
759
760
760 def _all_inline_comments_of_pull_request(self, pull_request):
761 def _all_inline_comments_of_pull_request(self, pull_request):
761 comments = Session().query(ChangesetComment)\
762 comments = Session().query(ChangesetComment)\
762 .filter(ChangesetComment.line_no != None)\
763 .filter(ChangesetComment.line_no != None)\
763 .filter(ChangesetComment.f_path != None)\
764 .filter(ChangesetComment.f_path != None)\
764 .filter(ChangesetComment.pull_request == pull_request)
765 .filter(ChangesetComment.pull_request == pull_request)
765 return comments
766 return comments
766
767
767 def _all_general_comments_of_pull_request(self, pull_request):
768 def _all_general_comments_of_pull_request(self, pull_request):
768 comments = Session().query(ChangesetComment)\
769 comments = Session().query(ChangesetComment)\
769 .filter(ChangesetComment.line_no == None)\
770 .filter(ChangesetComment.line_no == None)\
770 .filter(ChangesetComment.f_path == None)\
771 .filter(ChangesetComment.f_path == None)\
771 .filter(ChangesetComment.pull_request == pull_request)
772 .filter(ChangesetComment.pull_request == pull_request)
772
773
773 return comments
774 return comments
774
775
775 @staticmethod
776 @staticmethod
776 def use_outdated_comments(pull_request):
777 def use_outdated_comments(pull_request):
777 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
778 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
778 settings = settings_model.get_general_settings()
779 settings = settings_model.get_general_settings()
779 return settings.get('rhodecode_use_outdated_comments', False)
780 return settings.get('rhodecode_use_outdated_comments', False)
780
781
781 def trigger_commit_comment_hook(self, repo, user, action, data=None):
782 def trigger_commit_comment_hook(self, repo, user, action, data=None):
782 repo = self._get_repo(repo)
783 repo = self._get_repo(repo)
783 target_scm = repo.scm_instance()
784 target_scm = repo.scm_instance()
784 if action == 'create':
785 if action == 'create':
785 trigger_hook = hooks_utils.trigger_comment_commit_hooks
786 trigger_hook = hooks_utils.trigger_comment_commit_hooks
786 elif action == 'edit':
787 elif action == 'edit':
787 # TODO(dan): when this is supported we trigger edit hook too
788 # TODO(dan): when this is supported we trigger edit hook too
788 return
789 return
789 else:
790 else:
790 return
791 return
791
792
792 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
793 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
793 repo, action, trigger_hook)
794 repo, action, trigger_hook)
794 trigger_hook(
795 trigger_hook(
795 username=user.username,
796 username=user.username,
796 repo_name=repo.repo_name,
797 repo_name=repo.repo_name,
797 repo_type=target_scm.alias,
798 repo_type=target_scm.alias,
798 repo=repo,
799 repo=repo,
799 data=data)
800 data=data)
800
801
801
802
802 def _parse_comment_line_number(line_no):
803 def _parse_comment_line_number(line_no):
803 """
804 """
804 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
805 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
805 """
806 """
806 old_line = None
807 old_line = None
807 new_line = None
808 new_line = None
808 if line_no.startswith('o'):
809 if line_no.startswith('o'):
809 old_line = int(line_no[1:])
810 old_line = int(line_no[1:])
810 elif line_no.startswith('n'):
811 elif line_no.startswith('n'):
811 new_line = int(line_no[1:])
812 new_line = int(line_no[1:])
812 else:
813 else:
813 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
814 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
814 return diffs.DiffLineNumber(old_line, new_line)
815 return diffs.DiffLineNumber(old_line, new_line)
815
816
816
817
817 def _diff_to_comment_line_number(diff_line):
818 def _diff_to_comment_line_number(diff_line):
818 if diff_line.new is not None:
819 if diff_line.new is not None:
819 return u'n{}'.format(diff_line.new)
820 return u'n{}'.format(diff_line.new)
820 elif diff_line.old is not None:
821 elif diff_line.old is not None:
821 return u'o{}'.format(diff_line.old)
822 return u'o{}'.format(diff_line.old)
822 return u''
823 return u''
823
824
824
825
825 def _diff_line_delta(a, b):
826 def _diff_line_delta(a, b):
826 if None not in (a.new, b.new):
827 if None not in (a.new, b.new):
827 return abs(a.new - b.new)
828 return abs(a.new - b.new)
828 elif None not in (a.old, b.old):
829 elif None not in (a.old, b.old):
829 return abs(a.old - b.old)
830 return abs(a.old - b.old)
830 else:
831 else:
831 raise ValueError(
832 raise ValueError(
832 "Cannot compute delta between {} and {}".format(a, b))
833 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,237 +1,269 b''
1 // select2.less
1 // select2.less
2 // For use in RhodeCode application drop down select boxes;
2 // For use in RhodeCode application drop down select boxes;
3 // see style guide documentation for guidelines.
3 // see style guide documentation for guidelines.
4
4
5
5
6 // SELECT2 DROPDOWN MENUS
6 // SELECT2 DROPDOWN MENUS
7
7
8 //Select2 Dropdown
8 //Select2 Dropdown
9 .select2-results{
9 .select2-results{
10 .box-sizing(border-box);
10 .box-sizing(border-box);
11 overflow-y: scroll;
11 overflow-y: scroll;
12 }
12 }
13
13
14 .select2-container{margin: 0; position: relative; display: inline-block; zoom: 1;}
14 .select2-container{margin: 0; position: relative; display: inline-block; zoom: 1;}
15 .select2-container,
15 .select2-container,
16 .select2-drop,
16 .select2-drop,
17 .select2-search,
17 .select2-search,
18 .select2-search input {.box-sizing(border-box);}
18 .select2-search input {.box-sizing(border-box);}
19 .select2-container .select2-choice{display:block; line-height:1em; -webkit-touch-callout:none;-moz-user-select:none;-ms-user-select:none;user-select:none; }
19 .select2-container .select2-choice{display:block; line-height:1em; -webkit-touch-callout:none;-moz-user-select:none;-ms-user-select:none;user-select:none; }
20 .main .select2-container .select2-choice { background-color: white; box-shadow: @button-shadow;}
20 .main .select2-container .select2-choice { background-color: white; box-shadow: @button-shadow;}
21 .select2-container .select2-choice abbr { display: none; width: 12px; height: 12px; position: absolute; right: 24px; top: 8px; font-size: 1px; text-decoration: none; border: 0; background: url('../images/select2.png') right top no-repeat; cursor: pointer; outline: 0; }
21 .select2-container .select2-choice abbr { display: none; width: 12px; height: 12px; position: absolute; right: 24px; top: 8px; font-size: 1px; text-decoration: none; border: 0; background: url('../images/select2.png') right top no-repeat; cursor: pointer; outline: 0; }
22 .select2-container.select2-allowclear .select2-choice abbr {display: inline-block;}
22 .select2-container.select2-allowclear .select2-choice abbr {display: inline-block;}
23 .select2-container .select2-choice abbr:hover { background-position: right -11px; cursor: pointer; }
23 .select2-container .select2-choice abbr:hover { background-position: right -11px; cursor: pointer; }
24 .select2-drop-mask { border: 0; margin: 0; padding: 0; position: fixed; left: 0; top: 0; min-height: 100%; min-width: 100%; height: auto; width: auto; opacity: 0; z-index: 998; background-color: #fff; filter: alpha(opacity=0); }
24 .select2-drop-mask { border: 0; margin: 0; padding: 0; position: fixed; left: 0; top: 0; min-height: 100%; min-width: 100%; height: auto; width: auto; opacity: 0; z-index: 998; background-color: #fff; filter: alpha(opacity=0); }
25 .select2-drop { width: 100%; margin-top: -1px; position: absolute; z-index: 999; top: 100%; background: #fff; color: #000; border: @border-thickness solid @rcblue; border-top: 0; border-radius: 0 0 @border-radius @border-radius; }
25 .select2-drop { width: 100%; margin-top: -1px; position: absolute; z-index: 999; top: 100%; background: #fff; color: #000; border: @border-thickness solid @rcblue; border-top: 0; border-radius: 0 0 @border-radius @border-radius; }
26 .select2-drop.select2-drop-above { margin-top: 1px; border-top: @border-thickness solid @rclightblue; border-bottom: 0; border-radius: @border-radius @border-radius 0 0; }
26 .select2-drop.select2-drop-above { margin-top: 1px; border-top: @border-thickness solid @rclightblue; border-bottom: 0; border-radius: @border-radius @border-radius 0 0; }
27 .select2-drop-active { border: @border-thickness solid #5897fb; border-top: none; }
27 .select2-drop-active { border: @border-thickness solid #5897fb; border-top: none; }
28 .select2-drop.select2-drop-above.select2-drop-active {border-top: @border-thickness solid #5897fb;}
28 .select2-drop.select2-drop-above.select2-drop-active {border-top: @border-thickness solid #5897fb;}
29 .select2-drop-auto-width { border-top: @border-thickness solid #aaa; width: auto; }
29 .select2-drop-auto-width { border-top: @border-thickness solid #aaa; width: auto; }
30 .select2-drop-auto-width .select2-search {padding-top: 4px;}
30 .select2-drop-auto-width .select2-search {padding-top: 4px;}
31 html[dir="rtl"] .select2-container .select2-choice .select2-arrow { left: 0; right: auto; border-left: none; border-right: @border-thickness solid @grey5; border-radius: @border-radius 0 0 @border-radius; }
31 html[dir="rtl"] .select2-container .select2-choice .select2-arrow { left: 0; right: auto; border-left: none; border-right: @border-thickness solid @grey5; border-radius: @border-radius 0 0 @border-radius; }
32 html[dir="rtl"] .select2-container .select2-choice .select2-arrow b {background-position: 2px 1px;}
32 html[dir="rtl"] .select2-container .select2-choice .select2-arrow b {background-position: 2px 1px;}
33 .select2-search { display: inline-block; width: 100%; min-height: 26px; margin: 0; padding-left: 4px; padding-right: 4px; position: relative; z-index: 1000; white-space: nowrap; }
33 .select2-search { display: inline-block; width: 100%; min-height: 26px; margin: 0; padding-left: 4px; padding-right: 4px; position: relative; z-index: 1000; white-space: nowrap; }
34 .select2-search input { width: 100%; height: auto !important; min-height: 26px; padding: 4px 20px 4px 5px; margin: 0; outline: 0; }
34 .select2-search input { width: 100%; height: auto !important; min-height: 26px; padding: 4px 20px 4px 5px; margin: 0; outline: 0; }
35 html[dir="rtl"] .select2-search input { padding: 4px 5px 4px 20px; background: #fff url('../images/select2.png') no-repeat -37px -22px; }
35 html[dir="rtl"] .select2-search input { padding: 4px 5px 4px 20px; background: #fff url('../images/select2.png') no-repeat -37px -22px; }
36 .select2-drop.select2-drop-above .select2-search input {margin-top: 4px;}
36 .select2-drop.select2-drop-above .select2-search input {margin-top: 4px;}
37 .select2-dropdown-open .select2-choice .select2-arrow { background: transparent; border-left: none; filter: none; }
37 .select2-dropdown-open .select2-choice .select2-arrow { background: transparent; border-left: none; filter: none; }
38 html[dir="rtl"] .select2-dropdown-open .select2-choice .select2-arrow {border-right: none;}
38 html[dir="rtl"] .select2-dropdown-open .select2-choice .select2-arrow {border-right: none;}
39 .select2-hidden-accessible { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
39 .select2-hidden-accessible { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
40 /* results */
40 /* results */
41 .select2-results { max-height: 200px; padding: 0 0 0 4px; margin: 4px 4px 4px 0; position: relative; overflow-x: hidden; overflow-y: auto; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
41 .select2-results { max-height: 200px; padding: 0 0 0 4px; margin: 4px 4px 4px 0; position: relative; overflow-x: hidden; overflow-y: auto; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
42 html[dir="rtl"] .select2-results { padding: 0 4px 0 0; margin: 4px 0 4px 4px; }
42 html[dir="rtl"] .select2-results { padding: 0 4px 0 0; margin: 4px 0 4px 4px; }
43 .select2-results .select2-disabled{background:@grey6;display:list-item;cursor:default}
43 .select2-results .select2-disabled{background:@grey6;display:list-item;cursor:default}
44 .select2-results .select2-selected{display:none}
44 .select2-results .select2-selected{display:none}
45 .select2-more-results.select2-active{background:#f4f4f4 url('../images/select2-spinner.gif') no-repeat 100%}
45 .select2-more-results.select2-active{background:#f4f4f4 url('../images/select2-spinner.gif') no-repeat 100%}
46 .select2-container.select2-container-disabled .select2-choice abbr{display:none}
46 .select2-container.select2-container-disabled .select2-choice abbr{display:none}
47 .select2-container.select2-container-disabled {background:@grey6;cursor:default}
47 .select2-container.select2-container-disabled {background:@grey6;cursor:default}
48 .select2-container.select2-container-disabled .select2-choice {background:@grey6;cursor:default}
48 .select2-container.select2-container-disabled .select2-choice {background:@grey6;cursor:default}
49 .select2-container-multi .select2-choices li{float:left;list-style:none}
49 .select2-container-multi .select2-choices li{float:left;list-style:none}
50 .select2-container-multi .select2-choices .select2-search-field{margin:0;padding:0;white-space:nowrap}
50 .select2-container-multi .select2-choices .select2-search-field{margin:0;padding:0;white-space:nowrap}
51 .select2-container-multi .select2-choices .select2-search-choice .select2-chosen{cursor:default}
51 .select2-container-multi .select2-choices .select2-search-choice .select2-chosen{cursor:default}
52 .select2-search-choice-close{display:block;width:12px;height:13px;position:absolute;right:3px;top:4px;font-size:1px;outline:none;background:url('../images/select2.png') right top no-repeat}
52 .select2-search-choice-close{display:block;width:12px;height:13px;position:absolute;right:3px;top:4px;font-size:1px;outline:none;background:url('../images/select2.png') right top no-repeat}
53 .select2-container-multi .select2-search-choice-close{left:3px}
53 .select2-container-multi .select2-search-choice-close{left:3px}
54 .select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover{background-position:right -11px}
54 .select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover{background-position:right -11px}
55 .select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close{background-position:right -11px}
55 .select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close{background-position:right -11px}
56 .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close{display:none;background:none}
56 .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close{display:none;background:none}
57 .select2-offscreen,.select2-offscreen:focus{clip:rect(0 0 0 0) !important;width:1px !important;height:1px !important;
57 .select2-offscreen,.select2-offscreen:focus{clip:rect(0 0 0 0) !important;width:1px !important;height:1px !important;
58 border:0 !important;margin:0 !important;padding:0 !important;overflow:hidden !important;
58 border:0 !important;margin:0 !important;padding:0 !important;overflow:hidden !important;
59 position: absolute !important;outline:0 !important;left:0 !important;top:0 !important}
59 position: absolute !important;outline:0 !important;left:0 !important;top:0 !important}
60 .select2-display-none,
60 .select2-display-none,
61 .select2-search-hidden {display:none}
61 .select2-search-hidden {display:none}
62 .select2-search input { border-color: @rclightblue; }
62 .select2-search input { border-color: @rclightblue; }
63
63
64 .select2-measure-scrollbar{position:absolute;top:-10000px;left:-10000px;width:100px;height:100px;overflow:scroll}
64 .select2-measure-scrollbar{position:absolute;top:-10000px;left:-10000px;width:100px;height:100px;overflow:scroll}
65 @media only screen and (-webkit-min-device-pixel-ratio:1.5),
65 @media only screen and (-webkit-min-device-pixel-ratio:1.5),
66 only screen and (min-resolution:144dpi){
66 only screen and (min-resolution:144dpi){
67 .select2-search input,
67 .select2-search input,
68 .select2-search-choice-close,
68 .select2-search-choice-close,
69 .select2-container .select2-choice abbr,
69 .select2-container .select2-choice abbr,
70 .select2-container .select2-choice .select2-arrow b{background-image:url('../images/select2x2.png');background-repeat:no-repeat;background-size:60px 40px;}
70 .select2-container .select2-choice .select2-arrow b{background-image:url('../images/select2x2.png');background-repeat:no-repeat;background-size:60px 40px;}
71 .select2-search input{background-position:100% -21px}
71 .select2-search input{background-position:100% -21px}
72 }
72 }
73 [class^="input-"] [class^="select2-choice"]>div{display:none}
73 [class^="input-"] [class^="select2-choice"]>div{display:none}
74 [class^="input-"] .select2-offscreen{position:absolute}
74 [class^="input-"] .select2-offscreen{position:absolute}
75 select.select2{height:28px;visibility:hidden}
75 select.select2{height:28px;visibility:hidden}
76 .autocomplete-suggestions{overflow:auto}
76 .autocomplete-suggestions{overflow:auto}
77 .autocomplete-suggestion{white-space:nowrap;overflow:hidden}
77 .autocomplete-suggestion{white-space:nowrap;overflow:hidden}
78
78
79 /* Retina-ize icons */
79 /* Retina-ize icons */
80 @media only screen and (-webkit-min-device-pixel-ratio:1.5),
80 @media only screen and (-webkit-min-device-pixel-ratio:1.5),
81 only screen and (min-resolution:144dpi){
81 only screen and (min-resolution:144dpi){
82 .select2-search input,
82 .select2-search input,
83 .select2-search-choice-close,
83 .select2-search-choice-close,
84 .select2-container .select2-choice abbr,
84 .select2-container .select2-choice abbr,
85 .select2-container .select2-choice .select2-arrow b{background-image:url('../images/select2x2.png');background-repeat:no-repeat;background-size:60px 40px;}
85 .select2-container .select2-choice .select2-arrow b{background-image:url('../images/select2x2.png');background-repeat:no-repeat;background-size:60px 40px;}
86 .select2-search input{background-position:100% -21px}
86 .select2-search input{background-position:100% -21px}
87 }
87 }
88
88
89 //Internal Select2 Dropdown Menus
89 //Internal Select2 Dropdown Menus
90
90
91 .drop-menu-core {
91 .drop-menu-core {
92 min-width: 160px;
92 min-width: 160px;
93 margin: 0 @padding 0 0;
93 margin: 0 @padding 0 0;
94 padding: 0;
94 padding: 0;
95 border: @border-thickness solid @grey5;
95 border: @border-thickness solid @grey5;
96 border-radius: @border-radius;
96 border-radius: @border-radius;
97 color: @grey2;
97 color: @grey2;
98 background-color: white;
98 background-color: white;
99
99
100 a {
100 a {
101 color: @grey2;
101 color: @grey2;
102
102
103 &:hover {
103 &:hover {
104 color: @rcdarkblue;
104 color: @rcdarkblue;
105 }
105 }
106 }
106 }
107 }
107 }
108
108
109 .drop-menu-dropdown {
109 .drop-menu-dropdown {
110 .drop-menu-core;
110 .drop-menu-core;
111 }
111 }
112
112
113 .drop-menu-base {
113 .drop-menu-base {
114 .drop-menu-core;
114 .drop-menu-core;
115 position: relative;
115 position: relative;
116 display: inline-block;
116 display: inline-block;
117 line-height: 1em;
117 line-height: 1em;
118 z-index: 2;
118 z-index: 2;
119 cursor: pointer;
119 cursor: pointer;
120
120
121 a {
121 a {
122 display:block;
122 display:block;
123 padding: .7em;
123 padding: .7em;
124 padding-right: 2em;
124 padding-right: 2em;
125 position: relative;
125 position: relative;
126
126
127 &:after {
127 &:after {
128 position: absolute;
128 position: absolute;
129 content: "\00A0\25BE";
129 content: "\00A0\25BE";
130 right: .1em;
130 right: .1em;
131 line-height: 1em;
131 line-height: 1em;
132 top: 0.2em;
132 top: 0.2em;
133 width: 1em;
133 width: 1em;
134 font-size: 20px;
134 font-size: 20px;
135 }
135 }
136 }
136 }
137 }
137 }
138
138
139 .drop-menu {
139 .drop-menu {
140 .drop-menu-base;
140 .drop-menu-base;
141 width: auto !important;
141 width: auto !important;
142 }
142 }
143
143
144 .drop-menu-no-width {
144 .drop-menu-no-width {
145 .drop-menu-base;
145 .drop-menu-base;
146 width: auto;
146 width: auto;
147 min-width: 0;
147 min-width: 0;
148 margin: 0;
148 margin: 0;
149 }
149 }
150
150
151
152 .drop-menu-comment-history {
153 .drop-menu-core;
154 border: none;
155 padding: 0 6px 0 0;
156 width: auto;
157 min-width: 0;
158 margin: 0;
159 position: relative;
160 display: inline-block;
161 line-height: 1em;
162 z-index: 2;
163 cursor: pointer;
164
165 a {
166 display:block;
167 padding: 0;
168 position: relative;
169
170 &:after {
171 position: absolute;
172 content: "\00A0\25BE";
173 right: -0.80em;
174 line-height: 1em;
175 top: -0.20em;
176 width: 1em;
177 font-size: 16px;
178 }
179 }
180
181 }
182
151 .field-sm .drop-menu {
183 .field-sm .drop-menu {
152 padding: 1px 0 0 0;
184 padding: 1px 0 0 0;
153 a {
185 a {
154 padding: 6px;
186 padding: 6px;
155 };
187 };
156 }
188 }
157
189
158 .select2-search input {
190 .select2-search input {
159 width: 100%;
191 width: 100%;
160 margin: .5em 0;
192 margin: .5em 0;
161 padding: .5em;
193 padding: .5em;
162 border-color: @grey4;
194 border-color: @grey4;
163
195
164 &:focus, &:hover {
196 &:focus, &:hover {
165 border-color: @rcblue;
197 border-color: @rcblue;
166 box-shadow: @button-shadow;
198 box-shadow: @button-shadow;
167 }
199 }
168 }
200 }
169
201
170 .select2-no-results {
202 .select2-no-results {
171 padding: .5em;
203 padding: .5em;
172 }
204 }
173
205
174 .drop-menu-dropdown ul {
206 .drop-menu-dropdown ul {
175 width: auto;
207 width: auto;
176 margin: 0;
208 margin: 0;
177 padding: 0;
209 padding: 0;
178 z-index: 50;
210 z-index: 50;
179
211
180 li {
212 li {
181 margin: 0;
213 margin: 0;
182 line-height: 1em;
214 line-height: 1em;
183 list-style-type: none;
215 list-style-type: none;
184
216
185 &:hover,
217 &:hover,
186 &.select2-highlighted {
218 &.select2-highlighted {
187 background-color: @grey7;
219 background-color: @grey7;
188
220
189 .select2-result-label {
221 .select2-result-label {
190 &:hover {
222 &:hover {
191 color: @grey1!important;
223 color: @grey1!important;
192 }
224 }
193 }
225 }
194 }
226 }
195
227
196 &.select2-result-with-children {
228 &.select2-result-with-children {
197 &:hover {
229 &:hover {
198 background-color: white;
230 background-color: white;
199 }
231 }
200 }
232 }
201
233
202 .select2-result-label {
234 .select2-result-label {
203 display:block;
235 display:block;
204 padding: 8px;
236 padding: 8px;
205 font-family: @text-regular;
237 font-family: @text-regular;
206 color: @grey2;
238 color: @grey2;
207 cursor: pointer;
239 cursor: pointer;
208 white-space: nowrap;
240 white-space: nowrap;
209 }
241 }
210
242
211 &.select2-result-with-children {
243 &.select2-result-with-children {
212
244
213 .select2-result-label {
245 .select2-result-label {
214 color: @rcdarkblue;
246 color: @rcdarkblue;
215 cursor: default;
247 cursor: default;
216 font-weight: @text-semibold-weight;
248 font-weight: @text-semibold-weight;
217 font-family: @text-semibold;
249 font-family: @text-semibold;
218 }
250 }
219
251
220 ul.select2-result-sub li .select2-result-label {
252 ul.select2-result-sub li .select2-result-label {
221 padding-left: 16px;
253 padding-left: 16px;
222 font-family: @text-regular;
254 font-family: @text-regular;
223 color: @grey2;
255 color: @grey2;
224 cursor: pointer;
256 cursor: pointer;
225 white-space: nowrap;
257 white-space: nowrap;
226 }
258 }
227 }
259 }
228 }
260 }
229 }
261 }
230
262
231 .side-by-side-selector {
263 .side-by-side-selector {
232 .left-group,
264 .left-group,
233 .middle-group,
265 .middle-group,
234 .right-group {
266 .right-group {
235 margin-bottom: @padding;
267 margin-bottom: @padding;
236 }
268 }
237 }
269 }
@@ -1,1200 +1,1263 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45
45
46
46
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 failHandler = failHandler || function() {};
48 failHandler = failHandler || function() {};
49 postData = toQueryString(postData);
49 postData = toQueryString(postData);
50 var request = $.ajax({
50 var request = $.ajax({
51 url: url,
51 url: url,
52 type: 'POST',
52 type: 'POST',
53 data: postData,
53 data: postData,
54 headers: {'X-PARTIAL-XHR': true}
54 headers: {'X-PARTIAL-XHR': true}
55 })
55 })
56 .done(function (data) {
56 .done(function (data) {
57 successHandler(data);
57 successHandler(data);
58 })
58 })
59 .fail(function (data, textStatus, errorThrown) {
59 .fail(function (data, textStatus, errorThrown) {
60 failHandler(data, textStatus, errorThrown)
60 failHandler(data, textStatus, errorThrown)
61 });
61 });
62 return request;
62 return request;
63 };
63 };
64
64
65
65
66
66
67
67
68 /* Comment form for main and inline comments */
68 /* Comment form for main and inline comments */
69 (function(mod) {
69 (function(mod) {
70
70
71 if (typeof exports == "object" && typeof module == "object") {
71 if (typeof exports == "object" && typeof module == "object") {
72 // CommonJS
72 // CommonJS
73 module.exports = mod();
73 module.exports = mod();
74 }
74 }
75 else {
75 else {
76 // Plain browser env
76 // Plain browser env
77 (this || window).CommentForm = mod();
77 (this || window).CommentForm = mod();
78 }
78 }
79
79
80 })(function() {
80 })(function() {
81 "use strict";
81 "use strict";
82
82
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84
84
85 if (!(this instanceof CommentForm)) {
85 if (!(this instanceof CommentForm)) {
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 }
87 }
88
88
89 // bind the element instance to our Form
89 // bind the element instance to our Form
90 $(formElement).get(0).CommentForm = this;
90 $(formElement).get(0).CommentForm = this;
91
91
92 this.withLineNo = function(selector) {
92 this.withLineNo = function(selector) {
93 var lineNo = this.lineNo;
93 var lineNo = this.lineNo;
94 if (lineNo === undefined) {
94 if (lineNo === undefined) {
95 return selector
95 return selector
96 } else {
96 } else {
97 return selector + '_' + lineNo;
97 return selector + '_' + lineNo;
98 }
98 }
99 };
99 };
100
100
101 this.commitId = commitId;
101 this.commitId = commitId;
102 this.pullRequestId = pullRequestId;
102 this.pullRequestId = pullRequestId;
103 this.lineNo = lineNo;
103 this.lineNo = lineNo;
104 this.initAutocompleteActions = initAutocompleteActions;
104 this.initAutocompleteActions = initAutocompleteActions;
105
105
106 this.previewButton = this.withLineNo('#preview-btn');
106 this.previewButton = this.withLineNo('#preview-btn');
107 this.previewContainer = this.withLineNo('#preview-container');
107 this.previewContainer = this.withLineNo('#preview-container');
108
108
109 this.previewBoxSelector = this.withLineNo('#preview-box');
109 this.previewBoxSelector = this.withLineNo('#preview-box');
110
110
111 this.editButton = this.withLineNo('#edit-btn');
111 this.editButton = this.withLineNo('#edit-btn');
112 this.editContainer = this.withLineNo('#edit-container');
112 this.editContainer = this.withLineNo('#edit-container');
113 this.cancelButton = this.withLineNo('#cancel-btn');
113 this.cancelButton = this.withLineNo('#cancel-btn');
114 this.commentType = this.withLineNo('#comment_type');
114 this.commentType = this.withLineNo('#comment_type');
115
115
116 this.resolvesId = null;
116 this.resolvesId = null;
117 this.resolvesActionId = null;
117 this.resolvesActionId = null;
118
118
119 this.closesPr = '#close_pull_request';
119 this.closesPr = '#close_pull_request';
120
120
121 this.cmBox = this.withLineNo('#text');
121 this.cmBox = this.withLineNo('#text');
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123
123
124 this.statusChange = this.withLineNo('#change_status');
124 this.statusChange = this.withLineNo('#change_status');
125
125
126 this.submitForm = formElement;
126 this.submitForm = formElement;
127 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127 this.submitButton = $(this.submitForm).find('input[type="submit"]');
128 this.submitButtonText = this.submitButton.val();
128 this.submitButtonText = this.submitButton.val();
129
129
130
130
131 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
131 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
132 {'repo_name': templateContext.repo_name,
132 {'repo_name': templateContext.repo_name,
133 'commit_id': templateContext.commit_data.commit_id});
133 'commit_id': templateContext.commit_data.commit_id});
134
134
135 if (edit){
135 if (edit){
136 this.submitButtonText = _gettext('Updated Comment');
136 this.submitButtonText = _gettext('Updated Comment');
137 $(this.commentType).prop('disabled', true);
137 $(this.commentType).prop('disabled', true);
138 $(this.commentType).addClass('disabled');
138 $(this.commentType).addClass('disabled');
139 var editInfo =
139 var editInfo =
140 '';
140 '';
141 $(editInfo).insertBefore($(this.editButton).parent());
141 $(editInfo).insertBefore($(this.editButton).parent());
142 }
142 }
143
143
144 if (resolvesCommentId){
144 if (resolvesCommentId){
145 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
145 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
146 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
146 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
147 $(this.commentType).prop('disabled', true);
147 $(this.commentType).prop('disabled', true);
148 $(this.commentType).addClass('disabled');
148 $(this.commentType).addClass('disabled');
149
149
150 // disable select
150 // disable select
151 setTimeout(function() {
151 setTimeout(function() {
152 $(self.statusChange).select2('readonly', true);
152 $(self.statusChange).select2('readonly', true);
153 }, 10);
153 }, 10);
154
154
155 var resolvedInfo = (
155 var resolvedInfo = (
156 '<li class="resolve-action">' +
156 '<li class="resolve-action">' +
157 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
157 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
158 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
158 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
159 '</li>'
159 '</li>'
160 ).format(resolvesCommentId, _gettext('resolve comment'));
160 ).format(resolvesCommentId, _gettext('resolve comment'));
161 $(resolvedInfo).insertAfter($(this.commentType).parent());
161 $(resolvedInfo).insertAfter($(this.commentType).parent());
162 }
162 }
163
163
164 // based on commitId, or pullRequestId decide where do we submit
164 // based on commitId, or pullRequestId decide where do we submit
165 // out data
165 // out data
166 if (this.commitId){
166 if (this.commitId){
167 var pyurl = 'repo_commit_comment_create';
167 var pyurl = 'repo_commit_comment_create';
168 if(edit){
168 if(edit){
169 pyurl = 'repo_commit_comment_edit';
169 pyurl = 'repo_commit_comment_edit';
170 }
170 }
171 this.submitUrl = pyroutes.url(pyurl,
171 this.submitUrl = pyroutes.url(pyurl,
172 {'repo_name': templateContext.repo_name,
172 {'repo_name': templateContext.repo_name,
173 'commit_id': this.commitId,
173 'commit_id': this.commitId,
174 'comment_id': comment_id});
174 'comment_id': comment_id});
175 this.selfUrl = pyroutes.url('repo_commit',
175 this.selfUrl = pyroutes.url('repo_commit',
176 {'repo_name': templateContext.repo_name,
176 {'repo_name': templateContext.repo_name,
177 'commit_id': this.commitId});
177 'commit_id': this.commitId});
178
178
179 } else if (this.pullRequestId) {
179 } else if (this.pullRequestId) {
180 var pyurl = 'pullrequest_comment_create';
180 var pyurl = 'pullrequest_comment_create';
181 if(edit){
181 if(edit){
182 pyurl = 'pullrequest_comment_edit';
182 pyurl = 'pullrequest_comment_edit';
183 }
183 }
184 this.submitUrl = pyroutes.url(pyurl,
184 this.submitUrl = pyroutes.url(pyurl,
185 {'repo_name': templateContext.repo_name,
185 {'repo_name': templateContext.repo_name,
186 'pull_request_id': this.pullRequestId,
186 'pull_request_id': this.pullRequestId,
187 'comment_id': comment_id});
187 'comment_id': comment_id});
188 this.selfUrl = pyroutes.url('pullrequest_show',
188 this.selfUrl = pyroutes.url('pullrequest_show',
189 {'repo_name': templateContext.repo_name,
189 {'repo_name': templateContext.repo_name,
190 'pull_request_id': this.pullRequestId});
190 'pull_request_id': this.pullRequestId});
191
191
192 } else {
192 } else {
193 throw new Error(
193 throw new Error(
194 'CommentForm requires pullRequestId, or commitId to be specified.')
194 'CommentForm requires pullRequestId, or commitId to be specified.')
195 }
195 }
196
196
197 // FUNCTIONS and helpers
197 // FUNCTIONS and helpers
198 var self = this;
198 var self = this;
199
199
200 this.isInline = function(){
200 this.isInline = function(){
201 return this.lineNo && this.lineNo != 'general';
201 return this.lineNo && this.lineNo != 'general';
202 };
202 };
203
203
204 this.getCmInstance = function(){
204 this.getCmInstance = function(){
205 return this.cm
205 return this.cm
206 };
206 };
207
207
208 this.setPlaceholder = function(placeholder) {
208 this.setPlaceholder = function(placeholder) {
209 var cm = this.getCmInstance();
209 var cm = this.getCmInstance();
210 if (cm){
210 if (cm){
211 cm.setOption('placeholder', placeholder);
211 cm.setOption('placeholder', placeholder);
212 }
212 }
213 };
213 };
214
214
215 this.getCommentStatus = function() {
215 this.getCommentStatus = function() {
216 return $(this.submitForm).find(this.statusChange).val();
216 return $(this.submitForm).find(this.statusChange).val();
217 };
217 };
218 this.getCommentType = function() {
218 this.getCommentType = function() {
219 return $(this.submitForm).find(this.commentType).val();
219 return $(this.submitForm).find(this.commentType).val();
220 };
220 };
221
221
222 this.getResolvesId = function() {
222 this.getResolvesId = function() {
223 return $(this.submitForm).find(this.resolvesId).val() || null;
223 return $(this.submitForm).find(this.resolvesId).val() || null;
224 };
224 };
225
225
226 this.getClosePr = function() {
226 this.getClosePr = function() {
227 return $(this.submitForm).find(this.closesPr).val() || null;
227 return $(this.submitForm).find(this.closesPr).val() || null;
228 };
228 };
229
229
230 this.markCommentResolved = function(resolvedCommentId){
230 this.markCommentResolved = function(resolvedCommentId){
231 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
231 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
232 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
232 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
233 };
233 };
234
234
235 this.isAllowedToSubmit = function() {
235 this.isAllowedToSubmit = function() {
236 return !$(this.submitButton).prop('disabled');
236 return !$(this.submitButton).prop('disabled');
237 };
237 };
238
238
239 this.initStatusChangeSelector = function(){
239 this.initStatusChangeSelector = function(){
240 var formatChangeStatus = function(state, escapeMarkup) {
240 var formatChangeStatus = function(state, escapeMarkup) {
241 var originalOption = state.element;
241 var originalOption = state.element;
242 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
242 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
243 return tmpl
243 return tmpl
244 };
244 };
245 var formatResult = function(result, container, query, escapeMarkup) {
245 var formatResult = function(result, container, query, escapeMarkup) {
246 return formatChangeStatus(result, escapeMarkup);
246 return formatChangeStatus(result, escapeMarkup);
247 };
247 };
248
248
249 var formatSelection = function(data, container, escapeMarkup) {
249 var formatSelection = function(data, container, escapeMarkup) {
250 return formatChangeStatus(data, escapeMarkup);
250 return formatChangeStatus(data, escapeMarkup);
251 };
251 };
252
252
253 $(this.submitForm).find(this.statusChange).select2({
253 $(this.submitForm).find(this.statusChange).select2({
254 placeholder: _gettext('Status Review'),
254 placeholder: _gettext('Status Review'),
255 formatResult: formatResult,
255 formatResult: formatResult,
256 formatSelection: formatSelection,
256 formatSelection: formatSelection,
257 containerCssClass: "drop-menu status_box_menu",
257 containerCssClass: "drop-menu status_box_menu",
258 dropdownCssClass: "drop-menu-dropdown",
258 dropdownCssClass: "drop-menu-dropdown",
259 dropdownAutoWidth: true,
259 dropdownAutoWidth: true,
260 minimumResultsForSearch: -1
260 minimumResultsForSearch: -1
261 });
261 });
262 $(this.submitForm).find(this.statusChange).on('change', function() {
262 $(this.submitForm).find(this.statusChange).on('change', function() {
263 var status = self.getCommentStatus();
263 var status = self.getCommentStatus();
264
264
265 if (status && !self.isInline()) {
265 if (status && !self.isInline()) {
266 $(self.submitButton).prop('disabled', false);
266 $(self.submitButton).prop('disabled', false);
267 }
267 }
268
268
269 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
269 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
270 self.setPlaceholder(placeholderText)
270 self.setPlaceholder(placeholderText)
271 })
271 })
272 };
272 };
273
273
274 // reset the comment form into it's original state
274 // reset the comment form into it's original state
275 this.resetCommentFormState = function(content) {
275 this.resetCommentFormState = function(content) {
276 content = content || '';
276 content = content || '';
277
277
278 $(this.editContainer).show();
278 $(this.editContainer).show();
279 $(this.editButton).parent().addClass('active');
279 $(this.editButton).parent().addClass('active');
280
280
281 $(this.previewContainer).hide();
281 $(this.previewContainer).hide();
282 $(this.previewButton).parent().removeClass('active');
282 $(this.previewButton).parent().removeClass('active');
283
283
284 this.setActionButtonsDisabled(true);
284 this.setActionButtonsDisabled(true);
285 self.cm.setValue(content);
285 self.cm.setValue(content);
286 self.cm.setOption("readOnly", false);
286 self.cm.setOption("readOnly", false);
287
287
288 if (this.resolvesId) {
288 if (this.resolvesId) {
289 // destroy the resolve action
289 // destroy the resolve action
290 $(this.resolvesId).parent().remove();
290 $(this.resolvesId).parent().remove();
291 }
291 }
292 // reset closingPR flag
292 // reset closingPR flag
293 $('.close-pr-input').remove();
293 $('.close-pr-input').remove();
294
294
295 $(this.statusChange).select2('readonly', false);
295 $(this.statusChange).select2('readonly', false);
296 };
296 };
297
297
298 this.globalSubmitSuccessCallback = function(){
298 this.globalSubmitSuccessCallback = function(){
299 // default behaviour is to call GLOBAL hook, if it's registered.
299 // default behaviour is to call GLOBAL hook, if it's registered.
300 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
300 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
301 commentFormGlobalSubmitSuccessCallback();
301 commentFormGlobalSubmitSuccessCallback();
302 }
302 }
303 };
303 };
304
304
305 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
305 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
306 return _submitAjaxPOST(url, postData, successHandler, failHandler);
306 return _submitAjaxPOST(url, postData, successHandler, failHandler);
307 };
307 };
308
308
309 // overwrite a submitHandler, we need to do it for inline comments
309 // overwrite a submitHandler, we need to do it for inline comments
310 this.setHandleFormSubmit = function(callback) {
310 this.setHandleFormSubmit = function(callback) {
311 this.handleFormSubmit = callback;
311 this.handleFormSubmit = callback;
312 };
312 };
313
313
314 // overwrite a submitSuccessHandler
314 // overwrite a submitSuccessHandler
315 this.setGlobalSubmitSuccessCallback = function(callback) {
315 this.setGlobalSubmitSuccessCallback = function(callback) {
316 this.globalSubmitSuccessCallback = callback;
316 this.globalSubmitSuccessCallback = callback;
317 };
317 };
318
318
319 // default handler for for submit for main comments
319 // default handler for for submit for main comments
320 this.handleFormSubmit = function() {
320 this.handleFormSubmit = function() {
321 var text = self.cm.getValue();
321 var text = self.cm.getValue();
322 var status = self.getCommentStatus();
322 var status = self.getCommentStatus();
323 var commentType = self.getCommentType();
323 var commentType = self.getCommentType();
324 var resolvesCommentId = self.getResolvesId();
324 var resolvesCommentId = self.getResolvesId();
325 var closePullRequest = self.getClosePr();
325 var closePullRequest = self.getClosePr();
326
326
327 if (text === "" && !status) {
327 if (text === "" && !status) {
328 return;
328 return;
329 }
329 }
330
330
331 var excludeCancelBtn = false;
331 var excludeCancelBtn = false;
332 var submitEvent = true;
332 var submitEvent = true;
333 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
333 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
334 self.cm.setOption("readOnly", true);
334 self.cm.setOption("readOnly", true);
335
335
336 var postData = {
336 var postData = {
337 'text': text,
337 'text': text,
338 'changeset_status': status,
338 'changeset_status': status,
339 'comment_type': commentType,
339 'comment_type': commentType,
340 'csrf_token': CSRF_TOKEN
340 'csrf_token': CSRF_TOKEN
341 };
341 };
342
342
343 if (resolvesCommentId) {
343 if (resolvesCommentId) {
344 postData['resolves_comment_id'] = resolvesCommentId;
344 postData['resolves_comment_id'] = resolvesCommentId;
345 }
345 }
346
346
347 if (closePullRequest) {
347 if (closePullRequest) {
348 postData['close_pull_request'] = true;
348 postData['close_pull_request'] = true;
349 }
349 }
350
350
351 var submitSuccessCallback = function(o) {
351 var submitSuccessCallback = function(o) {
352 // reload page if we change status for single commit.
352 // reload page if we change status for single commit.
353 if (status && self.commitId) {
353 if (status && self.commitId) {
354 location.reload(true);
354 location.reload(true);
355 } else {
355 } else {
356 $('#injected_page_comments').append(o.rendered_text);
356 $('#injected_page_comments').append(o.rendered_text);
357 self.resetCommentFormState();
357 self.resetCommentFormState();
358 timeagoActivate();
358 timeagoActivate();
359 tooltipActivate();
359 tooltipActivate();
360
360
361 // mark visually which comment was resolved
361 // mark visually which comment was resolved
362 if (resolvesCommentId) {
362 if (resolvesCommentId) {
363 self.markCommentResolved(resolvesCommentId);
363 self.markCommentResolved(resolvesCommentId);
364 }
364 }
365 }
365 }
366
366
367 // run global callback on submit
367 // run global callback on submit
368 self.globalSubmitSuccessCallback();
368 self.globalSubmitSuccessCallback();
369
369
370 };
370 };
371 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
371 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
372 var prefix = "Error while submitting comment.\n"
372 var prefix = "Error while submitting comment.\n"
373 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
373 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
374 ajaxErrorSwal(message);
374 ajaxErrorSwal(message);
375 self.resetCommentFormState(text);
375 self.resetCommentFormState(text);
376 };
376 };
377 self.submitAjaxPOST(
377 self.submitAjaxPOST(
378 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
378 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
379 };
379 };
380
380
381 this.previewSuccessCallback = function(o) {
381 this.previewSuccessCallback = function(o) {
382 $(self.previewBoxSelector).html(o);
382 $(self.previewBoxSelector).html(o);
383 $(self.previewBoxSelector).removeClass('unloaded');
383 $(self.previewBoxSelector).removeClass('unloaded');
384
384
385 // swap buttons, making preview active
385 // swap buttons, making preview active
386 $(self.previewButton).parent().addClass('active');
386 $(self.previewButton).parent().addClass('active');
387 $(self.editButton).parent().removeClass('active');
387 $(self.editButton).parent().removeClass('active');
388
388
389 // unlock buttons
389 // unlock buttons
390 self.setActionButtonsDisabled(false);
390 self.setActionButtonsDisabled(false);
391 };
391 };
392
392
393 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
393 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
394 excludeCancelBtn = excludeCancelBtn || false;
394 excludeCancelBtn = excludeCancelBtn || false;
395 submitEvent = submitEvent || false;
395 submitEvent = submitEvent || false;
396
396
397 $(this.editButton).prop('disabled', state);
397 $(this.editButton).prop('disabled', state);
398 $(this.previewButton).prop('disabled', state);
398 $(this.previewButton).prop('disabled', state);
399
399
400 if (!excludeCancelBtn) {
400 if (!excludeCancelBtn) {
401 $(this.cancelButton).prop('disabled', state);
401 $(this.cancelButton).prop('disabled', state);
402 }
402 }
403
403
404 var submitState = state;
404 var submitState = state;
405 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
405 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
406 // if the value of commit review status is set, we allow
406 // if the value of commit review status is set, we allow
407 // submit button, but only on Main form, isInline means inline
407 // submit button, but only on Main form, isInline means inline
408 submitState = false
408 submitState = false
409 }
409 }
410
410
411 $(this.submitButton).prop('disabled', submitState);
411 $(this.submitButton).prop('disabled', submitState);
412 if (submitEvent) {
412 if (submitEvent) {
413 $(this.submitButton).val(_gettext('Submitting...'));
413 $(this.submitButton).val(_gettext('Submitting...'));
414 } else {
414 } else {
415 $(this.submitButton).val(this.submitButtonText);
415 $(this.submitButton).val(this.submitButtonText);
416 }
416 }
417
417
418 };
418 };
419
419
420 // lock preview/edit/submit buttons on load, but exclude cancel button
420 // lock preview/edit/submit buttons on load, but exclude cancel button
421 var excludeCancelBtn = true;
421 var excludeCancelBtn = true;
422 this.setActionButtonsDisabled(true, excludeCancelBtn);
422 this.setActionButtonsDisabled(true, excludeCancelBtn);
423
423
424 // anonymous users don't have access to initialized CM instance
424 // anonymous users don't have access to initialized CM instance
425 if (this.cm !== undefined){
425 if (this.cm !== undefined){
426 this.cm.on('change', function(cMirror) {
426 this.cm.on('change', function(cMirror) {
427 if (cMirror.getValue() === "") {
427 if (cMirror.getValue() === "") {
428 self.setActionButtonsDisabled(true, excludeCancelBtn)
428 self.setActionButtonsDisabled(true, excludeCancelBtn)
429 } else {
429 } else {
430 self.setActionButtonsDisabled(false, excludeCancelBtn)
430 self.setActionButtonsDisabled(false, excludeCancelBtn)
431 }
431 }
432 });
432 });
433 }
433 }
434
434
435 $(this.editButton).on('click', function(e) {
435 $(this.editButton).on('click', function(e) {
436 e.preventDefault();
436 e.preventDefault();
437
437
438 $(self.previewButton).parent().removeClass('active');
438 $(self.previewButton).parent().removeClass('active');
439 $(self.previewContainer).hide();
439 $(self.previewContainer).hide();
440
440
441 $(self.editButton).parent().addClass('active');
441 $(self.editButton).parent().addClass('active');
442 $(self.editContainer).show();
442 $(self.editContainer).show();
443
443
444 });
444 });
445
445
446 $(this.previewButton).on('click', function(e) {
446 $(this.previewButton).on('click', function(e) {
447 e.preventDefault();
447 e.preventDefault();
448 var text = self.cm.getValue();
448 var text = self.cm.getValue();
449
449
450 if (text === "") {
450 if (text === "") {
451 return;
451 return;
452 }
452 }
453
453
454 var postData = {
454 var postData = {
455 'text': text,
455 'text': text,
456 'renderer': templateContext.visual.default_renderer,
456 'renderer': templateContext.visual.default_renderer,
457 'csrf_token': CSRF_TOKEN
457 'csrf_token': CSRF_TOKEN
458 };
458 };
459
459
460 // lock ALL buttons on preview
460 // lock ALL buttons on preview
461 self.setActionButtonsDisabled(true);
461 self.setActionButtonsDisabled(true);
462
462
463 $(self.previewBoxSelector).addClass('unloaded');
463 $(self.previewBoxSelector).addClass('unloaded');
464 $(self.previewBoxSelector).html(_gettext('Loading ...'));
464 $(self.previewBoxSelector).html(_gettext('Loading ...'));
465
465
466 $(self.editContainer).hide();
466 $(self.editContainer).hide();
467 $(self.previewContainer).show();
467 $(self.previewContainer).show();
468
468
469 // by default we reset state of comment preserving the text
469 // by default we reset state of comment preserving the text
470 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
470 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
471 var prefix = "Error while preview of comment.\n"
471 var prefix = "Error while preview of comment.\n"
472 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
472 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
473 ajaxErrorSwal(message);
473 ajaxErrorSwal(message);
474
474
475 self.resetCommentFormState(text)
475 self.resetCommentFormState(text)
476 };
476 };
477 self.submitAjaxPOST(
477 self.submitAjaxPOST(
478 self.previewUrl, postData, self.previewSuccessCallback,
478 self.previewUrl, postData, self.previewSuccessCallback,
479 previewFailCallback);
479 previewFailCallback);
480
480
481 $(self.previewButton).parent().addClass('active');
481 $(self.previewButton).parent().addClass('active');
482 $(self.editButton).parent().removeClass('active');
482 $(self.editButton).parent().removeClass('active');
483 });
483 });
484
484
485 $(this.submitForm).submit(function(e) {
485 $(this.submitForm).submit(function(e) {
486 e.preventDefault();
486 e.preventDefault();
487 var allowedToSubmit = self.isAllowedToSubmit();
487 var allowedToSubmit = self.isAllowedToSubmit();
488 if (!allowedToSubmit){
488 if (!allowedToSubmit){
489 return false;
489 return false;
490 }
490 }
491 self.handleFormSubmit();
491 self.handleFormSubmit();
492 });
492 });
493
493
494 }
494 }
495
495
496 return CommentForm;
496 return CommentForm;
497 });
497 });
498
498
499 /* selector for comment versions */
500 var initVersionSelector = function(selector, initialData) {
501
502 var formatResult = function(result, container, query, escapeMarkup) {
503
504 return renderTemplate('commentVersion', {
505 show_disabled: true,
506 version: result.comment_version,
507 user_name: result.comment_author_username,
508 gravatar_url: result.comment_author_gravatar,
509 size: 16,
510 timeago_component: result.comment_created_on,
511 })
512 };
513
514 $(selector).select2({
515 placeholder: "Edited",
516 containerCssClass: "drop-menu-comment-history",
517 dropdownCssClass: "drop-menu-dropdown",
518 dropdownAutoWidth: true,
519 minimumResultsForSearch: -1,
520 data: initialData,
521 formatResult: formatResult,
522 });
523
524 $(selector).on('select2-selecting', function (e) {
525 // hide the mast as we later do preventDefault()
526 $("#select2-drop-mask").click();
527 e.preventDefault();
528 e.choice.action();
529 });
530
531 $(selector).on("select2-open", function() {
532 timeagoActivate();
533 });
534 };
535
499 /* comments controller */
536 /* comments controller */
500 var CommentsController = function() {
537 var CommentsController = function() {
501 var mainComment = '#text';
538 var mainComment = '#text';
502 var self = this;
539 var self = this;
503
540
504 this.cancelComment = function (node) {
541 this.cancelComment = function (node) {
505 var $node = $(node);
542 var $node = $(node);
506 var edit = $(this).attr('edit');
543 var edit = $(this).attr('edit');
507 if (edit) {
544 if (edit) {
508 var $general_comments = null;
545 var $general_comments = null;
509 var $inline_comments = $node.closest('div.inline-comments');
546 var $inline_comments = $node.closest('div.inline-comments');
510 if (!$inline_comments.length) {
547 if (!$inline_comments.length) {
511 $general_comments = $('#comments');
548 $general_comments = $('#comments');
512 var $comment = $general_comments.parent().find('div.comment:hidden');
549 var $comment = $general_comments.parent().find('div.comment:hidden');
513 // show hidden general comment form
550 // show hidden general comment form
514 $('#cb-comment-general-form-placeholder').show();
551 $('#cb-comment-general-form-placeholder').show();
515 } else {
552 } else {
516 var $comment = $inline_comments.find('div.comment:hidden');
553 var $comment = $inline_comments.find('div.comment:hidden');
517 }
554 }
518 $comment.show();
555 $comment.show();
519 }
556 }
520 $node.closest('.comment-inline-form').remove();
557 $node.closest('.comment-inline-form').remove();
521 return false;
558 return false;
522 };
559 };
523
560
524 this.showVersion = function (node) {
561 this.showVersion = function (comment_id, comment_history_id) {
525 var $node = $(node);
526 var selectedIndex = $node.context.selectedIndex;
527 var option = $node.find('option[value="'+ selectedIndex +'"]');
528 var zero_option = $node.find('option[value="0"]');
529 if (!option){
530 return;
531 }
532
562
533 // little trick to cheat onchange and allow to display the same version again
534 $node.context.selectedIndex = 0;
535 zero_option.text(selectedIndex);
536
537 var comment_history_id = option.attr('data-comment-history-id');
538 var comment_id = option.attr('data-comment-id');
539 var historyViewUrl = pyroutes.url(
563 var historyViewUrl = pyroutes.url(
540 'repo_commit_comment_history_view',
564 'repo_commit_comment_history_view',
541 {
565 {
542 'repo_name': templateContext.repo_name,
566 'repo_name': templateContext.repo_name,
543 'commit_id': comment_id,
567 'commit_id': comment_id,
544 'comment_history_id': comment_history_id,
568 'comment_history_id': comment_history_id,
545 }
569 }
546 );
570 );
547 successRenderCommit = function (data) {
571 successRenderCommit = function (data) {
548 SwalNoAnimation.fire({
572 SwalNoAnimation.fire({
549 html: data,
573 html: data,
550 title: '',
574 title: '',
551 });
575 });
552 };
576 };
553 failRenderCommit = function () {
577 failRenderCommit = function () {
554 SwalNoAnimation.fire({
578 SwalNoAnimation.fire({
555 html: 'Error while loading comment history',
579 html: 'Error while loading comment history',
556 title: '',
580 title: '',
557 });
581 });
558 };
582 };
559 _submitAjaxPOST(
583 _submitAjaxPOST(
560 historyViewUrl, {'csrf_token': CSRF_TOKEN}, successRenderCommit,
584 historyViewUrl, {'csrf_token': CSRF_TOKEN},
585 successRenderCommit,
561 failRenderCommit
586 failRenderCommit
562 );
587 );
563 };
588 };
564
589
565 this.getLineNumber = function(node) {
590 this.getLineNumber = function(node) {
566 var $node = $(node);
591 var $node = $(node);
567 var lineNo = $node.closest('td').attr('data-line-no');
592 var lineNo = $node.closest('td').attr('data-line-no');
568 if (lineNo === undefined && $node.data('commentInline')){
593 if (lineNo === undefined && $node.data('commentInline')){
569 lineNo = $node.data('commentLineNo')
594 lineNo = $node.data('commentLineNo')
570 }
595 }
571
596
572 return lineNo
597 return lineNo
573 };
598 };
574
599
575 this.scrollToComment = function(node, offset, outdated) {
600 this.scrollToComment = function(node, offset, outdated) {
576 if (offset === undefined) {
601 if (offset === undefined) {
577 offset = 0;
602 offset = 0;
578 }
603 }
579 var outdated = outdated || false;
604 var outdated = outdated || false;
580 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
605 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
581
606
582 if (!node) {
607 if (!node) {
583 node = $('.comment-selected');
608 node = $('.comment-selected');
584 if (!node.length) {
609 if (!node.length) {
585 node = $('comment-current')
610 node = $('comment-current')
586 }
611 }
587 }
612 }
588
613
589 $wrapper = $(node).closest('div.comment');
614 $wrapper = $(node).closest('div.comment');
590
615
591 // show hidden comment when referenced.
616 // show hidden comment when referenced.
592 if (!$wrapper.is(':visible')){
617 if (!$wrapper.is(':visible')){
593 $wrapper.show();
618 $wrapper.show();
594 }
619 }
595
620
596 $comment = $(node).closest(klass);
621 $comment = $(node).closest(klass);
597 $comments = $(klass);
622 $comments = $(klass);
598
623
599 $('.comment-selected').removeClass('comment-selected');
624 $('.comment-selected').removeClass('comment-selected');
600
625
601 var nextIdx = $(klass).index($comment) + offset;
626 var nextIdx = $(klass).index($comment) + offset;
602 if (nextIdx >= $comments.length) {
627 if (nextIdx >= $comments.length) {
603 nextIdx = 0;
628 nextIdx = 0;
604 }
629 }
605 var $next = $(klass).eq(nextIdx);
630 var $next = $(klass).eq(nextIdx);
606
631
607 var $cb = $next.closest('.cb');
632 var $cb = $next.closest('.cb');
608 $cb.removeClass('cb-collapsed');
633 $cb.removeClass('cb-collapsed');
609
634
610 var $filediffCollapseState = $cb.closest('.filediff').prev();
635 var $filediffCollapseState = $cb.closest('.filediff').prev();
611 $filediffCollapseState.prop('checked', false);
636 $filediffCollapseState.prop('checked', false);
612 $next.addClass('comment-selected');
637 $next.addClass('comment-selected');
613 scrollToElement($next);
638 scrollToElement($next);
614 return false;
639 return false;
615 };
640 };
616
641
617 this.nextComment = function(node) {
642 this.nextComment = function(node) {
618 return self.scrollToComment(node, 1);
643 return self.scrollToComment(node, 1);
619 };
644 };
620
645
621 this.prevComment = function(node) {
646 this.prevComment = function(node) {
622 return self.scrollToComment(node, -1);
647 return self.scrollToComment(node, -1);
623 };
648 };
624
649
625 this.nextOutdatedComment = function(node) {
650 this.nextOutdatedComment = function(node) {
626 return self.scrollToComment(node, 1, true);
651 return self.scrollToComment(node, 1, true);
627 };
652 };
628
653
629 this.prevOutdatedComment = function(node) {
654 this.prevOutdatedComment = function(node) {
630 return self.scrollToComment(node, -1, true);
655 return self.scrollToComment(node, -1, true);
631 };
656 };
632
657
633 this._deleteComment = function(node) {
658 this._deleteComment = function(node) {
634 var $node = $(node);
659 var $node = $(node);
635 var $td = $node.closest('td');
660 var $td = $node.closest('td');
636 var $comment = $node.closest('.comment');
661 var $comment = $node.closest('.comment');
637 var comment_id = $comment.attr('data-comment-id');
662 var comment_id = $comment.attr('data-comment-id');
638 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
663 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
639 var postData = {
664 var postData = {
640 'csrf_token': CSRF_TOKEN
665 'csrf_token': CSRF_TOKEN
641 };
666 };
642
667
643 $comment.addClass('comment-deleting');
668 $comment.addClass('comment-deleting');
644 $comment.hide('fast');
669 $comment.hide('fast');
645
670
646 var success = function(response) {
671 var success = function(response) {
647 $comment.remove();
672 $comment.remove();
648 return false;
673 return false;
649 };
674 };
650 var failure = function(jqXHR, textStatus, errorThrown) {
675 var failure = function(jqXHR, textStatus, errorThrown) {
651 var prefix = "Error while deleting this comment.\n"
676 var prefix = "Error while deleting this comment.\n"
652 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
677 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
653 ajaxErrorSwal(message);
678 ajaxErrorSwal(message);
654
679
655 $comment.show('fast');
680 $comment.show('fast');
656 $comment.removeClass('comment-deleting');
681 $comment.removeClass('comment-deleting');
657 return false;
682 return false;
658 };
683 };
659 ajaxPOST(url, postData, success, failure);
684 ajaxPOST(url, postData, success, failure);
660 }
685 }
661
686
662 this.deleteComment = function(node) {
687 this.deleteComment = function(node) {
663 var $comment = $(node).closest('.comment');
688 var $comment = $(node).closest('.comment');
664 var comment_id = $comment.attr('data-comment-id');
689 var comment_id = $comment.attr('data-comment-id');
665
690
666 SwalNoAnimation.fire({
691 SwalNoAnimation.fire({
667 title: 'Delete this comment?',
692 title: 'Delete this comment?',
668 icon: 'warning',
693 icon: 'warning',
669 showCancelButton: true,
694 showCancelButton: true,
670 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
695 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
671
696
672 }).then(function(result) {
697 }).then(function(result) {
673 if (result.value) {
698 if (result.value) {
674 self._deleteComment(node);
699 self._deleteComment(node);
675 }
700 }
676 })
701 })
677 };
702 };
678
703
679 this.toggleWideMode = function (node) {
704 this.toggleWideMode = function (node) {
680 if ($('#content').hasClass('wrapper')) {
705 if ($('#content').hasClass('wrapper')) {
681 $('#content').removeClass("wrapper");
706 $('#content').removeClass("wrapper");
682 $('#content').addClass("wide-mode-wrapper");
707 $('#content').addClass("wide-mode-wrapper");
683 $(node).addClass('btn-success');
708 $(node).addClass('btn-success');
684 return true
709 return true
685 } else {
710 } else {
686 $('#content').removeClass("wide-mode-wrapper");
711 $('#content').removeClass("wide-mode-wrapper");
687 $('#content').addClass("wrapper");
712 $('#content').addClass("wrapper");
688 $(node).removeClass('btn-success');
713 $(node).removeClass('btn-success');
689 return false
714 return false
690 }
715 }
691
716
692 };
717 };
693
718
694 this.toggleComments = function(node, show) {
719 this.toggleComments = function(node, show) {
695 var $filediff = $(node).closest('.filediff');
720 var $filediff = $(node).closest('.filediff');
696 if (show === true) {
721 if (show === true) {
697 $filediff.removeClass('hide-comments');
722 $filediff.removeClass('hide-comments');
698 } else if (show === false) {
723 } else if (show === false) {
699 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
724 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
700 $filediff.addClass('hide-comments');
725 $filediff.addClass('hide-comments');
701 } else {
726 } else {
702 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
727 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
703 $filediff.toggleClass('hide-comments');
728 $filediff.toggleClass('hide-comments');
704 }
729 }
705 return false;
730 return false;
706 };
731 };
707
732
708 this.toggleLineComments = function(node) {
733 this.toggleLineComments = function(node) {
709 self.toggleComments(node, true);
734 self.toggleComments(node, true);
710 var $node = $(node);
735 var $node = $(node);
711 // mark outdated comments as visible before the toggle;
736 // mark outdated comments as visible before the toggle;
712 $(node.closest('tr')).find('.comment-outdated').show();
737 $(node.closest('tr')).find('.comment-outdated').show();
713 $node.closest('tr').toggleClass('hide-line-comments');
738 $node.closest('tr').toggleClass('hide-line-comments');
714 };
739 };
715
740
716 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
741 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
717 var pullRequestId = templateContext.pull_request_data.pull_request_id;
742 var pullRequestId = templateContext.pull_request_data.pull_request_id;
718 var commitId = templateContext.commit_data.commit_id;
743 var commitId = templateContext.commit_data.commit_id;
719
744
720 var commentForm = new CommentForm(
745 var commentForm = new CommentForm(
721 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
746 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
722 var cm = commentForm.getCmInstance();
747 var cm = commentForm.getCmInstance();
723
748
724 if (resolvesCommentId){
749 if (resolvesCommentId){
725 var placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
750 var placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
726 }
751 }
727
752
728 setTimeout(function() {
753 setTimeout(function() {
729 // callbacks
754 // callbacks
730 if (cm !== undefined) {
755 if (cm !== undefined) {
731 commentForm.setPlaceholder(placeholderText);
756 commentForm.setPlaceholder(placeholderText);
732 if (commentForm.isInline()) {
757 if (commentForm.isInline()) {
733 cm.focus();
758 cm.focus();
734 cm.refresh();
759 cm.refresh();
735 }
760 }
736 }
761 }
737 }, 10);
762 }, 10);
738
763
739 // trigger scrolldown to the resolve comment, since it might be away
764 // trigger scrolldown to the resolve comment, since it might be away
740 // from the clicked
765 // from the clicked
741 if (resolvesCommentId){
766 if (resolvesCommentId){
742 var actionNode = $(commentForm.resolvesActionId).offset();
767 var actionNode = $(commentForm.resolvesActionId).offset();
743
768
744 setTimeout(function() {
769 setTimeout(function() {
745 if (actionNode) {
770 if (actionNode) {
746 $('body, html').animate({scrollTop: actionNode.top}, 10);
771 $('body, html').animate({scrollTop: actionNode.top}, 10);
747 }
772 }
748 }, 100);
773 }, 100);
749 }
774 }
750
775
751 // add dropzone support
776 // add dropzone support
752 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
777 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
753 var renderer = templateContext.visual.default_renderer;
778 var renderer = templateContext.visual.default_renderer;
754 if (renderer == 'rst') {
779 if (renderer == 'rst') {
755 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
780 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
756 if (isRendered){
781 if (isRendered){
757 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
782 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
758 }
783 }
759 } else if (renderer == 'markdown') {
784 } else if (renderer == 'markdown') {
760 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
785 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
761 if (isRendered){
786 if (isRendered){
762 attachmentUrl = '!' + attachmentUrl;
787 attachmentUrl = '!' + attachmentUrl;
763 }
788 }
764 } else {
789 } else {
765 var attachmentUrl = '{}'.format(attachmentStoreUrl);
790 var attachmentUrl = '{}'.format(attachmentStoreUrl);
766 }
791 }
767 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
792 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
768
793
769 return false;
794 return false;
770 };
795 };
771
796
772 //see: https://www.dropzonejs.com/#configuration
797 //see: https://www.dropzonejs.com/#configuration
773 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
798 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
774 {'repo_name': templateContext.repo_name,
799 {'repo_name': templateContext.repo_name,
775 'commit_id': templateContext.commit_data.commit_id})
800 'commit_id': templateContext.commit_data.commit_id})
776
801
777 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
802 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
778 if (previewTmpl !== undefined){
803 if (previewTmpl !== undefined){
779 var selectLink = $(formElement).find('.pick-attachment').get(0);
804 var selectLink = $(formElement).find('.pick-attachment').get(0);
780 $(formElement).find('.comment-attachment-uploader').dropzone({
805 $(formElement).find('.comment-attachment-uploader').dropzone({
781 url: storeUrl,
806 url: storeUrl,
782 headers: {"X-CSRF-Token": CSRF_TOKEN},
807 headers: {"X-CSRF-Token": CSRF_TOKEN},
783 paramName: function () {
808 paramName: function () {
784 return "attachment"
809 return "attachment"
785 }, // The name that will be used to transfer the file
810 }, // The name that will be used to transfer the file
786 clickable: selectLink,
811 clickable: selectLink,
787 parallelUploads: 1,
812 parallelUploads: 1,
788 maxFiles: 10,
813 maxFiles: 10,
789 maxFilesize: templateContext.attachment_store.max_file_size_mb,
814 maxFilesize: templateContext.attachment_store.max_file_size_mb,
790 uploadMultiple: false,
815 uploadMultiple: false,
791 autoProcessQueue: true, // if false queue will not be processed automatically.
816 autoProcessQueue: true, // if false queue will not be processed automatically.
792 createImageThumbnails: false,
817 createImageThumbnails: false,
793 previewTemplate: previewTmpl.innerHTML,
818 previewTemplate: previewTmpl.innerHTML,
794
819
795 accept: function (file, done) {
820 accept: function (file, done) {
796 done();
821 done();
797 },
822 },
798 init: function () {
823 init: function () {
799
824
800 this.on("sending", function (file, xhr, formData) {
825 this.on("sending", function (file, xhr, formData) {
801 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
826 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
802 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
827 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
803 });
828 });
804
829
805 this.on("success", function (file, response) {
830 this.on("success", function (file, response) {
806 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
831 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
807 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
832 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
808
833
809 var isRendered = false;
834 var isRendered = false;
810 var ext = file.name.split('.').pop();
835 var ext = file.name.split('.').pop();
811 var imageExts = templateContext.attachment_store.image_ext;
836 var imageExts = templateContext.attachment_store.image_ext;
812 if (imageExts.indexOf(ext) !== -1){
837 if (imageExts.indexOf(ext) !== -1){
813 isRendered = true;
838 isRendered = true;
814 }
839 }
815
840
816 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
841 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
817 });
842 });
818
843
819 this.on("error", function (file, errorMessage, xhr) {
844 this.on("error", function (file, errorMessage, xhr) {
820 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
845 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
821
846
822 var error = null;
847 var error = null;
823
848
824 if (xhr !== undefined){
849 if (xhr !== undefined){
825 var httpStatus = xhr.status + " " + xhr.statusText;
850 var httpStatus = xhr.status + " " + xhr.statusText;
826 if (xhr !== undefined && xhr.status >= 500) {
851 if (xhr !== undefined && xhr.status >= 500) {
827 error = httpStatus;
852 error = httpStatus;
828 }
853 }
829 }
854 }
830
855
831 if (error === null) {
856 if (error === null) {
832 error = errorMessage.error || errorMessage || httpStatus;
857 error = errorMessage.error || errorMessage || httpStatus;
833 }
858 }
834 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
859 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
835
860
836 });
861 });
837 }
862 }
838 });
863 });
839 }
864 }
840 return commentForm;
865 return commentForm;
841 };
866 };
842
867
843 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
868 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
844
869
845 var tmpl = $('#cb-comment-general-form-template').html();
870 var tmpl = $('#cb-comment-general-form-template').html();
846 tmpl = tmpl.format(null, 'general');
871 tmpl = tmpl.format(null, 'general');
847 var $form = $(tmpl);
872 var $form = $(tmpl);
848
873
849 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
874 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
850 var curForm = $formPlaceholder.find('form');
875 var curForm = $formPlaceholder.find('form');
851 if (curForm){
876 if (curForm){
852 curForm.remove();
877 curForm.remove();
853 }
878 }
854 $formPlaceholder.append($form);
879 $formPlaceholder.append($form);
855
880
856 var _form = $($form[0]);
881 var _form = $($form[0]);
857 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
882 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
858 var edit = false;
883 var edit = false;
859 var comment_id = null;
884 var comment_id = null;
860 var commentForm = this.createCommentForm(
885 var commentForm = this.createCommentForm(
861 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
886 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
862 commentForm.initStatusChangeSelector();
887 commentForm.initStatusChangeSelector();
863
888
864 return commentForm;
889 return commentForm;
865 };
890 };
891
866 this.editComment = function(node) {
892 this.editComment = function(node) {
867 var $node = $(node);
893 var $node = $(node);
868 var $comment = $(node).closest('.comment');
894 var $comment = $(node).closest('.comment');
869 var comment_id = $comment.attr('data-comment-id');
895 var comment_id = $comment.attr('data-comment-id');
870 var $form = null
896 var $form = null
871
897
872 var $comments = $node.closest('div.inline-comments');
898 var $comments = $node.closest('div.inline-comments');
873 var $general_comments = null;
899 var $general_comments = null;
874 var lineno = null;
900 var lineno = null;
875
901
876 if($comments.length){
902 if($comments.length){
877 // inline comments setup
903 // inline comments setup
878 $form = $comments.find('.comment-inline-form');
904 $form = $comments.find('.comment-inline-form');
879 lineno = self.getLineNumber(node)
905 lineno = self.getLineNumber(node)
880 }
906 }
881 else{
907 else{
882 // general comments setup
908 // general comments setup
883 $comments = $('#comments');
909 $comments = $('#comments');
884 $form = $comments.find('.comment-inline-form');
910 $form = $comments.find('.comment-inline-form');
885 lineno = $comment[0].id
911 lineno = $comment[0].id
886 $('#cb-comment-general-form-placeholder').hide();
912 $('#cb-comment-general-form-placeholder').hide();
887 }
913 }
888
914
889 this.edit = true;
915 this.edit = true;
890
916
891 if (!$form.length) {
917 if (!$form.length) {
892
918
893 var $filediff = $node.closest('.filediff');
919 var $filediff = $node.closest('.filediff');
894 $filediff.removeClass('hide-comments');
920 $filediff.removeClass('hide-comments');
895 var f_path = $filediff.attr('data-f-path');
921 var f_path = $filediff.attr('data-f-path');
896
922
897 // create a new HTML from template
923 // create a new HTML from template
898
924
899 var tmpl = $('#cb-comment-inline-form-template').html();
925 var tmpl = $('#cb-comment-inline-form-template').html();
900 tmpl = tmpl.format(escapeHtml(f_path), lineno);
926 tmpl = tmpl.format(escapeHtml(f_path), lineno);
901 $form = $(tmpl);
927 $form = $(tmpl);
902 $comment.after($form)
928 $comment.after($form)
903
929
904 var _form = $($form[0]).find('form');
930 var _form = $($form[0]).find('form');
905 var autocompleteActions = ['as_note',];
931 var autocompleteActions = ['as_note',];
906 var commentForm = this.createCommentForm(
932 var commentForm = this.createCommentForm(
907 _form, lineno, '', autocompleteActions, resolvesCommentId,
933 _form, lineno, '', autocompleteActions, resolvesCommentId,
908 this.edit, comment_id);
934 this.edit, comment_id);
909 var old_comment_text_binary = $comment.attr('data-comment-text');
935 var old_comment_text_binary = $comment.attr('data-comment-text');
910 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
936 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
911 commentForm.cm.setValue(old_comment_text);
937 commentForm.cm.setValue(old_comment_text);
912 $comment.hide();
938 $comment.hide();
913
939
914 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
940 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
915 form: _form,
941 form: _form,
916 parent: $comments,
942 parent: $comments,
917 lineno: lineno,
943 lineno: lineno,
918 f_path: f_path}
944 f_path: f_path}
919 );
945 );
946
920 // set a CUSTOM submit handler for inline comments.
947 // set a CUSTOM submit handler for inline comments.
921 commentForm.setHandleFormSubmit(function(o) {
948 commentForm.setHandleFormSubmit(function(o) {
922 var text = commentForm.cm.getValue();
949 var text = commentForm.cm.getValue();
923 var commentType = commentForm.getCommentType();
950 var commentType = commentForm.getCommentType();
924 var resolvesCommentId = commentForm.getResolvesId();
925
951
926 if (text === "") {
952 if (text === "") {
927 return;
953 return;
928 }
954 }
955
929 if (old_comment_text == text) {
956 if (old_comment_text == text) {
930 SwalNoAnimation.fire({
957 SwalNoAnimation.fire({
931 title: 'Unable to edit comment',
958 title: 'Unable to edit comment',
932 html: _gettext('Comment body was not changed.'),
959 html: _gettext('Comment body was not changed.'),
933 });
960 });
934 return;
961 return;
935 }
962 }
936 var excludeCancelBtn = false;
963 var excludeCancelBtn = false;
937 var submitEvent = true;
964 var submitEvent = true;
938 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
965 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
939 commentForm.cm.setOption("readOnly", true);
966 commentForm.cm.setOption("readOnly", true);
940 var dropDown = $('#comment_history_for_comment_'+comment_id);
941
967
942 var version = dropDown.children().last().val()
968 // Read last version known
969 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
970 var version = versionSelector.data('lastVersion');
971
943 if(!version){
972 if (!version) {
944 version = 0;
973 version = 0;
945 }
974 }
975
946 var postData = {
976 var postData = {
947 'text': text,
977 'text': text,
948 'f_path': f_path,
978 'f_path': f_path,
949 'line': lineno,
979 'line': lineno,
950 'comment_type': commentType,
980 'comment_type': commentType,
951 'csrf_token': CSRF_TOKEN,
952 'version': version,
981 'version': version,
982 'csrf_token': CSRF_TOKEN
953 };
983 };
954
984
955 var submitSuccessCallback = function(json_data) {
985 var submitSuccessCallback = function(json_data) {
956 $form.remove();
986 $form.remove();
957 $comment.show();
987 $comment.show();
958 var postData = {
988 var postData = {
959 'text': text,
989 'text': text,
960 'renderer': $comment.attr('data-comment-renderer'),
990 'renderer': $comment.attr('data-comment-renderer'),
961 'csrf_token': CSRF_TOKEN
991 'csrf_token': CSRF_TOKEN
962 };
992 };
963
993
994 /* Inject new edited version selector */
964 var updateCommentVersionDropDown = function () {
995 var updateCommentVersionDropDown = function () {
965 var dropDown = $('#comment_history_for_comment_'+comment_id);
996 var versionSelectId = '#comment_versions_'+comment_id;
997 var preLoadVersionData = [
998 {
999 id: json_data['comment_version'],
1000 text: "v{0}".format(json_data['comment_version']),
1001 action: function () {
1002 Rhodecode.comments.showVersion(
1003 json_data['comment_id'],
1004 json_data['comment_history_id']
1005 )
1006 },
1007 comment_version: json_data['comment_version'],
1008 comment_author_username: json_data['comment_author_username'],
1009 comment_author_gravatar: json_data['comment_author_gravatar'],
1010 comment_created_on: json_data['comment_created_on'],
1011 },
1012 ]
1013
1014
1015 if ($(versionSelectId).data('select2')) {
1016 var oldData = $(versionSelectId).data('select2').opts.data.results;
1017 $(versionSelectId).select2("destroy");
1018 preLoadVersionData = oldData.concat(preLoadVersionData)
1019 }
1020
1021 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1022
966 $comment.attr('data-comment-text', btoa(text));
1023 $comment.attr('data-comment-text', btoa(text));
967 var version = json_data['comment_version']
1024
968 var option = new Option(version, version);
1025 var versionSelector = $('#comment_versions_'+comment_id);
969 var $option = $(option);
1026
970 $option.attr('data-comment-history-id', json_data['comment_history_id']);
1027 // set lastVersion so we know our last edit version
971 $option.attr('data-comment-id', json_data['comment_id']);
1028 versionSelector.data('lastVersion', json_data['comment_version'])
972 dropDown.append(option);
1029 versionSelector.parent().show();
973 dropDown.parent().show();
974 }
1030 }
975 updateCommentVersionDropDown();
1031 updateCommentVersionDropDown();
1032
976 // by default we reset state of comment preserving the text
1033 // by default we reset state of comment preserving the text
977 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1034 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
978 var prefix = "Error while editing of comment.\n"
1035 var prefix = "Error while editing this comment.\n"
979 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1036 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
980 ajaxErrorSwal(message);
1037 ajaxErrorSwal(message);
1038 };
981
1039
982 };
983 var successRenderCommit = function(o){
1040 var successRenderCommit = function(o){
984 $comment.show();
1041 $comment.show();
985 $comment[0].lastElementChild.innerHTML = o;
1042 $comment[0].lastElementChild.innerHTML = o;
986 }
1043 };
987 var previewUrl = pyroutes.url('repo_commit_comment_preview',
1044
1045 var previewUrl = pyroutes.url(
1046 'repo_commit_comment_preview',
988 {'repo_name': templateContext.repo_name,
1047 {'repo_name': templateContext.repo_name,
989 'commit_id': templateContext.commit_data.commit_id});
1048 'commit_id': templateContext.commit_data.commit_id});
990
1049
991 _submitAjaxPOST(
1050 _submitAjaxPOST(
992 previewUrl, postData, successRenderCommit,
1051 previewUrl, postData, successRenderCommit,
993 failRenderCommit
1052 failRenderCommit
994 );
1053 );
995
1054
996 try {
1055 try {
997 var html = json_data.rendered_text;
1056 var html = json_data.rendered_text;
998 var lineno = json_data.line_no;
1057 var lineno = json_data.line_no;
999 var target_id = json_data.target_id;
1058 var target_id = json_data.target_id;
1000
1059
1001 $comments.find('.cb-comment-add-button').before(html);
1060 $comments.find('.cb-comment-add-button').before(html);
1002
1061
1003 //mark visually which comment was resolved
1004 if (resolvesCommentId) {
1005 commentForm.markCommentResolved(resolvesCommentId);
1006 }
1007
1008 // run global callback on submit
1062 // run global callback on submit
1009 commentForm.globalSubmitSuccessCallback();
1063 commentForm.globalSubmitSuccessCallback();
1010
1064
1011 } catch (e) {
1065 } catch (e) {
1012 console.error(e);
1066 console.error(e);
1013 }
1067 }
1014
1068
1015 // re trigger the linkification of next/prev navigation
1069 // re trigger the linkification of next/prev navigation
1016 linkifyComments($('.inline-comment-injected'));
1070 linkifyComments($('.inline-comment-injected'));
1017 timeagoActivate();
1071 timeagoActivate();
1018 tooltipActivate();
1072 tooltipActivate();
1019
1073
1020 if (window.updateSticky !== undefined) {
1074 if (window.updateSticky !== undefined) {
1021 // potentially our comments change the active window size, so we
1075 // potentially our comments change the active window size, so we
1022 // notify sticky elements
1076 // notify sticky elements
1023 updateSticky()
1077 updateSticky()
1024 }
1078 }
1025
1079
1026 commentForm.setActionButtonsDisabled(false);
1080 commentForm.setActionButtonsDisabled(false);
1027
1081
1028 };
1082 };
1029 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1083 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1030 var prefix = "Error while editing comment.\n"
1084 var prefix = "Error while editing comment.\n"
1031 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1085 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1086 if (jqXHR.status == 409){
1087 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1088 ajaxErrorSwal(message, 'Comment version mismatch.');
1089 } else {
1032 ajaxErrorSwal(message);
1090 ajaxErrorSwal(message);
1091 }
1092
1033 commentForm.resetCommentFormState(text)
1093 commentForm.resetCommentFormState(text)
1034 };
1094 };
1035 commentForm.submitAjaxPOST(
1095 commentForm.submitAjaxPOST(
1036 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1096 commentForm.submitUrl, postData,
1097 submitSuccessCallback,
1098 submitFailCallback);
1037 });
1099 });
1038 }
1100 }
1039
1101
1040 $form.addClass('comment-inline-form-open');
1102 $form.addClass('comment-inline-form-open');
1041 };
1103 };
1104
1042 this.createComment = function(node, resolutionComment) {
1105 this.createComment = function(node, resolutionComment) {
1043 var resolvesCommentId = resolutionComment || null;
1106 var resolvesCommentId = resolutionComment || null;
1044 var $node = $(node);
1107 var $node = $(node);
1045 var $td = $node.closest('td');
1108 var $td = $node.closest('td');
1046 var $form = $td.find('.comment-inline-form');
1109 var $form = $td.find('.comment-inline-form');
1047 this.edit = false;
1110 this.edit = false;
1048
1111
1049 if (!$form.length) {
1112 if (!$form.length) {
1050
1113
1051 var $filediff = $node.closest('.filediff');
1114 var $filediff = $node.closest('.filediff');
1052 $filediff.removeClass('hide-comments');
1115 $filediff.removeClass('hide-comments');
1053 var f_path = $filediff.attr('data-f-path');
1116 var f_path = $filediff.attr('data-f-path');
1054 var lineno = self.getLineNumber(node);
1117 var lineno = self.getLineNumber(node);
1055 // create a new HTML from template
1118 // create a new HTML from template
1056 var tmpl = $('#cb-comment-inline-form-template').html();
1119 var tmpl = $('#cb-comment-inline-form-template').html();
1057 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1120 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1058 $form = $(tmpl);
1121 $form = $(tmpl);
1059
1122
1060 var $comments = $td.find('.inline-comments');
1123 var $comments = $td.find('.inline-comments');
1061 if (!$comments.length) {
1124 if (!$comments.length) {
1062 $comments = $(
1125 $comments = $(
1063 $('#cb-comments-inline-container-template').html());
1126 $('#cb-comments-inline-container-template').html());
1064 $td.append($comments);
1127 $td.append($comments);
1065 }
1128 }
1066
1129
1067 $td.find('.cb-comment-add-button').before($form);
1130 $td.find('.cb-comment-add-button').before($form);
1068
1131
1069 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
1132 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
1070 var _form = $($form[0]).find('form');
1133 var _form = $($form[0]).find('form');
1071 var autocompleteActions = ['as_note', 'as_todo'];
1134 var autocompleteActions = ['as_note', 'as_todo'];
1072 var comment_id=null;
1135 var comment_id=null;
1073 var commentForm = this.createCommentForm(
1136 var commentForm = this.createCommentForm(
1074 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
1137 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
1075
1138
1076 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1139 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1077 form: _form,
1140 form: _form,
1078 parent: $td[0],
1141 parent: $td[0],
1079 lineno: lineno,
1142 lineno: lineno,
1080 f_path: f_path}
1143 f_path: f_path}
1081 );
1144 );
1082
1145
1083 // set a CUSTOM submit handler for inline comments.
1146 // set a CUSTOM submit handler for inline comments.
1084 commentForm.setHandleFormSubmit(function(o) {
1147 commentForm.setHandleFormSubmit(function(o) {
1085 var text = commentForm.cm.getValue();
1148 var text = commentForm.cm.getValue();
1086 var commentType = commentForm.getCommentType();
1149 var commentType = commentForm.getCommentType();
1087 var resolvesCommentId = commentForm.getResolvesId();
1150 var resolvesCommentId = commentForm.getResolvesId();
1088
1151
1089 if (text === "") {
1152 if (text === "") {
1090 return;
1153 return;
1091 }
1154 }
1092
1155
1093 if (lineno === undefined) {
1156 if (lineno === undefined) {
1094 alert('missing line !');
1157 alert('missing line !');
1095 return;
1158 return;
1096 }
1159 }
1097 if (f_path === undefined) {
1160 if (f_path === undefined) {
1098 alert('missing file path !');
1161 alert('missing file path !');
1099 return;
1162 return;
1100 }
1163 }
1101
1164
1102 var excludeCancelBtn = false;
1165 var excludeCancelBtn = false;
1103 var submitEvent = true;
1166 var submitEvent = true;
1104 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1167 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1105 commentForm.cm.setOption("readOnly", true);
1168 commentForm.cm.setOption("readOnly", true);
1106 var postData = {
1169 var postData = {
1107 'text': text,
1170 'text': text,
1108 'f_path': f_path,
1171 'f_path': f_path,
1109 'line': lineno,
1172 'line': lineno,
1110 'comment_type': commentType,
1173 'comment_type': commentType,
1111 'csrf_token': CSRF_TOKEN
1174 'csrf_token': CSRF_TOKEN
1112 };
1175 };
1113 if (resolvesCommentId){
1176 if (resolvesCommentId){
1114 postData['resolves_comment_id'] = resolvesCommentId;
1177 postData['resolves_comment_id'] = resolvesCommentId;
1115 }
1178 }
1116
1179
1117 var submitSuccessCallback = function(json_data) {
1180 var submitSuccessCallback = function(json_data) {
1118 $form.remove();
1181 $form.remove();
1119 try {
1182 try {
1120 var html = json_data.rendered_text;
1183 var html = json_data.rendered_text;
1121 var lineno = json_data.line_no;
1184 var lineno = json_data.line_no;
1122 var target_id = json_data.target_id;
1185 var target_id = json_data.target_id;
1123
1186
1124 $comments.find('.cb-comment-add-button').before(html);
1187 $comments.find('.cb-comment-add-button').before(html);
1125
1188
1126 //mark visually which comment was resolved
1189 //mark visually which comment was resolved
1127 if (resolvesCommentId) {
1190 if (resolvesCommentId) {
1128 commentForm.markCommentResolved(resolvesCommentId);
1191 commentForm.markCommentResolved(resolvesCommentId);
1129 }
1192 }
1130
1193
1131 // run global callback on submit
1194 // run global callback on submit
1132 commentForm.globalSubmitSuccessCallback();
1195 commentForm.globalSubmitSuccessCallback();
1133
1196
1134 } catch (e) {
1197 } catch (e) {
1135 console.error(e);
1198 console.error(e);
1136 }
1199 }
1137
1200
1138 // re trigger the linkification of next/prev navigation
1201 // re trigger the linkification of next/prev navigation
1139 linkifyComments($('.inline-comment-injected'));
1202 linkifyComments($('.inline-comment-injected'));
1140 timeagoActivate();
1203 timeagoActivate();
1141 tooltipActivate();
1204 tooltipActivate();
1142
1205
1143 if (window.updateSticky !== undefined) {
1206 if (window.updateSticky !== undefined) {
1144 // potentially our comments change the active window size, so we
1207 // potentially our comments change the active window size, so we
1145 // notify sticky elements
1208 // notify sticky elements
1146 updateSticky()
1209 updateSticky()
1147 }
1210 }
1148
1211
1149 commentForm.setActionButtonsDisabled(false);
1212 commentForm.setActionButtonsDisabled(false);
1150
1213
1151 };
1214 };
1152 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1215 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1153 var prefix = "Error while submitting comment.\n"
1216 var prefix = "Error while submitting comment.\n"
1154 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1217 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1155 ajaxErrorSwal(message);
1218 ajaxErrorSwal(message);
1156 commentForm.resetCommentFormState(text)
1219 commentForm.resetCommentFormState(text)
1157 };
1220 };
1158 commentForm.submitAjaxPOST(
1221 commentForm.submitAjaxPOST(
1159 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1222 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1160 });
1223 });
1161 }
1224 }
1162
1225
1163 $form.addClass('comment-inline-form-open');
1226 $form.addClass('comment-inline-form-open');
1164 };
1227 };
1165
1228
1166 this.createResolutionComment = function(commentId){
1229 this.createResolutionComment = function(commentId){
1167 // hide the trigger text
1230 // hide the trigger text
1168 $('#resolve-comment-{0}'.format(commentId)).hide();
1231 $('#resolve-comment-{0}'.format(commentId)).hide();
1169
1232
1170 var comment = $('#comment-'+commentId);
1233 var comment = $('#comment-'+commentId);
1171 var commentData = comment.data();
1234 var commentData = comment.data();
1172 if (commentData.commentInline) {
1235 if (commentData.commentInline) {
1173 this.createComment(comment, commentId)
1236 this.createComment(comment, commentId)
1174 } else {
1237 } else {
1175 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
1238 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
1176 }
1239 }
1177
1240
1178 return false;
1241 return false;
1179 };
1242 };
1180
1243
1181 this.submitResolution = function(commentId){
1244 this.submitResolution = function(commentId){
1182 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1245 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1183 var commentForm = form.get(0).CommentForm;
1246 var commentForm = form.get(0).CommentForm;
1184
1247
1185 var cm = commentForm.getCmInstance();
1248 var cm = commentForm.getCmInstance();
1186 var renderer = templateContext.visual.default_renderer;
1249 var renderer = templateContext.visual.default_renderer;
1187 if (renderer == 'rst'){
1250 if (renderer == 'rst'){
1188 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1251 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1189 } else if (renderer == 'markdown') {
1252 } else if (renderer == 'markdown') {
1190 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1253 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1191 } else {
1254 } else {
1192 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1255 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1193 }
1256 }
1194
1257
1195 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1258 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1196 form.submit();
1259 form.submit();
1197 return false;
1260 return false;
1198 };
1261 };
1199
1262
1200 };
1263 };
@@ -1,175 +1,178 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 /**
19 /**
20 * turns objects into GET query string
20 * turns objects into GET query string
21 */
21 */
22 var toQueryString = function(o) {
22 var toQueryString = function(o) {
23 if(typeof o === 'string') {
23 if(typeof o === 'string') {
24 return o;
24 return o;
25 }
25 }
26 if(typeof o !== 'object') {
26 if(typeof o !== 'object') {
27 return false;
27 return false;
28 }
28 }
29 var _p, _qs = [];
29 var _p, _qs = [];
30 for(_p in o) {
30 for(_p in o) {
31 _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
31 _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
32 }
32 }
33 return _qs.join('&');
33 return _qs.join('&');
34 };
34 };
35
35
36 /**
36 /**
37 * ajax call wrappers
37 * ajax call wrappers
38 */
38 */
39
39
40 var ajaxGET = function (url, success, failure) {
40 var ajaxGET = function (url, success, failure) {
41 var sUrl = url;
41 var sUrl = url;
42 var request = $.ajax({
42 var request = $.ajax({
43 url: sUrl,
43 url: sUrl,
44 headers: {'X-PARTIAL-XHR': true}
44 headers: {'X-PARTIAL-XHR': true}
45 })
45 })
46 .done(function (data) {
46 .done(function (data) {
47 success(data);
47 success(data);
48 })
48 })
49 .fail(function (jqXHR, textStatus, errorThrown) {
49 .fail(function (jqXHR, textStatus, errorThrown) {
50 if (failure) {
50 if (failure) {
51 failure(jqXHR, textStatus, errorThrown);
51 failure(jqXHR, textStatus, errorThrown);
52 } else {
52 } else {
53 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
53 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
54 ajaxErrorSwal(message);
54 ajaxErrorSwal(message);
55 }
55 }
56 });
56 });
57 return request;
57 return request;
58 };
58 };
59
59
60 var ajaxPOST = function (url, postData, success, failure) {
60 var ajaxPOST = function (url, postData, success, failure) {
61 var sUrl = url;
61 var sUrl = url;
62 var postData = toQueryString(postData);
62 var postData = toQueryString(postData);
63 var request = $.ajax({
63 var request = $.ajax({
64 type: 'POST',
64 type: 'POST',
65 url: sUrl,
65 url: sUrl,
66 data: postData,
66 data: postData,
67 headers: {'X-PARTIAL-XHR': true}
67 headers: {'X-PARTIAL-XHR': true}
68 })
68 })
69 .done(function (data) {
69 .done(function (data) {
70 success(data);
70 success(data);
71 })
71 })
72 .fail(function (jqXHR, textStatus, errorThrown) {
72 .fail(function (jqXHR, textStatus, errorThrown) {
73 if (failure) {
73 if (failure) {
74 failure(jqXHR, textStatus, errorThrown);
74 failure(jqXHR, textStatus, errorThrown);
75 } else {
75 } else {
76 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
76 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
77 ajaxErrorSwal(message);
77 ajaxErrorSwal(message);
78 }
78 }
79 });
79 });
80 return request;
80 return request;
81 };
81 };
82
82
83
83
84 SwalNoAnimation = Swal.mixin({
84 SwalNoAnimation = Swal.mixin({
85 confirmButtonColor: '#84a5d2',
85 confirmButtonColor: '#84a5d2',
86 cancelButtonColor: '#e85e4d',
86 cancelButtonColor: '#e85e4d',
87 showClass: {
87 showClass: {
88 popup: 'swal2-noanimation',
88 popup: 'swal2-noanimation',
89 backdrop: 'swal2-noanimation'
89 backdrop: 'swal2-noanimation'
90 },
90 },
91 hideClass: {
91 hideClass: {
92 popup: '',
92 popup: '',
93 backdrop: ''
93 backdrop: ''
94 },
94 },
95 })
95 })
96
96
97
97
98 /* Example usage:
98 /* Example usage:
99 *
99 *
100 error: function(jqXHR, textStatus, errorThrown) {
100 error: function(jqXHR, textStatus, errorThrown) {
101 var prefix = "Error while fetching entries.\n"
101 var prefix = "Error while fetching entries.\n"
102 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
102 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
103 ajaxErrorSwal(message);
103 ajaxErrorSwal(message);
104 }
104 }
105 *
105 *
106 * */
106 * */
107 function formatErrorMessage(jqXHR, textStatus, errorThrown, prefix) {
107 function formatErrorMessage(jqXHR, textStatus, errorThrown, prefix) {
108 if(typeof prefix === "undefined") {
108 if(typeof prefix === "undefined") {
109 prefix = ''
109 prefix = ''
110 }
110 }
111
111
112 if (jqXHR.status === 0) {
112 if (jqXHR.status === 0) {
113 return (prefix + 'Not connected.\nPlease verify your network connection.');
113 return (prefix + 'Not connected.\nPlease verify your network connection.');
114 } else if (jqXHR.status == 401) {
114 } else if (jqXHR.status == 401) {
115 return (prefix + 'Unauthorized access. [401]');
115 return (prefix + 'Unauthorized access. [401]');
116 } else if (jqXHR.status == 404) {
116 } else if (jqXHR.status == 404) {
117 return (prefix + 'The requested page not found. [404]');
117 return (prefix + 'The requested page not found. [404]');
118 } else if (jqXHR.status == 500) {
118 } else if (jqXHR.status == 500) {
119 return (prefix + 'Internal Server Error [500].');
119 return (prefix + 'Internal Server Error [500].');
120 } else if (jqXHR.status == 503) {
120 } else if (jqXHR.status == 503) {
121 return (prefix + 'Service unavailable [503].');
121 return (prefix + 'Service unavailable [503].');
122 } else if (errorThrown === 'parsererror') {
122 } else if (errorThrown === 'parsererror') {
123 return (prefix + 'Requested JSON parse failed.');
123 return (prefix + 'Requested JSON parse failed.');
124 } else if (errorThrown === 'timeout') {
124 } else if (errorThrown === 'timeout') {
125 return (prefix + 'Time out error.');
125 return (prefix + 'Time out error.');
126 } else if (errorThrown === 'abort') {
126 } else if (errorThrown === 'abort') {
127 return (prefix + 'Ajax request aborted.');
127 return (prefix + 'Ajax request aborted.');
128 } else {
128 } else {
129 return (prefix + 'Uncaught Error.\n' + jqXHR.responseText);
129 return (prefix + 'Uncaught Error.\n' + jqXHR.responseText);
130 }
130 }
131 }
131 }
132
132
133 function ajaxErrorSwal(message) {
133 function ajaxErrorSwal(message, title) {
134
135 var title = (typeof title !== 'undefined') ? title : _gettext('Ajax Request Error');
136
134 SwalNoAnimation.fire({
137 SwalNoAnimation.fire({
135 icon: 'error',
138 icon: 'error',
136 title: _gettext('Ajax Request Error'),
139 title: title,
137 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
140 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
138 showClass: {
141 showClass: {
139 popup: 'swal2-noanimation',
142 popup: 'swal2-noanimation',
140 backdrop: 'swal2-noanimation'
143 backdrop: 'swal2-noanimation'
141 },
144 },
142 hideClass: {
145 hideClass: {
143 popup: '',
146 popup: '',
144 backdrop: ''
147 backdrop: ''
145 }
148 }
146 })
149 })
147 }
150 }
148
151
149 /*
152 /*
150 * use in onclick attributes e.g
153 * use in onclick attributes e.g
151 * onclick="submitConfirm(event, this, _gettext('Confirm to delete '), _gettext('Confirm Delete'), 'what we delete')">
154 * onclick="submitConfirm(event, this, _gettext('Confirm to delete '), _gettext('Confirm Delete'), 'what we delete')">
152 * */
155 * */
153 function submitConfirm(event, self, question, confirmText, htmlText) {
156 function submitConfirm(event, self, question, confirmText, htmlText) {
154 if (htmlText === "undefined") {
157 if (htmlText === "undefined") {
155 htmlText = null;
158 htmlText = null;
156 }
159 }
157 if (confirmText === "undefined") {
160 if (confirmText === "undefined") {
158 confirmText = _gettext('Delete')
161 confirmText = _gettext('Delete')
159 }
162 }
160 event.preventDefault();
163 event.preventDefault();
161
164
162 SwalNoAnimation.fire({
165 SwalNoAnimation.fire({
163 title: question,
166 title: question,
164 icon: 'warning',
167 icon: 'warning',
165 html: htmlText,
168 html: htmlText,
166
169
167 showCancelButton: true,
170 showCancelButton: true,
168
171
169 confirmButtonText: confirmText
172 confirmButtonText: confirmText
170 }).then(function(result) {
173 }).then(function(result) {
171 if (result.value) {
174 if (result.value) {
172 $(self).closest("form").submit();
175 $(self).closest("form").submit();
173 }
176 }
174 })
177 })
175 } No newline at end of file
178 }
@@ -1,467 +1,479 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6
6
7 <%!
7 <%!
8 from rhodecode.lib import html_filters
8 from rhodecode.lib import html_filters
9 %>
9 %>
10
10
11 <%namespace name="base" file="/base/base.mako"/>
11 <%namespace name="base" file="/base/base.mako"/>
12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
13 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
13 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
14 <% latest_ver = len(getattr(c, 'versions', [])) %>
14 <% latest_ver = len(getattr(c, 'versions', [])) %>
15 % if inline:
15 % if inline:
16 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
16 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
17 % else:
17 % else:
18 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
18 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
19 % endif
19 % endif
20
20
21 <div class="comment
21 <div class="comment
22 ${'comment-inline' if inline else 'comment-general'}
22 ${'comment-inline' if inline else 'comment-general'}
23 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
23 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
24 id="comment-${comment.comment_id}"
24 id="comment-${comment.comment_id}"
25 line="${comment.line_no}"
25 line="${comment.line_no}"
26 data-comment-id="${comment.comment_id}"
26 data-comment-id="${comment.comment_id}"
27 data-comment-type="${comment.comment_type}"
27 data-comment-type="${comment.comment_type}"
28 data-comment-renderer="${comment.renderer}"
28 data-comment-renderer="${comment.renderer}"
29 data-comment-text="${comment.text | html_filters.base64,n}"
29 data-comment-text="${comment.text | html_filters.base64,n}"
30 data-comment-line-no="${comment.line_no}"
30 data-comment-line-no="${comment.line_no}"
31 data-comment-inline=${h.json.dumps(inline)}
31 data-comment-inline=${h.json.dumps(inline)}
32 style="${'display: none;' if outdated_at_ver else ''}">
32 style="${'display: none;' if outdated_at_ver else ''}">
33
33
34 <div class="meta">
34 <div class="meta">
35 <div class="comment-type-label">
35 <div class="comment-type-label">
36 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}" title="line: ${comment.line_no}">
36 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}" title="line: ${comment.line_no}">
37 % if comment.comment_type == 'todo':
37 % if comment.comment_type == 'todo':
38 % if comment.resolved:
38 % if comment.resolved:
39 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
39 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
40 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
40 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
41 </div>
41 </div>
42 % else:
42 % else:
43 <div class="resolved tooltip" style="display: none">
43 <div class="resolved tooltip" style="display: none">
44 <span>${comment.comment_type}</span>
44 <span>${comment.comment_type}</span>
45 </div>
45 </div>
46 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
46 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
47 ${comment.comment_type}
47 ${comment.comment_type}
48 </div>
48 </div>
49 % endif
49 % endif
50 % else:
50 % else:
51 % if comment.resolved_comment:
51 % if comment.resolved_comment:
52 fix
52 fix
53 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
53 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
54 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
54 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
55 </a>
55 </a>
56 % else:
56 % else:
57 ${comment.comment_type or 'note'}
57 ${comment.comment_type or 'note'}
58 % endif
58 % endif
59 % endif
59 % endif
60 </div>
60 </div>
61 </div>
61 </div>
62
62
63 <div class="author ${'author-inline' if inline else 'author-general'}">
63 <div class="author ${'author-inline' if inline else 'author-general'}">
64 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
64 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
65 </div>
65 </div>
66 <div class="date">
66 <div class="date">
67 ${h.age_component(comment.modified_at, time_is_local=True)}
67 ${h.age_component(comment.modified_at, time_is_local=True)}
68 </div>
68 </div>
69 <%
70 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
71 %>
72
69 % if comment.history:
73 % if comment.history:
70 <div class="date">
74 <div class="date">
71 <span class="comment-area-text">${_('Edited')}:</span>
72 <select class="comment-version-select" id="comment_history_for_comment_${comment.comment_id}"
73 onchange="return Rhodecode.comments.showVersion(this)"
74 name="comment_type">
75
75
76 <option style="display: none" value="0">---</option>
76 <input id="${comment_version_selector}" name="${comment_version_selector}"
77 type="hidden"
78 data-last-version="${comment.history[-1].version}">
79
80 <script type="text/javascript">
81
82 var preLoadVersionData = [
77 % for comment_history in comment.history:
83 % for comment_history in comment.history:
78 <option data-comment-history-id="${comment_history.comment_history_id}"
84 {
79 data-comment-id="${comment.comment_id}"
85 id: ${comment_history.comment_history_id},
80 value="${comment_history.version}">
86 text: 'v${comment_history.version}',
81 ${comment_history.version}
87 action: function () {
82 </option>
88 Rhodecode.comments.showVersion(
89 "${comment.comment_id}",
90 "${comment_history.comment_history_id}"
91 )
92 },
93 comment_version: "${comment_history.version}",
94 comment_author_username: "${comment_history.author.username}",
95 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
96 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
97 },
83 % endfor
98 % endfor
84 </select>
99 ]
100 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
101
102 </script>
103
85 </div>
104 </div>
86 % else:
105 % else:
87 <div class="date" style="display: none">
106 <div class="date" style="display: none">
88 <span class="comment-area-text">${_('Edited')}</span>
107 <input id="${comment_version_selector}" name="${comment_version_selector}"
89 <select class="comment-version-select" id="comment_history_for_comment_${comment.comment_id}"
108 type="hidden"
90 onchange="return Rhodecode.comments.showVersion(this)"
109 data-last-version="0">
91 name="comment_type">
92 <option style="display: none" value="0">---</option>
93 </select>
94 </div>
110 </div>
95 %endif
111 %endif
96 % if inline:
112 % if inline:
97 <span></span>
113 <span></span>
98 % else:
114 % else:
99 <div class="status-change">
115 <div class="status-change">
100 % if comment.pull_request:
116 % if comment.pull_request:
101 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
117 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
102 % if comment.status_change:
118 % if comment.status_change:
103 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}:
119 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}:
104 % else:
120 % else:
105 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}
121 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}
106 % endif
122 % endif
107 </a>
123 </a>
108 % else:
124 % else:
109 % if comment.status_change:
125 % if comment.status_change:
110 ${_('Status change on commit')}:
126 ${_('Status change on commit')}:
111 % endif
127 % endif
112 % endif
128 % endif
113 </div>
129 </div>
114 % endif
130 % endif
115
131
116 % if comment.status_change:
132 % if comment.status_change:
117 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
133 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
118 <div title="${_('Commit status')}" class="changeset-status-lbl">
134 <div title="${_('Commit status')}" class="changeset-status-lbl">
119 ${comment.status_change[0].status_lbl}
135 ${comment.status_change[0].status_lbl}
120 </div>
136 </div>
121 % endif
137 % endif
122
138
123 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
139 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
124
140
125 <div class="comment-links-block">
141 <div class="comment-links-block">
126 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
142 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
127 <span class="tag authortag tooltip" title="${_('Pull request author')}">
143 <span class="tag authortag tooltip" title="${_('Pull request author')}">
128 ${_('author')}
144 ${_('author')}
129 </span>
145 </span>
130 |
146 |
131 % endif
147 % endif
132 % if inline:
148 % if inline:
133 <div class="pr-version-inline">
149 <div class="pr-version-inline">
134 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
150 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
135 % if outdated_at_ver:
151 % if outdated_at_ver:
136 <code class="pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
152 <code class="pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
137 outdated ${'v{}'.format(pr_index_ver)} |
153 outdated ${'v{}'.format(pr_index_ver)} |
138 </code>
154 </code>
139 % elif pr_index_ver:
155 % elif pr_index_ver:
140 <code class="pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
156 <code class="pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
141 ${'v{}'.format(pr_index_ver)} |
157 ${'v{}'.format(pr_index_ver)} |
142 </code>
158 </code>
143 % endif
159 % endif
144 </a>
160 </a>
145 </div>
161 </div>
146 % else:
162 % else:
147 % if comment.pull_request_version_id and pr_index_ver:
163 % if comment.pull_request_version_id and pr_index_ver:
148 |
164 |
149 <div class="pr-version">
165 <div class="pr-version">
150 % if comment.outdated:
166 % if comment.outdated:
151 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
167 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
152 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
168 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
153 </a>
169 </a>
154 % else:
170 % else:
155 <div title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
171 <div title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
156 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
172 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
157 <code class="pr-version-num">
173 <code class="pr-version-num">
158 ${'v{}'.format(pr_index_ver)}
174 ${'v{}'.format(pr_index_ver)}
159 </code>
175 </code>
160 </a>
176 </a>
161 </div>
177 </div>
162 % endif
178 % endif
163 </div>
179 </div>
164 % endif
180 % endif
165 % endif
181 % endif
166
182
167 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
183 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
168 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
184 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
169 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
185 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
170 ## permissions to delete
186 ## permissions to delete
171 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
187 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
172 %if comment.comment_type == 'note':
173 <a onclick="return Rhodecode.comments.editComment(this);"
188 <a onclick="return Rhodecode.comments.editComment(this);"
174 class="edit-comment">${_('Edit')}</a>
189 class="edit-comment">${_('Edit')}</a>
175 %else:
176 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
177 %endif
178 | <a onclick="return Rhodecode.comments.deleteComment(this);"
190 | <a onclick="return Rhodecode.comments.deleteComment(this);"
179 class="delete-comment">${_('Delete')}</a>
191 class="delete-comment">${_('Delete')}</a>
180 %else:
192 %else:
181 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
193 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
182 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
194 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
183 %endif
195 %endif
184 %else:
196 %else:
185 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
197 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
186 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
198 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
187 %endif
199 %endif
188
200
189 % if outdated_at_ver:
201 % if outdated_at_ver:
190 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
202 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
191 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
203 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
192 % else:
204 % else:
193 | <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
205 | <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
194 | <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
206 | <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
195 % endif
207 % endif
196
208
197 </div>
209 </div>
198 </div>
210 </div>
199 <div class="text">
211 <div class="text">
200 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
212 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
201 </div>
213 </div>
202
214
203 </div>
215 </div>
204 </%def>
216 </%def>
205
217
206 ## generate main comments
218 ## generate main comments
207 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
219 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
208 <%
220 <%
209 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
221 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
210 %>
222 %>
211
223
212 <div class="general-comments" id="comments">
224 <div class="general-comments" id="comments">
213 %for comment in comments:
225 %for comment in comments:
214 <div id="comment-tr-${comment.comment_id}">
226 <div id="comment-tr-${comment.comment_id}">
215 ## only render comments that are not from pull request, or from
227 ## only render comments that are not from pull request, or from
216 ## pull request and a status change
228 ## pull request and a status change
217 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
229 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
218 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
230 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
219 %endif
231 %endif
220 </div>
232 </div>
221 %endfor
233 %endfor
222 ## to anchor ajax comments
234 ## to anchor ajax comments
223 <div id="injected_page_comments"></div>
235 <div id="injected_page_comments"></div>
224 </div>
236 </div>
225 </%def>
237 </%def>
226
238
227
239
228 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
240 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
229
241
230 <div class="comments">
242 <div class="comments">
231 <%
243 <%
232 if is_pull_request:
244 if is_pull_request:
233 placeholder = _('Leave a comment on this Pull Request.')
245 placeholder = _('Leave a comment on this Pull Request.')
234 elif is_compare:
246 elif is_compare:
235 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
247 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
236 else:
248 else:
237 placeholder = _('Leave a comment on this Commit.')
249 placeholder = _('Leave a comment on this Commit.')
238 %>
250 %>
239
251
240 % if c.rhodecode_user.username != h.DEFAULT_USER:
252 % if c.rhodecode_user.username != h.DEFAULT_USER:
241 <div class="js-template" id="cb-comment-general-form-template">
253 <div class="js-template" id="cb-comment-general-form-template">
242 ## template generated for injection
254 ## template generated for injection
243 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
255 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
244 </div>
256 </div>
245
257
246 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
258 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
247 ## inject form here
259 ## inject form here
248 </div>
260 </div>
249 <script type="text/javascript">
261 <script type="text/javascript">
250 var lineNo = 'general';
262 var lineNo = 'general';
251 var resolvesCommentId = null;
263 var resolvesCommentId = null;
252 var generalCommentForm = Rhodecode.comments.createGeneralComment(
264 var generalCommentForm = Rhodecode.comments.createGeneralComment(
253 lineNo, "${placeholder}", resolvesCommentId);
265 lineNo, "${placeholder}", resolvesCommentId);
254
266
255 // set custom success callback on rangeCommit
267 // set custom success callback on rangeCommit
256 % if is_compare:
268 % if is_compare:
257 generalCommentForm.setHandleFormSubmit(function(o) {
269 generalCommentForm.setHandleFormSubmit(function(o) {
258 var self = generalCommentForm;
270 var self = generalCommentForm;
259
271
260 var text = self.cm.getValue();
272 var text = self.cm.getValue();
261 var status = self.getCommentStatus();
273 var status = self.getCommentStatus();
262 var commentType = self.getCommentType();
274 var commentType = self.getCommentType();
263
275
264 if (text === "" && !status) {
276 if (text === "" && !status) {
265 return;
277 return;
266 }
278 }
267
279
268 // we can pick which commits we want to make the comment by
280 // we can pick which commits we want to make the comment by
269 // selecting them via click on preview pane, this will alter the hidden inputs
281 // selecting them via click on preview pane, this will alter the hidden inputs
270 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
282 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
271
283
272 var commitIds = [];
284 var commitIds = [];
273 $('#changeset_compare_view_content .compare_select').each(function(el) {
285 $('#changeset_compare_view_content .compare_select').each(function(el) {
274 var commitId = this.id.replace('row-', '');
286 var commitId = this.id.replace('row-', '');
275 if ($(this).hasClass('hl') || !cherryPicked) {
287 if ($(this).hasClass('hl') || !cherryPicked) {
276 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
288 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
277 commitIds.push(commitId);
289 commitIds.push(commitId);
278 } else {
290 } else {
279 $("input[data-commit-id='{0}']".format(commitId)).val('')
291 $("input[data-commit-id='{0}']".format(commitId)).val('')
280 }
292 }
281 });
293 });
282
294
283 self.setActionButtonsDisabled(true);
295 self.setActionButtonsDisabled(true);
284 self.cm.setOption("readOnly", true);
296 self.cm.setOption("readOnly", true);
285 var postData = {
297 var postData = {
286 'text': text,
298 'text': text,
287 'changeset_status': status,
299 'changeset_status': status,
288 'comment_type': commentType,
300 'comment_type': commentType,
289 'commit_ids': commitIds,
301 'commit_ids': commitIds,
290 'csrf_token': CSRF_TOKEN
302 'csrf_token': CSRF_TOKEN
291 };
303 };
292
304
293 var submitSuccessCallback = function(o) {
305 var submitSuccessCallback = function(o) {
294 location.reload(true);
306 location.reload(true);
295 };
307 };
296 var submitFailCallback = function(){
308 var submitFailCallback = function(){
297 self.resetCommentFormState(text)
309 self.resetCommentFormState(text)
298 };
310 };
299 self.submitAjaxPOST(
311 self.submitAjaxPOST(
300 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
312 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
301 });
313 });
302 % endif
314 % endif
303
315
304 </script>
316 </script>
305 % else:
317 % else:
306 ## form state when not logged in
318 ## form state when not logged in
307 <div class="comment-form ac">
319 <div class="comment-form ac">
308
320
309 <div class="comment-area">
321 <div class="comment-area">
310 <div class="comment-area-header">
322 <div class="comment-area-header">
311 <ul class="nav-links clearfix">
323 <ul class="nav-links clearfix">
312 <li class="active">
324 <li class="active">
313 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
325 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
314 </li>
326 </li>
315 <li class="">
327 <li class="">
316 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
328 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
317 </li>
329 </li>
318 </ul>
330 </ul>
319 </div>
331 </div>
320
332
321 <div class="comment-area-write" style="display: block;">
333 <div class="comment-area-write" style="display: block;">
322 <div id="edit-container">
334 <div id="edit-container">
323 <div style="padding: 40px 0">
335 <div style="padding: 40px 0">
324 ${_('You need to be logged in to leave comments.')}
336 ${_('You need to be logged in to leave comments.')}
325 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
337 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
326 </div>
338 </div>
327 </div>
339 </div>
328 <div id="preview-container" class="clearfix" style="display: none;">
340 <div id="preview-container" class="clearfix" style="display: none;">
329 <div id="preview-box" class="preview-box"></div>
341 <div id="preview-box" class="preview-box"></div>
330 </div>
342 </div>
331 </div>
343 </div>
332
344
333 <div class="comment-area-footer">
345 <div class="comment-area-footer">
334 <div class="toolbar">
346 <div class="toolbar">
335 <div class="toolbar-text">
347 <div class="toolbar-text">
336 </div>
348 </div>
337 </div>
349 </div>
338 </div>
350 </div>
339 </div>
351 </div>
340
352
341 <div class="comment-footer">
353 <div class="comment-footer">
342 </div>
354 </div>
343
355
344 </div>
356 </div>
345 % endif
357 % endif
346
358
347 <script type="text/javascript">
359 <script type="text/javascript">
348 bindToggleButtons();
360 bindToggleButtons();
349 </script>
361 </script>
350 </div>
362 </div>
351 </%def>
363 </%def>
352
364
353
365
354 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
366 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
355
367
356 ## comment injected based on assumption that user is logged in
368 ## comment injected based on assumption that user is logged in
357 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
369 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
358
370
359 <div class="comment-area">
371 <div class="comment-area">
360 <div class="comment-area-header">
372 <div class="comment-area-header">
361 <div class="pull-left">
373 <div class="pull-left">
362 <ul class="nav-links clearfix">
374 <ul class="nav-links clearfix">
363 <li class="active">
375 <li class="active">
364 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
376 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
365 </li>
377 </li>
366 <li class="">
378 <li class="">
367 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
379 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
368 </li>
380 </li>
369 </ul>
381 </ul>
370 </div>
382 </div>
371 <div class="pull-right">
383 <div class="pull-right">
372 <span class="comment-area-text">${_('Mark as')}:</span>
384 <span class="comment-area-text">${_('Mark as')}:</span>
373 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
385 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
374 % for val in c.visual.comment_types:
386 % for val in c.visual.comment_types:
375 <option value="${val}">${val.upper()}</option>
387 <option value="${val}">${val.upper()}</option>
376 % endfor
388 % endfor
377 </select>
389 </select>
378 </div>
390 </div>
379 </div>
391 </div>
380
392
381 <div class="comment-area-write" style="display: block;">
393 <div class="comment-area-write" style="display: block;">
382 <div id="edit-container_${lineno_id}">
394 <div id="edit-container_${lineno_id}">
383 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
395 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
384 </div>
396 </div>
385 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
397 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
386 <div id="preview-box_${lineno_id}" class="preview-box"></div>
398 <div id="preview-box_${lineno_id}" class="preview-box"></div>
387 </div>
399 </div>
388 </div>
400 </div>
389
401
390 <div class="comment-area-footer comment-attachment-uploader">
402 <div class="comment-area-footer comment-attachment-uploader">
391 <div class="toolbar">
403 <div class="toolbar">
392
404
393 <div class="comment-attachment-text">
405 <div class="comment-attachment-text">
394 <div class="dropzone-text">
406 <div class="dropzone-text">
395 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
407 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
396 </div>
408 </div>
397 <div class="dropzone-upload" style="display:none">
409 <div class="dropzone-upload" style="display:none">
398 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
410 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
399 </div>
411 </div>
400 </div>
412 </div>
401
413
402 ## comments dropzone template, empty on purpose
414 ## comments dropzone template, empty on purpose
403 <div style="display: none" class="comment-attachment-uploader-template">
415 <div style="display: none" class="comment-attachment-uploader-template">
404 <div class="dz-file-preview" style="margin: 0">
416 <div class="dz-file-preview" style="margin: 0">
405 <div class="dz-error-message"></div>
417 <div class="dz-error-message"></div>
406 </div>
418 </div>
407 </div>
419 </div>
408
420
409 </div>
421 </div>
410 </div>
422 </div>
411 </div>
423 </div>
412
424
413 <div class="comment-footer">
425 <div class="comment-footer">
414
426
415 ## inject extra inputs into the form
427 ## inject extra inputs into the form
416 % if form_extras and isinstance(form_extras, (list, tuple)):
428 % if form_extras and isinstance(form_extras, (list, tuple)):
417 <div id="comment_form_extras">
429 <div id="comment_form_extras">
418 % for form_ex_el in form_extras:
430 % for form_ex_el in form_extras:
419 ${form_ex_el|n}
431 ${form_ex_el|n}
420 % endfor
432 % endfor
421 </div>
433 </div>
422 % endif
434 % endif
423
435
424 <div class="action-buttons">
436 <div class="action-buttons">
425 % if form_type != 'inline':
437 % if form_type != 'inline':
426 <div class="action-buttons-extra"></div>
438 <div class="action-buttons-extra"></div>
427 % endif
439 % endif
428
440
429 <input class="btn btn-success comment-button-input" id="save_${lineno_id}" name="save" type="submit" value="${_('Comment')}">
441 <input class="btn btn-success comment-button-input" id="save_${lineno_id}" name="save" type="submit" value="${_('Comment')}">
430
442
431 ## inline for has a file, and line-number together with cancel hide button.
443 ## inline for has a file, and line-number together with cancel hide button.
432 % if form_type == 'inline':
444 % if form_type == 'inline':
433 <input type="hidden" name="f_path" value="{0}">
445 <input type="hidden" name="f_path" value="{0}">
434 <input type="hidden" name="line" value="${lineno_id}">
446 <input type="hidden" name="line" value="${lineno_id}">
435 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
447 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
436 ${_('Cancel')}
448 ${_('Cancel')}
437 </button>
449 </button>
438 % endif
450 % endif
439 </div>
451 </div>
440
452
441 % if review_statuses:
453 % if review_statuses:
442 <div class="status_box">
454 <div class="status_box">
443 <select id="change_status_${lineno_id}" name="changeset_status">
455 <select id="change_status_${lineno_id}" name="changeset_status">
444 <option></option> ## Placeholder
456 <option></option> ## Placeholder
445 % for status, lbl in review_statuses:
457 % for status, lbl in review_statuses:
446 <option value="${status}" data-status="${status}">${lbl}</option>
458 <option value="${status}" data-status="${status}">${lbl}</option>
447 %if is_pull_request and change_status and status in ('approved', 'rejected'):
459 %if is_pull_request and change_status and status in ('approved', 'rejected'):
448 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
460 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
449 %endif
461 %endif
450 % endfor
462 % endfor
451 </select>
463 </select>
452 </div>
464 </div>
453 % endif
465 % endif
454
466
455 <div class="toolbar-text">
467 <div class="toolbar-text">
456 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
468 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
457 ${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
469 ${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
458 <span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
470 <span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
459 ${_('and')}
471 ${_('and')}
460 <span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
472 <span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
461 ${_('actions supported.')}
473 ${_('actions supported.')}
462 </div>
474 </div>
463 </div>
475 </div>
464
476
465 </form>
477 </form>
466
478
467 </%def> No newline at end of file
479 </%def>
@@ -1,140 +1,168 b''
1 <%text>
1 <%text>
2 <div style="display: none">
2 <div style="display: none">
3
3
4 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
4 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
5
5
6 <%
6 <%
7 if (size > 16) {
7 if (size > 16) {
8 var gravatar_class = 'gravatar gravatar-large';
8 var gravatar_class = 'gravatar gravatar-large';
9 } else {
9 } else {
10 var gravatar_class = 'gravatar';
10 var gravatar_class = 'gravatar';
11 }
11 }
12
12
13 if (tooltip) {
13 if (tooltip) {
14 var gravatar_class = gravatar_class + ' tooltip-hovercard';
14 var gravatar_class = gravatar_class + ' tooltip-hovercard';
15 }
15 }
16
16
17 var data_hovercard_alt = username;
17 var data_hovercard_alt = username;
18
18
19 %>
19 %>
20
20
21 <%
21 <%
22 if (show_disabled) {
22 if (show_disabled) {
23 var user_cls = 'user user-disabled';
23 var user_cls = 'user user-disabled';
24 } else {
24 } else {
25 var user_cls = 'user';
25 var user_cls = 'user';
26 }
26 }
27 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
27 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
28 %>
28 %>
29
29
30 <div class="rc-user">
30 <div class="rc-user">
31 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
31 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
32 <span class="<%= user_cls %>"> <%- user_link -%> </span>
32 <span class="<%= user_cls %>"> <%- user_link -%> </span>
33 </div>
33 </div>
34
34
35 </script>
35 </script>
36
36
37 <script>
37 <script>
38 var CG = new ColorGenerator();
38 var CG = new ColorGenerator();
39 </script>
39 </script>
40
40
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
42
42
43 <li id="reviewer_<%= member.user_id %>" class="reviewer_entry">
43 <li id="reviewer_<%= member.user_id %>" class="reviewer_entry">
44 <%
44 <%
45 if (create) {
45 if (create) {
46 var edit_visibility = 'visible';
46 var edit_visibility = 'visible';
47 } else {
47 } else {
48 var edit_visibility = 'hidden';
48 var edit_visibility = 'hidden';
49 }
49 }
50
50
51 if (member.user_group && member.user_group.vote_rule) {
51 if (member.user_group && member.user_group.vote_rule) {
52 var groupStyle = 'border-left: 1px solid '+CG.asRGB(CG.getColor(member.user_group.vote_rule));
52 var groupStyle = 'border-left: 1px solid '+CG.asRGB(CG.getColor(member.user_group.vote_rule));
53 } else {
53 } else {
54 var groupStyle = 'border-left: 1px solid white';
54 var groupStyle = 'border-left: 1px solid white';
55 }
55 }
56 %>
56 %>
57
57
58 <div class="reviewers_member" style="<%= groupStyle%>" >
58 <div class="reviewers_member" style="<%= groupStyle%>" >
59 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
59 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
60 <i class="icon-circle review-status-<%= review_status %>"></i>
60 <i class="icon-circle review-status-<%= review_status %>"></i>
61 </div>
61 </div>
62 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
62 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
63 <% if (mandatory) { %>
63 <% if (mandatory) { %>
64 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
64 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
65 <i class="icon-lock"></i>
65 <i class="icon-lock"></i>
66 </div>
66 </div>
67 <% } %>
67 <% } %>
68
68
69 <%-
69 <%-
70 renderTemplate('gravatarWithUser', {
70 renderTemplate('gravatarWithUser', {
71 'size': 16,
71 'size': 16,
72 'show_disabled': false,
72 'show_disabled': false,
73 'tooltip': true,
73 'tooltip': true,
74 'username': member.username,
74 'username': member.username,
75 'user_id': member.user_id,
75 'user_id': member.user_id,
76 'user_link': member.user_link,
76 'user_link': member.user_link,
77 'gravatar_url': member.gravatar_link
77 'gravatar_url': member.gravatar_link
78 })
78 })
79 %>
79 %>
80 </div>
80 </div>
81
81
82 <input type="hidden" name="__start__" value="reviewer:mapping">
82 <input type="hidden" name="__start__" value="reviewer:mapping">
83
83
84
84
85 <%if (member.user_group && member.user_group.vote_rule) {%>
85 <%if (member.user_group && member.user_group.vote_rule) {%>
86 <div class="reviewer_reason">
86 <div class="reviewer_reason">
87
87
88 <%if (member.user_group.vote_rule == -1) {%>
88 <%if (member.user_group.vote_rule == -1) {%>
89 - group votes required: ALL
89 - group votes required: ALL
90 <%} else {%>
90 <%} else {%>
91 - group votes required: <%= member.user_group.vote_rule %>
91 - group votes required: <%= member.user_group.vote_rule %>
92 <%}%>
92 <%}%>
93 </div>
93 </div>
94 <%}%>
94 <%}%>
95
95
96 <input type="hidden" name="__start__" value="reasons:sequence">
96 <input type="hidden" name="__start__" value="reasons:sequence">
97 <% for (var i = 0; i < reasons.length; i++) { %>
97 <% for (var i = 0; i < reasons.length; i++) { %>
98 <% var reason = reasons[i] %>
98 <% var reason = reasons[i] %>
99 <div class="reviewer_reason">- <%= reason %></div>
99 <div class="reviewer_reason">- <%= reason %></div>
100 <input type="hidden" name="reason" value="<%= reason %>">
100 <input type="hidden" name="reason" value="<%= reason %>">
101 <% } %>
101 <% } %>
102 <input type="hidden" name="__end__" value="reasons:sequence">
102 <input type="hidden" name="__end__" value="reasons:sequence">
103
103
104 <input type="hidden" name="__start__" value="rules:sequence">
104 <input type="hidden" name="__start__" value="rules:sequence">
105 <% for (var i = 0; i < member.rules.length; i++) { %>
105 <% for (var i = 0; i < member.rules.length; i++) { %>
106 <% var rule = member.rules[i] %>
106 <% var rule = member.rules[i] %>
107 <input type="hidden" name="rule_id" value="<%= rule %>">
107 <input type="hidden" name="rule_id" value="<%= rule %>">
108 <% } %>
108 <% } %>
109 <input type="hidden" name="__end__" value="rules:sequence">
109 <input type="hidden" name="__end__" value="rules:sequence">
110
110
111 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
111 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
112 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
112 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
113
113
114 <input type="hidden" name="__end__" value="reviewer:mapping">
114 <input type="hidden" name="__end__" value="reviewer:mapping">
115
115
116 <% if (mandatory) { %>
116 <% if (mandatory) { %>
117 <div class="reviewer_member_mandatory_remove" style="visibility: <%= edit_visibility %>;">
117 <div class="reviewer_member_mandatory_remove" style="visibility: <%= edit_visibility %>;">
118 <i class="icon-remove"></i>
118 <i class="icon-remove"></i>
119 </div>
119 </div>
120 <% } else { %>
120 <% } else { %>
121 <% if (allowed_to_update) { %>
121 <% if (allowed_to_update) { %>
122 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
122 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
123 <i class="icon-remove" ></i>
123 <i class="icon-remove" ></i>
124 </div>
124 </div>
125 <% } %>
125 <% } %>
126 <% } %>
126 <% } %>
127 </div>
127 </div>
128 </li>
128 </li>
129
129
130 </script>
130 </script>
131
131
132
132
133 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
134
135 <%
136 if (size > 16) {
137 var gravatar_class = 'gravatar gravatar-large';
138 } else {
139 var gravatar_class = 'gravatar';
140 }
141
142 %>
143
144 <%
145 if (show_disabled) {
146 var user_cls = 'user user-disabled';
147 } else {
148 var user_cls = 'user';
149 }
150
151 %>
152
153 <div style='line-height: 20px'>
154 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
155 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
156 </div>
157
158 </script>
159
160
133 </div>
161 </div>
134
162
135 <script>
163 <script>
136 // registers the templates into global cache
164 // registers the templates into global cache
137 registerTemplates();
165 registerTemplates();
138 </script>
166 </script>
139
167
140 </%text>
168 </%text>
General Comments 0
You need to be logged in to leave comments. Login now