##// END OF EJS Templates
comments: counts exclude now draft comments, they shouldn't be visible.
milka -
r4553:e8a9a30e default
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_count = comments_model.get_all_comments(
737 comments_count = comments_model.get_all_comments(
738 repo_id, pull_request=pr, count_only=True)
738 repo_id, pull_request=pr, include_drafts=False, 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', comments_count),
763 'comments': _render('pullrequest_comments', comments_count),
764 'comments_raw': comments_count,
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,1856 +1,1857 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 (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 statuses=statuses, offset=start, length=limit,
83 statuses=statuses, offset=start, length=limit,
84 order_by=order_by, order_dir=order_dir)
84 order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name, search_q=search_q, source=source, statuses=statuses,
86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 opened_by=opened_by)
87 opened_by=opened_by)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 offset=start, length=limit, order_by=order_by,
92 offset=start, length=limit, order_by=order_by,
93 order_dir=order_dir)
93 order_dir=order_dir)
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 statuses=statuses, opened_by=opened_by)
96 statuses=statuses, opened_by=opened_by)
97 else:
97 else:
98 pull_requests = PullRequestModel().get_all(
98 pull_requests = PullRequestModel().get_all(
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 statuses=statuses, offset=start, length=limit,
100 statuses=statuses, offset=start, length=limit,
101 order_by=order_by, order_dir=order_dir)
101 order_by=order_by, order_dir=order_dir)
102 pull_requests_total_count = PullRequestModel().count_all(
102 pull_requests_total_count = PullRequestModel().count_all(
103 repo_name, search_q=search_q, source=source, statuses=statuses,
103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 opened_by=opened_by)
104 opened_by=opened_by)
105
105
106 data = []
106 data = []
107 comments_model = CommentsModel()
107 comments_model = CommentsModel()
108 for pr in pull_requests:
108 for pr in pull_requests:
109 comments_count = comments_model.get_all_comments(
109 comments_count = comments_model.get_all_comments(
110 self.db_repo.repo_id, pull_request=pr, count_only=True)
110 self.db_repo.repo_id, pull_request=pr,
111 include_drafts=False, count_only=True)
111
112
112 data.append({
113 data.append({
113 'name': _render('pullrequest_name',
114 'name': _render('pullrequest_name',
114 pr.pull_request_id, pr.pull_request_state,
115 pr.pull_request_id, pr.pull_request_state,
115 pr.work_in_progress, pr.target_repo.repo_name,
116 pr.work_in_progress, pr.target_repo.repo_name,
116 short=True),
117 short=True),
117 'name_raw': pr.pull_request_id,
118 'name_raw': pr.pull_request_id,
118 'status': _render('pullrequest_status',
119 'status': _render('pullrequest_status',
119 pr.calculated_review_status()),
120 pr.calculated_review_status()),
120 'title': _render('pullrequest_title', pr.title, pr.description),
121 'title': _render('pullrequest_title', pr.title, pr.description),
121 'description': h.escape(pr.description),
122 'description': h.escape(pr.description),
122 'updated_on': _render('pullrequest_updated_on',
123 'updated_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.updated_on)),
124 h.datetime_to_time(pr.updated_on)),
124 'updated_on_raw': h.datetime_to_time(pr.updated_on),
125 'updated_on_raw': h.datetime_to_time(pr.updated_on),
125 'created_on': _render('pullrequest_updated_on',
126 'created_on': _render('pullrequest_updated_on',
126 h.datetime_to_time(pr.created_on)),
127 h.datetime_to_time(pr.created_on)),
127 'created_on_raw': h.datetime_to_time(pr.created_on),
128 'created_on_raw': h.datetime_to_time(pr.created_on),
128 'state': pr.pull_request_state,
129 'state': pr.pull_request_state,
129 'author': _render('pullrequest_author',
130 'author': _render('pullrequest_author',
130 pr.author.full_contact, ),
131 pr.author.full_contact, ),
131 'author_raw': pr.author.full_name,
132 'author_raw': pr.author.full_name,
132 'comments': _render('pullrequest_comments', comments_count),
133 'comments': _render('pullrequest_comments', comments_count),
133 'comments_raw': comments_count,
134 'comments_raw': comments_count,
134 'closed': pr.is_closed(),
135 'closed': pr.is_closed(),
135 })
136 })
136
137
137 data = ({
138 data = ({
138 'draw': draw,
139 'draw': draw,
139 'data': data,
140 'data': data,
140 'recordsTotal': pull_requests_total_count,
141 'recordsTotal': pull_requests_total_count,
141 'recordsFiltered': pull_requests_total_count,
142 'recordsFiltered': pull_requests_total_count,
142 })
143 })
143 return data
144 return data
144
145
145 @LoginRequired()
146 @LoginRequired()
146 @HasRepoPermissionAnyDecorator(
147 @HasRepoPermissionAnyDecorator(
147 'repository.read', 'repository.write', 'repository.admin')
148 'repository.read', 'repository.write', 'repository.admin')
148 @view_config(
149 @view_config(
149 route_name='pullrequest_show_all', request_method='GET',
150 route_name='pullrequest_show_all', request_method='GET',
150 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
151 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
151 def pull_request_list(self):
152 def pull_request_list(self):
152 c = self.load_default_context()
153 c = self.load_default_context()
153
154
154 req_get = self.request.GET
155 req_get = self.request.GET
155 c.source = str2bool(req_get.get('source'))
156 c.source = str2bool(req_get.get('source'))
156 c.closed = str2bool(req_get.get('closed'))
157 c.closed = str2bool(req_get.get('closed'))
157 c.my = str2bool(req_get.get('my'))
158 c.my = str2bool(req_get.get('my'))
158 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
159 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
159 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
160 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
160
161
161 c.active = 'open'
162 c.active = 'open'
162 if c.my:
163 if c.my:
163 c.active = 'my'
164 c.active = 'my'
164 if c.closed:
165 if c.closed:
165 c.active = 'closed'
166 c.active = 'closed'
166 if c.awaiting_review and not c.source:
167 if c.awaiting_review and not c.source:
167 c.active = 'awaiting'
168 c.active = 'awaiting'
168 if c.source and not c.awaiting_review:
169 if c.source and not c.awaiting_review:
169 c.active = 'source'
170 c.active = 'source'
170 if c.awaiting_my_review:
171 if c.awaiting_my_review:
171 c.active = 'awaiting_my'
172 c.active = 'awaiting_my'
172
173
173 return self._get_template_context(c)
174 return self._get_template_context(c)
174
175
175 @LoginRequired()
176 @LoginRequired()
176 @HasRepoPermissionAnyDecorator(
177 @HasRepoPermissionAnyDecorator(
177 'repository.read', 'repository.write', 'repository.admin')
178 'repository.read', 'repository.write', 'repository.admin')
178 @view_config(
179 @view_config(
179 route_name='pullrequest_show_all_data', request_method='GET',
180 route_name='pullrequest_show_all_data', request_method='GET',
180 renderer='json_ext', xhr=True)
181 renderer='json_ext', xhr=True)
181 def pull_request_list_data(self):
182 def pull_request_list_data(self):
182 self.load_default_context()
183 self.load_default_context()
183
184
184 # additional filters
185 # additional filters
185 req_get = self.request.GET
186 req_get = self.request.GET
186 source = str2bool(req_get.get('source'))
187 source = str2bool(req_get.get('source'))
187 closed = str2bool(req_get.get('closed'))
188 closed = str2bool(req_get.get('closed'))
188 my = str2bool(req_get.get('my'))
189 my = str2bool(req_get.get('my'))
189 awaiting_review = str2bool(req_get.get('awaiting_review'))
190 awaiting_review = str2bool(req_get.get('awaiting_review'))
190 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
191 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
191
192
192 filter_type = 'awaiting_review' if awaiting_review \
193 filter_type = 'awaiting_review' if awaiting_review \
193 else 'awaiting_my_review' if awaiting_my_review \
194 else 'awaiting_my_review' if awaiting_my_review \
194 else None
195 else None
195
196
196 opened_by = None
197 opened_by = None
197 if my:
198 if my:
198 opened_by = [self._rhodecode_user.user_id]
199 opened_by = [self._rhodecode_user.user_id]
199
200
200 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
201 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
201 if closed:
202 if closed:
202 statuses = [PullRequest.STATUS_CLOSED]
203 statuses = [PullRequest.STATUS_CLOSED]
203
204
204 data = self._get_pull_requests_list(
205 data = self._get_pull_requests_list(
205 repo_name=self.db_repo_name, source=source,
206 repo_name=self.db_repo_name, source=source,
206 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
207 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
207
208
208 return data
209 return data
209
210
210 def _is_diff_cache_enabled(self, target_repo):
211 def _is_diff_cache_enabled(self, target_repo):
211 caching_enabled = self._get_general_setting(
212 caching_enabled = self._get_general_setting(
212 target_repo, 'rhodecode_diff_cache')
213 target_repo, 'rhodecode_diff_cache')
213 log.debug('Diff caching enabled: %s', caching_enabled)
214 log.debug('Diff caching enabled: %s', caching_enabled)
214 return caching_enabled
215 return caching_enabled
215
216
216 def _get_diffset(self, source_repo_name, source_repo,
217 def _get_diffset(self, source_repo_name, source_repo,
217 ancestor_commit,
218 ancestor_commit,
218 source_ref_id, target_ref_id,
219 source_ref_id, target_ref_id,
219 target_commit, source_commit, diff_limit, file_limit,
220 target_commit, source_commit, diff_limit, file_limit,
220 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
221 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
221
222
222 if use_ancestor:
223 if use_ancestor:
223 # we might want to not use it for versions
224 # we might want to not use it for versions
224 target_ref_id = ancestor_commit.raw_id
225 target_ref_id = ancestor_commit.raw_id
225
226
226 vcs_diff = PullRequestModel().get_diff(
227 vcs_diff = PullRequestModel().get_diff(
227 source_repo, source_ref_id, target_ref_id,
228 source_repo, source_ref_id, target_ref_id,
228 hide_whitespace_changes, diff_context)
229 hide_whitespace_changes, diff_context)
229
230
230 diff_processor = diffs.DiffProcessor(
231 diff_processor = diffs.DiffProcessor(
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 file_limit=file_limit, show_full_diff=fulldiff)
233 file_limit=file_limit, show_full_diff=fulldiff)
233
234
234 _parsed = diff_processor.prepare()
235 _parsed = diff_processor.prepare()
235
236
236 diffset = codeblocks.DiffSet(
237 diffset = codeblocks.DiffSet(
237 repo_name=self.db_repo_name,
238 repo_name=self.db_repo_name,
238 source_repo_name=source_repo_name,
239 source_repo_name=source_repo_name,
239 source_node_getter=codeblocks.diffset_node_getter(target_commit),
240 source_node_getter=codeblocks.diffset_node_getter(target_commit),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit),
241 target_node_getter=codeblocks.diffset_node_getter(source_commit),
241 )
242 )
242 diffset = self.path_filter.render_patchset_filtered(
243 diffset = self.path_filter.render_patchset_filtered(
243 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
244 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
244
245
245 return diffset
246 return diffset
246
247
247 def _get_range_diffset(self, source_scm, source_repo,
248 def _get_range_diffset(self, source_scm, source_repo,
248 commit1, commit2, diff_limit, file_limit,
249 commit1, commit2, diff_limit, file_limit,
249 fulldiff, hide_whitespace_changes, diff_context):
250 fulldiff, hide_whitespace_changes, diff_context):
250 vcs_diff = source_scm.get_diff(
251 vcs_diff = source_scm.get_diff(
251 commit1, commit2,
252 commit1, commit2,
252 ignore_whitespace=hide_whitespace_changes,
253 ignore_whitespace=hide_whitespace_changes,
253 context=diff_context)
254 context=diff_context)
254
255
255 diff_processor = diffs.DiffProcessor(
256 diff_processor = diffs.DiffProcessor(
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 file_limit=file_limit, show_full_diff=fulldiff)
258 file_limit=file_limit, show_full_diff=fulldiff)
258
259
259 _parsed = diff_processor.prepare()
260 _parsed = diff_processor.prepare()
260
261
261 diffset = codeblocks.DiffSet(
262 diffset = codeblocks.DiffSet(
262 repo_name=source_repo.repo_name,
263 repo_name=source_repo.repo_name,
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
265 target_node_getter=codeblocks.diffset_node_getter(commit2))
265
266
266 diffset = self.path_filter.render_patchset_filtered(
267 diffset = self.path_filter.render_patchset_filtered(
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268
269
269 return diffset
270 return diffset
270
271
271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 comments_model = CommentsModel()
273 comments_model = CommentsModel()
273
274
274 # GENERAL COMMENTS with versions #
275 # GENERAL COMMENTS with versions #
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 q = q.order_by(ChangesetComment.comment_id.asc())
277 q = q.order_by(ChangesetComment.comment_id.asc())
277 if not include_drafts:
278 if not include_drafts:
278 q = q.filter(ChangesetComment.draft == false())
279 q = q.filter(ChangesetComment.draft == false())
279 general_comments = q
280 general_comments = q
280
281
281 # pick comments we want to render at current version
282 # pick comments we want to render at current version
282 c.comment_versions = comments_model.aggregate_comments(
283 c.comment_versions = comments_model.aggregate_comments(
283 general_comments, versions, c.at_version_num)
284 general_comments, versions, c.at_version_num)
284
285
285 # INLINE COMMENTS with versions #
286 # INLINE COMMENTS with versions #
286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
287 q = comments_model._all_inline_comments_of_pull_request(pull_request)
287 q = q.order_by(ChangesetComment.comment_id.asc())
288 q = q.order_by(ChangesetComment.comment_id.asc())
288 if not include_drafts:
289 if not include_drafts:
289 q = q.filter(ChangesetComment.draft == false())
290 q = q.filter(ChangesetComment.draft == false())
290 inline_comments = q
291 inline_comments = q
291
292
292 c.inline_versions = comments_model.aggregate_comments(
293 c.inline_versions = comments_model.aggregate_comments(
293 inline_comments, versions, c.at_version_num, inline=True)
294 inline_comments, versions, c.at_version_num, inline=True)
294
295
295 # Comments inline+general
296 # Comments inline+general
296 if c.at_version:
297 if c.at_version:
297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
298 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
298 c.comments = c.comment_versions[c.at_version_num]['display']
299 c.comments = c.comment_versions[c.at_version_num]['display']
299 else:
300 else:
300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
301 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
301 c.comments = c.comment_versions[c.at_version_num]['until']
302 c.comments = c.comment_versions[c.at_version_num]['until']
302
303
303 return general_comments, inline_comments
304 return general_comments, inline_comments
304
305
305 @LoginRequired()
306 @LoginRequired()
306 @HasRepoPermissionAnyDecorator(
307 @HasRepoPermissionAnyDecorator(
307 'repository.read', 'repository.write', 'repository.admin')
308 'repository.read', 'repository.write', 'repository.admin')
308 @view_config(
309 @view_config(
309 route_name='pullrequest_show', request_method='GET',
310 route_name='pullrequest_show', request_method='GET',
310 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
311 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
311 def pull_request_show(self):
312 def pull_request_show(self):
312 _ = self.request.translate
313 _ = self.request.translate
313 c = self.load_default_context()
314 c = self.load_default_context()
314
315
315 pull_request = PullRequest.get_or_404(
316 pull_request = PullRequest.get_or_404(
316 self.request.matchdict['pull_request_id'])
317 self.request.matchdict['pull_request_id'])
317 pull_request_id = pull_request.pull_request_id
318 pull_request_id = pull_request.pull_request_id
318
319
319 c.state_progressing = pull_request.is_state_changing()
320 c.state_progressing = pull_request.is_state_changing()
320 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
321 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
321
322
322 _new_state = {
323 _new_state = {
323 'created': PullRequest.STATE_CREATED,
324 'created': PullRequest.STATE_CREATED,
324 }.get(self.request.GET.get('force_state'))
325 }.get(self.request.GET.get('force_state'))
325
326
326 if c.is_super_admin and _new_state:
327 if c.is_super_admin and _new_state:
327 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
328 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
328 h.flash(
329 h.flash(
329 _('Pull Request state was force changed to `{}`').format(_new_state),
330 _('Pull Request state was force changed to `{}`').format(_new_state),
330 category='success')
331 category='success')
331 Session().commit()
332 Session().commit()
332
333
333 raise HTTPFound(h.route_path(
334 raise HTTPFound(h.route_path(
334 'pullrequest_show', repo_name=self.db_repo_name,
335 'pullrequest_show', repo_name=self.db_repo_name,
335 pull_request_id=pull_request_id))
336 pull_request_id=pull_request_id))
336
337
337 version = self.request.GET.get('version')
338 version = self.request.GET.get('version')
338 from_version = self.request.GET.get('from_version') or version
339 from_version = self.request.GET.get('from_version') or version
339 merge_checks = self.request.GET.get('merge_checks')
340 merge_checks = self.request.GET.get('merge_checks')
340 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
341 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
341 force_refresh = str2bool(self.request.GET.get('force_refresh'))
342 force_refresh = str2bool(self.request.GET.get('force_refresh'))
342 c.range_diff_on = self.request.GET.get('range-diff') == "1"
343 c.range_diff_on = self.request.GET.get('range-diff') == "1"
343
344
344 # fetch global flags of ignore ws or context lines
345 # fetch global flags of ignore ws or context lines
345 diff_context = diffs.get_diff_context(self.request)
346 diff_context = diffs.get_diff_context(self.request)
346 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
347 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
347
348
348 (pull_request_latest,
349 (pull_request_latest,
349 pull_request_at_ver,
350 pull_request_at_ver,
350 pull_request_display_obj,
351 pull_request_display_obj,
351 at_version) = PullRequestModel().get_pr_version(
352 at_version) = PullRequestModel().get_pr_version(
352 pull_request_id, version=version)
353 pull_request_id, version=version)
353
354
354 pr_closed = pull_request_latest.is_closed()
355 pr_closed = pull_request_latest.is_closed()
355
356
356 if pr_closed and (version or from_version):
357 if pr_closed and (version or from_version):
357 # not allow to browse versions for closed PR
358 # not allow to browse versions for closed PR
358 raise HTTPFound(h.route_path(
359 raise HTTPFound(h.route_path(
359 'pullrequest_show', repo_name=self.db_repo_name,
360 'pullrequest_show', repo_name=self.db_repo_name,
360 pull_request_id=pull_request_id))
361 pull_request_id=pull_request_id))
361
362
362 versions = pull_request_display_obj.versions()
363 versions = pull_request_display_obj.versions()
363 # used to store per-commit range diffs
364 # used to store per-commit range diffs
364 c.changes = collections.OrderedDict()
365 c.changes = collections.OrderedDict()
365
366
366 c.at_version = at_version
367 c.at_version = at_version
367 c.at_version_num = (at_version
368 c.at_version_num = (at_version
368 if at_version and at_version != PullRequest.LATEST_VER
369 if at_version and at_version != PullRequest.LATEST_VER
369 else None)
370 else None)
370
371
371 c.at_version_index = ChangesetComment.get_index_from_version(
372 c.at_version_index = ChangesetComment.get_index_from_version(
372 c.at_version_num, versions)
373 c.at_version_num, versions)
373
374
374 (prev_pull_request_latest,
375 (prev_pull_request_latest,
375 prev_pull_request_at_ver,
376 prev_pull_request_at_ver,
376 prev_pull_request_display_obj,
377 prev_pull_request_display_obj,
377 prev_at_version) = PullRequestModel().get_pr_version(
378 prev_at_version) = PullRequestModel().get_pr_version(
378 pull_request_id, version=from_version)
379 pull_request_id, version=from_version)
379
380
380 c.from_version = prev_at_version
381 c.from_version = prev_at_version
381 c.from_version_num = (prev_at_version
382 c.from_version_num = (prev_at_version
382 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
383 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
383 else None)
384 else None)
384 c.from_version_index = ChangesetComment.get_index_from_version(
385 c.from_version_index = ChangesetComment.get_index_from_version(
385 c.from_version_num, versions)
386 c.from_version_num, versions)
386
387
387 # define if we're in COMPARE mode or VIEW at version mode
388 # define if we're in COMPARE mode or VIEW at version mode
388 compare = at_version != prev_at_version
389 compare = at_version != prev_at_version
389
390
390 # pull_requests repo_name we opened it against
391 # pull_requests repo_name we opened it against
391 # ie. target_repo must match
392 # ie. target_repo must match
392 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
393 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
393 log.warning('Mismatch between the current repo: %s, and target %s',
394 log.warning('Mismatch between the current repo: %s, and target %s',
394 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
395 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
395 raise HTTPNotFound()
396 raise HTTPNotFound()
396
397
397 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
398 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
398
399
399 c.pull_request = pull_request_display_obj
400 c.pull_request = pull_request_display_obj
400 c.renderer = pull_request_at_ver.description_renderer or c.renderer
401 c.renderer = pull_request_at_ver.description_renderer or c.renderer
401 c.pull_request_latest = pull_request_latest
402 c.pull_request_latest = pull_request_latest
402
403
403 # inject latest version
404 # inject latest version
404 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
405 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
405 c.versions = versions + [latest_ver]
406 c.versions = versions + [latest_ver]
406
407
407 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
408 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
408 c.allowed_to_change_status = False
409 c.allowed_to_change_status = False
409 c.allowed_to_update = False
410 c.allowed_to_update = False
410 c.allowed_to_merge = False
411 c.allowed_to_merge = False
411 c.allowed_to_delete = False
412 c.allowed_to_delete = False
412 c.allowed_to_comment = False
413 c.allowed_to_comment = False
413 c.allowed_to_close = False
414 c.allowed_to_close = False
414 else:
415 else:
415 can_change_status = PullRequestModel().check_user_change_status(
416 can_change_status = PullRequestModel().check_user_change_status(
416 pull_request_at_ver, self._rhodecode_user)
417 pull_request_at_ver, self._rhodecode_user)
417 c.allowed_to_change_status = can_change_status and not pr_closed
418 c.allowed_to_change_status = can_change_status and not pr_closed
418
419
419 c.allowed_to_update = PullRequestModel().check_user_update(
420 c.allowed_to_update = PullRequestModel().check_user_update(
420 pull_request_latest, self._rhodecode_user) and not pr_closed
421 pull_request_latest, self._rhodecode_user) and not pr_closed
421 c.allowed_to_merge = PullRequestModel().check_user_merge(
422 c.allowed_to_merge = PullRequestModel().check_user_merge(
422 pull_request_latest, self._rhodecode_user) and not pr_closed
423 pull_request_latest, self._rhodecode_user) and not pr_closed
423 c.allowed_to_delete = PullRequestModel().check_user_delete(
424 c.allowed_to_delete = PullRequestModel().check_user_delete(
424 pull_request_latest, self._rhodecode_user) and not pr_closed
425 pull_request_latest, self._rhodecode_user) and not pr_closed
425 c.allowed_to_comment = not pr_closed
426 c.allowed_to_comment = not pr_closed
426 c.allowed_to_close = c.allowed_to_merge and not pr_closed
427 c.allowed_to_close = c.allowed_to_merge and not pr_closed
427
428
428 c.forbid_adding_reviewers = False
429 c.forbid_adding_reviewers = False
429 c.forbid_author_to_review = False
430 c.forbid_author_to_review = False
430 c.forbid_commit_author_to_review = False
431 c.forbid_commit_author_to_review = False
431
432
432 if pull_request_latest.reviewer_data and \
433 if pull_request_latest.reviewer_data and \
433 'rules' in pull_request_latest.reviewer_data:
434 'rules' in pull_request_latest.reviewer_data:
434 rules = pull_request_latest.reviewer_data['rules'] or {}
435 rules = pull_request_latest.reviewer_data['rules'] or {}
435 try:
436 try:
436 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
437 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
437 c.forbid_author_to_review = rules.get('forbid_author_to_review')
438 c.forbid_author_to_review = rules.get('forbid_author_to_review')
438 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
439 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
439 except Exception:
440 except Exception:
440 pass
441 pass
441
442
442 # check merge capabilities
443 # check merge capabilities
443 _merge_check = MergeCheck.validate(
444 _merge_check = MergeCheck.validate(
444 pull_request_latest, auth_user=self._rhodecode_user,
445 pull_request_latest, auth_user=self._rhodecode_user,
445 translator=self.request.translate,
446 translator=self.request.translate,
446 force_shadow_repo_refresh=force_refresh)
447 force_shadow_repo_refresh=force_refresh)
447
448
448 c.pr_merge_errors = _merge_check.error_details
449 c.pr_merge_errors = _merge_check.error_details
449 c.pr_merge_possible = not _merge_check.failed
450 c.pr_merge_possible = not _merge_check.failed
450 c.pr_merge_message = _merge_check.merge_msg
451 c.pr_merge_message = _merge_check.merge_msg
451 c.pr_merge_source_commit = _merge_check.source_commit
452 c.pr_merge_source_commit = _merge_check.source_commit
452 c.pr_merge_target_commit = _merge_check.target_commit
453 c.pr_merge_target_commit = _merge_check.target_commit
453
454
454 c.pr_merge_info = MergeCheck.get_merge_conditions(
455 c.pr_merge_info = MergeCheck.get_merge_conditions(
455 pull_request_latest, translator=self.request.translate)
456 pull_request_latest, translator=self.request.translate)
456
457
457 c.pull_request_review_status = _merge_check.review_status
458 c.pull_request_review_status = _merge_check.review_status
458 if merge_checks:
459 if merge_checks:
459 self.request.override_renderer = \
460 self.request.override_renderer = \
460 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
461 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
461 return self._get_template_context(c)
462 return self._get_template_context(c)
462
463
463 c.reviewers_count = pull_request.reviewers_count
464 c.reviewers_count = pull_request.reviewers_count
464 c.observers_count = pull_request.observers_count
465 c.observers_count = pull_request.observers_count
465
466
466 # reviewers and statuses
467 # reviewers and statuses
467 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
468 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
468 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
469 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
469 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
470 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
470
471
471 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
472 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
472 member_reviewer = h.reviewer_as_json(
473 member_reviewer = h.reviewer_as_json(
473 member, reasons=reasons, mandatory=mandatory,
474 member, reasons=reasons, mandatory=mandatory,
474 role=review_obj.role,
475 role=review_obj.role,
475 user_group=review_obj.rule_user_group_data()
476 user_group=review_obj.rule_user_group_data()
476 )
477 )
477
478
478 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
479 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
479 member_reviewer['review_status'] = current_review_status
480 member_reviewer['review_status'] = current_review_status
480 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
481 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
481 member_reviewer['allowed_to_update'] = c.allowed_to_update
482 member_reviewer['allowed_to_update'] = c.allowed_to_update
482 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
483 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
483
484
484 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
485 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
485
486
486 for observer_obj, member in pull_request_at_ver.observers():
487 for observer_obj, member in pull_request_at_ver.observers():
487 member_observer = h.reviewer_as_json(
488 member_observer = h.reviewer_as_json(
488 member, reasons=[], mandatory=False,
489 member, reasons=[], mandatory=False,
489 role=observer_obj.role,
490 role=observer_obj.role,
490 user_group=observer_obj.rule_user_group_data()
491 user_group=observer_obj.rule_user_group_data()
491 )
492 )
492 member_observer['allowed_to_update'] = c.allowed_to_update
493 member_observer['allowed_to_update'] = c.allowed_to_update
493 c.pull_request_set_observers_data_json['observers'].append(member_observer)
494 c.pull_request_set_observers_data_json['observers'].append(member_observer)
494
495
495 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
496 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
496
497
497 general_comments, inline_comments = \
498 general_comments, inline_comments = \
498 self.register_comments_vars(c, pull_request_latest, versions)
499 self.register_comments_vars(c, pull_request_latest, versions)
499
500
500 # TODOs
501 # TODOs
501 c.unresolved_comments = CommentsModel() \
502 c.unresolved_comments = CommentsModel() \
502 .get_pull_request_unresolved_todos(pull_request_latest)
503 .get_pull_request_unresolved_todos(pull_request_latest)
503 c.resolved_comments = CommentsModel() \
504 c.resolved_comments = CommentsModel() \
504 .get_pull_request_resolved_todos(pull_request_latest)
505 .get_pull_request_resolved_todos(pull_request_latest)
505
506
506 # if we use version, then do not show later comments
507 # if we use version, then do not show later comments
507 # than current version
508 # than current version
508 display_inline_comments = collections.defaultdict(
509 display_inline_comments = collections.defaultdict(
509 lambda: collections.defaultdict(list))
510 lambda: collections.defaultdict(list))
510 for co in inline_comments:
511 for co in inline_comments:
511 if c.at_version_num:
512 if c.at_version_num:
512 # pick comments that are at least UPTO given version, so we
513 # pick comments that are at least UPTO given version, so we
513 # don't render comments for higher version
514 # don't render comments for higher version
514 should_render = co.pull_request_version_id and \
515 should_render = co.pull_request_version_id and \
515 co.pull_request_version_id <= c.at_version_num
516 co.pull_request_version_id <= c.at_version_num
516 else:
517 else:
517 # showing all, for 'latest'
518 # showing all, for 'latest'
518 should_render = True
519 should_render = True
519
520
520 if should_render:
521 if should_render:
521 display_inline_comments[co.f_path][co.line_no].append(co)
522 display_inline_comments[co.f_path][co.line_no].append(co)
522
523
523 # load diff data into template context, if we use compare mode then
524 # load diff data into template context, if we use compare mode then
524 # diff is calculated based on changes between versions of PR
525 # diff is calculated based on changes between versions of PR
525
526
526 source_repo = pull_request_at_ver.source_repo
527 source_repo = pull_request_at_ver.source_repo
527 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
528 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
528
529
529 target_repo = pull_request_at_ver.target_repo
530 target_repo = pull_request_at_ver.target_repo
530 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
531 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
531
532
532 if compare:
533 if compare:
533 # in compare switch the diff base to latest commit from prev version
534 # in compare switch the diff base to latest commit from prev version
534 target_ref_id = prev_pull_request_display_obj.revisions[0]
535 target_ref_id = prev_pull_request_display_obj.revisions[0]
535
536
536 # despite opening commits for bookmarks/branches/tags, we always
537 # despite opening commits for bookmarks/branches/tags, we always
537 # convert this to rev to prevent changes after bookmark or branch change
538 # convert this to rev to prevent changes after bookmark or branch change
538 c.source_ref_type = 'rev'
539 c.source_ref_type = 'rev'
539 c.source_ref = source_ref_id
540 c.source_ref = source_ref_id
540
541
541 c.target_ref_type = 'rev'
542 c.target_ref_type = 'rev'
542 c.target_ref = target_ref_id
543 c.target_ref = target_ref_id
543
544
544 c.source_repo = source_repo
545 c.source_repo = source_repo
545 c.target_repo = target_repo
546 c.target_repo = target_repo
546
547
547 c.commit_ranges = []
548 c.commit_ranges = []
548 source_commit = EmptyCommit()
549 source_commit = EmptyCommit()
549 target_commit = EmptyCommit()
550 target_commit = EmptyCommit()
550 c.missing_requirements = False
551 c.missing_requirements = False
551
552
552 source_scm = source_repo.scm_instance()
553 source_scm = source_repo.scm_instance()
553 target_scm = target_repo.scm_instance()
554 target_scm = target_repo.scm_instance()
554
555
555 shadow_scm = None
556 shadow_scm = None
556 try:
557 try:
557 shadow_scm = pull_request_latest.get_shadow_repo()
558 shadow_scm = pull_request_latest.get_shadow_repo()
558 except Exception:
559 except Exception:
559 log.debug('Failed to get shadow repo', exc_info=True)
560 log.debug('Failed to get shadow repo', exc_info=True)
560 # try first the existing source_repo, and then shadow
561 # try first the existing source_repo, and then shadow
561 # repo if we can obtain one
562 # repo if we can obtain one
562 commits_source_repo = source_scm
563 commits_source_repo = source_scm
563 if shadow_scm:
564 if shadow_scm:
564 commits_source_repo = shadow_scm
565 commits_source_repo = shadow_scm
565
566
566 c.commits_source_repo = commits_source_repo
567 c.commits_source_repo = commits_source_repo
567 c.ancestor = None # set it to None, to hide it from PR view
568 c.ancestor = None # set it to None, to hide it from PR view
568
569
569 # empty version means latest, so we keep this to prevent
570 # empty version means latest, so we keep this to prevent
570 # double caching
571 # double caching
571 version_normalized = version or PullRequest.LATEST_VER
572 version_normalized = version or PullRequest.LATEST_VER
572 from_version_normalized = from_version or PullRequest.LATEST_VER
573 from_version_normalized = from_version or PullRequest.LATEST_VER
573
574
574 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
575 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
575 cache_file_path = diff_cache_exist(
576 cache_file_path = diff_cache_exist(
576 cache_path, 'pull_request', pull_request_id, version_normalized,
577 cache_path, 'pull_request', pull_request_id, version_normalized,
577 from_version_normalized, source_ref_id, target_ref_id,
578 from_version_normalized, source_ref_id, target_ref_id,
578 hide_whitespace_changes, diff_context, c.fulldiff)
579 hide_whitespace_changes, diff_context, c.fulldiff)
579
580
580 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
581 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
581 force_recache = self.get_recache_flag()
582 force_recache = self.get_recache_flag()
582
583
583 cached_diff = None
584 cached_diff = None
584 if caching_enabled:
585 if caching_enabled:
585 cached_diff = load_cached_diff(cache_file_path)
586 cached_diff = load_cached_diff(cache_file_path)
586
587
587 has_proper_commit_cache = (
588 has_proper_commit_cache = (
588 cached_diff and cached_diff.get('commits')
589 cached_diff and cached_diff.get('commits')
589 and len(cached_diff.get('commits', [])) == 5
590 and len(cached_diff.get('commits', [])) == 5
590 and cached_diff.get('commits')[0]
591 and cached_diff.get('commits')[0]
591 and cached_diff.get('commits')[3])
592 and cached_diff.get('commits')[3])
592
593
593 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
594 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
594 diff_commit_cache = \
595 diff_commit_cache = \
595 (ancestor_commit, commit_cache, missing_requirements,
596 (ancestor_commit, commit_cache, missing_requirements,
596 source_commit, target_commit) = cached_diff['commits']
597 source_commit, target_commit) = cached_diff['commits']
597 else:
598 else:
598 # NOTE(marcink): we reach potentially unreachable errors when a PR has
599 # NOTE(marcink): we reach potentially unreachable errors when a PR has
599 # merge errors resulting in potentially hidden commits in the shadow repo.
600 # merge errors resulting in potentially hidden commits in the shadow repo.
600 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
601 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
601 and _merge_check.merge_response
602 and _merge_check.merge_response
602 maybe_unreachable = maybe_unreachable \
603 maybe_unreachable = maybe_unreachable \
603 and _merge_check.merge_response.metadata.get('unresolved_files')
604 and _merge_check.merge_response.metadata.get('unresolved_files')
604 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
605 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
605 diff_commit_cache = \
606 diff_commit_cache = \
606 (ancestor_commit, commit_cache, missing_requirements,
607 (ancestor_commit, commit_cache, missing_requirements,
607 source_commit, target_commit) = self.get_commits(
608 source_commit, target_commit) = self.get_commits(
608 commits_source_repo,
609 commits_source_repo,
609 pull_request_at_ver,
610 pull_request_at_ver,
610 source_commit,
611 source_commit,
611 source_ref_id,
612 source_ref_id,
612 source_scm,
613 source_scm,
613 target_commit,
614 target_commit,
614 target_ref_id,
615 target_ref_id,
615 target_scm,
616 target_scm,
616 maybe_unreachable=maybe_unreachable)
617 maybe_unreachable=maybe_unreachable)
617
618
618 # register our commit range
619 # register our commit range
619 for comm in commit_cache.values():
620 for comm in commit_cache.values():
620 c.commit_ranges.append(comm)
621 c.commit_ranges.append(comm)
621
622
622 c.missing_requirements = missing_requirements
623 c.missing_requirements = missing_requirements
623 c.ancestor_commit = ancestor_commit
624 c.ancestor_commit = ancestor_commit
624 c.statuses = source_repo.statuses(
625 c.statuses = source_repo.statuses(
625 [x.raw_id for x in c.commit_ranges])
626 [x.raw_id for x in c.commit_ranges])
626
627
627 # auto collapse if we have more than limit
628 # auto collapse if we have more than limit
628 collapse_limit = diffs.DiffProcessor._collapse_commits_over
629 collapse_limit = diffs.DiffProcessor._collapse_commits_over
629 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
630 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
630 c.compare_mode = compare
631 c.compare_mode = compare
631
632
632 # diff_limit is the old behavior, will cut off the whole diff
633 # diff_limit is the old behavior, will cut off the whole diff
633 # if the limit is applied otherwise will just hide the
634 # if the limit is applied otherwise will just hide the
634 # big files from the front-end
635 # big files from the front-end
635 diff_limit = c.visual.cut_off_limit_diff
636 diff_limit = c.visual.cut_off_limit_diff
636 file_limit = c.visual.cut_off_limit_file
637 file_limit = c.visual.cut_off_limit_file
637
638
638 c.missing_commits = False
639 c.missing_commits = False
639 if (c.missing_requirements
640 if (c.missing_requirements
640 or isinstance(source_commit, EmptyCommit)
641 or isinstance(source_commit, EmptyCommit)
641 or source_commit == target_commit):
642 or source_commit == target_commit):
642
643
643 c.missing_commits = True
644 c.missing_commits = True
644 else:
645 else:
645 c.inline_comments = display_inline_comments
646 c.inline_comments = display_inline_comments
646
647
647 use_ancestor = True
648 use_ancestor = True
648 if from_version_normalized != version_normalized:
649 if from_version_normalized != version_normalized:
649 use_ancestor = False
650 use_ancestor = False
650
651
651 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
652 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
652 if not force_recache and has_proper_diff_cache:
653 if not force_recache and has_proper_diff_cache:
653 c.diffset = cached_diff['diff']
654 c.diffset = cached_diff['diff']
654 else:
655 else:
655 try:
656 try:
656 c.diffset = self._get_diffset(
657 c.diffset = self._get_diffset(
657 c.source_repo.repo_name, commits_source_repo,
658 c.source_repo.repo_name, commits_source_repo,
658 c.ancestor_commit,
659 c.ancestor_commit,
659 source_ref_id, target_ref_id,
660 source_ref_id, target_ref_id,
660 target_commit, source_commit,
661 target_commit, source_commit,
661 diff_limit, file_limit, c.fulldiff,
662 diff_limit, file_limit, c.fulldiff,
662 hide_whitespace_changes, diff_context,
663 hide_whitespace_changes, diff_context,
663 use_ancestor=use_ancestor
664 use_ancestor=use_ancestor
664 )
665 )
665
666
666 # save cached diff
667 # save cached diff
667 if caching_enabled:
668 if caching_enabled:
668 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
669 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
669 except CommitDoesNotExistError:
670 except CommitDoesNotExistError:
670 log.exception('Failed to generate diffset')
671 log.exception('Failed to generate diffset')
671 c.missing_commits = True
672 c.missing_commits = True
672
673
673 if not c.missing_commits:
674 if not c.missing_commits:
674
675
675 c.limited_diff = c.diffset.limited_diff
676 c.limited_diff = c.diffset.limited_diff
676
677
677 # calculate removed files that are bound to comments
678 # calculate removed files that are bound to comments
678 comment_deleted_files = [
679 comment_deleted_files = [
679 fname for fname in display_inline_comments
680 fname for fname in display_inline_comments
680 if fname not in c.diffset.file_stats]
681 if fname not in c.diffset.file_stats]
681
682
682 c.deleted_files_comments = collections.defaultdict(dict)
683 c.deleted_files_comments = collections.defaultdict(dict)
683 for fname, per_line_comments in display_inline_comments.items():
684 for fname, per_line_comments in display_inline_comments.items():
684 if fname in comment_deleted_files:
685 if fname in comment_deleted_files:
685 c.deleted_files_comments[fname]['stats'] = 0
686 c.deleted_files_comments[fname]['stats'] = 0
686 c.deleted_files_comments[fname]['comments'] = list()
687 c.deleted_files_comments[fname]['comments'] = list()
687 for lno, comments in per_line_comments.items():
688 for lno, comments in per_line_comments.items():
688 c.deleted_files_comments[fname]['comments'].extend(comments)
689 c.deleted_files_comments[fname]['comments'].extend(comments)
689
690
690 # maybe calculate the range diff
691 # maybe calculate the range diff
691 if c.range_diff_on:
692 if c.range_diff_on:
692 # TODO(marcink): set whitespace/context
693 # TODO(marcink): set whitespace/context
693 context_lcl = 3
694 context_lcl = 3
694 ign_whitespace_lcl = False
695 ign_whitespace_lcl = False
695
696
696 for commit in c.commit_ranges:
697 for commit in c.commit_ranges:
697 commit2 = commit
698 commit2 = commit
698 commit1 = commit.first_parent
699 commit1 = commit.first_parent
699
700
700 range_diff_cache_file_path = diff_cache_exist(
701 range_diff_cache_file_path = diff_cache_exist(
701 cache_path, 'diff', commit.raw_id,
702 cache_path, 'diff', commit.raw_id,
702 ign_whitespace_lcl, context_lcl, c.fulldiff)
703 ign_whitespace_lcl, context_lcl, c.fulldiff)
703
704
704 cached_diff = None
705 cached_diff = None
705 if caching_enabled:
706 if caching_enabled:
706 cached_diff = load_cached_diff(range_diff_cache_file_path)
707 cached_diff = load_cached_diff(range_diff_cache_file_path)
707
708
708 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
709 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
709 if not force_recache and has_proper_diff_cache:
710 if not force_recache and has_proper_diff_cache:
710 diffset = cached_diff['diff']
711 diffset = cached_diff['diff']
711 else:
712 else:
712 diffset = self._get_range_diffset(
713 diffset = self._get_range_diffset(
713 commits_source_repo, source_repo,
714 commits_source_repo, source_repo,
714 commit1, commit2, diff_limit, file_limit,
715 commit1, commit2, diff_limit, file_limit,
715 c.fulldiff, ign_whitespace_lcl, context_lcl
716 c.fulldiff, ign_whitespace_lcl, context_lcl
716 )
717 )
717
718
718 # save cached diff
719 # save cached diff
719 if caching_enabled:
720 if caching_enabled:
720 cache_diff(range_diff_cache_file_path, diffset, None)
721 cache_diff(range_diff_cache_file_path, diffset, None)
721
722
722 c.changes[commit.raw_id] = diffset
723 c.changes[commit.raw_id] = diffset
723
724
724 # this is a hack to properly display links, when creating PR, the
725 # this is a hack to properly display links, when creating PR, the
725 # compare view and others uses different notation, and
726 # compare view and others uses different notation, and
726 # compare_commits.mako renders links based on the target_repo.
727 # compare_commits.mako renders links based on the target_repo.
727 # We need to swap that here to generate it properly on the html side
728 # We need to swap that here to generate it properly on the html side
728 c.target_repo = c.source_repo
729 c.target_repo = c.source_repo
729
730
730 c.commit_statuses = ChangesetStatus.STATUSES
731 c.commit_statuses = ChangesetStatus.STATUSES
731
732
732 c.show_version_changes = not pr_closed
733 c.show_version_changes = not pr_closed
733 if c.show_version_changes:
734 if c.show_version_changes:
734 cur_obj = pull_request_at_ver
735 cur_obj = pull_request_at_ver
735 prev_obj = prev_pull_request_at_ver
736 prev_obj = prev_pull_request_at_ver
736
737
737 old_commit_ids = prev_obj.revisions
738 old_commit_ids = prev_obj.revisions
738 new_commit_ids = cur_obj.revisions
739 new_commit_ids = cur_obj.revisions
739 commit_changes = PullRequestModel()._calculate_commit_id_changes(
740 commit_changes = PullRequestModel()._calculate_commit_id_changes(
740 old_commit_ids, new_commit_ids)
741 old_commit_ids, new_commit_ids)
741 c.commit_changes_summary = commit_changes
742 c.commit_changes_summary = commit_changes
742
743
743 # calculate the diff for commits between versions
744 # calculate the diff for commits between versions
744 c.commit_changes = []
745 c.commit_changes = []
745
746
746 def mark(cs, fw):
747 def mark(cs, fw):
747 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
748 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
748
749
749 for c_type, raw_id in mark(commit_changes.added, 'a') \
750 for c_type, raw_id in mark(commit_changes.added, 'a') \
750 + mark(commit_changes.removed, 'r') \
751 + mark(commit_changes.removed, 'r') \
751 + mark(commit_changes.common, 'c'):
752 + mark(commit_changes.common, 'c'):
752
753
753 if raw_id in commit_cache:
754 if raw_id in commit_cache:
754 commit = commit_cache[raw_id]
755 commit = commit_cache[raw_id]
755 else:
756 else:
756 try:
757 try:
757 commit = commits_source_repo.get_commit(raw_id)
758 commit = commits_source_repo.get_commit(raw_id)
758 except CommitDoesNotExistError:
759 except CommitDoesNotExistError:
759 # in case we fail extracting still use "dummy" commit
760 # in case we fail extracting still use "dummy" commit
760 # for display in commit diff
761 # for display in commit diff
761 commit = h.AttributeDict(
762 commit = h.AttributeDict(
762 {'raw_id': raw_id,
763 {'raw_id': raw_id,
763 'message': 'EMPTY or MISSING COMMIT'})
764 'message': 'EMPTY or MISSING COMMIT'})
764 c.commit_changes.append([c_type, commit])
765 c.commit_changes.append([c_type, commit])
765
766
766 # current user review statuses for each version
767 # current user review statuses for each version
767 c.review_versions = {}
768 c.review_versions = {}
768 is_reviewer = PullRequestModel().is_user_reviewer(
769 is_reviewer = PullRequestModel().is_user_reviewer(
769 pull_request, self._rhodecode_user)
770 pull_request, self._rhodecode_user)
770 if is_reviewer:
771 if is_reviewer:
771 for co in general_comments:
772 for co in general_comments:
772 if co.author.user_id == self._rhodecode_user.user_id:
773 if co.author.user_id == self._rhodecode_user.user_id:
773 status = co.status_change
774 status = co.status_change
774 if status:
775 if status:
775 _ver_pr = status[0].comment.pull_request_version_id
776 _ver_pr = status[0].comment.pull_request_version_id
776 c.review_versions[_ver_pr] = status[0]
777 c.review_versions[_ver_pr] = status[0]
777
778
778 return self._get_template_context(c)
779 return self._get_template_context(c)
779
780
780 def get_commits(
781 def get_commits(
781 self, commits_source_repo, pull_request_at_ver, source_commit,
782 self, commits_source_repo, pull_request_at_ver, source_commit,
782 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
783 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
783 maybe_unreachable=False):
784 maybe_unreachable=False):
784
785
785 commit_cache = collections.OrderedDict()
786 commit_cache = collections.OrderedDict()
786 missing_requirements = False
787 missing_requirements = False
787
788
788 try:
789 try:
789 pre_load = ["author", "date", "message", "branch", "parents"]
790 pre_load = ["author", "date", "message", "branch", "parents"]
790
791
791 pull_request_commits = pull_request_at_ver.revisions
792 pull_request_commits = pull_request_at_ver.revisions
792 log.debug('Loading %s commits from %s',
793 log.debug('Loading %s commits from %s',
793 len(pull_request_commits), commits_source_repo)
794 len(pull_request_commits), commits_source_repo)
794
795
795 for rev in pull_request_commits:
796 for rev in pull_request_commits:
796 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
797 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
797 maybe_unreachable=maybe_unreachable)
798 maybe_unreachable=maybe_unreachable)
798 commit_cache[comm.raw_id] = comm
799 commit_cache[comm.raw_id] = comm
799
800
800 # Order here matters, we first need to get target, and then
801 # Order here matters, we first need to get target, and then
801 # the source
802 # the source
802 target_commit = commits_source_repo.get_commit(
803 target_commit = commits_source_repo.get_commit(
803 commit_id=safe_str(target_ref_id))
804 commit_id=safe_str(target_ref_id))
804
805
805 source_commit = commits_source_repo.get_commit(
806 source_commit = commits_source_repo.get_commit(
806 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
807 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
807 except CommitDoesNotExistError:
808 except CommitDoesNotExistError:
808 log.warning('Failed to get commit from `{}` repo'.format(
809 log.warning('Failed to get commit from `{}` repo'.format(
809 commits_source_repo), exc_info=True)
810 commits_source_repo), exc_info=True)
810 except RepositoryRequirementError:
811 except RepositoryRequirementError:
811 log.warning('Failed to get all required data from repo', exc_info=True)
812 log.warning('Failed to get all required data from repo', exc_info=True)
812 missing_requirements = True
813 missing_requirements = True
813
814
814 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
815 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
815
816
816 try:
817 try:
817 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
818 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
818 except Exception:
819 except Exception:
819 ancestor_commit = None
820 ancestor_commit = None
820
821
821 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
822 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
822
823
823 def assure_not_empty_repo(self):
824 def assure_not_empty_repo(self):
824 _ = self.request.translate
825 _ = self.request.translate
825
826
826 try:
827 try:
827 self.db_repo.scm_instance().get_commit()
828 self.db_repo.scm_instance().get_commit()
828 except EmptyRepositoryError:
829 except EmptyRepositoryError:
829 h.flash(h.literal(_('There are no commits yet')),
830 h.flash(h.literal(_('There are no commits yet')),
830 category='warning')
831 category='warning')
831 raise HTTPFound(
832 raise HTTPFound(
832 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
833 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
833
834
834 @LoginRequired()
835 @LoginRequired()
835 @NotAnonymous()
836 @NotAnonymous()
836 @HasRepoPermissionAnyDecorator(
837 @HasRepoPermissionAnyDecorator(
837 'repository.read', 'repository.write', 'repository.admin')
838 'repository.read', 'repository.write', 'repository.admin')
838 @view_config(
839 @view_config(
839 route_name='pullrequest_new', request_method='GET',
840 route_name='pullrequest_new', request_method='GET',
840 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
841 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
841 def pull_request_new(self):
842 def pull_request_new(self):
842 _ = self.request.translate
843 _ = self.request.translate
843 c = self.load_default_context()
844 c = self.load_default_context()
844
845
845 self.assure_not_empty_repo()
846 self.assure_not_empty_repo()
846 source_repo = self.db_repo
847 source_repo = self.db_repo
847
848
848 commit_id = self.request.GET.get('commit')
849 commit_id = self.request.GET.get('commit')
849 branch_ref = self.request.GET.get('branch')
850 branch_ref = self.request.GET.get('branch')
850 bookmark_ref = self.request.GET.get('bookmark')
851 bookmark_ref = self.request.GET.get('bookmark')
851
852
852 try:
853 try:
853 source_repo_data = PullRequestModel().generate_repo_data(
854 source_repo_data = PullRequestModel().generate_repo_data(
854 source_repo, commit_id=commit_id,
855 source_repo, commit_id=commit_id,
855 branch=branch_ref, bookmark=bookmark_ref,
856 branch=branch_ref, bookmark=bookmark_ref,
856 translator=self.request.translate)
857 translator=self.request.translate)
857 except CommitDoesNotExistError as e:
858 except CommitDoesNotExistError as e:
858 log.exception(e)
859 log.exception(e)
859 h.flash(_('Commit does not exist'), 'error')
860 h.flash(_('Commit does not exist'), 'error')
860 raise HTTPFound(
861 raise HTTPFound(
861 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
862 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
862
863
863 default_target_repo = source_repo
864 default_target_repo = source_repo
864
865
865 if source_repo.parent and c.has_origin_repo_read_perm:
866 if source_repo.parent and c.has_origin_repo_read_perm:
866 parent_vcs_obj = source_repo.parent.scm_instance()
867 parent_vcs_obj = source_repo.parent.scm_instance()
867 if parent_vcs_obj and not parent_vcs_obj.is_empty():
868 if parent_vcs_obj and not parent_vcs_obj.is_empty():
868 # change default if we have a parent repo
869 # change default if we have a parent repo
869 default_target_repo = source_repo.parent
870 default_target_repo = source_repo.parent
870
871
871 target_repo_data = PullRequestModel().generate_repo_data(
872 target_repo_data = PullRequestModel().generate_repo_data(
872 default_target_repo, translator=self.request.translate)
873 default_target_repo, translator=self.request.translate)
873
874
874 selected_source_ref = source_repo_data['refs']['selected_ref']
875 selected_source_ref = source_repo_data['refs']['selected_ref']
875 title_source_ref = ''
876 title_source_ref = ''
876 if selected_source_ref:
877 if selected_source_ref:
877 title_source_ref = selected_source_ref.split(':', 2)[1]
878 title_source_ref = selected_source_ref.split(':', 2)[1]
878 c.default_title = PullRequestModel().generate_pullrequest_title(
879 c.default_title = PullRequestModel().generate_pullrequest_title(
879 source=source_repo.repo_name,
880 source=source_repo.repo_name,
880 source_ref=title_source_ref,
881 source_ref=title_source_ref,
881 target=default_target_repo.repo_name
882 target=default_target_repo.repo_name
882 )
883 )
883
884
884 c.default_repo_data = {
885 c.default_repo_data = {
885 'source_repo_name': source_repo.repo_name,
886 'source_repo_name': source_repo.repo_name,
886 'source_refs_json': json.dumps(source_repo_data),
887 'source_refs_json': json.dumps(source_repo_data),
887 'target_repo_name': default_target_repo.repo_name,
888 'target_repo_name': default_target_repo.repo_name,
888 'target_refs_json': json.dumps(target_repo_data),
889 'target_refs_json': json.dumps(target_repo_data),
889 }
890 }
890 c.default_source_ref = selected_source_ref
891 c.default_source_ref = selected_source_ref
891
892
892 return self._get_template_context(c)
893 return self._get_template_context(c)
893
894
894 @LoginRequired()
895 @LoginRequired()
895 @NotAnonymous()
896 @NotAnonymous()
896 @HasRepoPermissionAnyDecorator(
897 @HasRepoPermissionAnyDecorator(
897 'repository.read', 'repository.write', 'repository.admin')
898 'repository.read', 'repository.write', 'repository.admin')
898 @view_config(
899 @view_config(
899 route_name='pullrequest_repo_refs', request_method='GET',
900 route_name='pullrequest_repo_refs', request_method='GET',
900 renderer='json_ext', xhr=True)
901 renderer='json_ext', xhr=True)
901 def pull_request_repo_refs(self):
902 def pull_request_repo_refs(self):
902 self.load_default_context()
903 self.load_default_context()
903 target_repo_name = self.request.matchdict['target_repo_name']
904 target_repo_name = self.request.matchdict['target_repo_name']
904 repo = Repository.get_by_repo_name(target_repo_name)
905 repo = Repository.get_by_repo_name(target_repo_name)
905 if not repo:
906 if not repo:
906 raise HTTPNotFound()
907 raise HTTPNotFound()
907
908
908 target_perm = HasRepoPermissionAny(
909 target_perm = HasRepoPermissionAny(
909 'repository.read', 'repository.write', 'repository.admin')(
910 'repository.read', 'repository.write', 'repository.admin')(
910 target_repo_name)
911 target_repo_name)
911 if not target_perm:
912 if not target_perm:
912 raise HTTPNotFound()
913 raise HTTPNotFound()
913
914
914 return PullRequestModel().generate_repo_data(
915 return PullRequestModel().generate_repo_data(
915 repo, translator=self.request.translate)
916 repo, translator=self.request.translate)
916
917
917 @LoginRequired()
918 @LoginRequired()
918 @NotAnonymous()
919 @NotAnonymous()
919 @HasRepoPermissionAnyDecorator(
920 @HasRepoPermissionAnyDecorator(
920 'repository.read', 'repository.write', 'repository.admin')
921 'repository.read', 'repository.write', 'repository.admin')
921 @view_config(
922 @view_config(
922 route_name='pullrequest_repo_targets', request_method='GET',
923 route_name='pullrequest_repo_targets', request_method='GET',
923 renderer='json_ext', xhr=True)
924 renderer='json_ext', xhr=True)
924 def pullrequest_repo_targets(self):
925 def pullrequest_repo_targets(self):
925 _ = self.request.translate
926 _ = self.request.translate
926 filter_query = self.request.GET.get('query')
927 filter_query = self.request.GET.get('query')
927
928
928 # get the parents
929 # get the parents
929 parent_target_repos = []
930 parent_target_repos = []
930 if self.db_repo.parent:
931 if self.db_repo.parent:
931 parents_query = Repository.query() \
932 parents_query = Repository.query() \
932 .order_by(func.length(Repository.repo_name)) \
933 .order_by(func.length(Repository.repo_name)) \
933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
934 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
934
935
935 if filter_query:
936 if filter_query:
936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
937 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
937 parents_query = parents_query.filter(
938 parents_query = parents_query.filter(
938 Repository.repo_name.ilike(ilike_expression))
939 Repository.repo_name.ilike(ilike_expression))
939 parents = parents_query.limit(20).all()
940 parents = parents_query.limit(20).all()
940
941
941 for parent in parents:
942 for parent in parents:
942 parent_vcs_obj = parent.scm_instance()
943 parent_vcs_obj = parent.scm_instance()
943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
944 if parent_vcs_obj and not parent_vcs_obj.is_empty():
944 parent_target_repos.append(parent)
945 parent_target_repos.append(parent)
945
946
946 # get other forks, and repo itself
947 # get other forks, and repo itself
947 query = Repository.query() \
948 query = Repository.query() \
948 .order_by(func.length(Repository.repo_name)) \
949 .order_by(func.length(Repository.repo_name)) \
949 .filter(
950 .filter(
950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
951 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
952 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
952 ) \
953 ) \
953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
954 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
954
955
955 if filter_query:
956 if filter_query:
956 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
957 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
958 query = query.filter(Repository.repo_name.ilike(ilike_expression))
958
959
959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
960 limit = max(20 - len(parent_target_repos), 5) # not less then 5
960 target_repos = query.limit(limit).all()
961 target_repos = query.limit(limit).all()
961
962
962 all_target_repos = target_repos + parent_target_repos
963 all_target_repos = target_repos + parent_target_repos
963
964
964 repos = []
965 repos = []
965 # This checks permissions to the repositories
966 # This checks permissions to the repositories
966 for obj in ScmModel().get_repos(all_target_repos):
967 for obj in ScmModel().get_repos(all_target_repos):
967 repos.append({
968 repos.append({
968 'id': obj['name'],
969 'id': obj['name'],
969 'text': obj['name'],
970 'text': obj['name'],
970 'type': 'repo',
971 'type': 'repo',
971 'repo_id': obj['dbrepo']['repo_id'],
972 'repo_id': obj['dbrepo']['repo_id'],
972 'repo_type': obj['dbrepo']['repo_type'],
973 'repo_type': obj['dbrepo']['repo_type'],
973 'private': obj['dbrepo']['private'],
974 'private': obj['dbrepo']['private'],
974
975
975 })
976 })
976
977
977 data = {
978 data = {
978 'more': False,
979 'more': False,
979 'results': [{
980 'results': [{
980 'text': _('Repositories'),
981 'text': _('Repositories'),
981 'children': repos
982 'children': repos
982 }] if repos else []
983 }] if repos else []
983 }
984 }
984 return data
985 return data
985
986
986 @classmethod
987 @classmethod
987 def get_comment_ids(cls, post_data):
988 def get_comment_ids(cls, post_data):
988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
989 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
989
990
990 @LoginRequired()
991 @LoginRequired()
991 @NotAnonymous()
992 @NotAnonymous()
992 @HasRepoPermissionAnyDecorator(
993 @HasRepoPermissionAnyDecorator(
993 'repository.read', 'repository.write', 'repository.admin')
994 'repository.read', 'repository.write', 'repository.admin')
994 @view_config(
995 @view_config(
995 route_name='pullrequest_comments', request_method='POST',
996 route_name='pullrequest_comments', request_method='POST',
996 renderer='string_html', xhr=True)
997 renderer='string_html', xhr=True)
997 def pullrequest_comments(self):
998 def pullrequest_comments(self):
998 self.load_default_context()
999 self.load_default_context()
999
1000
1000 pull_request = PullRequest.get_or_404(
1001 pull_request = PullRequest.get_or_404(
1001 self.request.matchdict['pull_request_id'])
1002 self.request.matchdict['pull_request_id'])
1002 pull_request_id = pull_request.pull_request_id
1003 pull_request_id = pull_request.pull_request_id
1003 version = self.request.GET.get('version')
1004 version = self.request.GET.get('version')
1004
1005
1005 _render = self.request.get_partial_renderer(
1006 _render = self.request.get_partial_renderer(
1006 'rhodecode:templates/base/sidebar.mako')
1007 'rhodecode:templates/base/sidebar.mako')
1007 c = _render.get_call_context()
1008 c = _render.get_call_context()
1008
1009
1009 (pull_request_latest,
1010 (pull_request_latest,
1010 pull_request_at_ver,
1011 pull_request_at_ver,
1011 pull_request_display_obj,
1012 pull_request_display_obj,
1012 at_version) = PullRequestModel().get_pr_version(
1013 at_version) = PullRequestModel().get_pr_version(
1013 pull_request_id, version=version)
1014 pull_request_id, version=version)
1014 versions = pull_request_display_obj.versions()
1015 versions = pull_request_display_obj.versions()
1015 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1016 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1016 c.versions = versions + [latest_ver]
1017 c.versions = versions + [latest_ver]
1017
1018
1018 c.at_version = at_version
1019 c.at_version = at_version
1019 c.at_version_num = (at_version
1020 c.at_version_num = (at_version
1020 if at_version and at_version != PullRequest.LATEST_VER
1021 if at_version and at_version != PullRequest.LATEST_VER
1021 else None)
1022 else None)
1022
1023
1023 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1024 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1024 all_comments = c.inline_comments_flat + c.comments
1025 all_comments = c.inline_comments_flat + c.comments
1025
1026
1026 existing_ids = self.get_comment_ids(self.request.POST)
1027 existing_ids = self.get_comment_ids(self.request.POST)
1027 return _render('comments_table', all_comments, len(all_comments),
1028 return _render('comments_table', all_comments, len(all_comments),
1028 existing_ids=existing_ids)
1029 existing_ids=existing_ids)
1029
1030
1030 @LoginRequired()
1031 @LoginRequired()
1031 @NotAnonymous()
1032 @NotAnonymous()
1032 @HasRepoPermissionAnyDecorator(
1033 @HasRepoPermissionAnyDecorator(
1033 'repository.read', 'repository.write', 'repository.admin')
1034 'repository.read', 'repository.write', 'repository.admin')
1034 @view_config(
1035 @view_config(
1035 route_name='pullrequest_todos', request_method='POST',
1036 route_name='pullrequest_todos', request_method='POST',
1036 renderer='string_html', xhr=True)
1037 renderer='string_html', xhr=True)
1037 def pullrequest_todos(self):
1038 def pullrequest_todos(self):
1038 self.load_default_context()
1039 self.load_default_context()
1039
1040
1040 pull_request = PullRequest.get_or_404(
1041 pull_request = PullRequest.get_or_404(
1041 self.request.matchdict['pull_request_id'])
1042 self.request.matchdict['pull_request_id'])
1042 pull_request_id = pull_request.pull_request_id
1043 pull_request_id = pull_request.pull_request_id
1043 version = self.request.GET.get('version')
1044 version = self.request.GET.get('version')
1044
1045
1045 _render = self.request.get_partial_renderer(
1046 _render = self.request.get_partial_renderer(
1046 'rhodecode:templates/base/sidebar.mako')
1047 'rhodecode:templates/base/sidebar.mako')
1047 c = _render.get_call_context()
1048 c = _render.get_call_context()
1048 (pull_request_latest,
1049 (pull_request_latest,
1049 pull_request_at_ver,
1050 pull_request_at_ver,
1050 pull_request_display_obj,
1051 pull_request_display_obj,
1051 at_version) = PullRequestModel().get_pr_version(
1052 at_version) = PullRequestModel().get_pr_version(
1052 pull_request_id, version=version)
1053 pull_request_id, version=version)
1053 versions = pull_request_display_obj.versions()
1054 versions = pull_request_display_obj.versions()
1054 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1055 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1055 c.versions = versions + [latest_ver]
1056 c.versions = versions + [latest_ver]
1056
1057
1057 c.at_version = at_version
1058 c.at_version = at_version
1058 c.at_version_num = (at_version
1059 c.at_version_num = (at_version
1059 if at_version and at_version != PullRequest.LATEST_VER
1060 if at_version and at_version != PullRequest.LATEST_VER
1060 else None)
1061 else None)
1061
1062
1062 c.unresolved_comments = CommentsModel() \
1063 c.unresolved_comments = CommentsModel() \
1063 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1064 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1064 c.resolved_comments = CommentsModel() \
1065 c.resolved_comments = CommentsModel() \
1065 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1066 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1066
1067
1067 all_comments = c.unresolved_comments + c.resolved_comments
1068 all_comments = c.unresolved_comments + c.resolved_comments
1068 existing_ids = self.get_comment_ids(self.request.POST)
1069 existing_ids = self.get_comment_ids(self.request.POST)
1069 return _render('comments_table', all_comments, len(c.unresolved_comments),
1070 return _render('comments_table', all_comments, len(c.unresolved_comments),
1070 todo_comments=True, existing_ids=existing_ids)
1071 todo_comments=True, existing_ids=existing_ids)
1071
1072
1072 @LoginRequired()
1073 @LoginRequired()
1073 @NotAnonymous()
1074 @NotAnonymous()
1074 @HasRepoPermissionAnyDecorator(
1075 @HasRepoPermissionAnyDecorator(
1075 'repository.read', 'repository.write', 'repository.admin')
1076 'repository.read', 'repository.write', 'repository.admin')
1076 @CSRFRequired()
1077 @CSRFRequired()
1077 @view_config(
1078 @view_config(
1078 route_name='pullrequest_create', request_method='POST',
1079 route_name='pullrequest_create', request_method='POST',
1079 renderer=None)
1080 renderer=None)
1080 def pull_request_create(self):
1081 def pull_request_create(self):
1081 _ = self.request.translate
1082 _ = self.request.translate
1082 self.assure_not_empty_repo()
1083 self.assure_not_empty_repo()
1083 self.load_default_context()
1084 self.load_default_context()
1084
1085
1085 controls = peppercorn.parse(self.request.POST.items())
1086 controls = peppercorn.parse(self.request.POST.items())
1086
1087
1087 try:
1088 try:
1088 form = PullRequestForm(
1089 form = PullRequestForm(
1089 self.request.translate, self.db_repo.repo_id)()
1090 self.request.translate, self.db_repo.repo_id)()
1090 _form = form.to_python(controls)
1091 _form = form.to_python(controls)
1091 except formencode.Invalid as errors:
1092 except formencode.Invalid as errors:
1092 if errors.error_dict.get('revisions'):
1093 if errors.error_dict.get('revisions'):
1093 msg = 'Revisions: %s' % errors.error_dict['revisions']
1094 msg = 'Revisions: %s' % errors.error_dict['revisions']
1094 elif errors.error_dict.get('pullrequest_title'):
1095 elif errors.error_dict.get('pullrequest_title'):
1095 msg = errors.error_dict.get('pullrequest_title')
1096 msg = errors.error_dict.get('pullrequest_title')
1096 else:
1097 else:
1097 msg = _('Error creating pull request: {}').format(errors)
1098 msg = _('Error creating pull request: {}').format(errors)
1098 log.exception(msg)
1099 log.exception(msg)
1099 h.flash(msg, 'error')
1100 h.flash(msg, 'error')
1100
1101
1101 # would rather just go back to form ...
1102 # would rather just go back to form ...
1102 raise HTTPFound(
1103 raise HTTPFound(
1103 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1104 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1104
1105
1105 source_repo = _form['source_repo']
1106 source_repo = _form['source_repo']
1106 source_ref = _form['source_ref']
1107 source_ref = _form['source_ref']
1107 target_repo = _form['target_repo']
1108 target_repo = _form['target_repo']
1108 target_ref = _form['target_ref']
1109 target_ref = _form['target_ref']
1109 commit_ids = _form['revisions'][::-1]
1110 commit_ids = _form['revisions'][::-1]
1110 common_ancestor_id = _form['common_ancestor']
1111 common_ancestor_id = _form['common_ancestor']
1111
1112
1112 # find the ancestor for this pr
1113 # find the ancestor for this pr
1113 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1114 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1114 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1115 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1115
1116
1116 if not (source_db_repo or target_db_repo):
1117 if not (source_db_repo or target_db_repo):
1117 h.flash(_('source_repo or target repo not found'), category='error')
1118 h.flash(_('source_repo or target repo not found'), category='error')
1118 raise HTTPFound(
1119 raise HTTPFound(
1119 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1120 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1120
1121
1121 # re-check permissions again here
1122 # re-check permissions again here
1122 # source_repo we must have read permissions
1123 # source_repo we must have read permissions
1123
1124
1124 source_perm = HasRepoPermissionAny(
1125 source_perm = HasRepoPermissionAny(
1125 'repository.read', 'repository.write', 'repository.admin')(
1126 'repository.read', 'repository.write', 'repository.admin')(
1126 source_db_repo.repo_name)
1127 source_db_repo.repo_name)
1127 if not source_perm:
1128 if not source_perm:
1128 msg = _('Not Enough permissions to source repo `{}`.'.format(
1129 msg = _('Not Enough permissions to source repo `{}`.'.format(
1129 source_db_repo.repo_name))
1130 source_db_repo.repo_name))
1130 h.flash(msg, category='error')
1131 h.flash(msg, category='error')
1131 # copy the args back to redirect
1132 # copy the args back to redirect
1132 org_query = self.request.GET.mixed()
1133 org_query = self.request.GET.mixed()
1133 raise HTTPFound(
1134 raise HTTPFound(
1134 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1135 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1135 _query=org_query))
1136 _query=org_query))
1136
1137
1137 # target repo we must have read permissions, and also later on
1138 # target repo we must have read permissions, and also later on
1138 # we want to check branch permissions here
1139 # we want to check branch permissions here
1139 target_perm = HasRepoPermissionAny(
1140 target_perm = HasRepoPermissionAny(
1140 'repository.read', 'repository.write', 'repository.admin')(
1141 'repository.read', 'repository.write', 'repository.admin')(
1141 target_db_repo.repo_name)
1142 target_db_repo.repo_name)
1142 if not target_perm:
1143 if not target_perm:
1143 msg = _('Not Enough permissions to target repo `{}`.'.format(
1144 msg = _('Not Enough permissions to target repo `{}`.'.format(
1144 target_db_repo.repo_name))
1145 target_db_repo.repo_name))
1145 h.flash(msg, category='error')
1146 h.flash(msg, category='error')
1146 # copy the args back to redirect
1147 # copy the args back to redirect
1147 org_query = self.request.GET.mixed()
1148 org_query = self.request.GET.mixed()
1148 raise HTTPFound(
1149 raise HTTPFound(
1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1150 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1150 _query=org_query))
1151 _query=org_query))
1151
1152
1152 source_scm = source_db_repo.scm_instance()
1153 source_scm = source_db_repo.scm_instance()
1153 target_scm = target_db_repo.scm_instance()
1154 target_scm = target_db_repo.scm_instance()
1154
1155
1155 source_ref_obj = unicode_to_reference(source_ref)
1156 source_ref_obj = unicode_to_reference(source_ref)
1156 target_ref_obj = unicode_to_reference(target_ref)
1157 target_ref_obj = unicode_to_reference(target_ref)
1157
1158
1158 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1159 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1159 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1160 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1160
1161
1161 ancestor = source_scm.get_common_ancestor(
1162 ancestor = source_scm.get_common_ancestor(
1162 source_commit.raw_id, target_commit.raw_id, target_scm)
1163 source_commit.raw_id, target_commit.raw_id, target_scm)
1163
1164
1164 # recalculate target ref based on ancestor
1165 # recalculate target ref based on ancestor
1165 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1166 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1166
1167
1167 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1168 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1168 PullRequestModel().get_reviewer_functions()
1169 PullRequestModel().get_reviewer_functions()
1169
1170
1170 # recalculate reviewers logic, to make sure we can validate this
1171 # recalculate reviewers logic, to make sure we can validate this
1171 reviewer_rules = get_default_reviewers_data(
1172 reviewer_rules = get_default_reviewers_data(
1172 self._rhodecode_db_user,
1173 self._rhodecode_db_user,
1173 source_db_repo,
1174 source_db_repo,
1174 source_ref_obj,
1175 source_ref_obj,
1175 target_db_repo,
1176 target_db_repo,
1176 target_ref_obj,
1177 target_ref_obj,
1177 include_diff_info=False)
1178 include_diff_info=False)
1178
1179
1179 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1180 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1180 observers = validate_observers(_form['observer_members'], reviewer_rules)
1181 observers = validate_observers(_form['observer_members'], reviewer_rules)
1181
1182
1182 pullrequest_title = _form['pullrequest_title']
1183 pullrequest_title = _form['pullrequest_title']
1183 title_source_ref = source_ref_obj.name
1184 title_source_ref = source_ref_obj.name
1184 if not pullrequest_title:
1185 if not pullrequest_title:
1185 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1186 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1186 source=source_repo,
1187 source=source_repo,
1187 source_ref=title_source_ref,
1188 source_ref=title_source_ref,
1188 target=target_repo
1189 target=target_repo
1189 )
1190 )
1190
1191
1191 description = _form['pullrequest_desc']
1192 description = _form['pullrequest_desc']
1192 description_renderer = _form['description_renderer']
1193 description_renderer = _form['description_renderer']
1193
1194
1194 try:
1195 try:
1195 pull_request = PullRequestModel().create(
1196 pull_request = PullRequestModel().create(
1196 created_by=self._rhodecode_user.user_id,
1197 created_by=self._rhodecode_user.user_id,
1197 source_repo=source_repo,
1198 source_repo=source_repo,
1198 source_ref=source_ref,
1199 source_ref=source_ref,
1199 target_repo=target_repo,
1200 target_repo=target_repo,
1200 target_ref=target_ref,
1201 target_ref=target_ref,
1201 revisions=commit_ids,
1202 revisions=commit_ids,
1202 common_ancestor_id=common_ancestor_id,
1203 common_ancestor_id=common_ancestor_id,
1203 reviewers=reviewers,
1204 reviewers=reviewers,
1204 observers=observers,
1205 observers=observers,
1205 title=pullrequest_title,
1206 title=pullrequest_title,
1206 description=description,
1207 description=description,
1207 description_renderer=description_renderer,
1208 description_renderer=description_renderer,
1208 reviewer_data=reviewer_rules,
1209 reviewer_data=reviewer_rules,
1209 auth_user=self._rhodecode_user
1210 auth_user=self._rhodecode_user
1210 )
1211 )
1211 Session().commit()
1212 Session().commit()
1212
1213
1213 h.flash(_('Successfully opened new pull request'),
1214 h.flash(_('Successfully opened new pull request'),
1214 category='success')
1215 category='success')
1215 except Exception:
1216 except Exception:
1216 msg = _('Error occurred during creation of this pull request.')
1217 msg = _('Error occurred during creation of this pull request.')
1217 log.exception(msg)
1218 log.exception(msg)
1218 h.flash(msg, category='error')
1219 h.flash(msg, category='error')
1219
1220
1220 # copy the args back to redirect
1221 # copy the args back to redirect
1221 org_query = self.request.GET.mixed()
1222 org_query = self.request.GET.mixed()
1222 raise HTTPFound(
1223 raise HTTPFound(
1223 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1224 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1224 _query=org_query))
1225 _query=org_query))
1225
1226
1226 raise HTTPFound(
1227 raise HTTPFound(
1227 h.route_path('pullrequest_show', repo_name=target_repo,
1228 h.route_path('pullrequest_show', repo_name=target_repo,
1228 pull_request_id=pull_request.pull_request_id))
1229 pull_request_id=pull_request.pull_request_id))
1229
1230
1230 @LoginRequired()
1231 @LoginRequired()
1231 @NotAnonymous()
1232 @NotAnonymous()
1232 @HasRepoPermissionAnyDecorator(
1233 @HasRepoPermissionAnyDecorator(
1233 'repository.read', 'repository.write', 'repository.admin')
1234 'repository.read', 'repository.write', 'repository.admin')
1234 @CSRFRequired()
1235 @CSRFRequired()
1235 @view_config(
1236 @view_config(
1236 route_name='pullrequest_update', request_method='POST',
1237 route_name='pullrequest_update', request_method='POST',
1237 renderer='json_ext')
1238 renderer='json_ext')
1238 def pull_request_update(self):
1239 def pull_request_update(self):
1239 pull_request = PullRequest.get_or_404(
1240 pull_request = PullRequest.get_or_404(
1240 self.request.matchdict['pull_request_id'])
1241 self.request.matchdict['pull_request_id'])
1241 _ = self.request.translate
1242 _ = self.request.translate
1242
1243
1243 c = self.load_default_context()
1244 c = self.load_default_context()
1244 redirect_url = None
1245 redirect_url = None
1245
1246
1246 if pull_request.is_closed():
1247 if pull_request.is_closed():
1247 log.debug('update: forbidden because pull request is closed')
1248 log.debug('update: forbidden because pull request is closed')
1248 msg = _(u'Cannot update closed pull requests.')
1249 msg = _(u'Cannot update closed pull requests.')
1249 h.flash(msg, category='error')
1250 h.flash(msg, category='error')
1250 return {'response': True,
1251 return {'response': True,
1251 'redirect_url': redirect_url}
1252 'redirect_url': redirect_url}
1252
1253
1253 is_state_changing = pull_request.is_state_changing()
1254 is_state_changing = pull_request.is_state_changing()
1254 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1255 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1255
1256
1256 # only owner or admin can update it
1257 # only owner or admin can update it
1257 allowed_to_update = PullRequestModel().check_user_update(
1258 allowed_to_update = PullRequestModel().check_user_update(
1258 pull_request, self._rhodecode_user)
1259 pull_request, self._rhodecode_user)
1259
1260
1260 if allowed_to_update:
1261 if allowed_to_update:
1261 controls = peppercorn.parse(self.request.POST.items())
1262 controls = peppercorn.parse(self.request.POST.items())
1262 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1263 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1263
1264
1264 if 'review_members' in controls:
1265 if 'review_members' in controls:
1265 self._update_reviewers(
1266 self._update_reviewers(
1266 c,
1267 c,
1267 pull_request, controls['review_members'],
1268 pull_request, controls['review_members'],
1268 pull_request.reviewer_data,
1269 pull_request.reviewer_data,
1269 PullRequestReviewers.ROLE_REVIEWER)
1270 PullRequestReviewers.ROLE_REVIEWER)
1270 elif 'observer_members' in controls:
1271 elif 'observer_members' in controls:
1271 self._update_reviewers(
1272 self._update_reviewers(
1272 c,
1273 c,
1273 pull_request, controls['observer_members'],
1274 pull_request, controls['observer_members'],
1274 pull_request.reviewer_data,
1275 pull_request.reviewer_data,
1275 PullRequestReviewers.ROLE_OBSERVER)
1276 PullRequestReviewers.ROLE_OBSERVER)
1276 elif str2bool(self.request.POST.get('update_commits', 'false')):
1277 elif str2bool(self.request.POST.get('update_commits', 'false')):
1277 if is_state_changing:
1278 if is_state_changing:
1278 log.debug('commits update: forbidden because pull request is in state %s',
1279 log.debug('commits update: forbidden because pull request is in state %s',
1279 pull_request.pull_request_state)
1280 pull_request.pull_request_state)
1280 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1281 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1281 u'Current state is: `{}`').format(
1282 u'Current state is: `{}`').format(
1282 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1283 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1283 h.flash(msg, category='error')
1284 h.flash(msg, category='error')
1284 return {'response': True,
1285 return {'response': True,
1285 'redirect_url': redirect_url}
1286 'redirect_url': redirect_url}
1286
1287
1287 self._update_commits(c, pull_request)
1288 self._update_commits(c, pull_request)
1288 if force_refresh:
1289 if force_refresh:
1289 redirect_url = h.route_path(
1290 redirect_url = h.route_path(
1290 'pullrequest_show', repo_name=self.db_repo_name,
1291 'pullrequest_show', repo_name=self.db_repo_name,
1291 pull_request_id=pull_request.pull_request_id,
1292 pull_request_id=pull_request.pull_request_id,
1292 _query={"force_refresh": 1})
1293 _query={"force_refresh": 1})
1293 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1294 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1294 self._edit_pull_request(pull_request)
1295 self._edit_pull_request(pull_request)
1295 else:
1296 else:
1296 log.error('Unhandled update data.')
1297 log.error('Unhandled update data.')
1297 raise HTTPBadRequest()
1298 raise HTTPBadRequest()
1298
1299
1299 return {'response': True,
1300 return {'response': True,
1300 'redirect_url': redirect_url}
1301 'redirect_url': redirect_url}
1301 raise HTTPForbidden()
1302 raise HTTPForbidden()
1302
1303
1303 def _edit_pull_request(self, pull_request):
1304 def _edit_pull_request(self, pull_request):
1304 """
1305 """
1305 Edit title and description
1306 Edit title and description
1306 """
1307 """
1307 _ = self.request.translate
1308 _ = self.request.translate
1308
1309
1309 try:
1310 try:
1310 PullRequestModel().edit(
1311 PullRequestModel().edit(
1311 pull_request,
1312 pull_request,
1312 self.request.POST.get('title'),
1313 self.request.POST.get('title'),
1313 self.request.POST.get('description'),
1314 self.request.POST.get('description'),
1314 self.request.POST.get('description_renderer'),
1315 self.request.POST.get('description_renderer'),
1315 self._rhodecode_user)
1316 self._rhodecode_user)
1316 except ValueError:
1317 except ValueError:
1317 msg = _(u'Cannot update closed pull requests.')
1318 msg = _(u'Cannot update closed pull requests.')
1318 h.flash(msg, category='error')
1319 h.flash(msg, category='error')
1319 return
1320 return
1320 else:
1321 else:
1321 Session().commit()
1322 Session().commit()
1322
1323
1323 msg = _(u'Pull request title & description updated.')
1324 msg = _(u'Pull request title & description updated.')
1324 h.flash(msg, category='success')
1325 h.flash(msg, category='success')
1325 return
1326 return
1326
1327
1327 def _update_commits(self, c, pull_request):
1328 def _update_commits(self, c, pull_request):
1328 _ = self.request.translate
1329 _ = self.request.translate
1329
1330
1330 with pull_request.set_state(PullRequest.STATE_UPDATING):
1331 with pull_request.set_state(PullRequest.STATE_UPDATING):
1331 resp = PullRequestModel().update_commits(
1332 resp = PullRequestModel().update_commits(
1332 pull_request, self._rhodecode_db_user)
1333 pull_request, self._rhodecode_db_user)
1333
1334
1334 if resp.executed:
1335 if resp.executed:
1335
1336
1336 if resp.target_changed and resp.source_changed:
1337 if resp.target_changed and resp.source_changed:
1337 changed = 'target and source repositories'
1338 changed = 'target and source repositories'
1338 elif resp.target_changed and not resp.source_changed:
1339 elif resp.target_changed and not resp.source_changed:
1339 changed = 'target repository'
1340 changed = 'target repository'
1340 elif not resp.target_changed and resp.source_changed:
1341 elif not resp.target_changed and resp.source_changed:
1341 changed = 'source repository'
1342 changed = 'source repository'
1342 else:
1343 else:
1343 changed = 'nothing'
1344 changed = 'nothing'
1344
1345
1345 msg = _(u'Pull request updated to "{source_commit_id}" with '
1346 msg = _(u'Pull request updated to "{source_commit_id}" with '
1346 u'{count_added} added, {count_removed} removed commits. '
1347 u'{count_added} added, {count_removed} removed commits. '
1347 u'Source of changes: {change_source}.')
1348 u'Source of changes: {change_source}.')
1348 msg = msg.format(
1349 msg = msg.format(
1349 source_commit_id=pull_request.source_ref_parts.commit_id,
1350 source_commit_id=pull_request.source_ref_parts.commit_id,
1350 count_added=len(resp.changes.added),
1351 count_added=len(resp.changes.added),
1351 count_removed=len(resp.changes.removed),
1352 count_removed=len(resp.changes.removed),
1352 change_source=changed)
1353 change_source=changed)
1353 h.flash(msg, category='success')
1354 h.flash(msg, category='success')
1354 channelstream.pr_update_channelstream_push(
1355 channelstream.pr_update_channelstream_push(
1355 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1356 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1356 else:
1357 else:
1357 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1358 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1358 warning_reasons = [
1359 warning_reasons = [
1359 UpdateFailureReason.NO_CHANGE,
1360 UpdateFailureReason.NO_CHANGE,
1360 UpdateFailureReason.WRONG_REF_TYPE,
1361 UpdateFailureReason.WRONG_REF_TYPE,
1361 ]
1362 ]
1362 category = 'warning' if resp.reason in warning_reasons else 'error'
1363 category = 'warning' if resp.reason in warning_reasons else 'error'
1363 h.flash(msg, category=category)
1364 h.flash(msg, category=category)
1364
1365
1365 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1366 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1366 _ = self.request.translate
1367 _ = self.request.translate
1367
1368
1368 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1369 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1369 PullRequestModel().get_reviewer_functions()
1370 PullRequestModel().get_reviewer_functions()
1370
1371
1371 if role == PullRequestReviewers.ROLE_REVIEWER:
1372 if role == PullRequestReviewers.ROLE_REVIEWER:
1372 try:
1373 try:
1373 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1374 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1374 except ValueError as e:
1375 except ValueError as e:
1375 log.error('Reviewers Validation: {}'.format(e))
1376 log.error('Reviewers Validation: {}'.format(e))
1376 h.flash(e, category='error')
1377 h.flash(e, category='error')
1377 return
1378 return
1378
1379
1379 old_calculated_status = pull_request.calculated_review_status()
1380 old_calculated_status = pull_request.calculated_review_status()
1380 PullRequestModel().update_reviewers(
1381 PullRequestModel().update_reviewers(
1381 pull_request, reviewers, self._rhodecode_db_user)
1382 pull_request, reviewers, self._rhodecode_db_user)
1382
1383
1383 Session().commit()
1384 Session().commit()
1384
1385
1385 msg = _('Pull request reviewers updated.')
1386 msg = _('Pull request reviewers updated.')
1386 h.flash(msg, category='success')
1387 h.flash(msg, category='success')
1387 channelstream.pr_update_channelstream_push(
1388 channelstream.pr_update_channelstream_push(
1388 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1389 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1389
1390
1390 # trigger status changed if change in reviewers changes the status
1391 # trigger status changed if change in reviewers changes the status
1391 calculated_status = pull_request.calculated_review_status()
1392 calculated_status = pull_request.calculated_review_status()
1392 if old_calculated_status != calculated_status:
1393 if old_calculated_status != calculated_status:
1393 PullRequestModel().trigger_pull_request_hook(
1394 PullRequestModel().trigger_pull_request_hook(
1394 pull_request, self._rhodecode_user, 'review_status_change',
1395 pull_request, self._rhodecode_user, 'review_status_change',
1395 data={'status': calculated_status})
1396 data={'status': calculated_status})
1396
1397
1397 elif role == PullRequestReviewers.ROLE_OBSERVER:
1398 elif role == PullRequestReviewers.ROLE_OBSERVER:
1398 try:
1399 try:
1399 observers = validate_observers(review_members, reviewer_rules)
1400 observers = validate_observers(review_members, reviewer_rules)
1400 except ValueError as e:
1401 except ValueError as e:
1401 log.error('Observers Validation: {}'.format(e))
1402 log.error('Observers Validation: {}'.format(e))
1402 h.flash(e, category='error')
1403 h.flash(e, category='error')
1403 return
1404 return
1404
1405
1405 PullRequestModel().update_observers(
1406 PullRequestModel().update_observers(
1406 pull_request, observers, self._rhodecode_db_user)
1407 pull_request, observers, self._rhodecode_db_user)
1407
1408
1408 Session().commit()
1409 Session().commit()
1409 msg = _('Pull request observers updated.')
1410 msg = _('Pull request observers updated.')
1410 h.flash(msg, category='success')
1411 h.flash(msg, category='success')
1411 channelstream.pr_update_channelstream_push(
1412 channelstream.pr_update_channelstream_push(
1412 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1413 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1413
1414
1414 @LoginRequired()
1415 @LoginRequired()
1415 @NotAnonymous()
1416 @NotAnonymous()
1416 @HasRepoPermissionAnyDecorator(
1417 @HasRepoPermissionAnyDecorator(
1417 'repository.read', 'repository.write', 'repository.admin')
1418 'repository.read', 'repository.write', 'repository.admin')
1418 @CSRFRequired()
1419 @CSRFRequired()
1419 @view_config(
1420 @view_config(
1420 route_name='pullrequest_merge', request_method='POST',
1421 route_name='pullrequest_merge', request_method='POST',
1421 renderer='json_ext')
1422 renderer='json_ext')
1422 def pull_request_merge(self):
1423 def pull_request_merge(self):
1423 """
1424 """
1424 Merge will perform a server-side merge of the specified
1425 Merge will perform a server-side merge of the specified
1425 pull request, if the pull request is approved and mergeable.
1426 pull request, if the pull request is approved and mergeable.
1426 After successful merging, the pull request is automatically
1427 After successful merging, the pull request is automatically
1427 closed, with a relevant comment.
1428 closed, with a relevant comment.
1428 """
1429 """
1429 pull_request = PullRequest.get_or_404(
1430 pull_request = PullRequest.get_or_404(
1430 self.request.matchdict['pull_request_id'])
1431 self.request.matchdict['pull_request_id'])
1431 _ = self.request.translate
1432 _ = self.request.translate
1432
1433
1433 if pull_request.is_state_changing():
1434 if pull_request.is_state_changing():
1434 log.debug('show: forbidden because pull request is in state %s',
1435 log.debug('show: forbidden because pull request is in state %s',
1435 pull_request.pull_request_state)
1436 pull_request.pull_request_state)
1436 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1437 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1437 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1438 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1438 pull_request.pull_request_state)
1439 pull_request.pull_request_state)
1439 h.flash(msg, category='error')
1440 h.flash(msg, category='error')
1440 raise HTTPFound(
1441 raise HTTPFound(
1441 h.route_path('pullrequest_show',
1442 h.route_path('pullrequest_show',
1442 repo_name=pull_request.target_repo.repo_name,
1443 repo_name=pull_request.target_repo.repo_name,
1443 pull_request_id=pull_request.pull_request_id))
1444 pull_request_id=pull_request.pull_request_id))
1444
1445
1445 self.load_default_context()
1446 self.load_default_context()
1446
1447
1447 with pull_request.set_state(PullRequest.STATE_UPDATING):
1448 with pull_request.set_state(PullRequest.STATE_UPDATING):
1448 check = MergeCheck.validate(
1449 check = MergeCheck.validate(
1449 pull_request, auth_user=self._rhodecode_user,
1450 pull_request, auth_user=self._rhodecode_user,
1450 translator=self.request.translate)
1451 translator=self.request.translate)
1451 merge_possible = not check.failed
1452 merge_possible = not check.failed
1452
1453
1453 for err_type, error_msg in check.errors:
1454 for err_type, error_msg in check.errors:
1454 h.flash(error_msg, category=err_type)
1455 h.flash(error_msg, category=err_type)
1455
1456
1456 if merge_possible:
1457 if merge_possible:
1457 log.debug("Pre-conditions checked, trying to merge.")
1458 log.debug("Pre-conditions checked, trying to merge.")
1458 extras = vcs_operation_context(
1459 extras = vcs_operation_context(
1459 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1460 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1460 username=self._rhodecode_db_user.username, action='push',
1461 username=self._rhodecode_db_user.username, action='push',
1461 scm=pull_request.target_repo.repo_type)
1462 scm=pull_request.target_repo.repo_type)
1462 with pull_request.set_state(PullRequest.STATE_UPDATING):
1463 with pull_request.set_state(PullRequest.STATE_UPDATING):
1463 self._merge_pull_request(
1464 self._merge_pull_request(
1464 pull_request, self._rhodecode_db_user, extras)
1465 pull_request, self._rhodecode_db_user, extras)
1465 else:
1466 else:
1466 log.debug("Pre-conditions failed, NOT merging.")
1467 log.debug("Pre-conditions failed, NOT merging.")
1467
1468
1468 raise HTTPFound(
1469 raise HTTPFound(
1469 h.route_path('pullrequest_show',
1470 h.route_path('pullrequest_show',
1470 repo_name=pull_request.target_repo.repo_name,
1471 repo_name=pull_request.target_repo.repo_name,
1471 pull_request_id=pull_request.pull_request_id))
1472 pull_request_id=pull_request.pull_request_id))
1472
1473
1473 def _merge_pull_request(self, pull_request, user, extras):
1474 def _merge_pull_request(self, pull_request, user, extras):
1474 _ = self.request.translate
1475 _ = self.request.translate
1475 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1476 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1476
1477
1477 if merge_resp.executed:
1478 if merge_resp.executed:
1478 log.debug("The merge was successful, closing the pull request.")
1479 log.debug("The merge was successful, closing the pull request.")
1479 PullRequestModel().close_pull_request(
1480 PullRequestModel().close_pull_request(
1480 pull_request.pull_request_id, user)
1481 pull_request.pull_request_id, user)
1481 Session().commit()
1482 Session().commit()
1482 msg = _('Pull request was successfully merged and closed.')
1483 msg = _('Pull request was successfully merged and closed.')
1483 h.flash(msg, category='success')
1484 h.flash(msg, category='success')
1484 else:
1485 else:
1485 log.debug(
1486 log.debug(
1486 "The merge was not successful. Merge response: %s", merge_resp)
1487 "The merge was not successful. Merge response: %s", merge_resp)
1487 msg = merge_resp.merge_status_message
1488 msg = merge_resp.merge_status_message
1488 h.flash(msg, category='error')
1489 h.flash(msg, category='error')
1489
1490
1490 @LoginRequired()
1491 @LoginRequired()
1491 @NotAnonymous()
1492 @NotAnonymous()
1492 @HasRepoPermissionAnyDecorator(
1493 @HasRepoPermissionAnyDecorator(
1493 'repository.read', 'repository.write', 'repository.admin')
1494 'repository.read', 'repository.write', 'repository.admin')
1494 @CSRFRequired()
1495 @CSRFRequired()
1495 @view_config(
1496 @view_config(
1496 route_name='pullrequest_delete', request_method='POST',
1497 route_name='pullrequest_delete', request_method='POST',
1497 renderer='json_ext')
1498 renderer='json_ext')
1498 def pull_request_delete(self):
1499 def pull_request_delete(self):
1499 _ = self.request.translate
1500 _ = self.request.translate
1500
1501
1501 pull_request = PullRequest.get_or_404(
1502 pull_request = PullRequest.get_or_404(
1502 self.request.matchdict['pull_request_id'])
1503 self.request.matchdict['pull_request_id'])
1503 self.load_default_context()
1504 self.load_default_context()
1504
1505
1505 pr_closed = pull_request.is_closed()
1506 pr_closed = pull_request.is_closed()
1506 allowed_to_delete = PullRequestModel().check_user_delete(
1507 allowed_to_delete = PullRequestModel().check_user_delete(
1507 pull_request, self._rhodecode_user) and not pr_closed
1508 pull_request, self._rhodecode_user) and not pr_closed
1508
1509
1509 # only owner can delete it !
1510 # only owner can delete it !
1510 if allowed_to_delete:
1511 if allowed_to_delete:
1511 PullRequestModel().delete(pull_request, self._rhodecode_user)
1512 PullRequestModel().delete(pull_request, self._rhodecode_user)
1512 Session().commit()
1513 Session().commit()
1513 h.flash(_('Successfully deleted pull request'),
1514 h.flash(_('Successfully deleted pull request'),
1514 category='success')
1515 category='success')
1515 raise HTTPFound(h.route_path('pullrequest_show_all',
1516 raise HTTPFound(h.route_path('pullrequest_show_all',
1516 repo_name=self.db_repo_name))
1517 repo_name=self.db_repo_name))
1517
1518
1518 log.warning('user %s tried to delete pull request without access',
1519 log.warning('user %s tried to delete pull request without access',
1519 self._rhodecode_user)
1520 self._rhodecode_user)
1520 raise HTTPNotFound()
1521 raise HTTPNotFound()
1521
1522
1522 def _pull_request_comments_create(self, pull_request, comments):
1523 def _pull_request_comments_create(self, pull_request, comments):
1523 _ = self.request.translate
1524 _ = self.request.translate
1524 data = {}
1525 data = {}
1525 pull_request_id = pull_request.pull_request_id
1526 pull_request_id = pull_request.pull_request_id
1526 if not comments:
1527 if not comments:
1527 return
1528 return
1528
1529
1529 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1530 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1530
1531
1531 for entry in comments:
1532 for entry in comments:
1532 c = self.load_default_context()
1533 c = self.load_default_context()
1533 comment_type = entry['comment_type']
1534 comment_type = entry['comment_type']
1534 text = entry['text']
1535 text = entry['text']
1535 status = entry['status']
1536 status = entry['status']
1536 is_draft = str2bool(entry['is_draft'])
1537 is_draft = str2bool(entry['is_draft'])
1537 resolves_comment_id = entry['resolves_comment_id']
1538 resolves_comment_id = entry['resolves_comment_id']
1538 close_pull_request = entry['close_pull_request']
1539 close_pull_request = entry['close_pull_request']
1539 f_path = entry['f_path']
1540 f_path = entry['f_path']
1540 line_no = entry['line']
1541 line_no = entry['line']
1541 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1542 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1542
1543
1543 # the logic here should work like following, if we submit close
1544 # the logic here should work like following, if we submit close
1544 # pr comment, use `close_pull_request_with_comment` function
1545 # pr comment, use `close_pull_request_with_comment` function
1545 # else handle regular comment logic
1546 # else handle regular comment logic
1546
1547
1547 if close_pull_request:
1548 if close_pull_request:
1548 # only owner or admin or person with write permissions
1549 # only owner or admin or person with write permissions
1549 allowed_to_close = PullRequestModel().check_user_update(
1550 allowed_to_close = PullRequestModel().check_user_update(
1550 pull_request, self._rhodecode_user)
1551 pull_request, self._rhodecode_user)
1551 if not allowed_to_close:
1552 if not allowed_to_close:
1552 log.debug('comment: forbidden because not allowed to close '
1553 log.debug('comment: forbidden because not allowed to close '
1553 'pull request %s', pull_request_id)
1554 'pull request %s', pull_request_id)
1554 raise HTTPForbidden()
1555 raise HTTPForbidden()
1555
1556
1556 # This also triggers `review_status_change`
1557 # This also triggers `review_status_change`
1557 comment, status = PullRequestModel().close_pull_request_with_comment(
1558 comment, status = PullRequestModel().close_pull_request_with_comment(
1558 pull_request, self._rhodecode_user, self.db_repo, message=text,
1559 pull_request, self._rhodecode_user, self.db_repo, message=text,
1559 auth_user=self._rhodecode_user)
1560 auth_user=self._rhodecode_user)
1560 Session().flush()
1561 Session().flush()
1561 is_inline = comment.is_inline
1562 is_inline = comment.is_inline
1562
1563
1563 PullRequestModel().trigger_pull_request_hook(
1564 PullRequestModel().trigger_pull_request_hook(
1564 pull_request, self._rhodecode_user, 'comment',
1565 pull_request, self._rhodecode_user, 'comment',
1565 data={'comment': comment})
1566 data={'comment': comment})
1566
1567
1567 else:
1568 else:
1568 # regular comment case, could be inline, or one with status.
1569 # regular comment case, could be inline, or one with status.
1569 # for that one we check also permissions
1570 # for that one we check also permissions
1570 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1571 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1571 allowed_to_change_status = PullRequestModel().check_user_change_status(
1572 allowed_to_change_status = PullRequestModel().check_user_change_status(
1572 pull_request, self._rhodecode_user) and not is_draft
1573 pull_request, self._rhodecode_user) and not is_draft
1573
1574
1574 if status and allowed_to_change_status:
1575 if status and allowed_to_change_status:
1575 message = (_('Status change %(transition_icon)s %(status)s')
1576 message = (_('Status change %(transition_icon)s %(status)s')
1576 % {'transition_icon': '>',
1577 % {'transition_icon': '>',
1577 'status': ChangesetStatus.get_status_lbl(status)})
1578 'status': ChangesetStatus.get_status_lbl(status)})
1578 text = text or message
1579 text = text or message
1579
1580
1580 comment = CommentsModel().create(
1581 comment = CommentsModel().create(
1581 text=text,
1582 text=text,
1582 repo=self.db_repo.repo_id,
1583 repo=self.db_repo.repo_id,
1583 user=self._rhodecode_user.user_id,
1584 user=self._rhodecode_user.user_id,
1584 pull_request=pull_request,
1585 pull_request=pull_request,
1585 f_path=f_path,
1586 f_path=f_path,
1586 line_no=line_no,
1587 line_no=line_no,
1587 status_change=(ChangesetStatus.get_status_lbl(status)
1588 status_change=(ChangesetStatus.get_status_lbl(status)
1588 if status and allowed_to_change_status else None),
1589 if status and allowed_to_change_status else None),
1589 status_change_type=(status
1590 status_change_type=(status
1590 if status and allowed_to_change_status else None),
1591 if status and allowed_to_change_status else None),
1591 comment_type=comment_type,
1592 comment_type=comment_type,
1592 is_draft=is_draft,
1593 is_draft=is_draft,
1593 resolves_comment_id=resolves_comment_id,
1594 resolves_comment_id=resolves_comment_id,
1594 auth_user=self._rhodecode_user,
1595 auth_user=self._rhodecode_user,
1595 send_email=not is_draft, # skip notification for draft comments
1596 send_email=not is_draft, # skip notification for draft comments
1596 )
1597 )
1597 is_inline = comment.is_inline
1598 is_inline = comment.is_inline
1598
1599
1599 if allowed_to_change_status:
1600 if allowed_to_change_status:
1600 # calculate old status before we change it
1601 # calculate old status before we change it
1601 old_calculated_status = pull_request.calculated_review_status()
1602 old_calculated_status = pull_request.calculated_review_status()
1602
1603
1603 # get status if set !
1604 # get status if set !
1604 if status:
1605 if status:
1605 ChangesetStatusModel().set_status(
1606 ChangesetStatusModel().set_status(
1606 self.db_repo.repo_id,
1607 self.db_repo.repo_id,
1607 status,
1608 status,
1608 self._rhodecode_user.user_id,
1609 self._rhodecode_user.user_id,
1609 comment,
1610 comment,
1610 pull_request=pull_request
1611 pull_request=pull_request
1611 )
1612 )
1612
1613
1613 Session().flush()
1614 Session().flush()
1614 # this is somehow required to get access to some relationship
1615 # this is somehow required to get access to some relationship
1615 # loaded on comment
1616 # loaded on comment
1616 Session().refresh(comment)
1617 Session().refresh(comment)
1617
1618
1618 PullRequestModel().trigger_pull_request_hook(
1619 PullRequestModel().trigger_pull_request_hook(
1619 pull_request, self._rhodecode_user, 'comment',
1620 pull_request, self._rhodecode_user, 'comment',
1620 data={'comment': comment})
1621 data={'comment': comment})
1621
1622
1622 # we now calculate the status of pull request, and based on that
1623 # we now calculate the status of pull request, and based on that
1623 # calculation we set the commits status
1624 # calculation we set the commits status
1624 calculated_status = pull_request.calculated_review_status()
1625 calculated_status = pull_request.calculated_review_status()
1625 if old_calculated_status != calculated_status:
1626 if old_calculated_status != calculated_status:
1626 PullRequestModel().trigger_pull_request_hook(
1627 PullRequestModel().trigger_pull_request_hook(
1627 pull_request, self._rhodecode_user, 'review_status_change',
1628 pull_request, self._rhodecode_user, 'review_status_change',
1628 data={'status': calculated_status})
1629 data={'status': calculated_status})
1629
1630
1630 comment_id = comment.comment_id
1631 comment_id = comment.comment_id
1631 data[comment_id] = {
1632 data[comment_id] = {
1632 'target_id': target_elem_id
1633 'target_id': target_elem_id
1633 }
1634 }
1634 Session().flush()
1635 Session().flush()
1635
1636
1636 c.co = comment
1637 c.co = comment
1637 c.at_version_num = None
1638 c.at_version_num = None
1638 c.is_new = True
1639 c.is_new = True
1639 rendered_comment = render(
1640 rendered_comment = render(
1640 'rhodecode:templates/changeset/changeset_comment_block.mako',
1641 'rhodecode:templates/changeset/changeset_comment_block.mako',
1641 self._get_template_context(c), self.request)
1642 self._get_template_context(c), self.request)
1642
1643
1643 data[comment_id].update(comment.get_dict())
1644 data[comment_id].update(comment.get_dict())
1644 data[comment_id].update({'rendered_text': rendered_comment})
1645 data[comment_id].update({'rendered_text': rendered_comment})
1645
1646
1646 Session().commit()
1647 Session().commit()
1647
1648
1648 # skip channelstream for draft comments
1649 # skip channelstream for draft comments
1649 if all_drafts:
1650 if all_drafts:
1650 comment_broadcast_channel = channelstream.comment_channel(
1651 comment_broadcast_channel = channelstream.comment_channel(
1651 self.db_repo_name, pull_request_obj=pull_request)
1652 self.db_repo_name, pull_request_obj=pull_request)
1652
1653
1653 comment_data = data
1654 comment_data = data
1654 comment_type = 'inline' if is_inline else 'general'
1655 comment_type = 'inline' if is_inline else 'general'
1655 if len(data) == 1:
1656 if len(data) == 1:
1656 msg = _('posted {} new {} comment').format(len(data), comment_type)
1657 msg = _('posted {} new {} comment').format(len(data), comment_type)
1657 else:
1658 else:
1658 msg = _('posted {} new {} comments').format(len(data), comment_type)
1659 msg = _('posted {} new {} comments').format(len(data), comment_type)
1659
1660
1660 channelstream.comment_channelstream_push(
1661 channelstream.comment_channelstream_push(
1661 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1662 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1662 comment_data=comment_data)
1663 comment_data=comment_data)
1663
1664
1664 return data
1665 return data
1665
1666
1666 @LoginRequired()
1667 @LoginRequired()
1667 @NotAnonymous()
1668 @NotAnonymous()
1668 @HasRepoPermissionAnyDecorator(
1669 @HasRepoPermissionAnyDecorator(
1669 'repository.read', 'repository.write', 'repository.admin')
1670 'repository.read', 'repository.write', 'repository.admin')
1670 @CSRFRequired()
1671 @CSRFRequired()
1671 @view_config(
1672 @view_config(
1672 route_name='pullrequest_comment_create', request_method='POST',
1673 route_name='pullrequest_comment_create', request_method='POST',
1673 renderer='json_ext')
1674 renderer='json_ext')
1674 def pull_request_comment_create(self):
1675 def pull_request_comment_create(self):
1675 _ = self.request.translate
1676 _ = self.request.translate
1676
1677
1677 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1678 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1678
1679
1679 if pull_request.is_closed():
1680 if pull_request.is_closed():
1680 log.debug('comment: forbidden because pull request is closed')
1681 log.debug('comment: forbidden because pull request is closed')
1681 raise HTTPForbidden()
1682 raise HTTPForbidden()
1682
1683
1683 allowed_to_comment = PullRequestModel().check_user_comment(
1684 allowed_to_comment = PullRequestModel().check_user_comment(
1684 pull_request, self._rhodecode_user)
1685 pull_request, self._rhodecode_user)
1685 if not allowed_to_comment:
1686 if not allowed_to_comment:
1686 log.debug('comment: forbidden because pull request is from forbidden repo')
1687 log.debug('comment: forbidden because pull request is from forbidden repo')
1687 raise HTTPForbidden()
1688 raise HTTPForbidden()
1688
1689
1689 comment_data = {
1690 comment_data = {
1690 'comment_type': self.request.POST.get('comment_type'),
1691 'comment_type': self.request.POST.get('comment_type'),
1691 'text': self.request.POST.get('text'),
1692 'text': self.request.POST.get('text'),
1692 'status': self.request.POST.get('changeset_status', None),
1693 'status': self.request.POST.get('changeset_status', None),
1693 'is_draft': self.request.POST.get('draft'),
1694 'is_draft': self.request.POST.get('draft'),
1694 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1695 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1695 'close_pull_request': self.request.POST.get('close_pull_request'),
1696 'close_pull_request': self.request.POST.get('close_pull_request'),
1696 'f_path': self.request.POST.get('f_path'),
1697 'f_path': self.request.POST.get('f_path'),
1697 'line': self.request.POST.get('line'),
1698 'line': self.request.POST.get('line'),
1698 }
1699 }
1699 data = self._pull_request_comments_create(pull_request, [comment_data])
1700 data = self._pull_request_comments_create(pull_request, [comment_data])
1700
1701
1701 return data
1702 return data
1702
1703
1703 @LoginRequired()
1704 @LoginRequired()
1704 @NotAnonymous()
1705 @NotAnonymous()
1705 @HasRepoPermissionAnyDecorator(
1706 @HasRepoPermissionAnyDecorator(
1706 'repository.read', 'repository.write', 'repository.admin')
1707 'repository.read', 'repository.write', 'repository.admin')
1707 @CSRFRequired()
1708 @CSRFRequired()
1708 @view_config(
1709 @view_config(
1709 route_name='pullrequest_comment_delete', request_method='POST',
1710 route_name='pullrequest_comment_delete', request_method='POST',
1710 renderer='json_ext')
1711 renderer='json_ext')
1711 def pull_request_comment_delete(self):
1712 def pull_request_comment_delete(self):
1712 pull_request = PullRequest.get_or_404(
1713 pull_request = PullRequest.get_or_404(
1713 self.request.matchdict['pull_request_id'])
1714 self.request.matchdict['pull_request_id'])
1714
1715
1715 comment = ChangesetComment.get_or_404(
1716 comment = ChangesetComment.get_or_404(
1716 self.request.matchdict['comment_id'])
1717 self.request.matchdict['comment_id'])
1717 comment_id = comment.comment_id
1718 comment_id = comment.comment_id
1718
1719
1719 if comment.immutable:
1720 if comment.immutable:
1720 # don't allow deleting comments that are immutable
1721 # don't allow deleting comments that are immutable
1721 raise HTTPForbidden()
1722 raise HTTPForbidden()
1722
1723
1723 if pull_request.is_closed():
1724 if pull_request.is_closed():
1724 log.debug('comment: forbidden because pull request is closed')
1725 log.debug('comment: forbidden because pull request is closed')
1725 raise HTTPForbidden()
1726 raise HTTPForbidden()
1726
1727
1727 if not comment:
1728 if not comment:
1728 log.debug('Comment with id:%s not found, skipping', comment_id)
1729 log.debug('Comment with id:%s not found, skipping', comment_id)
1729 # comment already deleted in another call probably
1730 # comment already deleted in another call probably
1730 return True
1731 return True
1731
1732
1732 if comment.pull_request.is_closed():
1733 if comment.pull_request.is_closed():
1733 # don't allow deleting comments on closed pull request
1734 # don't allow deleting comments on closed pull request
1734 raise HTTPForbidden()
1735 raise HTTPForbidden()
1735
1736
1736 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1737 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1737 super_admin = h.HasPermissionAny('hg.admin')()
1738 super_admin = h.HasPermissionAny('hg.admin')()
1738 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1739 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1739 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1740 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1740 comment_repo_admin = is_repo_admin and is_repo_comment
1741 comment_repo_admin = is_repo_admin and is_repo_comment
1741
1742
1742 if super_admin or comment_owner or comment_repo_admin:
1743 if super_admin or comment_owner or comment_repo_admin:
1743 old_calculated_status = comment.pull_request.calculated_review_status()
1744 old_calculated_status = comment.pull_request.calculated_review_status()
1744 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1745 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1745 Session().commit()
1746 Session().commit()
1746 calculated_status = comment.pull_request.calculated_review_status()
1747 calculated_status = comment.pull_request.calculated_review_status()
1747 if old_calculated_status != calculated_status:
1748 if old_calculated_status != calculated_status:
1748 PullRequestModel().trigger_pull_request_hook(
1749 PullRequestModel().trigger_pull_request_hook(
1749 comment.pull_request, self._rhodecode_user, 'review_status_change',
1750 comment.pull_request, self._rhodecode_user, 'review_status_change',
1750 data={'status': calculated_status})
1751 data={'status': calculated_status})
1751 return True
1752 return True
1752 else:
1753 else:
1753 log.warning('No permissions for user %s to delete comment_id: %s',
1754 log.warning('No permissions for user %s to delete comment_id: %s',
1754 self._rhodecode_db_user, comment_id)
1755 self._rhodecode_db_user, comment_id)
1755 raise HTTPNotFound()
1756 raise HTTPNotFound()
1756
1757
1757 @LoginRequired()
1758 @LoginRequired()
1758 @NotAnonymous()
1759 @NotAnonymous()
1759 @HasRepoPermissionAnyDecorator(
1760 @HasRepoPermissionAnyDecorator(
1760 'repository.read', 'repository.write', 'repository.admin')
1761 'repository.read', 'repository.write', 'repository.admin')
1761 @CSRFRequired()
1762 @CSRFRequired()
1762 @view_config(
1763 @view_config(
1763 route_name='pullrequest_comment_edit', request_method='POST',
1764 route_name='pullrequest_comment_edit', request_method='POST',
1764 renderer='json_ext')
1765 renderer='json_ext')
1765 def pull_request_comment_edit(self):
1766 def pull_request_comment_edit(self):
1766 self.load_default_context()
1767 self.load_default_context()
1767
1768
1768 pull_request = PullRequest.get_or_404(
1769 pull_request = PullRequest.get_or_404(
1769 self.request.matchdict['pull_request_id']
1770 self.request.matchdict['pull_request_id']
1770 )
1771 )
1771 comment = ChangesetComment.get_or_404(
1772 comment = ChangesetComment.get_or_404(
1772 self.request.matchdict['comment_id']
1773 self.request.matchdict['comment_id']
1773 )
1774 )
1774 comment_id = comment.comment_id
1775 comment_id = comment.comment_id
1775
1776
1776 if comment.immutable:
1777 if comment.immutable:
1777 # don't allow deleting comments that are immutable
1778 # don't allow deleting comments that are immutable
1778 raise HTTPForbidden()
1779 raise HTTPForbidden()
1779
1780
1780 if pull_request.is_closed():
1781 if pull_request.is_closed():
1781 log.debug('comment: forbidden because pull request is closed')
1782 log.debug('comment: forbidden because pull request is closed')
1782 raise HTTPForbidden()
1783 raise HTTPForbidden()
1783
1784
1784 if not comment:
1785 if not comment:
1785 log.debug('Comment with id:%s not found, skipping', comment_id)
1786 log.debug('Comment with id:%s not found, skipping', comment_id)
1786 # comment already deleted in another call probably
1787 # comment already deleted in another call probably
1787 return True
1788 return True
1788
1789
1789 if comment.pull_request.is_closed():
1790 if comment.pull_request.is_closed():
1790 # don't allow deleting comments on closed pull request
1791 # don't allow deleting comments on closed pull request
1791 raise HTTPForbidden()
1792 raise HTTPForbidden()
1792
1793
1793 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1794 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1794 super_admin = h.HasPermissionAny('hg.admin')()
1795 super_admin = h.HasPermissionAny('hg.admin')()
1795 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1796 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1796 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1797 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1797 comment_repo_admin = is_repo_admin and is_repo_comment
1798 comment_repo_admin = is_repo_admin and is_repo_comment
1798
1799
1799 if super_admin or comment_owner or comment_repo_admin:
1800 if super_admin or comment_owner or comment_repo_admin:
1800 text = self.request.POST.get('text')
1801 text = self.request.POST.get('text')
1801 version = self.request.POST.get('version')
1802 version = self.request.POST.get('version')
1802 if text == comment.text:
1803 if text == comment.text:
1803 log.warning(
1804 log.warning(
1804 'Comment(PR): '
1805 'Comment(PR): '
1805 'Trying to create new version '
1806 'Trying to create new version '
1806 'with the same comment body {}'.format(
1807 'with the same comment body {}'.format(
1807 comment_id,
1808 comment_id,
1808 )
1809 )
1809 )
1810 )
1810 raise HTTPNotFound()
1811 raise HTTPNotFound()
1811
1812
1812 if version.isdigit():
1813 if version.isdigit():
1813 version = int(version)
1814 version = int(version)
1814 else:
1815 else:
1815 log.warning(
1816 log.warning(
1816 'Comment(PR): Wrong version type {} {} '
1817 'Comment(PR): Wrong version type {} {} '
1817 'for comment {}'.format(
1818 'for comment {}'.format(
1818 version,
1819 version,
1819 type(version),
1820 type(version),
1820 comment_id,
1821 comment_id,
1821 )
1822 )
1822 )
1823 )
1823 raise HTTPNotFound()
1824 raise HTTPNotFound()
1824
1825
1825 try:
1826 try:
1826 comment_history = CommentsModel().edit(
1827 comment_history = CommentsModel().edit(
1827 comment_id=comment_id,
1828 comment_id=comment_id,
1828 text=text,
1829 text=text,
1829 auth_user=self._rhodecode_user,
1830 auth_user=self._rhodecode_user,
1830 version=version,
1831 version=version,
1831 )
1832 )
1832 except CommentVersionMismatch:
1833 except CommentVersionMismatch:
1833 raise HTTPConflict()
1834 raise HTTPConflict()
1834
1835
1835 if not comment_history:
1836 if not comment_history:
1836 raise HTTPNotFound()
1837 raise HTTPNotFound()
1837
1838
1838 Session().commit()
1839 Session().commit()
1839
1840
1840 PullRequestModel().trigger_pull_request_hook(
1841 PullRequestModel().trigger_pull_request_hook(
1841 pull_request, self._rhodecode_user, 'comment_edit',
1842 pull_request, self._rhodecode_user, 'comment_edit',
1842 data={'comment': comment})
1843 data={'comment': comment})
1843
1844
1844 return {
1845 return {
1845 'comment_history_id': comment_history.comment_history_id,
1846 'comment_history_id': comment_history.comment_history_id,
1846 'comment_id': comment.comment_id,
1847 'comment_id': comment.comment_id,
1847 'comment_version': comment_history.version,
1848 'comment_version': comment_history.version,
1848 'comment_author_username': comment_history.author.username,
1849 'comment_author_username': comment_history.author.username,
1849 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1850 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1850 'comment_created_on': h.age_component(comment_history.created_on,
1851 'comment_created_on': h.age_component(comment_history.created_on,
1851 time_is_local=True),
1852 time_is_local=True),
1852 }
1853 }
1853 else:
1854 else:
1854 log.warning('No permissions for user %s to edit comment_id: %s',
1855 log.warning('No permissions for user %s to edit comment_id: %s',
1855 self._rhodecode_db_user, comment_id)
1856 self._rhodecode_db_user, comment_id)
1856 raise HTTPNotFound()
1857 raise HTTPNotFound()
@@ -1,843 +1,846 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 false,
40 false,
41 ChangesetComment,
41 ChangesetComment,
42 User,
42 User,
43 Notification,
43 Notification,
44 PullRequest,
44 PullRequest,
45 AttributeDict,
45 AttributeDict,
46 ChangesetCommentHistory,
46 ChangesetCommentHistory,
47 )
47 )
48 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.notification import NotificationModel
49 from rhodecode.model.meta import Session
49 from rhodecode.model.meta import Session
50 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.settings import VcsSettingsModel
51 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.notification import EmailNotificationModel
52 from rhodecode.model.validation_schema.schemas import comment_schema
52 from rhodecode.model.validation_schema.schemas import comment_schema
53
53
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class CommentsModel(BaseModel):
58 class CommentsModel(BaseModel):
59
59
60 cls = ChangesetComment
60 cls = ChangesetComment
61
61
62 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_BEFORE = 3
63 DIFF_CONTEXT_AFTER = 3
63 DIFF_CONTEXT_AFTER = 3
64
64
65 def __get_commit_comment(self, changeset_comment):
65 def __get_commit_comment(self, changeset_comment):
66 return self._get_instance(ChangesetComment, changeset_comment)
66 return self._get_instance(ChangesetComment, changeset_comment)
67
67
68 def __get_pull_request(self, pull_request):
68 def __get_pull_request(self, pull_request):
69 return self._get_instance(PullRequest, pull_request)
69 return self._get_instance(PullRequest, pull_request)
70
70
71 def _extract_mentions(self, s):
71 def _extract_mentions(self, s):
72 user_objects = []
72 user_objects = []
73 for username in extract_mentioned_users(s):
73 for username in extract_mentioned_users(s):
74 user_obj = User.get_by_username(username, case_insensitive=True)
74 user_obj = User.get_by_username(username, case_insensitive=True)
75 if user_obj:
75 if user_obj:
76 user_objects.append(user_obj)
76 user_objects.append(user_obj)
77 return user_objects
77 return user_objects
78
78
79 def _get_renderer(self, global_renderer='rst', request=None):
79 def _get_renderer(self, global_renderer='rst', request=None):
80 request = request or get_current_request()
80 request = request or get_current_request()
81
81
82 try:
82 try:
83 global_renderer = request.call_context.visual.default_renderer
83 global_renderer = request.call_context.visual.default_renderer
84 except AttributeError:
84 except AttributeError:
85 log.debug("Renderer not set, falling back "
85 log.debug("Renderer not set, falling back "
86 "to default renderer '%s'", global_renderer)
86 "to default renderer '%s'", global_renderer)
87 except Exception:
87 except Exception:
88 log.error(traceback.format_exc())
88 log.error(traceback.format_exc())
89 return global_renderer
89 return global_renderer
90
90
91 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 def aggregate_comments(self, comments, versions, show_version, inline=False):
92 # group by versions, and count until, and display objects
92 # group by versions, and count until, and display objects
93
93
94 comment_groups = collections.defaultdict(list)
94 comment_groups = collections.defaultdict(list)
95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
96
96
97 def yield_comments(pos):
97 def yield_comments(pos):
98 for co in comment_groups[pos]:
98 for co in comment_groups[pos]:
99 yield co
99 yield co
100
100
101 comment_versions = collections.defaultdict(
101 comment_versions = collections.defaultdict(
102 lambda: collections.defaultdict(list))
102 lambda: collections.defaultdict(list))
103 prev_prvid = -1
103 prev_prvid = -1
104 # fake last entry with None, to aggregate on "latest" version which
104 # fake last entry with None, to aggregate on "latest" version which
105 # doesn't have an pull_request_version_id
105 # doesn't have an pull_request_version_id
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
107 prvid = ver.pull_request_version_id
107 prvid = ver.pull_request_version_id
108 if prev_prvid == -1:
108 if prev_prvid == -1:
109 prev_prvid = prvid
109 prev_prvid = prvid
110
110
111 for co in yield_comments(prvid):
111 for co in yield_comments(prvid):
112 comment_versions[prvid]['at'].append(co)
112 comment_versions[prvid]['at'].append(co)
113
113
114 # save until
114 # save until
115 current = comment_versions[prvid]['at']
115 current = comment_versions[prvid]['at']
116 prev_until = comment_versions[prev_prvid]['until']
116 prev_until = comment_versions[prev_prvid]['until']
117 cur_until = prev_until + current
117 cur_until = prev_until + current
118 comment_versions[prvid]['until'].extend(cur_until)
118 comment_versions[prvid]['until'].extend(cur_until)
119
119
120 # save outdated
120 # save outdated
121 if inline:
121 if inline:
122 outdated = [x for x in cur_until
122 outdated = [x for x in cur_until
123 if x.outdated_at_version(show_version)]
123 if x.outdated_at_version(show_version)]
124 else:
124 else:
125 outdated = [x for x in cur_until
125 outdated = [x for x in cur_until
126 if x.older_than_version(show_version)]
126 if x.older_than_version(show_version)]
127 display = [x for x in cur_until if x not in outdated]
127 display = [x for x in cur_until if x not in outdated]
128
128
129 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['outdated'] = outdated
130 comment_versions[prvid]['display'] = display
130 comment_versions[prvid]['display'] = display
131
131
132 prev_prvid = prvid
132 prev_prvid = prvid
133
133
134 return comment_versions
134 return comment_versions
135
135
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
137 qry = Session().query(ChangesetComment) \
137 qry = Session().query(ChangesetComment) \
138 .filter(ChangesetComment.repo == repo)
138 .filter(ChangesetComment.repo == repo)
139
139
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
142
142
143 if user:
143 if user:
144 user = self._get_user(user)
144 user = self._get_user(user)
145 if user:
145 if user:
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
147
147
148 if commit_id:
148 if commit_id:
149 qry = qry.filter(ChangesetComment.revision == commit_id)
149 qry = qry.filter(ChangesetComment.revision == commit_id)
150
150
151 qry = qry.order_by(ChangesetComment.created_on)
151 qry = qry.order_by(ChangesetComment.created_on)
152 return qry.all()
152 return qry.all()
153
153
154 def get_repository_unresolved_todos(self, repo):
154 def get_repository_unresolved_todos(self, repo):
155 todos = Session().query(ChangesetComment) \
155 todos = Session().query(ChangesetComment) \
156 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.repo == repo) \
157 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.resolved_by == None) \
158 .filter(ChangesetComment.comment_type
158 .filter(ChangesetComment.comment_type
159 == ChangesetComment.COMMENT_TYPE_TODO)
159 == ChangesetComment.COMMENT_TYPE_TODO)
160 todos = todos.all()
160 todos = todos.all()
161
161
162 return todos
162 return todos
163
163
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
165
165
166 todos = Session().query(ChangesetComment) \
166 todos = Session().query(ChangesetComment) \
167 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.pull_request == pull_request) \
168 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.resolved_by == None) \
169 .filter(ChangesetComment.comment_type
169 .filter(ChangesetComment.comment_type
170 == ChangesetComment.COMMENT_TYPE_TODO)
170 == ChangesetComment.COMMENT_TYPE_TODO)
171
171
172 if not include_drafts:
172 if not include_drafts:
173 todos = todos.filter(ChangesetComment.draft == false())
173 todos = todos.filter(ChangesetComment.draft == false())
174
174
175 if not show_outdated:
175 if not show_outdated:
176 todos = todos.filter(
176 todos = todos.filter(
177 coalesce(ChangesetComment.display_state, '') !=
177 coalesce(ChangesetComment.display_state, '') !=
178 ChangesetComment.COMMENT_OUTDATED)
178 ChangesetComment.COMMENT_OUTDATED)
179
179
180 todos = todos.all()
180 todos = todos.all()
181
181
182 return todos
182 return todos
183
183
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
185
185
186 todos = Session().query(ChangesetComment) \
186 todos = Session().query(ChangesetComment) \
187 .filter(ChangesetComment.pull_request == pull_request) \
187 .filter(ChangesetComment.pull_request == pull_request) \
188 .filter(ChangesetComment.resolved_by != None) \
188 .filter(ChangesetComment.resolved_by != None) \
189 .filter(ChangesetComment.comment_type
189 .filter(ChangesetComment.comment_type
190 == ChangesetComment.COMMENT_TYPE_TODO)
190 == ChangesetComment.COMMENT_TYPE_TODO)
191
191
192 if not include_drafts:
192 if not include_drafts:
193 todos = todos.filter(ChangesetComment.draft == false())
193 todos = todos.filter(ChangesetComment.draft == false())
194
194
195 if not show_outdated:
195 if not show_outdated:
196 todos = todos.filter(
196 todos = todos.filter(
197 coalesce(ChangesetComment.display_state, '') !=
197 coalesce(ChangesetComment.display_state, '') !=
198 ChangesetComment.COMMENT_OUTDATED)
198 ChangesetComment.COMMENT_OUTDATED)
199
199
200 todos = todos.all()
200 todos = todos.all()
201
201
202 return todos
202 return todos
203
203
204 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
204 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
205
205
206 todos = Session().query(ChangesetComment) \
206 todos = Session().query(ChangesetComment) \
207 .filter(ChangesetComment.revision == commit_id) \
207 .filter(ChangesetComment.revision == commit_id) \
208 .filter(ChangesetComment.resolved_by == None) \
208 .filter(ChangesetComment.resolved_by == None) \
209 .filter(ChangesetComment.comment_type
209 .filter(ChangesetComment.comment_type
210 == ChangesetComment.COMMENT_TYPE_TODO)
210 == ChangesetComment.COMMENT_TYPE_TODO)
211
211
212 if not include_drafts:
212 if not include_drafts:
213 todos = todos.filter(ChangesetComment.draft == false())
213 todos = todos.filter(ChangesetComment.draft == false())
214
214
215 if not show_outdated:
215 if not show_outdated:
216 todos = todos.filter(
216 todos = todos.filter(
217 coalesce(ChangesetComment.display_state, '') !=
217 coalesce(ChangesetComment.display_state, '') !=
218 ChangesetComment.COMMENT_OUTDATED)
218 ChangesetComment.COMMENT_OUTDATED)
219
219
220 todos = todos.all()
220 todos = todos.all()
221
221
222 return todos
222 return todos
223
223
224 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
224 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
225
225
226 todos = Session().query(ChangesetComment) \
226 todos = Session().query(ChangesetComment) \
227 .filter(ChangesetComment.revision == commit_id) \
227 .filter(ChangesetComment.revision == commit_id) \
228 .filter(ChangesetComment.resolved_by != None) \
228 .filter(ChangesetComment.resolved_by != None) \
229 .filter(ChangesetComment.comment_type
229 .filter(ChangesetComment.comment_type
230 == ChangesetComment.COMMENT_TYPE_TODO)
230 == ChangesetComment.COMMENT_TYPE_TODO)
231
231
232 if not include_drafts:
232 if not include_drafts:
233 todos = todos.filter(ChangesetComment.draft == false())
233 todos = todos.filter(ChangesetComment.draft == false())
234
234
235 if not show_outdated:
235 if not show_outdated:
236 todos = todos.filter(
236 todos = todos.filter(
237 coalesce(ChangesetComment.display_state, '') !=
237 coalesce(ChangesetComment.display_state, '') !=
238 ChangesetComment.COMMENT_OUTDATED)
238 ChangesetComment.COMMENT_OUTDATED)
239
239
240 todos = todos.all()
240 todos = todos.all()
241
241
242 return todos
242 return todos
243
243
244 def get_commit_inline_comments(self, commit_id, include_drafts=True):
244 def get_commit_inline_comments(self, commit_id, include_drafts=True):
245 inline_comments = Session().query(ChangesetComment) \
245 inline_comments = Session().query(ChangesetComment) \
246 .filter(ChangesetComment.line_no != None) \
246 .filter(ChangesetComment.line_no != None) \
247 .filter(ChangesetComment.f_path != None) \
247 .filter(ChangesetComment.f_path != None) \
248 .filter(ChangesetComment.revision == commit_id)
248 .filter(ChangesetComment.revision == commit_id)
249
249
250 if not include_drafts:
250 if not include_drafts:
251 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
251 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
252
252
253 inline_comments = inline_comments.all()
253 inline_comments = inline_comments.all()
254 return inline_comments
254 return inline_comments
255
255
256 def _log_audit_action(self, action, action_data, auth_user, comment):
256 def _log_audit_action(self, action, action_data, auth_user, comment):
257 audit_logger.store(
257 audit_logger.store(
258 action=action,
258 action=action,
259 action_data=action_data,
259 action_data=action_data,
260 user=auth_user,
260 user=auth_user,
261 repo=comment.repo)
261 repo=comment.repo)
262
262
263 def create(self, text, repo, user, commit_id=None, pull_request=None,
263 def create(self, text, repo, user, commit_id=None, pull_request=None,
264 f_path=None, line_no=None, status_change=None,
264 f_path=None, line_no=None, status_change=None,
265 status_change_type=None, comment_type=None, is_draft=False,
265 status_change_type=None, comment_type=None, is_draft=False,
266 resolves_comment_id=None, closing_pr=False, send_email=True,
266 resolves_comment_id=None, closing_pr=False, send_email=True,
267 renderer=None, auth_user=None, extra_recipients=None):
267 renderer=None, auth_user=None, extra_recipients=None):
268 """
268 """
269 Creates new comment for commit or pull request.
269 Creates new comment for commit or pull request.
270 IF status_change is not none this comment is associated with a
270 IF status_change is not none this comment is associated with a
271 status change of commit or commit associated with pull request
271 status change of commit or commit associated with pull request
272
272
273 :param text:
273 :param text:
274 :param repo:
274 :param repo:
275 :param user:
275 :param user:
276 :param commit_id:
276 :param commit_id:
277 :param pull_request:
277 :param pull_request:
278 :param f_path:
278 :param f_path:
279 :param line_no:
279 :param line_no:
280 :param status_change: Label for status change
280 :param status_change: Label for status change
281 :param comment_type: Type of comment
281 :param comment_type: Type of comment
282 :param is_draft: is comment a draft only
282 :param is_draft: is comment a draft only
283 :param resolves_comment_id: id of comment which this one will resolve
283 :param resolves_comment_id: id of comment which this one will resolve
284 :param status_change_type: type of status change
284 :param status_change_type: type of status change
285 :param closing_pr:
285 :param closing_pr:
286 :param send_email:
286 :param send_email:
287 :param renderer: pick renderer for this comment
287 :param renderer: pick renderer for this comment
288 :param auth_user: current authenticated user calling this method
288 :param auth_user: current authenticated user calling this method
289 :param extra_recipients: list of extra users to be added to recipients
289 :param extra_recipients: list of extra users to be added to recipients
290 """
290 """
291
291
292 if not text:
292 if not text:
293 log.warning('Missing text for comment, skipping...')
293 log.warning('Missing text for comment, skipping...')
294 return
294 return
295 request = get_current_request()
295 request = get_current_request()
296 _ = request.translate
296 _ = request.translate
297
297
298 if not renderer:
298 if not renderer:
299 renderer = self._get_renderer(request=request)
299 renderer = self._get_renderer(request=request)
300
300
301 repo = self._get_repo(repo)
301 repo = self._get_repo(repo)
302 user = self._get_user(user)
302 user = self._get_user(user)
303 auth_user = auth_user or user
303 auth_user = auth_user or user
304
304
305 schema = comment_schema.CommentSchema()
305 schema = comment_schema.CommentSchema()
306 validated_kwargs = schema.deserialize(dict(
306 validated_kwargs = schema.deserialize(dict(
307 comment_body=text,
307 comment_body=text,
308 comment_type=comment_type,
308 comment_type=comment_type,
309 is_draft=is_draft,
309 is_draft=is_draft,
310 comment_file=f_path,
310 comment_file=f_path,
311 comment_line=line_no,
311 comment_line=line_no,
312 renderer_type=renderer,
312 renderer_type=renderer,
313 status_change=status_change_type,
313 status_change=status_change_type,
314 resolves_comment_id=resolves_comment_id,
314 resolves_comment_id=resolves_comment_id,
315 repo=repo.repo_id,
315 repo=repo.repo_id,
316 user=user.user_id,
316 user=user.user_id,
317 ))
317 ))
318 is_draft = validated_kwargs['is_draft']
318 is_draft = validated_kwargs['is_draft']
319
319
320 comment = ChangesetComment()
320 comment = ChangesetComment()
321 comment.renderer = validated_kwargs['renderer_type']
321 comment.renderer = validated_kwargs['renderer_type']
322 comment.text = validated_kwargs['comment_body']
322 comment.text = validated_kwargs['comment_body']
323 comment.f_path = validated_kwargs['comment_file']
323 comment.f_path = validated_kwargs['comment_file']
324 comment.line_no = validated_kwargs['comment_line']
324 comment.line_no = validated_kwargs['comment_line']
325 comment.comment_type = validated_kwargs['comment_type']
325 comment.comment_type = validated_kwargs['comment_type']
326 comment.draft = is_draft
326 comment.draft = is_draft
327
327
328 comment.repo = repo
328 comment.repo = repo
329 comment.author = user
329 comment.author = user
330 resolved_comment = self.__get_commit_comment(
330 resolved_comment = self.__get_commit_comment(
331 validated_kwargs['resolves_comment_id'])
331 validated_kwargs['resolves_comment_id'])
332 # check if the comment actually belongs to this PR
332 # check if the comment actually belongs to this PR
333 if resolved_comment and resolved_comment.pull_request and \
333 if resolved_comment and resolved_comment.pull_request and \
334 resolved_comment.pull_request != pull_request:
334 resolved_comment.pull_request != pull_request:
335 log.warning('Comment tried to resolved unrelated todo comment: %s',
335 log.warning('Comment tried to resolved unrelated todo comment: %s',
336 resolved_comment)
336 resolved_comment)
337 # comment not bound to this pull request, forbid
337 # comment not bound to this pull request, forbid
338 resolved_comment = None
338 resolved_comment = None
339
339
340 elif resolved_comment and resolved_comment.repo and \
340 elif resolved_comment and resolved_comment.repo and \
341 resolved_comment.repo != repo:
341 resolved_comment.repo != repo:
342 log.warning('Comment tried to resolved unrelated todo comment: %s',
342 log.warning('Comment tried to resolved unrelated todo comment: %s',
343 resolved_comment)
343 resolved_comment)
344 # comment not bound to this repo, forbid
344 # comment not bound to this repo, forbid
345 resolved_comment = None
345 resolved_comment = None
346
346
347 comment.resolved_comment = resolved_comment
347 comment.resolved_comment = resolved_comment
348
348
349 pull_request_id = pull_request
349 pull_request_id = pull_request
350
350
351 commit_obj = None
351 commit_obj = None
352 pull_request_obj = None
352 pull_request_obj = None
353
353
354 if commit_id:
354 if commit_id:
355 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
355 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
356 # do a lookup, so we don't pass something bad here
356 # do a lookup, so we don't pass something bad here
357 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
357 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
358 comment.revision = commit_obj.raw_id
358 comment.revision = commit_obj.raw_id
359
359
360 elif pull_request_id:
360 elif pull_request_id:
361 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
361 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
362 pull_request_obj = self.__get_pull_request(pull_request_id)
362 pull_request_obj = self.__get_pull_request(pull_request_id)
363 comment.pull_request = pull_request_obj
363 comment.pull_request = pull_request_obj
364 else:
364 else:
365 raise Exception('Please specify commit or pull_request_id')
365 raise Exception('Please specify commit or pull_request_id')
366
366
367 Session().add(comment)
367 Session().add(comment)
368 Session().flush()
368 Session().flush()
369 kwargs = {
369 kwargs = {
370 'user': user,
370 'user': user,
371 'renderer_type': renderer,
371 'renderer_type': renderer,
372 'repo_name': repo.repo_name,
372 'repo_name': repo.repo_name,
373 'status_change': status_change,
373 'status_change': status_change,
374 'status_change_type': status_change_type,
374 'status_change_type': status_change_type,
375 'comment_body': text,
375 'comment_body': text,
376 'comment_file': f_path,
376 'comment_file': f_path,
377 'comment_line': line_no,
377 'comment_line': line_no,
378 'comment_type': comment_type or 'note',
378 'comment_type': comment_type or 'note',
379 'comment_id': comment.comment_id
379 'comment_id': comment.comment_id
380 }
380 }
381
381
382 if commit_obj:
382 if commit_obj:
383 recipients = ChangesetComment.get_users(
383 recipients = ChangesetComment.get_users(
384 revision=commit_obj.raw_id)
384 revision=commit_obj.raw_id)
385 # add commit author if it's in RhodeCode system
385 # add commit author if it's in RhodeCode system
386 cs_author = User.get_from_cs_author(commit_obj.author)
386 cs_author = User.get_from_cs_author(commit_obj.author)
387 if not cs_author:
387 if not cs_author:
388 # use repo owner if we cannot extract the author correctly
388 # use repo owner if we cannot extract the author correctly
389 cs_author = repo.user
389 cs_author = repo.user
390 recipients += [cs_author]
390 recipients += [cs_author]
391
391
392 commit_comment_url = self.get_url(comment, request=request)
392 commit_comment_url = self.get_url(comment, request=request)
393 commit_comment_reply_url = self.get_url(
393 commit_comment_reply_url = self.get_url(
394 comment, request=request,
394 comment, request=request,
395 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
395 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
396
396
397 target_repo_url = h.link_to(
397 target_repo_url = h.link_to(
398 repo.repo_name,
398 repo.repo_name,
399 h.route_url('repo_summary', repo_name=repo.repo_name))
399 h.route_url('repo_summary', repo_name=repo.repo_name))
400
400
401 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
401 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
402 commit_id=commit_id)
402 commit_id=commit_id)
403
403
404 # commit specifics
404 # commit specifics
405 kwargs.update({
405 kwargs.update({
406 'commit': commit_obj,
406 'commit': commit_obj,
407 'commit_message': commit_obj.message,
407 'commit_message': commit_obj.message,
408 'commit_target_repo_url': target_repo_url,
408 'commit_target_repo_url': target_repo_url,
409 'commit_comment_url': commit_comment_url,
409 'commit_comment_url': commit_comment_url,
410 'commit_comment_reply_url': commit_comment_reply_url,
410 'commit_comment_reply_url': commit_comment_reply_url,
411 'commit_url': commit_url,
411 'commit_url': commit_url,
412 'thread_ids': [commit_url, commit_comment_url],
412 'thread_ids': [commit_url, commit_comment_url],
413 })
413 })
414
414
415 elif pull_request_obj:
415 elif pull_request_obj:
416 # get the current participants of this pull request
416 # get the current participants of this pull request
417 recipients = ChangesetComment.get_users(
417 recipients = ChangesetComment.get_users(
418 pull_request_id=pull_request_obj.pull_request_id)
418 pull_request_id=pull_request_obj.pull_request_id)
419 # add pull request author
419 # add pull request author
420 recipients += [pull_request_obj.author]
420 recipients += [pull_request_obj.author]
421
421
422 # add the reviewers to notification
422 # add the reviewers to notification
423 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
423 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
424
424
425 pr_target_repo = pull_request_obj.target_repo
425 pr_target_repo = pull_request_obj.target_repo
426 pr_source_repo = pull_request_obj.source_repo
426 pr_source_repo = pull_request_obj.source_repo
427
427
428 pr_comment_url = self.get_url(comment, request=request)
428 pr_comment_url = self.get_url(comment, request=request)
429 pr_comment_reply_url = self.get_url(
429 pr_comment_reply_url = self.get_url(
430 comment, request=request,
430 comment, request=request,
431 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
431 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
432
432
433 pr_url = h.route_url(
433 pr_url = h.route_url(
434 'pullrequest_show',
434 'pullrequest_show',
435 repo_name=pr_target_repo.repo_name,
435 repo_name=pr_target_repo.repo_name,
436 pull_request_id=pull_request_obj.pull_request_id, )
436 pull_request_id=pull_request_obj.pull_request_id, )
437
437
438 # set some variables for email notification
438 # set some variables for email notification
439 pr_target_repo_url = h.route_url(
439 pr_target_repo_url = h.route_url(
440 'repo_summary', repo_name=pr_target_repo.repo_name)
440 'repo_summary', repo_name=pr_target_repo.repo_name)
441
441
442 pr_source_repo_url = h.route_url(
442 pr_source_repo_url = h.route_url(
443 'repo_summary', repo_name=pr_source_repo.repo_name)
443 'repo_summary', repo_name=pr_source_repo.repo_name)
444
444
445 # pull request specifics
445 # pull request specifics
446 kwargs.update({
446 kwargs.update({
447 'pull_request': pull_request_obj,
447 'pull_request': pull_request_obj,
448 'pr_id': pull_request_obj.pull_request_id,
448 'pr_id': pull_request_obj.pull_request_id,
449 'pull_request_url': pr_url,
449 'pull_request_url': pr_url,
450 'pull_request_target_repo': pr_target_repo,
450 'pull_request_target_repo': pr_target_repo,
451 'pull_request_target_repo_url': pr_target_repo_url,
451 'pull_request_target_repo_url': pr_target_repo_url,
452 'pull_request_source_repo': pr_source_repo,
452 'pull_request_source_repo': pr_source_repo,
453 'pull_request_source_repo_url': pr_source_repo_url,
453 'pull_request_source_repo_url': pr_source_repo_url,
454 'pr_comment_url': pr_comment_url,
454 'pr_comment_url': pr_comment_url,
455 'pr_comment_reply_url': pr_comment_reply_url,
455 'pr_comment_reply_url': pr_comment_reply_url,
456 'pr_closing': closing_pr,
456 'pr_closing': closing_pr,
457 'thread_ids': [pr_url, pr_comment_url],
457 'thread_ids': [pr_url, pr_comment_url],
458 })
458 })
459
459
460 if send_email:
460 if send_email:
461 recipients += [self._get_user(u) for u in (extra_recipients or [])]
461 recipients += [self._get_user(u) for u in (extra_recipients or [])]
462 # pre-generate the subject for notification itself
462 # pre-generate the subject for notification itself
463 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
463 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
464 notification_type, **kwargs)
464 notification_type, **kwargs)
465
465
466 mention_recipients = set(
466 mention_recipients = set(
467 self._extract_mentions(text)).difference(recipients)
467 self._extract_mentions(text)).difference(recipients)
468
468
469 # create notification objects, and emails
469 # create notification objects, and emails
470 NotificationModel().create(
470 NotificationModel().create(
471 created_by=user,
471 created_by=user,
472 notification_subject=subject,
472 notification_subject=subject,
473 notification_body=body_plaintext,
473 notification_body=body_plaintext,
474 notification_type=notification_type,
474 notification_type=notification_type,
475 recipients=recipients,
475 recipients=recipients,
476 mention_recipients=mention_recipients,
476 mention_recipients=mention_recipients,
477 email_kwargs=kwargs,
477 email_kwargs=kwargs,
478 )
478 )
479
479
480 Session().flush()
480 Session().flush()
481 if comment.pull_request:
481 if comment.pull_request:
482 action = 'repo.pull_request.comment.create'
482 action = 'repo.pull_request.comment.create'
483 else:
483 else:
484 action = 'repo.commit.comment.create'
484 action = 'repo.commit.comment.create'
485
485
486 if not is_draft:
486 if not is_draft:
487 comment_data = comment.get_api_data()
487 comment_data = comment.get_api_data()
488
488
489 self._log_audit_action(
489 self._log_audit_action(
490 action, {'data': comment_data}, auth_user, comment)
490 action, {'data': comment_data}, auth_user, comment)
491
491
492 return comment
492 return comment
493
493
494 def edit(self, comment_id, text, auth_user, version):
494 def edit(self, comment_id, text, auth_user, version):
495 """
495 """
496 Change existing comment for commit or pull request.
496 Change existing comment for commit or pull request.
497
497
498 :param comment_id:
498 :param comment_id:
499 :param text:
499 :param text:
500 :param auth_user: current authenticated user calling this method
500 :param auth_user: current authenticated user calling this method
501 :param version: last comment version
501 :param version: last comment version
502 """
502 """
503 if not text:
503 if not text:
504 log.warning('Missing text for comment, skipping...')
504 log.warning('Missing text for comment, skipping...')
505 return
505 return
506
506
507 comment = ChangesetComment.get(comment_id)
507 comment = ChangesetComment.get(comment_id)
508 old_comment_text = comment.text
508 old_comment_text = comment.text
509 comment.text = text
509 comment.text = text
510 comment.modified_at = datetime.datetime.now()
510 comment.modified_at = datetime.datetime.now()
511 version = safe_int(version)
511 version = safe_int(version)
512
512
513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
514 # would return 3 here
514 # would return 3 here
515 comment_version = ChangesetCommentHistory.get_version(comment_id)
515 comment_version = ChangesetCommentHistory.get_version(comment_id)
516
516
517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
518 log.warning(
518 log.warning(
519 'Version mismatch comment_version {} submitted {}, skipping'.format(
519 'Version mismatch comment_version {} submitted {}, skipping'.format(
520 comment_version-1, # -1 since note above
520 comment_version-1, # -1 since note above
521 version
521 version
522 )
522 )
523 )
523 )
524 raise CommentVersionMismatch()
524 raise CommentVersionMismatch()
525
525
526 comment_history = ChangesetCommentHistory()
526 comment_history = ChangesetCommentHistory()
527 comment_history.comment_id = comment_id
527 comment_history.comment_id = comment_id
528 comment_history.version = comment_version
528 comment_history.version = comment_version
529 comment_history.created_by_user_id = auth_user.user_id
529 comment_history.created_by_user_id = auth_user.user_id
530 comment_history.text = old_comment_text
530 comment_history.text = old_comment_text
531 # TODO add email notification
531 # TODO add email notification
532 Session().add(comment_history)
532 Session().add(comment_history)
533 Session().add(comment)
533 Session().add(comment)
534 Session().flush()
534 Session().flush()
535
535
536 if comment.pull_request:
536 if comment.pull_request:
537 action = 'repo.pull_request.comment.edit'
537 action = 'repo.pull_request.comment.edit'
538 else:
538 else:
539 action = 'repo.commit.comment.edit'
539 action = 'repo.commit.comment.edit'
540
540
541 comment_data = comment.get_api_data()
541 comment_data = comment.get_api_data()
542 comment_data['old_comment_text'] = old_comment_text
542 comment_data['old_comment_text'] = old_comment_text
543 self._log_audit_action(
543 self._log_audit_action(
544 action, {'data': comment_data}, auth_user, comment)
544 action, {'data': comment_data}, auth_user, comment)
545
545
546 return comment_history
546 return comment_history
547
547
548 def delete(self, comment, auth_user):
548 def delete(self, comment, auth_user):
549 """
549 """
550 Deletes given comment
550 Deletes given comment
551 """
551 """
552 comment = self.__get_commit_comment(comment)
552 comment = self.__get_commit_comment(comment)
553 old_data = comment.get_api_data()
553 old_data = comment.get_api_data()
554 Session().delete(comment)
554 Session().delete(comment)
555
555
556 if comment.pull_request:
556 if comment.pull_request:
557 action = 'repo.pull_request.comment.delete'
557 action = 'repo.pull_request.comment.delete'
558 else:
558 else:
559 action = 'repo.commit.comment.delete'
559 action = 'repo.commit.comment.delete'
560
560
561 self._log_audit_action(
561 self._log_audit_action(
562 action, {'old_data': old_data}, auth_user, comment)
562 action, {'old_data': old_data}, auth_user, comment)
563
563
564 return comment
564 return comment
565
565
566 def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
566 def get_all_comments(self, repo_id, revision=None, pull_request=None,
567 include_drafts=True, count_only=False):
567 q = ChangesetComment.query()\
568 q = ChangesetComment.query()\
568 .filter(ChangesetComment.repo_id == repo_id)
569 .filter(ChangesetComment.repo_id == repo_id)
569 if revision:
570 if revision:
570 q = q.filter(ChangesetComment.revision == revision)
571 q = q.filter(ChangesetComment.revision == revision)
571 elif pull_request:
572 elif pull_request:
572 pull_request = self.__get_pull_request(pull_request)
573 pull_request = self.__get_pull_request(pull_request)
573 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
574 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
574 else:
575 else:
575 raise Exception('Please specify commit or pull_request')
576 raise Exception('Please specify commit or pull_request')
577 if not include_drafts:
578 q = q.filter(ChangesetComment.draft == false())
576 q = q.order_by(ChangesetComment.created_on)
579 q = q.order_by(ChangesetComment.created_on)
577 if count_only:
580 if count_only:
578 return q.count()
581 return q.count()
579
582
580 return q.all()
583 return q.all()
581
584
582 def get_url(self, comment, request=None, permalink=False, anchor=None):
585 def get_url(self, comment, request=None, permalink=False, anchor=None):
583 if not request:
586 if not request:
584 request = get_current_request()
587 request = get_current_request()
585
588
586 comment = self.__get_commit_comment(comment)
589 comment = self.__get_commit_comment(comment)
587 if anchor is None:
590 if anchor is None:
588 anchor = 'comment-{}'.format(comment.comment_id)
591 anchor = 'comment-{}'.format(comment.comment_id)
589
592
590 if comment.pull_request:
593 if comment.pull_request:
591 pull_request = comment.pull_request
594 pull_request = comment.pull_request
592 if permalink:
595 if permalink:
593 return request.route_url(
596 return request.route_url(
594 'pull_requests_global',
597 'pull_requests_global',
595 pull_request_id=pull_request.pull_request_id,
598 pull_request_id=pull_request.pull_request_id,
596 _anchor=anchor)
599 _anchor=anchor)
597 else:
600 else:
598 return request.route_url(
601 return request.route_url(
599 'pullrequest_show',
602 'pullrequest_show',
600 repo_name=safe_str(pull_request.target_repo.repo_name),
603 repo_name=safe_str(pull_request.target_repo.repo_name),
601 pull_request_id=pull_request.pull_request_id,
604 pull_request_id=pull_request.pull_request_id,
602 _anchor=anchor)
605 _anchor=anchor)
603
606
604 else:
607 else:
605 repo = comment.repo
608 repo = comment.repo
606 commit_id = comment.revision
609 commit_id = comment.revision
607
610
608 if permalink:
611 if permalink:
609 return request.route_url(
612 return request.route_url(
610 'repo_commit', repo_name=safe_str(repo.repo_id),
613 'repo_commit', repo_name=safe_str(repo.repo_id),
611 commit_id=commit_id,
614 commit_id=commit_id,
612 _anchor=anchor)
615 _anchor=anchor)
613
616
614 else:
617 else:
615 return request.route_url(
618 return request.route_url(
616 'repo_commit', repo_name=safe_str(repo.repo_name),
619 'repo_commit', repo_name=safe_str(repo.repo_name),
617 commit_id=commit_id,
620 commit_id=commit_id,
618 _anchor=anchor)
621 _anchor=anchor)
619
622
620 def get_comments(self, repo_id, revision=None, pull_request=None):
623 def get_comments(self, repo_id, revision=None, pull_request=None):
621 """
624 """
622 Gets main comments based on revision or pull_request_id
625 Gets main comments based on revision or pull_request_id
623
626
624 :param repo_id:
627 :param repo_id:
625 :param revision:
628 :param revision:
626 :param pull_request:
629 :param pull_request:
627 """
630 """
628
631
629 q = ChangesetComment.query()\
632 q = ChangesetComment.query()\
630 .filter(ChangesetComment.repo_id == repo_id)\
633 .filter(ChangesetComment.repo_id == repo_id)\
631 .filter(ChangesetComment.line_no == None)\
634 .filter(ChangesetComment.line_no == None)\
632 .filter(ChangesetComment.f_path == None)
635 .filter(ChangesetComment.f_path == None)
633 if revision:
636 if revision:
634 q = q.filter(ChangesetComment.revision == revision)
637 q = q.filter(ChangesetComment.revision == revision)
635 elif pull_request:
638 elif pull_request:
636 pull_request = self.__get_pull_request(pull_request)
639 pull_request = self.__get_pull_request(pull_request)
637 q = q.filter(ChangesetComment.pull_request == pull_request)
640 q = q.filter(ChangesetComment.pull_request == pull_request)
638 else:
641 else:
639 raise Exception('Please specify commit or pull_request')
642 raise Exception('Please specify commit or pull_request')
640 q = q.order_by(ChangesetComment.created_on)
643 q = q.order_by(ChangesetComment.created_on)
641 return q.all()
644 return q.all()
642
645
643 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
646 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
644 q = self._get_inline_comments_query(repo_id, revision, pull_request)
647 q = self._get_inline_comments_query(repo_id, revision, pull_request)
645 return self._group_comments_by_path_and_line_number(q)
648 return self._group_comments_by_path_and_line_number(q)
646
649
647 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
650 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
648 version=None):
651 version=None):
649 inline_comms = []
652 inline_comms = []
650 for fname, per_line_comments in inline_comments.iteritems():
653 for fname, per_line_comments in inline_comments.iteritems():
651 for lno, comments in per_line_comments.iteritems():
654 for lno, comments in per_line_comments.iteritems():
652 for comm in comments:
655 for comm in comments:
653 if not comm.outdated_at_version(version) and skip_outdated:
656 if not comm.outdated_at_version(version) and skip_outdated:
654 inline_comms.append(comm)
657 inline_comms.append(comm)
655
658
656 return inline_comms
659 return inline_comms
657
660
658 def get_outdated_comments(self, repo_id, pull_request):
661 def get_outdated_comments(self, repo_id, pull_request):
659 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
662 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
660 # of a pull request.
663 # of a pull request.
661 q = self._all_inline_comments_of_pull_request(pull_request)
664 q = self._all_inline_comments_of_pull_request(pull_request)
662 q = q.filter(
665 q = q.filter(
663 ChangesetComment.display_state ==
666 ChangesetComment.display_state ==
664 ChangesetComment.COMMENT_OUTDATED
667 ChangesetComment.COMMENT_OUTDATED
665 ).order_by(ChangesetComment.comment_id.asc())
668 ).order_by(ChangesetComment.comment_id.asc())
666
669
667 return self._group_comments_by_path_and_line_number(q)
670 return self._group_comments_by_path_and_line_number(q)
668
671
669 def _get_inline_comments_query(self, repo_id, revision, pull_request):
672 def _get_inline_comments_query(self, repo_id, revision, pull_request):
670 # TODO: johbo: Split this into two methods: One for PR and one for
673 # TODO: johbo: Split this into two methods: One for PR and one for
671 # commit.
674 # commit.
672 if revision:
675 if revision:
673 q = Session().query(ChangesetComment).filter(
676 q = Session().query(ChangesetComment).filter(
674 ChangesetComment.repo_id == repo_id,
677 ChangesetComment.repo_id == repo_id,
675 ChangesetComment.line_no != null(),
678 ChangesetComment.line_no != null(),
676 ChangesetComment.f_path != null(),
679 ChangesetComment.f_path != null(),
677 ChangesetComment.revision == revision)
680 ChangesetComment.revision == revision)
678
681
679 elif pull_request:
682 elif pull_request:
680 pull_request = self.__get_pull_request(pull_request)
683 pull_request = self.__get_pull_request(pull_request)
681 if not CommentsModel.use_outdated_comments(pull_request):
684 if not CommentsModel.use_outdated_comments(pull_request):
682 q = self._visible_inline_comments_of_pull_request(pull_request)
685 q = self._visible_inline_comments_of_pull_request(pull_request)
683 else:
686 else:
684 q = self._all_inline_comments_of_pull_request(pull_request)
687 q = self._all_inline_comments_of_pull_request(pull_request)
685
688
686 else:
689 else:
687 raise Exception('Please specify commit or pull_request_id')
690 raise Exception('Please specify commit or pull_request_id')
688 q = q.order_by(ChangesetComment.comment_id.asc())
691 q = q.order_by(ChangesetComment.comment_id.asc())
689 return q
692 return q
690
693
691 def _group_comments_by_path_and_line_number(self, q):
694 def _group_comments_by_path_and_line_number(self, q):
692 comments = q.all()
695 comments = q.all()
693 paths = collections.defaultdict(lambda: collections.defaultdict(list))
696 paths = collections.defaultdict(lambda: collections.defaultdict(list))
694 for co in comments:
697 for co in comments:
695 paths[co.f_path][co.line_no].append(co)
698 paths[co.f_path][co.line_no].append(co)
696 return paths
699 return paths
697
700
698 @classmethod
701 @classmethod
699 def needed_extra_diff_context(cls):
702 def needed_extra_diff_context(cls):
700 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
703 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
701
704
702 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
705 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
703 if not CommentsModel.use_outdated_comments(pull_request):
706 if not CommentsModel.use_outdated_comments(pull_request):
704 return
707 return
705
708
706 comments = self._visible_inline_comments_of_pull_request(pull_request)
709 comments = self._visible_inline_comments_of_pull_request(pull_request)
707 comments_to_outdate = comments.all()
710 comments_to_outdate = comments.all()
708
711
709 for comment in comments_to_outdate:
712 for comment in comments_to_outdate:
710 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
713 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
711
714
712 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
715 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
713 diff_line = _parse_comment_line_number(comment.line_no)
716 diff_line = _parse_comment_line_number(comment.line_no)
714
717
715 try:
718 try:
716 old_context = old_diff_proc.get_context_of_line(
719 old_context = old_diff_proc.get_context_of_line(
717 path=comment.f_path, diff_line=diff_line)
720 path=comment.f_path, diff_line=diff_line)
718 new_context = new_diff_proc.get_context_of_line(
721 new_context = new_diff_proc.get_context_of_line(
719 path=comment.f_path, diff_line=diff_line)
722 path=comment.f_path, diff_line=diff_line)
720 except (diffs.LineNotInDiffException,
723 except (diffs.LineNotInDiffException,
721 diffs.FileNotInDiffException):
724 diffs.FileNotInDiffException):
722 comment.display_state = ChangesetComment.COMMENT_OUTDATED
725 comment.display_state = ChangesetComment.COMMENT_OUTDATED
723 return
726 return
724
727
725 if old_context == new_context:
728 if old_context == new_context:
726 return
729 return
727
730
728 if self._should_relocate_diff_line(diff_line):
731 if self._should_relocate_diff_line(diff_line):
729 new_diff_lines = new_diff_proc.find_context(
732 new_diff_lines = new_diff_proc.find_context(
730 path=comment.f_path, context=old_context,
733 path=comment.f_path, context=old_context,
731 offset=self.DIFF_CONTEXT_BEFORE)
734 offset=self.DIFF_CONTEXT_BEFORE)
732 if not new_diff_lines:
735 if not new_diff_lines:
733 comment.display_state = ChangesetComment.COMMENT_OUTDATED
736 comment.display_state = ChangesetComment.COMMENT_OUTDATED
734 else:
737 else:
735 new_diff_line = self._choose_closest_diff_line(
738 new_diff_line = self._choose_closest_diff_line(
736 diff_line, new_diff_lines)
739 diff_line, new_diff_lines)
737 comment.line_no = _diff_to_comment_line_number(new_diff_line)
740 comment.line_no = _diff_to_comment_line_number(new_diff_line)
738 else:
741 else:
739 comment.display_state = ChangesetComment.COMMENT_OUTDATED
742 comment.display_state = ChangesetComment.COMMENT_OUTDATED
740
743
741 def _should_relocate_diff_line(self, diff_line):
744 def _should_relocate_diff_line(self, diff_line):
742 """
745 """
743 Checks if relocation shall be tried for the given `diff_line`.
746 Checks if relocation shall be tried for the given `diff_line`.
744
747
745 If a comment points into the first lines, then we can have a situation
748 If a comment points into the first lines, then we can have a situation
746 that after an update another line has been added on top. In this case
749 that after an update another line has been added on top. In this case
747 we would find the context still and move the comment around. This
750 we would find the context still and move the comment around. This
748 would be wrong.
751 would be wrong.
749 """
752 """
750 should_relocate = (
753 should_relocate = (
751 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
754 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
752 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
755 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
753 return should_relocate
756 return should_relocate
754
757
755 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
758 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
756 candidate = new_diff_lines[0]
759 candidate = new_diff_lines[0]
757 best_delta = _diff_line_delta(diff_line, candidate)
760 best_delta = _diff_line_delta(diff_line, candidate)
758 for new_diff_line in new_diff_lines[1:]:
761 for new_diff_line in new_diff_lines[1:]:
759 delta = _diff_line_delta(diff_line, new_diff_line)
762 delta = _diff_line_delta(diff_line, new_diff_line)
760 if delta < best_delta:
763 if delta < best_delta:
761 candidate = new_diff_line
764 candidate = new_diff_line
762 best_delta = delta
765 best_delta = delta
763 return candidate
766 return candidate
764
767
765 def _visible_inline_comments_of_pull_request(self, pull_request):
768 def _visible_inline_comments_of_pull_request(self, pull_request):
766 comments = self._all_inline_comments_of_pull_request(pull_request)
769 comments = self._all_inline_comments_of_pull_request(pull_request)
767 comments = comments.filter(
770 comments = comments.filter(
768 coalesce(ChangesetComment.display_state, '') !=
771 coalesce(ChangesetComment.display_state, '') !=
769 ChangesetComment.COMMENT_OUTDATED)
772 ChangesetComment.COMMENT_OUTDATED)
770 return comments
773 return comments
771
774
772 def _all_inline_comments_of_pull_request(self, pull_request):
775 def _all_inline_comments_of_pull_request(self, pull_request):
773 comments = Session().query(ChangesetComment)\
776 comments = Session().query(ChangesetComment)\
774 .filter(ChangesetComment.line_no != None)\
777 .filter(ChangesetComment.line_no != None)\
775 .filter(ChangesetComment.f_path != None)\
778 .filter(ChangesetComment.f_path != None)\
776 .filter(ChangesetComment.pull_request == pull_request)
779 .filter(ChangesetComment.pull_request == pull_request)
777 return comments
780 return comments
778
781
779 def _all_general_comments_of_pull_request(self, pull_request):
782 def _all_general_comments_of_pull_request(self, pull_request):
780 comments = Session().query(ChangesetComment)\
783 comments = Session().query(ChangesetComment)\
781 .filter(ChangesetComment.line_no == None)\
784 .filter(ChangesetComment.line_no == None)\
782 .filter(ChangesetComment.f_path == None)\
785 .filter(ChangesetComment.f_path == None)\
783 .filter(ChangesetComment.pull_request == pull_request)
786 .filter(ChangesetComment.pull_request == pull_request)
784
787
785 return comments
788 return comments
786
789
787 @staticmethod
790 @staticmethod
788 def use_outdated_comments(pull_request):
791 def use_outdated_comments(pull_request):
789 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
792 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
790 settings = settings_model.get_general_settings()
793 settings = settings_model.get_general_settings()
791 return settings.get('rhodecode_use_outdated_comments', False)
794 return settings.get('rhodecode_use_outdated_comments', False)
792
795
793 def trigger_commit_comment_hook(self, repo, user, action, data=None):
796 def trigger_commit_comment_hook(self, repo, user, action, data=None):
794 repo = self._get_repo(repo)
797 repo = self._get_repo(repo)
795 target_scm = repo.scm_instance()
798 target_scm = repo.scm_instance()
796 if action == 'create':
799 if action == 'create':
797 trigger_hook = hooks_utils.trigger_comment_commit_hooks
800 trigger_hook = hooks_utils.trigger_comment_commit_hooks
798 elif action == 'edit':
801 elif action == 'edit':
799 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
802 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
800 else:
803 else:
801 return
804 return
802
805
803 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
806 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
804 repo, action, trigger_hook)
807 repo, action, trigger_hook)
805 trigger_hook(
808 trigger_hook(
806 username=user.username,
809 username=user.username,
807 repo_name=repo.repo_name,
810 repo_name=repo.repo_name,
808 repo_type=target_scm.alias,
811 repo_type=target_scm.alias,
809 repo=repo,
812 repo=repo,
810 data=data)
813 data=data)
811
814
812
815
813 def _parse_comment_line_number(line_no):
816 def _parse_comment_line_number(line_no):
814 """
817 """
815 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
818 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
816 """
819 """
817 old_line = None
820 old_line = None
818 new_line = None
821 new_line = None
819 if line_no.startswith('o'):
822 if line_no.startswith('o'):
820 old_line = int(line_no[1:])
823 old_line = int(line_no[1:])
821 elif line_no.startswith('n'):
824 elif line_no.startswith('n'):
822 new_line = int(line_no[1:])
825 new_line = int(line_no[1:])
823 else:
826 else:
824 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
827 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
825 return diffs.DiffLineNumber(old_line, new_line)
828 return diffs.DiffLineNumber(old_line, new_line)
826
829
827
830
828 def _diff_to_comment_line_number(diff_line):
831 def _diff_to_comment_line_number(diff_line):
829 if diff_line.new is not None:
832 if diff_line.new is not None:
830 return u'n{}'.format(diff_line.new)
833 return u'n{}'.format(diff_line.new)
831 elif diff_line.old is not None:
834 elif diff_line.old is not None:
832 return u'o{}'.format(diff_line.old)
835 return u'o{}'.format(diff_line.old)
833 return u''
836 return u''
834
837
835
838
836 def _diff_line_delta(a, b):
839 def _diff_line_delta(a, b):
837 if None not in (a.new, b.new):
840 if None not in (a.new, b.new):
838 return abs(a.new - b.new)
841 return abs(a.new - b.new)
839 elif None not in (a.old, b.old):
842 elif None not in (a.old, b.old):
840 return abs(a.old - b.old)
843 return abs(a.old - b.old)
841 else:
844 else:
842 raise ValueError(
845 raise ValueError(
843 "Cannot compute delta between {} and {}".format(a, b))
846 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1415 +1,1415 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
3
3
4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
5 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
6 %></%def>
6 %></%def>
7
7
8 <%def name="action_class(action)">
8 <%def name="action_class(action)">
9 <%
9 <%
10 return {
10 return {
11 '-': 'cb-deletion',
11 '-': 'cb-deletion',
12 '+': 'cb-addition',
12 '+': 'cb-addition',
13 ' ': 'cb-context',
13 ' ': 'cb-context',
14 }.get(action, 'cb-empty')
14 }.get(action, 'cb-empty')
15 %>
15 %>
16 </%def>
16 </%def>
17
17
18 <%def name="op_class(op_id)">
18 <%def name="op_class(op_id)">
19 <%
19 <%
20 return {
20 return {
21 DEL_FILENODE: 'deletion', # file deleted
21 DEL_FILENODE: 'deletion', # file deleted
22 BIN_FILENODE: 'warning' # binary diff hidden
22 BIN_FILENODE: 'warning' # binary diff hidden
23 }.get(op_id, 'addition')
23 }.get(op_id, 'addition')
24 %>
24 %>
25 </%def>
25 </%def>
26
26
27
27
28
28
29 <%def name="render_diffset(diffset, commit=None,
29 <%def name="render_diffset(diffset, commit=None,
30
30
31 # collapse all file diff entries when there are more than this amount of files in the diff
31 # collapse all file diff entries when there are more than this amount of files in the diff
32 collapse_when_files_over=20,
32 collapse_when_files_over=20,
33
33
34 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 # collapse lines in the diff when more than this amount of lines changed in the file diff
35 lines_changed_limit=500,
35 lines_changed_limit=500,
36
36
37 # add a ruler at to the output
37 # add a ruler at to the output
38 ruler_at_chars=0,
38 ruler_at_chars=0,
39
39
40 # show inline comments
40 # show inline comments
41 use_comments=False,
41 use_comments=False,
42
42
43 # disable new comments
43 # disable new comments
44 disable_new_comments=False,
44 disable_new_comments=False,
45
45
46 # special file-comments that were deleted in previous versions
46 # special file-comments that were deleted in previous versions
47 # it's used for showing outdated comments for deleted files in a PR
47 # it's used for showing outdated comments for deleted files in a PR
48 deleted_files_comments=None,
48 deleted_files_comments=None,
49
49
50 # for cache purpose
50 # for cache purpose
51 inline_comments=None,
51 inline_comments=None,
52
52
53 # additional menu for PRs
53 # additional menu for PRs
54 pull_request_menu=None,
54 pull_request_menu=None,
55
55
56 # show/hide todo next to comments
56 # show/hide todo next to comments
57 show_todos=True,
57 show_todos=True,
58
58
59 )">
59 )">
60
60
61 <%
61 <%
62 diffset_container_id = h.md5(diffset.target_ref)
62 diffset_container_id = h.md5(diffset.target_ref)
63 collapse_all = len(diffset.files) > collapse_when_files_over
63 collapse_all = len(diffset.files) > collapse_when_files_over
64 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
64 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
65 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
65 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
66 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
66 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
67 %>
67 %>
68
68
69 %if use_comments:
69 %if use_comments:
70
70
71 ## Template for injecting comments
71 ## Template for injecting comments
72 <div id="cb-comments-inline-container-template" class="js-template">
72 <div id="cb-comments-inline-container-template" class="js-template">
73 ${inline_comments_container([])}
73 ${inline_comments_container([])}
74 </div>
74 </div>
75
75
76 <div class="js-template" id="cb-comment-inline-form-template">
76 <div class="js-template" id="cb-comment-inline-form-template">
77 <div class="comment-inline-form ac">
77 <div class="comment-inline-form ac">
78 %if not c.rhodecode_user.is_default:
78 %if not c.rhodecode_user.is_default:
79 ## render template for inline comments
79 ## render template for inline comments
80 ${commentblock.comment_form(form_type='inline')}
80 ${commentblock.comment_form(form_type='inline')}
81 %endif
81 %endif
82 </div>
82 </div>
83 </div>
83 </div>
84
84
85 %endif
85 %endif
86
86
87 %if c.user_session_attrs["diffmode"] == 'sideside':
87 %if c.user_session_attrs["diffmode"] == 'sideside':
88 <style>
88 <style>
89 .wrapper {
89 .wrapper {
90 max-width: 1600px !important;
90 max-width: 1600px !important;
91 }
91 }
92 </style>
92 </style>
93 %endif
93 %endif
94
94
95 %if ruler_at_chars:
95 %if ruler_at_chars:
96 <style>
96 <style>
97 .diff table.cb .cb-content:after {
97 .diff table.cb .cb-content:after {
98 content: "";
98 content: "";
99 border-left: 1px solid blue;
99 border-left: 1px solid blue;
100 position: absolute;
100 position: absolute;
101 top: 0;
101 top: 0;
102 height: 18px;
102 height: 18px;
103 opacity: .2;
103 opacity: .2;
104 z-index: 10;
104 z-index: 10;
105 //## +5 to account for diff action (+/-)
105 //## +5 to account for diff action (+/-)
106 left: ${ruler_at_chars + 5}ch;
106 left: ${ruler_at_chars + 5}ch;
107 </style>
107 </style>
108 %endif
108 %endif
109
109
110 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
111
111
112 <div style="height: 20px; line-height: 20px">
112 <div style="height: 20px; line-height: 20px">
113 ## expand/collapse action
113 ## expand/collapse action
114 <div class="pull-left">
114 <div class="pull-left">
115 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
115 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
116 % if collapse_all:
116 % if collapse_all:
117 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
117 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
118 % else:
118 % else:
119 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
119 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
120 % endif
120 % endif
121 </a>
121 </a>
122
122
123 </div>
123 </div>
124
124
125 ## todos
125 ## todos
126 % if show_todos and getattr(c, 'at_version', None):
126 % if show_todos and getattr(c, 'at_version', None):
127 <div class="pull-right">
127 <div class="pull-right">
128 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
128 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
129 ${_('not available in this view')}
129 ${_('not available in this view')}
130 </div>
130 </div>
131 % elif show_todos:
131 % elif show_todos:
132 <div class="pull-right">
132 <div class="pull-right">
133 <div class="comments-number" style="padding-left: 10px">
133 <div class="comments-number" style="padding-left: 10px">
134 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
134 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
135 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
135 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
136 % if c.unresolved_comments:
136 % if c.unresolved_comments:
137 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
137 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
138 ${_('{} unresolved').format(len(c.unresolved_comments))}
138 ${_('{} unresolved').format(len(c.unresolved_comments))}
139 </a>
139 </a>
140 % else:
140 % else:
141 ${_('0 unresolved')}
141 ${_('0 unresolved')}
142 % endif
142 % endif
143
143
144 ${_('{} Resolved').format(len(c.resolved_comments))}
144 ${_('{} Resolved').format(len(c.resolved_comments))}
145 % endif
145 % endif
146 </div>
146 </div>
147 </div>
147 </div>
148 % endif
148 % endif
149
149
150 ## ## comments
150 ## ## comments
151 ## <div class="pull-right">
151 ## <div class="pull-right">
152 ## <div class="comments-number" style="padding-left: 10px">
152 ## <div class="comments-number" style="padding-left: 10px">
153 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
153 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
154 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
154 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
155 ## % if c.comments:
155 ## % if c.comments:
156 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
156 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
157 ## % else:
157 ## % else:
158 ## ${_('0 General')}
158 ## ${_('0 General')}
159 ## % endif
159 ## % endif
160 ##
160 ##
161 ## % if c.inline_cnt:
161 ## % if c.inline_cnt:
162 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
162 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
163 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
163 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
164 ## </a>
164 ## </a>
165 ## % else:
165 ## % else:
166 ## ${_('0 Inline')}
166 ## ${_('0 Inline')}
167 ## % endif
167 ## % endif
168 ## % endif
168 ## % endif
169 ##
169 ##
170 ## % if pull_request_menu:
170 ## % if pull_request_menu:
171 ## <%
171 ## <%
172 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
172 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
173 ## %>
173 ## %>
174 ##
174 ##
175 ## % if outdated_comm_count_ver:
175 ## % if outdated_comm_count_ver:
176 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
176 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
177 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
177 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
178 ## </a>
178 ## </a>
179 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
179 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
180 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
180 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
181 ## % else:
181 ## % else:
182 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
182 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
183 ## % endif
183 ## % endif
184 ##
184 ##
185 ## % endif
185 ## % endif
186 ##
186 ##
187 ## </div>
187 ## </div>
188 ## </div>
188 ## </div>
189
189
190 </div>
190 </div>
191
191
192 % if diffset.limited_diff:
192 % if diffset.limited_diff:
193 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
193 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
194 <h2 class="clearinner">
194 <h2 class="clearinner">
195 ${_('The requested changes are too big and content was truncated.')}
195 ${_('The requested changes are too big and content was truncated.')}
196 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
196 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
197 </h2>
197 </h2>
198 </div>
198 </div>
199 % endif
199 % endif
200
200
201 <div id="todo-box">
201 <div id="todo-box">
202 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
202 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
203 % for co in c.unresolved_comments:
203 % for co in c.unresolved_comments:
204 <a class="permalink" href="#comment-${co.comment_id}"
204 <a class="permalink" href="#comment-${co.comment_id}"
205 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
205 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
206 <i class="icon-flag-filled-red"></i>
206 <i class="icon-flag-filled-red"></i>
207 ${co.comment_id}</a>${('' if loop.last else ',')}
207 ${co.comment_id}</a>${('' if loop.last else ',')}
208 % endfor
208 % endfor
209 % endif
209 % endif
210 </div>
210 </div>
211 %if diffset.has_hidden_changes:
211 %if diffset.has_hidden_changes:
212 <p class="empty_data">${_('Some changes may be hidden')}</p>
212 <p class="empty_data">${_('Some changes may be hidden')}</p>
213 %elif not diffset.files:
213 %elif not diffset.files:
214 <p class="empty_data">${_('No files')}</p>
214 <p class="empty_data">${_('No files')}</p>
215 %endif
215 %endif
216
216
217 <div class="filediffs">
217 <div class="filediffs">
218
218
219 ## initial value could be marked as False later on
219 ## initial value could be marked as False later on
220 <% over_lines_changed_limit = False %>
220 <% over_lines_changed_limit = False %>
221 %for i, filediff in enumerate(diffset.files):
221 %for i, filediff in enumerate(diffset.files):
222
222
223 %if filediff.source_file_path and filediff.target_file_path:
223 %if filediff.source_file_path and filediff.target_file_path:
224 %if filediff.source_file_path != filediff.target_file_path:
224 %if filediff.source_file_path != filediff.target_file_path:
225 ## file was renamed, or copied
225 ## file was renamed, or copied
226 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
226 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
227 <%
227 <%
228 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
228 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
229 final_path = filediff.target_file_path
229 final_path = filediff.target_file_path
230 %>
230 %>
231 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
231 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
232 <%
232 <%
233 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
233 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
234 final_path = filediff.target_file_path
234 final_path = filediff.target_file_path
235 %>
235 %>
236 %endif
236 %endif
237 %else:
237 %else:
238 ## file was modified
238 ## file was modified
239 <%
239 <%
240 final_file_name = filediff.source_file_path
240 final_file_name = filediff.source_file_path
241 final_path = final_file_name
241 final_path = final_file_name
242 %>
242 %>
243 %endif
243 %endif
244 %else:
244 %else:
245 %if filediff.source_file_path:
245 %if filediff.source_file_path:
246 ## file was deleted
246 ## file was deleted
247 <%
247 <%
248 final_file_name = filediff.source_file_path
248 final_file_name = filediff.source_file_path
249 final_path = final_file_name
249 final_path = final_file_name
250 %>
250 %>
251 %else:
251 %else:
252 ## file was added
252 ## file was added
253 <%
253 <%
254 final_file_name = filediff.target_file_path
254 final_file_name = filediff.target_file_path
255 final_path = final_file_name
255 final_path = final_file_name
256 %>
256 %>
257 %endif
257 %endif
258 %endif
258 %endif
259
259
260 <%
260 <%
261 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
261 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
262 over_lines_changed_limit = lines_changed > lines_changed_limit
262 over_lines_changed_limit = lines_changed > lines_changed_limit
263 %>
263 %>
264 ## anchor with support of sticky header
264 ## anchor with support of sticky header
265 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
265 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
266
266
267 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
267 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
268 <div
268 <div
269 class="filediff"
269 class="filediff"
270 data-f-path="${filediff.patch['filename']}"
270 data-f-path="${filediff.patch['filename']}"
271 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
271 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
272 >
272 >
273 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
273 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
274 <%
274 <%
275 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
275 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
276 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
276 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not (_c.outdated or _c.draft)]
277 %>
277 %>
278 <div class="filediff-collapse-indicator icon-"></div>
278 <div class="filediff-collapse-indicator icon-"></div>
279
279
280 ## Comments/Options PILL
280 ## Comments/Options PILL
281 <span class="pill-group pull-right">
281 <span class="pill-group pull-right">
282 <span class="pill" op="comments">
282 <span class="pill" op="comments">
283 <i class="icon-comment"></i> ${len(total_file_comments)}
283 <i class="icon-comment"></i> ${len(total_file_comments)}
284 </span>
284 </span>
285
285
286 <details class="details-reset details-inline-block">
286 <details class="details-reset details-inline-block">
287 <summary class="noselect">
287 <summary class="noselect">
288 <i class="pill icon-options cursor-pointer" op="options"></i>
288 <i class="pill icon-options cursor-pointer" op="options"></i>
289 </summary>
289 </summary>
290 <details-menu class="details-dropdown">
290 <details-menu class="details-dropdown">
291
291
292 <div class="dropdown-item">
292 <div class="dropdown-item">
293 <span>${final_path}</span>
293 <span>${final_path}</span>
294 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
294 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
295 </div>
295 </div>
296
296
297 <div class="dropdown-divider"></div>
297 <div class="dropdown-divider"></div>
298
298
299 <div class="dropdown-item">
299 <div class="dropdown-item">
300 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
300 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
301 <a href="${permalink}">ΒΆ permalink</a>
301 <a href="${permalink}">ΒΆ permalink</a>
302 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
302 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
303 </div>
303 </div>
304
304
305
305
306 </details-menu>
306 </details-menu>
307 </details>
307 </details>
308
308
309 </span>
309 </span>
310
310
311 ${diff_ops(final_file_name, filediff)}
311 ${diff_ops(final_file_name, filediff)}
312
312
313 </label>
313 </label>
314
314
315 ${diff_menu(filediff, use_comments=use_comments)}
315 ${diff_menu(filediff, use_comments=use_comments)}
316 <table id="file-${h.safeid(h.safe_unicode(filediff.patch['filename']))}" data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
316 <table id="file-${h.safeid(h.safe_unicode(filediff.patch['filename']))}" data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
317
317
318 ## new/deleted/empty content case
318 ## new/deleted/empty content case
319 % if not filediff.hunks:
319 % if not filediff.hunks:
320 ## Comment container, on "fakes" hunk that contains all data to render comments
320 ## Comment container, on "fakes" hunk that contains all data to render comments
321 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
321 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
322 % endif
322 % endif
323
323
324 %if filediff.limited_diff:
324 %if filediff.limited_diff:
325 <tr class="cb-warning cb-collapser">
325 <tr class="cb-warning cb-collapser">
326 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
326 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
327 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
327 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
328 </td>
328 </td>
329 </tr>
329 </tr>
330 %else:
330 %else:
331 %if over_lines_changed_limit:
331 %if over_lines_changed_limit:
332 <tr class="cb-warning cb-collapser">
332 <tr class="cb-warning cb-collapser">
333 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
333 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
334 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
334 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
335 <a href="#" class="cb-expand"
335 <a href="#" class="cb-expand"
336 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
336 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
337 </a>
337 </a>
338 <a href="#" class="cb-collapse"
338 <a href="#" class="cb-collapse"
339 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
339 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
340 </a>
340 </a>
341 </td>
341 </td>
342 </tr>
342 </tr>
343 %endif
343 %endif
344 %endif
344 %endif
345
345
346 % for hunk in filediff.hunks:
346 % for hunk in filediff.hunks:
347 <tr class="cb-hunk">
347 <tr class="cb-hunk">
348 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
348 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
349 ## TODO: dan: add ajax loading of more context here
349 ## TODO: dan: add ajax loading of more context here
350 ## <a href="#">
350 ## <a href="#">
351 <i class="icon-more"></i>
351 <i class="icon-more"></i>
352 ## </a>
352 ## </a>
353 </td>
353 </td>
354 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
354 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
355 @@
355 @@
356 -${hunk.source_start},${hunk.source_length}
356 -${hunk.source_start},${hunk.source_length}
357 +${hunk.target_start},${hunk.target_length}
357 +${hunk.target_start},${hunk.target_length}
358 ${hunk.section_header}
358 ${hunk.section_header}
359 </td>
359 </td>
360 </tr>
360 </tr>
361
361
362 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
362 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
363 % endfor
363 % endfor
364
364
365 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
365 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
366
366
367 ## outdated comments that do not fit into currently displayed lines
367 ## outdated comments that do not fit into currently displayed lines
368 % for lineno, comments in unmatched_comments.items():
368 % for lineno, comments in unmatched_comments.items():
369
369
370 %if c.user_session_attrs["diffmode"] == 'unified':
370 %if c.user_session_attrs["diffmode"] == 'unified':
371 % if loop.index == 0:
371 % if loop.index == 0:
372 <tr class="cb-hunk">
372 <tr class="cb-hunk">
373 <td colspan="3"></td>
373 <td colspan="3"></td>
374 <td>
374 <td>
375 <div>
375 <div>
376 ${_('Unmatched/outdated inline comments below')}
376 ${_('Unmatched/outdated inline comments below')}
377 </div>
377 </div>
378 </td>
378 </td>
379 </tr>
379 </tr>
380 % endif
380 % endif
381 <tr class="cb-line">
381 <tr class="cb-line">
382 <td class="cb-data cb-context"></td>
382 <td class="cb-data cb-context"></td>
383 <td class="cb-lineno cb-context"></td>
383 <td class="cb-lineno cb-context"></td>
384 <td class="cb-lineno cb-context"></td>
384 <td class="cb-lineno cb-context"></td>
385 <td class="cb-content cb-context">
385 <td class="cb-content cb-context">
386 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
386 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
387 </td>
387 </td>
388 </tr>
388 </tr>
389 %elif c.user_session_attrs["diffmode"] == 'sideside':
389 %elif c.user_session_attrs["diffmode"] == 'sideside':
390 % if loop.index == 0:
390 % if loop.index == 0:
391 <tr class="cb-comment-info">
391 <tr class="cb-comment-info">
392 <td colspan="2"></td>
392 <td colspan="2"></td>
393 <td class="cb-line">
393 <td class="cb-line">
394 <div>
394 <div>
395 ${_('Unmatched/outdated inline comments below')}
395 ${_('Unmatched/outdated inline comments below')}
396 </div>
396 </div>
397 </td>
397 </td>
398 <td colspan="2"></td>
398 <td colspan="2"></td>
399 <td class="cb-line">
399 <td class="cb-line">
400 <div>
400 <div>
401 ${_('Unmatched/outdated comments below')}
401 ${_('Unmatched/outdated comments below')}
402 </div>
402 </div>
403 </td>
403 </td>
404 </tr>
404 </tr>
405 % endif
405 % endif
406 <tr class="cb-line">
406 <tr class="cb-line">
407 <td class="cb-data cb-context"></td>
407 <td class="cb-data cb-context"></td>
408 <td class="cb-lineno cb-context"></td>
408 <td class="cb-lineno cb-context"></td>
409 <td class="cb-content cb-context">
409 <td class="cb-content cb-context">
410 % if lineno.startswith('o'):
410 % if lineno.startswith('o'):
411 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
411 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
412 % endif
412 % endif
413 </td>
413 </td>
414
414
415 <td class="cb-data cb-context"></td>
415 <td class="cb-data cb-context"></td>
416 <td class="cb-lineno cb-context"></td>
416 <td class="cb-lineno cb-context"></td>
417 <td class="cb-content cb-context">
417 <td class="cb-content cb-context">
418 % if lineno.startswith('n'):
418 % if lineno.startswith('n'):
419 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
419 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
420 % endif
420 % endif
421 </td>
421 </td>
422 </tr>
422 </tr>
423 %endif
423 %endif
424
424
425 % endfor
425 % endfor
426
426
427 </table>
427 </table>
428 </div>
428 </div>
429 %endfor
429 %endfor
430
430
431 ## outdated comments that are made for a file that has been deleted
431 ## outdated comments that are made for a file that has been deleted
432 % for filename, comments_dict in (deleted_files_comments or {}).items():
432 % for filename, comments_dict in (deleted_files_comments or {}).items():
433
433
434 <%
434 <%
435 display_state = 'display: none'
435 display_state = 'display: none'
436 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
436 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
437 if open_comments_in_file:
437 if open_comments_in_file:
438 display_state = ''
438 display_state = ''
439 fid = str(id(filename))
439 fid = str(id(filename))
440 %>
440 %>
441 <div class="filediffs filediff-outdated" style="${display_state}">
441 <div class="filediffs filediff-outdated" style="${display_state}">
442 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
442 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
443 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
443 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
444 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
444 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
445 <div class="filediff-collapse-indicator icon-"></div>
445 <div class="filediff-collapse-indicator icon-"></div>
446
446
447 <span class="pill">
447 <span class="pill">
448 ## file was deleted
448 ## file was deleted
449 ${filename}
449 ${filename}
450 </span>
450 </span>
451 <span class="pill-group pull-left" >
451 <span class="pill-group pull-left" >
452 ## file op, doesn't need translation
452 ## file op, doesn't need translation
453 <span class="pill" op="removed">unresolved comments</span>
453 <span class="pill" op="removed">unresolved comments</span>
454 </span>
454 </span>
455 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
455 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
456 <span class="pill-group pull-right">
456 <span class="pill-group pull-right">
457 <span class="pill" op="deleted">
457 <span class="pill" op="deleted">
458 % if comments_dict['stats'] >0:
458 % if comments_dict['stats'] >0:
459 -${comments_dict['stats']}
459 -${comments_dict['stats']}
460 % else:
460 % else:
461 ${comments_dict['stats']}
461 ${comments_dict['stats']}
462 % endif
462 % endif
463 </span>
463 </span>
464 </span>
464 </span>
465 </label>
465 </label>
466
466
467 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
467 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
468 <tr>
468 <tr>
469 % if c.user_session_attrs["diffmode"] == 'unified':
469 % if c.user_session_attrs["diffmode"] == 'unified':
470 <td></td>
470 <td></td>
471 %endif
471 %endif
472
472
473 <td></td>
473 <td></td>
474 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
474 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
475 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
475 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
476 ${_('There are still outdated/unresolved comments attached to it.')}
476 ${_('There are still outdated/unresolved comments attached to it.')}
477 </td>
477 </td>
478 </tr>
478 </tr>
479 %if c.user_session_attrs["diffmode"] == 'unified':
479 %if c.user_session_attrs["diffmode"] == 'unified':
480 <tr class="cb-line">
480 <tr class="cb-line">
481 <td class="cb-data cb-context"></td>
481 <td class="cb-data cb-context"></td>
482 <td class="cb-lineno cb-context"></td>
482 <td class="cb-lineno cb-context"></td>
483 <td class="cb-lineno cb-context"></td>
483 <td class="cb-lineno cb-context"></td>
484 <td class="cb-content cb-context">
484 <td class="cb-content cb-context">
485 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
485 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
486 </td>
486 </td>
487 </tr>
487 </tr>
488 %elif c.user_session_attrs["diffmode"] == 'sideside':
488 %elif c.user_session_attrs["diffmode"] == 'sideside':
489 <tr class="cb-line">
489 <tr class="cb-line">
490 <td class="cb-data cb-context"></td>
490 <td class="cb-data cb-context"></td>
491 <td class="cb-lineno cb-context"></td>
491 <td class="cb-lineno cb-context"></td>
492 <td class="cb-content cb-context"></td>
492 <td class="cb-content cb-context"></td>
493
493
494 <td class="cb-data cb-context"></td>
494 <td class="cb-data cb-context"></td>
495 <td class="cb-lineno cb-context"></td>
495 <td class="cb-lineno cb-context"></td>
496 <td class="cb-content cb-context">
496 <td class="cb-content cb-context">
497 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
497 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
498 </td>
498 </td>
499 </tr>
499 </tr>
500 %endif
500 %endif
501 </table>
501 </table>
502 </div>
502 </div>
503 </div>
503 </div>
504 % endfor
504 % endfor
505
505
506 </div>
506 </div>
507 </div>
507 </div>
508 </%def>
508 </%def>
509
509
510 <%def name="diff_ops(file_name, filediff)">
510 <%def name="diff_ops(file_name, filediff)">
511 <%
511 <%
512 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
512 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
513 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
513 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
514 %>
514 %>
515 <span class="pill">
515 <span class="pill">
516 <i class="icon-file-text"></i>
516 <i class="icon-file-text"></i>
517 ${file_name}
517 ${file_name}
518 </span>
518 </span>
519
519
520 <span class="pill-group pull-right">
520 <span class="pill-group pull-right">
521
521
522 ## ops pills
522 ## ops pills
523 %if filediff.limited_diff:
523 %if filediff.limited_diff:
524 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
524 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
525 %endif
525 %endif
526
526
527 %if NEW_FILENODE in filediff.patch['stats']['ops']:
527 %if NEW_FILENODE in filediff.patch['stats']['ops']:
528 <span class="pill" op="created">created</span>
528 <span class="pill" op="created">created</span>
529 %if filediff['target_mode'].startswith('120'):
529 %if filediff['target_mode'].startswith('120'):
530 <span class="pill" op="symlink">symlink</span>
530 <span class="pill" op="symlink">symlink</span>
531 %else:
531 %else:
532 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
532 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
533 %endif
533 %endif
534 %endif
534 %endif
535
535
536 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
536 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
537 <span class="pill" op="renamed">renamed</span>
537 <span class="pill" op="renamed">renamed</span>
538 %endif
538 %endif
539
539
540 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
540 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
541 <span class="pill" op="copied">copied</span>
541 <span class="pill" op="copied">copied</span>
542 %endif
542 %endif
543
543
544 %if DEL_FILENODE in filediff.patch['stats']['ops']:
544 %if DEL_FILENODE in filediff.patch['stats']['ops']:
545 <span class="pill" op="removed">removed</span>
545 <span class="pill" op="removed">removed</span>
546 %endif
546 %endif
547
547
548 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
548 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
549 <span class="pill" op="mode">
549 <span class="pill" op="mode">
550 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
550 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
551 </span>
551 </span>
552 %endif
552 %endif
553
553
554 %if BIN_FILENODE in filediff.patch['stats']['ops']:
554 %if BIN_FILENODE in filediff.patch['stats']['ops']:
555 <span class="pill" op="binary">binary</span>
555 <span class="pill" op="binary">binary</span>
556 %if MOD_FILENODE in filediff.patch['stats']['ops']:
556 %if MOD_FILENODE in filediff.patch['stats']['ops']:
557 <span class="pill" op="modified">modified</span>
557 <span class="pill" op="modified">modified</span>
558 %endif
558 %endif
559 %endif
559 %endif
560
560
561 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
561 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
562 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
562 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
563
563
564 </span>
564 </span>
565
565
566 </%def>
566 </%def>
567
567
568 <%def name="nice_mode(filemode)">
568 <%def name="nice_mode(filemode)">
569 ${(filemode.startswith('100') and filemode[3:] or filemode)}
569 ${(filemode.startswith('100') and filemode[3:] or filemode)}
570 </%def>
570 </%def>
571
571
572 <%def name="diff_menu(filediff, use_comments=False)">
572 <%def name="diff_menu(filediff, use_comments=False)">
573 <div class="filediff-menu">
573 <div class="filediff-menu">
574
574
575 %if filediff.diffset.source_ref:
575 %if filediff.diffset.source_ref:
576
576
577 ## FILE BEFORE CHANGES
577 ## FILE BEFORE CHANGES
578 %if filediff.operation in ['D', 'M']:
578 %if filediff.operation in ['D', 'M']:
579 <a
579 <a
580 class="tooltip"
580 class="tooltip"
581 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
581 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
582 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
582 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
583 >
583 >
584 ${_('Show file before')}
584 ${_('Show file before')}
585 </a> |
585 </a> |
586 %else:
586 %else:
587 <span
587 <span
588 class="tooltip"
588 class="tooltip"
589 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
589 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
590 >
590 >
591 ${_('Show file before')}
591 ${_('Show file before')}
592 </span> |
592 </span> |
593 %endif
593 %endif
594
594
595 ## FILE AFTER CHANGES
595 ## FILE AFTER CHANGES
596 %if filediff.operation in ['A', 'M']:
596 %if filediff.operation in ['A', 'M']:
597 <a
597 <a
598 class="tooltip"
598 class="tooltip"
599 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
599 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
600 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
600 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
601 >
601 >
602 ${_('Show file after')}
602 ${_('Show file after')}
603 </a>
603 </a>
604 %else:
604 %else:
605 <span
605 <span
606 class="tooltip"
606 class="tooltip"
607 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
607 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
608 >
608 >
609 ${_('Show file after')}
609 ${_('Show file after')}
610 </span>
610 </span>
611 %endif
611 %endif
612
612
613 % if use_comments:
613 % if use_comments:
614 |
614 |
615 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
615 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
616 data-toggle-on="${_('Hide comments')}"
616 data-toggle-on="${_('Hide comments')}"
617 data-toggle-off="${_('Show comments')}">
617 data-toggle-off="${_('Show comments')}">
618 <span class="hide-comment-button">${_('Hide comments')}</span>
618 <span class="hide-comment-button">${_('Hide comments')}</span>
619 </a>
619 </a>
620 % endif
620 % endif
621
621
622 %endif
622 %endif
623
623
624 </div>
624 </div>
625 </%def>
625 </%def>
626
626
627
627
628 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
628 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
629
629
630 <div class="inline-comments">
630 <div class="inline-comments">
631 %for comment in comments:
631 %for comment in comments:
632 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
632 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
633 %endfor
633 %endfor
634
634
635 <%
635 <%
636 extra_class = ''
636 extra_class = ''
637 extra_style = ''
637 extra_style = ''
638
638
639 if comments and comments[-1].outdated:
639 if comments and comments[-1].outdated:
640 extra_class = ' comment-outdated'
640 extra_class = ' comment-outdated'
641 extra_style = 'display: none;'
641 extra_style = 'display: none;'
642
642
643 %>
643 %>
644 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
644 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
645 <div class="reply-thread-container${extra_class}">
645 <div class="reply-thread-container${extra_class}">
646 <div class="reply-thread-gravatar">
646 <div class="reply-thread-gravatar">
647 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
647 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
648 </div>
648 </div>
649 <div class="reply-thread-reply-button">
649 <div class="reply-thread-reply-button">
650 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
650 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
651 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
651 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
652 </div>
652 </div>
653 <div class="reply-thread-last"></div>
653 <div class="reply-thread-last"></div>
654 </div>
654 </div>
655 </div>
655 </div>
656 </div>
656 </div>
657
657
658 </%def>
658 </%def>
659
659
660 <%!
660 <%!
661
661
662 def get_inline_comments(comments, filename):
662 def get_inline_comments(comments, filename):
663 if hasattr(filename, 'unicode_path'):
663 if hasattr(filename, 'unicode_path'):
664 filename = filename.unicode_path
664 filename = filename.unicode_path
665
665
666 if not isinstance(filename, (unicode, str)):
666 if not isinstance(filename, (unicode, str)):
667 return None
667 return None
668
668
669 if comments and filename in comments:
669 if comments and filename in comments:
670 return comments[filename]
670 return comments[filename]
671
671
672 return None
672 return None
673
673
674 def get_comments_for(diff_type, comments, filename, line_version, line_number):
674 def get_comments_for(diff_type, comments, filename, line_version, line_number):
675 if hasattr(filename, 'unicode_path'):
675 if hasattr(filename, 'unicode_path'):
676 filename = filename.unicode_path
676 filename = filename.unicode_path
677
677
678 if not isinstance(filename, (unicode, str)):
678 if not isinstance(filename, (unicode, str)):
679 return None
679 return None
680
680
681 file_comments = get_inline_comments(comments, filename)
681 file_comments = get_inline_comments(comments, filename)
682 if file_comments is None:
682 if file_comments is None:
683 return None
683 return None
684
684
685 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
685 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
686 if line_key in file_comments:
686 if line_key in file_comments:
687 data = file_comments.pop(line_key)
687 data = file_comments.pop(line_key)
688 return data
688 return data
689 %>
689 %>
690
690
691 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
691 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
692
692
693 <% chunk_count = 1 %>
693 <% chunk_count = 1 %>
694 %for loop_obj, item in h.looper(hunk.sideside):
694 %for loop_obj, item in h.looper(hunk.sideside):
695 <%
695 <%
696 line = item
696 line = item
697 i = loop_obj.index
697 i = loop_obj.index
698 prev_line = loop_obj.previous
698 prev_line = loop_obj.previous
699 old_line_anchor, new_line_anchor = None, None
699 old_line_anchor, new_line_anchor = None, None
700
700
701 if line.original.lineno:
701 if line.original.lineno:
702 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
702 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
703 if line.modified.lineno:
703 if line.modified.lineno:
704 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
704 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
705
705
706 line_action = line.modified.action or line.original.action
706 line_action = line.modified.action or line.original.action
707 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
707 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
708 %>
708 %>
709
709
710 <tr class="cb-line">
710 <tr class="cb-line">
711 <td class="cb-data ${action_class(line.original.action)}"
711 <td class="cb-data ${action_class(line.original.action)}"
712 data-line-no="${line.original.lineno}"
712 data-line-no="${line.original.lineno}"
713 >
713 >
714
714
715 <% line_old_comments, line_old_comments_no_drafts = None, None %>
715 <% line_old_comments, line_old_comments_no_drafts = None, None %>
716 %if line.original.get_comment_args:
716 %if line.original.get_comment_args:
717 <%
717 <%
718 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
718 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
719 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
719 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
720 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
720 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
721 %>
721 %>
722 %endif
722 %endif
723 %if line_old_comments_no_drafts:
723 %if line_old_comments_no_drafts:
724 % if has_outdated:
724 % if has_outdated:
725 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
725 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
726 % else:
726 % else:
727 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
727 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
728 % endif
728 % endif
729 %endif
729 %endif
730 </td>
730 </td>
731 <td class="cb-lineno ${action_class(line.original.action)}"
731 <td class="cb-lineno ${action_class(line.original.action)}"
732 data-line-no="${line.original.lineno}"
732 data-line-no="${line.original.lineno}"
733 %if old_line_anchor:
733 %if old_line_anchor:
734 id="${old_line_anchor}"
734 id="${old_line_anchor}"
735 %endif
735 %endif
736 >
736 >
737 %if line.original.lineno:
737 %if line.original.lineno:
738 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
738 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
739 %endif
739 %endif
740 </td>
740 </td>
741
741
742 <% line_no = 'o{}'.format(line.original.lineno) %>
742 <% line_no = 'o{}'.format(line.original.lineno) %>
743 <td class="cb-content ${action_class(line.original.action)}"
743 <td class="cb-content ${action_class(line.original.action)}"
744 data-line-no="${line_no}"
744 data-line-no="${line_no}"
745 >
745 >
746 %if use_comments and line.original.lineno:
746 %if use_comments and line.original.lineno:
747 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
747 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
748 %endif
748 %endif
749 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
749 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
750
750
751 %if use_comments and line.original.lineno and line_old_comments:
751 %if use_comments and line.original.lineno and line_old_comments:
752 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
752 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
753 %endif
753 %endif
754
754
755 </td>
755 </td>
756 <td class="cb-data ${action_class(line.modified.action)}"
756 <td class="cb-data ${action_class(line.modified.action)}"
757 data-line-no="${line.modified.lineno}"
757 data-line-no="${line.modified.lineno}"
758 >
758 >
759 <div>
759 <div>
760
760
761 <% line_new_comments, line_new_comments_no_drafts = None, None %>
761 <% line_new_comments, line_new_comments_no_drafts = None, None %>
762 %if line.modified.get_comment_args:
762 %if line.modified.get_comment_args:
763 <%
763 <%
764 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
764 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
765 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
765 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
766 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
766 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
767 %>
767 %>
768 %endif
768 %endif
769
769
770 %if line_new_comments_no_drafts:
770 %if line_new_comments_no_drafts:
771 % if has_outdated:
771 % if has_outdated:
772 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
772 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
773 % else:
773 % else:
774 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
774 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
775 % endif
775 % endif
776 %endif
776 %endif
777 </div>
777 </div>
778 </td>
778 </td>
779 <td class="cb-lineno ${action_class(line.modified.action)}"
779 <td class="cb-lineno ${action_class(line.modified.action)}"
780 data-line-no="${line.modified.lineno}"
780 data-line-no="${line.modified.lineno}"
781 %if new_line_anchor:
781 %if new_line_anchor:
782 id="${new_line_anchor}"
782 id="${new_line_anchor}"
783 %endif
783 %endif
784 >
784 >
785 %if line.modified.lineno:
785 %if line.modified.lineno:
786 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
786 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
787 %endif
787 %endif
788 </td>
788 </td>
789
789
790 <% line_no = 'n{}'.format(line.modified.lineno) %>
790 <% line_no = 'n{}'.format(line.modified.lineno) %>
791 <td class="cb-content ${action_class(line.modified.action)}"
791 <td class="cb-content ${action_class(line.modified.action)}"
792 data-line-no="${line_no}"
792 data-line-no="${line_no}"
793 >
793 >
794 %if use_comments and line.modified.lineno:
794 %if use_comments and line.modified.lineno:
795 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
795 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
796 %endif
796 %endif
797 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
797 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
798 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
798 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
799 <div class="nav-chunk" style="visibility: hidden">
799 <div class="nav-chunk" style="visibility: hidden">
800 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
800 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
801 </div>
801 </div>
802 <% chunk_count +=1 %>
802 <% chunk_count +=1 %>
803 % endif
803 % endif
804 %if use_comments and line.modified.lineno and line_new_comments:
804 %if use_comments and line.modified.lineno and line_new_comments:
805 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
805 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
806 %endif
806 %endif
807
807
808 </td>
808 </td>
809 </tr>
809 </tr>
810 %endfor
810 %endfor
811 </%def>
811 </%def>
812
812
813
813
814 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
814 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
815 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
815 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
816
816
817 <%
817 <%
818 old_line_anchor, new_line_anchor = None, None
818 old_line_anchor, new_line_anchor = None, None
819 if old_line_no:
819 if old_line_no:
820 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
820 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
821 if new_line_no:
821 if new_line_no:
822 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
822 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
823 %>
823 %>
824 <tr class="cb-line">
824 <tr class="cb-line">
825 <td class="cb-data ${action_class(action)}">
825 <td class="cb-data ${action_class(action)}">
826 <div>
826 <div>
827
827
828 <% comments, comments_no_drafts = None, None %>
828 <% comments, comments_no_drafts = None, None %>
829 %if comments_args:
829 %if comments_args:
830 <%
830 <%
831 comments = get_comments_for('unified', inline_comments, *comments_args)
831 comments = get_comments_for('unified', inline_comments, *comments_args)
832 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
832 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
833 has_outdated = any([x.outdated for x in comments_no_drafts])
833 has_outdated = any([x.outdated for x in comments_no_drafts])
834 %>
834 %>
835 %endif
835 %endif
836
836
837 % if comments_no_drafts:
837 % if comments_no_drafts:
838 % if has_outdated:
838 % if has_outdated:
839 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
839 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
840 % else:
840 % else:
841 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
841 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
842 % endif
842 % endif
843 % endif
843 % endif
844 </div>
844 </div>
845 </td>
845 </td>
846 <td class="cb-lineno ${action_class(action)}"
846 <td class="cb-lineno ${action_class(action)}"
847 data-line-no="${old_line_no}"
847 data-line-no="${old_line_no}"
848 %if old_line_anchor:
848 %if old_line_anchor:
849 id="${old_line_anchor}"
849 id="${old_line_anchor}"
850 %endif
850 %endif
851 >
851 >
852 %if old_line_anchor:
852 %if old_line_anchor:
853 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
853 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
854 %endif
854 %endif
855 </td>
855 </td>
856 <td class="cb-lineno ${action_class(action)}"
856 <td class="cb-lineno ${action_class(action)}"
857 data-line-no="${new_line_no}"
857 data-line-no="${new_line_no}"
858 %if new_line_anchor:
858 %if new_line_anchor:
859 id="${new_line_anchor}"
859 id="${new_line_anchor}"
860 %endif
860 %endif
861 >
861 >
862 %if new_line_anchor:
862 %if new_line_anchor:
863 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
863 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
864 %endif
864 %endif
865 </td>
865 </td>
866 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
866 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
867 <td class="cb-content ${action_class(action)}"
867 <td class="cb-content ${action_class(action)}"
868 data-line-no="${line_no}"
868 data-line-no="${line_no}"
869 >
869 >
870 %if use_comments:
870 %if use_comments:
871 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
871 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
872 %endif
872 %endif
873 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
873 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
874 %if use_comments and comments:
874 %if use_comments and comments:
875 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
875 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
876 %endif
876 %endif
877 </td>
877 </td>
878 </tr>
878 </tr>
879 %endfor
879 %endfor
880 </%def>
880 </%def>
881
881
882
882
883 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
883 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
884 % if diff_mode == 'unified':
884 % if diff_mode == 'unified':
885 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
885 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
886 % elif diff_mode == 'sideside':
886 % elif diff_mode == 'sideside':
887 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
887 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
888 % else:
888 % else:
889 <tr class="cb-line">
889 <tr class="cb-line">
890 <td>unknown diff mode</td>
890 <td>unknown diff mode</td>
891 </tr>
891 </tr>
892 % endif
892 % endif
893 </%def>file changes
893 </%def>file changes
894
894
895
895
896 <%def name="render_add_comment_button(line_no='', f_path='')">
896 <%def name="render_add_comment_button(line_no='', f_path='')">
897 % if not c.rhodecode_user.is_default:
897 % if not c.rhodecode_user.is_default:
898 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
898 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
899 <span><i class="icon-comment"></i></span>
899 <span><i class="icon-comment"></i></span>
900 </button>
900 </button>
901 % endif
901 % endif
902 </%def>
902 </%def>
903
903
904 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
904 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
905 <% diffset_container_id = h.md5(diffset.target_ref) %>
905 <% diffset_container_id = h.md5(diffset.target_ref) %>
906
906
907 <div id="diff-file-sticky" class="diffset-menu clearinner">
907 <div id="diff-file-sticky" class="diffset-menu clearinner">
908 ## auto adjustable
908 ## auto adjustable
909 <div class="sidebar__inner">
909 <div class="sidebar__inner">
910 <div class="sidebar__bar">
910 <div class="sidebar__bar">
911 <div class="pull-right">
911 <div class="pull-right">
912 <div class="btn-group">
912 <div class="btn-group">
913 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
913 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
914 <i class="icon-wide-mode"></i>
914 <i class="icon-wide-mode"></i>
915 </a>
915 </a>
916 </div>
916 </div>
917 <div class="btn-group">
917 <div class="btn-group">
918
918
919 <a
919 <a
920 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
920 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
921 title="${h.tooltip(_('View diff as side by side'))}"
921 title="${h.tooltip(_('View diff as side by side'))}"
922 href="${h.current_route_path(request, diffmode='sideside')}">
922 href="${h.current_route_path(request, diffmode='sideside')}">
923 <span>${_('Side by Side')}</span>
923 <span>${_('Side by Side')}</span>
924 </a>
924 </a>
925
925
926 <a
926 <a
927 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
927 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
928 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
928 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
929 <span>${_('Unified')}</span>
929 <span>${_('Unified')}</span>
930 </a>
930 </a>
931
931
932 % if range_diff_on is True:
932 % if range_diff_on is True:
933 <a
933 <a
934 title="${_('Turn off: Show the diff as commit range')}"
934 title="${_('Turn off: Show the diff as commit range')}"
935 class="btn btn-primary"
935 class="btn btn-primary"
936 href="${h.current_route_path(request, **{"range-diff":"0"})}">
936 href="${h.current_route_path(request, **{"range-diff":"0"})}">
937 <span>${_('Range Diff')}</span>
937 <span>${_('Range Diff')}</span>
938 </a>
938 </a>
939 % elif range_diff_on is False:
939 % elif range_diff_on is False:
940 <a
940 <a
941 title="${_('Show the diff as commit range')}"
941 title="${_('Show the diff as commit range')}"
942 class="btn"
942 class="btn"
943 href="${h.current_route_path(request, **{"range-diff":"1"})}">
943 href="${h.current_route_path(request, **{"range-diff":"1"})}">
944 <span>${_('Range Diff')}</span>
944 <span>${_('Range Diff')}</span>
945 </a>
945 </a>
946 % endif
946 % endif
947 </div>
947 </div>
948 <div class="btn-group">
948 <div class="btn-group">
949
949
950 <div class="pull-left">
950 <div class="pull-left">
951 ${h.hidden('diff_menu_{}'.format(diffset_container_id))}
951 ${h.hidden('diff_menu_{}'.format(diffset_container_id))}
952 </div>
952 </div>
953
953
954 </div>
954 </div>
955 </div>
955 </div>
956 <div class="pull-left">
956 <div class="pull-left">
957 <div class="btn-group">
957 <div class="btn-group">
958 <div class="pull-left">
958 <div class="pull-left">
959 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
959 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
960 </div>
960 </div>
961
961
962 </div>
962 </div>
963 </div>
963 </div>
964 </div>
964 </div>
965 <div class="fpath-placeholder pull-left">
965 <div class="fpath-placeholder pull-left">
966 <i class="icon-file-text"></i>
966 <i class="icon-file-text"></i>
967 <strong class="fpath-placeholder-text">
967 <strong class="fpath-placeholder-text">
968 Context file:
968 Context file:
969 </strong>
969 </strong>
970 </div>
970 </div>
971 <div class="pull-right noselect">
971 <div class="pull-right noselect">
972
972
973 %if commit:
973 %if commit:
974 <span>
974 <span>
975 <code>${h.show_id(commit)}</code>
975 <code>${h.show_id(commit)}</code>
976 </span>
976 </span>
977 %elif pull_request_menu and pull_request_menu.get('pull_request'):
977 %elif pull_request_menu and pull_request_menu.get('pull_request'):
978 <span>
978 <span>
979 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
979 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
980 </span>
980 </span>
981 %endif
981 %endif
982 % if commit or pull_request_menu:
982 % if commit or pull_request_menu:
983 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
983 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
984 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
984 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
985 <i class="icon-angle-up"></i>
985 <i class="icon-angle-up"></i>
986 </span>
986 </span>
987 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
987 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
988 <i class="icon-angle-down"></i>
988 <i class="icon-angle-down"></i>
989 </span>
989 </span>
990 % endif
990 % endif
991 </div>
991 </div>
992 <div class="sidebar_inner_shadow"></div>
992 <div class="sidebar_inner_shadow"></div>
993 </div>
993 </div>
994 </div>
994 </div>
995
995
996 % if diffset:
996 % if diffset:
997 %if diffset.limited_diff:
997 %if diffset.limited_diff:
998 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
998 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
999 %else:
999 %else:
1000 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
1000 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
1001 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
1001 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
1002
1002
1003 %endif
1003 %endif
1004 ## case on range-diff placeholder needs to be updated
1004 ## case on range-diff placeholder needs to be updated
1005 % if range_diff_on is True:
1005 % if range_diff_on is True:
1006 <% file_placeholder = _('Disabled on range diff') %>
1006 <% file_placeholder = _('Disabled on range diff') %>
1007 % endif
1007 % endif
1008
1008
1009 <script type="text/javascript">
1009 <script type="text/javascript">
1010 var feedFilesOptions = function (query, initialData) {
1010 var feedFilesOptions = function (query, initialData) {
1011 var data = {results: []};
1011 var data = {results: []};
1012 var isQuery = typeof query.term !== 'undefined';
1012 var isQuery = typeof query.term !== 'undefined';
1013
1013
1014 var section = _gettext('Changed files');
1014 var section = _gettext('Changed files');
1015 var filteredData = [];
1015 var filteredData = [];
1016
1016
1017 //filter results
1017 //filter results
1018 $.each(initialData.results, function (idx, value) {
1018 $.each(initialData.results, function (idx, value) {
1019
1019
1020 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
1020 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
1021 filteredData.push({
1021 filteredData.push({
1022 'id': this.id,
1022 'id': this.id,
1023 'text': this.text,
1023 'text': this.text,
1024 "ops": this.ops,
1024 "ops": this.ops,
1025 })
1025 })
1026 }
1026 }
1027
1027
1028 });
1028 });
1029
1029
1030 data.results = filteredData;
1030 data.results = filteredData;
1031
1031
1032 query.callback(data);
1032 query.callback(data);
1033 };
1033 };
1034
1034
1035 var selectionFormatter = function(data, escapeMarkup) {
1035 var selectionFormatter = function(data, escapeMarkup) {
1036 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
1036 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
1037 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
1037 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
1038 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
1038 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
1039 '<span class="pill" op="added">{0}</span>' +
1039 '<span class="pill" op="added">{0}</span>' +
1040 '<span class="pill" op="deleted">{1}</span>' +
1040 '<span class="pill" op="deleted">{1}</span>' +
1041 '</div>'
1041 '</div>'
1042 ;
1042 ;
1043 var added = data['ops']['added'];
1043 var added = data['ops']['added'];
1044 if (added === 0) {
1044 if (added === 0) {
1045 // don't show +0
1045 // don't show +0
1046 added = 0;
1046 added = 0;
1047 } else {
1047 } else {
1048 added = '+' + added;
1048 added = '+' + added;
1049 }
1049 }
1050
1050
1051 var deleted = -1*data['ops']['deleted'];
1051 var deleted = -1*data['ops']['deleted'];
1052
1052
1053 tmpl += pill.format(added, deleted);
1053 tmpl += pill.format(added, deleted);
1054 return container.format(tmpl);
1054 return container.format(tmpl);
1055 };
1055 };
1056 var formatFileResult = function(result, container, query, escapeMarkup) {
1056 var formatFileResult = function(result, container, query, escapeMarkup) {
1057 return selectionFormatter(result, escapeMarkup);
1057 return selectionFormatter(result, escapeMarkup);
1058 };
1058 };
1059
1059
1060 var formatSelection = function (data, container) {
1060 var formatSelection = function (data, container) {
1061 return '${file_placeholder}'
1061 return '${file_placeholder}'
1062 };
1062 };
1063
1063
1064 if (window.preloadFileFilterData === undefined) {
1064 if (window.preloadFileFilterData === undefined) {
1065 window.preloadFileFilterData = {}
1065 window.preloadFileFilterData = {}
1066 }
1066 }
1067
1067
1068 preloadFileFilterData["${diffset_container_id}"] = {
1068 preloadFileFilterData["${diffset_container_id}"] = {
1069 results: [
1069 results: [
1070 % for filediff in diffset.files:
1070 % for filediff in diffset.files:
1071 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
1071 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
1072 text:"${filediff.patch['filename']}",
1072 text:"${filediff.patch['filename']}",
1073 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
1073 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
1074 % endfor
1074 % endfor
1075 ]
1075 ]
1076 };
1076 };
1077
1077
1078 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
1078 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
1079 var diffFileFilter = $(diffFileFilterId).select2({
1079 var diffFileFilter = $(diffFileFilterId).select2({
1080 'dropdownAutoWidth': true,
1080 'dropdownAutoWidth': true,
1081 'width': 'auto',
1081 'width': 'auto',
1082
1082
1083 containerCssClass: "drop-menu",
1083 containerCssClass: "drop-menu",
1084 dropdownCssClass: "drop-menu-dropdown",
1084 dropdownCssClass: "drop-menu-dropdown",
1085 data: preloadFileFilterData["${diffset_container_id}"],
1085 data: preloadFileFilterData["${diffset_container_id}"],
1086 query: function(query) {
1086 query: function(query) {
1087 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1087 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1088 },
1088 },
1089 initSelection: function(element, callback) {
1089 initSelection: function(element, callback) {
1090 callback({'init': true});
1090 callback({'init': true});
1091 },
1091 },
1092 formatResult: formatFileResult,
1092 formatResult: formatFileResult,
1093 formatSelection: formatSelection
1093 formatSelection: formatSelection
1094 });
1094 });
1095
1095
1096 % if range_diff_on is True:
1096 % if range_diff_on is True:
1097 diffFileFilter.select2("enable", false);
1097 diffFileFilter.select2("enable", false);
1098 % endif
1098 % endif
1099
1099
1100 $(diffFileFilterId).on('select2-selecting', function (e) {
1100 $(diffFileFilterId).on('select2-selecting', function (e) {
1101 var idSelector = e.choice.id;
1101 var idSelector = e.choice.id;
1102
1102
1103 // expand the container if we quick-select the field
1103 // expand the container if we quick-select the field
1104 $('#'+idSelector).next().prop('checked', false);
1104 $('#'+idSelector).next().prop('checked', false);
1105 // hide the mast as we later do preventDefault()
1105 // hide the mast as we later do preventDefault()
1106 $("#select2-drop-mask").click();
1106 $("#select2-drop-mask").click();
1107
1107
1108 window.location.hash = '#'+idSelector;
1108 window.location.hash = '#'+idSelector;
1109 updateSticky();
1109 updateSticky();
1110
1110
1111 e.preventDefault();
1111 e.preventDefault();
1112 });
1112 });
1113
1113
1114 diffNavText = 'diff navigation:'
1114 diffNavText = 'diff navigation:'
1115
1115
1116 getCurrentChunk = function () {
1116 getCurrentChunk = function () {
1117
1117
1118 var chunksAll = $('.nav-chunk').filter(function () {
1118 var chunksAll = $('.nav-chunk').filter(function () {
1119 return $(this).parents('.filediff').prev().get(0).checked !== true
1119 return $(this).parents('.filediff').prev().get(0).checked !== true
1120 })
1120 })
1121 var chunkSelected = $('.nav-chunk.selected');
1121 var chunkSelected = $('.nav-chunk.selected');
1122 var initial = false;
1122 var initial = false;
1123
1123
1124 if (chunkSelected.length === 0) {
1124 if (chunkSelected.length === 0) {
1125 // no initial chunk selected, we pick first
1125 // no initial chunk selected, we pick first
1126 chunkSelected = $(chunksAll.get(0));
1126 chunkSelected = $(chunksAll.get(0));
1127 var initial = true;
1127 var initial = true;
1128 }
1128 }
1129
1129
1130 return {
1130 return {
1131 'all': chunksAll,
1131 'all': chunksAll,
1132 'selected': chunkSelected,
1132 'selected': chunkSelected,
1133 'initial': initial,
1133 'initial': initial,
1134 }
1134 }
1135 }
1135 }
1136
1136
1137 animateDiffNavText = function () {
1137 animateDiffNavText = function () {
1138 var $diffNav = $('#diff_nav')
1138 var $diffNav = $('#diff_nav')
1139
1139
1140 var callback = function () {
1140 var callback = function () {
1141 $diffNav.animate({'opacity': 1.00}, 200)
1141 $diffNav.animate({'opacity': 1.00}, 200)
1142 };
1142 };
1143 $diffNav.animate({'opacity': 0.15}, 200, callback);
1143 $diffNav.animate({'opacity': 0.15}, 200, callback);
1144 }
1144 }
1145
1145
1146 scrollToChunk = function (moveBy) {
1146 scrollToChunk = function (moveBy) {
1147 var chunk = getCurrentChunk();
1147 var chunk = getCurrentChunk();
1148 var all = chunk.all
1148 var all = chunk.all
1149 var selected = chunk.selected
1149 var selected = chunk.selected
1150
1150
1151 var curPos = all.index(selected);
1151 var curPos = all.index(selected);
1152 var newPos = curPos;
1152 var newPos = curPos;
1153 if (!chunk.initial) {
1153 if (!chunk.initial) {
1154 var newPos = curPos + moveBy;
1154 var newPos = curPos + moveBy;
1155 }
1155 }
1156
1156
1157 var curElem = all.get(newPos);
1157 var curElem = all.get(newPos);
1158
1158
1159 if (curElem === undefined) {
1159 if (curElem === undefined) {
1160 // end or back
1160 // end or back
1161 $('#diff_nav').html('no next diff element:')
1161 $('#diff_nav').html('no next diff element:')
1162 animateDiffNavText()
1162 animateDiffNavText()
1163 return
1163 return
1164 } else if (newPos < 0) {
1164 } else if (newPos < 0) {
1165 $('#diff_nav').html('no previous diff element:')
1165 $('#diff_nav').html('no previous diff element:')
1166 animateDiffNavText()
1166 animateDiffNavText()
1167 return
1167 return
1168 } else {
1168 } else {
1169 $('#diff_nav').html(diffNavText)
1169 $('#diff_nav').html(diffNavText)
1170 }
1170 }
1171
1171
1172 curElem = $(curElem)
1172 curElem = $(curElem)
1173 var offset = 100;
1173 var offset = 100;
1174 $(window).scrollTop(curElem.position().top - offset);
1174 $(window).scrollTop(curElem.position().top - offset);
1175
1175
1176 //clear selection
1176 //clear selection
1177 all.removeClass('selected')
1177 all.removeClass('selected')
1178 curElem.addClass('selected')
1178 curElem.addClass('selected')
1179 }
1179 }
1180
1180
1181 scrollToPrevChunk = function () {
1181 scrollToPrevChunk = function () {
1182 scrollToChunk(-1)
1182 scrollToChunk(-1)
1183 }
1183 }
1184 scrollToNextChunk = function () {
1184 scrollToNextChunk = function () {
1185 scrollToChunk(1)
1185 scrollToChunk(1)
1186 }
1186 }
1187
1187
1188 </script>
1188 </script>
1189 % endif
1189 % endif
1190
1190
1191 <script type="text/javascript">
1191 <script type="text/javascript">
1192 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1192 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1193
1193
1194 $(document).ready(function () {
1194 $(document).ready(function () {
1195
1195
1196 var contextPrefix = _gettext('Context file: ');
1196 var contextPrefix = _gettext('Context file: ');
1197 ## sticky sidebar
1197 ## sticky sidebar
1198 var sidebarElement = document.getElementById('diff-file-sticky');
1198 var sidebarElement = document.getElementById('diff-file-sticky');
1199 sidebar = new StickySidebar(sidebarElement, {
1199 sidebar = new StickySidebar(sidebarElement, {
1200 topSpacing: 0,
1200 topSpacing: 0,
1201 bottomSpacing: 0,
1201 bottomSpacing: 0,
1202 innerWrapperSelector: '.sidebar__inner'
1202 innerWrapperSelector: '.sidebar__inner'
1203 });
1203 });
1204 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1204 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1205 // reset our file so it's not holding new value
1205 // reset our file so it's not holding new value
1206 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1206 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1207 });
1207 });
1208
1208
1209 updateSticky = function () {
1209 updateSticky = function () {
1210 sidebar.updateSticky();
1210 sidebar.updateSticky();
1211 Waypoint.refreshAll();
1211 Waypoint.refreshAll();
1212 };
1212 };
1213
1213
1214 var animateText = function (fPath, anchorId) {
1214 var animateText = function (fPath, anchorId) {
1215 fPath = Select2.util.escapeMarkup(fPath);
1215 fPath = Select2.util.escapeMarkup(fPath);
1216 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1216 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1217 };
1217 };
1218
1218
1219 ## dynamic file waypoints
1219 ## dynamic file waypoints
1220 var setFPathInfo = function(fPath, anchorId){
1220 var setFPathInfo = function(fPath, anchorId){
1221 animateText(fPath, anchorId)
1221 animateText(fPath, anchorId)
1222 };
1222 };
1223
1223
1224 var codeBlock = $('.filediff');
1224 var codeBlock = $('.filediff');
1225
1225
1226 // forward waypoint
1226 // forward waypoint
1227 codeBlock.waypoint(
1227 codeBlock.waypoint(
1228 function(direction) {
1228 function(direction) {
1229 if (direction === "down"){
1229 if (direction === "down"){
1230 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1230 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1231 }
1231 }
1232 }, {
1232 }, {
1233 offset: function () {
1233 offset: function () {
1234 return 70;
1234 return 70;
1235 },
1235 },
1236 context: '.fpath-placeholder'
1236 context: '.fpath-placeholder'
1237 }
1237 }
1238 );
1238 );
1239
1239
1240 // backward waypoint
1240 // backward waypoint
1241 codeBlock.waypoint(
1241 codeBlock.waypoint(
1242 function(direction) {
1242 function(direction) {
1243 if (direction === "up"){
1243 if (direction === "up"){
1244 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1244 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1245 }
1245 }
1246 }, {
1246 }, {
1247 offset: function () {
1247 offset: function () {
1248 return -this.element.clientHeight + 90;
1248 return -this.element.clientHeight + 90;
1249 },
1249 },
1250 context: '.fpath-placeholder'
1250 context: '.fpath-placeholder'
1251 }
1251 }
1252 );
1252 );
1253
1253
1254 toggleWideDiff = function (el) {
1254 toggleWideDiff = function (el) {
1255 updateSticky();
1255 updateSticky();
1256 var wide = Rhodecode.comments.toggleWideMode(this);
1256 var wide = Rhodecode.comments.toggleWideMode(this);
1257 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1257 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1258 if (wide === true) {
1258 if (wide === true) {
1259 $(el).addClass('btn-active');
1259 $(el).addClass('btn-active');
1260 } else {
1260 } else {
1261 $(el).removeClass('btn-active');
1261 $(el).removeClass('btn-active');
1262 }
1262 }
1263 return null;
1263 return null;
1264 };
1264 };
1265
1265
1266 var preloadDiffMenuData = {
1266 var preloadDiffMenuData = {
1267 results: [
1267 results: [
1268
1268
1269 ## Whitespace change
1269 ## Whitespace change
1270 % if request.GET.get('ignorews', '') == '1':
1270 % if request.GET.get('ignorews', '') == '1':
1271 {
1271 {
1272 id: 2,
1272 id: 2,
1273 text: _gettext('Show whitespace changes'),
1273 text: _gettext('Show whitespace changes'),
1274 action: function () {},
1274 action: function () {},
1275 url: "${h.current_route_path(request, ignorews=0)|n}"
1275 url: "${h.current_route_path(request, ignorews=0)|n}"
1276 },
1276 },
1277 % else:
1277 % else:
1278 {
1278 {
1279 id: 2,
1279 id: 2,
1280 text: _gettext('Hide whitespace changes'),
1280 text: _gettext('Hide whitespace changes'),
1281 action: function () {},
1281 action: function () {},
1282 url: "${h.current_route_path(request, ignorews=1)|n}"
1282 url: "${h.current_route_path(request, ignorews=1)|n}"
1283 },
1283 },
1284 % endif
1284 % endif
1285
1285
1286 ## FULL CONTEXT
1286 ## FULL CONTEXT
1287 % if request.GET.get('fullcontext', '') == '1':
1287 % if request.GET.get('fullcontext', '') == '1':
1288 {
1288 {
1289 id: 3,
1289 id: 3,
1290 text: _gettext('Hide full context diff'),
1290 text: _gettext('Hide full context diff'),
1291 action: function () {},
1291 action: function () {},
1292 url: "${h.current_route_path(request, fullcontext=0)|n}"
1292 url: "${h.current_route_path(request, fullcontext=0)|n}"
1293 },
1293 },
1294 % else:
1294 % else:
1295 {
1295 {
1296 id: 3,
1296 id: 3,
1297 text: _gettext('Show full context diff'),
1297 text: _gettext('Show full context diff'),
1298 action: function () {},
1298 action: function () {},
1299 url: "${h.current_route_path(request, fullcontext=1)|n}"
1299 url: "${h.current_route_path(request, fullcontext=1)|n}"
1300 },
1300 },
1301 % endif
1301 % endif
1302
1302
1303 ]
1303 ]
1304 };
1304 };
1305
1305
1306 var diffMenuId = "#diff_menu_" + "${diffset_container_id}";
1306 var diffMenuId = "#diff_menu_" + "${diffset_container_id}";
1307 $(diffMenuId).select2({
1307 $(diffMenuId).select2({
1308 minimumResultsForSearch: -1,
1308 minimumResultsForSearch: -1,
1309 containerCssClass: "drop-menu-no-width",
1309 containerCssClass: "drop-menu-no-width",
1310 dropdownCssClass: "drop-menu-dropdown",
1310 dropdownCssClass: "drop-menu-dropdown",
1311 dropdownAutoWidth: true,
1311 dropdownAutoWidth: true,
1312 data: preloadDiffMenuData,
1312 data: preloadDiffMenuData,
1313 placeholder: "${_('...')}",
1313 placeholder: "${_('...')}",
1314 });
1314 });
1315 $(diffMenuId).on('select2-selecting', function (e) {
1315 $(diffMenuId).on('select2-selecting', function (e) {
1316 e.choice.action();
1316 e.choice.action();
1317 if (e.choice.url !== null) {
1317 if (e.choice.url !== null) {
1318 window.location = e.choice.url
1318 window.location = e.choice.url
1319 }
1319 }
1320 });
1320 });
1321 toggleExpand = function (el, diffsetEl) {
1321 toggleExpand = function (el, diffsetEl) {
1322 var el = $(el);
1322 var el = $(el);
1323 if (el.hasClass('collapsed')) {
1323 if (el.hasClass('collapsed')) {
1324 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1324 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1325 el.removeClass('collapsed');
1325 el.removeClass('collapsed');
1326 el.html(
1326 el.html(
1327 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1327 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1328 _gettext('Collapse all files'));
1328 _gettext('Collapse all files'));
1329 }
1329 }
1330 else {
1330 else {
1331 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1331 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1332 el.addClass('collapsed');
1332 el.addClass('collapsed');
1333 el.html(
1333 el.html(
1334 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1334 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1335 _gettext('Expand all files'));
1335 _gettext('Expand all files'));
1336 }
1336 }
1337 updateSticky()
1337 updateSticky()
1338 };
1338 };
1339
1339
1340 toggleCommitExpand = function (el) {
1340 toggleCommitExpand = function (el) {
1341 var $el = $(el);
1341 var $el = $(el);
1342 var commits = $el.data('toggleCommitsCnt');
1342 var commits = $el.data('toggleCommitsCnt');
1343 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1343 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1344 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1344 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1345
1345
1346 if ($el.hasClass('collapsed')) {
1346 if ($el.hasClass('collapsed')) {
1347 $('.compare_select').show();
1347 $('.compare_select').show();
1348 $('.compare_select_hidden').hide();
1348 $('.compare_select_hidden').hide();
1349
1349
1350 $el.removeClass('collapsed');
1350 $el.removeClass('collapsed');
1351 $el.html(
1351 $el.html(
1352 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1352 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1353 collapseMsg);
1353 collapseMsg);
1354 }
1354 }
1355 else {
1355 else {
1356 $('.compare_select').hide();
1356 $('.compare_select').hide();
1357 $('.compare_select_hidden').show();
1357 $('.compare_select_hidden').show();
1358 $el.addClass('collapsed');
1358 $el.addClass('collapsed');
1359 $el.html(
1359 $el.html(
1360 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1360 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1361 expandMsg);
1361 expandMsg);
1362 }
1362 }
1363 updateSticky();
1363 updateSticky();
1364 };
1364 };
1365
1365
1366 // get stored diff mode and pre-enable it
1366 // get stored diff mode and pre-enable it
1367 if (templateContext.session_attrs.wide_diff_mode === "true") {
1367 if (templateContext.session_attrs.wide_diff_mode === "true") {
1368 Rhodecode.comments.toggleWideMode(null);
1368 Rhodecode.comments.toggleWideMode(null);
1369 $('.toggle-wide-diff').addClass('btn-active');
1369 $('.toggle-wide-diff').addClass('btn-active');
1370 updateSticky();
1370 updateSticky();
1371 }
1371 }
1372
1372
1373 // DIFF NAV //
1373 // DIFF NAV //
1374
1374
1375 // element to detect scroll direction of
1375 // element to detect scroll direction of
1376 var $window = $(window);
1376 var $window = $(window);
1377
1377
1378 // initialize last scroll position
1378 // initialize last scroll position
1379 var lastScrollY = $window.scrollTop();
1379 var lastScrollY = $window.scrollTop();
1380
1380
1381 $window.on('resize scrollstop', {latency: 350}, function () {
1381 $window.on('resize scrollstop', {latency: 350}, function () {
1382 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1382 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1383
1383
1384 // get current scroll position
1384 // get current scroll position
1385 var currentScrollY = $window.scrollTop();
1385 var currentScrollY = $window.scrollTop();
1386
1386
1387 // determine current scroll direction
1387 // determine current scroll direction
1388 if (currentScrollY > lastScrollY) {
1388 if (currentScrollY > lastScrollY) {
1389 var y = 'down'
1389 var y = 'down'
1390 } else if (currentScrollY !== lastScrollY) {
1390 } else if (currentScrollY !== lastScrollY) {
1391 var y = 'up';
1391 var y = 'up';
1392 }
1392 }
1393
1393
1394 var pos = -1; // by default we use last element in viewport
1394 var pos = -1; // by default we use last element in viewport
1395 if (y === 'down') {
1395 if (y === 'down') {
1396 pos = -1;
1396 pos = -1;
1397 } else if (y === 'up') {
1397 } else if (y === 'up') {
1398 pos = 0;
1398 pos = 0;
1399 }
1399 }
1400
1400
1401 if (visibleChunks.length > 0) {
1401 if (visibleChunks.length > 0) {
1402 $('.nav-chunk').removeClass('selected');
1402 $('.nav-chunk').removeClass('selected');
1403 $(visibleChunks.get(pos)).addClass('selected');
1403 $(visibleChunks.get(pos)).addClass('selected');
1404 }
1404 }
1405
1405
1406 // update last scroll position to current position
1406 // update last scroll position to current position
1407 lastScrollY = currentScrollY;
1407 lastScrollY = currentScrollY;
1408
1408
1409 });
1409 });
1410 $('#diff_nav').html(diffNavText);
1410 $('#diff_nav').html(diffNavText);
1411
1411
1412 });
1412 });
1413 </script>
1413 </script>
1414
1414
1415 </%def>
1415 </%def>
General Comments 0
You need to be logged in to leave comments. Login now