##// END OF EJS Templates
pull-requests: use count only for comments related to display grids on my account and repo view.
marcink -
r4506:ed3be682 stable
parent child Browse files
Show More
@@ -1,822 +1,822 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-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 datetime
22 import datetime
23 import string
23 import string
24
24
25 import formencode
25 import formencode
26 import formencode.htmlfill
26 import formencode.htmlfill
27 import peppercorn
27 import peppercorn
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30
30
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode import forms
32 from rhodecode import forms
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.lib import audit_logger
34 from rhodecode.lib import audit_logger
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, NotAnonymous, CSRFRequired,
37 LoginRequired, NotAnonymous, CSRFRequired,
38 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
39 from rhodecode.lib.channelstream import (
39 from rhodecode.lib.channelstream import (
40 channelstream_request, ChannelstreamException)
40 channelstream_request, ChannelstreamException)
41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
42 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.auth_token import AuthTokenModel
43 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.comment import CommentsModel
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 IntegrityError, or_, in_filter_generator,
45 IntegrityError, or_, in_filter_generator,
46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
47 PullRequest, UserBookmark, RepoGroup)
47 PullRequest, UserBookmark, RepoGroup)
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
50 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
51 from rhodecode.model.user_group import UserGroupModel
51 from rhodecode.model.user_group import UserGroupModel
52 from rhodecode.model.validation_schema.schemas import user_schema
52 from rhodecode.model.validation_schema.schemas import user_schema
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class MyAccountView(BaseAppView, DataGridAppView):
57 class MyAccountView(BaseAppView, DataGridAppView):
58 ALLOW_SCOPED_TOKENS = False
58 ALLOW_SCOPED_TOKENS = False
59 """
59 """
60 This view has alternative version inside EE, if modified please take a look
60 This view has alternative version inside EE, if modified please take a look
61 in there as well.
61 in there as well.
62 """
62 """
63
63
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context()
65 c = self._get_local_tmpl_context()
66 c.user = c.auth_user.get_instance()
66 c.user = c.auth_user.get_instance()
67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68
68
69 return c
69 return c
70
70
71 @LoginRequired()
71 @LoginRequired()
72 @NotAnonymous()
72 @NotAnonymous()
73 @view_config(
73 @view_config(
74 route_name='my_account_profile', request_method='GET',
74 route_name='my_account_profile', request_method='GET',
75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
76 def my_account_profile(self):
76 def my_account_profile(self):
77 c = self.load_default_context()
77 c = self.load_default_context()
78 c.active = 'profile'
78 c.active = 'profile'
79 c.extern_type = c.user.extern_type
79 c.extern_type = c.user.extern_type
80 return self._get_template_context(c)
80 return self._get_template_context(c)
81
81
82 @LoginRequired()
82 @LoginRequired()
83 @NotAnonymous()
83 @NotAnonymous()
84 @view_config(
84 @view_config(
85 route_name='my_account_password', request_method='GET',
85 route_name='my_account_password', request_method='GET',
86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
87 def my_account_password(self):
87 def my_account_password(self):
88 c = self.load_default_context()
88 c = self.load_default_context()
89 c.active = 'password'
89 c.active = 'password'
90 c.extern_type = c.user.extern_type
90 c.extern_type = c.user.extern_type
91
91
92 schema = user_schema.ChangePasswordSchema().bind(
92 schema = user_schema.ChangePasswordSchema().bind(
93 username=c.user.username)
93 username=c.user.username)
94
94
95 form = forms.Form(
95 form = forms.Form(
96 schema,
96 schema,
97 action=h.route_path('my_account_password_update'),
97 action=h.route_path('my_account_password_update'),
98 buttons=(forms.buttons.save, forms.buttons.reset))
98 buttons=(forms.buttons.save, forms.buttons.reset))
99
99
100 c.form = form
100 c.form = form
101 return self._get_template_context(c)
101 return self._get_template_context(c)
102
102
103 @LoginRequired()
103 @LoginRequired()
104 @NotAnonymous()
104 @NotAnonymous()
105 @CSRFRequired()
105 @CSRFRequired()
106 @view_config(
106 @view_config(
107 route_name='my_account_password_update', request_method='POST',
107 route_name='my_account_password_update', request_method='POST',
108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
109 def my_account_password_update(self):
109 def my_account_password_update(self):
110 _ = self.request.translate
110 _ = self.request.translate
111 c = self.load_default_context()
111 c = self.load_default_context()
112 c.active = 'password'
112 c.active = 'password'
113 c.extern_type = c.user.extern_type
113 c.extern_type = c.user.extern_type
114
114
115 schema = user_schema.ChangePasswordSchema().bind(
115 schema = user_schema.ChangePasswordSchema().bind(
116 username=c.user.username)
116 username=c.user.username)
117
117
118 form = forms.Form(
118 form = forms.Form(
119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
120
120
121 if c.extern_type != 'rhodecode':
121 if c.extern_type != 'rhodecode':
122 raise HTTPFound(self.request.route_path('my_account_password'))
122 raise HTTPFound(self.request.route_path('my_account_password'))
123
123
124 controls = self.request.POST.items()
124 controls = self.request.POST.items()
125 try:
125 try:
126 valid_data = form.validate(controls)
126 valid_data = form.validate(controls)
127 UserModel().update_user(c.user.user_id, **valid_data)
127 UserModel().update_user(c.user.user_id, **valid_data)
128 c.user.update_userdata(force_password_change=False)
128 c.user.update_userdata(force_password_change=False)
129 Session().commit()
129 Session().commit()
130 except forms.ValidationFailure as e:
130 except forms.ValidationFailure as e:
131 c.form = e
131 c.form = e
132 return self._get_template_context(c)
132 return self._get_template_context(c)
133
133
134 except Exception:
134 except Exception:
135 log.exception("Exception updating password")
135 log.exception("Exception updating password")
136 h.flash(_('Error occurred during update of user password'),
136 h.flash(_('Error occurred during update of user password'),
137 category='error')
137 category='error')
138 else:
138 else:
139 instance = c.auth_user.get_instance()
139 instance = c.auth_user.get_instance()
140 self.session.setdefault('rhodecode_user', {}).update(
140 self.session.setdefault('rhodecode_user', {}).update(
141 {'password': md5(instance.password)})
141 {'password': md5(instance.password)})
142 self.session.save()
142 self.session.save()
143 h.flash(_("Successfully updated password"), category='success')
143 h.flash(_("Successfully updated password"), category='success')
144
144
145 raise HTTPFound(self.request.route_path('my_account_password'))
145 raise HTTPFound(self.request.route_path('my_account_password'))
146
146
147 @LoginRequired()
147 @LoginRequired()
148 @NotAnonymous()
148 @NotAnonymous()
149 @view_config(
149 @view_config(
150 route_name='my_account_auth_tokens', request_method='GET',
150 route_name='my_account_auth_tokens', request_method='GET',
151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
152 def my_account_auth_tokens(self):
152 def my_account_auth_tokens(self):
153 _ = self.request.translate
153 _ = self.request.translate
154
154
155 c = self.load_default_context()
155 c = self.load_default_context()
156 c.active = 'auth_tokens'
156 c.active = 'auth_tokens'
157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
158 c.role_values = [
158 c.role_values = [
159 (x, AuthTokenModel.cls._get_role_name(x))
159 (x, AuthTokenModel.cls._get_role_name(x))
160 for x in AuthTokenModel.cls.ROLES]
160 for x in AuthTokenModel.cls.ROLES]
161 c.role_options = [(c.role_values, _("Role"))]
161 c.role_options = [(c.role_values, _("Role"))]
162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
163 c.user.user_id, show_expired=True)
163 c.user.user_id, show_expired=True)
164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
165 return self._get_template_context(c)
165 return self._get_template_context(c)
166
166
167 @LoginRequired()
167 @LoginRequired()
168 @NotAnonymous()
168 @NotAnonymous()
169 @CSRFRequired()
169 @CSRFRequired()
170 @view_config(
170 @view_config(
171 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
171 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
172 renderer='json_ext')
172 renderer='json_ext')
173 def my_account_auth_tokens_view(self):
173 def my_account_auth_tokens_view(self):
174 _ = self.request.translate
174 _ = self.request.translate
175 c = self.load_default_context()
175 c = self.load_default_context()
176
176
177 auth_token_id = self.request.POST.get('auth_token_id')
177 auth_token_id = self.request.POST.get('auth_token_id')
178
178
179 if auth_token_id:
179 if auth_token_id:
180 token = UserApiKeys.get_or_404(auth_token_id)
180 token = UserApiKeys.get_or_404(auth_token_id)
181 if token.user.user_id != c.user.user_id:
181 if token.user.user_id != c.user.user_id:
182 raise HTTPNotFound()
182 raise HTTPNotFound()
183
183
184 return {
184 return {
185 'auth_token': token.api_key
185 'auth_token': token.api_key
186 }
186 }
187
187
188 def maybe_attach_token_scope(self, token):
188 def maybe_attach_token_scope(self, token):
189 # implemented in EE edition
189 # implemented in EE edition
190 pass
190 pass
191
191
192 @LoginRequired()
192 @LoginRequired()
193 @NotAnonymous()
193 @NotAnonymous()
194 @CSRFRequired()
194 @CSRFRequired()
195 @view_config(
195 @view_config(
196 route_name='my_account_auth_tokens_add', request_method='POST',)
196 route_name='my_account_auth_tokens_add', request_method='POST',)
197 def my_account_auth_tokens_add(self):
197 def my_account_auth_tokens_add(self):
198 _ = self.request.translate
198 _ = self.request.translate
199 c = self.load_default_context()
199 c = self.load_default_context()
200
200
201 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
201 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
202 description = self.request.POST.get('description')
202 description = self.request.POST.get('description')
203 role = self.request.POST.get('role')
203 role = self.request.POST.get('role')
204
204
205 token = UserModel().add_auth_token(
205 token = UserModel().add_auth_token(
206 user=c.user.user_id,
206 user=c.user.user_id,
207 lifetime_minutes=lifetime, role=role, description=description,
207 lifetime_minutes=lifetime, role=role, description=description,
208 scope_callback=self.maybe_attach_token_scope)
208 scope_callback=self.maybe_attach_token_scope)
209 token_data = token.get_api_data()
209 token_data = token.get_api_data()
210
210
211 audit_logger.store_web(
211 audit_logger.store_web(
212 'user.edit.token.add', action_data={
212 'user.edit.token.add', action_data={
213 'data': {'token': token_data, 'user': 'self'}},
213 'data': {'token': token_data, 'user': 'self'}},
214 user=self._rhodecode_user, )
214 user=self._rhodecode_user, )
215 Session().commit()
215 Session().commit()
216
216
217 h.flash(_("Auth token successfully created"), category='success')
217 h.flash(_("Auth token successfully created"), category='success')
218 return HTTPFound(h.route_path('my_account_auth_tokens'))
218 return HTTPFound(h.route_path('my_account_auth_tokens'))
219
219
220 @LoginRequired()
220 @LoginRequired()
221 @NotAnonymous()
221 @NotAnonymous()
222 @CSRFRequired()
222 @CSRFRequired()
223 @view_config(
223 @view_config(
224 route_name='my_account_auth_tokens_delete', request_method='POST')
224 route_name='my_account_auth_tokens_delete', request_method='POST')
225 def my_account_auth_tokens_delete(self):
225 def my_account_auth_tokens_delete(self):
226 _ = self.request.translate
226 _ = self.request.translate
227 c = self.load_default_context()
227 c = self.load_default_context()
228
228
229 del_auth_token = self.request.POST.get('del_auth_token')
229 del_auth_token = self.request.POST.get('del_auth_token')
230
230
231 if del_auth_token:
231 if del_auth_token:
232 token = UserApiKeys.get_or_404(del_auth_token)
232 token = UserApiKeys.get_or_404(del_auth_token)
233 token_data = token.get_api_data()
233 token_data = token.get_api_data()
234
234
235 AuthTokenModel().delete(del_auth_token, c.user.user_id)
235 AuthTokenModel().delete(del_auth_token, c.user.user_id)
236 audit_logger.store_web(
236 audit_logger.store_web(
237 'user.edit.token.delete', action_data={
237 'user.edit.token.delete', action_data={
238 'data': {'token': token_data, 'user': 'self'}},
238 'data': {'token': token_data, 'user': 'self'}},
239 user=self._rhodecode_user,)
239 user=self._rhodecode_user,)
240 Session().commit()
240 Session().commit()
241 h.flash(_("Auth token successfully deleted"), category='success')
241 h.flash(_("Auth token successfully deleted"), category='success')
242
242
243 return HTTPFound(h.route_path('my_account_auth_tokens'))
243 return HTTPFound(h.route_path('my_account_auth_tokens'))
244
244
245 @LoginRequired()
245 @LoginRequired()
246 @NotAnonymous()
246 @NotAnonymous()
247 @view_config(
247 @view_config(
248 route_name='my_account_emails', request_method='GET',
248 route_name='my_account_emails', request_method='GET',
249 renderer='rhodecode:templates/admin/my_account/my_account.mako')
249 renderer='rhodecode:templates/admin/my_account/my_account.mako')
250 def my_account_emails(self):
250 def my_account_emails(self):
251 _ = self.request.translate
251 _ = self.request.translate
252
252
253 c = self.load_default_context()
253 c = self.load_default_context()
254 c.active = 'emails'
254 c.active = 'emails'
255
255
256 c.user_email_map = UserEmailMap.query()\
256 c.user_email_map = UserEmailMap.query()\
257 .filter(UserEmailMap.user == c.user).all()
257 .filter(UserEmailMap.user == c.user).all()
258
258
259 schema = user_schema.AddEmailSchema().bind(
259 schema = user_schema.AddEmailSchema().bind(
260 username=c.user.username, user_emails=c.user.emails)
260 username=c.user.username, user_emails=c.user.emails)
261
261
262 form = forms.RcForm(schema,
262 form = forms.RcForm(schema,
263 action=h.route_path('my_account_emails_add'),
263 action=h.route_path('my_account_emails_add'),
264 buttons=(forms.buttons.save, forms.buttons.reset))
264 buttons=(forms.buttons.save, forms.buttons.reset))
265
265
266 c.form = form
266 c.form = form
267 return self._get_template_context(c)
267 return self._get_template_context(c)
268
268
269 @LoginRequired()
269 @LoginRequired()
270 @NotAnonymous()
270 @NotAnonymous()
271 @CSRFRequired()
271 @CSRFRequired()
272 @view_config(
272 @view_config(
273 route_name='my_account_emails_add', request_method='POST',
273 route_name='my_account_emails_add', request_method='POST',
274 renderer='rhodecode:templates/admin/my_account/my_account.mako')
274 renderer='rhodecode:templates/admin/my_account/my_account.mako')
275 def my_account_emails_add(self):
275 def my_account_emails_add(self):
276 _ = self.request.translate
276 _ = self.request.translate
277 c = self.load_default_context()
277 c = self.load_default_context()
278 c.active = 'emails'
278 c.active = 'emails'
279
279
280 schema = user_schema.AddEmailSchema().bind(
280 schema = user_schema.AddEmailSchema().bind(
281 username=c.user.username, user_emails=c.user.emails)
281 username=c.user.username, user_emails=c.user.emails)
282
282
283 form = forms.RcForm(
283 form = forms.RcForm(
284 schema, action=h.route_path('my_account_emails_add'),
284 schema, action=h.route_path('my_account_emails_add'),
285 buttons=(forms.buttons.save, forms.buttons.reset))
285 buttons=(forms.buttons.save, forms.buttons.reset))
286
286
287 controls = self.request.POST.items()
287 controls = self.request.POST.items()
288 try:
288 try:
289 valid_data = form.validate(controls)
289 valid_data = form.validate(controls)
290 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
290 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
291 audit_logger.store_web(
291 audit_logger.store_web(
292 'user.edit.email.add', action_data={
292 'user.edit.email.add', action_data={
293 'data': {'email': valid_data['email'], 'user': 'self'}},
293 'data': {'email': valid_data['email'], 'user': 'self'}},
294 user=self._rhodecode_user,)
294 user=self._rhodecode_user,)
295 Session().commit()
295 Session().commit()
296 except formencode.Invalid as error:
296 except formencode.Invalid as error:
297 h.flash(h.escape(error.error_dict['email']), category='error')
297 h.flash(h.escape(error.error_dict['email']), category='error')
298 except forms.ValidationFailure as e:
298 except forms.ValidationFailure as e:
299 c.user_email_map = UserEmailMap.query() \
299 c.user_email_map = UserEmailMap.query() \
300 .filter(UserEmailMap.user == c.user).all()
300 .filter(UserEmailMap.user == c.user).all()
301 c.form = e
301 c.form = e
302 return self._get_template_context(c)
302 return self._get_template_context(c)
303 except Exception:
303 except Exception:
304 log.exception("Exception adding email")
304 log.exception("Exception adding email")
305 h.flash(_('Error occurred during adding email'),
305 h.flash(_('Error occurred during adding email'),
306 category='error')
306 category='error')
307 else:
307 else:
308 h.flash(_("Successfully added email"), category='success')
308 h.flash(_("Successfully added email"), category='success')
309
309
310 raise HTTPFound(self.request.route_path('my_account_emails'))
310 raise HTTPFound(self.request.route_path('my_account_emails'))
311
311
312 @LoginRequired()
312 @LoginRequired()
313 @NotAnonymous()
313 @NotAnonymous()
314 @CSRFRequired()
314 @CSRFRequired()
315 @view_config(
315 @view_config(
316 route_name='my_account_emails_delete', request_method='POST')
316 route_name='my_account_emails_delete', request_method='POST')
317 def my_account_emails_delete(self):
317 def my_account_emails_delete(self):
318 _ = self.request.translate
318 _ = self.request.translate
319 c = self.load_default_context()
319 c = self.load_default_context()
320
320
321 del_email_id = self.request.POST.get('del_email_id')
321 del_email_id = self.request.POST.get('del_email_id')
322 if del_email_id:
322 if del_email_id:
323 email = UserEmailMap.get_or_404(del_email_id).email
323 email = UserEmailMap.get_or_404(del_email_id).email
324 UserModel().delete_extra_email(c.user.user_id, del_email_id)
324 UserModel().delete_extra_email(c.user.user_id, del_email_id)
325 audit_logger.store_web(
325 audit_logger.store_web(
326 'user.edit.email.delete', action_data={
326 'user.edit.email.delete', action_data={
327 'data': {'email': email, 'user': 'self'}},
327 'data': {'email': email, 'user': 'self'}},
328 user=self._rhodecode_user,)
328 user=self._rhodecode_user,)
329 Session().commit()
329 Session().commit()
330 h.flash(_("Email successfully deleted"),
330 h.flash(_("Email successfully deleted"),
331 category='success')
331 category='success')
332 return HTTPFound(h.route_path('my_account_emails'))
332 return HTTPFound(h.route_path('my_account_emails'))
333
333
334 @LoginRequired()
334 @LoginRequired()
335 @NotAnonymous()
335 @NotAnonymous()
336 @CSRFRequired()
336 @CSRFRequired()
337 @view_config(
337 @view_config(
338 route_name='my_account_notifications_test_channelstream',
338 route_name='my_account_notifications_test_channelstream',
339 request_method='POST', renderer='json_ext')
339 request_method='POST', renderer='json_ext')
340 def my_account_notifications_test_channelstream(self):
340 def my_account_notifications_test_channelstream(self):
341 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
341 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
342 self._rhodecode_user.username, datetime.datetime.now())
342 self._rhodecode_user.username, datetime.datetime.now())
343 payload = {
343 payload = {
344 # 'channel': 'broadcast',
344 # 'channel': 'broadcast',
345 'type': 'message',
345 'type': 'message',
346 'timestamp': datetime.datetime.utcnow(),
346 'timestamp': datetime.datetime.utcnow(),
347 'user': 'system',
347 'user': 'system',
348 'pm_users': [self._rhodecode_user.username],
348 'pm_users': [self._rhodecode_user.username],
349 'message': {
349 'message': {
350 'message': message,
350 'message': message,
351 'level': 'info',
351 'level': 'info',
352 'topic': '/notifications'
352 'topic': '/notifications'
353 }
353 }
354 }
354 }
355
355
356 registry = self.request.registry
356 registry = self.request.registry
357 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
357 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 channelstream_config = rhodecode_plugins.get('channelstream', {})
358 channelstream_config = rhodecode_plugins.get('channelstream', {})
359
359
360 try:
360 try:
361 channelstream_request(channelstream_config, [payload], '/message')
361 channelstream_request(channelstream_config, [payload], '/message')
362 except ChannelstreamException as e:
362 except ChannelstreamException as e:
363 log.exception('Failed to send channelstream data')
363 log.exception('Failed to send channelstream data')
364 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
364 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
365 return {"response": 'Channelstream data sent. '
365 return {"response": 'Channelstream data sent. '
366 'You should see a new live message now.'}
366 'You should see a new live message now.'}
367
367
368 def _load_my_repos_data(self, watched=False):
368 def _load_my_repos_data(self, watched=False):
369
369
370 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
370 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
371
371
372 if watched:
372 if watched:
373 # repos user watch
373 # repos user watch
374 repo_list = Session().query(
374 repo_list = Session().query(
375 Repository
375 Repository
376 ) \
376 ) \
377 .join(
377 .join(
378 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
378 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
379 ) \
379 ) \
380 .filter(
380 .filter(
381 UserFollowing.user_id == self._rhodecode_user.user_id
381 UserFollowing.user_id == self._rhodecode_user.user_id
382 ) \
382 ) \
383 .filter(or_(
383 .filter(or_(
384 # generate multiple IN to fix limitation problems
384 # generate multiple IN to fix limitation problems
385 *in_filter_generator(Repository.repo_id, allowed_ids))
385 *in_filter_generator(Repository.repo_id, allowed_ids))
386 ) \
386 ) \
387 .order_by(Repository.repo_name) \
387 .order_by(Repository.repo_name) \
388 .all()
388 .all()
389
389
390 else:
390 else:
391 # repos user is owner of
391 # repos user is owner of
392 repo_list = Session().query(
392 repo_list = Session().query(
393 Repository
393 Repository
394 ) \
394 ) \
395 .filter(
395 .filter(
396 Repository.user_id == self._rhodecode_user.user_id
396 Repository.user_id == self._rhodecode_user.user_id
397 ) \
397 ) \
398 .filter(or_(
398 .filter(or_(
399 # generate multiple IN to fix limitation problems
399 # generate multiple IN to fix limitation problems
400 *in_filter_generator(Repository.repo_id, allowed_ids))
400 *in_filter_generator(Repository.repo_id, allowed_ids))
401 ) \
401 ) \
402 .order_by(Repository.repo_name) \
402 .order_by(Repository.repo_name) \
403 .all()
403 .all()
404
404
405 _render = self.request.get_partial_renderer(
405 _render = self.request.get_partial_renderer(
406 'rhodecode:templates/data_table/_dt_elements.mako')
406 'rhodecode:templates/data_table/_dt_elements.mako')
407
407
408 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
408 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
409 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
409 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
410 short_name=False, admin=False)
410 short_name=False, admin=False)
411
411
412 repos_data = []
412 repos_data = []
413 for repo in repo_list:
413 for repo in repo_list:
414 row = {
414 row = {
415 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
415 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
416 repo.private, repo.archived, repo.fork),
416 repo.private, repo.archived, repo.fork),
417 "name_raw": repo.repo_name.lower(),
417 "name_raw": repo.repo_name.lower(),
418 }
418 }
419
419
420 repos_data.append(row)
420 repos_data.append(row)
421
421
422 # json used to render the grid
422 # json used to render the grid
423 return json.dumps(repos_data)
423 return json.dumps(repos_data)
424
424
425 @LoginRequired()
425 @LoginRequired()
426 @NotAnonymous()
426 @NotAnonymous()
427 @view_config(
427 @view_config(
428 route_name='my_account_repos', request_method='GET',
428 route_name='my_account_repos', request_method='GET',
429 renderer='rhodecode:templates/admin/my_account/my_account.mako')
429 renderer='rhodecode:templates/admin/my_account/my_account.mako')
430 def my_account_repos(self):
430 def my_account_repos(self):
431 c = self.load_default_context()
431 c = self.load_default_context()
432 c.active = 'repos'
432 c.active = 'repos'
433
433
434 # json used to render the grid
434 # json used to render the grid
435 c.data = self._load_my_repos_data()
435 c.data = self._load_my_repos_data()
436 return self._get_template_context(c)
436 return self._get_template_context(c)
437
437
438 @LoginRequired()
438 @LoginRequired()
439 @NotAnonymous()
439 @NotAnonymous()
440 @view_config(
440 @view_config(
441 route_name='my_account_watched', request_method='GET',
441 route_name='my_account_watched', request_method='GET',
442 renderer='rhodecode:templates/admin/my_account/my_account.mako')
442 renderer='rhodecode:templates/admin/my_account/my_account.mako')
443 def my_account_watched(self):
443 def my_account_watched(self):
444 c = self.load_default_context()
444 c = self.load_default_context()
445 c.active = 'watched'
445 c.active = 'watched'
446
446
447 # json used to render the grid
447 # json used to render the grid
448 c.data = self._load_my_repos_data(watched=True)
448 c.data = self._load_my_repos_data(watched=True)
449 return self._get_template_context(c)
449 return self._get_template_context(c)
450
450
451 @LoginRequired()
451 @LoginRequired()
452 @NotAnonymous()
452 @NotAnonymous()
453 @view_config(
453 @view_config(
454 route_name='my_account_bookmarks', request_method='GET',
454 route_name='my_account_bookmarks', request_method='GET',
455 renderer='rhodecode:templates/admin/my_account/my_account.mako')
455 renderer='rhodecode:templates/admin/my_account/my_account.mako')
456 def my_account_bookmarks(self):
456 def my_account_bookmarks(self):
457 c = self.load_default_context()
457 c = self.load_default_context()
458 c.active = 'bookmarks'
458 c.active = 'bookmarks'
459 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
459 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
460 self._rhodecode_db_user.user_id, cache=False)
460 self._rhodecode_db_user.user_id, cache=False)
461 return self._get_template_context(c)
461 return self._get_template_context(c)
462
462
463 def _process_bookmark_entry(self, entry, user_id):
463 def _process_bookmark_entry(self, entry, user_id):
464 position = safe_int(entry.get('position'))
464 position = safe_int(entry.get('position'))
465 cur_position = safe_int(entry.get('cur_position'))
465 cur_position = safe_int(entry.get('cur_position'))
466 if position is None:
466 if position is None:
467 return
467 return
468
468
469 # check if this is an existing entry
469 # check if this is an existing entry
470 is_new = False
470 is_new = False
471 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
471 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
472
472
473 if db_entry and str2bool(entry.get('remove')):
473 if db_entry and str2bool(entry.get('remove')):
474 log.debug('Marked bookmark %s for deletion', db_entry)
474 log.debug('Marked bookmark %s for deletion', db_entry)
475 Session().delete(db_entry)
475 Session().delete(db_entry)
476 return
476 return
477
477
478 if not db_entry:
478 if not db_entry:
479 # new
479 # new
480 db_entry = UserBookmark()
480 db_entry = UserBookmark()
481 is_new = True
481 is_new = True
482
482
483 should_save = False
483 should_save = False
484 default_redirect_url = ''
484 default_redirect_url = ''
485
485
486 # save repo
486 # save repo
487 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
487 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
488 repo = Repository.get(entry['bookmark_repo'])
488 repo = Repository.get(entry['bookmark_repo'])
489 perm_check = HasRepoPermissionAny(
489 perm_check = HasRepoPermissionAny(
490 'repository.read', 'repository.write', 'repository.admin')
490 'repository.read', 'repository.write', 'repository.admin')
491 if repo and perm_check(repo_name=repo.repo_name):
491 if repo and perm_check(repo_name=repo.repo_name):
492 db_entry.repository = repo
492 db_entry.repository = repo
493 should_save = True
493 should_save = True
494 default_redirect_url = '${repo_url}'
494 default_redirect_url = '${repo_url}'
495 # save repo group
495 # save repo group
496 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
496 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
497 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
497 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
498 perm_check = HasRepoGroupPermissionAny(
498 perm_check = HasRepoGroupPermissionAny(
499 'group.read', 'group.write', 'group.admin')
499 'group.read', 'group.write', 'group.admin')
500
500
501 if repo_group and perm_check(group_name=repo_group.group_name):
501 if repo_group and perm_check(group_name=repo_group.group_name):
502 db_entry.repository_group = repo_group
502 db_entry.repository_group = repo_group
503 should_save = True
503 should_save = True
504 default_redirect_url = '${repo_group_url}'
504 default_redirect_url = '${repo_group_url}'
505 # save generic info
505 # save generic info
506 elif entry.get('title') and entry.get('redirect_url'):
506 elif entry.get('title') and entry.get('redirect_url'):
507 should_save = True
507 should_save = True
508
508
509 if should_save:
509 if should_save:
510 # mark user and position
510 # mark user and position
511 db_entry.user_id = user_id
511 db_entry.user_id = user_id
512 db_entry.position = position
512 db_entry.position = position
513 db_entry.title = entry.get('title')
513 db_entry.title = entry.get('title')
514 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
514 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
515 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
515 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
516
516
517 Session().add(db_entry)
517 Session().add(db_entry)
518
518
519 @LoginRequired()
519 @LoginRequired()
520 @NotAnonymous()
520 @NotAnonymous()
521 @CSRFRequired()
521 @CSRFRequired()
522 @view_config(
522 @view_config(
523 route_name='my_account_bookmarks_update', request_method='POST')
523 route_name='my_account_bookmarks_update', request_method='POST')
524 def my_account_bookmarks_update(self):
524 def my_account_bookmarks_update(self):
525 _ = self.request.translate
525 _ = self.request.translate
526 c = self.load_default_context()
526 c = self.load_default_context()
527 c.active = 'bookmarks'
527 c.active = 'bookmarks'
528
528
529 controls = peppercorn.parse(self.request.POST.items())
529 controls = peppercorn.parse(self.request.POST.items())
530 user_id = c.user.user_id
530 user_id = c.user.user_id
531
531
532 # validate positions
532 # validate positions
533 positions = {}
533 positions = {}
534 for entry in controls.get('bookmarks', []):
534 for entry in controls.get('bookmarks', []):
535 position = safe_int(entry['position'])
535 position = safe_int(entry['position'])
536 if position is None:
536 if position is None:
537 continue
537 continue
538
538
539 if position in positions:
539 if position in positions:
540 h.flash(_("Position {} is defined twice. "
540 h.flash(_("Position {} is defined twice. "
541 "Please correct this error.").format(position), category='error')
541 "Please correct this error.").format(position), category='error')
542 return HTTPFound(h.route_path('my_account_bookmarks'))
542 return HTTPFound(h.route_path('my_account_bookmarks'))
543
543
544 entry['position'] = position
544 entry['position'] = position
545 entry['cur_position'] = safe_int(entry.get('cur_position'))
545 entry['cur_position'] = safe_int(entry.get('cur_position'))
546 positions[position] = entry
546 positions[position] = entry
547
547
548 try:
548 try:
549 for entry in positions.values():
549 for entry in positions.values():
550 self._process_bookmark_entry(entry, user_id)
550 self._process_bookmark_entry(entry, user_id)
551
551
552 Session().commit()
552 Session().commit()
553 h.flash(_("Update Bookmarks"), category='success')
553 h.flash(_("Update Bookmarks"), category='success')
554 except IntegrityError:
554 except IntegrityError:
555 h.flash(_("Failed to update bookmarks. "
555 h.flash(_("Failed to update bookmarks. "
556 "Make sure an unique position is used."), category='error')
556 "Make sure an unique position is used."), category='error')
557
557
558 return HTTPFound(h.route_path('my_account_bookmarks'))
558 return HTTPFound(h.route_path('my_account_bookmarks'))
559
559
560 @LoginRequired()
560 @LoginRequired()
561 @NotAnonymous()
561 @NotAnonymous()
562 @view_config(
562 @view_config(
563 route_name='my_account_goto_bookmark', request_method='GET',
563 route_name='my_account_goto_bookmark', request_method='GET',
564 renderer='rhodecode:templates/admin/my_account/my_account.mako')
564 renderer='rhodecode:templates/admin/my_account/my_account.mako')
565 def my_account_goto_bookmark(self):
565 def my_account_goto_bookmark(self):
566
566
567 bookmark_id = self.request.matchdict['bookmark_id']
567 bookmark_id = self.request.matchdict['bookmark_id']
568 user_bookmark = UserBookmark().query()\
568 user_bookmark = UserBookmark().query()\
569 .filter(UserBookmark.user_id == self.request.user.user_id) \
569 .filter(UserBookmark.user_id == self.request.user.user_id) \
570 .filter(UserBookmark.position == bookmark_id).scalar()
570 .filter(UserBookmark.position == bookmark_id).scalar()
571
571
572 redirect_url = h.route_path('my_account_bookmarks')
572 redirect_url = h.route_path('my_account_bookmarks')
573 if not user_bookmark:
573 if not user_bookmark:
574 raise HTTPFound(redirect_url)
574 raise HTTPFound(redirect_url)
575
575
576 # repository set
576 # repository set
577 if user_bookmark.repository:
577 if user_bookmark.repository:
578 repo_name = user_bookmark.repository.repo_name
578 repo_name = user_bookmark.repository.repo_name
579 base_redirect_url = h.route_path(
579 base_redirect_url = h.route_path(
580 'repo_summary', repo_name=repo_name)
580 'repo_summary', repo_name=repo_name)
581 if user_bookmark.redirect_url and \
581 if user_bookmark.redirect_url and \
582 '${repo_url}' in user_bookmark.redirect_url:
582 '${repo_url}' in user_bookmark.redirect_url:
583 redirect_url = string.Template(user_bookmark.redirect_url)\
583 redirect_url = string.Template(user_bookmark.redirect_url)\
584 .safe_substitute({'repo_url': base_redirect_url})
584 .safe_substitute({'repo_url': base_redirect_url})
585 else:
585 else:
586 redirect_url = base_redirect_url
586 redirect_url = base_redirect_url
587 # repository group set
587 # repository group set
588 elif user_bookmark.repository_group:
588 elif user_bookmark.repository_group:
589 repo_group_name = user_bookmark.repository_group.group_name
589 repo_group_name = user_bookmark.repository_group.group_name
590 base_redirect_url = h.route_path(
590 base_redirect_url = h.route_path(
591 'repo_group_home', repo_group_name=repo_group_name)
591 'repo_group_home', repo_group_name=repo_group_name)
592 if user_bookmark.redirect_url and \
592 if user_bookmark.redirect_url and \
593 '${repo_group_url}' in user_bookmark.redirect_url:
593 '${repo_group_url}' in user_bookmark.redirect_url:
594 redirect_url = string.Template(user_bookmark.redirect_url)\
594 redirect_url = string.Template(user_bookmark.redirect_url)\
595 .safe_substitute({'repo_group_url': base_redirect_url})
595 .safe_substitute({'repo_group_url': base_redirect_url})
596 else:
596 else:
597 redirect_url = base_redirect_url
597 redirect_url = base_redirect_url
598 # custom URL set
598 # custom URL set
599 elif user_bookmark.redirect_url:
599 elif user_bookmark.redirect_url:
600 server_url = h.route_url('home').rstrip('/')
600 server_url = h.route_url('home').rstrip('/')
601 redirect_url = string.Template(user_bookmark.redirect_url) \
601 redirect_url = string.Template(user_bookmark.redirect_url) \
602 .safe_substitute({'server_url': server_url})
602 .safe_substitute({'server_url': server_url})
603
603
604 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
604 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
605 raise HTTPFound(redirect_url)
605 raise HTTPFound(redirect_url)
606
606
607 @LoginRequired()
607 @LoginRequired()
608 @NotAnonymous()
608 @NotAnonymous()
609 @view_config(
609 @view_config(
610 route_name='my_account_perms', request_method='GET',
610 route_name='my_account_perms', request_method='GET',
611 renderer='rhodecode:templates/admin/my_account/my_account.mako')
611 renderer='rhodecode:templates/admin/my_account/my_account.mako')
612 def my_account_perms(self):
612 def my_account_perms(self):
613 c = self.load_default_context()
613 c = self.load_default_context()
614 c.active = 'perms'
614 c.active = 'perms'
615
615
616 c.perm_user = c.auth_user
616 c.perm_user = c.auth_user
617 return self._get_template_context(c)
617 return self._get_template_context(c)
618
618
619 @LoginRequired()
619 @LoginRequired()
620 @NotAnonymous()
620 @NotAnonymous()
621 @view_config(
621 @view_config(
622 route_name='my_account_notifications', request_method='GET',
622 route_name='my_account_notifications', request_method='GET',
623 renderer='rhodecode:templates/admin/my_account/my_account.mako')
623 renderer='rhodecode:templates/admin/my_account/my_account.mako')
624 def my_notifications(self):
624 def my_notifications(self):
625 c = self.load_default_context()
625 c = self.load_default_context()
626 c.active = 'notifications'
626 c.active = 'notifications'
627
627
628 return self._get_template_context(c)
628 return self._get_template_context(c)
629
629
630 @LoginRequired()
630 @LoginRequired()
631 @NotAnonymous()
631 @NotAnonymous()
632 @CSRFRequired()
632 @CSRFRequired()
633 @view_config(
633 @view_config(
634 route_name='my_account_notifications_toggle_visibility',
634 route_name='my_account_notifications_toggle_visibility',
635 request_method='POST', renderer='json_ext')
635 request_method='POST', renderer='json_ext')
636 def my_notifications_toggle_visibility(self):
636 def my_notifications_toggle_visibility(self):
637 user = self._rhodecode_db_user
637 user = self._rhodecode_db_user
638 new_status = not user.user_data.get('notification_status', True)
638 new_status = not user.user_data.get('notification_status', True)
639 user.update_userdata(notification_status=new_status)
639 user.update_userdata(notification_status=new_status)
640 Session().commit()
640 Session().commit()
641 return user.user_data['notification_status']
641 return user.user_data['notification_status']
642
642
643 @LoginRequired()
643 @LoginRequired()
644 @NotAnonymous()
644 @NotAnonymous()
645 @view_config(
645 @view_config(
646 route_name='my_account_edit',
646 route_name='my_account_edit',
647 request_method='GET',
647 request_method='GET',
648 renderer='rhodecode:templates/admin/my_account/my_account.mako')
648 renderer='rhodecode:templates/admin/my_account/my_account.mako')
649 def my_account_edit(self):
649 def my_account_edit(self):
650 c = self.load_default_context()
650 c = self.load_default_context()
651 c.active = 'profile_edit'
651 c.active = 'profile_edit'
652 c.extern_type = c.user.extern_type
652 c.extern_type = c.user.extern_type
653 c.extern_name = c.user.extern_name
653 c.extern_name = c.user.extern_name
654
654
655 schema = user_schema.UserProfileSchema().bind(
655 schema = user_schema.UserProfileSchema().bind(
656 username=c.user.username, user_emails=c.user.emails)
656 username=c.user.username, user_emails=c.user.emails)
657 appstruct = {
657 appstruct = {
658 'username': c.user.username,
658 'username': c.user.username,
659 'email': c.user.email,
659 'email': c.user.email,
660 'firstname': c.user.firstname,
660 'firstname': c.user.firstname,
661 'lastname': c.user.lastname,
661 'lastname': c.user.lastname,
662 'description': c.user.description,
662 'description': c.user.description,
663 }
663 }
664 c.form = forms.RcForm(
664 c.form = forms.RcForm(
665 schema, appstruct=appstruct,
665 schema, appstruct=appstruct,
666 action=h.route_path('my_account_update'),
666 action=h.route_path('my_account_update'),
667 buttons=(forms.buttons.save, forms.buttons.reset))
667 buttons=(forms.buttons.save, forms.buttons.reset))
668
668
669 return self._get_template_context(c)
669 return self._get_template_context(c)
670
670
671 @LoginRequired()
671 @LoginRequired()
672 @NotAnonymous()
672 @NotAnonymous()
673 @CSRFRequired()
673 @CSRFRequired()
674 @view_config(
674 @view_config(
675 route_name='my_account_update',
675 route_name='my_account_update',
676 request_method='POST',
676 request_method='POST',
677 renderer='rhodecode:templates/admin/my_account/my_account.mako')
677 renderer='rhodecode:templates/admin/my_account/my_account.mako')
678 def my_account_update(self):
678 def my_account_update(self):
679 _ = self.request.translate
679 _ = self.request.translate
680 c = self.load_default_context()
680 c = self.load_default_context()
681 c.active = 'profile_edit'
681 c.active = 'profile_edit'
682 c.perm_user = c.auth_user
682 c.perm_user = c.auth_user
683 c.extern_type = c.user.extern_type
683 c.extern_type = c.user.extern_type
684 c.extern_name = c.user.extern_name
684 c.extern_name = c.user.extern_name
685
685
686 schema = user_schema.UserProfileSchema().bind(
686 schema = user_schema.UserProfileSchema().bind(
687 username=c.user.username, user_emails=c.user.emails)
687 username=c.user.username, user_emails=c.user.emails)
688 form = forms.RcForm(
688 form = forms.RcForm(
689 schema, buttons=(forms.buttons.save, forms.buttons.reset))
689 schema, buttons=(forms.buttons.save, forms.buttons.reset))
690
690
691 controls = self.request.POST.items()
691 controls = self.request.POST.items()
692 try:
692 try:
693 valid_data = form.validate(controls)
693 valid_data = form.validate(controls)
694 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
694 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
695 'new_password', 'password_confirmation']
695 'new_password', 'password_confirmation']
696 if c.extern_type != "rhodecode":
696 if c.extern_type != "rhodecode":
697 # forbid updating username for external accounts
697 # forbid updating username for external accounts
698 skip_attrs.append('username')
698 skip_attrs.append('username')
699 old_email = c.user.email
699 old_email = c.user.email
700 UserModel().update_user(
700 UserModel().update_user(
701 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
701 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
702 **valid_data)
702 **valid_data)
703 if old_email != valid_data['email']:
703 if old_email != valid_data['email']:
704 old = UserEmailMap.query() \
704 old = UserEmailMap.query() \
705 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
705 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
706 old.email = old_email
706 old.email = old_email
707 h.flash(_('Your account was updated successfully'), category='success')
707 h.flash(_('Your account was updated successfully'), category='success')
708 Session().commit()
708 Session().commit()
709 except forms.ValidationFailure as e:
709 except forms.ValidationFailure as e:
710 c.form = e
710 c.form = e
711 return self._get_template_context(c)
711 return self._get_template_context(c)
712 except Exception:
712 except Exception:
713 log.exception("Exception updating user")
713 log.exception("Exception updating user")
714 h.flash(_('Error occurred during update of user'),
714 h.flash(_('Error occurred during update of user'),
715 category='error')
715 category='error')
716 raise HTTPFound(h.route_path('my_account_profile'))
716 raise HTTPFound(h.route_path('my_account_profile'))
717
717
718 def _get_pull_requests_list(self, statuses):
718 def _get_pull_requests_list(self, statuses):
719 draw, start, limit = self._extract_chunk(self.request)
719 draw, start, limit = self._extract_chunk(self.request)
720 search_q, order_by, order_dir = self._extract_ordering(self.request)
720 search_q, order_by, order_dir = self._extract_ordering(self.request)
721 _render = self.request.get_partial_renderer(
721 _render = self.request.get_partial_renderer(
722 'rhodecode:templates/data_table/_dt_elements.mako')
722 'rhodecode:templates/data_table/_dt_elements.mako')
723
723
724 pull_requests = PullRequestModel().get_im_participating_in(
724 pull_requests = PullRequestModel().get_im_participating_in(
725 user_id=self._rhodecode_user.user_id,
725 user_id=self._rhodecode_user.user_id,
726 statuses=statuses, query=search_q,
726 statuses=statuses, query=search_q,
727 offset=start, length=limit, order_by=order_by,
727 offset=start, length=limit, order_by=order_by,
728 order_dir=order_dir)
728 order_dir=order_dir)
729
729
730 pull_requests_total_count = PullRequestModel().count_im_participating_in(
730 pull_requests_total_count = PullRequestModel().count_im_participating_in(
731 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
731 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
732
732
733 data = []
733 data = []
734 comments_model = CommentsModel()
734 comments_model = CommentsModel()
735 for pr in pull_requests:
735 for pr in pull_requests:
736 repo_id = pr.target_repo_id
736 repo_id = pr.target_repo_id
737 comments = comments_model.get_all_comments(
737 comments_count = comments_model.get_all_comments(
738 repo_id, pull_request=pr)
738 repo_id, pull_request=pr, count_only=True)
739 owned = pr.user_id == self._rhodecode_user.user_id
739 owned = pr.user_id == self._rhodecode_user.user_id
740
740
741 data.append({
741 data.append({
742 'target_repo': _render('pullrequest_target_repo',
742 'target_repo': _render('pullrequest_target_repo',
743 pr.target_repo.repo_name),
743 pr.target_repo.repo_name),
744 'name': _render('pullrequest_name',
744 'name': _render('pullrequest_name',
745 pr.pull_request_id, pr.pull_request_state,
745 pr.pull_request_id, pr.pull_request_state,
746 pr.work_in_progress, pr.target_repo.repo_name,
746 pr.work_in_progress, pr.target_repo.repo_name,
747 short=True),
747 short=True),
748 'name_raw': pr.pull_request_id,
748 'name_raw': pr.pull_request_id,
749 'status': _render('pullrequest_status',
749 'status': _render('pullrequest_status',
750 pr.calculated_review_status()),
750 pr.calculated_review_status()),
751 'title': _render('pullrequest_title', pr.title, pr.description),
751 'title': _render('pullrequest_title', pr.title, pr.description),
752 'description': h.escape(pr.description),
752 'description': h.escape(pr.description),
753 'updated_on': _render('pullrequest_updated_on',
753 'updated_on': _render('pullrequest_updated_on',
754 h.datetime_to_time(pr.updated_on)),
754 h.datetime_to_time(pr.updated_on)),
755 'updated_on_raw': h.datetime_to_time(pr.updated_on),
755 'updated_on_raw': h.datetime_to_time(pr.updated_on),
756 'created_on': _render('pullrequest_updated_on',
756 'created_on': _render('pullrequest_updated_on',
757 h.datetime_to_time(pr.created_on)),
757 h.datetime_to_time(pr.created_on)),
758 'created_on_raw': h.datetime_to_time(pr.created_on),
758 'created_on_raw': h.datetime_to_time(pr.created_on),
759 'state': pr.pull_request_state,
759 'state': pr.pull_request_state,
760 'author': _render('pullrequest_author',
760 'author': _render('pullrequest_author',
761 pr.author.full_contact, ),
761 pr.author.full_contact, ),
762 'author_raw': pr.author.full_name,
762 'author_raw': pr.author.full_name,
763 'comments': _render('pullrequest_comments', len(comments)),
763 'comments': _render('pullrequest_comments', comments_count),
764 'comments_raw': len(comments),
764 'comments_raw': comments_count,
765 'closed': pr.is_closed(),
765 'closed': pr.is_closed(),
766 'owned': owned
766 'owned': owned
767 })
767 })
768
768
769 # json used to render the grid
769 # json used to render the grid
770 data = ({
770 data = ({
771 'draw': draw,
771 'draw': draw,
772 'data': data,
772 'data': data,
773 'recordsTotal': pull_requests_total_count,
773 'recordsTotal': pull_requests_total_count,
774 'recordsFiltered': pull_requests_total_count,
774 'recordsFiltered': pull_requests_total_count,
775 })
775 })
776 return data
776 return data
777
777
778 @LoginRequired()
778 @LoginRequired()
779 @NotAnonymous()
779 @NotAnonymous()
780 @view_config(
780 @view_config(
781 route_name='my_account_pullrequests',
781 route_name='my_account_pullrequests',
782 request_method='GET',
782 request_method='GET',
783 renderer='rhodecode:templates/admin/my_account/my_account.mako')
783 renderer='rhodecode:templates/admin/my_account/my_account.mako')
784 def my_account_pullrequests(self):
784 def my_account_pullrequests(self):
785 c = self.load_default_context()
785 c = self.load_default_context()
786 c.active = 'pullrequests'
786 c.active = 'pullrequests'
787 req_get = self.request.GET
787 req_get = self.request.GET
788
788
789 c.closed = str2bool(req_get.get('pr_show_closed'))
789 c.closed = str2bool(req_get.get('pr_show_closed'))
790
790
791 return self._get_template_context(c)
791 return self._get_template_context(c)
792
792
793 @LoginRequired()
793 @LoginRequired()
794 @NotAnonymous()
794 @NotAnonymous()
795 @view_config(
795 @view_config(
796 route_name='my_account_pullrequests_data',
796 route_name='my_account_pullrequests_data',
797 request_method='GET', renderer='json_ext')
797 request_method='GET', renderer='json_ext')
798 def my_account_pullrequests_data(self):
798 def my_account_pullrequests_data(self):
799 self.load_default_context()
799 self.load_default_context()
800 req_get = self.request.GET
800 req_get = self.request.GET
801 closed = str2bool(req_get.get('closed'))
801 closed = str2bool(req_get.get('closed'))
802
802
803 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
803 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
804 if closed:
804 if closed:
805 statuses += [PullRequest.STATUS_CLOSED]
805 statuses += [PullRequest.STATUS_CLOSED]
806
806
807 data = self._get_pull_requests_list(statuses=statuses)
807 data = self._get_pull_requests_list(statuses=statuses)
808 return data
808 return data
809
809
810 @LoginRequired()
810 @LoginRequired()
811 @NotAnonymous()
811 @NotAnonymous()
812 @view_config(
812 @view_config(
813 route_name='my_account_user_group_membership',
813 route_name='my_account_user_group_membership',
814 request_method='GET',
814 request_method='GET',
815 renderer='rhodecode:templates/admin/my_account/my_account.mako')
815 renderer='rhodecode:templates/admin/my_account/my_account.mako')
816 def my_account_user_group_membership(self):
816 def my_account_user_group_membership(self):
817 c = self.load_default_context()
817 c = self.load_default_context()
818 c.active = 'user_group_membership'
818 c.active = 'user_group_membership'
819 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
819 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
820 for group in self._rhodecode_db_user.group_member]
820 for group in self._rhodecode_db_user.group_member]
821 c.user_groups = json.dumps(groups)
821 c.user_groups = json.dumps(groups)
822 return self._get_template_context(c)
822 return self._get_template_context(c)
@@ -1,1806 +1,1806 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, HTTPConflict)
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.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (
48 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 PullRequestReviewers)
50 PullRequestReviewers)
51 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.scm import ScmModel
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60
60
61 def load_default_context(self):
61 def load_default_context(self):
62 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 # backward compat., we use for OLD PRs a plain renderer
65 # backward compat., we use for OLD PRs a plain renderer
66 c.renderer = 'plain'
66 c.renderer = 'plain'
67 return c
67 return c
68
68
69 def _get_pull_requests_list(
69 def _get_pull_requests_list(
70 self, repo_name, source, filter_type, opened_by, statuses):
70 self, repo_name, source, filter_type, opened_by, statuses):
71
71
72 draw, start, limit = self._extract_chunk(self.request)
72 draw, start, limit = self._extract_chunk(self.request)
73 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 _render = self.request.get_partial_renderer(
74 _render = self.request.get_partial_renderer(
75 'rhodecode:templates/data_table/_dt_elements.mako')
75 'rhodecode:templates/data_table/_dt_elements.mako')
76
76
77 # pagination
77 # pagination
78
78
79 if filter_type == 'awaiting_review':
79 if filter_type == 'awaiting_review':
80 pull_requests = PullRequestModel().get_awaiting_review(
80 pull_requests = PullRequestModel().get_awaiting_review(
81 repo_name, search_q=search_q, source=source, opened_by=opened_by,
81 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 statuses=statuses, offset=start, length=limit,
82 statuses=statuses, offset=start, length=limit,
83 order_by=order_by, order_dir=order_dir)
83 order_by=order_by, order_dir=order_dir)
84 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 repo_name, search_q=search_q, source=source, statuses=statuses,
85 repo_name, search_q=search_q, source=source, statuses=statuses,
86 opened_by=opened_by)
86 opened_by=opened_by)
87 elif filter_type == 'awaiting_my_review':
87 elif filter_type == 'awaiting_my_review':
88 pull_requests = PullRequestModel().get_awaiting_my_review(
88 pull_requests = PullRequestModel().get_awaiting_my_review(
89 repo_name, search_q=search_q, source=source, opened_by=opened_by,
89 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 offset=start, length=limit, order_by=order_by,
91 offset=start, length=limit, order_by=order_by,
92 order_dir=order_dir)
92 order_dir=order_dir)
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
95 statuses=statuses, opened_by=opened_by)
95 statuses=statuses, opened_by=opened_by)
96 else:
96 else:
97 pull_requests = PullRequestModel().get_all(
97 pull_requests = PullRequestModel().get_all(
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 statuses=statuses, offset=start, length=limit,
99 statuses=statuses, offset=start, length=limit,
100 order_by=order_by, order_dir=order_dir)
100 order_by=order_by, order_dir=order_dir)
101 pull_requests_total_count = PullRequestModel().count_all(
101 pull_requests_total_count = PullRequestModel().count_all(
102 repo_name, search_q=search_q, source=source, statuses=statuses,
102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 opened_by=opened_by)
103 opened_by=opened_by)
104
104
105 data = []
105 data = []
106 comments_model = CommentsModel()
106 comments_model = CommentsModel()
107 for pr in pull_requests:
107 for pr in pull_requests:
108 comments = comments_model.get_all_comments(
108 comments_count = comments_model.get_all_comments(
109 self.db_repo.repo_id, pull_request=pr)
109 self.db_repo.repo_id, pull_request=pr, count_only=True)
110
110
111 data.append({
111 data.append({
112 'name': _render('pullrequest_name',
112 'name': _render('pullrequest_name',
113 pr.pull_request_id, pr.pull_request_state,
113 pr.pull_request_id, pr.pull_request_state,
114 pr.work_in_progress, pr.target_repo.repo_name),
114 pr.work_in_progress, pr.target_repo.repo_name),
115 'name_raw': pr.pull_request_id,
115 'name_raw': pr.pull_request_id,
116 'status': _render('pullrequest_status',
116 'status': _render('pullrequest_status',
117 pr.calculated_review_status()),
117 pr.calculated_review_status()),
118 'title': _render('pullrequest_title', pr.title, pr.description),
118 'title': _render('pullrequest_title', pr.title, pr.description),
119 'description': h.escape(pr.description),
119 'description': h.escape(pr.description),
120 'updated_on': _render('pullrequest_updated_on',
120 'updated_on': _render('pullrequest_updated_on',
121 h.datetime_to_time(pr.updated_on)),
121 h.datetime_to_time(pr.updated_on)),
122 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 'updated_on_raw': h.datetime_to_time(pr.updated_on),
123 'created_on': _render('pullrequest_updated_on',
123 'created_on': _render('pullrequest_updated_on',
124 h.datetime_to_time(pr.created_on)),
124 h.datetime_to_time(pr.created_on)),
125 'created_on_raw': h.datetime_to_time(pr.created_on),
125 'created_on_raw': h.datetime_to_time(pr.created_on),
126 'state': pr.pull_request_state,
126 'state': pr.pull_request_state,
127 'author': _render('pullrequest_author',
127 'author': _render('pullrequest_author',
128 pr.author.full_contact, ),
128 pr.author.full_contact, ),
129 'author_raw': pr.author.full_name,
129 'author_raw': pr.author.full_name,
130 'comments': _render('pullrequest_comments', len(comments)),
130 'comments': _render('pullrequest_comments', comments_count),
131 'comments_raw': len(comments),
131 'comments_raw': comments_count,
132 'closed': pr.is_closed(),
132 'closed': pr.is_closed(),
133 })
133 })
134
134
135 data = ({
135 data = ({
136 'draw': draw,
136 'draw': draw,
137 'data': data,
137 'data': data,
138 'recordsTotal': pull_requests_total_count,
138 'recordsTotal': pull_requests_total_count,
139 'recordsFiltered': pull_requests_total_count,
139 'recordsFiltered': pull_requests_total_count,
140 })
140 })
141 return data
141 return data
142
142
143 @LoginRequired()
143 @LoginRequired()
144 @HasRepoPermissionAnyDecorator(
144 @HasRepoPermissionAnyDecorator(
145 'repository.read', 'repository.write', 'repository.admin')
145 'repository.read', 'repository.write', 'repository.admin')
146 @view_config(
146 @view_config(
147 route_name='pullrequest_show_all', request_method='GET',
147 route_name='pullrequest_show_all', request_method='GET',
148 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
148 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
149 def pull_request_list(self):
149 def pull_request_list(self):
150 c = self.load_default_context()
150 c = self.load_default_context()
151
151
152 req_get = self.request.GET
152 req_get = self.request.GET
153 c.source = str2bool(req_get.get('source'))
153 c.source = str2bool(req_get.get('source'))
154 c.closed = str2bool(req_get.get('closed'))
154 c.closed = str2bool(req_get.get('closed'))
155 c.my = str2bool(req_get.get('my'))
155 c.my = str2bool(req_get.get('my'))
156 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
156 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
157 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
157 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
158
158
159 c.active = 'open'
159 c.active = 'open'
160 if c.my:
160 if c.my:
161 c.active = 'my'
161 c.active = 'my'
162 if c.closed:
162 if c.closed:
163 c.active = 'closed'
163 c.active = 'closed'
164 if c.awaiting_review and not c.source:
164 if c.awaiting_review and not c.source:
165 c.active = 'awaiting'
165 c.active = 'awaiting'
166 if c.source and not c.awaiting_review:
166 if c.source and not c.awaiting_review:
167 c.active = 'source'
167 c.active = 'source'
168 if c.awaiting_my_review:
168 if c.awaiting_my_review:
169 c.active = 'awaiting_my'
169 c.active = 'awaiting_my'
170
170
171 return self._get_template_context(c)
171 return self._get_template_context(c)
172
172
173 @LoginRequired()
173 @LoginRequired()
174 @HasRepoPermissionAnyDecorator(
174 @HasRepoPermissionAnyDecorator(
175 'repository.read', 'repository.write', 'repository.admin')
175 'repository.read', 'repository.write', 'repository.admin')
176 @view_config(
176 @view_config(
177 route_name='pullrequest_show_all_data', request_method='GET',
177 route_name='pullrequest_show_all_data', request_method='GET',
178 renderer='json_ext', xhr=True)
178 renderer='json_ext', xhr=True)
179 def pull_request_list_data(self):
179 def pull_request_list_data(self):
180 self.load_default_context()
180 self.load_default_context()
181
181
182 # additional filters
182 # additional filters
183 req_get = self.request.GET
183 req_get = self.request.GET
184 source = str2bool(req_get.get('source'))
184 source = str2bool(req_get.get('source'))
185 closed = str2bool(req_get.get('closed'))
185 closed = str2bool(req_get.get('closed'))
186 my = str2bool(req_get.get('my'))
186 my = str2bool(req_get.get('my'))
187 awaiting_review = str2bool(req_get.get('awaiting_review'))
187 awaiting_review = str2bool(req_get.get('awaiting_review'))
188 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
188 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
189
189
190 filter_type = 'awaiting_review' if awaiting_review \
190 filter_type = 'awaiting_review' if awaiting_review \
191 else 'awaiting_my_review' if awaiting_my_review \
191 else 'awaiting_my_review' if awaiting_my_review \
192 else None
192 else None
193
193
194 opened_by = None
194 opened_by = None
195 if my:
195 if my:
196 opened_by = [self._rhodecode_user.user_id]
196 opened_by = [self._rhodecode_user.user_id]
197
197
198 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
198 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
199 if closed:
199 if closed:
200 statuses = [PullRequest.STATUS_CLOSED]
200 statuses = [PullRequest.STATUS_CLOSED]
201
201
202 data = self._get_pull_requests_list(
202 data = self._get_pull_requests_list(
203 repo_name=self.db_repo_name, source=source,
203 repo_name=self.db_repo_name, source=source,
204 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
204 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
205
205
206 return data
206 return data
207
207
208 def _is_diff_cache_enabled(self, target_repo):
208 def _is_diff_cache_enabled(self, target_repo):
209 caching_enabled = self._get_general_setting(
209 caching_enabled = self._get_general_setting(
210 target_repo, 'rhodecode_diff_cache')
210 target_repo, 'rhodecode_diff_cache')
211 log.debug('Diff caching enabled: %s', caching_enabled)
211 log.debug('Diff caching enabled: %s', caching_enabled)
212 return caching_enabled
212 return caching_enabled
213
213
214 def _get_diffset(self, source_repo_name, source_repo,
214 def _get_diffset(self, source_repo_name, source_repo,
215 ancestor_commit,
215 ancestor_commit,
216 source_ref_id, target_ref_id,
216 source_ref_id, target_ref_id,
217 target_commit, source_commit, diff_limit, file_limit,
217 target_commit, source_commit, diff_limit, file_limit,
218 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
218 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
219
219
220 if use_ancestor:
220 if use_ancestor:
221 # we might want to not use it for versions
221 # we might want to not use it for versions
222 target_ref_id = ancestor_commit.raw_id
222 target_ref_id = ancestor_commit.raw_id
223
223
224 vcs_diff = PullRequestModel().get_diff(
224 vcs_diff = PullRequestModel().get_diff(
225 source_repo, source_ref_id, target_ref_id,
225 source_repo, source_ref_id, target_ref_id,
226 hide_whitespace_changes, diff_context)
226 hide_whitespace_changes, diff_context)
227
227
228 diff_processor = diffs.DiffProcessor(
228 diff_processor = diffs.DiffProcessor(
229 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 vcs_diff, format='newdiff', diff_limit=diff_limit,
230 file_limit=file_limit, show_full_diff=fulldiff)
230 file_limit=file_limit, show_full_diff=fulldiff)
231
231
232 _parsed = diff_processor.prepare()
232 _parsed = diff_processor.prepare()
233
233
234 diffset = codeblocks.DiffSet(
234 diffset = codeblocks.DiffSet(
235 repo_name=self.db_repo_name,
235 repo_name=self.db_repo_name,
236 source_repo_name=source_repo_name,
236 source_repo_name=source_repo_name,
237 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 source_node_getter=codeblocks.diffset_node_getter(target_commit),
238 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 target_node_getter=codeblocks.diffset_node_getter(source_commit),
239 )
239 )
240 diffset = self.path_filter.render_patchset_filtered(
240 diffset = self.path_filter.render_patchset_filtered(
241 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
242
242
243 return diffset
243 return diffset
244
244
245 def _get_range_diffset(self, source_scm, source_repo,
245 def _get_range_diffset(self, source_scm, source_repo,
246 commit1, commit2, diff_limit, file_limit,
246 commit1, commit2, diff_limit, file_limit,
247 fulldiff, hide_whitespace_changes, diff_context):
247 fulldiff, hide_whitespace_changes, diff_context):
248 vcs_diff = source_scm.get_diff(
248 vcs_diff = source_scm.get_diff(
249 commit1, commit2,
249 commit1, commit2,
250 ignore_whitespace=hide_whitespace_changes,
250 ignore_whitespace=hide_whitespace_changes,
251 context=diff_context)
251 context=diff_context)
252
252
253 diff_processor = diffs.DiffProcessor(
253 diff_processor = diffs.DiffProcessor(
254 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 vcs_diff, format='newdiff', diff_limit=diff_limit,
255 file_limit=file_limit, show_full_diff=fulldiff)
255 file_limit=file_limit, show_full_diff=fulldiff)
256
256
257 _parsed = diff_processor.prepare()
257 _parsed = diff_processor.prepare()
258
258
259 diffset = codeblocks.DiffSet(
259 diffset = codeblocks.DiffSet(
260 repo_name=source_repo.repo_name,
260 repo_name=source_repo.repo_name,
261 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 source_node_getter=codeblocks.diffset_node_getter(commit1),
262 target_node_getter=codeblocks.diffset_node_getter(commit2))
262 target_node_getter=codeblocks.diffset_node_getter(commit2))
263
263
264 diffset = self.path_filter.render_patchset_filtered(
264 diffset = self.path_filter.render_patchset_filtered(
265 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265 diffset, _parsed, commit1.raw_id, commit2.raw_id)
266
266
267 return diffset
267 return diffset
268
268
269 def register_comments_vars(self, c, pull_request, versions):
269 def register_comments_vars(self, c, pull_request, versions):
270 comments_model = CommentsModel()
270 comments_model = CommentsModel()
271
271
272 # GENERAL COMMENTS with versions #
272 # GENERAL COMMENTS with versions #
273 q = comments_model._all_general_comments_of_pull_request(pull_request)
273 q = comments_model._all_general_comments_of_pull_request(pull_request)
274 q = q.order_by(ChangesetComment.comment_id.asc())
274 q = q.order_by(ChangesetComment.comment_id.asc())
275 general_comments = q
275 general_comments = q
276
276
277 # pick comments we want to render at current version
277 # pick comments we want to render at current version
278 c.comment_versions = comments_model.aggregate_comments(
278 c.comment_versions = comments_model.aggregate_comments(
279 general_comments, versions, c.at_version_num)
279 general_comments, versions, c.at_version_num)
280
280
281 # INLINE COMMENTS with versions #
281 # INLINE COMMENTS with versions #
282 q = comments_model._all_inline_comments_of_pull_request(pull_request)
282 q = comments_model._all_inline_comments_of_pull_request(pull_request)
283 q = q.order_by(ChangesetComment.comment_id.asc())
283 q = q.order_by(ChangesetComment.comment_id.asc())
284 inline_comments = q
284 inline_comments = q
285
285
286 c.inline_versions = comments_model.aggregate_comments(
286 c.inline_versions = comments_model.aggregate_comments(
287 inline_comments, versions, c.at_version_num, inline=True)
287 inline_comments, versions, c.at_version_num, inline=True)
288
288
289 # Comments inline+general
289 # Comments inline+general
290 if c.at_version:
290 if c.at_version:
291 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
291 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
292 c.comments = c.comment_versions[c.at_version_num]['display']
292 c.comments = c.comment_versions[c.at_version_num]['display']
293 else:
293 else:
294 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
294 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
295 c.comments = c.comment_versions[c.at_version_num]['until']
295 c.comments = c.comment_versions[c.at_version_num]['until']
296
296
297 return general_comments, inline_comments
297 return general_comments, inline_comments
298
298
299 @LoginRequired()
299 @LoginRequired()
300 @HasRepoPermissionAnyDecorator(
300 @HasRepoPermissionAnyDecorator(
301 'repository.read', 'repository.write', 'repository.admin')
301 'repository.read', 'repository.write', 'repository.admin')
302 @view_config(
302 @view_config(
303 route_name='pullrequest_show', request_method='GET',
303 route_name='pullrequest_show', request_method='GET',
304 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
304 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
305 def pull_request_show(self):
305 def pull_request_show(self):
306 _ = self.request.translate
306 _ = self.request.translate
307 c = self.load_default_context()
307 c = self.load_default_context()
308
308
309 pull_request = PullRequest.get_or_404(
309 pull_request = PullRequest.get_or_404(
310 self.request.matchdict['pull_request_id'])
310 self.request.matchdict['pull_request_id'])
311 pull_request_id = pull_request.pull_request_id
311 pull_request_id = pull_request.pull_request_id
312
312
313 c.state_progressing = pull_request.is_state_changing()
313 c.state_progressing = pull_request.is_state_changing()
314 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
314 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
315
315
316 _new_state = {
316 _new_state = {
317 'created': PullRequest.STATE_CREATED,
317 'created': PullRequest.STATE_CREATED,
318 }.get(self.request.GET.get('force_state'))
318 }.get(self.request.GET.get('force_state'))
319
319
320 if c.is_super_admin and _new_state:
320 if c.is_super_admin and _new_state:
321 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
321 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
322 h.flash(
322 h.flash(
323 _('Pull Request state was force changed to `{}`').format(_new_state),
323 _('Pull Request state was force changed to `{}`').format(_new_state),
324 category='success')
324 category='success')
325 Session().commit()
325 Session().commit()
326
326
327 raise HTTPFound(h.route_path(
327 raise HTTPFound(h.route_path(
328 'pullrequest_show', repo_name=self.db_repo_name,
328 'pullrequest_show', repo_name=self.db_repo_name,
329 pull_request_id=pull_request_id))
329 pull_request_id=pull_request_id))
330
330
331 version = self.request.GET.get('version')
331 version = self.request.GET.get('version')
332 from_version = self.request.GET.get('from_version') or version
332 from_version = self.request.GET.get('from_version') or version
333 merge_checks = self.request.GET.get('merge_checks')
333 merge_checks = self.request.GET.get('merge_checks')
334 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
334 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
335 force_refresh = str2bool(self.request.GET.get('force_refresh'))
335 force_refresh = str2bool(self.request.GET.get('force_refresh'))
336 c.range_diff_on = self.request.GET.get('range-diff') == "1"
336 c.range_diff_on = self.request.GET.get('range-diff') == "1"
337
337
338 # fetch global flags of ignore ws or context lines
338 # fetch global flags of ignore ws or context lines
339 diff_context = diffs.get_diff_context(self.request)
339 diff_context = diffs.get_diff_context(self.request)
340 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
340 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
341
341
342 (pull_request_latest,
342 (pull_request_latest,
343 pull_request_at_ver,
343 pull_request_at_ver,
344 pull_request_display_obj,
344 pull_request_display_obj,
345 at_version) = PullRequestModel().get_pr_version(
345 at_version) = PullRequestModel().get_pr_version(
346 pull_request_id, version=version)
346 pull_request_id, version=version)
347
347
348 pr_closed = pull_request_latest.is_closed()
348 pr_closed = pull_request_latest.is_closed()
349
349
350 if pr_closed and (version or from_version):
350 if pr_closed and (version or from_version):
351 # not allow to browse versions for closed PR
351 # not allow to browse versions for closed PR
352 raise HTTPFound(h.route_path(
352 raise HTTPFound(h.route_path(
353 'pullrequest_show', repo_name=self.db_repo_name,
353 'pullrequest_show', repo_name=self.db_repo_name,
354 pull_request_id=pull_request_id))
354 pull_request_id=pull_request_id))
355
355
356 versions = pull_request_display_obj.versions()
356 versions = pull_request_display_obj.versions()
357 # used to store per-commit range diffs
357 # used to store per-commit range diffs
358 c.changes = collections.OrderedDict()
358 c.changes = collections.OrderedDict()
359
359
360 c.at_version = at_version
360 c.at_version = at_version
361 c.at_version_num = (at_version
361 c.at_version_num = (at_version
362 if at_version and at_version != PullRequest.LATEST_VER
362 if at_version and at_version != PullRequest.LATEST_VER
363 else None)
363 else None)
364
364
365 c.at_version_index = ChangesetComment.get_index_from_version(
365 c.at_version_index = ChangesetComment.get_index_from_version(
366 c.at_version_num, versions)
366 c.at_version_num, versions)
367
367
368 (prev_pull_request_latest,
368 (prev_pull_request_latest,
369 prev_pull_request_at_ver,
369 prev_pull_request_at_ver,
370 prev_pull_request_display_obj,
370 prev_pull_request_display_obj,
371 prev_at_version) = PullRequestModel().get_pr_version(
371 prev_at_version) = PullRequestModel().get_pr_version(
372 pull_request_id, version=from_version)
372 pull_request_id, version=from_version)
373
373
374 c.from_version = prev_at_version
374 c.from_version = prev_at_version
375 c.from_version_num = (prev_at_version
375 c.from_version_num = (prev_at_version
376 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
376 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
377 else None)
377 else None)
378 c.from_version_index = ChangesetComment.get_index_from_version(
378 c.from_version_index = ChangesetComment.get_index_from_version(
379 c.from_version_num, versions)
379 c.from_version_num, versions)
380
380
381 # define if we're in COMPARE mode or VIEW at version mode
381 # define if we're in COMPARE mode or VIEW at version mode
382 compare = at_version != prev_at_version
382 compare = at_version != prev_at_version
383
383
384 # pull_requests repo_name we opened it against
384 # pull_requests repo_name we opened it against
385 # ie. target_repo must match
385 # ie. target_repo must match
386 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
386 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
387 log.warning('Mismatch between the current repo: %s, and target %s',
387 log.warning('Mismatch between the current repo: %s, and target %s',
388 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
388 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
389 raise HTTPNotFound()
389 raise HTTPNotFound()
390
390
391 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
391 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
392
392
393 c.pull_request = pull_request_display_obj
393 c.pull_request = pull_request_display_obj
394 c.renderer = pull_request_at_ver.description_renderer or c.renderer
394 c.renderer = pull_request_at_ver.description_renderer or c.renderer
395 c.pull_request_latest = pull_request_latest
395 c.pull_request_latest = pull_request_latest
396
396
397 # inject latest version
397 # inject latest version
398 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
398 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
399 c.versions = versions + [latest_ver]
399 c.versions = versions + [latest_ver]
400
400
401 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
401 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
402 c.allowed_to_change_status = False
402 c.allowed_to_change_status = False
403 c.allowed_to_update = False
403 c.allowed_to_update = False
404 c.allowed_to_merge = False
404 c.allowed_to_merge = False
405 c.allowed_to_delete = False
405 c.allowed_to_delete = False
406 c.allowed_to_comment = False
406 c.allowed_to_comment = False
407 c.allowed_to_close = False
407 c.allowed_to_close = False
408 else:
408 else:
409 can_change_status = PullRequestModel().check_user_change_status(
409 can_change_status = PullRequestModel().check_user_change_status(
410 pull_request_at_ver, self._rhodecode_user)
410 pull_request_at_ver, self._rhodecode_user)
411 c.allowed_to_change_status = can_change_status and not pr_closed
411 c.allowed_to_change_status = can_change_status and not pr_closed
412
412
413 c.allowed_to_update = PullRequestModel().check_user_update(
413 c.allowed_to_update = PullRequestModel().check_user_update(
414 pull_request_latest, self._rhodecode_user) and not pr_closed
414 pull_request_latest, self._rhodecode_user) and not pr_closed
415 c.allowed_to_merge = PullRequestModel().check_user_merge(
415 c.allowed_to_merge = PullRequestModel().check_user_merge(
416 pull_request_latest, self._rhodecode_user) and not pr_closed
416 pull_request_latest, self._rhodecode_user) and not pr_closed
417 c.allowed_to_delete = PullRequestModel().check_user_delete(
417 c.allowed_to_delete = PullRequestModel().check_user_delete(
418 pull_request_latest, self._rhodecode_user) and not pr_closed
418 pull_request_latest, self._rhodecode_user) and not pr_closed
419 c.allowed_to_comment = not pr_closed
419 c.allowed_to_comment = not pr_closed
420 c.allowed_to_close = c.allowed_to_merge and not pr_closed
420 c.allowed_to_close = c.allowed_to_merge and not pr_closed
421
421
422 c.forbid_adding_reviewers = False
422 c.forbid_adding_reviewers = False
423 c.forbid_author_to_review = False
423 c.forbid_author_to_review = False
424 c.forbid_commit_author_to_review = False
424 c.forbid_commit_author_to_review = False
425
425
426 if pull_request_latest.reviewer_data and \
426 if pull_request_latest.reviewer_data and \
427 'rules' in pull_request_latest.reviewer_data:
427 'rules' in pull_request_latest.reviewer_data:
428 rules = pull_request_latest.reviewer_data['rules'] or {}
428 rules = pull_request_latest.reviewer_data['rules'] or {}
429 try:
429 try:
430 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
430 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
431 c.forbid_author_to_review = rules.get('forbid_author_to_review')
431 c.forbid_author_to_review = rules.get('forbid_author_to_review')
432 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
432 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
433 except Exception:
433 except Exception:
434 pass
434 pass
435
435
436 # check merge capabilities
436 # check merge capabilities
437 _merge_check = MergeCheck.validate(
437 _merge_check = MergeCheck.validate(
438 pull_request_latest, auth_user=self._rhodecode_user,
438 pull_request_latest, auth_user=self._rhodecode_user,
439 translator=self.request.translate,
439 translator=self.request.translate,
440 force_shadow_repo_refresh=force_refresh)
440 force_shadow_repo_refresh=force_refresh)
441
441
442 c.pr_merge_errors = _merge_check.error_details
442 c.pr_merge_errors = _merge_check.error_details
443 c.pr_merge_possible = not _merge_check.failed
443 c.pr_merge_possible = not _merge_check.failed
444 c.pr_merge_message = _merge_check.merge_msg
444 c.pr_merge_message = _merge_check.merge_msg
445 c.pr_merge_source_commit = _merge_check.source_commit
445 c.pr_merge_source_commit = _merge_check.source_commit
446 c.pr_merge_target_commit = _merge_check.target_commit
446 c.pr_merge_target_commit = _merge_check.target_commit
447
447
448 c.pr_merge_info = MergeCheck.get_merge_conditions(
448 c.pr_merge_info = MergeCheck.get_merge_conditions(
449 pull_request_latest, translator=self.request.translate)
449 pull_request_latest, translator=self.request.translate)
450
450
451 c.pull_request_review_status = _merge_check.review_status
451 c.pull_request_review_status = _merge_check.review_status
452 if merge_checks:
452 if merge_checks:
453 self.request.override_renderer = \
453 self.request.override_renderer = \
454 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
454 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
455 return self._get_template_context(c)
455 return self._get_template_context(c)
456
456
457 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
457 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
458 c.reviewers_count = pull_request.reviewers_count
458 c.reviewers_count = pull_request.reviewers_count
459 c.observers_count = pull_request.observers_count
459 c.observers_count = pull_request.observers_count
460
460
461 # reviewers and statuses
461 # reviewers and statuses
462 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
462 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
463 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
463 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
464 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
464 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
465
465
466 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
466 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
467 member_reviewer = h.reviewer_as_json(
467 member_reviewer = h.reviewer_as_json(
468 member, reasons=reasons, mandatory=mandatory,
468 member, reasons=reasons, mandatory=mandatory,
469 role=review_obj.role,
469 role=review_obj.role,
470 user_group=review_obj.rule_user_group_data()
470 user_group=review_obj.rule_user_group_data()
471 )
471 )
472
472
473 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
473 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
474 member_reviewer['review_status'] = current_review_status
474 member_reviewer['review_status'] = current_review_status
475 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
475 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
476 member_reviewer['allowed_to_update'] = c.allowed_to_update
476 member_reviewer['allowed_to_update'] = c.allowed_to_update
477 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
477 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
478
478
479 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
479 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
480
480
481 for observer_obj, member in pull_request_at_ver.observers():
481 for observer_obj, member in pull_request_at_ver.observers():
482 member_observer = h.reviewer_as_json(
482 member_observer = h.reviewer_as_json(
483 member, reasons=[], mandatory=False,
483 member, reasons=[], mandatory=False,
484 role=observer_obj.role,
484 role=observer_obj.role,
485 user_group=observer_obj.rule_user_group_data()
485 user_group=observer_obj.rule_user_group_data()
486 )
486 )
487 member_observer['allowed_to_update'] = c.allowed_to_update
487 member_observer['allowed_to_update'] = c.allowed_to_update
488 c.pull_request_set_observers_data_json['observers'].append(member_observer)
488 c.pull_request_set_observers_data_json['observers'].append(member_observer)
489
489
490 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
490 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
491
491
492 general_comments, inline_comments = \
492 general_comments, inline_comments = \
493 self.register_comments_vars(c, pull_request_latest, versions)
493 self.register_comments_vars(c, pull_request_latest, versions)
494
494
495 # TODOs
495 # TODOs
496 c.unresolved_comments = CommentsModel() \
496 c.unresolved_comments = CommentsModel() \
497 .get_pull_request_unresolved_todos(pull_request_latest)
497 .get_pull_request_unresolved_todos(pull_request_latest)
498 c.resolved_comments = CommentsModel() \
498 c.resolved_comments = CommentsModel() \
499 .get_pull_request_resolved_todos(pull_request_latest)
499 .get_pull_request_resolved_todos(pull_request_latest)
500
500
501 # if we use version, then do not show later comments
501 # if we use version, then do not show later comments
502 # than current version
502 # than current version
503 display_inline_comments = collections.defaultdict(
503 display_inline_comments = collections.defaultdict(
504 lambda: collections.defaultdict(list))
504 lambda: collections.defaultdict(list))
505 for co in inline_comments:
505 for co in inline_comments:
506 if c.at_version_num:
506 if c.at_version_num:
507 # pick comments that are at least UPTO given version, so we
507 # pick comments that are at least UPTO given version, so we
508 # don't render comments for higher version
508 # don't render comments for higher version
509 should_render = co.pull_request_version_id and \
509 should_render = co.pull_request_version_id and \
510 co.pull_request_version_id <= c.at_version_num
510 co.pull_request_version_id <= c.at_version_num
511 else:
511 else:
512 # showing all, for 'latest'
512 # showing all, for 'latest'
513 should_render = True
513 should_render = True
514
514
515 if should_render:
515 if should_render:
516 display_inline_comments[co.f_path][co.line_no].append(co)
516 display_inline_comments[co.f_path][co.line_no].append(co)
517
517
518 # load diff data into template context, if we use compare mode then
518 # load diff data into template context, if we use compare mode then
519 # diff is calculated based on changes between versions of PR
519 # diff is calculated based on changes between versions of PR
520
520
521 source_repo = pull_request_at_ver.source_repo
521 source_repo = pull_request_at_ver.source_repo
522 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
522 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
523
523
524 target_repo = pull_request_at_ver.target_repo
524 target_repo = pull_request_at_ver.target_repo
525 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
525 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
526
526
527 if compare:
527 if compare:
528 # in compare switch the diff base to latest commit from prev version
528 # in compare switch the diff base to latest commit from prev version
529 target_ref_id = prev_pull_request_display_obj.revisions[0]
529 target_ref_id = prev_pull_request_display_obj.revisions[0]
530
530
531 # despite opening commits for bookmarks/branches/tags, we always
531 # despite opening commits for bookmarks/branches/tags, we always
532 # convert this to rev to prevent changes after bookmark or branch change
532 # convert this to rev to prevent changes after bookmark or branch change
533 c.source_ref_type = 'rev'
533 c.source_ref_type = 'rev'
534 c.source_ref = source_ref_id
534 c.source_ref = source_ref_id
535
535
536 c.target_ref_type = 'rev'
536 c.target_ref_type = 'rev'
537 c.target_ref = target_ref_id
537 c.target_ref = target_ref_id
538
538
539 c.source_repo = source_repo
539 c.source_repo = source_repo
540 c.target_repo = target_repo
540 c.target_repo = target_repo
541
541
542 c.commit_ranges = []
542 c.commit_ranges = []
543 source_commit = EmptyCommit()
543 source_commit = EmptyCommit()
544 target_commit = EmptyCommit()
544 target_commit = EmptyCommit()
545 c.missing_requirements = False
545 c.missing_requirements = False
546
546
547 source_scm = source_repo.scm_instance()
547 source_scm = source_repo.scm_instance()
548 target_scm = target_repo.scm_instance()
548 target_scm = target_repo.scm_instance()
549
549
550 shadow_scm = None
550 shadow_scm = None
551 try:
551 try:
552 shadow_scm = pull_request_latest.get_shadow_repo()
552 shadow_scm = pull_request_latest.get_shadow_repo()
553 except Exception:
553 except Exception:
554 log.debug('Failed to get shadow repo', exc_info=True)
554 log.debug('Failed to get shadow repo', exc_info=True)
555 # try first the existing source_repo, and then shadow
555 # try first the existing source_repo, and then shadow
556 # repo if we can obtain one
556 # repo if we can obtain one
557 commits_source_repo = source_scm
557 commits_source_repo = source_scm
558 if shadow_scm:
558 if shadow_scm:
559 commits_source_repo = shadow_scm
559 commits_source_repo = shadow_scm
560
560
561 c.commits_source_repo = commits_source_repo
561 c.commits_source_repo = commits_source_repo
562 c.ancestor = None # set it to None, to hide it from PR view
562 c.ancestor = None # set it to None, to hide it from PR view
563
563
564 # empty version means latest, so we keep this to prevent
564 # empty version means latest, so we keep this to prevent
565 # double caching
565 # double caching
566 version_normalized = version or PullRequest.LATEST_VER
566 version_normalized = version or PullRequest.LATEST_VER
567 from_version_normalized = from_version or PullRequest.LATEST_VER
567 from_version_normalized = from_version or PullRequest.LATEST_VER
568
568
569 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
569 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
570 cache_file_path = diff_cache_exist(
570 cache_file_path = diff_cache_exist(
571 cache_path, 'pull_request', pull_request_id, version_normalized,
571 cache_path, 'pull_request', pull_request_id, version_normalized,
572 from_version_normalized, source_ref_id, target_ref_id,
572 from_version_normalized, source_ref_id, target_ref_id,
573 hide_whitespace_changes, diff_context, c.fulldiff)
573 hide_whitespace_changes, diff_context, c.fulldiff)
574
574
575 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
575 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
576 force_recache = self.get_recache_flag()
576 force_recache = self.get_recache_flag()
577
577
578 cached_diff = None
578 cached_diff = None
579 if caching_enabled:
579 if caching_enabled:
580 cached_diff = load_cached_diff(cache_file_path)
580 cached_diff = load_cached_diff(cache_file_path)
581
581
582 has_proper_commit_cache = (
582 has_proper_commit_cache = (
583 cached_diff and cached_diff.get('commits')
583 cached_diff and cached_diff.get('commits')
584 and len(cached_diff.get('commits', [])) == 5
584 and len(cached_diff.get('commits', [])) == 5
585 and cached_diff.get('commits')[0]
585 and cached_diff.get('commits')[0]
586 and cached_diff.get('commits')[3])
586 and cached_diff.get('commits')[3])
587
587
588 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
588 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
589 diff_commit_cache = \
589 diff_commit_cache = \
590 (ancestor_commit, commit_cache, missing_requirements,
590 (ancestor_commit, commit_cache, missing_requirements,
591 source_commit, target_commit) = cached_diff['commits']
591 source_commit, target_commit) = cached_diff['commits']
592 else:
592 else:
593 # NOTE(marcink): we reach potentially unreachable errors when a PR has
593 # NOTE(marcink): we reach potentially unreachable errors when a PR has
594 # merge errors resulting in potentially hidden commits in the shadow repo.
594 # merge errors resulting in potentially hidden commits in the shadow repo.
595 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
595 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
596 and _merge_check.merge_response
596 and _merge_check.merge_response
597 maybe_unreachable = maybe_unreachable \
597 maybe_unreachable = maybe_unreachable \
598 and _merge_check.merge_response.metadata.get('unresolved_files')
598 and _merge_check.merge_response.metadata.get('unresolved_files')
599 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
599 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
600 diff_commit_cache = \
600 diff_commit_cache = \
601 (ancestor_commit, commit_cache, missing_requirements,
601 (ancestor_commit, commit_cache, missing_requirements,
602 source_commit, target_commit) = self.get_commits(
602 source_commit, target_commit) = self.get_commits(
603 commits_source_repo,
603 commits_source_repo,
604 pull_request_at_ver,
604 pull_request_at_ver,
605 source_commit,
605 source_commit,
606 source_ref_id,
606 source_ref_id,
607 source_scm,
607 source_scm,
608 target_commit,
608 target_commit,
609 target_ref_id,
609 target_ref_id,
610 target_scm,
610 target_scm,
611 maybe_unreachable=maybe_unreachable)
611 maybe_unreachable=maybe_unreachable)
612
612
613 # register our commit range
613 # register our commit range
614 for comm in commit_cache.values():
614 for comm in commit_cache.values():
615 c.commit_ranges.append(comm)
615 c.commit_ranges.append(comm)
616
616
617 c.missing_requirements = missing_requirements
617 c.missing_requirements = missing_requirements
618 c.ancestor_commit = ancestor_commit
618 c.ancestor_commit = ancestor_commit
619 c.statuses = source_repo.statuses(
619 c.statuses = source_repo.statuses(
620 [x.raw_id for x in c.commit_ranges])
620 [x.raw_id for x in c.commit_ranges])
621
621
622 # auto collapse if we have more than limit
622 # auto collapse if we have more than limit
623 collapse_limit = diffs.DiffProcessor._collapse_commits_over
623 collapse_limit = diffs.DiffProcessor._collapse_commits_over
624 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
624 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
625 c.compare_mode = compare
625 c.compare_mode = compare
626
626
627 # diff_limit is the old behavior, will cut off the whole diff
627 # diff_limit is the old behavior, will cut off the whole diff
628 # if the limit is applied otherwise will just hide the
628 # if the limit is applied otherwise will just hide the
629 # big files from the front-end
629 # big files from the front-end
630 diff_limit = c.visual.cut_off_limit_diff
630 diff_limit = c.visual.cut_off_limit_diff
631 file_limit = c.visual.cut_off_limit_file
631 file_limit = c.visual.cut_off_limit_file
632
632
633 c.missing_commits = False
633 c.missing_commits = False
634 if (c.missing_requirements
634 if (c.missing_requirements
635 or isinstance(source_commit, EmptyCommit)
635 or isinstance(source_commit, EmptyCommit)
636 or source_commit == target_commit):
636 or source_commit == target_commit):
637
637
638 c.missing_commits = True
638 c.missing_commits = True
639 else:
639 else:
640 c.inline_comments = display_inline_comments
640 c.inline_comments = display_inline_comments
641
641
642 use_ancestor = True
642 use_ancestor = True
643 if from_version_normalized != version_normalized:
643 if from_version_normalized != version_normalized:
644 use_ancestor = False
644 use_ancestor = False
645
645
646 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
646 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
647 if not force_recache and has_proper_diff_cache:
647 if not force_recache and has_proper_diff_cache:
648 c.diffset = cached_diff['diff']
648 c.diffset = cached_diff['diff']
649 else:
649 else:
650 try:
650 try:
651 c.diffset = self._get_diffset(
651 c.diffset = self._get_diffset(
652 c.source_repo.repo_name, commits_source_repo,
652 c.source_repo.repo_name, commits_source_repo,
653 c.ancestor_commit,
653 c.ancestor_commit,
654 source_ref_id, target_ref_id,
654 source_ref_id, target_ref_id,
655 target_commit, source_commit,
655 target_commit, source_commit,
656 diff_limit, file_limit, c.fulldiff,
656 diff_limit, file_limit, c.fulldiff,
657 hide_whitespace_changes, diff_context,
657 hide_whitespace_changes, diff_context,
658 use_ancestor=use_ancestor
658 use_ancestor=use_ancestor
659 )
659 )
660
660
661 # save cached diff
661 # save cached diff
662 if caching_enabled:
662 if caching_enabled:
663 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
663 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
664 except CommitDoesNotExistError:
664 except CommitDoesNotExistError:
665 log.exception('Failed to generate diffset')
665 log.exception('Failed to generate diffset')
666 c.missing_commits = True
666 c.missing_commits = True
667
667
668 if not c.missing_commits:
668 if not c.missing_commits:
669
669
670 c.limited_diff = c.diffset.limited_diff
670 c.limited_diff = c.diffset.limited_diff
671
671
672 # calculate removed files that are bound to comments
672 # calculate removed files that are bound to comments
673 comment_deleted_files = [
673 comment_deleted_files = [
674 fname for fname in display_inline_comments
674 fname for fname in display_inline_comments
675 if fname not in c.diffset.file_stats]
675 if fname not in c.diffset.file_stats]
676
676
677 c.deleted_files_comments = collections.defaultdict(dict)
677 c.deleted_files_comments = collections.defaultdict(dict)
678 for fname, per_line_comments in display_inline_comments.items():
678 for fname, per_line_comments in display_inline_comments.items():
679 if fname in comment_deleted_files:
679 if fname in comment_deleted_files:
680 c.deleted_files_comments[fname]['stats'] = 0
680 c.deleted_files_comments[fname]['stats'] = 0
681 c.deleted_files_comments[fname]['comments'] = list()
681 c.deleted_files_comments[fname]['comments'] = list()
682 for lno, comments in per_line_comments.items():
682 for lno, comments in per_line_comments.items():
683 c.deleted_files_comments[fname]['comments'].extend(comments)
683 c.deleted_files_comments[fname]['comments'].extend(comments)
684
684
685 # maybe calculate the range diff
685 # maybe calculate the range diff
686 if c.range_diff_on:
686 if c.range_diff_on:
687 # TODO(marcink): set whitespace/context
687 # TODO(marcink): set whitespace/context
688 context_lcl = 3
688 context_lcl = 3
689 ign_whitespace_lcl = False
689 ign_whitespace_lcl = False
690
690
691 for commit in c.commit_ranges:
691 for commit in c.commit_ranges:
692 commit2 = commit
692 commit2 = commit
693 commit1 = commit.first_parent
693 commit1 = commit.first_parent
694
694
695 range_diff_cache_file_path = diff_cache_exist(
695 range_diff_cache_file_path = diff_cache_exist(
696 cache_path, 'diff', commit.raw_id,
696 cache_path, 'diff', commit.raw_id,
697 ign_whitespace_lcl, context_lcl, c.fulldiff)
697 ign_whitespace_lcl, context_lcl, c.fulldiff)
698
698
699 cached_diff = None
699 cached_diff = None
700 if caching_enabled:
700 if caching_enabled:
701 cached_diff = load_cached_diff(range_diff_cache_file_path)
701 cached_diff = load_cached_diff(range_diff_cache_file_path)
702
702
703 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
703 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
704 if not force_recache and has_proper_diff_cache:
704 if not force_recache and has_proper_diff_cache:
705 diffset = cached_diff['diff']
705 diffset = cached_diff['diff']
706 else:
706 else:
707 diffset = self._get_range_diffset(
707 diffset = self._get_range_diffset(
708 commits_source_repo, source_repo,
708 commits_source_repo, source_repo,
709 commit1, commit2, diff_limit, file_limit,
709 commit1, commit2, diff_limit, file_limit,
710 c.fulldiff, ign_whitespace_lcl, context_lcl
710 c.fulldiff, ign_whitespace_lcl, context_lcl
711 )
711 )
712
712
713 # save cached diff
713 # save cached diff
714 if caching_enabled:
714 if caching_enabled:
715 cache_diff(range_diff_cache_file_path, diffset, None)
715 cache_diff(range_diff_cache_file_path, diffset, None)
716
716
717 c.changes[commit.raw_id] = diffset
717 c.changes[commit.raw_id] = diffset
718
718
719 # this is a hack to properly display links, when creating PR, the
719 # this is a hack to properly display links, when creating PR, the
720 # compare view and others uses different notation, and
720 # compare view and others uses different notation, and
721 # compare_commits.mako renders links based on the target_repo.
721 # compare_commits.mako renders links based on the target_repo.
722 # We need to swap that here to generate it properly on the html side
722 # We need to swap that here to generate it properly on the html side
723 c.target_repo = c.source_repo
723 c.target_repo = c.source_repo
724
724
725 c.commit_statuses = ChangesetStatus.STATUSES
725 c.commit_statuses = ChangesetStatus.STATUSES
726
726
727 c.show_version_changes = not pr_closed
727 c.show_version_changes = not pr_closed
728 if c.show_version_changes:
728 if c.show_version_changes:
729 cur_obj = pull_request_at_ver
729 cur_obj = pull_request_at_ver
730 prev_obj = prev_pull_request_at_ver
730 prev_obj = prev_pull_request_at_ver
731
731
732 old_commit_ids = prev_obj.revisions
732 old_commit_ids = prev_obj.revisions
733 new_commit_ids = cur_obj.revisions
733 new_commit_ids = cur_obj.revisions
734 commit_changes = PullRequestModel()._calculate_commit_id_changes(
734 commit_changes = PullRequestModel()._calculate_commit_id_changes(
735 old_commit_ids, new_commit_ids)
735 old_commit_ids, new_commit_ids)
736 c.commit_changes_summary = commit_changes
736 c.commit_changes_summary = commit_changes
737
737
738 # calculate the diff for commits between versions
738 # calculate the diff for commits between versions
739 c.commit_changes = []
739 c.commit_changes = []
740
740
741 def mark(cs, fw):
741 def mark(cs, fw):
742 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
742 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
743
743
744 for c_type, raw_id in mark(commit_changes.added, 'a') \
744 for c_type, raw_id in mark(commit_changes.added, 'a') \
745 + mark(commit_changes.removed, 'r') \
745 + mark(commit_changes.removed, 'r') \
746 + mark(commit_changes.common, 'c'):
746 + mark(commit_changes.common, 'c'):
747
747
748 if raw_id in commit_cache:
748 if raw_id in commit_cache:
749 commit = commit_cache[raw_id]
749 commit = commit_cache[raw_id]
750 else:
750 else:
751 try:
751 try:
752 commit = commits_source_repo.get_commit(raw_id)
752 commit = commits_source_repo.get_commit(raw_id)
753 except CommitDoesNotExistError:
753 except CommitDoesNotExistError:
754 # in case we fail extracting still use "dummy" commit
754 # in case we fail extracting still use "dummy" commit
755 # for display in commit diff
755 # for display in commit diff
756 commit = h.AttributeDict(
756 commit = h.AttributeDict(
757 {'raw_id': raw_id,
757 {'raw_id': raw_id,
758 'message': 'EMPTY or MISSING COMMIT'})
758 'message': 'EMPTY or MISSING COMMIT'})
759 c.commit_changes.append([c_type, commit])
759 c.commit_changes.append([c_type, commit])
760
760
761 # current user review statuses for each version
761 # current user review statuses for each version
762 c.review_versions = {}
762 c.review_versions = {}
763 if self._rhodecode_user.user_id in c.allowed_reviewers:
763 if self._rhodecode_user.user_id in c.allowed_reviewers:
764 for co in general_comments:
764 for co in general_comments:
765 if co.author.user_id == self._rhodecode_user.user_id:
765 if co.author.user_id == self._rhodecode_user.user_id:
766 status = co.status_change
766 status = co.status_change
767 if status:
767 if status:
768 _ver_pr = status[0].comment.pull_request_version_id
768 _ver_pr = status[0].comment.pull_request_version_id
769 c.review_versions[_ver_pr] = status[0]
769 c.review_versions[_ver_pr] = status[0]
770
770
771 return self._get_template_context(c)
771 return self._get_template_context(c)
772
772
773 def get_commits(
773 def get_commits(
774 self, commits_source_repo, pull_request_at_ver, source_commit,
774 self, commits_source_repo, pull_request_at_ver, source_commit,
775 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
775 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
776 maybe_unreachable=False):
776 maybe_unreachable=False):
777
777
778 commit_cache = collections.OrderedDict()
778 commit_cache = collections.OrderedDict()
779 missing_requirements = False
779 missing_requirements = False
780
780
781 try:
781 try:
782 pre_load = ["author", "date", "message", "branch", "parents"]
782 pre_load = ["author", "date", "message", "branch", "parents"]
783
783
784 pull_request_commits = pull_request_at_ver.revisions
784 pull_request_commits = pull_request_at_ver.revisions
785 log.debug('Loading %s commits from %s',
785 log.debug('Loading %s commits from %s',
786 len(pull_request_commits), commits_source_repo)
786 len(pull_request_commits), commits_source_repo)
787
787
788 for rev in pull_request_commits:
788 for rev in pull_request_commits:
789 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
789 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
790 maybe_unreachable=maybe_unreachable)
790 maybe_unreachable=maybe_unreachable)
791 commit_cache[comm.raw_id] = comm
791 commit_cache[comm.raw_id] = comm
792
792
793 # Order here matters, we first need to get target, and then
793 # Order here matters, we first need to get target, and then
794 # the source
794 # the source
795 target_commit = commits_source_repo.get_commit(
795 target_commit = commits_source_repo.get_commit(
796 commit_id=safe_str(target_ref_id))
796 commit_id=safe_str(target_ref_id))
797
797
798 source_commit = commits_source_repo.get_commit(
798 source_commit = commits_source_repo.get_commit(
799 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
799 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
800 except CommitDoesNotExistError:
800 except CommitDoesNotExistError:
801 log.warning('Failed to get commit from `{}` repo'.format(
801 log.warning('Failed to get commit from `{}` repo'.format(
802 commits_source_repo), exc_info=True)
802 commits_source_repo), exc_info=True)
803 except RepositoryRequirementError:
803 except RepositoryRequirementError:
804 log.warning('Failed to get all required data from repo', exc_info=True)
804 log.warning('Failed to get all required data from repo', exc_info=True)
805 missing_requirements = True
805 missing_requirements = True
806
806
807 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
807 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
808
808
809 try:
809 try:
810 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
810 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
811 except Exception:
811 except Exception:
812 ancestor_commit = None
812 ancestor_commit = None
813
813
814 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
814 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
815
815
816 def assure_not_empty_repo(self):
816 def assure_not_empty_repo(self):
817 _ = self.request.translate
817 _ = self.request.translate
818
818
819 try:
819 try:
820 self.db_repo.scm_instance().get_commit()
820 self.db_repo.scm_instance().get_commit()
821 except EmptyRepositoryError:
821 except EmptyRepositoryError:
822 h.flash(h.literal(_('There are no commits yet')),
822 h.flash(h.literal(_('There are no commits yet')),
823 category='warning')
823 category='warning')
824 raise HTTPFound(
824 raise HTTPFound(
825 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
825 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
826
826
827 @LoginRequired()
827 @LoginRequired()
828 @NotAnonymous()
828 @NotAnonymous()
829 @HasRepoPermissionAnyDecorator(
829 @HasRepoPermissionAnyDecorator(
830 'repository.read', 'repository.write', 'repository.admin')
830 'repository.read', 'repository.write', 'repository.admin')
831 @view_config(
831 @view_config(
832 route_name='pullrequest_new', request_method='GET',
832 route_name='pullrequest_new', request_method='GET',
833 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
833 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
834 def pull_request_new(self):
834 def pull_request_new(self):
835 _ = self.request.translate
835 _ = self.request.translate
836 c = self.load_default_context()
836 c = self.load_default_context()
837
837
838 self.assure_not_empty_repo()
838 self.assure_not_empty_repo()
839 source_repo = self.db_repo
839 source_repo = self.db_repo
840
840
841 commit_id = self.request.GET.get('commit')
841 commit_id = self.request.GET.get('commit')
842 branch_ref = self.request.GET.get('branch')
842 branch_ref = self.request.GET.get('branch')
843 bookmark_ref = self.request.GET.get('bookmark')
843 bookmark_ref = self.request.GET.get('bookmark')
844
844
845 try:
845 try:
846 source_repo_data = PullRequestModel().generate_repo_data(
846 source_repo_data = PullRequestModel().generate_repo_data(
847 source_repo, commit_id=commit_id,
847 source_repo, commit_id=commit_id,
848 branch=branch_ref, bookmark=bookmark_ref,
848 branch=branch_ref, bookmark=bookmark_ref,
849 translator=self.request.translate)
849 translator=self.request.translate)
850 except CommitDoesNotExistError as e:
850 except CommitDoesNotExistError as e:
851 log.exception(e)
851 log.exception(e)
852 h.flash(_('Commit does not exist'), 'error')
852 h.flash(_('Commit does not exist'), 'error')
853 raise HTTPFound(
853 raise HTTPFound(
854 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
854 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
855
855
856 default_target_repo = source_repo
856 default_target_repo = source_repo
857
857
858 if source_repo.parent and c.has_origin_repo_read_perm:
858 if source_repo.parent and c.has_origin_repo_read_perm:
859 parent_vcs_obj = source_repo.parent.scm_instance()
859 parent_vcs_obj = source_repo.parent.scm_instance()
860 if parent_vcs_obj and not parent_vcs_obj.is_empty():
860 if parent_vcs_obj and not parent_vcs_obj.is_empty():
861 # change default if we have a parent repo
861 # change default if we have a parent repo
862 default_target_repo = source_repo.parent
862 default_target_repo = source_repo.parent
863
863
864 target_repo_data = PullRequestModel().generate_repo_data(
864 target_repo_data = PullRequestModel().generate_repo_data(
865 default_target_repo, translator=self.request.translate)
865 default_target_repo, translator=self.request.translate)
866
866
867 selected_source_ref = source_repo_data['refs']['selected_ref']
867 selected_source_ref = source_repo_data['refs']['selected_ref']
868 title_source_ref = ''
868 title_source_ref = ''
869 if selected_source_ref:
869 if selected_source_ref:
870 title_source_ref = selected_source_ref.split(':', 2)[1]
870 title_source_ref = selected_source_ref.split(':', 2)[1]
871 c.default_title = PullRequestModel().generate_pullrequest_title(
871 c.default_title = PullRequestModel().generate_pullrequest_title(
872 source=source_repo.repo_name,
872 source=source_repo.repo_name,
873 source_ref=title_source_ref,
873 source_ref=title_source_ref,
874 target=default_target_repo.repo_name
874 target=default_target_repo.repo_name
875 )
875 )
876
876
877 c.default_repo_data = {
877 c.default_repo_data = {
878 'source_repo_name': source_repo.repo_name,
878 'source_repo_name': source_repo.repo_name,
879 'source_refs_json': json.dumps(source_repo_data),
879 'source_refs_json': json.dumps(source_repo_data),
880 'target_repo_name': default_target_repo.repo_name,
880 'target_repo_name': default_target_repo.repo_name,
881 'target_refs_json': json.dumps(target_repo_data),
881 'target_refs_json': json.dumps(target_repo_data),
882 }
882 }
883 c.default_source_ref = selected_source_ref
883 c.default_source_ref = selected_source_ref
884
884
885 return self._get_template_context(c)
885 return self._get_template_context(c)
886
886
887 @LoginRequired()
887 @LoginRequired()
888 @NotAnonymous()
888 @NotAnonymous()
889 @HasRepoPermissionAnyDecorator(
889 @HasRepoPermissionAnyDecorator(
890 'repository.read', 'repository.write', 'repository.admin')
890 'repository.read', 'repository.write', 'repository.admin')
891 @view_config(
891 @view_config(
892 route_name='pullrequest_repo_refs', request_method='GET',
892 route_name='pullrequest_repo_refs', request_method='GET',
893 renderer='json_ext', xhr=True)
893 renderer='json_ext', xhr=True)
894 def pull_request_repo_refs(self):
894 def pull_request_repo_refs(self):
895 self.load_default_context()
895 self.load_default_context()
896 target_repo_name = self.request.matchdict['target_repo_name']
896 target_repo_name = self.request.matchdict['target_repo_name']
897 repo = Repository.get_by_repo_name(target_repo_name)
897 repo = Repository.get_by_repo_name(target_repo_name)
898 if not repo:
898 if not repo:
899 raise HTTPNotFound()
899 raise HTTPNotFound()
900
900
901 target_perm = HasRepoPermissionAny(
901 target_perm = HasRepoPermissionAny(
902 'repository.read', 'repository.write', 'repository.admin')(
902 'repository.read', 'repository.write', 'repository.admin')(
903 target_repo_name)
903 target_repo_name)
904 if not target_perm:
904 if not target_perm:
905 raise HTTPNotFound()
905 raise HTTPNotFound()
906
906
907 return PullRequestModel().generate_repo_data(
907 return PullRequestModel().generate_repo_data(
908 repo, translator=self.request.translate)
908 repo, translator=self.request.translate)
909
909
910 @LoginRequired()
910 @LoginRequired()
911 @NotAnonymous()
911 @NotAnonymous()
912 @HasRepoPermissionAnyDecorator(
912 @HasRepoPermissionAnyDecorator(
913 'repository.read', 'repository.write', 'repository.admin')
913 'repository.read', 'repository.write', 'repository.admin')
914 @view_config(
914 @view_config(
915 route_name='pullrequest_repo_targets', request_method='GET',
915 route_name='pullrequest_repo_targets', request_method='GET',
916 renderer='json_ext', xhr=True)
916 renderer='json_ext', xhr=True)
917 def pullrequest_repo_targets(self):
917 def pullrequest_repo_targets(self):
918 _ = self.request.translate
918 _ = self.request.translate
919 filter_query = self.request.GET.get('query')
919 filter_query = self.request.GET.get('query')
920
920
921 # get the parents
921 # get the parents
922 parent_target_repos = []
922 parent_target_repos = []
923 if self.db_repo.parent:
923 if self.db_repo.parent:
924 parents_query = Repository.query() \
924 parents_query = Repository.query() \
925 .order_by(func.length(Repository.repo_name)) \
925 .order_by(func.length(Repository.repo_name)) \
926 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
926 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
927
927
928 if filter_query:
928 if filter_query:
929 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
929 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
930 parents_query = parents_query.filter(
930 parents_query = parents_query.filter(
931 Repository.repo_name.ilike(ilike_expression))
931 Repository.repo_name.ilike(ilike_expression))
932 parents = parents_query.limit(20).all()
932 parents = parents_query.limit(20).all()
933
933
934 for parent in parents:
934 for parent in parents:
935 parent_vcs_obj = parent.scm_instance()
935 parent_vcs_obj = parent.scm_instance()
936 if parent_vcs_obj and not parent_vcs_obj.is_empty():
936 if parent_vcs_obj and not parent_vcs_obj.is_empty():
937 parent_target_repos.append(parent)
937 parent_target_repos.append(parent)
938
938
939 # get other forks, and repo itself
939 # get other forks, and repo itself
940 query = Repository.query() \
940 query = Repository.query() \
941 .order_by(func.length(Repository.repo_name)) \
941 .order_by(func.length(Repository.repo_name)) \
942 .filter(
942 .filter(
943 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
943 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
944 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
944 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
945 ) \
945 ) \
946 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
946 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
947
947
948 if filter_query:
948 if filter_query:
949 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
949 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
950 query = query.filter(Repository.repo_name.ilike(ilike_expression))
950 query = query.filter(Repository.repo_name.ilike(ilike_expression))
951
951
952 limit = max(20 - len(parent_target_repos), 5) # not less then 5
952 limit = max(20 - len(parent_target_repos), 5) # not less then 5
953 target_repos = query.limit(limit).all()
953 target_repos = query.limit(limit).all()
954
954
955 all_target_repos = target_repos + parent_target_repos
955 all_target_repos = target_repos + parent_target_repos
956
956
957 repos = []
957 repos = []
958 # This checks permissions to the repositories
958 # This checks permissions to the repositories
959 for obj in ScmModel().get_repos(all_target_repos):
959 for obj in ScmModel().get_repos(all_target_repos):
960 repos.append({
960 repos.append({
961 'id': obj['name'],
961 'id': obj['name'],
962 'text': obj['name'],
962 'text': obj['name'],
963 'type': 'repo',
963 'type': 'repo',
964 'repo_id': obj['dbrepo']['repo_id'],
964 'repo_id': obj['dbrepo']['repo_id'],
965 'repo_type': obj['dbrepo']['repo_type'],
965 'repo_type': obj['dbrepo']['repo_type'],
966 'private': obj['dbrepo']['private'],
966 'private': obj['dbrepo']['private'],
967
967
968 })
968 })
969
969
970 data = {
970 data = {
971 'more': False,
971 'more': False,
972 'results': [{
972 'results': [{
973 'text': _('Repositories'),
973 'text': _('Repositories'),
974 'children': repos
974 'children': repos
975 }] if repos else []
975 }] if repos else []
976 }
976 }
977 return data
977 return data
978
978
979 def _get_existing_ids(self, post_data):
979 def _get_existing_ids(self, post_data):
980 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
980 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
981
981
982 @LoginRequired()
982 @LoginRequired()
983 @NotAnonymous()
983 @NotAnonymous()
984 @HasRepoPermissionAnyDecorator(
984 @HasRepoPermissionAnyDecorator(
985 'repository.read', 'repository.write', 'repository.admin')
985 'repository.read', 'repository.write', 'repository.admin')
986 @view_config(
986 @view_config(
987 route_name='pullrequest_comments', request_method='POST',
987 route_name='pullrequest_comments', request_method='POST',
988 renderer='string_html', xhr=True)
988 renderer='string_html', xhr=True)
989 def pullrequest_comments(self):
989 def pullrequest_comments(self):
990 self.load_default_context()
990 self.load_default_context()
991
991
992 pull_request = PullRequest.get_or_404(
992 pull_request = PullRequest.get_or_404(
993 self.request.matchdict['pull_request_id'])
993 self.request.matchdict['pull_request_id'])
994 pull_request_id = pull_request.pull_request_id
994 pull_request_id = pull_request.pull_request_id
995 version = self.request.GET.get('version')
995 version = self.request.GET.get('version')
996
996
997 _render = self.request.get_partial_renderer(
997 _render = self.request.get_partial_renderer(
998 'rhodecode:templates/base/sidebar.mako')
998 'rhodecode:templates/base/sidebar.mako')
999 c = _render.get_call_context()
999 c = _render.get_call_context()
1000
1000
1001 (pull_request_latest,
1001 (pull_request_latest,
1002 pull_request_at_ver,
1002 pull_request_at_ver,
1003 pull_request_display_obj,
1003 pull_request_display_obj,
1004 at_version) = PullRequestModel().get_pr_version(
1004 at_version) = PullRequestModel().get_pr_version(
1005 pull_request_id, version=version)
1005 pull_request_id, version=version)
1006 versions = pull_request_display_obj.versions()
1006 versions = pull_request_display_obj.versions()
1007 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1007 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1008 c.versions = versions + [latest_ver]
1008 c.versions = versions + [latest_ver]
1009
1009
1010 c.at_version = at_version
1010 c.at_version = at_version
1011 c.at_version_num = (at_version
1011 c.at_version_num = (at_version
1012 if at_version and at_version != PullRequest.LATEST_VER
1012 if at_version and at_version != PullRequest.LATEST_VER
1013 else None)
1013 else None)
1014
1014
1015 self.register_comments_vars(c, pull_request_latest, versions)
1015 self.register_comments_vars(c, pull_request_latest, versions)
1016 all_comments = c.inline_comments_flat + c.comments
1016 all_comments = c.inline_comments_flat + c.comments
1017
1017
1018 existing_ids = self._get_existing_ids(self.request.POST)
1018 existing_ids = self._get_existing_ids(self.request.POST)
1019 return _render('comments_table', all_comments, len(all_comments),
1019 return _render('comments_table', all_comments, len(all_comments),
1020 existing_ids=existing_ids)
1020 existing_ids=existing_ids)
1021
1021
1022 @LoginRequired()
1022 @LoginRequired()
1023 @NotAnonymous()
1023 @NotAnonymous()
1024 @HasRepoPermissionAnyDecorator(
1024 @HasRepoPermissionAnyDecorator(
1025 'repository.read', 'repository.write', 'repository.admin')
1025 'repository.read', 'repository.write', 'repository.admin')
1026 @view_config(
1026 @view_config(
1027 route_name='pullrequest_todos', request_method='POST',
1027 route_name='pullrequest_todos', request_method='POST',
1028 renderer='string_html', xhr=True)
1028 renderer='string_html', xhr=True)
1029 def pullrequest_todos(self):
1029 def pullrequest_todos(self):
1030 self.load_default_context()
1030 self.load_default_context()
1031
1031
1032 pull_request = PullRequest.get_or_404(
1032 pull_request = PullRequest.get_or_404(
1033 self.request.matchdict['pull_request_id'])
1033 self.request.matchdict['pull_request_id'])
1034 pull_request_id = pull_request.pull_request_id
1034 pull_request_id = pull_request.pull_request_id
1035 version = self.request.GET.get('version')
1035 version = self.request.GET.get('version')
1036
1036
1037 _render = self.request.get_partial_renderer(
1037 _render = self.request.get_partial_renderer(
1038 'rhodecode:templates/base/sidebar.mako')
1038 'rhodecode:templates/base/sidebar.mako')
1039 c = _render.get_call_context()
1039 c = _render.get_call_context()
1040 (pull_request_latest,
1040 (pull_request_latest,
1041 pull_request_at_ver,
1041 pull_request_at_ver,
1042 pull_request_display_obj,
1042 pull_request_display_obj,
1043 at_version) = PullRequestModel().get_pr_version(
1043 at_version) = PullRequestModel().get_pr_version(
1044 pull_request_id, version=version)
1044 pull_request_id, version=version)
1045 versions = pull_request_display_obj.versions()
1045 versions = pull_request_display_obj.versions()
1046 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1046 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1047 c.versions = versions + [latest_ver]
1047 c.versions = versions + [latest_ver]
1048
1048
1049 c.at_version = at_version
1049 c.at_version = at_version
1050 c.at_version_num = (at_version
1050 c.at_version_num = (at_version
1051 if at_version and at_version != PullRequest.LATEST_VER
1051 if at_version and at_version != PullRequest.LATEST_VER
1052 else None)
1052 else None)
1053
1053
1054 c.unresolved_comments = CommentsModel() \
1054 c.unresolved_comments = CommentsModel() \
1055 .get_pull_request_unresolved_todos(pull_request)
1055 .get_pull_request_unresolved_todos(pull_request)
1056 c.resolved_comments = CommentsModel() \
1056 c.resolved_comments = CommentsModel() \
1057 .get_pull_request_resolved_todos(pull_request)
1057 .get_pull_request_resolved_todos(pull_request)
1058
1058
1059 all_comments = c.unresolved_comments + c.resolved_comments
1059 all_comments = c.unresolved_comments + c.resolved_comments
1060 existing_ids = self._get_existing_ids(self.request.POST)
1060 existing_ids = self._get_existing_ids(self.request.POST)
1061 return _render('comments_table', all_comments, len(c.unresolved_comments),
1061 return _render('comments_table', all_comments, len(c.unresolved_comments),
1062 todo_comments=True, existing_ids=existing_ids)
1062 todo_comments=True, existing_ids=existing_ids)
1063
1063
1064 @LoginRequired()
1064 @LoginRequired()
1065 @NotAnonymous()
1065 @NotAnonymous()
1066 @HasRepoPermissionAnyDecorator(
1066 @HasRepoPermissionAnyDecorator(
1067 'repository.read', 'repository.write', 'repository.admin')
1067 'repository.read', 'repository.write', 'repository.admin')
1068 @CSRFRequired()
1068 @CSRFRequired()
1069 @view_config(
1069 @view_config(
1070 route_name='pullrequest_create', request_method='POST',
1070 route_name='pullrequest_create', request_method='POST',
1071 renderer=None)
1071 renderer=None)
1072 def pull_request_create(self):
1072 def pull_request_create(self):
1073 _ = self.request.translate
1073 _ = self.request.translate
1074 self.assure_not_empty_repo()
1074 self.assure_not_empty_repo()
1075 self.load_default_context()
1075 self.load_default_context()
1076
1076
1077 controls = peppercorn.parse(self.request.POST.items())
1077 controls = peppercorn.parse(self.request.POST.items())
1078
1078
1079 try:
1079 try:
1080 form = PullRequestForm(
1080 form = PullRequestForm(
1081 self.request.translate, self.db_repo.repo_id)()
1081 self.request.translate, self.db_repo.repo_id)()
1082 _form = form.to_python(controls)
1082 _form = form.to_python(controls)
1083 except formencode.Invalid as errors:
1083 except formencode.Invalid as errors:
1084 if errors.error_dict.get('revisions'):
1084 if errors.error_dict.get('revisions'):
1085 msg = 'Revisions: %s' % errors.error_dict['revisions']
1085 msg = 'Revisions: %s' % errors.error_dict['revisions']
1086 elif errors.error_dict.get('pullrequest_title'):
1086 elif errors.error_dict.get('pullrequest_title'):
1087 msg = errors.error_dict.get('pullrequest_title')
1087 msg = errors.error_dict.get('pullrequest_title')
1088 else:
1088 else:
1089 msg = _('Error creating pull request: {}').format(errors)
1089 msg = _('Error creating pull request: {}').format(errors)
1090 log.exception(msg)
1090 log.exception(msg)
1091 h.flash(msg, 'error')
1091 h.flash(msg, 'error')
1092
1092
1093 # would rather just go back to form ...
1093 # would rather just go back to form ...
1094 raise HTTPFound(
1094 raise HTTPFound(
1095 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1095 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1096
1096
1097 source_repo = _form['source_repo']
1097 source_repo = _form['source_repo']
1098 source_ref = _form['source_ref']
1098 source_ref = _form['source_ref']
1099 target_repo = _form['target_repo']
1099 target_repo = _form['target_repo']
1100 target_ref = _form['target_ref']
1100 target_ref = _form['target_ref']
1101 commit_ids = _form['revisions'][::-1]
1101 commit_ids = _form['revisions'][::-1]
1102 common_ancestor_id = _form['common_ancestor']
1102 common_ancestor_id = _form['common_ancestor']
1103
1103
1104 # find the ancestor for this pr
1104 # find the ancestor for this pr
1105 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1105 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1106 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1106 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1107
1107
1108 if not (source_db_repo or target_db_repo):
1108 if not (source_db_repo or target_db_repo):
1109 h.flash(_('source_repo or target repo not found'), category='error')
1109 h.flash(_('source_repo or target repo not found'), category='error')
1110 raise HTTPFound(
1110 raise HTTPFound(
1111 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1111 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1112
1112
1113 # re-check permissions again here
1113 # re-check permissions again here
1114 # source_repo we must have read permissions
1114 # source_repo we must have read permissions
1115
1115
1116 source_perm = HasRepoPermissionAny(
1116 source_perm = HasRepoPermissionAny(
1117 'repository.read', 'repository.write', 'repository.admin')(
1117 'repository.read', 'repository.write', 'repository.admin')(
1118 source_db_repo.repo_name)
1118 source_db_repo.repo_name)
1119 if not source_perm:
1119 if not source_perm:
1120 msg = _('Not Enough permissions to source repo `{}`.'.format(
1120 msg = _('Not Enough permissions to source repo `{}`.'.format(
1121 source_db_repo.repo_name))
1121 source_db_repo.repo_name))
1122 h.flash(msg, category='error')
1122 h.flash(msg, category='error')
1123 # copy the args back to redirect
1123 # copy the args back to redirect
1124 org_query = self.request.GET.mixed()
1124 org_query = self.request.GET.mixed()
1125 raise HTTPFound(
1125 raise HTTPFound(
1126 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1126 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1127 _query=org_query))
1127 _query=org_query))
1128
1128
1129 # target repo we must have read permissions, and also later on
1129 # target repo we must have read permissions, and also later on
1130 # we want to check branch permissions here
1130 # we want to check branch permissions here
1131 target_perm = HasRepoPermissionAny(
1131 target_perm = HasRepoPermissionAny(
1132 'repository.read', 'repository.write', 'repository.admin')(
1132 'repository.read', 'repository.write', 'repository.admin')(
1133 target_db_repo.repo_name)
1133 target_db_repo.repo_name)
1134 if not target_perm:
1134 if not target_perm:
1135 msg = _('Not Enough permissions to target repo `{}`.'.format(
1135 msg = _('Not Enough permissions to target repo `{}`.'.format(
1136 target_db_repo.repo_name))
1136 target_db_repo.repo_name))
1137 h.flash(msg, category='error')
1137 h.flash(msg, category='error')
1138 # copy the args back to redirect
1138 # copy the args back to redirect
1139 org_query = self.request.GET.mixed()
1139 org_query = self.request.GET.mixed()
1140 raise HTTPFound(
1140 raise HTTPFound(
1141 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1141 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1142 _query=org_query))
1142 _query=org_query))
1143
1143
1144 source_scm = source_db_repo.scm_instance()
1144 source_scm = source_db_repo.scm_instance()
1145 target_scm = target_db_repo.scm_instance()
1145 target_scm = target_db_repo.scm_instance()
1146
1146
1147 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1147 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1148 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1148 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1149
1149
1150 ancestor = source_scm.get_common_ancestor(
1150 ancestor = source_scm.get_common_ancestor(
1151 source_commit.raw_id, target_commit.raw_id, target_scm)
1151 source_commit.raw_id, target_commit.raw_id, target_scm)
1152
1152
1153 # recalculate target ref based on ancestor
1153 # recalculate target ref based on ancestor
1154 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1154 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1155 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1155 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1156
1156
1157 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1157 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1158 PullRequestModel().get_reviewer_functions()
1158 PullRequestModel().get_reviewer_functions()
1159
1159
1160 # recalculate reviewers logic, to make sure we can validate this
1160 # recalculate reviewers logic, to make sure we can validate this
1161 reviewer_rules = get_default_reviewers_data(
1161 reviewer_rules = get_default_reviewers_data(
1162 self._rhodecode_db_user, source_db_repo,
1162 self._rhodecode_db_user, source_db_repo,
1163 source_commit, target_db_repo, target_commit)
1163 source_commit, target_db_repo, target_commit)
1164
1164
1165 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1165 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1166 observers = validate_observers(_form['observer_members'], reviewer_rules)
1166 observers = validate_observers(_form['observer_members'], reviewer_rules)
1167
1167
1168 pullrequest_title = _form['pullrequest_title']
1168 pullrequest_title = _form['pullrequest_title']
1169 title_source_ref = source_ref.split(':', 2)[1]
1169 title_source_ref = source_ref.split(':', 2)[1]
1170 if not pullrequest_title:
1170 if not pullrequest_title:
1171 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1171 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1172 source=source_repo,
1172 source=source_repo,
1173 source_ref=title_source_ref,
1173 source_ref=title_source_ref,
1174 target=target_repo
1174 target=target_repo
1175 )
1175 )
1176
1176
1177 description = _form['pullrequest_desc']
1177 description = _form['pullrequest_desc']
1178 description_renderer = _form['description_renderer']
1178 description_renderer = _form['description_renderer']
1179
1179
1180 try:
1180 try:
1181 pull_request = PullRequestModel().create(
1181 pull_request = PullRequestModel().create(
1182 created_by=self._rhodecode_user.user_id,
1182 created_by=self._rhodecode_user.user_id,
1183 source_repo=source_repo,
1183 source_repo=source_repo,
1184 source_ref=source_ref,
1184 source_ref=source_ref,
1185 target_repo=target_repo,
1185 target_repo=target_repo,
1186 target_ref=target_ref,
1186 target_ref=target_ref,
1187 revisions=commit_ids,
1187 revisions=commit_ids,
1188 common_ancestor_id=common_ancestor_id,
1188 common_ancestor_id=common_ancestor_id,
1189 reviewers=reviewers,
1189 reviewers=reviewers,
1190 observers=observers,
1190 observers=observers,
1191 title=pullrequest_title,
1191 title=pullrequest_title,
1192 description=description,
1192 description=description,
1193 description_renderer=description_renderer,
1193 description_renderer=description_renderer,
1194 reviewer_data=reviewer_rules,
1194 reviewer_data=reviewer_rules,
1195 auth_user=self._rhodecode_user
1195 auth_user=self._rhodecode_user
1196 )
1196 )
1197 Session().commit()
1197 Session().commit()
1198
1198
1199 h.flash(_('Successfully opened new pull request'),
1199 h.flash(_('Successfully opened new pull request'),
1200 category='success')
1200 category='success')
1201 except Exception:
1201 except Exception:
1202 msg = _('Error occurred during creation of this pull request.')
1202 msg = _('Error occurred during creation of this pull request.')
1203 log.exception(msg)
1203 log.exception(msg)
1204 h.flash(msg, category='error')
1204 h.flash(msg, category='error')
1205
1205
1206 # copy the args back to redirect
1206 # copy the args back to redirect
1207 org_query = self.request.GET.mixed()
1207 org_query = self.request.GET.mixed()
1208 raise HTTPFound(
1208 raise HTTPFound(
1209 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1209 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1210 _query=org_query))
1210 _query=org_query))
1211
1211
1212 raise HTTPFound(
1212 raise HTTPFound(
1213 h.route_path('pullrequest_show', repo_name=target_repo,
1213 h.route_path('pullrequest_show', repo_name=target_repo,
1214 pull_request_id=pull_request.pull_request_id))
1214 pull_request_id=pull_request.pull_request_id))
1215
1215
1216 @LoginRequired()
1216 @LoginRequired()
1217 @NotAnonymous()
1217 @NotAnonymous()
1218 @HasRepoPermissionAnyDecorator(
1218 @HasRepoPermissionAnyDecorator(
1219 'repository.read', 'repository.write', 'repository.admin')
1219 'repository.read', 'repository.write', 'repository.admin')
1220 @CSRFRequired()
1220 @CSRFRequired()
1221 @view_config(
1221 @view_config(
1222 route_name='pullrequest_update', request_method='POST',
1222 route_name='pullrequest_update', request_method='POST',
1223 renderer='json_ext')
1223 renderer='json_ext')
1224 def pull_request_update(self):
1224 def pull_request_update(self):
1225 pull_request = PullRequest.get_or_404(
1225 pull_request = PullRequest.get_or_404(
1226 self.request.matchdict['pull_request_id'])
1226 self.request.matchdict['pull_request_id'])
1227 _ = self.request.translate
1227 _ = self.request.translate
1228
1228
1229 c = self.load_default_context()
1229 c = self.load_default_context()
1230 redirect_url = None
1230 redirect_url = None
1231
1231
1232 if pull_request.is_closed():
1232 if pull_request.is_closed():
1233 log.debug('update: forbidden because pull request is closed')
1233 log.debug('update: forbidden because pull request is closed')
1234 msg = _(u'Cannot update closed pull requests.')
1234 msg = _(u'Cannot update closed pull requests.')
1235 h.flash(msg, category='error')
1235 h.flash(msg, category='error')
1236 return {'response': True,
1236 return {'response': True,
1237 'redirect_url': redirect_url}
1237 'redirect_url': redirect_url}
1238
1238
1239 is_state_changing = pull_request.is_state_changing()
1239 is_state_changing = pull_request.is_state_changing()
1240 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1240 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1241
1241
1242 # only owner or admin can update it
1242 # only owner or admin can update it
1243 allowed_to_update = PullRequestModel().check_user_update(
1243 allowed_to_update = PullRequestModel().check_user_update(
1244 pull_request, self._rhodecode_user)
1244 pull_request, self._rhodecode_user)
1245
1245
1246 if allowed_to_update:
1246 if allowed_to_update:
1247 controls = peppercorn.parse(self.request.POST.items())
1247 controls = peppercorn.parse(self.request.POST.items())
1248 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1248 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1249
1249
1250 if 'review_members' in controls:
1250 if 'review_members' in controls:
1251 self._update_reviewers(
1251 self._update_reviewers(
1252 c,
1252 c,
1253 pull_request, controls['review_members'],
1253 pull_request, controls['review_members'],
1254 pull_request.reviewer_data,
1254 pull_request.reviewer_data,
1255 PullRequestReviewers.ROLE_REVIEWER)
1255 PullRequestReviewers.ROLE_REVIEWER)
1256 elif 'observer_members' in controls:
1256 elif 'observer_members' in controls:
1257 self._update_reviewers(
1257 self._update_reviewers(
1258 c,
1258 c,
1259 pull_request, controls['observer_members'],
1259 pull_request, controls['observer_members'],
1260 pull_request.reviewer_data,
1260 pull_request.reviewer_data,
1261 PullRequestReviewers.ROLE_OBSERVER)
1261 PullRequestReviewers.ROLE_OBSERVER)
1262 elif str2bool(self.request.POST.get('update_commits', 'false')):
1262 elif str2bool(self.request.POST.get('update_commits', 'false')):
1263 if is_state_changing:
1263 if is_state_changing:
1264 log.debug('commits update: forbidden because pull request is in state %s',
1264 log.debug('commits update: forbidden because pull request is in state %s',
1265 pull_request.pull_request_state)
1265 pull_request.pull_request_state)
1266 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1266 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1267 u'Current state is: `{}`').format(
1267 u'Current state is: `{}`').format(
1268 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1268 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1269 h.flash(msg, category='error')
1269 h.flash(msg, category='error')
1270 return {'response': True,
1270 return {'response': True,
1271 'redirect_url': redirect_url}
1271 'redirect_url': redirect_url}
1272
1272
1273 self._update_commits(c, pull_request)
1273 self._update_commits(c, pull_request)
1274 if force_refresh:
1274 if force_refresh:
1275 redirect_url = h.route_path(
1275 redirect_url = h.route_path(
1276 'pullrequest_show', repo_name=self.db_repo_name,
1276 'pullrequest_show', repo_name=self.db_repo_name,
1277 pull_request_id=pull_request.pull_request_id,
1277 pull_request_id=pull_request.pull_request_id,
1278 _query={"force_refresh": 1})
1278 _query={"force_refresh": 1})
1279 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1279 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1280 self._edit_pull_request(pull_request)
1280 self._edit_pull_request(pull_request)
1281 else:
1281 else:
1282 log.error('Unhandled update data.')
1282 log.error('Unhandled update data.')
1283 raise HTTPBadRequest()
1283 raise HTTPBadRequest()
1284
1284
1285 return {'response': True,
1285 return {'response': True,
1286 'redirect_url': redirect_url}
1286 'redirect_url': redirect_url}
1287 raise HTTPForbidden()
1287 raise HTTPForbidden()
1288
1288
1289 def _edit_pull_request(self, pull_request):
1289 def _edit_pull_request(self, pull_request):
1290 """
1290 """
1291 Edit title and description
1291 Edit title and description
1292 """
1292 """
1293 _ = self.request.translate
1293 _ = self.request.translate
1294
1294
1295 try:
1295 try:
1296 PullRequestModel().edit(
1296 PullRequestModel().edit(
1297 pull_request,
1297 pull_request,
1298 self.request.POST.get('title'),
1298 self.request.POST.get('title'),
1299 self.request.POST.get('description'),
1299 self.request.POST.get('description'),
1300 self.request.POST.get('description_renderer'),
1300 self.request.POST.get('description_renderer'),
1301 self._rhodecode_user)
1301 self._rhodecode_user)
1302 except ValueError:
1302 except ValueError:
1303 msg = _(u'Cannot update closed pull requests.')
1303 msg = _(u'Cannot update closed pull requests.')
1304 h.flash(msg, category='error')
1304 h.flash(msg, category='error')
1305 return
1305 return
1306 else:
1306 else:
1307 Session().commit()
1307 Session().commit()
1308
1308
1309 msg = _(u'Pull request title & description updated.')
1309 msg = _(u'Pull request title & description updated.')
1310 h.flash(msg, category='success')
1310 h.flash(msg, category='success')
1311 return
1311 return
1312
1312
1313 def _update_commits(self, c, pull_request):
1313 def _update_commits(self, c, pull_request):
1314 _ = self.request.translate
1314 _ = self.request.translate
1315
1315
1316 with pull_request.set_state(PullRequest.STATE_UPDATING):
1316 with pull_request.set_state(PullRequest.STATE_UPDATING):
1317 resp = PullRequestModel().update_commits(
1317 resp = PullRequestModel().update_commits(
1318 pull_request, self._rhodecode_db_user)
1318 pull_request, self._rhodecode_db_user)
1319
1319
1320 if resp.executed:
1320 if resp.executed:
1321
1321
1322 if resp.target_changed and resp.source_changed:
1322 if resp.target_changed and resp.source_changed:
1323 changed = 'target and source repositories'
1323 changed = 'target and source repositories'
1324 elif resp.target_changed and not resp.source_changed:
1324 elif resp.target_changed and not resp.source_changed:
1325 changed = 'target repository'
1325 changed = 'target repository'
1326 elif not resp.target_changed and resp.source_changed:
1326 elif not resp.target_changed and resp.source_changed:
1327 changed = 'source repository'
1327 changed = 'source repository'
1328 else:
1328 else:
1329 changed = 'nothing'
1329 changed = 'nothing'
1330
1330
1331 msg = _(u'Pull request updated to "{source_commit_id}" with '
1331 msg = _(u'Pull request updated to "{source_commit_id}" with '
1332 u'{count_added} added, {count_removed} removed commits. '
1332 u'{count_added} added, {count_removed} removed commits. '
1333 u'Source of changes: {change_source}.')
1333 u'Source of changes: {change_source}.')
1334 msg = msg.format(
1334 msg = msg.format(
1335 source_commit_id=pull_request.source_ref_parts.commit_id,
1335 source_commit_id=pull_request.source_ref_parts.commit_id,
1336 count_added=len(resp.changes.added),
1336 count_added=len(resp.changes.added),
1337 count_removed=len(resp.changes.removed),
1337 count_removed=len(resp.changes.removed),
1338 change_source=changed)
1338 change_source=changed)
1339 h.flash(msg, category='success')
1339 h.flash(msg, category='success')
1340 channelstream.pr_update_channelstream_push(
1340 channelstream.pr_update_channelstream_push(
1341 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1341 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1342 else:
1342 else:
1343 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1343 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1344 warning_reasons = [
1344 warning_reasons = [
1345 UpdateFailureReason.NO_CHANGE,
1345 UpdateFailureReason.NO_CHANGE,
1346 UpdateFailureReason.WRONG_REF_TYPE,
1346 UpdateFailureReason.WRONG_REF_TYPE,
1347 ]
1347 ]
1348 category = 'warning' if resp.reason in warning_reasons else 'error'
1348 category = 'warning' if resp.reason in warning_reasons else 'error'
1349 h.flash(msg, category=category)
1349 h.flash(msg, category=category)
1350
1350
1351 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1351 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1352 _ = self.request.translate
1352 _ = self.request.translate
1353
1353
1354 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1354 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1355 PullRequestModel().get_reviewer_functions()
1355 PullRequestModel().get_reviewer_functions()
1356
1356
1357 if role == PullRequestReviewers.ROLE_REVIEWER:
1357 if role == PullRequestReviewers.ROLE_REVIEWER:
1358 try:
1358 try:
1359 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1359 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1360 except ValueError as e:
1360 except ValueError as e:
1361 log.error('Reviewers Validation: {}'.format(e))
1361 log.error('Reviewers Validation: {}'.format(e))
1362 h.flash(e, category='error')
1362 h.flash(e, category='error')
1363 return
1363 return
1364
1364
1365 old_calculated_status = pull_request.calculated_review_status()
1365 old_calculated_status = pull_request.calculated_review_status()
1366 PullRequestModel().update_reviewers(
1366 PullRequestModel().update_reviewers(
1367 pull_request, reviewers, self._rhodecode_user)
1367 pull_request, reviewers, self._rhodecode_user)
1368
1368
1369 Session().commit()
1369 Session().commit()
1370
1370
1371 msg = _('Pull request reviewers updated.')
1371 msg = _('Pull request reviewers updated.')
1372 h.flash(msg, category='success')
1372 h.flash(msg, category='success')
1373 channelstream.pr_update_channelstream_push(
1373 channelstream.pr_update_channelstream_push(
1374 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1374 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1375
1375
1376 # trigger status changed if change in reviewers changes the status
1376 # trigger status changed if change in reviewers changes the status
1377 calculated_status = pull_request.calculated_review_status()
1377 calculated_status = pull_request.calculated_review_status()
1378 if old_calculated_status != calculated_status:
1378 if old_calculated_status != calculated_status:
1379 PullRequestModel().trigger_pull_request_hook(
1379 PullRequestModel().trigger_pull_request_hook(
1380 pull_request, self._rhodecode_user, 'review_status_change',
1380 pull_request, self._rhodecode_user, 'review_status_change',
1381 data={'status': calculated_status})
1381 data={'status': calculated_status})
1382
1382
1383 elif role == PullRequestReviewers.ROLE_OBSERVER:
1383 elif role == PullRequestReviewers.ROLE_OBSERVER:
1384 try:
1384 try:
1385 observers = validate_observers(review_members, reviewer_rules)
1385 observers = validate_observers(review_members, reviewer_rules)
1386 except ValueError as e:
1386 except ValueError as e:
1387 log.error('Observers Validation: {}'.format(e))
1387 log.error('Observers Validation: {}'.format(e))
1388 h.flash(e, category='error')
1388 h.flash(e, category='error')
1389 return
1389 return
1390
1390
1391 PullRequestModel().update_observers(
1391 PullRequestModel().update_observers(
1392 pull_request, observers, self._rhodecode_user)
1392 pull_request, observers, self._rhodecode_user)
1393
1393
1394 Session().commit()
1394 Session().commit()
1395 msg = _('Pull request observers updated.')
1395 msg = _('Pull request observers updated.')
1396 h.flash(msg, category='success')
1396 h.flash(msg, category='success')
1397 channelstream.pr_update_channelstream_push(
1397 channelstream.pr_update_channelstream_push(
1398 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1398 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1399
1399
1400 @LoginRequired()
1400 @LoginRequired()
1401 @NotAnonymous()
1401 @NotAnonymous()
1402 @HasRepoPermissionAnyDecorator(
1402 @HasRepoPermissionAnyDecorator(
1403 'repository.read', 'repository.write', 'repository.admin')
1403 'repository.read', 'repository.write', 'repository.admin')
1404 @CSRFRequired()
1404 @CSRFRequired()
1405 @view_config(
1405 @view_config(
1406 route_name='pullrequest_merge', request_method='POST',
1406 route_name='pullrequest_merge', request_method='POST',
1407 renderer='json_ext')
1407 renderer='json_ext')
1408 def pull_request_merge(self):
1408 def pull_request_merge(self):
1409 """
1409 """
1410 Merge will perform a server-side merge of the specified
1410 Merge will perform a server-side merge of the specified
1411 pull request, if the pull request is approved and mergeable.
1411 pull request, if the pull request is approved and mergeable.
1412 After successful merging, the pull request is automatically
1412 After successful merging, the pull request is automatically
1413 closed, with a relevant comment.
1413 closed, with a relevant comment.
1414 """
1414 """
1415 pull_request = PullRequest.get_or_404(
1415 pull_request = PullRequest.get_or_404(
1416 self.request.matchdict['pull_request_id'])
1416 self.request.matchdict['pull_request_id'])
1417 _ = self.request.translate
1417 _ = self.request.translate
1418
1418
1419 if pull_request.is_state_changing():
1419 if pull_request.is_state_changing():
1420 log.debug('show: forbidden because pull request is in state %s',
1420 log.debug('show: forbidden because pull request is in state %s',
1421 pull_request.pull_request_state)
1421 pull_request.pull_request_state)
1422 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1422 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1423 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1423 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1424 pull_request.pull_request_state)
1424 pull_request.pull_request_state)
1425 h.flash(msg, category='error')
1425 h.flash(msg, category='error')
1426 raise HTTPFound(
1426 raise HTTPFound(
1427 h.route_path('pullrequest_show',
1427 h.route_path('pullrequest_show',
1428 repo_name=pull_request.target_repo.repo_name,
1428 repo_name=pull_request.target_repo.repo_name,
1429 pull_request_id=pull_request.pull_request_id))
1429 pull_request_id=pull_request.pull_request_id))
1430
1430
1431 self.load_default_context()
1431 self.load_default_context()
1432
1432
1433 with pull_request.set_state(PullRequest.STATE_UPDATING):
1433 with pull_request.set_state(PullRequest.STATE_UPDATING):
1434 check = MergeCheck.validate(
1434 check = MergeCheck.validate(
1435 pull_request, auth_user=self._rhodecode_user,
1435 pull_request, auth_user=self._rhodecode_user,
1436 translator=self.request.translate)
1436 translator=self.request.translate)
1437 merge_possible = not check.failed
1437 merge_possible = not check.failed
1438
1438
1439 for err_type, error_msg in check.errors:
1439 for err_type, error_msg in check.errors:
1440 h.flash(error_msg, category=err_type)
1440 h.flash(error_msg, category=err_type)
1441
1441
1442 if merge_possible:
1442 if merge_possible:
1443 log.debug("Pre-conditions checked, trying to merge.")
1443 log.debug("Pre-conditions checked, trying to merge.")
1444 extras = vcs_operation_context(
1444 extras = vcs_operation_context(
1445 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1445 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1446 username=self._rhodecode_db_user.username, action='push',
1446 username=self._rhodecode_db_user.username, action='push',
1447 scm=pull_request.target_repo.repo_type)
1447 scm=pull_request.target_repo.repo_type)
1448 with pull_request.set_state(PullRequest.STATE_UPDATING):
1448 with pull_request.set_state(PullRequest.STATE_UPDATING):
1449 self._merge_pull_request(
1449 self._merge_pull_request(
1450 pull_request, self._rhodecode_db_user, extras)
1450 pull_request, self._rhodecode_db_user, extras)
1451 else:
1451 else:
1452 log.debug("Pre-conditions failed, NOT merging.")
1452 log.debug("Pre-conditions failed, NOT merging.")
1453
1453
1454 raise HTTPFound(
1454 raise HTTPFound(
1455 h.route_path('pullrequest_show',
1455 h.route_path('pullrequest_show',
1456 repo_name=pull_request.target_repo.repo_name,
1456 repo_name=pull_request.target_repo.repo_name,
1457 pull_request_id=pull_request.pull_request_id))
1457 pull_request_id=pull_request.pull_request_id))
1458
1458
1459 def _merge_pull_request(self, pull_request, user, extras):
1459 def _merge_pull_request(self, pull_request, user, extras):
1460 _ = self.request.translate
1460 _ = self.request.translate
1461 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1461 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1462
1462
1463 if merge_resp.executed:
1463 if merge_resp.executed:
1464 log.debug("The merge was successful, closing the pull request.")
1464 log.debug("The merge was successful, closing the pull request.")
1465 PullRequestModel().close_pull_request(
1465 PullRequestModel().close_pull_request(
1466 pull_request.pull_request_id, user)
1466 pull_request.pull_request_id, user)
1467 Session().commit()
1467 Session().commit()
1468 msg = _('Pull request was successfully merged and closed.')
1468 msg = _('Pull request was successfully merged and closed.')
1469 h.flash(msg, category='success')
1469 h.flash(msg, category='success')
1470 else:
1470 else:
1471 log.debug(
1471 log.debug(
1472 "The merge was not successful. Merge response: %s", merge_resp)
1472 "The merge was not successful. Merge response: %s", merge_resp)
1473 msg = merge_resp.merge_status_message
1473 msg = merge_resp.merge_status_message
1474 h.flash(msg, category='error')
1474 h.flash(msg, category='error')
1475
1475
1476 @LoginRequired()
1476 @LoginRequired()
1477 @NotAnonymous()
1477 @NotAnonymous()
1478 @HasRepoPermissionAnyDecorator(
1478 @HasRepoPermissionAnyDecorator(
1479 'repository.read', 'repository.write', 'repository.admin')
1479 'repository.read', 'repository.write', 'repository.admin')
1480 @CSRFRequired()
1480 @CSRFRequired()
1481 @view_config(
1481 @view_config(
1482 route_name='pullrequest_delete', request_method='POST',
1482 route_name='pullrequest_delete', request_method='POST',
1483 renderer='json_ext')
1483 renderer='json_ext')
1484 def pull_request_delete(self):
1484 def pull_request_delete(self):
1485 _ = self.request.translate
1485 _ = self.request.translate
1486
1486
1487 pull_request = PullRequest.get_or_404(
1487 pull_request = PullRequest.get_or_404(
1488 self.request.matchdict['pull_request_id'])
1488 self.request.matchdict['pull_request_id'])
1489 self.load_default_context()
1489 self.load_default_context()
1490
1490
1491 pr_closed = pull_request.is_closed()
1491 pr_closed = pull_request.is_closed()
1492 allowed_to_delete = PullRequestModel().check_user_delete(
1492 allowed_to_delete = PullRequestModel().check_user_delete(
1493 pull_request, self._rhodecode_user) and not pr_closed
1493 pull_request, self._rhodecode_user) and not pr_closed
1494
1494
1495 # only owner can delete it !
1495 # only owner can delete it !
1496 if allowed_to_delete:
1496 if allowed_to_delete:
1497 PullRequestModel().delete(pull_request, self._rhodecode_user)
1497 PullRequestModel().delete(pull_request, self._rhodecode_user)
1498 Session().commit()
1498 Session().commit()
1499 h.flash(_('Successfully deleted pull request'),
1499 h.flash(_('Successfully deleted pull request'),
1500 category='success')
1500 category='success')
1501 raise HTTPFound(h.route_path('pullrequest_show_all',
1501 raise HTTPFound(h.route_path('pullrequest_show_all',
1502 repo_name=self.db_repo_name))
1502 repo_name=self.db_repo_name))
1503
1503
1504 log.warning('user %s tried to delete pull request without access',
1504 log.warning('user %s tried to delete pull request without access',
1505 self._rhodecode_user)
1505 self._rhodecode_user)
1506 raise HTTPNotFound()
1506 raise HTTPNotFound()
1507
1507
1508 @LoginRequired()
1508 @LoginRequired()
1509 @NotAnonymous()
1509 @NotAnonymous()
1510 @HasRepoPermissionAnyDecorator(
1510 @HasRepoPermissionAnyDecorator(
1511 'repository.read', 'repository.write', 'repository.admin')
1511 'repository.read', 'repository.write', 'repository.admin')
1512 @CSRFRequired()
1512 @CSRFRequired()
1513 @view_config(
1513 @view_config(
1514 route_name='pullrequest_comment_create', request_method='POST',
1514 route_name='pullrequest_comment_create', request_method='POST',
1515 renderer='json_ext')
1515 renderer='json_ext')
1516 def pull_request_comment_create(self):
1516 def pull_request_comment_create(self):
1517 _ = self.request.translate
1517 _ = self.request.translate
1518
1518
1519 pull_request = PullRequest.get_or_404(
1519 pull_request = PullRequest.get_or_404(
1520 self.request.matchdict['pull_request_id'])
1520 self.request.matchdict['pull_request_id'])
1521 pull_request_id = pull_request.pull_request_id
1521 pull_request_id = pull_request.pull_request_id
1522
1522
1523 if pull_request.is_closed():
1523 if pull_request.is_closed():
1524 log.debug('comment: forbidden because pull request is closed')
1524 log.debug('comment: forbidden because pull request is closed')
1525 raise HTTPForbidden()
1525 raise HTTPForbidden()
1526
1526
1527 allowed_to_comment = PullRequestModel().check_user_comment(
1527 allowed_to_comment = PullRequestModel().check_user_comment(
1528 pull_request, self._rhodecode_user)
1528 pull_request, self._rhodecode_user)
1529 if not allowed_to_comment:
1529 if not allowed_to_comment:
1530 log.debug('comment: forbidden because pull request is from forbidden repo')
1530 log.debug('comment: forbidden because pull request is from forbidden repo')
1531 raise HTTPForbidden()
1531 raise HTTPForbidden()
1532
1532
1533 c = self.load_default_context()
1533 c = self.load_default_context()
1534
1534
1535 status = self.request.POST.get('changeset_status', None)
1535 status = self.request.POST.get('changeset_status', None)
1536 text = self.request.POST.get('text')
1536 text = self.request.POST.get('text')
1537 comment_type = self.request.POST.get('comment_type')
1537 comment_type = self.request.POST.get('comment_type')
1538 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1538 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1539 close_pull_request = self.request.POST.get('close_pull_request')
1539 close_pull_request = self.request.POST.get('close_pull_request')
1540
1540
1541 # the logic here should work like following, if we submit close
1541 # the logic here should work like following, if we submit close
1542 # pr comment, use `close_pull_request_with_comment` function
1542 # pr comment, use `close_pull_request_with_comment` function
1543 # else handle regular comment logic
1543 # else handle regular comment logic
1544
1544
1545 if close_pull_request:
1545 if close_pull_request:
1546 # only owner or admin or person with write permissions
1546 # only owner or admin or person with write permissions
1547 allowed_to_close = PullRequestModel().check_user_update(
1547 allowed_to_close = PullRequestModel().check_user_update(
1548 pull_request, self._rhodecode_user)
1548 pull_request, self._rhodecode_user)
1549 if not allowed_to_close:
1549 if not allowed_to_close:
1550 log.debug('comment: forbidden because not allowed to close '
1550 log.debug('comment: forbidden because not allowed to close '
1551 'pull request %s', pull_request_id)
1551 'pull request %s', pull_request_id)
1552 raise HTTPForbidden()
1552 raise HTTPForbidden()
1553
1553
1554 # This also triggers `review_status_change`
1554 # This also triggers `review_status_change`
1555 comment, status = PullRequestModel().close_pull_request_with_comment(
1555 comment, status = PullRequestModel().close_pull_request_with_comment(
1556 pull_request, self._rhodecode_user, self.db_repo, message=text,
1556 pull_request, self._rhodecode_user, self.db_repo, message=text,
1557 auth_user=self._rhodecode_user)
1557 auth_user=self._rhodecode_user)
1558 Session().flush()
1558 Session().flush()
1559
1559
1560 PullRequestModel().trigger_pull_request_hook(
1560 PullRequestModel().trigger_pull_request_hook(
1561 pull_request, self._rhodecode_user, 'comment',
1561 pull_request, self._rhodecode_user, 'comment',
1562 data={'comment': comment})
1562 data={'comment': comment})
1563
1563
1564 else:
1564 else:
1565 # regular comment case, could be inline, or one with status.
1565 # regular comment case, could be inline, or one with status.
1566 # for that one we check also permissions
1566 # for that one we check also permissions
1567
1567
1568 allowed_to_change_status = PullRequestModel().check_user_change_status(
1568 allowed_to_change_status = PullRequestModel().check_user_change_status(
1569 pull_request, self._rhodecode_user)
1569 pull_request, self._rhodecode_user)
1570
1570
1571 if status and allowed_to_change_status:
1571 if status and allowed_to_change_status:
1572 message = (_('Status change %(transition_icon)s %(status)s')
1572 message = (_('Status change %(transition_icon)s %(status)s')
1573 % {'transition_icon': '>',
1573 % {'transition_icon': '>',
1574 'status': ChangesetStatus.get_status_lbl(status)})
1574 'status': ChangesetStatus.get_status_lbl(status)})
1575 text = text or message
1575 text = text or message
1576
1576
1577 comment = CommentsModel().create(
1577 comment = CommentsModel().create(
1578 text=text,
1578 text=text,
1579 repo=self.db_repo.repo_id,
1579 repo=self.db_repo.repo_id,
1580 user=self._rhodecode_user.user_id,
1580 user=self._rhodecode_user.user_id,
1581 pull_request=pull_request,
1581 pull_request=pull_request,
1582 f_path=self.request.POST.get('f_path'),
1582 f_path=self.request.POST.get('f_path'),
1583 line_no=self.request.POST.get('line'),
1583 line_no=self.request.POST.get('line'),
1584 status_change=(ChangesetStatus.get_status_lbl(status)
1584 status_change=(ChangesetStatus.get_status_lbl(status)
1585 if status and allowed_to_change_status else None),
1585 if status and allowed_to_change_status else None),
1586 status_change_type=(status
1586 status_change_type=(status
1587 if status and allowed_to_change_status else None),
1587 if status and allowed_to_change_status else None),
1588 comment_type=comment_type,
1588 comment_type=comment_type,
1589 resolves_comment_id=resolves_comment_id,
1589 resolves_comment_id=resolves_comment_id,
1590 auth_user=self._rhodecode_user
1590 auth_user=self._rhodecode_user
1591 )
1591 )
1592 is_inline = bool(comment.f_path and comment.line_no)
1592 is_inline = bool(comment.f_path and comment.line_no)
1593
1593
1594 if allowed_to_change_status:
1594 if allowed_to_change_status:
1595 # calculate old status before we change it
1595 # calculate old status before we change it
1596 old_calculated_status = pull_request.calculated_review_status()
1596 old_calculated_status = pull_request.calculated_review_status()
1597
1597
1598 # get status if set !
1598 # get status if set !
1599 if status:
1599 if status:
1600 ChangesetStatusModel().set_status(
1600 ChangesetStatusModel().set_status(
1601 self.db_repo.repo_id,
1601 self.db_repo.repo_id,
1602 status,
1602 status,
1603 self._rhodecode_user.user_id,
1603 self._rhodecode_user.user_id,
1604 comment,
1604 comment,
1605 pull_request=pull_request
1605 pull_request=pull_request
1606 )
1606 )
1607
1607
1608 Session().flush()
1608 Session().flush()
1609 # this is somehow required to get access to some relationship
1609 # this is somehow required to get access to some relationship
1610 # loaded on comment
1610 # loaded on comment
1611 Session().refresh(comment)
1611 Session().refresh(comment)
1612
1612
1613 PullRequestModel().trigger_pull_request_hook(
1613 PullRequestModel().trigger_pull_request_hook(
1614 pull_request, self._rhodecode_user, 'comment',
1614 pull_request, self._rhodecode_user, 'comment',
1615 data={'comment': comment})
1615 data={'comment': comment})
1616
1616
1617 # we now calculate the status of pull request, and based on that
1617 # we now calculate the status of pull request, and based on that
1618 # calculation we set the commits status
1618 # calculation we set the commits status
1619 calculated_status = pull_request.calculated_review_status()
1619 calculated_status = pull_request.calculated_review_status()
1620 if old_calculated_status != calculated_status:
1620 if old_calculated_status != calculated_status:
1621 PullRequestModel().trigger_pull_request_hook(
1621 PullRequestModel().trigger_pull_request_hook(
1622 pull_request, self._rhodecode_user, 'review_status_change',
1622 pull_request, self._rhodecode_user, 'review_status_change',
1623 data={'status': calculated_status})
1623 data={'status': calculated_status})
1624
1624
1625 Session().commit()
1625 Session().commit()
1626
1626
1627 data = {
1627 data = {
1628 'target_id': h.safeid(h.safe_unicode(
1628 'target_id': h.safeid(h.safe_unicode(
1629 self.request.POST.get('f_path'))),
1629 self.request.POST.get('f_path'))),
1630 }
1630 }
1631 if comment:
1631 if comment:
1632 c.co = comment
1632 c.co = comment
1633 c.at_version_num = None
1633 c.at_version_num = None
1634 rendered_comment = render(
1634 rendered_comment = render(
1635 'rhodecode:templates/changeset/changeset_comment_block.mako',
1635 'rhodecode:templates/changeset/changeset_comment_block.mako',
1636 self._get_template_context(c), self.request)
1636 self._get_template_context(c), self.request)
1637
1637
1638 data.update(comment.get_dict())
1638 data.update(comment.get_dict())
1639 data.update({'rendered_text': rendered_comment})
1639 data.update({'rendered_text': rendered_comment})
1640
1640
1641 comment_broadcast_channel = channelstream.comment_channel(
1641 comment_broadcast_channel = channelstream.comment_channel(
1642 self.db_repo_name, pull_request_obj=pull_request)
1642 self.db_repo_name, pull_request_obj=pull_request)
1643
1643
1644 comment_data = data
1644 comment_data = data
1645 comment_type = 'inline' if is_inline else 'general'
1645 comment_type = 'inline' if is_inline else 'general'
1646 channelstream.comment_channelstream_push(
1646 channelstream.comment_channelstream_push(
1647 self.request, comment_broadcast_channel, self._rhodecode_user,
1647 self.request, comment_broadcast_channel, self._rhodecode_user,
1648 _('posted a new {} comment').format(comment_type),
1648 _('posted a new {} comment').format(comment_type),
1649 comment_data=comment_data)
1649 comment_data=comment_data)
1650
1650
1651 return data
1651 return data
1652
1652
1653 @LoginRequired()
1653 @LoginRequired()
1654 @NotAnonymous()
1654 @NotAnonymous()
1655 @HasRepoPermissionAnyDecorator(
1655 @HasRepoPermissionAnyDecorator(
1656 'repository.read', 'repository.write', 'repository.admin')
1656 'repository.read', 'repository.write', 'repository.admin')
1657 @CSRFRequired()
1657 @CSRFRequired()
1658 @view_config(
1658 @view_config(
1659 route_name='pullrequest_comment_delete', request_method='POST',
1659 route_name='pullrequest_comment_delete', request_method='POST',
1660 renderer='json_ext')
1660 renderer='json_ext')
1661 def pull_request_comment_delete(self):
1661 def pull_request_comment_delete(self):
1662 pull_request = PullRequest.get_or_404(
1662 pull_request = PullRequest.get_or_404(
1663 self.request.matchdict['pull_request_id'])
1663 self.request.matchdict['pull_request_id'])
1664
1664
1665 comment = ChangesetComment.get_or_404(
1665 comment = ChangesetComment.get_or_404(
1666 self.request.matchdict['comment_id'])
1666 self.request.matchdict['comment_id'])
1667 comment_id = comment.comment_id
1667 comment_id = comment.comment_id
1668
1668
1669 if comment.immutable:
1669 if comment.immutable:
1670 # don't allow deleting comments that are immutable
1670 # don't allow deleting comments that are immutable
1671 raise HTTPForbidden()
1671 raise HTTPForbidden()
1672
1672
1673 if pull_request.is_closed():
1673 if pull_request.is_closed():
1674 log.debug('comment: forbidden because pull request is closed')
1674 log.debug('comment: forbidden because pull request is closed')
1675 raise HTTPForbidden()
1675 raise HTTPForbidden()
1676
1676
1677 if not comment:
1677 if not comment:
1678 log.debug('Comment with id:%s not found, skipping', comment_id)
1678 log.debug('Comment with id:%s not found, skipping', comment_id)
1679 # comment already deleted in another call probably
1679 # comment already deleted in another call probably
1680 return True
1680 return True
1681
1681
1682 if comment.pull_request.is_closed():
1682 if comment.pull_request.is_closed():
1683 # don't allow deleting comments on closed pull request
1683 # don't allow deleting comments on closed pull request
1684 raise HTTPForbidden()
1684 raise HTTPForbidden()
1685
1685
1686 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1686 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1687 super_admin = h.HasPermissionAny('hg.admin')()
1687 super_admin = h.HasPermissionAny('hg.admin')()
1688 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1688 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1689 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1689 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1690 comment_repo_admin = is_repo_admin and is_repo_comment
1690 comment_repo_admin = is_repo_admin and is_repo_comment
1691
1691
1692 if super_admin or comment_owner or comment_repo_admin:
1692 if super_admin or comment_owner or comment_repo_admin:
1693 old_calculated_status = comment.pull_request.calculated_review_status()
1693 old_calculated_status = comment.pull_request.calculated_review_status()
1694 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1694 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1695 Session().commit()
1695 Session().commit()
1696 calculated_status = comment.pull_request.calculated_review_status()
1696 calculated_status = comment.pull_request.calculated_review_status()
1697 if old_calculated_status != calculated_status:
1697 if old_calculated_status != calculated_status:
1698 PullRequestModel().trigger_pull_request_hook(
1698 PullRequestModel().trigger_pull_request_hook(
1699 comment.pull_request, self._rhodecode_user, 'review_status_change',
1699 comment.pull_request, self._rhodecode_user, 'review_status_change',
1700 data={'status': calculated_status})
1700 data={'status': calculated_status})
1701 return True
1701 return True
1702 else:
1702 else:
1703 log.warning('No permissions for user %s to delete comment_id: %s',
1703 log.warning('No permissions for user %s to delete comment_id: %s',
1704 self._rhodecode_db_user, comment_id)
1704 self._rhodecode_db_user, comment_id)
1705 raise HTTPNotFound()
1705 raise HTTPNotFound()
1706
1706
1707 @LoginRequired()
1707 @LoginRequired()
1708 @NotAnonymous()
1708 @NotAnonymous()
1709 @HasRepoPermissionAnyDecorator(
1709 @HasRepoPermissionAnyDecorator(
1710 'repository.read', 'repository.write', 'repository.admin')
1710 'repository.read', 'repository.write', 'repository.admin')
1711 @CSRFRequired()
1711 @CSRFRequired()
1712 @view_config(
1712 @view_config(
1713 route_name='pullrequest_comment_edit', request_method='POST',
1713 route_name='pullrequest_comment_edit', request_method='POST',
1714 renderer='json_ext')
1714 renderer='json_ext')
1715 def pull_request_comment_edit(self):
1715 def pull_request_comment_edit(self):
1716 self.load_default_context()
1716 self.load_default_context()
1717
1717
1718 pull_request = PullRequest.get_or_404(
1718 pull_request = PullRequest.get_or_404(
1719 self.request.matchdict['pull_request_id']
1719 self.request.matchdict['pull_request_id']
1720 )
1720 )
1721 comment = ChangesetComment.get_or_404(
1721 comment = ChangesetComment.get_or_404(
1722 self.request.matchdict['comment_id']
1722 self.request.matchdict['comment_id']
1723 )
1723 )
1724 comment_id = comment.comment_id
1724 comment_id = comment.comment_id
1725
1725
1726 if comment.immutable:
1726 if comment.immutable:
1727 # don't allow deleting comments that are immutable
1727 # don't allow deleting comments that are immutable
1728 raise HTTPForbidden()
1728 raise HTTPForbidden()
1729
1729
1730 if pull_request.is_closed():
1730 if pull_request.is_closed():
1731 log.debug('comment: forbidden because pull request is closed')
1731 log.debug('comment: forbidden because pull request is closed')
1732 raise HTTPForbidden()
1732 raise HTTPForbidden()
1733
1733
1734 if not comment:
1734 if not comment:
1735 log.debug('Comment with id:%s not found, skipping', comment_id)
1735 log.debug('Comment with id:%s not found, skipping', comment_id)
1736 # comment already deleted in another call probably
1736 # comment already deleted in another call probably
1737 return True
1737 return True
1738
1738
1739 if comment.pull_request.is_closed():
1739 if comment.pull_request.is_closed():
1740 # don't allow deleting comments on closed pull request
1740 # don't allow deleting comments on closed pull request
1741 raise HTTPForbidden()
1741 raise HTTPForbidden()
1742
1742
1743 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1743 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1744 super_admin = h.HasPermissionAny('hg.admin')()
1744 super_admin = h.HasPermissionAny('hg.admin')()
1745 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1745 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1746 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1746 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1747 comment_repo_admin = is_repo_admin and is_repo_comment
1747 comment_repo_admin = is_repo_admin and is_repo_comment
1748
1748
1749 if super_admin or comment_owner or comment_repo_admin:
1749 if super_admin or comment_owner or comment_repo_admin:
1750 text = self.request.POST.get('text')
1750 text = self.request.POST.get('text')
1751 version = self.request.POST.get('version')
1751 version = self.request.POST.get('version')
1752 if text == comment.text:
1752 if text == comment.text:
1753 log.warning(
1753 log.warning(
1754 'Comment(PR): '
1754 'Comment(PR): '
1755 'Trying to create new version '
1755 'Trying to create new version '
1756 'with the same comment body {}'.format(
1756 'with the same comment body {}'.format(
1757 comment_id,
1757 comment_id,
1758 )
1758 )
1759 )
1759 )
1760 raise HTTPNotFound()
1760 raise HTTPNotFound()
1761
1761
1762 if version.isdigit():
1762 if version.isdigit():
1763 version = int(version)
1763 version = int(version)
1764 else:
1764 else:
1765 log.warning(
1765 log.warning(
1766 'Comment(PR): Wrong version type {} {} '
1766 'Comment(PR): Wrong version type {} {} '
1767 'for comment {}'.format(
1767 'for comment {}'.format(
1768 version,
1768 version,
1769 type(version),
1769 type(version),
1770 comment_id,
1770 comment_id,
1771 )
1771 )
1772 )
1772 )
1773 raise HTTPNotFound()
1773 raise HTTPNotFound()
1774
1774
1775 try:
1775 try:
1776 comment_history = CommentsModel().edit(
1776 comment_history = CommentsModel().edit(
1777 comment_id=comment_id,
1777 comment_id=comment_id,
1778 text=text,
1778 text=text,
1779 auth_user=self._rhodecode_user,
1779 auth_user=self._rhodecode_user,
1780 version=version,
1780 version=version,
1781 )
1781 )
1782 except CommentVersionMismatch:
1782 except CommentVersionMismatch:
1783 raise HTTPConflict()
1783 raise HTTPConflict()
1784
1784
1785 if not comment_history:
1785 if not comment_history:
1786 raise HTTPNotFound()
1786 raise HTTPNotFound()
1787
1787
1788 Session().commit()
1788 Session().commit()
1789
1789
1790 PullRequestModel().trigger_pull_request_hook(
1790 PullRequestModel().trigger_pull_request_hook(
1791 pull_request, self._rhodecode_user, 'comment_edit',
1791 pull_request, self._rhodecode_user, 'comment_edit',
1792 data={'comment': comment})
1792 data={'comment': comment})
1793
1793
1794 return {
1794 return {
1795 'comment_history_id': comment_history.comment_history_id,
1795 'comment_history_id': comment_history.comment_history_id,
1796 'comment_id': comment.comment_id,
1796 'comment_id': comment.comment_id,
1797 'comment_version': comment_history.version,
1797 'comment_version': comment_history.version,
1798 'comment_author_username': comment_history.author.username,
1798 'comment_author_username': comment_history.author.username,
1799 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1799 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1800 'comment_created_on': h.age_component(comment_history.created_on,
1800 'comment_created_on': h.age_component(comment_history.created_on,
1801 time_is_local=True),
1801 time_is_local=True),
1802 }
1802 }
1803 else:
1803 else:
1804 log.warning('No permissions for user %s to edit comment_id: %s',
1804 log.warning('No permissions for user %s to edit comment_id: %s',
1805 self._rhodecode_db_user, comment_id)
1805 self._rhodecode_db_user, comment_id)
1806 raise HTTPNotFound()
1806 raise HTTPNotFound()
@@ -1,818 +1,821 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.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 ChangesetComment,
40 ChangesetComment,
41 User,
41 User,
42 Notification,
42 Notification,
43 PullRequest,
43 PullRequest,
44 AttributeDict,
44 AttributeDict,
45 ChangesetCommentHistory,
45 ChangesetCommentHistory,
46 )
46 )
47 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.settings import VcsSettingsModel
49 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.notification import EmailNotificationModel
50 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.validation_schema.schemas import comment_schema
51 from rhodecode.model.validation_schema.schemas import comment_schema
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class CommentsModel(BaseModel):
57 class CommentsModel(BaseModel):
58
58
59 cls = ChangesetComment
59 cls = ChangesetComment
60
60
61 DIFF_CONTEXT_BEFORE = 3
61 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_AFTER = 3
62 DIFF_CONTEXT_AFTER = 3
63
63
64 def __get_commit_comment(self, changeset_comment):
64 def __get_commit_comment(self, changeset_comment):
65 return self._get_instance(ChangesetComment, changeset_comment)
65 return self._get_instance(ChangesetComment, changeset_comment)
66
66
67 def __get_pull_request(self, pull_request):
67 def __get_pull_request(self, pull_request):
68 return self._get_instance(PullRequest, pull_request)
68 return self._get_instance(PullRequest, pull_request)
69
69
70 def _extract_mentions(self, s):
70 def _extract_mentions(self, s):
71 user_objects = []
71 user_objects = []
72 for username in extract_mentioned_users(s):
72 for username in extract_mentioned_users(s):
73 user_obj = User.get_by_username(username, case_insensitive=True)
73 user_obj = User.get_by_username(username, case_insensitive=True)
74 if user_obj:
74 if user_obj:
75 user_objects.append(user_obj)
75 user_objects.append(user_obj)
76 return user_objects
76 return user_objects
77
77
78 def _get_renderer(self, global_renderer='rst', request=None):
78 def _get_renderer(self, global_renderer='rst', request=None):
79 request = request or get_current_request()
79 request = request or get_current_request()
80
80
81 try:
81 try:
82 global_renderer = request.call_context.visual.default_renderer
82 global_renderer = request.call_context.visual.default_renderer
83 except AttributeError:
83 except AttributeError:
84 log.debug("Renderer not set, falling back "
84 log.debug("Renderer not set, falling back "
85 "to default renderer '%s'", global_renderer)
85 "to default renderer '%s'", global_renderer)
86 except Exception:
86 except Exception:
87 log.error(traceback.format_exc())
87 log.error(traceback.format_exc())
88 return global_renderer
88 return global_renderer
89
89
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 # group by versions, and count until, and display objects
91 # group by versions, and count until, and display objects
92
92
93 comment_groups = collections.defaultdict(list)
93 comment_groups = collections.defaultdict(list)
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95
95
96 def yield_comments(pos):
96 def yield_comments(pos):
97 for co in comment_groups[pos]:
97 for co in comment_groups[pos]:
98 yield co
98 yield co
99
99
100 comment_versions = collections.defaultdict(
100 comment_versions = collections.defaultdict(
101 lambda: collections.defaultdict(list))
101 lambda: collections.defaultdict(list))
102 prev_prvid = -1
102 prev_prvid = -1
103 # fake last entry with None, to aggregate on "latest" version which
103 # fake last entry with None, to aggregate on "latest" version which
104 # doesn't have an pull_request_version_id
104 # doesn't have an pull_request_version_id
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 prvid = ver.pull_request_version_id
106 prvid = ver.pull_request_version_id
107 if prev_prvid == -1:
107 if prev_prvid == -1:
108 prev_prvid = prvid
108 prev_prvid = prvid
109
109
110 for co in yield_comments(prvid):
110 for co in yield_comments(prvid):
111 comment_versions[prvid]['at'].append(co)
111 comment_versions[prvid]['at'].append(co)
112
112
113 # save until
113 # save until
114 current = comment_versions[prvid]['at']
114 current = comment_versions[prvid]['at']
115 prev_until = comment_versions[prev_prvid]['until']
115 prev_until = comment_versions[prev_prvid]['until']
116 cur_until = prev_until + current
116 cur_until = prev_until + current
117 comment_versions[prvid]['until'].extend(cur_until)
117 comment_versions[prvid]['until'].extend(cur_until)
118
118
119 # save outdated
119 # save outdated
120 if inline:
120 if inline:
121 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
122 if x.outdated_at_version(show_version)]
122 if x.outdated_at_version(show_version)]
123 else:
123 else:
124 outdated = [x for x in cur_until
124 outdated = [x for x in cur_until
125 if x.older_than_version(show_version)]
125 if x.older_than_version(show_version)]
126 display = [x for x in cur_until if x not in outdated]
126 display = [x for x in cur_until if x not in outdated]
127
127
128 comment_versions[prvid]['outdated'] = outdated
128 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['display'] = display
129 comment_versions[prvid]['display'] = display
130
130
131 prev_prvid = prvid
131 prev_prvid = prvid
132
132
133 return comment_versions
133 return comment_versions
134
134
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 qry = Session().query(ChangesetComment) \
136 qry = Session().query(ChangesetComment) \
137 .filter(ChangesetComment.repo == repo)
137 .filter(ChangesetComment.repo == repo)
138
138
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141
141
142 if user:
142 if user:
143 user = self._get_user(user)
143 user = self._get_user(user)
144 if user:
144 if user:
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146
146
147 if commit_id:
147 if commit_id:
148 qry = qry.filter(ChangesetComment.revision == commit_id)
148 qry = qry.filter(ChangesetComment.revision == commit_id)
149
149
150 qry = qry.order_by(ChangesetComment.created_on)
150 qry = qry.order_by(ChangesetComment.created_on)
151 return qry.all()
151 return qry.all()
152
152
153 def get_repository_unresolved_todos(self, repo):
153 def get_repository_unresolved_todos(self, repo):
154 todos = Session().query(ChangesetComment) \
154 todos = Session().query(ChangesetComment) \
155 .filter(ChangesetComment.repo == repo) \
155 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.resolved_by == None) \
156 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.comment_type
157 .filter(ChangesetComment.comment_type
158 == ChangesetComment.COMMENT_TYPE_TODO)
158 == ChangesetComment.COMMENT_TYPE_TODO)
159 todos = todos.all()
159 todos = todos.all()
160
160
161 return todos
161 return todos
162
162
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
164
164
165 todos = Session().query(ChangesetComment) \
165 todos = Session().query(ChangesetComment) \
166 .filter(ChangesetComment.pull_request == pull_request) \
166 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.resolved_by == None) \
167 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.comment_type
168 .filter(ChangesetComment.comment_type
169 == ChangesetComment.COMMENT_TYPE_TODO)
169 == ChangesetComment.COMMENT_TYPE_TODO)
170
170
171 if not show_outdated:
171 if not show_outdated:
172 todos = todos.filter(
172 todos = todos.filter(
173 coalesce(ChangesetComment.display_state, '') !=
173 coalesce(ChangesetComment.display_state, '') !=
174 ChangesetComment.COMMENT_OUTDATED)
174 ChangesetComment.COMMENT_OUTDATED)
175
175
176 todos = todos.all()
176 todos = todos.all()
177
177
178 return todos
178 return todos
179
179
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
181
181
182 todos = Session().query(ChangesetComment) \
182 todos = Session().query(ChangesetComment) \
183 .filter(ChangesetComment.pull_request == pull_request) \
183 .filter(ChangesetComment.pull_request == pull_request) \
184 .filter(ChangesetComment.resolved_by != None) \
184 .filter(ChangesetComment.resolved_by != None) \
185 .filter(ChangesetComment.comment_type
185 .filter(ChangesetComment.comment_type
186 == ChangesetComment.COMMENT_TYPE_TODO)
186 == ChangesetComment.COMMENT_TYPE_TODO)
187
187
188 if not show_outdated:
188 if not show_outdated:
189 todos = todos.filter(
189 todos = todos.filter(
190 coalesce(ChangesetComment.display_state, '') !=
190 coalesce(ChangesetComment.display_state, '') !=
191 ChangesetComment.COMMENT_OUTDATED)
191 ChangesetComment.COMMENT_OUTDATED)
192
192
193 todos = todos.all()
193 todos = todos.all()
194
194
195 return todos
195 return todos
196
196
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
198
198
199 todos = Session().query(ChangesetComment) \
199 todos = Session().query(ChangesetComment) \
200 .filter(ChangesetComment.revision == commit_id) \
200 .filter(ChangesetComment.revision == commit_id) \
201 .filter(ChangesetComment.resolved_by == None) \
201 .filter(ChangesetComment.resolved_by == None) \
202 .filter(ChangesetComment.comment_type
202 .filter(ChangesetComment.comment_type
203 == ChangesetComment.COMMENT_TYPE_TODO)
203 == ChangesetComment.COMMENT_TYPE_TODO)
204
204
205 if not show_outdated:
205 if not show_outdated:
206 todos = todos.filter(
206 todos = todos.filter(
207 coalesce(ChangesetComment.display_state, '') !=
207 coalesce(ChangesetComment.display_state, '') !=
208 ChangesetComment.COMMENT_OUTDATED)
208 ChangesetComment.COMMENT_OUTDATED)
209
209
210 todos = todos.all()
210 todos = todos.all()
211
211
212 return todos
212 return todos
213
213
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
215
215
216 todos = Session().query(ChangesetComment) \
216 todos = Session().query(ChangesetComment) \
217 .filter(ChangesetComment.revision == commit_id) \
217 .filter(ChangesetComment.revision == commit_id) \
218 .filter(ChangesetComment.resolved_by != None) \
218 .filter(ChangesetComment.resolved_by != None) \
219 .filter(ChangesetComment.comment_type
219 .filter(ChangesetComment.comment_type
220 == ChangesetComment.COMMENT_TYPE_TODO)
220 == ChangesetComment.COMMENT_TYPE_TODO)
221
221
222 if not show_outdated:
222 if not show_outdated:
223 todos = todos.filter(
223 todos = todos.filter(
224 coalesce(ChangesetComment.display_state, '') !=
224 coalesce(ChangesetComment.display_state, '') !=
225 ChangesetComment.COMMENT_OUTDATED)
225 ChangesetComment.COMMENT_OUTDATED)
226
226
227 todos = todos.all()
227 todos = todos.all()
228
228
229 return todos
229 return todos
230
230
231 def get_commit_inline_comments(self, commit_id):
231 def get_commit_inline_comments(self, commit_id):
232 inline_comments = Session().query(ChangesetComment) \
232 inline_comments = Session().query(ChangesetComment) \
233 .filter(ChangesetComment.line_no != None) \
233 .filter(ChangesetComment.line_no != None) \
234 .filter(ChangesetComment.f_path != None) \
234 .filter(ChangesetComment.f_path != None) \
235 .filter(ChangesetComment.revision == commit_id)
235 .filter(ChangesetComment.revision == commit_id)
236 inline_comments = inline_comments.all()
236 inline_comments = inline_comments.all()
237 return inline_comments
237 return inline_comments
238
238
239 def _log_audit_action(self, action, action_data, auth_user, comment):
239 def _log_audit_action(self, action, action_data, auth_user, comment):
240 audit_logger.store(
240 audit_logger.store(
241 action=action,
241 action=action,
242 action_data=action_data,
242 action_data=action_data,
243 user=auth_user,
243 user=auth_user,
244 repo=comment.repo)
244 repo=comment.repo)
245
245
246 def create(self, text, repo, user, commit_id=None, pull_request=None,
246 def create(self, text, repo, user, commit_id=None, pull_request=None,
247 f_path=None, line_no=None, status_change=None,
247 f_path=None, line_no=None, status_change=None,
248 status_change_type=None, comment_type=None,
248 status_change_type=None, comment_type=None,
249 resolves_comment_id=None, closing_pr=False, send_email=True,
249 resolves_comment_id=None, closing_pr=False, send_email=True,
250 renderer=None, auth_user=None, extra_recipients=None):
250 renderer=None, auth_user=None, extra_recipients=None):
251 """
251 """
252 Creates new comment for commit or pull request.
252 Creates new comment for commit or pull request.
253 IF status_change is not none this comment is associated with a
253 IF status_change is not none this comment is associated with a
254 status change of commit or commit associated with pull request
254 status change of commit or commit associated with pull request
255
255
256 :param text:
256 :param text:
257 :param repo:
257 :param repo:
258 :param user:
258 :param user:
259 :param commit_id:
259 :param commit_id:
260 :param pull_request:
260 :param pull_request:
261 :param f_path:
261 :param f_path:
262 :param line_no:
262 :param line_no:
263 :param status_change: Label for status change
263 :param status_change: Label for status change
264 :param comment_type: Type of comment
264 :param comment_type: Type of comment
265 :param resolves_comment_id: id of comment which this one will resolve
265 :param resolves_comment_id: id of comment which this one will resolve
266 :param status_change_type: type of status change
266 :param status_change_type: type of status change
267 :param closing_pr:
267 :param closing_pr:
268 :param send_email:
268 :param send_email:
269 :param renderer: pick renderer for this comment
269 :param renderer: pick renderer for this comment
270 :param auth_user: current authenticated user calling this method
270 :param auth_user: current authenticated user calling this method
271 :param extra_recipients: list of extra users to be added to recipients
271 :param extra_recipients: list of extra users to be added to recipients
272 """
272 """
273
273
274 if not text:
274 if not text:
275 log.warning('Missing text for comment, skipping...')
275 log.warning('Missing text for comment, skipping...')
276 return
276 return
277 request = get_current_request()
277 request = get_current_request()
278 _ = request.translate
278 _ = request.translate
279
279
280 if not renderer:
280 if not renderer:
281 renderer = self._get_renderer(request=request)
281 renderer = self._get_renderer(request=request)
282
282
283 repo = self._get_repo(repo)
283 repo = self._get_repo(repo)
284 user = self._get_user(user)
284 user = self._get_user(user)
285 auth_user = auth_user or user
285 auth_user = auth_user or user
286
286
287 schema = comment_schema.CommentSchema()
287 schema = comment_schema.CommentSchema()
288 validated_kwargs = schema.deserialize(dict(
288 validated_kwargs = schema.deserialize(dict(
289 comment_body=text,
289 comment_body=text,
290 comment_type=comment_type,
290 comment_type=comment_type,
291 comment_file=f_path,
291 comment_file=f_path,
292 comment_line=line_no,
292 comment_line=line_no,
293 renderer_type=renderer,
293 renderer_type=renderer,
294 status_change=status_change_type,
294 status_change=status_change_type,
295 resolves_comment_id=resolves_comment_id,
295 resolves_comment_id=resolves_comment_id,
296 repo=repo.repo_id,
296 repo=repo.repo_id,
297 user=user.user_id,
297 user=user.user_id,
298 ))
298 ))
299
299
300 comment = ChangesetComment()
300 comment = ChangesetComment()
301 comment.renderer = validated_kwargs['renderer_type']
301 comment.renderer = validated_kwargs['renderer_type']
302 comment.text = validated_kwargs['comment_body']
302 comment.text = validated_kwargs['comment_body']
303 comment.f_path = validated_kwargs['comment_file']
303 comment.f_path = validated_kwargs['comment_file']
304 comment.line_no = validated_kwargs['comment_line']
304 comment.line_no = validated_kwargs['comment_line']
305 comment.comment_type = validated_kwargs['comment_type']
305 comment.comment_type = validated_kwargs['comment_type']
306
306
307 comment.repo = repo
307 comment.repo = repo
308 comment.author = user
308 comment.author = user
309 resolved_comment = self.__get_commit_comment(
309 resolved_comment = self.__get_commit_comment(
310 validated_kwargs['resolves_comment_id'])
310 validated_kwargs['resolves_comment_id'])
311 # check if the comment actually belongs to this PR
311 # check if the comment actually belongs to this PR
312 if resolved_comment and resolved_comment.pull_request and \
312 if resolved_comment and resolved_comment.pull_request and \
313 resolved_comment.pull_request != pull_request:
313 resolved_comment.pull_request != pull_request:
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
315 resolved_comment)
315 resolved_comment)
316 # comment not bound to this pull request, forbid
316 # comment not bound to this pull request, forbid
317 resolved_comment = None
317 resolved_comment = None
318
318
319 elif resolved_comment and resolved_comment.repo and \
319 elif resolved_comment and resolved_comment.repo and \
320 resolved_comment.repo != repo:
320 resolved_comment.repo != repo:
321 log.warning('Comment tried to resolved unrelated todo comment: %s',
321 log.warning('Comment tried to resolved unrelated todo comment: %s',
322 resolved_comment)
322 resolved_comment)
323 # comment not bound to this repo, forbid
323 # comment not bound to this repo, forbid
324 resolved_comment = None
324 resolved_comment = None
325
325
326 comment.resolved_comment = resolved_comment
326 comment.resolved_comment = resolved_comment
327
327
328 pull_request_id = pull_request
328 pull_request_id = pull_request
329
329
330 commit_obj = None
330 commit_obj = None
331 pull_request_obj = None
331 pull_request_obj = None
332
332
333 if commit_id:
333 if commit_id:
334 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
334 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
335 # do a lookup, so we don't pass something bad here
335 # do a lookup, so we don't pass something bad here
336 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
336 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
337 comment.revision = commit_obj.raw_id
337 comment.revision = commit_obj.raw_id
338
338
339 elif pull_request_id:
339 elif pull_request_id:
340 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
340 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
341 pull_request_obj = self.__get_pull_request(pull_request_id)
341 pull_request_obj = self.__get_pull_request(pull_request_id)
342 comment.pull_request = pull_request_obj
342 comment.pull_request = pull_request_obj
343 else:
343 else:
344 raise Exception('Please specify commit or pull_request_id')
344 raise Exception('Please specify commit or pull_request_id')
345
345
346 Session().add(comment)
346 Session().add(comment)
347 Session().flush()
347 Session().flush()
348 kwargs = {
348 kwargs = {
349 'user': user,
349 'user': user,
350 'renderer_type': renderer,
350 'renderer_type': renderer,
351 'repo_name': repo.repo_name,
351 'repo_name': repo.repo_name,
352 'status_change': status_change,
352 'status_change': status_change,
353 'status_change_type': status_change_type,
353 'status_change_type': status_change_type,
354 'comment_body': text,
354 'comment_body': text,
355 'comment_file': f_path,
355 'comment_file': f_path,
356 'comment_line': line_no,
356 'comment_line': line_no,
357 'comment_type': comment_type or 'note',
357 'comment_type': comment_type or 'note',
358 'comment_id': comment.comment_id
358 'comment_id': comment.comment_id
359 }
359 }
360
360
361 if commit_obj:
361 if commit_obj:
362 recipients = ChangesetComment.get_users(
362 recipients = ChangesetComment.get_users(
363 revision=commit_obj.raw_id)
363 revision=commit_obj.raw_id)
364 # add commit author if it's in RhodeCode system
364 # add commit author if it's in RhodeCode system
365 cs_author = User.get_from_cs_author(commit_obj.author)
365 cs_author = User.get_from_cs_author(commit_obj.author)
366 if not cs_author:
366 if not cs_author:
367 # use repo owner if we cannot extract the author correctly
367 # use repo owner if we cannot extract the author correctly
368 cs_author = repo.user
368 cs_author = repo.user
369 recipients += [cs_author]
369 recipients += [cs_author]
370
370
371 commit_comment_url = self.get_url(comment, request=request)
371 commit_comment_url = self.get_url(comment, request=request)
372 commit_comment_reply_url = self.get_url(
372 commit_comment_reply_url = self.get_url(
373 comment, request=request,
373 comment, request=request,
374 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
374 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
375
375
376 target_repo_url = h.link_to(
376 target_repo_url = h.link_to(
377 repo.repo_name,
377 repo.repo_name,
378 h.route_url('repo_summary', repo_name=repo.repo_name))
378 h.route_url('repo_summary', repo_name=repo.repo_name))
379
379
380 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
380 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
381 commit_id=commit_id)
381 commit_id=commit_id)
382
382
383 # commit specifics
383 # commit specifics
384 kwargs.update({
384 kwargs.update({
385 'commit': commit_obj,
385 'commit': commit_obj,
386 'commit_message': commit_obj.message,
386 'commit_message': commit_obj.message,
387 'commit_target_repo_url': target_repo_url,
387 'commit_target_repo_url': target_repo_url,
388 'commit_comment_url': commit_comment_url,
388 'commit_comment_url': commit_comment_url,
389 'commit_comment_reply_url': commit_comment_reply_url,
389 'commit_comment_reply_url': commit_comment_reply_url,
390 'commit_url': commit_url,
390 'commit_url': commit_url,
391 'thread_ids': [commit_url, commit_comment_url],
391 'thread_ids': [commit_url, commit_comment_url],
392 })
392 })
393
393
394 elif pull_request_obj:
394 elif pull_request_obj:
395 # get the current participants of this pull request
395 # get the current participants of this pull request
396 recipients = ChangesetComment.get_users(
396 recipients = ChangesetComment.get_users(
397 pull_request_id=pull_request_obj.pull_request_id)
397 pull_request_id=pull_request_obj.pull_request_id)
398 # add pull request author
398 # add pull request author
399 recipients += [pull_request_obj.author]
399 recipients += [pull_request_obj.author]
400
400
401 # add the reviewers to notification
401 # add the reviewers to notification
402 recipients += [x.user for x in pull_request_obj.reviewers]
402 recipients += [x.user for x in pull_request_obj.reviewers]
403
403
404 pr_target_repo = pull_request_obj.target_repo
404 pr_target_repo = pull_request_obj.target_repo
405 pr_source_repo = pull_request_obj.source_repo
405 pr_source_repo = pull_request_obj.source_repo
406
406
407 pr_comment_url = self.get_url(comment, request=request)
407 pr_comment_url = self.get_url(comment, request=request)
408 pr_comment_reply_url = self.get_url(
408 pr_comment_reply_url = self.get_url(
409 comment, request=request,
409 comment, request=request,
410 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
410 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
411
411
412 pr_url = h.route_url(
412 pr_url = h.route_url(
413 'pullrequest_show',
413 'pullrequest_show',
414 repo_name=pr_target_repo.repo_name,
414 repo_name=pr_target_repo.repo_name,
415 pull_request_id=pull_request_obj.pull_request_id, )
415 pull_request_id=pull_request_obj.pull_request_id, )
416
416
417 # set some variables for email notification
417 # set some variables for email notification
418 pr_target_repo_url = h.route_url(
418 pr_target_repo_url = h.route_url(
419 'repo_summary', repo_name=pr_target_repo.repo_name)
419 'repo_summary', repo_name=pr_target_repo.repo_name)
420
420
421 pr_source_repo_url = h.route_url(
421 pr_source_repo_url = h.route_url(
422 'repo_summary', repo_name=pr_source_repo.repo_name)
422 'repo_summary', repo_name=pr_source_repo.repo_name)
423
423
424 # pull request specifics
424 # pull request specifics
425 kwargs.update({
425 kwargs.update({
426 'pull_request': pull_request_obj,
426 'pull_request': pull_request_obj,
427 'pr_id': pull_request_obj.pull_request_id,
427 'pr_id': pull_request_obj.pull_request_id,
428 'pull_request_url': pr_url,
428 'pull_request_url': pr_url,
429 'pull_request_target_repo': pr_target_repo,
429 'pull_request_target_repo': pr_target_repo,
430 'pull_request_target_repo_url': pr_target_repo_url,
430 'pull_request_target_repo_url': pr_target_repo_url,
431 'pull_request_source_repo': pr_source_repo,
431 'pull_request_source_repo': pr_source_repo,
432 'pull_request_source_repo_url': pr_source_repo_url,
432 'pull_request_source_repo_url': pr_source_repo_url,
433 'pr_comment_url': pr_comment_url,
433 'pr_comment_url': pr_comment_url,
434 'pr_comment_reply_url': pr_comment_reply_url,
434 'pr_comment_reply_url': pr_comment_reply_url,
435 'pr_closing': closing_pr,
435 'pr_closing': closing_pr,
436 'thread_ids': [pr_url, pr_comment_url],
436 'thread_ids': [pr_url, pr_comment_url],
437 })
437 })
438
438
439 if send_email:
439 if send_email:
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
441 # pre-generate the subject for notification itself
441 # pre-generate the subject for notification itself
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
443 notification_type, **kwargs)
443 notification_type, **kwargs)
444
444
445 mention_recipients = set(
445 mention_recipients = set(
446 self._extract_mentions(text)).difference(recipients)
446 self._extract_mentions(text)).difference(recipients)
447
447
448 # create notification objects, and emails
448 # create notification objects, and emails
449 NotificationModel().create(
449 NotificationModel().create(
450 created_by=user,
450 created_by=user,
451 notification_subject=subject,
451 notification_subject=subject,
452 notification_body=body_plaintext,
452 notification_body=body_plaintext,
453 notification_type=notification_type,
453 notification_type=notification_type,
454 recipients=recipients,
454 recipients=recipients,
455 mention_recipients=mention_recipients,
455 mention_recipients=mention_recipients,
456 email_kwargs=kwargs,
456 email_kwargs=kwargs,
457 )
457 )
458
458
459 Session().flush()
459 Session().flush()
460 if comment.pull_request:
460 if comment.pull_request:
461 action = 'repo.pull_request.comment.create'
461 action = 'repo.pull_request.comment.create'
462 else:
462 else:
463 action = 'repo.commit.comment.create'
463 action = 'repo.commit.comment.create'
464
464
465 comment_data = comment.get_api_data()
465 comment_data = comment.get_api_data()
466
466
467 self._log_audit_action(
467 self._log_audit_action(
468 action, {'data': comment_data}, auth_user, comment)
468 action, {'data': comment_data}, auth_user, comment)
469
469
470 return comment
470 return comment
471
471
472 def edit(self, comment_id, text, auth_user, version):
472 def edit(self, comment_id, text, auth_user, version):
473 """
473 """
474 Change existing comment for commit or pull request.
474 Change existing comment for commit or pull request.
475
475
476 :param comment_id:
476 :param comment_id:
477 :param text:
477 :param text:
478 :param auth_user: current authenticated user calling this method
478 :param auth_user: current authenticated user calling this method
479 :param version: last comment version
479 :param version: last comment version
480 """
480 """
481 if not text:
481 if not text:
482 log.warning('Missing text for comment, skipping...')
482 log.warning('Missing text for comment, skipping...')
483 return
483 return
484
484
485 comment = ChangesetComment.get(comment_id)
485 comment = ChangesetComment.get(comment_id)
486 old_comment_text = comment.text
486 old_comment_text = comment.text
487 comment.text = text
487 comment.text = text
488 comment.modified_at = datetime.datetime.now()
488 comment.modified_at = datetime.datetime.now()
489 version = safe_int(version)
489 version = safe_int(version)
490
490
491 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
491 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
492 # would return 3 here
492 # would return 3 here
493 comment_version = ChangesetCommentHistory.get_version(comment_id)
493 comment_version = ChangesetCommentHistory.get_version(comment_id)
494
494
495 if isinstance(version, (int, long)) and (comment_version - version) != 1:
495 if isinstance(version, (int, long)) and (comment_version - version) != 1:
496 log.warning(
496 log.warning(
497 'Version mismatch comment_version {} submitted {}, skipping'.format(
497 'Version mismatch comment_version {} submitted {}, skipping'.format(
498 comment_version-1, # -1 since note above
498 comment_version-1, # -1 since note above
499 version
499 version
500 )
500 )
501 )
501 )
502 raise CommentVersionMismatch()
502 raise CommentVersionMismatch()
503
503
504 comment_history = ChangesetCommentHistory()
504 comment_history = ChangesetCommentHistory()
505 comment_history.comment_id = comment_id
505 comment_history.comment_id = comment_id
506 comment_history.version = comment_version
506 comment_history.version = comment_version
507 comment_history.created_by_user_id = auth_user.user_id
507 comment_history.created_by_user_id = auth_user.user_id
508 comment_history.text = old_comment_text
508 comment_history.text = old_comment_text
509 # TODO add email notification
509 # TODO add email notification
510 Session().add(comment_history)
510 Session().add(comment_history)
511 Session().add(comment)
511 Session().add(comment)
512 Session().flush()
512 Session().flush()
513
513
514 if comment.pull_request:
514 if comment.pull_request:
515 action = 'repo.pull_request.comment.edit'
515 action = 'repo.pull_request.comment.edit'
516 else:
516 else:
517 action = 'repo.commit.comment.edit'
517 action = 'repo.commit.comment.edit'
518
518
519 comment_data = comment.get_api_data()
519 comment_data = comment.get_api_data()
520 comment_data['old_comment_text'] = old_comment_text
520 comment_data['old_comment_text'] = old_comment_text
521 self._log_audit_action(
521 self._log_audit_action(
522 action, {'data': comment_data}, auth_user, comment)
522 action, {'data': comment_data}, auth_user, comment)
523
523
524 return comment_history
524 return comment_history
525
525
526 def delete(self, comment, auth_user):
526 def delete(self, comment, auth_user):
527 """
527 """
528 Deletes given comment
528 Deletes given comment
529 """
529 """
530 comment = self.__get_commit_comment(comment)
530 comment = self.__get_commit_comment(comment)
531 old_data = comment.get_api_data()
531 old_data = comment.get_api_data()
532 Session().delete(comment)
532 Session().delete(comment)
533
533
534 if comment.pull_request:
534 if comment.pull_request:
535 action = 'repo.pull_request.comment.delete'
535 action = 'repo.pull_request.comment.delete'
536 else:
536 else:
537 action = 'repo.commit.comment.delete'
537 action = 'repo.commit.comment.delete'
538
538
539 self._log_audit_action(
539 self._log_audit_action(
540 action, {'old_data': old_data}, auth_user, comment)
540 action, {'old_data': old_data}, auth_user, comment)
541
541
542 return comment
542 return comment
543
543
544 def get_all_comments(self, repo_id, revision=None, pull_request=None):
544 def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
545 q = ChangesetComment.query()\
545 q = ChangesetComment.query()\
546 .filter(ChangesetComment.repo_id == repo_id)
546 .filter(ChangesetComment.repo_id == repo_id)
547 if revision:
547 if revision:
548 q = q.filter(ChangesetComment.revision == revision)
548 q = q.filter(ChangesetComment.revision == revision)
549 elif pull_request:
549 elif pull_request:
550 pull_request = self.__get_pull_request(pull_request)
550 pull_request = self.__get_pull_request(pull_request)
551 q = q.filter(ChangesetComment.pull_request == pull_request)
551 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
552 else:
552 else:
553 raise Exception('Please specify commit or pull_request')
553 raise Exception('Please specify commit or pull_request')
554 q = q.order_by(ChangesetComment.created_on)
554 q = q.order_by(ChangesetComment.created_on)
555 if count_only:
556 return q.count()
557
555 return q.all()
558 return q.all()
556
559
557 def get_url(self, comment, request=None, permalink=False, anchor=None):
560 def get_url(self, comment, request=None, permalink=False, anchor=None):
558 if not request:
561 if not request:
559 request = get_current_request()
562 request = get_current_request()
560
563
561 comment = self.__get_commit_comment(comment)
564 comment = self.__get_commit_comment(comment)
562 if anchor is None:
565 if anchor is None:
563 anchor = 'comment-{}'.format(comment.comment_id)
566 anchor = 'comment-{}'.format(comment.comment_id)
564
567
565 if comment.pull_request:
568 if comment.pull_request:
566 pull_request = comment.pull_request
569 pull_request = comment.pull_request
567 if permalink:
570 if permalink:
568 return request.route_url(
571 return request.route_url(
569 'pull_requests_global',
572 'pull_requests_global',
570 pull_request_id=pull_request.pull_request_id,
573 pull_request_id=pull_request.pull_request_id,
571 _anchor=anchor)
574 _anchor=anchor)
572 else:
575 else:
573 return request.route_url(
576 return request.route_url(
574 'pullrequest_show',
577 'pullrequest_show',
575 repo_name=safe_str(pull_request.target_repo.repo_name),
578 repo_name=safe_str(pull_request.target_repo.repo_name),
576 pull_request_id=pull_request.pull_request_id,
579 pull_request_id=pull_request.pull_request_id,
577 _anchor=anchor)
580 _anchor=anchor)
578
581
579 else:
582 else:
580 repo = comment.repo
583 repo = comment.repo
581 commit_id = comment.revision
584 commit_id = comment.revision
582
585
583 if permalink:
586 if permalink:
584 return request.route_url(
587 return request.route_url(
585 'repo_commit', repo_name=safe_str(repo.repo_id),
588 'repo_commit', repo_name=safe_str(repo.repo_id),
586 commit_id=commit_id,
589 commit_id=commit_id,
587 _anchor=anchor)
590 _anchor=anchor)
588
591
589 else:
592 else:
590 return request.route_url(
593 return request.route_url(
591 'repo_commit', repo_name=safe_str(repo.repo_name),
594 'repo_commit', repo_name=safe_str(repo.repo_name),
592 commit_id=commit_id,
595 commit_id=commit_id,
593 _anchor=anchor)
596 _anchor=anchor)
594
597
595 def get_comments(self, repo_id, revision=None, pull_request=None):
598 def get_comments(self, repo_id, revision=None, pull_request=None):
596 """
599 """
597 Gets main comments based on revision or pull_request_id
600 Gets main comments based on revision or pull_request_id
598
601
599 :param repo_id:
602 :param repo_id:
600 :param revision:
603 :param revision:
601 :param pull_request:
604 :param pull_request:
602 """
605 """
603
606
604 q = ChangesetComment.query()\
607 q = ChangesetComment.query()\
605 .filter(ChangesetComment.repo_id == repo_id)\
608 .filter(ChangesetComment.repo_id == repo_id)\
606 .filter(ChangesetComment.line_no == None)\
609 .filter(ChangesetComment.line_no == None)\
607 .filter(ChangesetComment.f_path == None)
610 .filter(ChangesetComment.f_path == None)
608 if revision:
611 if revision:
609 q = q.filter(ChangesetComment.revision == revision)
612 q = q.filter(ChangesetComment.revision == revision)
610 elif pull_request:
613 elif pull_request:
611 pull_request = self.__get_pull_request(pull_request)
614 pull_request = self.__get_pull_request(pull_request)
612 q = q.filter(ChangesetComment.pull_request == pull_request)
615 q = q.filter(ChangesetComment.pull_request == pull_request)
613 else:
616 else:
614 raise Exception('Please specify commit or pull_request')
617 raise Exception('Please specify commit or pull_request')
615 q = q.order_by(ChangesetComment.created_on)
618 q = q.order_by(ChangesetComment.created_on)
616 return q.all()
619 return q.all()
617
620
618 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
621 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
619 q = self._get_inline_comments_query(repo_id, revision, pull_request)
622 q = self._get_inline_comments_query(repo_id, revision, pull_request)
620 return self._group_comments_by_path_and_line_number(q)
623 return self._group_comments_by_path_and_line_number(q)
621
624
622 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
625 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
623 version=None):
626 version=None):
624 inline_comms = []
627 inline_comms = []
625 for fname, per_line_comments in inline_comments.iteritems():
628 for fname, per_line_comments in inline_comments.iteritems():
626 for lno, comments in per_line_comments.iteritems():
629 for lno, comments in per_line_comments.iteritems():
627 for comm in comments:
630 for comm in comments:
628 if not comm.outdated_at_version(version) and skip_outdated:
631 if not comm.outdated_at_version(version) and skip_outdated:
629 inline_comms.append(comm)
632 inline_comms.append(comm)
630
633
631 return inline_comms
634 return inline_comms
632
635
633 def get_outdated_comments(self, repo_id, pull_request):
636 def get_outdated_comments(self, repo_id, pull_request):
634 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
637 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
635 # of a pull request.
638 # of a pull request.
636 q = self._all_inline_comments_of_pull_request(pull_request)
639 q = self._all_inline_comments_of_pull_request(pull_request)
637 q = q.filter(
640 q = q.filter(
638 ChangesetComment.display_state ==
641 ChangesetComment.display_state ==
639 ChangesetComment.COMMENT_OUTDATED
642 ChangesetComment.COMMENT_OUTDATED
640 ).order_by(ChangesetComment.comment_id.asc())
643 ).order_by(ChangesetComment.comment_id.asc())
641
644
642 return self._group_comments_by_path_and_line_number(q)
645 return self._group_comments_by_path_and_line_number(q)
643
646
644 def _get_inline_comments_query(self, repo_id, revision, pull_request):
647 def _get_inline_comments_query(self, repo_id, revision, pull_request):
645 # TODO: johbo: Split this into two methods: One for PR and one for
648 # TODO: johbo: Split this into two methods: One for PR and one for
646 # commit.
649 # commit.
647 if revision:
650 if revision:
648 q = Session().query(ChangesetComment).filter(
651 q = Session().query(ChangesetComment).filter(
649 ChangesetComment.repo_id == repo_id,
652 ChangesetComment.repo_id == repo_id,
650 ChangesetComment.line_no != null(),
653 ChangesetComment.line_no != null(),
651 ChangesetComment.f_path != null(),
654 ChangesetComment.f_path != null(),
652 ChangesetComment.revision == revision)
655 ChangesetComment.revision == revision)
653
656
654 elif pull_request:
657 elif pull_request:
655 pull_request = self.__get_pull_request(pull_request)
658 pull_request = self.__get_pull_request(pull_request)
656 if not CommentsModel.use_outdated_comments(pull_request):
659 if not CommentsModel.use_outdated_comments(pull_request):
657 q = self._visible_inline_comments_of_pull_request(pull_request)
660 q = self._visible_inline_comments_of_pull_request(pull_request)
658 else:
661 else:
659 q = self._all_inline_comments_of_pull_request(pull_request)
662 q = self._all_inline_comments_of_pull_request(pull_request)
660
663
661 else:
664 else:
662 raise Exception('Please specify commit or pull_request_id')
665 raise Exception('Please specify commit or pull_request_id')
663 q = q.order_by(ChangesetComment.comment_id.asc())
666 q = q.order_by(ChangesetComment.comment_id.asc())
664 return q
667 return q
665
668
666 def _group_comments_by_path_and_line_number(self, q):
669 def _group_comments_by_path_and_line_number(self, q):
667 comments = q.all()
670 comments = q.all()
668 paths = collections.defaultdict(lambda: collections.defaultdict(list))
671 paths = collections.defaultdict(lambda: collections.defaultdict(list))
669 for co in comments:
672 for co in comments:
670 paths[co.f_path][co.line_no].append(co)
673 paths[co.f_path][co.line_no].append(co)
671 return paths
674 return paths
672
675
673 @classmethod
676 @classmethod
674 def needed_extra_diff_context(cls):
677 def needed_extra_diff_context(cls):
675 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
678 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
676
679
677 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
680 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
678 if not CommentsModel.use_outdated_comments(pull_request):
681 if not CommentsModel.use_outdated_comments(pull_request):
679 return
682 return
680
683
681 comments = self._visible_inline_comments_of_pull_request(pull_request)
684 comments = self._visible_inline_comments_of_pull_request(pull_request)
682 comments_to_outdate = comments.all()
685 comments_to_outdate = comments.all()
683
686
684 for comment in comments_to_outdate:
687 for comment in comments_to_outdate:
685 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
688 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
686
689
687 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
690 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
688 diff_line = _parse_comment_line_number(comment.line_no)
691 diff_line = _parse_comment_line_number(comment.line_no)
689
692
690 try:
693 try:
691 old_context = old_diff_proc.get_context_of_line(
694 old_context = old_diff_proc.get_context_of_line(
692 path=comment.f_path, diff_line=diff_line)
695 path=comment.f_path, diff_line=diff_line)
693 new_context = new_diff_proc.get_context_of_line(
696 new_context = new_diff_proc.get_context_of_line(
694 path=comment.f_path, diff_line=diff_line)
697 path=comment.f_path, diff_line=diff_line)
695 except (diffs.LineNotInDiffException,
698 except (diffs.LineNotInDiffException,
696 diffs.FileNotInDiffException):
699 diffs.FileNotInDiffException):
697 comment.display_state = ChangesetComment.COMMENT_OUTDATED
700 comment.display_state = ChangesetComment.COMMENT_OUTDATED
698 return
701 return
699
702
700 if old_context == new_context:
703 if old_context == new_context:
701 return
704 return
702
705
703 if self._should_relocate_diff_line(diff_line):
706 if self._should_relocate_diff_line(diff_line):
704 new_diff_lines = new_diff_proc.find_context(
707 new_diff_lines = new_diff_proc.find_context(
705 path=comment.f_path, context=old_context,
708 path=comment.f_path, context=old_context,
706 offset=self.DIFF_CONTEXT_BEFORE)
709 offset=self.DIFF_CONTEXT_BEFORE)
707 if not new_diff_lines:
710 if not new_diff_lines:
708 comment.display_state = ChangesetComment.COMMENT_OUTDATED
711 comment.display_state = ChangesetComment.COMMENT_OUTDATED
709 else:
712 else:
710 new_diff_line = self._choose_closest_diff_line(
713 new_diff_line = self._choose_closest_diff_line(
711 diff_line, new_diff_lines)
714 diff_line, new_diff_lines)
712 comment.line_no = _diff_to_comment_line_number(new_diff_line)
715 comment.line_no = _diff_to_comment_line_number(new_diff_line)
713 else:
716 else:
714 comment.display_state = ChangesetComment.COMMENT_OUTDATED
717 comment.display_state = ChangesetComment.COMMENT_OUTDATED
715
718
716 def _should_relocate_diff_line(self, diff_line):
719 def _should_relocate_diff_line(self, diff_line):
717 """
720 """
718 Checks if relocation shall be tried for the given `diff_line`.
721 Checks if relocation shall be tried for the given `diff_line`.
719
722
720 If a comment points into the first lines, then we can have a situation
723 If a comment points into the first lines, then we can have a situation
721 that after an update another line has been added on top. In this case
724 that after an update another line has been added on top. In this case
722 we would find the context still and move the comment around. This
725 we would find the context still and move the comment around. This
723 would be wrong.
726 would be wrong.
724 """
727 """
725 should_relocate = (
728 should_relocate = (
726 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
729 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
727 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
730 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
728 return should_relocate
731 return should_relocate
729
732
730 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
733 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
731 candidate = new_diff_lines[0]
734 candidate = new_diff_lines[0]
732 best_delta = _diff_line_delta(diff_line, candidate)
735 best_delta = _diff_line_delta(diff_line, candidate)
733 for new_diff_line in new_diff_lines[1:]:
736 for new_diff_line in new_diff_lines[1:]:
734 delta = _diff_line_delta(diff_line, new_diff_line)
737 delta = _diff_line_delta(diff_line, new_diff_line)
735 if delta < best_delta:
738 if delta < best_delta:
736 candidate = new_diff_line
739 candidate = new_diff_line
737 best_delta = delta
740 best_delta = delta
738 return candidate
741 return candidate
739
742
740 def _visible_inline_comments_of_pull_request(self, pull_request):
743 def _visible_inline_comments_of_pull_request(self, pull_request):
741 comments = self._all_inline_comments_of_pull_request(pull_request)
744 comments = self._all_inline_comments_of_pull_request(pull_request)
742 comments = comments.filter(
745 comments = comments.filter(
743 coalesce(ChangesetComment.display_state, '') !=
746 coalesce(ChangesetComment.display_state, '') !=
744 ChangesetComment.COMMENT_OUTDATED)
747 ChangesetComment.COMMENT_OUTDATED)
745 return comments
748 return comments
746
749
747 def _all_inline_comments_of_pull_request(self, pull_request):
750 def _all_inline_comments_of_pull_request(self, pull_request):
748 comments = Session().query(ChangesetComment)\
751 comments = Session().query(ChangesetComment)\
749 .filter(ChangesetComment.line_no != None)\
752 .filter(ChangesetComment.line_no != None)\
750 .filter(ChangesetComment.f_path != None)\
753 .filter(ChangesetComment.f_path != None)\
751 .filter(ChangesetComment.pull_request == pull_request)
754 .filter(ChangesetComment.pull_request == pull_request)
752 return comments
755 return comments
753
756
754 def _all_general_comments_of_pull_request(self, pull_request):
757 def _all_general_comments_of_pull_request(self, pull_request):
755 comments = Session().query(ChangesetComment)\
758 comments = Session().query(ChangesetComment)\
756 .filter(ChangesetComment.line_no == None)\
759 .filter(ChangesetComment.line_no == None)\
757 .filter(ChangesetComment.f_path == None)\
760 .filter(ChangesetComment.f_path == None)\
758 .filter(ChangesetComment.pull_request == pull_request)
761 .filter(ChangesetComment.pull_request == pull_request)
759
762
760 return comments
763 return comments
761
764
762 @staticmethod
765 @staticmethod
763 def use_outdated_comments(pull_request):
766 def use_outdated_comments(pull_request):
764 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
767 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
765 settings = settings_model.get_general_settings()
768 settings = settings_model.get_general_settings()
766 return settings.get('rhodecode_use_outdated_comments', False)
769 return settings.get('rhodecode_use_outdated_comments', False)
767
770
768 def trigger_commit_comment_hook(self, repo, user, action, data=None):
771 def trigger_commit_comment_hook(self, repo, user, action, data=None):
769 repo = self._get_repo(repo)
772 repo = self._get_repo(repo)
770 target_scm = repo.scm_instance()
773 target_scm = repo.scm_instance()
771 if action == 'create':
774 if action == 'create':
772 trigger_hook = hooks_utils.trigger_comment_commit_hooks
775 trigger_hook = hooks_utils.trigger_comment_commit_hooks
773 elif action == 'edit':
776 elif action == 'edit':
774 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
777 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
775 else:
778 else:
776 return
779 return
777
780
778 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
781 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
779 repo, action, trigger_hook)
782 repo, action, trigger_hook)
780 trigger_hook(
783 trigger_hook(
781 username=user.username,
784 username=user.username,
782 repo_name=repo.repo_name,
785 repo_name=repo.repo_name,
783 repo_type=target_scm.alias,
786 repo_type=target_scm.alias,
784 repo=repo,
787 repo=repo,
785 data=data)
788 data=data)
786
789
787
790
788 def _parse_comment_line_number(line_no):
791 def _parse_comment_line_number(line_no):
789 """
792 """
790 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
793 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
791 """
794 """
792 old_line = None
795 old_line = None
793 new_line = None
796 new_line = None
794 if line_no.startswith('o'):
797 if line_no.startswith('o'):
795 old_line = int(line_no[1:])
798 old_line = int(line_no[1:])
796 elif line_no.startswith('n'):
799 elif line_no.startswith('n'):
797 new_line = int(line_no[1:])
800 new_line = int(line_no[1:])
798 else:
801 else:
799 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
802 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
800 return diffs.DiffLineNumber(old_line, new_line)
803 return diffs.DiffLineNumber(old_line, new_line)
801
804
802
805
803 def _diff_to_comment_line_number(diff_line):
806 def _diff_to_comment_line_number(diff_line):
804 if diff_line.new is not None:
807 if diff_line.new is not None:
805 return u'n{}'.format(diff_line.new)
808 return u'n{}'.format(diff_line.new)
806 elif diff_line.old is not None:
809 elif diff_line.old is not None:
807 return u'o{}'.format(diff_line.old)
810 return u'o{}'.format(diff_line.old)
808 return u''
811 return u''
809
812
810
813
811 def _diff_line_delta(a, b):
814 def _diff_line_delta(a, b):
812 if None not in (a.new, b.new):
815 if None not in (a.new, b.new):
813 return abs(a.new - b.new)
816 return abs(a.new - b.new)
814 elif None not in (a.old, b.old):
817 elif None not in (a.old, b.old):
815 return abs(a.old - b.old)
818 return abs(a.old - b.old)
816 else:
819 else:
817 raise ValueError(
820 raise ValueError(
818 "Cannot compute delta between {} and {}".format(a, b))
821 "Cannot compute delta between {} and {}".format(a, b))
General Comments 0
You need to be logged in to leave comments. Login now