##// END OF EJS Templates
pull-requests: added filters to my account pull requests page.
ergo -
r4318:635c5bc5 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,
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)
731 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
732
732
733 data = []
733 data = []
734 comments_model = CommentsModel()
734 comments_model = CommentsModel()
735 for pr in pull_requests:
735 for pr in pull_requests:
736 repo_id = pr.target_repo_id
736 repo_id = pr.target_repo_id
737 comments = comments_model.get_all_comments(
737 comments = comments_model.get_all_comments(
738 repo_id, pull_request=pr)
738 repo_id, pull_request=pr)
739 owned = pr.user_id == self._rhodecode_user.user_id
739 owned = pr.user_id == self._rhodecode_user.user_id
740
740
741 data.append({
741 data.append({
742 'target_repo': _render('pullrequest_target_repo',
742 'target_repo': _render('pullrequest_target_repo',
743 pr.target_repo.repo_name),
743 pr.target_repo.repo_name),
744 'name': _render('pullrequest_name',
744 'name': _render('pullrequest_name',
745 pr.pull_request_id, pr.pull_request_state,
745 pr.pull_request_id, pr.pull_request_state,
746 pr.work_in_progress, pr.target_repo.repo_name,
746 pr.work_in_progress, pr.target_repo.repo_name,
747 short=True),
747 short=True),
748 'name_raw': pr.pull_request_id,
748 'name_raw': pr.pull_request_id,
749 'status': _render('pullrequest_status',
749 'status': _render('pullrequest_status',
750 pr.calculated_review_status()),
750 pr.calculated_review_status()),
751 'title': _render('pullrequest_title', pr.title, pr.description),
751 'title': _render('pullrequest_title', pr.title, pr.description),
752 'description': h.escape(pr.description),
752 'description': h.escape(pr.description),
753 'updated_on': _render('pullrequest_updated_on',
753 'updated_on': _render('pullrequest_updated_on',
754 h.datetime_to_time(pr.updated_on)),
754 h.datetime_to_time(pr.updated_on)),
755 'updated_on_raw': h.datetime_to_time(pr.updated_on),
755 'updated_on_raw': h.datetime_to_time(pr.updated_on),
756 'created_on': _render('pullrequest_updated_on',
756 'created_on': _render('pullrequest_updated_on',
757 h.datetime_to_time(pr.created_on)),
757 h.datetime_to_time(pr.created_on)),
758 'created_on_raw': h.datetime_to_time(pr.created_on),
758 'created_on_raw': h.datetime_to_time(pr.created_on),
759 'state': pr.pull_request_state,
759 'state': pr.pull_request_state,
760 'author': _render('pullrequest_author',
760 'author': _render('pullrequest_author',
761 pr.author.full_contact, ),
761 pr.author.full_contact, ),
762 'author_raw': pr.author.full_name,
762 'author_raw': pr.author.full_name,
763 'comments': _render('pullrequest_comments', len(comments)),
763 'comments': _render('pullrequest_comments', len(comments)),
764 'comments_raw': len(comments),
764 'comments_raw': len(comments),
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,1922 +1,1929 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-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 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30
30
31 import datetime
31 import datetime
32 import urllib
32 import urllib
33 import collections
33 import collections
34
34
35 from pyramid import compat
35 from pyramid import compat
36 from pyramid.threadlocal import get_current_request
36 from pyramid.threadlocal import get_current_request
37
37
38 from rhodecode.translation import lazy_ugettext
38 from rhodecode.translation import lazy_ugettext
39 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 from rhodecode.lib import audit_logger
40 from rhodecode.lib import audit_logger
41 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.compat import OrderedDict
42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.markup_renderer import (
43 from rhodecode.lib.markup_renderer import (
44 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe, AttributeDict, safe_int
45 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe, AttributeDict, safe_int
46 from rhodecode.lib.vcs.backends.base import (
46 from rhodecode.lib.vcs.backends.base import (
47 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
47 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
48 TargetRefMissing, SourceRefMissing)
48 TargetRefMissing, SourceRefMissing)
49 from rhodecode.lib.vcs.conf import settings as vcs_settings
49 from rhodecode.lib.vcs.conf import settings as vcs_settings
50 from rhodecode.lib.vcs.exceptions import (
50 from rhodecode.lib.vcs.exceptions import (
51 CommitDoesNotExistError, EmptyRepositoryError)
51 CommitDoesNotExistError, EmptyRepositoryError)
52 from rhodecode.model import BaseModel
52 from rhodecode.model import BaseModel
53 from rhodecode.model.changeset_status import ChangesetStatusModel
53 from rhodecode.model.changeset_status import ChangesetStatusModel
54 from rhodecode.model.comment import CommentsModel
54 from rhodecode.model.comment import CommentsModel
55 from rhodecode.model.db import (
55 from rhodecode.model.db import (
56 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
56 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
57 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
57 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
58 from rhodecode.model.meta import Session
58 from rhodecode.model.meta import Session
59 from rhodecode.model.notification import NotificationModel, \
59 from rhodecode.model.notification import NotificationModel, \
60 EmailNotificationModel
60 EmailNotificationModel
61 from rhodecode.model.scm import ScmModel
61 from rhodecode.model.scm import ScmModel
62 from rhodecode.model.settings import VcsSettingsModel
62 from rhodecode.model.settings import VcsSettingsModel
63
63
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 # Data structure to hold the response data when updating commits during a pull
68 # Data structure to hold the response data when updating commits during a pull
69 # request update.
69 # request update.
70 class UpdateResponse(object):
70 class UpdateResponse(object):
71
71
72 def __init__(self, executed, reason, new, old, common_ancestor_id,
72 def __init__(self, executed, reason, new, old, common_ancestor_id,
73 commit_changes, source_changed, target_changed):
73 commit_changes, source_changed, target_changed):
74
74
75 self.executed = executed
75 self.executed = executed
76 self.reason = reason
76 self.reason = reason
77 self.new = new
77 self.new = new
78 self.old = old
78 self.old = old
79 self.common_ancestor_id = common_ancestor_id
79 self.common_ancestor_id = common_ancestor_id
80 self.changes = commit_changes
80 self.changes = commit_changes
81 self.source_changed = source_changed
81 self.source_changed = source_changed
82 self.target_changed = target_changed
82 self.target_changed = target_changed
83
83
84
84
85 class PullRequestModel(BaseModel):
85 class PullRequestModel(BaseModel):
86
86
87 cls = PullRequest
87 cls = PullRequest
88
88
89 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
89 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
90
90
91 UPDATE_STATUS_MESSAGES = {
91 UPDATE_STATUS_MESSAGES = {
92 UpdateFailureReason.NONE: lazy_ugettext(
92 UpdateFailureReason.NONE: lazy_ugettext(
93 'Pull request update successful.'),
93 'Pull request update successful.'),
94 UpdateFailureReason.UNKNOWN: lazy_ugettext(
94 UpdateFailureReason.UNKNOWN: lazy_ugettext(
95 'Pull request update failed because of an unknown error.'),
95 'Pull request update failed because of an unknown error.'),
96 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
96 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
97 'No update needed because the source and target have not changed.'),
97 'No update needed because the source and target have not changed.'),
98 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
98 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
99 'Pull request cannot be updated because the reference type is '
99 'Pull request cannot be updated because the reference type is '
100 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
100 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
101 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
101 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
102 'This pull request cannot be updated because the target '
102 'This pull request cannot be updated because the target '
103 'reference is missing.'),
103 'reference is missing.'),
104 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
104 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
105 'This pull request cannot be updated because the source '
105 'This pull request cannot be updated because the source '
106 'reference is missing.'),
106 'reference is missing.'),
107 }
107 }
108 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
108 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
109 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
109 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
110
110
111 def __get_pull_request(self, pull_request):
111 def __get_pull_request(self, pull_request):
112 return self._get_instance((
112 return self._get_instance((
113 PullRequest, PullRequestVersion), pull_request)
113 PullRequest, PullRequestVersion), pull_request)
114
114
115 def _check_perms(self, perms, pull_request, user, api=False):
115 def _check_perms(self, perms, pull_request, user, api=False):
116 if not api:
116 if not api:
117 return h.HasRepoPermissionAny(*perms)(
117 return h.HasRepoPermissionAny(*perms)(
118 user=user, repo_name=pull_request.target_repo.repo_name)
118 user=user, repo_name=pull_request.target_repo.repo_name)
119 else:
119 else:
120 return h.HasRepoPermissionAnyApi(*perms)(
120 return h.HasRepoPermissionAnyApi(*perms)(
121 user=user, repo_name=pull_request.target_repo.repo_name)
121 user=user, repo_name=pull_request.target_repo.repo_name)
122
122
123 def check_user_read(self, pull_request, user, api=False):
123 def check_user_read(self, pull_request, user, api=False):
124 _perms = ('repository.admin', 'repository.write', 'repository.read',)
124 _perms = ('repository.admin', 'repository.write', 'repository.read',)
125 return self._check_perms(_perms, pull_request, user, api)
125 return self._check_perms(_perms, pull_request, user, api)
126
126
127 def check_user_merge(self, pull_request, user, api=False):
127 def check_user_merge(self, pull_request, user, api=False):
128 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
128 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
129 return self._check_perms(_perms, pull_request, user, api)
129 return self._check_perms(_perms, pull_request, user, api)
130
130
131 def check_user_update(self, pull_request, user, api=False):
131 def check_user_update(self, pull_request, user, api=False):
132 owner = user.user_id == pull_request.user_id
132 owner = user.user_id == pull_request.user_id
133 return self.check_user_merge(pull_request, user, api) or owner
133 return self.check_user_merge(pull_request, user, api) or owner
134
134
135 def check_user_delete(self, pull_request, user):
135 def check_user_delete(self, pull_request, user):
136 owner = user.user_id == pull_request.user_id
136 owner = user.user_id == pull_request.user_id
137 _perms = ('repository.admin',)
137 _perms = ('repository.admin',)
138 return self._check_perms(_perms, pull_request, user) or owner
138 return self._check_perms(_perms, pull_request, user) or owner
139
139
140 def check_user_change_status(self, pull_request, user, api=False):
140 def check_user_change_status(self, pull_request, user, api=False):
141 reviewer = user.user_id in [x.user_id for x in
141 reviewer = user.user_id in [x.user_id for x in
142 pull_request.reviewers]
142 pull_request.reviewers]
143 return self.check_user_update(pull_request, user, api) or reviewer
143 return self.check_user_update(pull_request, user, api) or reviewer
144
144
145 def check_user_comment(self, pull_request, user):
145 def check_user_comment(self, pull_request, user):
146 owner = user.user_id == pull_request.user_id
146 owner = user.user_id == pull_request.user_id
147 return self.check_user_read(pull_request, user) or owner
147 return self.check_user_read(pull_request, user) or owner
148
148
149 def get(self, pull_request):
149 def get(self, pull_request):
150 return self.__get_pull_request(pull_request)
150 return self.__get_pull_request(pull_request)
151
151
152 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
152 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
153 statuses=None, opened_by=None, order_by=None,
153 statuses=None, opened_by=None, order_by=None,
154 order_dir='desc', only_created=False):
154 order_dir='desc', only_created=False):
155 repo = None
155 repo = None
156 if repo_name:
156 if repo_name:
157 repo = self._get_repo(repo_name)
157 repo = self._get_repo(repo_name)
158
158
159 q = PullRequest.query()
159 q = PullRequest.query()
160
160
161 if search_q:
161 if search_q:
162 like_expression = u'%{}%'.format(safe_unicode(search_q))
162 like_expression = u'%{}%'.format(safe_unicode(search_q))
163 q = q.filter(or_(
163 q = q.filter(or_(
164 cast(PullRequest.pull_request_id, String).ilike(like_expression),
164 cast(PullRequest.pull_request_id, String).ilike(like_expression),
165 PullRequest.title.ilike(like_expression),
165 PullRequest.title.ilike(like_expression),
166 PullRequest.description.ilike(like_expression),
166 PullRequest.description.ilike(like_expression),
167 ))
167 ))
168
168
169 # source or target
169 # source or target
170 if repo and source:
170 if repo and source:
171 q = q.filter(PullRequest.source_repo == repo)
171 q = q.filter(PullRequest.source_repo == repo)
172 elif repo:
172 elif repo:
173 q = q.filter(PullRequest.target_repo == repo)
173 q = q.filter(PullRequest.target_repo == repo)
174
174
175 # closed,opened
175 # closed,opened
176 if statuses:
176 if statuses:
177 q = q.filter(PullRequest.status.in_(statuses))
177 q = q.filter(PullRequest.status.in_(statuses))
178
178
179 # opened by filter
179 # opened by filter
180 if opened_by:
180 if opened_by:
181 q = q.filter(PullRequest.user_id.in_(opened_by))
181 q = q.filter(PullRequest.user_id.in_(opened_by))
182
182
183 # only get those that are in "created" state
183 # only get those that are in "created" state
184 if only_created:
184 if only_created:
185 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
185 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
186
186
187 if order_by:
187 if order_by:
188 order_map = {
188 order_map = {
189 'name_raw': PullRequest.pull_request_id,
189 'name_raw': PullRequest.pull_request_id,
190 'id': PullRequest.pull_request_id,
190 'id': PullRequest.pull_request_id,
191 'title': PullRequest.title,
191 'title': PullRequest.title,
192 'updated_on_raw': PullRequest.updated_on,
192 'updated_on_raw': PullRequest.updated_on,
193 'target_repo': PullRequest.target_repo_id
193 'target_repo': PullRequest.target_repo_id
194 }
194 }
195 if order_dir == 'asc':
195 if order_dir == 'asc':
196 q = q.order_by(order_map[order_by].asc())
196 q = q.order_by(order_map[order_by].asc())
197 else:
197 else:
198 q = q.order_by(order_map[order_by].desc())
198 q = q.order_by(order_map[order_by].desc())
199
199
200 return q
200 return q
201
201
202 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
202 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
203 opened_by=None):
203 opened_by=None):
204 """
204 """
205 Count the number of pull requests for a specific repository.
205 Count the number of pull requests for a specific repository.
206
206
207 :param repo_name: target or source repo
207 :param repo_name: target or source repo
208 :param search_q: filter by text
208 :param search_q: filter by text
209 :param source: boolean flag to specify if repo_name refers to source
209 :param source: boolean flag to specify if repo_name refers to source
210 :param statuses: list of pull request statuses
210 :param statuses: list of pull request statuses
211 :param opened_by: author user of the pull request
211 :param opened_by: author user of the pull request
212 :returns: int number of pull requests
212 :returns: int number of pull requests
213 """
213 """
214 q = self._prepare_get_all_query(
214 q = self._prepare_get_all_query(
215 repo_name, search_q=search_q, source=source, statuses=statuses,
215 repo_name, search_q=search_q, source=source, statuses=statuses,
216 opened_by=opened_by)
216 opened_by=opened_by)
217
217
218 return q.count()
218 return q.count()
219
219
220 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
220 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
221 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
221 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
222 """
222 """
223 Get all pull requests for a specific repository.
223 Get all pull requests for a specific repository.
224
224
225 :param repo_name: target or source repo
225 :param repo_name: target or source repo
226 :param search_q: filter by text
226 :param search_q: filter by text
227 :param source: boolean flag to specify if repo_name refers to source
227 :param source: boolean flag to specify if repo_name refers to source
228 :param statuses: list of pull request statuses
228 :param statuses: list of pull request statuses
229 :param opened_by: author user of the pull request
229 :param opened_by: author user of the pull request
230 :param offset: pagination offset
230 :param offset: pagination offset
231 :param length: length of returned list
231 :param length: length of returned list
232 :param order_by: order of the returned list
232 :param order_by: order of the returned list
233 :param order_dir: 'asc' or 'desc' ordering direction
233 :param order_dir: 'asc' or 'desc' ordering direction
234 :returns: list of pull requests
234 :returns: list of pull requests
235 """
235 """
236 q = self._prepare_get_all_query(
236 q = self._prepare_get_all_query(
237 repo_name, search_q=search_q, source=source, statuses=statuses,
237 repo_name, search_q=search_q, source=source, statuses=statuses,
238 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
238 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
239
239
240 if length:
240 if length:
241 pull_requests = q.limit(length).offset(offset).all()
241 pull_requests = q.limit(length).offset(offset).all()
242 else:
242 else:
243 pull_requests = q.all()
243 pull_requests = q.all()
244
244
245 return pull_requests
245 return pull_requests
246
246
247 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
247 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
248 opened_by=None):
248 opened_by=None):
249 """
249 """
250 Count the number of pull requests for a specific repository that are
250 Count the number of pull requests for a specific repository that are
251 awaiting review.
251 awaiting review.
252
252
253 :param repo_name: target or source repo
253 :param repo_name: target or source repo
254 :param search_q: filter by text
254 :param search_q: filter by text
255 :param source: boolean flag to specify if repo_name refers to source
255 :param source: boolean flag to specify if repo_name refers to source
256 :param statuses: list of pull request statuses
256 :param statuses: list of pull request statuses
257 :param opened_by: author user of the pull request
257 :param opened_by: author user of the pull request
258 :returns: int number of pull requests
258 :returns: int number of pull requests
259 """
259 """
260 pull_requests = self.get_awaiting_review(
260 pull_requests = self.get_awaiting_review(
261 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
261 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
262
262
263 return len(pull_requests)
263 return len(pull_requests)
264
264
265 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
265 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
266 opened_by=None, offset=0, length=None,
266 opened_by=None, offset=0, length=None,
267 order_by=None, order_dir='desc'):
267 order_by=None, order_dir='desc'):
268 """
268 """
269 Get all pull requests for a specific repository that are awaiting
269 Get all pull requests for a specific repository that are awaiting
270 review.
270 review.
271
271
272 :param repo_name: target or source repo
272 :param repo_name: target or source repo
273 :param search_q: filter by text
273 :param search_q: filter by text
274 :param source: boolean flag to specify if repo_name refers to source
274 :param source: boolean flag to specify if repo_name refers to source
275 :param statuses: list of pull request statuses
275 :param statuses: list of pull request statuses
276 :param opened_by: author user of the pull request
276 :param opened_by: author user of the pull request
277 :param offset: pagination offset
277 :param offset: pagination offset
278 :param length: length of returned list
278 :param length: length of returned list
279 :param order_by: order of the returned list
279 :param order_by: order of the returned list
280 :param order_dir: 'asc' or 'desc' ordering direction
280 :param order_dir: 'asc' or 'desc' ordering direction
281 :returns: list of pull requests
281 :returns: list of pull requests
282 """
282 """
283 pull_requests = self.get_all(
283 pull_requests = self.get_all(
284 repo_name, search_q=search_q, source=source, statuses=statuses,
284 repo_name, search_q=search_q, source=source, statuses=statuses,
285 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
285 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
286
286
287 _filtered_pull_requests = []
287 _filtered_pull_requests = []
288 for pr in pull_requests:
288 for pr in pull_requests:
289 status = pr.calculated_review_status()
289 status = pr.calculated_review_status()
290 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
290 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
291 ChangesetStatus.STATUS_UNDER_REVIEW]:
291 ChangesetStatus.STATUS_UNDER_REVIEW]:
292 _filtered_pull_requests.append(pr)
292 _filtered_pull_requests.append(pr)
293 if length:
293 if length:
294 return _filtered_pull_requests[offset:offset+length]
294 return _filtered_pull_requests[offset:offset+length]
295 else:
295 else:
296 return _filtered_pull_requests
296 return _filtered_pull_requests
297
297
298 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
298 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
299 opened_by=None, user_id=None):
299 opened_by=None, user_id=None):
300 """
300 """
301 Count the number of pull requests for a specific repository that are
301 Count the number of pull requests for a specific repository that are
302 awaiting review from a specific user.
302 awaiting review from a specific user.
303
303
304 :param repo_name: target or source repo
304 :param repo_name: target or source repo
305 :param search_q: filter by text
305 :param search_q: filter by text
306 :param source: boolean flag to specify if repo_name refers to source
306 :param source: boolean flag to specify if repo_name refers to source
307 :param statuses: list of pull request statuses
307 :param statuses: list of pull request statuses
308 :param opened_by: author user of the pull request
308 :param opened_by: author user of the pull request
309 :param user_id: reviewer user of the pull request
309 :param user_id: reviewer user of the pull request
310 :returns: int number of pull requests
310 :returns: int number of pull requests
311 """
311 """
312 pull_requests = self.get_awaiting_my_review(
312 pull_requests = self.get_awaiting_my_review(
313 repo_name, search_q=search_q, source=source, statuses=statuses,
313 repo_name, search_q=search_q, source=source, statuses=statuses,
314 opened_by=opened_by, user_id=user_id)
314 opened_by=opened_by, user_id=user_id)
315
315
316 return len(pull_requests)
316 return len(pull_requests)
317
317
318 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
318 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
319 opened_by=None, user_id=None, offset=0,
319 opened_by=None, user_id=None, offset=0,
320 length=None, order_by=None, order_dir='desc'):
320 length=None, order_by=None, order_dir='desc'):
321 """
321 """
322 Get all pull requests for a specific repository that are awaiting
322 Get all pull requests for a specific repository that are awaiting
323 review from a specific user.
323 review from a specific user.
324
324
325 :param repo_name: target or source repo
325 :param repo_name: target or source repo
326 :param search_q: filter by text
326 :param search_q: filter by text
327 :param source: boolean flag to specify if repo_name refers to source
327 :param source: boolean flag to specify if repo_name refers to source
328 :param statuses: list of pull request statuses
328 :param statuses: list of pull request statuses
329 :param opened_by: author user of the pull request
329 :param opened_by: author user of the pull request
330 :param user_id: reviewer user of the pull request
330 :param user_id: reviewer user of the pull request
331 :param offset: pagination offset
331 :param offset: pagination offset
332 :param length: length of returned list
332 :param length: length of returned list
333 :param order_by: order of the returned list
333 :param order_by: order of the returned list
334 :param order_dir: 'asc' or 'desc' ordering direction
334 :param order_dir: 'asc' or 'desc' ordering direction
335 :returns: list of pull requests
335 :returns: list of pull requests
336 """
336 """
337 pull_requests = self.get_all(
337 pull_requests = self.get_all(
338 repo_name, search_q=search_q, source=source, statuses=statuses,
338 repo_name, search_q=search_q, source=source, statuses=statuses,
339 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
339 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
340
340
341 _my = PullRequestModel().get_not_reviewed(user_id)
341 _my = PullRequestModel().get_not_reviewed(user_id)
342 my_participation = []
342 my_participation = []
343 for pr in pull_requests:
343 for pr in pull_requests:
344 if pr in _my:
344 if pr in _my:
345 my_participation.append(pr)
345 my_participation.append(pr)
346 _filtered_pull_requests = my_participation
346 _filtered_pull_requests = my_participation
347 if length:
347 if length:
348 return _filtered_pull_requests[offset:offset+length]
348 return _filtered_pull_requests[offset:offset+length]
349 else:
349 else:
350 return _filtered_pull_requests
350 return _filtered_pull_requests
351
351
352 def get_not_reviewed(self, user_id):
352 def get_not_reviewed(self, user_id):
353 return [
353 return [
354 x.pull_request for x in PullRequestReviewers.query().filter(
354 x.pull_request for x in PullRequestReviewers.query().filter(
355 PullRequestReviewers.user_id == user_id).all()
355 PullRequestReviewers.user_id == user_id).all()
356 ]
356 ]
357
357
358 def _prepare_participating_query(self, user_id=None, statuses=None,
358 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
359 order_by=None, order_dir='desc'):
359 order_by=None, order_dir='desc'):
360 q = PullRequest.query()
360 q = PullRequest.query()
361 if user_id:
361 if user_id:
362 reviewers_subquery = Session().query(
362 reviewers_subquery = Session().query(
363 PullRequestReviewers.pull_request_id).filter(
363 PullRequestReviewers.pull_request_id).filter(
364 PullRequestReviewers.user_id == user_id).subquery()
364 PullRequestReviewers.user_id == user_id).subquery()
365 user_filter = or_(
365 user_filter = or_(
366 PullRequest.user_id == user_id,
366 PullRequest.user_id == user_id,
367 PullRequest.pull_request_id.in_(reviewers_subquery)
367 PullRequest.pull_request_id.in_(reviewers_subquery)
368 )
368 )
369 q = PullRequest.query().filter(user_filter)
369 q = PullRequest.query().filter(user_filter)
370
370
371 # closed,opened
371 # closed,opened
372 if statuses:
372 if statuses:
373 q = q.filter(PullRequest.status.in_(statuses))
373 q = q.filter(PullRequest.status.in_(statuses))
374
374
375 if query:
376 like_expression = u'%{}%'.format(safe_unicode(query))
377 q = q.filter(or_(
378 cast(PullRequest.pull_request_id, String).ilike(like_expression),
379 PullRequest.title.ilike(like_expression),
380 PullRequest.description.ilike(like_expression),
381 ))
375 if order_by:
382 if order_by:
376 order_map = {
383 order_map = {
377 'name_raw': PullRequest.pull_request_id,
384 'name_raw': PullRequest.pull_request_id,
378 'title': PullRequest.title,
385 'title': PullRequest.title,
379 'updated_on_raw': PullRequest.updated_on,
386 'updated_on_raw': PullRequest.updated_on,
380 'target_repo': PullRequest.target_repo_id
387 'target_repo': PullRequest.target_repo_id
381 }
388 }
382 if order_dir == 'asc':
389 if order_dir == 'asc':
383 q = q.order_by(order_map[order_by].asc())
390 q = q.order_by(order_map[order_by].asc())
384 else:
391 else:
385 q = q.order_by(order_map[order_by].desc())
392 q = q.order_by(order_map[order_by].desc())
386
393
387 return q
394 return q
388
395
389 def count_im_participating_in(self, user_id=None, statuses=None):
396 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
390 q = self._prepare_participating_query(user_id, statuses=statuses)
397 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
391 return q.count()
398 return q.count()
392
399
393 def get_im_participating_in(
400 def get_im_participating_in(
394 self, user_id=None, statuses=None, offset=0,
401 self, user_id=None, statuses=None, query='', offset=0,
395 length=None, order_by=None, order_dir='desc'):
402 length=None, order_by=None, order_dir='desc'):
396 """
403 """
397 Get all Pull requests that i'm participating in, or i have opened
404 Get all Pull requests that i'm participating in, or i have opened
398 """
405 """
399
406
400 q = self._prepare_participating_query(
407 q = self._prepare_participating_query(
401 user_id, statuses=statuses, order_by=order_by,
408 user_id, statuses=statuses, query=query, order_by=order_by,
402 order_dir=order_dir)
409 order_dir=order_dir)
403
410
404 if length:
411 if length:
405 pull_requests = q.limit(length).offset(offset).all()
412 pull_requests = q.limit(length).offset(offset).all()
406 else:
413 else:
407 pull_requests = q.all()
414 pull_requests = q.all()
408
415
409 return pull_requests
416 return pull_requests
410
417
411 def get_versions(self, pull_request):
418 def get_versions(self, pull_request):
412 """
419 """
413 returns version of pull request sorted by ID descending
420 returns version of pull request sorted by ID descending
414 """
421 """
415 return PullRequestVersion.query()\
422 return PullRequestVersion.query()\
416 .filter(PullRequestVersion.pull_request == pull_request)\
423 .filter(PullRequestVersion.pull_request == pull_request)\
417 .order_by(PullRequestVersion.pull_request_version_id.asc())\
424 .order_by(PullRequestVersion.pull_request_version_id.asc())\
418 .all()
425 .all()
419
426
420 def get_pr_version(self, pull_request_id, version=None):
427 def get_pr_version(self, pull_request_id, version=None):
421 at_version = None
428 at_version = None
422
429
423 if version and version == 'latest':
430 if version and version == 'latest':
424 pull_request_ver = PullRequest.get(pull_request_id)
431 pull_request_ver = PullRequest.get(pull_request_id)
425 pull_request_obj = pull_request_ver
432 pull_request_obj = pull_request_ver
426 _org_pull_request_obj = pull_request_obj
433 _org_pull_request_obj = pull_request_obj
427 at_version = 'latest'
434 at_version = 'latest'
428 elif version:
435 elif version:
429 pull_request_ver = PullRequestVersion.get_or_404(version)
436 pull_request_ver = PullRequestVersion.get_or_404(version)
430 pull_request_obj = pull_request_ver
437 pull_request_obj = pull_request_ver
431 _org_pull_request_obj = pull_request_ver.pull_request
438 _org_pull_request_obj = pull_request_ver.pull_request
432 at_version = pull_request_ver.pull_request_version_id
439 at_version = pull_request_ver.pull_request_version_id
433 else:
440 else:
434 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
441 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
435 pull_request_id)
442 pull_request_id)
436
443
437 pull_request_display_obj = PullRequest.get_pr_display_object(
444 pull_request_display_obj = PullRequest.get_pr_display_object(
438 pull_request_obj, _org_pull_request_obj)
445 pull_request_obj, _org_pull_request_obj)
439
446
440 return _org_pull_request_obj, pull_request_obj, \
447 return _org_pull_request_obj, pull_request_obj, \
441 pull_request_display_obj, at_version
448 pull_request_display_obj, at_version
442
449
443 def create(self, created_by, source_repo, source_ref, target_repo,
450 def create(self, created_by, source_repo, source_ref, target_repo,
444 target_ref, revisions, reviewers, title, description=None,
451 target_ref, revisions, reviewers, title, description=None,
445 description_renderer=None,
452 description_renderer=None,
446 reviewer_data=None, translator=None, auth_user=None):
453 reviewer_data=None, translator=None, auth_user=None):
447 translator = translator or get_current_request().translate
454 translator = translator or get_current_request().translate
448
455
449 created_by_user = self._get_user(created_by)
456 created_by_user = self._get_user(created_by)
450 auth_user = auth_user or created_by_user.AuthUser()
457 auth_user = auth_user or created_by_user.AuthUser()
451 source_repo = self._get_repo(source_repo)
458 source_repo = self._get_repo(source_repo)
452 target_repo = self._get_repo(target_repo)
459 target_repo = self._get_repo(target_repo)
453
460
454 pull_request = PullRequest()
461 pull_request = PullRequest()
455 pull_request.source_repo = source_repo
462 pull_request.source_repo = source_repo
456 pull_request.source_ref = source_ref
463 pull_request.source_ref = source_ref
457 pull_request.target_repo = target_repo
464 pull_request.target_repo = target_repo
458 pull_request.target_ref = target_ref
465 pull_request.target_ref = target_ref
459 pull_request.revisions = revisions
466 pull_request.revisions = revisions
460 pull_request.title = title
467 pull_request.title = title
461 pull_request.description = description
468 pull_request.description = description
462 pull_request.description_renderer = description_renderer
469 pull_request.description_renderer = description_renderer
463 pull_request.author = created_by_user
470 pull_request.author = created_by_user
464 pull_request.reviewer_data = reviewer_data
471 pull_request.reviewer_data = reviewer_data
465 pull_request.pull_request_state = pull_request.STATE_CREATING
472 pull_request.pull_request_state = pull_request.STATE_CREATING
466 Session().add(pull_request)
473 Session().add(pull_request)
467 Session().flush()
474 Session().flush()
468
475
469 reviewer_ids = set()
476 reviewer_ids = set()
470 # members / reviewers
477 # members / reviewers
471 for reviewer_object in reviewers:
478 for reviewer_object in reviewers:
472 user_id, reasons, mandatory, rules = reviewer_object
479 user_id, reasons, mandatory, rules = reviewer_object
473 user = self._get_user(user_id)
480 user = self._get_user(user_id)
474
481
475 # skip duplicates
482 # skip duplicates
476 if user.user_id in reviewer_ids:
483 if user.user_id in reviewer_ids:
477 continue
484 continue
478
485
479 reviewer_ids.add(user.user_id)
486 reviewer_ids.add(user.user_id)
480
487
481 reviewer = PullRequestReviewers()
488 reviewer = PullRequestReviewers()
482 reviewer.user = user
489 reviewer.user = user
483 reviewer.pull_request = pull_request
490 reviewer.pull_request = pull_request
484 reviewer.reasons = reasons
491 reviewer.reasons = reasons
485 reviewer.mandatory = mandatory
492 reviewer.mandatory = mandatory
486
493
487 # NOTE(marcink): pick only first rule for now
494 # NOTE(marcink): pick only first rule for now
488 rule_id = list(rules)[0] if rules else None
495 rule_id = list(rules)[0] if rules else None
489 rule = RepoReviewRule.get(rule_id) if rule_id else None
496 rule = RepoReviewRule.get(rule_id) if rule_id else None
490 if rule:
497 if rule:
491 review_group = rule.user_group_vote_rule(user_id)
498 review_group = rule.user_group_vote_rule(user_id)
492 # we check if this particular reviewer is member of a voting group
499 # we check if this particular reviewer is member of a voting group
493 if review_group:
500 if review_group:
494 # NOTE(marcink):
501 # NOTE(marcink):
495 # can be that user is member of more but we pick the first same,
502 # can be that user is member of more but we pick the first same,
496 # same as default reviewers algo
503 # same as default reviewers algo
497 review_group = review_group[0]
504 review_group = review_group[0]
498
505
499 rule_data = {
506 rule_data = {
500 'rule_name':
507 'rule_name':
501 rule.review_rule_name,
508 rule.review_rule_name,
502 'rule_user_group_entry_id':
509 'rule_user_group_entry_id':
503 review_group.repo_review_rule_users_group_id,
510 review_group.repo_review_rule_users_group_id,
504 'rule_user_group_name':
511 'rule_user_group_name':
505 review_group.users_group.users_group_name,
512 review_group.users_group.users_group_name,
506 'rule_user_group_members':
513 'rule_user_group_members':
507 [x.user.username for x in review_group.users_group.members],
514 [x.user.username for x in review_group.users_group.members],
508 'rule_user_group_members_id':
515 'rule_user_group_members_id':
509 [x.user.user_id for x in review_group.users_group.members],
516 [x.user.user_id for x in review_group.users_group.members],
510 }
517 }
511 # e.g {'vote_rule': -1, 'mandatory': True}
518 # e.g {'vote_rule': -1, 'mandatory': True}
512 rule_data.update(review_group.rule_data())
519 rule_data.update(review_group.rule_data())
513
520
514 reviewer.rule_data = rule_data
521 reviewer.rule_data = rule_data
515
522
516 Session().add(reviewer)
523 Session().add(reviewer)
517 Session().flush()
524 Session().flush()
518
525
519 # Set approval status to "Under Review" for all commits which are
526 # Set approval status to "Under Review" for all commits which are
520 # part of this pull request.
527 # part of this pull request.
521 ChangesetStatusModel().set_status(
528 ChangesetStatusModel().set_status(
522 repo=target_repo,
529 repo=target_repo,
523 status=ChangesetStatus.STATUS_UNDER_REVIEW,
530 status=ChangesetStatus.STATUS_UNDER_REVIEW,
524 user=created_by_user,
531 user=created_by_user,
525 pull_request=pull_request
532 pull_request=pull_request
526 )
533 )
527 # we commit early at this point. This has to do with a fact
534 # we commit early at this point. This has to do with a fact
528 # that before queries do some row-locking. And because of that
535 # that before queries do some row-locking. And because of that
529 # we need to commit and finish transaction before below validate call
536 # we need to commit and finish transaction before below validate call
530 # that for large repos could be long resulting in long row locks
537 # that for large repos could be long resulting in long row locks
531 Session().commit()
538 Session().commit()
532
539
533 # prepare workspace, and run initial merge simulation. Set state during that
540 # prepare workspace, and run initial merge simulation. Set state during that
534 # operation
541 # operation
535 pull_request = PullRequest.get(pull_request.pull_request_id)
542 pull_request = PullRequest.get(pull_request.pull_request_id)
536
543
537 # set as merging, for merge simulation, and if finished to created so we mark
544 # set as merging, for merge simulation, and if finished to created so we mark
538 # simulation is working fine
545 # simulation is working fine
539 with pull_request.set_state(PullRequest.STATE_MERGING,
546 with pull_request.set_state(PullRequest.STATE_MERGING,
540 final_state=PullRequest.STATE_CREATED) as state_obj:
547 final_state=PullRequest.STATE_CREATED) as state_obj:
541 MergeCheck.validate(
548 MergeCheck.validate(
542 pull_request, auth_user=auth_user, translator=translator)
549 pull_request, auth_user=auth_user, translator=translator)
543
550
544 self.notify_reviewers(pull_request, reviewer_ids)
551 self.notify_reviewers(pull_request, reviewer_ids)
545 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
552 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
546
553
547 creation_data = pull_request.get_api_data(with_merge_state=False)
554 creation_data = pull_request.get_api_data(with_merge_state=False)
548 self._log_audit_action(
555 self._log_audit_action(
549 'repo.pull_request.create', {'data': creation_data},
556 'repo.pull_request.create', {'data': creation_data},
550 auth_user, pull_request)
557 auth_user, pull_request)
551
558
552 return pull_request
559 return pull_request
553
560
554 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
561 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
555 pull_request = self.__get_pull_request(pull_request)
562 pull_request = self.__get_pull_request(pull_request)
556 target_scm = pull_request.target_repo.scm_instance()
563 target_scm = pull_request.target_repo.scm_instance()
557 if action == 'create':
564 if action == 'create':
558 trigger_hook = hooks_utils.trigger_create_pull_request_hook
565 trigger_hook = hooks_utils.trigger_create_pull_request_hook
559 elif action == 'merge':
566 elif action == 'merge':
560 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
567 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
561 elif action == 'close':
568 elif action == 'close':
562 trigger_hook = hooks_utils.trigger_close_pull_request_hook
569 trigger_hook = hooks_utils.trigger_close_pull_request_hook
563 elif action == 'review_status_change':
570 elif action == 'review_status_change':
564 trigger_hook = hooks_utils.trigger_review_pull_request_hook
571 trigger_hook = hooks_utils.trigger_review_pull_request_hook
565 elif action == 'update':
572 elif action == 'update':
566 trigger_hook = hooks_utils.trigger_update_pull_request_hook
573 trigger_hook = hooks_utils.trigger_update_pull_request_hook
567 elif action == 'comment':
574 elif action == 'comment':
568 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
575 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
569 else:
576 else:
570 return
577 return
571
578
572 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
579 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
573 pull_request, action, trigger_hook)
580 pull_request, action, trigger_hook)
574 trigger_hook(
581 trigger_hook(
575 username=user.username,
582 username=user.username,
576 repo_name=pull_request.target_repo.repo_name,
583 repo_name=pull_request.target_repo.repo_name,
577 repo_type=target_scm.alias,
584 repo_type=target_scm.alias,
578 pull_request=pull_request,
585 pull_request=pull_request,
579 data=data)
586 data=data)
580
587
581 def _get_commit_ids(self, pull_request):
588 def _get_commit_ids(self, pull_request):
582 """
589 """
583 Return the commit ids of the merged pull request.
590 Return the commit ids of the merged pull request.
584
591
585 This method is not dealing correctly yet with the lack of autoupdates
592 This method is not dealing correctly yet with the lack of autoupdates
586 nor with the implicit target updates.
593 nor with the implicit target updates.
587 For example: if a commit in the source repo is already in the target it
594 For example: if a commit in the source repo is already in the target it
588 will be reported anyways.
595 will be reported anyways.
589 """
596 """
590 merge_rev = pull_request.merge_rev
597 merge_rev = pull_request.merge_rev
591 if merge_rev is None:
598 if merge_rev is None:
592 raise ValueError('This pull request was not merged yet')
599 raise ValueError('This pull request was not merged yet')
593
600
594 commit_ids = list(pull_request.revisions)
601 commit_ids = list(pull_request.revisions)
595 if merge_rev not in commit_ids:
602 if merge_rev not in commit_ids:
596 commit_ids.append(merge_rev)
603 commit_ids.append(merge_rev)
597
604
598 return commit_ids
605 return commit_ids
599
606
600 def merge_repo(self, pull_request, user, extras):
607 def merge_repo(self, pull_request, user, extras):
601 log.debug("Merging pull request %s", pull_request.pull_request_id)
608 log.debug("Merging pull request %s", pull_request.pull_request_id)
602 extras['user_agent'] = 'internal-merge'
609 extras['user_agent'] = 'internal-merge'
603 merge_state = self._merge_pull_request(pull_request, user, extras)
610 merge_state = self._merge_pull_request(pull_request, user, extras)
604 if merge_state.executed:
611 if merge_state.executed:
605 log.debug("Merge was successful, updating the pull request comments.")
612 log.debug("Merge was successful, updating the pull request comments.")
606 self._comment_and_close_pr(pull_request, user, merge_state)
613 self._comment_and_close_pr(pull_request, user, merge_state)
607
614
608 self._log_audit_action(
615 self._log_audit_action(
609 'repo.pull_request.merge',
616 'repo.pull_request.merge',
610 {'merge_state': merge_state.__dict__},
617 {'merge_state': merge_state.__dict__},
611 user, pull_request)
618 user, pull_request)
612
619
613 else:
620 else:
614 log.warn("Merge failed, not updating the pull request.")
621 log.warn("Merge failed, not updating the pull request.")
615 return merge_state
622 return merge_state
616
623
617 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
624 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
618 target_vcs = pull_request.target_repo.scm_instance()
625 target_vcs = pull_request.target_repo.scm_instance()
619 source_vcs = pull_request.source_repo.scm_instance()
626 source_vcs = pull_request.source_repo.scm_instance()
620
627
621 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
628 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
622 pr_id=pull_request.pull_request_id,
629 pr_id=pull_request.pull_request_id,
623 pr_title=pull_request.title,
630 pr_title=pull_request.title,
624 source_repo=source_vcs.name,
631 source_repo=source_vcs.name,
625 source_ref_name=pull_request.source_ref_parts.name,
632 source_ref_name=pull_request.source_ref_parts.name,
626 target_repo=target_vcs.name,
633 target_repo=target_vcs.name,
627 target_ref_name=pull_request.target_ref_parts.name,
634 target_ref_name=pull_request.target_ref_parts.name,
628 )
635 )
629
636
630 workspace_id = self._workspace_id(pull_request)
637 workspace_id = self._workspace_id(pull_request)
631 repo_id = pull_request.target_repo.repo_id
638 repo_id = pull_request.target_repo.repo_id
632 use_rebase = self._use_rebase_for_merging(pull_request)
639 use_rebase = self._use_rebase_for_merging(pull_request)
633 close_branch = self._close_branch_before_merging(pull_request)
640 close_branch = self._close_branch_before_merging(pull_request)
634 user_name = self._user_name_for_merging(pull_request, user)
641 user_name = self._user_name_for_merging(pull_request, user)
635
642
636 target_ref = self._refresh_reference(
643 target_ref = self._refresh_reference(
637 pull_request.target_ref_parts, target_vcs)
644 pull_request.target_ref_parts, target_vcs)
638
645
639 callback_daemon, extras = prepare_callback_daemon(
646 callback_daemon, extras = prepare_callback_daemon(
640 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
647 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
641 host=vcs_settings.HOOKS_HOST,
648 host=vcs_settings.HOOKS_HOST,
642 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
649 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
643
650
644 with callback_daemon:
651 with callback_daemon:
645 # TODO: johbo: Implement a clean way to run a config_override
652 # TODO: johbo: Implement a clean way to run a config_override
646 # for a single call.
653 # for a single call.
647 target_vcs.config.set(
654 target_vcs.config.set(
648 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
655 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
649
656
650 merge_state = target_vcs.merge(
657 merge_state = target_vcs.merge(
651 repo_id, workspace_id, target_ref, source_vcs,
658 repo_id, workspace_id, target_ref, source_vcs,
652 pull_request.source_ref_parts,
659 pull_request.source_ref_parts,
653 user_name=user_name, user_email=user.email,
660 user_name=user_name, user_email=user.email,
654 message=message, use_rebase=use_rebase,
661 message=message, use_rebase=use_rebase,
655 close_branch=close_branch)
662 close_branch=close_branch)
656 return merge_state
663 return merge_state
657
664
658 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
665 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
659 pull_request.merge_rev = merge_state.merge_ref.commit_id
666 pull_request.merge_rev = merge_state.merge_ref.commit_id
660 pull_request.updated_on = datetime.datetime.now()
667 pull_request.updated_on = datetime.datetime.now()
661 close_msg = close_msg or 'Pull request merged and closed'
668 close_msg = close_msg or 'Pull request merged and closed'
662
669
663 CommentsModel().create(
670 CommentsModel().create(
664 text=safe_unicode(close_msg),
671 text=safe_unicode(close_msg),
665 repo=pull_request.target_repo.repo_id,
672 repo=pull_request.target_repo.repo_id,
666 user=user.user_id,
673 user=user.user_id,
667 pull_request=pull_request.pull_request_id,
674 pull_request=pull_request.pull_request_id,
668 f_path=None,
675 f_path=None,
669 line_no=None,
676 line_no=None,
670 closing_pr=True
677 closing_pr=True
671 )
678 )
672
679
673 Session().add(pull_request)
680 Session().add(pull_request)
674 Session().flush()
681 Session().flush()
675 # TODO: paris: replace invalidation with less radical solution
682 # TODO: paris: replace invalidation with less radical solution
676 ScmModel().mark_for_invalidation(
683 ScmModel().mark_for_invalidation(
677 pull_request.target_repo.repo_name)
684 pull_request.target_repo.repo_name)
678 self.trigger_pull_request_hook(pull_request, user, 'merge')
685 self.trigger_pull_request_hook(pull_request, user, 'merge')
679
686
680 def has_valid_update_type(self, pull_request):
687 def has_valid_update_type(self, pull_request):
681 source_ref_type = pull_request.source_ref_parts.type
688 source_ref_type = pull_request.source_ref_parts.type
682 return source_ref_type in self.REF_TYPES
689 return source_ref_type in self.REF_TYPES
683
690
684 def get_flow_commits(self, pull_request):
691 def get_flow_commits(self, pull_request):
685
692
686 # source repo
693 # source repo
687 source_ref_name = pull_request.source_ref_parts.name
694 source_ref_name = pull_request.source_ref_parts.name
688 source_ref_type = pull_request.source_ref_parts.type
695 source_ref_type = pull_request.source_ref_parts.type
689 source_ref_id = pull_request.source_ref_parts.commit_id
696 source_ref_id = pull_request.source_ref_parts.commit_id
690 source_repo = pull_request.source_repo.scm_instance()
697 source_repo = pull_request.source_repo.scm_instance()
691
698
692 try:
699 try:
693 if source_ref_type in self.REF_TYPES:
700 if source_ref_type in self.REF_TYPES:
694 source_commit = source_repo.get_commit(source_ref_name)
701 source_commit = source_repo.get_commit(source_ref_name)
695 else:
702 else:
696 source_commit = source_repo.get_commit(source_ref_id)
703 source_commit = source_repo.get_commit(source_ref_id)
697 except CommitDoesNotExistError:
704 except CommitDoesNotExistError:
698 raise SourceRefMissing()
705 raise SourceRefMissing()
699
706
700 # target repo
707 # target repo
701 target_ref_name = pull_request.target_ref_parts.name
708 target_ref_name = pull_request.target_ref_parts.name
702 target_ref_type = pull_request.target_ref_parts.type
709 target_ref_type = pull_request.target_ref_parts.type
703 target_ref_id = pull_request.target_ref_parts.commit_id
710 target_ref_id = pull_request.target_ref_parts.commit_id
704 target_repo = pull_request.target_repo.scm_instance()
711 target_repo = pull_request.target_repo.scm_instance()
705
712
706 try:
713 try:
707 if target_ref_type in self.REF_TYPES:
714 if target_ref_type in self.REF_TYPES:
708 target_commit = target_repo.get_commit(target_ref_name)
715 target_commit = target_repo.get_commit(target_ref_name)
709 else:
716 else:
710 target_commit = target_repo.get_commit(target_ref_id)
717 target_commit = target_repo.get_commit(target_ref_id)
711 except CommitDoesNotExistError:
718 except CommitDoesNotExistError:
712 raise TargetRefMissing()
719 raise TargetRefMissing()
713
720
714 return source_commit, target_commit
721 return source_commit, target_commit
715
722
716 def update_commits(self, pull_request, updating_user):
723 def update_commits(self, pull_request, updating_user):
717 """
724 """
718 Get the updated list of commits for the pull request
725 Get the updated list of commits for the pull request
719 and return the new pull request version and the list
726 and return the new pull request version and the list
720 of commits processed by this update action
727 of commits processed by this update action
721
728
722 updating_user is the user_object who triggered the update
729 updating_user is the user_object who triggered the update
723 """
730 """
724 pull_request = self.__get_pull_request(pull_request)
731 pull_request = self.__get_pull_request(pull_request)
725 source_ref_type = pull_request.source_ref_parts.type
732 source_ref_type = pull_request.source_ref_parts.type
726 source_ref_name = pull_request.source_ref_parts.name
733 source_ref_name = pull_request.source_ref_parts.name
727 source_ref_id = pull_request.source_ref_parts.commit_id
734 source_ref_id = pull_request.source_ref_parts.commit_id
728
735
729 target_ref_type = pull_request.target_ref_parts.type
736 target_ref_type = pull_request.target_ref_parts.type
730 target_ref_name = pull_request.target_ref_parts.name
737 target_ref_name = pull_request.target_ref_parts.name
731 target_ref_id = pull_request.target_ref_parts.commit_id
738 target_ref_id = pull_request.target_ref_parts.commit_id
732
739
733 if not self.has_valid_update_type(pull_request):
740 if not self.has_valid_update_type(pull_request):
734 log.debug("Skipping update of pull request %s due to ref type: %s",
741 log.debug("Skipping update of pull request %s due to ref type: %s",
735 pull_request, source_ref_type)
742 pull_request, source_ref_type)
736 return UpdateResponse(
743 return UpdateResponse(
737 executed=False,
744 executed=False,
738 reason=UpdateFailureReason.WRONG_REF_TYPE,
745 reason=UpdateFailureReason.WRONG_REF_TYPE,
739 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
746 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
740 source_changed=False, target_changed=False)
747 source_changed=False, target_changed=False)
741
748
742 try:
749 try:
743 source_commit, target_commit = self.get_flow_commits(pull_request)
750 source_commit, target_commit = self.get_flow_commits(pull_request)
744 except SourceRefMissing:
751 except SourceRefMissing:
745 return UpdateResponse(
752 return UpdateResponse(
746 executed=False,
753 executed=False,
747 reason=UpdateFailureReason.MISSING_SOURCE_REF,
754 reason=UpdateFailureReason.MISSING_SOURCE_REF,
748 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
755 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
749 source_changed=False, target_changed=False)
756 source_changed=False, target_changed=False)
750 except TargetRefMissing:
757 except TargetRefMissing:
751 return UpdateResponse(
758 return UpdateResponse(
752 executed=False,
759 executed=False,
753 reason=UpdateFailureReason.MISSING_TARGET_REF,
760 reason=UpdateFailureReason.MISSING_TARGET_REF,
754 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
761 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
755 source_changed=False, target_changed=False)
762 source_changed=False, target_changed=False)
756
763
757 source_changed = source_ref_id != source_commit.raw_id
764 source_changed = source_ref_id != source_commit.raw_id
758 target_changed = target_ref_id != target_commit.raw_id
765 target_changed = target_ref_id != target_commit.raw_id
759
766
760 if not (source_changed or target_changed):
767 if not (source_changed or target_changed):
761 log.debug("Nothing changed in pull request %s", pull_request)
768 log.debug("Nothing changed in pull request %s", pull_request)
762 return UpdateResponse(
769 return UpdateResponse(
763 executed=False,
770 executed=False,
764 reason=UpdateFailureReason.NO_CHANGE,
771 reason=UpdateFailureReason.NO_CHANGE,
765 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
772 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
766 source_changed=target_changed, target_changed=source_changed)
773 source_changed=target_changed, target_changed=source_changed)
767
774
768 change_in_found = 'target repo' if target_changed else 'source repo'
775 change_in_found = 'target repo' if target_changed else 'source repo'
769 log.debug('Updating pull request because of change in %s detected',
776 log.debug('Updating pull request because of change in %s detected',
770 change_in_found)
777 change_in_found)
771
778
772 # Finally there is a need for an update, in case of source change
779 # Finally there is a need for an update, in case of source change
773 # we create a new version, else just an update
780 # we create a new version, else just an update
774 if source_changed:
781 if source_changed:
775 pull_request_version = self._create_version_from_snapshot(pull_request)
782 pull_request_version = self._create_version_from_snapshot(pull_request)
776 self._link_comments_to_version(pull_request_version)
783 self._link_comments_to_version(pull_request_version)
777 else:
784 else:
778 try:
785 try:
779 ver = pull_request.versions[-1]
786 ver = pull_request.versions[-1]
780 except IndexError:
787 except IndexError:
781 ver = None
788 ver = None
782
789
783 pull_request.pull_request_version_id = \
790 pull_request.pull_request_version_id = \
784 ver.pull_request_version_id if ver else None
791 ver.pull_request_version_id if ver else None
785 pull_request_version = pull_request
792 pull_request_version = pull_request
786
793
787 source_repo = pull_request.source_repo.scm_instance()
794 source_repo = pull_request.source_repo.scm_instance()
788 target_repo = pull_request.target_repo.scm_instance()
795 target_repo = pull_request.target_repo.scm_instance()
789
796
790 # re-compute commit ids
797 # re-compute commit ids
791 old_commit_ids = pull_request.revisions
798 old_commit_ids = pull_request.revisions
792 pre_load = ["author", "date", "message", "branch"]
799 pre_load = ["author", "date", "message", "branch"]
793 commit_ranges = target_repo.compare(
800 commit_ranges = target_repo.compare(
794 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
801 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
795 pre_load=pre_load)
802 pre_load=pre_load)
796
803
797 ancestor_commit_id = source_repo.get_common_ancestor(
804 ancestor_commit_id = source_repo.get_common_ancestor(
798 source_commit.raw_id, target_commit.raw_id, target_repo)
805 source_commit.raw_id, target_commit.raw_id, target_repo)
799
806
800 pull_request.source_ref = '%s:%s:%s' % (
807 pull_request.source_ref = '%s:%s:%s' % (
801 source_ref_type, source_ref_name, source_commit.raw_id)
808 source_ref_type, source_ref_name, source_commit.raw_id)
802 pull_request.target_ref = '%s:%s:%s' % (
809 pull_request.target_ref = '%s:%s:%s' % (
803 target_ref_type, target_ref_name, ancestor_commit_id)
810 target_ref_type, target_ref_name, ancestor_commit_id)
804
811
805 pull_request.revisions = [
812 pull_request.revisions = [
806 commit.raw_id for commit in reversed(commit_ranges)]
813 commit.raw_id for commit in reversed(commit_ranges)]
807 pull_request.updated_on = datetime.datetime.now()
814 pull_request.updated_on = datetime.datetime.now()
808 Session().add(pull_request)
815 Session().add(pull_request)
809 new_commit_ids = pull_request.revisions
816 new_commit_ids = pull_request.revisions
810
817
811 old_diff_data, new_diff_data = self._generate_update_diffs(
818 old_diff_data, new_diff_data = self._generate_update_diffs(
812 pull_request, pull_request_version)
819 pull_request, pull_request_version)
813
820
814 # calculate commit and file changes
821 # calculate commit and file changes
815 commit_changes = self._calculate_commit_id_changes(
822 commit_changes = self._calculate_commit_id_changes(
816 old_commit_ids, new_commit_ids)
823 old_commit_ids, new_commit_ids)
817 file_changes = self._calculate_file_changes(
824 file_changes = self._calculate_file_changes(
818 old_diff_data, new_diff_data)
825 old_diff_data, new_diff_data)
819
826
820 # set comments as outdated if DIFFS changed
827 # set comments as outdated if DIFFS changed
821 CommentsModel().outdate_comments(
828 CommentsModel().outdate_comments(
822 pull_request, old_diff_data=old_diff_data,
829 pull_request, old_diff_data=old_diff_data,
823 new_diff_data=new_diff_data)
830 new_diff_data=new_diff_data)
824
831
825 valid_commit_changes = (commit_changes.added or commit_changes.removed)
832 valid_commit_changes = (commit_changes.added or commit_changes.removed)
826 file_node_changes = (
833 file_node_changes = (
827 file_changes.added or file_changes.modified or file_changes.removed)
834 file_changes.added or file_changes.modified or file_changes.removed)
828 pr_has_changes = valid_commit_changes or file_node_changes
835 pr_has_changes = valid_commit_changes or file_node_changes
829
836
830 # Add an automatic comment to the pull request, in case
837 # Add an automatic comment to the pull request, in case
831 # anything has changed
838 # anything has changed
832 if pr_has_changes:
839 if pr_has_changes:
833 update_comment = CommentsModel().create(
840 update_comment = CommentsModel().create(
834 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
841 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
835 repo=pull_request.target_repo,
842 repo=pull_request.target_repo,
836 user=pull_request.author,
843 user=pull_request.author,
837 pull_request=pull_request,
844 pull_request=pull_request,
838 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
845 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
839
846
840 # Update status to "Under Review" for added commits
847 # Update status to "Under Review" for added commits
841 for commit_id in commit_changes.added:
848 for commit_id in commit_changes.added:
842 ChangesetStatusModel().set_status(
849 ChangesetStatusModel().set_status(
843 repo=pull_request.source_repo,
850 repo=pull_request.source_repo,
844 status=ChangesetStatus.STATUS_UNDER_REVIEW,
851 status=ChangesetStatus.STATUS_UNDER_REVIEW,
845 comment=update_comment,
852 comment=update_comment,
846 user=pull_request.author,
853 user=pull_request.author,
847 pull_request=pull_request,
854 pull_request=pull_request,
848 revision=commit_id)
855 revision=commit_id)
849
856
850 # send update email to users
857 # send update email to users
851 try:
858 try:
852 self.notify_users(pull_request=pull_request, updating_user=updating_user,
859 self.notify_users(pull_request=pull_request, updating_user=updating_user,
853 ancestor_commit_id=ancestor_commit_id,
860 ancestor_commit_id=ancestor_commit_id,
854 commit_changes=commit_changes,
861 commit_changes=commit_changes,
855 file_changes=file_changes)
862 file_changes=file_changes)
856 except Exception:
863 except Exception:
857 log.exception('Failed to send email notification to users')
864 log.exception('Failed to send email notification to users')
858
865
859 log.debug(
866 log.debug(
860 'Updated pull request %s, added_ids: %s, common_ids: %s, '
867 'Updated pull request %s, added_ids: %s, common_ids: %s, '
861 'removed_ids: %s', pull_request.pull_request_id,
868 'removed_ids: %s', pull_request.pull_request_id,
862 commit_changes.added, commit_changes.common, commit_changes.removed)
869 commit_changes.added, commit_changes.common, commit_changes.removed)
863 log.debug(
870 log.debug(
864 'Updated pull request with the following file changes: %s',
871 'Updated pull request with the following file changes: %s',
865 file_changes)
872 file_changes)
866
873
867 log.info(
874 log.info(
868 "Updated pull request %s from commit %s to commit %s, "
875 "Updated pull request %s from commit %s to commit %s, "
869 "stored new version %s of this pull request.",
876 "stored new version %s of this pull request.",
870 pull_request.pull_request_id, source_ref_id,
877 pull_request.pull_request_id, source_ref_id,
871 pull_request.source_ref_parts.commit_id,
878 pull_request.source_ref_parts.commit_id,
872 pull_request_version.pull_request_version_id)
879 pull_request_version.pull_request_version_id)
873 Session().commit()
880 Session().commit()
874 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
881 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
875
882
876 return UpdateResponse(
883 return UpdateResponse(
877 executed=True, reason=UpdateFailureReason.NONE,
884 executed=True, reason=UpdateFailureReason.NONE,
878 old=pull_request, new=pull_request_version,
885 old=pull_request, new=pull_request_version,
879 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
886 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
880 source_changed=source_changed, target_changed=target_changed)
887 source_changed=source_changed, target_changed=target_changed)
881
888
882 def _create_version_from_snapshot(self, pull_request):
889 def _create_version_from_snapshot(self, pull_request):
883 version = PullRequestVersion()
890 version = PullRequestVersion()
884 version.title = pull_request.title
891 version.title = pull_request.title
885 version.description = pull_request.description
892 version.description = pull_request.description
886 version.status = pull_request.status
893 version.status = pull_request.status
887 version.pull_request_state = pull_request.pull_request_state
894 version.pull_request_state = pull_request.pull_request_state
888 version.created_on = datetime.datetime.now()
895 version.created_on = datetime.datetime.now()
889 version.updated_on = pull_request.updated_on
896 version.updated_on = pull_request.updated_on
890 version.user_id = pull_request.user_id
897 version.user_id = pull_request.user_id
891 version.source_repo = pull_request.source_repo
898 version.source_repo = pull_request.source_repo
892 version.source_ref = pull_request.source_ref
899 version.source_ref = pull_request.source_ref
893 version.target_repo = pull_request.target_repo
900 version.target_repo = pull_request.target_repo
894 version.target_ref = pull_request.target_ref
901 version.target_ref = pull_request.target_ref
895
902
896 version._last_merge_source_rev = pull_request._last_merge_source_rev
903 version._last_merge_source_rev = pull_request._last_merge_source_rev
897 version._last_merge_target_rev = pull_request._last_merge_target_rev
904 version._last_merge_target_rev = pull_request._last_merge_target_rev
898 version.last_merge_status = pull_request.last_merge_status
905 version.last_merge_status = pull_request.last_merge_status
899 version.last_merge_metadata = pull_request.last_merge_metadata
906 version.last_merge_metadata = pull_request.last_merge_metadata
900 version.shadow_merge_ref = pull_request.shadow_merge_ref
907 version.shadow_merge_ref = pull_request.shadow_merge_ref
901 version.merge_rev = pull_request.merge_rev
908 version.merge_rev = pull_request.merge_rev
902 version.reviewer_data = pull_request.reviewer_data
909 version.reviewer_data = pull_request.reviewer_data
903
910
904 version.revisions = pull_request.revisions
911 version.revisions = pull_request.revisions
905 version.pull_request = pull_request
912 version.pull_request = pull_request
906 Session().add(version)
913 Session().add(version)
907 Session().flush()
914 Session().flush()
908
915
909 return version
916 return version
910
917
911 def _generate_update_diffs(self, pull_request, pull_request_version):
918 def _generate_update_diffs(self, pull_request, pull_request_version):
912
919
913 diff_context = (
920 diff_context = (
914 self.DIFF_CONTEXT +
921 self.DIFF_CONTEXT +
915 CommentsModel.needed_extra_diff_context())
922 CommentsModel.needed_extra_diff_context())
916 hide_whitespace_changes = False
923 hide_whitespace_changes = False
917 source_repo = pull_request_version.source_repo
924 source_repo = pull_request_version.source_repo
918 source_ref_id = pull_request_version.source_ref_parts.commit_id
925 source_ref_id = pull_request_version.source_ref_parts.commit_id
919 target_ref_id = pull_request_version.target_ref_parts.commit_id
926 target_ref_id = pull_request_version.target_ref_parts.commit_id
920 old_diff = self._get_diff_from_pr_or_version(
927 old_diff = self._get_diff_from_pr_or_version(
921 source_repo, source_ref_id, target_ref_id,
928 source_repo, source_ref_id, target_ref_id,
922 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
929 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
923
930
924 source_repo = pull_request.source_repo
931 source_repo = pull_request.source_repo
925 source_ref_id = pull_request.source_ref_parts.commit_id
932 source_ref_id = pull_request.source_ref_parts.commit_id
926 target_ref_id = pull_request.target_ref_parts.commit_id
933 target_ref_id = pull_request.target_ref_parts.commit_id
927
934
928 new_diff = self._get_diff_from_pr_or_version(
935 new_diff = self._get_diff_from_pr_or_version(
929 source_repo, source_ref_id, target_ref_id,
936 source_repo, source_ref_id, target_ref_id,
930 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
937 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
931
938
932 old_diff_data = diffs.DiffProcessor(old_diff)
939 old_diff_data = diffs.DiffProcessor(old_diff)
933 old_diff_data.prepare()
940 old_diff_data.prepare()
934 new_diff_data = diffs.DiffProcessor(new_diff)
941 new_diff_data = diffs.DiffProcessor(new_diff)
935 new_diff_data.prepare()
942 new_diff_data.prepare()
936
943
937 return old_diff_data, new_diff_data
944 return old_diff_data, new_diff_data
938
945
939 def _link_comments_to_version(self, pull_request_version):
946 def _link_comments_to_version(self, pull_request_version):
940 """
947 """
941 Link all unlinked comments of this pull request to the given version.
948 Link all unlinked comments of this pull request to the given version.
942
949
943 :param pull_request_version: The `PullRequestVersion` to which
950 :param pull_request_version: The `PullRequestVersion` to which
944 the comments shall be linked.
951 the comments shall be linked.
945
952
946 """
953 """
947 pull_request = pull_request_version.pull_request
954 pull_request = pull_request_version.pull_request
948 comments = ChangesetComment.query()\
955 comments = ChangesetComment.query()\
949 .filter(
956 .filter(
950 # TODO: johbo: Should we query for the repo at all here?
957 # TODO: johbo: Should we query for the repo at all here?
951 # Pending decision on how comments of PRs are to be related
958 # Pending decision on how comments of PRs are to be related
952 # to either the source repo, the target repo or no repo at all.
959 # to either the source repo, the target repo or no repo at all.
953 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
960 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
954 ChangesetComment.pull_request == pull_request,
961 ChangesetComment.pull_request == pull_request,
955 ChangesetComment.pull_request_version == None)\
962 ChangesetComment.pull_request_version == None)\
956 .order_by(ChangesetComment.comment_id.asc())
963 .order_by(ChangesetComment.comment_id.asc())
957
964
958 # TODO: johbo: Find out why this breaks if it is done in a bulk
965 # TODO: johbo: Find out why this breaks if it is done in a bulk
959 # operation.
966 # operation.
960 for comment in comments:
967 for comment in comments:
961 comment.pull_request_version_id = (
968 comment.pull_request_version_id = (
962 pull_request_version.pull_request_version_id)
969 pull_request_version.pull_request_version_id)
963 Session().add(comment)
970 Session().add(comment)
964
971
965 def _calculate_commit_id_changes(self, old_ids, new_ids):
972 def _calculate_commit_id_changes(self, old_ids, new_ids):
966 added = [x for x in new_ids if x not in old_ids]
973 added = [x for x in new_ids if x not in old_ids]
967 common = [x for x in new_ids if x in old_ids]
974 common = [x for x in new_ids if x in old_ids]
968 removed = [x for x in old_ids if x not in new_ids]
975 removed = [x for x in old_ids if x not in new_ids]
969 total = new_ids
976 total = new_ids
970 return ChangeTuple(added, common, removed, total)
977 return ChangeTuple(added, common, removed, total)
971
978
972 def _calculate_file_changes(self, old_diff_data, new_diff_data):
979 def _calculate_file_changes(self, old_diff_data, new_diff_data):
973
980
974 old_files = OrderedDict()
981 old_files = OrderedDict()
975 for diff_data in old_diff_data.parsed_diff:
982 for diff_data in old_diff_data.parsed_diff:
976 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
983 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
977
984
978 added_files = []
985 added_files = []
979 modified_files = []
986 modified_files = []
980 removed_files = []
987 removed_files = []
981 for diff_data in new_diff_data.parsed_diff:
988 for diff_data in new_diff_data.parsed_diff:
982 new_filename = diff_data['filename']
989 new_filename = diff_data['filename']
983 new_hash = md5_safe(diff_data['raw_diff'])
990 new_hash = md5_safe(diff_data['raw_diff'])
984
991
985 old_hash = old_files.get(new_filename)
992 old_hash = old_files.get(new_filename)
986 if not old_hash:
993 if not old_hash:
987 # file is not present in old diff, we have to figure out from parsed diff
994 # file is not present in old diff, we have to figure out from parsed diff
988 # operation ADD/REMOVE
995 # operation ADD/REMOVE
989 operations_dict = diff_data['stats']['ops']
996 operations_dict = diff_data['stats']['ops']
990 if diffs.DEL_FILENODE in operations_dict:
997 if diffs.DEL_FILENODE in operations_dict:
991 removed_files.append(new_filename)
998 removed_files.append(new_filename)
992 else:
999 else:
993 added_files.append(new_filename)
1000 added_files.append(new_filename)
994 else:
1001 else:
995 if new_hash != old_hash:
1002 if new_hash != old_hash:
996 modified_files.append(new_filename)
1003 modified_files.append(new_filename)
997 # now remove a file from old, since we have seen it already
1004 # now remove a file from old, since we have seen it already
998 del old_files[new_filename]
1005 del old_files[new_filename]
999
1006
1000 # removed files is when there are present in old, but not in NEW,
1007 # removed files is when there are present in old, but not in NEW,
1001 # since we remove old files that are present in new diff, left-overs
1008 # since we remove old files that are present in new diff, left-overs
1002 # if any should be the removed files
1009 # if any should be the removed files
1003 removed_files.extend(old_files.keys())
1010 removed_files.extend(old_files.keys())
1004
1011
1005 return FileChangeTuple(added_files, modified_files, removed_files)
1012 return FileChangeTuple(added_files, modified_files, removed_files)
1006
1013
1007 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1014 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1008 """
1015 """
1009 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1016 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1010 so it's always looking the same disregarding on which default
1017 so it's always looking the same disregarding on which default
1011 renderer system is using.
1018 renderer system is using.
1012
1019
1013 :param ancestor_commit_id: ancestor raw_id
1020 :param ancestor_commit_id: ancestor raw_id
1014 :param changes: changes named tuple
1021 :param changes: changes named tuple
1015 :param file_changes: file changes named tuple
1022 :param file_changes: file changes named tuple
1016
1023
1017 """
1024 """
1018 new_status = ChangesetStatus.get_status_lbl(
1025 new_status = ChangesetStatus.get_status_lbl(
1019 ChangesetStatus.STATUS_UNDER_REVIEW)
1026 ChangesetStatus.STATUS_UNDER_REVIEW)
1020
1027
1021 changed_files = (
1028 changed_files = (
1022 file_changes.added + file_changes.modified + file_changes.removed)
1029 file_changes.added + file_changes.modified + file_changes.removed)
1023
1030
1024 params = {
1031 params = {
1025 'under_review_label': new_status,
1032 'under_review_label': new_status,
1026 'added_commits': changes.added,
1033 'added_commits': changes.added,
1027 'removed_commits': changes.removed,
1034 'removed_commits': changes.removed,
1028 'changed_files': changed_files,
1035 'changed_files': changed_files,
1029 'added_files': file_changes.added,
1036 'added_files': file_changes.added,
1030 'modified_files': file_changes.modified,
1037 'modified_files': file_changes.modified,
1031 'removed_files': file_changes.removed,
1038 'removed_files': file_changes.removed,
1032 'ancestor_commit_id': ancestor_commit_id
1039 'ancestor_commit_id': ancestor_commit_id
1033 }
1040 }
1034 renderer = RstTemplateRenderer()
1041 renderer = RstTemplateRenderer()
1035 return renderer.render('pull_request_update.mako', **params)
1042 return renderer.render('pull_request_update.mako', **params)
1036
1043
1037 def edit(self, pull_request, title, description, description_renderer, user):
1044 def edit(self, pull_request, title, description, description_renderer, user):
1038 pull_request = self.__get_pull_request(pull_request)
1045 pull_request = self.__get_pull_request(pull_request)
1039 old_data = pull_request.get_api_data(with_merge_state=False)
1046 old_data = pull_request.get_api_data(with_merge_state=False)
1040 if pull_request.is_closed():
1047 if pull_request.is_closed():
1041 raise ValueError('This pull request is closed')
1048 raise ValueError('This pull request is closed')
1042 if title:
1049 if title:
1043 pull_request.title = title
1050 pull_request.title = title
1044 pull_request.description = description
1051 pull_request.description = description
1045 pull_request.updated_on = datetime.datetime.now()
1052 pull_request.updated_on = datetime.datetime.now()
1046 pull_request.description_renderer = description_renderer
1053 pull_request.description_renderer = description_renderer
1047 Session().add(pull_request)
1054 Session().add(pull_request)
1048 self._log_audit_action(
1055 self._log_audit_action(
1049 'repo.pull_request.edit', {'old_data': old_data},
1056 'repo.pull_request.edit', {'old_data': old_data},
1050 user, pull_request)
1057 user, pull_request)
1051
1058
1052 def update_reviewers(self, pull_request, reviewer_data, user):
1059 def update_reviewers(self, pull_request, reviewer_data, user):
1053 """
1060 """
1054 Update the reviewers in the pull request
1061 Update the reviewers in the pull request
1055
1062
1056 :param pull_request: the pr to update
1063 :param pull_request: the pr to update
1057 :param reviewer_data: list of tuples
1064 :param reviewer_data: list of tuples
1058 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1065 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1059 """
1066 """
1060 pull_request = self.__get_pull_request(pull_request)
1067 pull_request = self.__get_pull_request(pull_request)
1061 if pull_request.is_closed():
1068 if pull_request.is_closed():
1062 raise ValueError('This pull request is closed')
1069 raise ValueError('This pull request is closed')
1063
1070
1064 reviewers = {}
1071 reviewers = {}
1065 for user_id, reasons, mandatory, rules in reviewer_data:
1072 for user_id, reasons, mandatory, rules in reviewer_data:
1066 if isinstance(user_id, (int, compat.string_types)):
1073 if isinstance(user_id, (int, compat.string_types)):
1067 user_id = self._get_user(user_id).user_id
1074 user_id = self._get_user(user_id).user_id
1068 reviewers[user_id] = {
1075 reviewers[user_id] = {
1069 'reasons': reasons, 'mandatory': mandatory}
1076 'reasons': reasons, 'mandatory': mandatory}
1070
1077
1071 reviewers_ids = set(reviewers.keys())
1078 reviewers_ids = set(reviewers.keys())
1072 current_reviewers = PullRequestReviewers.query()\
1079 current_reviewers = PullRequestReviewers.query()\
1073 .filter(PullRequestReviewers.pull_request ==
1080 .filter(PullRequestReviewers.pull_request ==
1074 pull_request).all()
1081 pull_request).all()
1075 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1082 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1076
1083
1077 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1084 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1078 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1085 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1079
1086
1080 log.debug("Adding %s reviewers", ids_to_add)
1087 log.debug("Adding %s reviewers", ids_to_add)
1081 log.debug("Removing %s reviewers", ids_to_remove)
1088 log.debug("Removing %s reviewers", ids_to_remove)
1082 changed = False
1089 changed = False
1083 added_audit_reviewers = []
1090 added_audit_reviewers = []
1084 removed_audit_reviewers = []
1091 removed_audit_reviewers = []
1085
1092
1086 for uid in ids_to_add:
1093 for uid in ids_to_add:
1087 changed = True
1094 changed = True
1088 _usr = self._get_user(uid)
1095 _usr = self._get_user(uid)
1089 reviewer = PullRequestReviewers()
1096 reviewer = PullRequestReviewers()
1090 reviewer.user = _usr
1097 reviewer.user = _usr
1091 reviewer.pull_request = pull_request
1098 reviewer.pull_request = pull_request
1092 reviewer.reasons = reviewers[uid]['reasons']
1099 reviewer.reasons = reviewers[uid]['reasons']
1093 # NOTE(marcink): mandatory shouldn't be changed now
1100 # NOTE(marcink): mandatory shouldn't be changed now
1094 # reviewer.mandatory = reviewers[uid]['reasons']
1101 # reviewer.mandatory = reviewers[uid]['reasons']
1095 Session().add(reviewer)
1102 Session().add(reviewer)
1096 added_audit_reviewers.append(reviewer.get_dict())
1103 added_audit_reviewers.append(reviewer.get_dict())
1097
1104
1098 for uid in ids_to_remove:
1105 for uid in ids_to_remove:
1099 changed = True
1106 changed = True
1100 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1107 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1101 # that prevents and fixes cases that we added the same reviewer twice.
1108 # that prevents and fixes cases that we added the same reviewer twice.
1102 # this CAN happen due to the lack of DB checks
1109 # this CAN happen due to the lack of DB checks
1103 reviewers = PullRequestReviewers.query()\
1110 reviewers = PullRequestReviewers.query()\
1104 .filter(PullRequestReviewers.user_id == uid,
1111 .filter(PullRequestReviewers.user_id == uid,
1105 PullRequestReviewers.pull_request == pull_request)\
1112 PullRequestReviewers.pull_request == pull_request)\
1106 .all()
1113 .all()
1107
1114
1108 for obj in reviewers:
1115 for obj in reviewers:
1109 added_audit_reviewers.append(obj.get_dict())
1116 added_audit_reviewers.append(obj.get_dict())
1110 Session().delete(obj)
1117 Session().delete(obj)
1111
1118
1112 if changed:
1119 if changed:
1113 Session().expire_all()
1120 Session().expire_all()
1114 pull_request.updated_on = datetime.datetime.now()
1121 pull_request.updated_on = datetime.datetime.now()
1115 Session().add(pull_request)
1122 Session().add(pull_request)
1116
1123
1117 # finally store audit logs
1124 # finally store audit logs
1118 for user_data in added_audit_reviewers:
1125 for user_data in added_audit_reviewers:
1119 self._log_audit_action(
1126 self._log_audit_action(
1120 'repo.pull_request.reviewer.add', {'data': user_data},
1127 'repo.pull_request.reviewer.add', {'data': user_data},
1121 user, pull_request)
1128 user, pull_request)
1122 for user_data in removed_audit_reviewers:
1129 for user_data in removed_audit_reviewers:
1123 self._log_audit_action(
1130 self._log_audit_action(
1124 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1131 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1125 user, pull_request)
1132 user, pull_request)
1126
1133
1127 self.notify_reviewers(pull_request, ids_to_add)
1134 self.notify_reviewers(pull_request, ids_to_add)
1128 return ids_to_add, ids_to_remove
1135 return ids_to_add, ids_to_remove
1129
1136
1130 def get_url(self, pull_request, request=None, permalink=False):
1137 def get_url(self, pull_request, request=None, permalink=False):
1131 if not request:
1138 if not request:
1132 request = get_current_request()
1139 request = get_current_request()
1133
1140
1134 if permalink:
1141 if permalink:
1135 return request.route_url(
1142 return request.route_url(
1136 'pull_requests_global',
1143 'pull_requests_global',
1137 pull_request_id=pull_request.pull_request_id,)
1144 pull_request_id=pull_request.pull_request_id,)
1138 else:
1145 else:
1139 return request.route_url('pullrequest_show',
1146 return request.route_url('pullrequest_show',
1140 repo_name=safe_str(pull_request.target_repo.repo_name),
1147 repo_name=safe_str(pull_request.target_repo.repo_name),
1141 pull_request_id=pull_request.pull_request_id,)
1148 pull_request_id=pull_request.pull_request_id,)
1142
1149
1143 def get_shadow_clone_url(self, pull_request, request=None):
1150 def get_shadow_clone_url(self, pull_request, request=None):
1144 """
1151 """
1145 Returns qualified url pointing to the shadow repository. If this pull
1152 Returns qualified url pointing to the shadow repository. If this pull
1146 request is closed there is no shadow repository and ``None`` will be
1153 request is closed there is no shadow repository and ``None`` will be
1147 returned.
1154 returned.
1148 """
1155 """
1149 if pull_request.is_closed():
1156 if pull_request.is_closed():
1150 return None
1157 return None
1151 else:
1158 else:
1152 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1159 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1153 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1160 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1154
1161
1155 def notify_reviewers(self, pull_request, reviewers_ids):
1162 def notify_reviewers(self, pull_request, reviewers_ids):
1156 # notification to reviewers
1163 # notification to reviewers
1157 if not reviewers_ids:
1164 if not reviewers_ids:
1158 return
1165 return
1159
1166
1160 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1167 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1161
1168
1162 pull_request_obj = pull_request
1169 pull_request_obj = pull_request
1163 # get the current participants of this pull request
1170 # get the current participants of this pull request
1164 recipients = reviewers_ids
1171 recipients = reviewers_ids
1165 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1172 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1166
1173
1167 pr_source_repo = pull_request_obj.source_repo
1174 pr_source_repo = pull_request_obj.source_repo
1168 pr_target_repo = pull_request_obj.target_repo
1175 pr_target_repo = pull_request_obj.target_repo
1169
1176
1170 pr_url = h.route_url('pullrequest_show',
1177 pr_url = h.route_url('pullrequest_show',
1171 repo_name=pr_target_repo.repo_name,
1178 repo_name=pr_target_repo.repo_name,
1172 pull_request_id=pull_request_obj.pull_request_id,)
1179 pull_request_id=pull_request_obj.pull_request_id,)
1173
1180
1174 # set some variables for email notification
1181 # set some variables for email notification
1175 pr_target_repo_url = h.route_url(
1182 pr_target_repo_url = h.route_url(
1176 'repo_summary', repo_name=pr_target_repo.repo_name)
1183 'repo_summary', repo_name=pr_target_repo.repo_name)
1177
1184
1178 pr_source_repo_url = h.route_url(
1185 pr_source_repo_url = h.route_url(
1179 'repo_summary', repo_name=pr_source_repo.repo_name)
1186 'repo_summary', repo_name=pr_source_repo.repo_name)
1180
1187
1181 # pull request specifics
1188 # pull request specifics
1182 pull_request_commits = [
1189 pull_request_commits = [
1183 (x.raw_id, x.message)
1190 (x.raw_id, x.message)
1184 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1191 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1185
1192
1186 kwargs = {
1193 kwargs = {
1187 'user': pull_request.author,
1194 'user': pull_request.author,
1188 'pull_request': pull_request_obj,
1195 'pull_request': pull_request_obj,
1189 'pull_request_commits': pull_request_commits,
1196 'pull_request_commits': pull_request_commits,
1190
1197
1191 'pull_request_target_repo': pr_target_repo,
1198 'pull_request_target_repo': pr_target_repo,
1192 'pull_request_target_repo_url': pr_target_repo_url,
1199 'pull_request_target_repo_url': pr_target_repo_url,
1193
1200
1194 'pull_request_source_repo': pr_source_repo,
1201 'pull_request_source_repo': pr_source_repo,
1195 'pull_request_source_repo_url': pr_source_repo_url,
1202 'pull_request_source_repo_url': pr_source_repo_url,
1196
1203
1197 'pull_request_url': pr_url,
1204 'pull_request_url': pr_url,
1198 }
1205 }
1199
1206
1200 # pre-generate the subject for notification itself
1207 # pre-generate the subject for notification itself
1201 (subject,
1208 (subject,
1202 _h, _e, # we don't care about those
1209 _h, _e, # we don't care about those
1203 body_plaintext) = EmailNotificationModel().render_email(
1210 body_plaintext) = EmailNotificationModel().render_email(
1204 notification_type, **kwargs)
1211 notification_type, **kwargs)
1205
1212
1206 # create notification objects, and emails
1213 # create notification objects, and emails
1207 NotificationModel().create(
1214 NotificationModel().create(
1208 created_by=pull_request.author,
1215 created_by=pull_request.author,
1209 notification_subject=subject,
1216 notification_subject=subject,
1210 notification_body=body_plaintext,
1217 notification_body=body_plaintext,
1211 notification_type=notification_type,
1218 notification_type=notification_type,
1212 recipients=recipients,
1219 recipients=recipients,
1213 email_kwargs=kwargs,
1220 email_kwargs=kwargs,
1214 )
1221 )
1215
1222
1216 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1223 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1217 commit_changes, file_changes):
1224 commit_changes, file_changes):
1218
1225
1219 updating_user_id = updating_user.user_id
1226 updating_user_id = updating_user.user_id
1220 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1227 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1221 # NOTE(marcink): send notification to all other users except to
1228 # NOTE(marcink): send notification to all other users except to
1222 # person who updated the PR
1229 # person who updated the PR
1223 recipients = reviewers.difference(set([updating_user_id]))
1230 recipients = reviewers.difference(set([updating_user_id]))
1224
1231
1225 log.debug('Notify following recipients about pull-request update %s', recipients)
1232 log.debug('Notify following recipients about pull-request update %s', recipients)
1226
1233
1227 pull_request_obj = pull_request
1234 pull_request_obj = pull_request
1228
1235
1229 # send email about the update
1236 # send email about the update
1230 changed_files = (
1237 changed_files = (
1231 file_changes.added + file_changes.modified + file_changes.removed)
1238 file_changes.added + file_changes.modified + file_changes.removed)
1232
1239
1233 pr_source_repo = pull_request_obj.source_repo
1240 pr_source_repo = pull_request_obj.source_repo
1234 pr_target_repo = pull_request_obj.target_repo
1241 pr_target_repo = pull_request_obj.target_repo
1235
1242
1236 pr_url = h.route_url('pullrequest_show',
1243 pr_url = h.route_url('pullrequest_show',
1237 repo_name=pr_target_repo.repo_name,
1244 repo_name=pr_target_repo.repo_name,
1238 pull_request_id=pull_request_obj.pull_request_id,)
1245 pull_request_id=pull_request_obj.pull_request_id,)
1239
1246
1240 # set some variables for email notification
1247 # set some variables for email notification
1241 pr_target_repo_url = h.route_url(
1248 pr_target_repo_url = h.route_url(
1242 'repo_summary', repo_name=pr_target_repo.repo_name)
1249 'repo_summary', repo_name=pr_target_repo.repo_name)
1243
1250
1244 pr_source_repo_url = h.route_url(
1251 pr_source_repo_url = h.route_url(
1245 'repo_summary', repo_name=pr_source_repo.repo_name)
1252 'repo_summary', repo_name=pr_source_repo.repo_name)
1246
1253
1247 email_kwargs = {
1254 email_kwargs = {
1248 'date': datetime.datetime.now(),
1255 'date': datetime.datetime.now(),
1249 'updating_user': updating_user,
1256 'updating_user': updating_user,
1250
1257
1251 'pull_request': pull_request_obj,
1258 'pull_request': pull_request_obj,
1252
1259
1253 'pull_request_target_repo': pr_target_repo,
1260 'pull_request_target_repo': pr_target_repo,
1254 'pull_request_target_repo_url': pr_target_repo_url,
1261 'pull_request_target_repo_url': pr_target_repo_url,
1255
1262
1256 'pull_request_source_repo': pr_source_repo,
1263 'pull_request_source_repo': pr_source_repo,
1257 'pull_request_source_repo_url': pr_source_repo_url,
1264 'pull_request_source_repo_url': pr_source_repo_url,
1258
1265
1259 'pull_request_url': pr_url,
1266 'pull_request_url': pr_url,
1260
1267
1261 'ancestor_commit_id': ancestor_commit_id,
1268 'ancestor_commit_id': ancestor_commit_id,
1262 'added_commits': commit_changes.added,
1269 'added_commits': commit_changes.added,
1263 'removed_commits': commit_changes.removed,
1270 'removed_commits': commit_changes.removed,
1264 'changed_files': changed_files,
1271 'changed_files': changed_files,
1265 'added_files': file_changes.added,
1272 'added_files': file_changes.added,
1266 'modified_files': file_changes.modified,
1273 'modified_files': file_changes.modified,
1267 'removed_files': file_changes.removed,
1274 'removed_files': file_changes.removed,
1268 }
1275 }
1269
1276
1270 (subject,
1277 (subject,
1271 _h, _e, # we don't care about those
1278 _h, _e, # we don't care about those
1272 body_plaintext) = EmailNotificationModel().render_email(
1279 body_plaintext) = EmailNotificationModel().render_email(
1273 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1280 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1274
1281
1275 # create notification objects, and emails
1282 # create notification objects, and emails
1276 NotificationModel().create(
1283 NotificationModel().create(
1277 created_by=updating_user,
1284 created_by=updating_user,
1278 notification_subject=subject,
1285 notification_subject=subject,
1279 notification_body=body_plaintext,
1286 notification_body=body_plaintext,
1280 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1287 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1281 recipients=recipients,
1288 recipients=recipients,
1282 email_kwargs=email_kwargs,
1289 email_kwargs=email_kwargs,
1283 )
1290 )
1284
1291
1285 def delete(self, pull_request, user):
1292 def delete(self, pull_request, user):
1286 pull_request = self.__get_pull_request(pull_request)
1293 pull_request = self.__get_pull_request(pull_request)
1287 old_data = pull_request.get_api_data(with_merge_state=False)
1294 old_data = pull_request.get_api_data(with_merge_state=False)
1288 self._cleanup_merge_workspace(pull_request)
1295 self._cleanup_merge_workspace(pull_request)
1289 self._log_audit_action(
1296 self._log_audit_action(
1290 'repo.pull_request.delete', {'old_data': old_data},
1297 'repo.pull_request.delete', {'old_data': old_data},
1291 user, pull_request)
1298 user, pull_request)
1292 Session().delete(pull_request)
1299 Session().delete(pull_request)
1293
1300
1294 def close_pull_request(self, pull_request, user):
1301 def close_pull_request(self, pull_request, user):
1295 pull_request = self.__get_pull_request(pull_request)
1302 pull_request = self.__get_pull_request(pull_request)
1296 self._cleanup_merge_workspace(pull_request)
1303 self._cleanup_merge_workspace(pull_request)
1297 pull_request.status = PullRequest.STATUS_CLOSED
1304 pull_request.status = PullRequest.STATUS_CLOSED
1298 pull_request.updated_on = datetime.datetime.now()
1305 pull_request.updated_on = datetime.datetime.now()
1299 Session().add(pull_request)
1306 Session().add(pull_request)
1300 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1307 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1301
1308
1302 pr_data = pull_request.get_api_data(with_merge_state=False)
1309 pr_data = pull_request.get_api_data(with_merge_state=False)
1303 self._log_audit_action(
1310 self._log_audit_action(
1304 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1311 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1305
1312
1306 def close_pull_request_with_comment(
1313 def close_pull_request_with_comment(
1307 self, pull_request, user, repo, message=None, auth_user=None):
1314 self, pull_request, user, repo, message=None, auth_user=None):
1308
1315
1309 pull_request_review_status = pull_request.calculated_review_status()
1316 pull_request_review_status = pull_request.calculated_review_status()
1310
1317
1311 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1318 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1312 # approved only if we have voting consent
1319 # approved only if we have voting consent
1313 status = ChangesetStatus.STATUS_APPROVED
1320 status = ChangesetStatus.STATUS_APPROVED
1314 else:
1321 else:
1315 status = ChangesetStatus.STATUS_REJECTED
1322 status = ChangesetStatus.STATUS_REJECTED
1316 status_lbl = ChangesetStatus.get_status_lbl(status)
1323 status_lbl = ChangesetStatus.get_status_lbl(status)
1317
1324
1318 default_message = (
1325 default_message = (
1319 'Closing with status change {transition_icon} {status}.'
1326 'Closing with status change {transition_icon} {status}.'
1320 ).format(transition_icon='>', status=status_lbl)
1327 ).format(transition_icon='>', status=status_lbl)
1321 text = message or default_message
1328 text = message or default_message
1322
1329
1323 # create a comment, and link it to new status
1330 # create a comment, and link it to new status
1324 comment = CommentsModel().create(
1331 comment = CommentsModel().create(
1325 text=text,
1332 text=text,
1326 repo=repo.repo_id,
1333 repo=repo.repo_id,
1327 user=user.user_id,
1334 user=user.user_id,
1328 pull_request=pull_request.pull_request_id,
1335 pull_request=pull_request.pull_request_id,
1329 status_change=status_lbl,
1336 status_change=status_lbl,
1330 status_change_type=status,
1337 status_change_type=status,
1331 closing_pr=True,
1338 closing_pr=True,
1332 auth_user=auth_user,
1339 auth_user=auth_user,
1333 )
1340 )
1334
1341
1335 # calculate old status before we change it
1342 # calculate old status before we change it
1336 old_calculated_status = pull_request.calculated_review_status()
1343 old_calculated_status = pull_request.calculated_review_status()
1337 ChangesetStatusModel().set_status(
1344 ChangesetStatusModel().set_status(
1338 repo.repo_id,
1345 repo.repo_id,
1339 status,
1346 status,
1340 user.user_id,
1347 user.user_id,
1341 comment=comment,
1348 comment=comment,
1342 pull_request=pull_request.pull_request_id
1349 pull_request=pull_request.pull_request_id
1343 )
1350 )
1344
1351
1345 Session().flush()
1352 Session().flush()
1346
1353
1347 self.trigger_pull_request_hook(pull_request, user, 'comment',
1354 self.trigger_pull_request_hook(pull_request, user, 'comment',
1348 data={'comment': comment})
1355 data={'comment': comment})
1349
1356
1350 # we now calculate the status of pull request again, and based on that
1357 # we now calculate the status of pull request again, and based on that
1351 # calculation trigger status change. This might happen in cases
1358 # calculation trigger status change. This might happen in cases
1352 # that non-reviewer admin closes a pr, which means his vote doesn't
1359 # that non-reviewer admin closes a pr, which means his vote doesn't
1353 # change the status, while if he's a reviewer this might change it.
1360 # change the status, while if he's a reviewer this might change it.
1354 calculated_status = pull_request.calculated_review_status()
1361 calculated_status = pull_request.calculated_review_status()
1355 if old_calculated_status != calculated_status:
1362 if old_calculated_status != calculated_status:
1356 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1363 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1357 data={'status': calculated_status})
1364 data={'status': calculated_status})
1358
1365
1359 # finally close the PR
1366 # finally close the PR
1360 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1367 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1361
1368
1362 return comment, status
1369 return comment, status
1363
1370
1364 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1371 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1365 _ = translator or get_current_request().translate
1372 _ = translator or get_current_request().translate
1366
1373
1367 if not self._is_merge_enabled(pull_request):
1374 if not self._is_merge_enabled(pull_request):
1368 return None, False, _('Server-side pull request merging is disabled.')
1375 return None, False, _('Server-side pull request merging is disabled.')
1369
1376
1370 if pull_request.is_closed():
1377 if pull_request.is_closed():
1371 return None, False, _('This pull request is closed.')
1378 return None, False, _('This pull request is closed.')
1372
1379
1373 merge_possible, msg = self._check_repo_requirements(
1380 merge_possible, msg = self._check_repo_requirements(
1374 target=pull_request.target_repo, source=pull_request.source_repo,
1381 target=pull_request.target_repo, source=pull_request.source_repo,
1375 translator=_)
1382 translator=_)
1376 if not merge_possible:
1383 if not merge_possible:
1377 return None, merge_possible, msg
1384 return None, merge_possible, msg
1378
1385
1379 try:
1386 try:
1380 merge_response = self._try_merge(
1387 merge_response = self._try_merge(
1381 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1388 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1382 log.debug("Merge response: %s", merge_response)
1389 log.debug("Merge response: %s", merge_response)
1383 return merge_response, merge_response.possible, merge_response.merge_status_message
1390 return merge_response, merge_response.possible, merge_response.merge_status_message
1384 except NotImplementedError:
1391 except NotImplementedError:
1385 return None, False, _('Pull request merging is not supported.')
1392 return None, False, _('Pull request merging is not supported.')
1386
1393
1387 def _check_repo_requirements(self, target, source, translator):
1394 def _check_repo_requirements(self, target, source, translator):
1388 """
1395 """
1389 Check if `target` and `source` have compatible requirements.
1396 Check if `target` and `source` have compatible requirements.
1390
1397
1391 Currently this is just checking for largefiles.
1398 Currently this is just checking for largefiles.
1392 """
1399 """
1393 _ = translator
1400 _ = translator
1394 target_has_largefiles = self._has_largefiles(target)
1401 target_has_largefiles = self._has_largefiles(target)
1395 source_has_largefiles = self._has_largefiles(source)
1402 source_has_largefiles = self._has_largefiles(source)
1396 merge_possible = True
1403 merge_possible = True
1397 message = u''
1404 message = u''
1398
1405
1399 if target_has_largefiles != source_has_largefiles:
1406 if target_has_largefiles != source_has_largefiles:
1400 merge_possible = False
1407 merge_possible = False
1401 if source_has_largefiles:
1408 if source_has_largefiles:
1402 message = _(
1409 message = _(
1403 'Target repository large files support is disabled.')
1410 'Target repository large files support is disabled.')
1404 else:
1411 else:
1405 message = _(
1412 message = _(
1406 'Source repository large files support is disabled.')
1413 'Source repository large files support is disabled.')
1407
1414
1408 return merge_possible, message
1415 return merge_possible, message
1409
1416
1410 def _has_largefiles(self, repo):
1417 def _has_largefiles(self, repo):
1411 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1418 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1412 'extensions', 'largefiles')
1419 'extensions', 'largefiles')
1413 return largefiles_ui and largefiles_ui[0].active
1420 return largefiles_ui and largefiles_ui[0].active
1414
1421
1415 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1422 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1416 """
1423 """
1417 Try to merge the pull request and return the merge status.
1424 Try to merge the pull request and return the merge status.
1418 """
1425 """
1419 log.debug(
1426 log.debug(
1420 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1427 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1421 pull_request.pull_request_id, force_shadow_repo_refresh)
1428 pull_request.pull_request_id, force_shadow_repo_refresh)
1422 target_vcs = pull_request.target_repo.scm_instance()
1429 target_vcs = pull_request.target_repo.scm_instance()
1423 # Refresh the target reference.
1430 # Refresh the target reference.
1424 try:
1431 try:
1425 target_ref = self._refresh_reference(
1432 target_ref = self._refresh_reference(
1426 pull_request.target_ref_parts, target_vcs)
1433 pull_request.target_ref_parts, target_vcs)
1427 except CommitDoesNotExistError:
1434 except CommitDoesNotExistError:
1428 merge_state = MergeResponse(
1435 merge_state = MergeResponse(
1429 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1436 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1430 metadata={'target_ref': pull_request.target_ref_parts})
1437 metadata={'target_ref': pull_request.target_ref_parts})
1431 return merge_state
1438 return merge_state
1432
1439
1433 target_locked = pull_request.target_repo.locked
1440 target_locked = pull_request.target_repo.locked
1434 if target_locked and target_locked[0]:
1441 if target_locked and target_locked[0]:
1435 locked_by = 'user:{}'.format(target_locked[0])
1442 locked_by = 'user:{}'.format(target_locked[0])
1436 log.debug("The target repository is locked by %s.", locked_by)
1443 log.debug("The target repository is locked by %s.", locked_by)
1437 merge_state = MergeResponse(
1444 merge_state = MergeResponse(
1438 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1445 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1439 metadata={'locked_by': locked_by})
1446 metadata={'locked_by': locked_by})
1440 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1447 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1441 pull_request, target_ref):
1448 pull_request, target_ref):
1442 log.debug("Refreshing the merge status of the repository.")
1449 log.debug("Refreshing the merge status of the repository.")
1443 merge_state = self._refresh_merge_state(
1450 merge_state = self._refresh_merge_state(
1444 pull_request, target_vcs, target_ref)
1451 pull_request, target_vcs, target_ref)
1445 else:
1452 else:
1446 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1453 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1447 metadata = {
1454 metadata = {
1448 'unresolved_files': '',
1455 'unresolved_files': '',
1449 'target_ref': pull_request.target_ref_parts,
1456 'target_ref': pull_request.target_ref_parts,
1450 'source_ref': pull_request.source_ref_parts,
1457 'source_ref': pull_request.source_ref_parts,
1451 }
1458 }
1452 if pull_request.last_merge_metadata:
1459 if pull_request.last_merge_metadata:
1453 metadata.update(pull_request.last_merge_metadata)
1460 metadata.update(pull_request.last_merge_metadata)
1454
1461
1455 if not possible and target_ref.type == 'branch':
1462 if not possible and target_ref.type == 'branch':
1456 # NOTE(marcink): case for mercurial multiple heads on branch
1463 # NOTE(marcink): case for mercurial multiple heads on branch
1457 heads = target_vcs._heads(target_ref.name)
1464 heads = target_vcs._heads(target_ref.name)
1458 if len(heads) != 1:
1465 if len(heads) != 1:
1459 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1466 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1460 metadata.update({
1467 metadata.update({
1461 'heads': heads
1468 'heads': heads
1462 })
1469 })
1463
1470
1464 merge_state = MergeResponse(
1471 merge_state = MergeResponse(
1465 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1472 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1466
1473
1467 return merge_state
1474 return merge_state
1468
1475
1469 def _refresh_reference(self, reference, vcs_repository):
1476 def _refresh_reference(self, reference, vcs_repository):
1470 if reference.type in self.UPDATABLE_REF_TYPES:
1477 if reference.type in self.UPDATABLE_REF_TYPES:
1471 name_or_id = reference.name
1478 name_or_id = reference.name
1472 else:
1479 else:
1473 name_or_id = reference.commit_id
1480 name_or_id = reference.commit_id
1474
1481
1475 refreshed_commit = vcs_repository.get_commit(name_or_id)
1482 refreshed_commit = vcs_repository.get_commit(name_or_id)
1476 refreshed_reference = Reference(
1483 refreshed_reference = Reference(
1477 reference.type, reference.name, refreshed_commit.raw_id)
1484 reference.type, reference.name, refreshed_commit.raw_id)
1478 return refreshed_reference
1485 return refreshed_reference
1479
1486
1480 def _needs_merge_state_refresh(self, pull_request, target_reference):
1487 def _needs_merge_state_refresh(self, pull_request, target_reference):
1481 return not(
1488 return not(
1482 pull_request.revisions and
1489 pull_request.revisions and
1483 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1490 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1484 target_reference.commit_id == pull_request._last_merge_target_rev)
1491 target_reference.commit_id == pull_request._last_merge_target_rev)
1485
1492
1486 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1493 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1487 workspace_id = self._workspace_id(pull_request)
1494 workspace_id = self._workspace_id(pull_request)
1488 source_vcs = pull_request.source_repo.scm_instance()
1495 source_vcs = pull_request.source_repo.scm_instance()
1489 repo_id = pull_request.target_repo.repo_id
1496 repo_id = pull_request.target_repo.repo_id
1490 use_rebase = self._use_rebase_for_merging(pull_request)
1497 use_rebase = self._use_rebase_for_merging(pull_request)
1491 close_branch = self._close_branch_before_merging(pull_request)
1498 close_branch = self._close_branch_before_merging(pull_request)
1492 merge_state = target_vcs.merge(
1499 merge_state = target_vcs.merge(
1493 repo_id, workspace_id,
1500 repo_id, workspace_id,
1494 target_reference, source_vcs, pull_request.source_ref_parts,
1501 target_reference, source_vcs, pull_request.source_ref_parts,
1495 dry_run=True, use_rebase=use_rebase,
1502 dry_run=True, use_rebase=use_rebase,
1496 close_branch=close_branch)
1503 close_branch=close_branch)
1497
1504
1498 # Do not store the response if there was an unknown error.
1505 # Do not store the response if there was an unknown error.
1499 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1506 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1500 pull_request._last_merge_source_rev = \
1507 pull_request._last_merge_source_rev = \
1501 pull_request.source_ref_parts.commit_id
1508 pull_request.source_ref_parts.commit_id
1502 pull_request._last_merge_target_rev = target_reference.commit_id
1509 pull_request._last_merge_target_rev = target_reference.commit_id
1503 pull_request.last_merge_status = merge_state.failure_reason
1510 pull_request.last_merge_status = merge_state.failure_reason
1504 pull_request.last_merge_metadata = merge_state.metadata
1511 pull_request.last_merge_metadata = merge_state.metadata
1505
1512
1506 pull_request.shadow_merge_ref = merge_state.merge_ref
1513 pull_request.shadow_merge_ref = merge_state.merge_ref
1507 Session().add(pull_request)
1514 Session().add(pull_request)
1508 Session().commit()
1515 Session().commit()
1509
1516
1510 return merge_state
1517 return merge_state
1511
1518
1512 def _workspace_id(self, pull_request):
1519 def _workspace_id(self, pull_request):
1513 workspace_id = 'pr-%s' % pull_request.pull_request_id
1520 workspace_id = 'pr-%s' % pull_request.pull_request_id
1514 return workspace_id
1521 return workspace_id
1515
1522
1516 def generate_repo_data(self, repo, commit_id=None, branch=None,
1523 def generate_repo_data(self, repo, commit_id=None, branch=None,
1517 bookmark=None, translator=None):
1524 bookmark=None, translator=None):
1518 from rhodecode.model.repo import RepoModel
1525 from rhodecode.model.repo import RepoModel
1519
1526
1520 all_refs, selected_ref = \
1527 all_refs, selected_ref = \
1521 self._get_repo_pullrequest_sources(
1528 self._get_repo_pullrequest_sources(
1522 repo.scm_instance(), commit_id=commit_id,
1529 repo.scm_instance(), commit_id=commit_id,
1523 branch=branch, bookmark=bookmark, translator=translator)
1530 branch=branch, bookmark=bookmark, translator=translator)
1524
1531
1525 refs_select2 = []
1532 refs_select2 = []
1526 for element in all_refs:
1533 for element in all_refs:
1527 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1534 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1528 refs_select2.append({'text': element[1], 'children': children})
1535 refs_select2.append({'text': element[1], 'children': children})
1529
1536
1530 return {
1537 return {
1531 'user': {
1538 'user': {
1532 'user_id': repo.user.user_id,
1539 'user_id': repo.user.user_id,
1533 'username': repo.user.username,
1540 'username': repo.user.username,
1534 'firstname': repo.user.first_name,
1541 'firstname': repo.user.first_name,
1535 'lastname': repo.user.last_name,
1542 'lastname': repo.user.last_name,
1536 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1543 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1537 },
1544 },
1538 'name': repo.repo_name,
1545 'name': repo.repo_name,
1539 'link': RepoModel().get_url(repo),
1546 'link': RepoModel().get_url(repo),
1540 'description': h.chop_at_smart(repo.description_safe, '\n'),
1547 'description': h.chop_at_smart(repo.description_safe, '\n'),
1541 'refs': {
1548 'refs': {
1542 'all_refs': all_refs,
1549 'all_refs': all_refs,
1543 'selected_ref': selected_ref,
1550 'selected_ref': selected_ref,
1544 'select2_refs': refs_select2
1551 'select2_refs': refs_select2
1545 }
1552 }
1546 }
1553 }
1547
1554
1548 def generate_pullrequest_title(self, source, source_ref, target):
1555 def generate_pullrequest_title(self, source, source_ref, target):
1549 return u'{source}#{at_ref} to {target}'.format(
1556 return u'{source}#{at_ref} to {target}'.format(
1550 source=source,
1557 source=source,
1551 at_ref=source_ref,
1558 at_ref=source_ref,
1552 target=target,
1559 target=target,
1553 )
1560 )
1554
1561
1555 def _cleanup_merge_workspace(self, pull_request):
1562 def _cleanup_merge_workspace(self, pull_request):
1556 # Merging related cleanup
1563 # Merging related cleanup
1557 repo_id = pull_request.target_repo.repo_id
1564 repo_id = pull_request.target_repo.repo_id
1558 target_scm = pull_request.target_repo.scm_instance()
1565 target_scm = pull_request.target_repo.scm_instance()
1559 workspace_id = self._workspace_id(pull_request)
1566 workspace_id = self._workspace_id(pull_request)
1560
1567
1561 try:
1568 try:
1562 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1569 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1563 except NotImplementedError:
1570 except NotImplementedError:
1564 pass
1571 pass
1565
1572
1566 def _get_repo_pullrequest_sources(
1573 def _get_repo_pullrequest_sources(
1567 self, repo, commit_id=None, branch=None, bookmark=None,
1574 self, repo, commit_id=None, branch=None, bookmark=None,
1568 translator=None):
1575 translator=None):
1569 """
1576 """
1570 Return a structure with repo's interesting commits, suitable for
1577 Return a structure with repo's interesting commits, suitable for
1571 the selectors in pullrequest controller
1578 the selectors in pullrequest controller
1572
1579
1573 :param commit_id: a commit that must be in the list somehow
1580 :param commit_id: a commit that must be in the list somehow
1574 and selected by default
1581 and selected by default
1575 :param branch: a branch that must be in the list and selected
1582 :param branch: a branch that must be in the list and selected
1576 by default - even if closed
1583 by default - even if closed
1577 :param bookmark: a bookmark that must be in the list and selected
1584 :param bookmark: a bookmark that must be in the list and selected
1578 """
1585 """
1579 _ = translator or get_current_request().translate
1586 _ = translator or get_current_request().translate
1580
1587
1581 commit_id = safe_str(commit_id) if commit_id else None
1588 commit_id = safe_str(commit_id) if commit_id else None
1582 branch = safe_unicode(branch) if branch else None
1589 branch = safe_unicode(branch) if branch else None
1583 bookmark = safe_unicode(bookmark) if bookmark else None
1590 bookmark = safe_unicode(bookmark) if bookmark else None
1584
1591
1585 selected = None
1592 selected = None
1586
1593
1587 # order matters: first source that has commit_id in it will be selected
1594 # order matters: first source that has commit_id in it will be selected
1588 sources = []
1595 sources = []
1589 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1596 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1590 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1597 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1591
1598
1592 if commit_id:
1599 if commit_id:
1593 ref_commit = (h.short_id(commit_id), commit_id)
1600 ref_commit = (h.short_id(commit_id), commit_id)
1594 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1601 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1595
1602
1596 sources.append(
1603 sources.append(
1597 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1604 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1598 )
1605 )
1599
1606
1600 groups = []
1607 groups = []
1601
1608
1602 for group_key, ref_list, group_name, match in sources:
1609 for group_key, ref_list, group_name, match in sources:
1603 group_refs = []
1610 group_refs = []
1604 for ref_name, ref_id in ref_list:
1611 for ref_name, ref_id in ref_list:
1605 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1612 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1606 group_refs.append((ref_key, ref_name))
1613 group_refs.append((ref_key, ref_name))
1607
1614
1608 if not selected:
1615 if not selected:
1609 if set([commit_id, match]) & set([ref_id, ref_name]):
1616 if set([commit_id, match]) & set([ref_id, ref_name]):
1610 selected = ref_key
1617 selected = ref_key
1611
1618
1612 if group_refs:
1619 if group_refs:
1613 groups.append((group_refs, group_name))
1620 groups.append((group_refs, group_name))
1614
1621
1615 if not selected:
1622 if not selected:
1616 ref = commit_id or branch or bookmark
1623 ref = commit_id or branch or bookmark
1617 if ref:
1624 if ref:
1618 raise CommitDoesNotExistError(
1625 raise CommitDoesNotExistError(
1619 u'No commit refs could be found matching: {}'.format(ref))
1626 u'No commit refs could be found matching: {}'.format(ref))
1620 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1627 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1621 selected = u'branch:{}:{}'.format(
1628 selected = u'branch:{}:{}'.format(
1622 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1629 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1623 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1630 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1624 )
1631 )
1625 elif repo.commit_ids:
1632 elif repo.commit_ids:
1626 # make the user select in this case
1633 # make the user select in this case
1627 selected = None
1634 selected = None
1628 else:
1635 else:
1629 raise EmptyRepositoryError()
1636 raise EmptyRepositoryError()
1630 return groups, selected
1637 return groups, selected
1631
1638
1632 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1639 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1633 hide_whitespace_changes, diff_context):
1640 hide_whitespace_changes, diff_context):
1634
1641
1635 return self._get_diff_from_pr_or_version(
1642 return self._get_diff_from_pr_or_version(
1636 source_repo, source_ref_id, target_ref_id,
1643 source_repo, source_ref_id, target_ref_id,
1637 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1644 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1638
1645
1639 def _get_diff_from_pr_or_version(
1646 def _get_diff_from_pr_or_version(
1640 self, source_repo, source_ref_id, target_ref_id,
1647 self, source_repo, source_ref_id, target_ref_id,
1641 hide_whitespace_changes, diff_context):
1648 hide_whitespace_changes, diff_context):
1642
1649
1643 target_commit = source_repo.get_commit(
1650 target_commit = source_repo.get_commit(
1644 commit_id=safe_str(target_ref_id))
1651 commit_id=safe_str(target_ref_id))
1645 source_commit = source_repo.get_commit(
1652 source_commit = source_repo.get_commit(
1646 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1653 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1647 if isinstance(source_repo, Repository):
1654 if isinstance(source_repo, Repository):
1648 vcs_repo = source_repo.scm_instance()
1655 vcs_repo = source_repo.scm_instance()
1649 else:
1656 else:
1650 vcs_repo = source_repo
1657 vcs_repo = source_repo
1651
1658
1652 # TODO: johbo: In the context of an update, we cannot reach
1659 # TODO: johbo: In the context of an update, we cannot reach
1653 # the old commit anymore with our normal mechanisms. It needs
1660 # the old commit anymore with our normal mechanisms. It needs
1654 # some sort of special support in the vcs layer to avoid this
1661 # some sort of special support in the vcs layer to avoid this
1655 # workaround.
1662 # workaround.
1656 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1663 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1657 vcs_repo.alias == 'git'):
1664 vcs_repo.alias == 'git'):
1658 source_commit.raw_id = safe_str(source_ref_id)
1665 source_commit.raw_id = safe_str(source_ref_id)
1659
1666
1660 log.debug('calculating diff between '
1667 log.debug('calculating diff between '
1661 'source_ref:%s and target_ref:%s for repo `%s`',
1668 'source_ref:%s and target_ref:%s for repo `%s`',
1662 target_ref_id, source_ref_id,
1669 target_ref_id, source_ref_id,
1663 safe_unicode(vcs_repo.path))
1670 safe_unicode(vcs_repo.path))
1664
1671
1665 vcs_diff = vcs_repo.get_diff(
1672 vcs_diff = vcs_repo.get_diff(
1666 commit1=target_commit, commit2=source_commit,
1673 commit1=target_commit, commit2=source_commit,
1667 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1674 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1668 return vcs_diff
1675 return vcs_diff
1669
1676
1670 def _is_merge_enabled(self, pull_request):
1677 def _is_merge_enabled(self, pull_request):
1671 return self._get_general_setting(
1678 return self._get_general_setting(
1672 pull_request, 'rhodecode_pr_merge_enabled')
1679 pull_request, 'rhodecode_pr_merge_enabled')
1673
1680
1674 def _use_rebase_for_merging(self, pull_request):
1681 def _use_rebase_for_merging(self, pull_request):
1675 repo_type = pull_request.target_repo.repo_type
1682 repo_type = pull_request.target_repo.repo_type
1676 if repo_type == 'hg':
1683 if repo_type == 'hg':
1677 return self._get_general_setting(
1684 return self._get_general_setting(
1678 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1685 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1679 elif repo_type == 'git':
1686 elif repo_type == 'git':
1680 return self._get_general_setting(
1687 return self._get_general_setting(
1681 pull_request, 'rhodecode_git_use_rebase_for_merging')
1688 pull_request, 'rhodecode_git_use_rebase_for_merging')
1682
1689
1683 return False
1690 return False
1684
1691
1685 def _user_name_for_merging(self, pull_request, user):
1692 def _user_name_for_merging(self, pull_request, user):
1686 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1693 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1687 if env_user_name_attr and hasattr(user, env_user_name_attr):
1694 if env_user_name_attr and hasattr(user, env_user_name_attr):
1688 user_name_attr = env_user_name_attr
1695 user_name_attr = env_user_name_attr
1689 else:
1696 else:
1690 user_name_attr = 'short_contact'
1697 user_name_attr = 'short_contact'
1691
1698
1692 user_name = getattr(user, user_name_attr)
1699 user_name = getattr(user, user_name_attr)
1693 return user_name
1700 return user_name
1694
1701
1695 def _close_branch_before_merging(self, pull_request):
1702 def _close_branch_before_merging(self, pull_request):
1696 repo_type = pull_request.target_repo.repo_type
1703 repo_type = pull_request.target_repo.repo_type
1697 if repo_type == 'hg':
1704 if repo_type == 'hg':
1698 return self._get_general_setting(
1705 return self._get_general_setting(
1699 pull_request, 'rhodecode_hg_close_branch_before_merging')
1706 pull_request, 'rhodecode_hg_close_branch_before_merging')
1700 elif repo_type == 'git':
1707 elif repo_type == 'git':
1701 return self._get_general_setting(
1708 return self._get_general_setting(
1702 pull_request, 'rhodecode_git_close_branch_before_merging')
1709 pull_request, 'rhodecode_git_close_branch_before_merging')
1703
1710
1704 return False
1711 return False
1705
1712
1706 def _get_general_setting(self, pull_request, settings_key, default=False):
1713 def _get_general_setting(self, pull_request, settings_key, default=False):
1707 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1714 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1708 settings = settings_model.get_general_settings()
1715 settings = settings_model.get_general_settings()
1709 return settings.get(settings_key, default)
1716 return settings.get(settings_key, default)
1710
1717
1711 def _log_audit_action(self, action, action_data, user, pull_request):
1718 def _log_audit_action(self, action, action_data, user, pull_request):
1712 audit_logger.store(
1719 audit_logger.store(
1713 action=action,
1720 action=action,
1714 action_data=action_data,
1721 action_data=action_data,
1715 user=user,
1722 user=user,
1716 repo=pull_request.target_repo)
1723 repo=pull_request.target_repo)
1717
1724
1718 def get_reviewer_functions(self):
1725 def get_reviewer_functions(self):
1719 """
1726 """
1720 Fetches functions for validation and fetching default reviewers.
1727 Fetches functions for validation and fetching default reviewers.
1721 If available we use the EE package, else we fallback to CE
1728 If available we use the EE package, else we fallback to CE
1722 package functions
1729 package functions
1723 """
1730 """
1724 try:
1731 try:
1725 from rc_reviewers.utils import get_default_reviewers_data
1732 from rc_reviewers.utils import get_default_reviewers_data
1726 from rc_reviewers.utils import validate_default_reviewers
1733 from rc_reviewers.utils import validate_default_reviewers
1727 except ImportError:
1734 except ImportError:
1728 from rhodecode.apps.repository.utils import get_default_reviewers_data
1735 from rhodecode.apps.repository.utils import get_default_reviewers_data
1729 from rhodecode.apps.repository.utils import validate_default_reviewers
1736 from rhodecode.apps.repository.utils import validate_default_reviewers
1730
1737
1731 return get_default_reviewers_data, validate_default_reviewers
1738 return get_default_reviewers_data, validate_default_reviewers
1732
1739
1733
1740
1734 class MergeCheck(object):
1741 class MergeCheck(object):
1735 """
1742 """
1736 Perform Merge Checks and returns a check object which stores information
1743 Perform Merge Checks and returns a check object which stores information
1737 about merge errors, and merge conditions
1744 about merge errors, and merge conditions
1738 """
1745 """
1739 TODO_CHECK = 'todo'
1746 TODO_CHECK = 'todo'
1740 PERM_CHECK = 'perm'
1747 PERM_CHECK = 'perm'
1741 REVIEW_CHECK = 'review'
1748 REVIEW_CHECK = 'review'
1742 MERGE_CHECK = 'merge'
1749 MERGE_CHECK = 'merge'
1743 WIP_CHECK = 'wip'
1750 WIP_CHECK = 'wip'
1744
1751
1745 def __init__(self):
1752 def __init__(self):
1746 self.review_status = None
1753 self.review_status = None
1747 self.merge_possible = None
1754 self.merge_possible = None
1748 self.merge_msg = ''
1755 self.merge_msg = ''
1749 self.merge_response = None
1756 self.merge_response = None
1750 self.failed = None
1757 self.failed = None
1751 self.errors = []
1758 self.errors = []
1752 self.error_details = OrderedDict()
1759 self.error_details = OrderedDict()
1753 self.source_commit = AttributeDict()
1760 self.source_commit = AttributeDict()
1754 self.target_commit = AttributeDict()
1761 self.target_commit = AttributeDict()
1755
1762
1756 def __repr__(self):
1763 def __repr__(self):
1757 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
1764 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
1758 self.merge_possible, self.failed, self.errors)
1765 self.merge_possible, self.failed, self.errors)
1759
1766
1760 def push_error(self, error_type, message, error_key, details):
1767 def push_error(self, error_type, message, error_key, details):
1761 self.failed = True
1768 self.failed = True
1762 self.errors.append([error_type, message])
1769 self.errors.append([error_type, message])
1763 self.error_details[error_key] = dict(
1770 self.error_details[error_key] = dict(
1764 details=details,
1771 details=details,
1765 error_type=error_type,
1772 error_type=error_type,
1766 message=message
1773 message=message
1767 )
1774 )
1768
1775
1769 @classmethod
1776 @classmethod
1770 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1777 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1771 force_shadow_repo_refresh=False):
1778 force_shadow_repo_refresh=False):
1772 _ = translator
1779 _ = translator
1773 merge_check = cls()
1780 merge_check = cls()
1774
1781
1775 # title has WIP:
1782 # title has WIP:
1776 if pull_request.work_in_progress:
1783 if pull_request.work_in_progress:
1777 log.debug("MergeCheck: cannot merge, title has wip: marker.")
1784 log.debug("MergeCheck: cannot merge, title has wip: marker.")
1778
1785
1779 msg = _('WIP marker in title prevents from accidental merge.')
1786 msg = _('WIP marker in title prevents from accidental merge.')
1780 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
1787 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
1781 if fail_early:
1788 if fail_early:
1782 return merge_check
1789 return merge_check
1783
1790
1784 # permissions to merge
1791 # permissions to merge
1785 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
1792 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
1786 if not user_allowed_to_merge:
1793 if not user_allowed_to_merge:
1787 log.debug("MergeCheck: cannot merge, approval is pending.")
1794 log.debug("MergeCheck: cannot merge, approval is pending.")
1788
1795
1789 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1796 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1790 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1797 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1791 if fail_early:
1798 if fail_early:
1792 return merge_check
1799 return merge_check
1793
1800
1794 # permission to merge into the target branch
1801 # permission to merge into the target branch
1795 target_commit_id = pull_request.target_ref_parts.commit_id
1802 target_commit_id = pull_request.target_ref_parts.commit_id
1796 if pull_request.target_ref_parts.type == 'branch':
1803 if pull_request.target_ref_parts.type == 'branch':
1797 branch_name = pull_request.target_ref_parts.name
1804 branch_name = pull_request.target_ref_parts.name
1798 else:
1805 else:
1799 # for mercurial we can always figure out the branch from the commit
1806 # for mercurial we can always figure out the branch from the commit
1800 # in case of bookmark
1807 # in case of bookmark
1801 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1808 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1802 branch_name = target_commit.branch
1809 branch_name = target_commit.branch
1803
1810
1804 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1811 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1805 pull_request.target_repo.repo_name, branch_name)
1812 pull_request.target_repo.repo_name, branch_name)
1806 if branch_perm and branch_perm == 'branch.none':
1813 if branch_perm and branch_perm == 'branch.none':
1807 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1814 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1808 branch_name, rule)
1815 branch_name, rule)
1809 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1816 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1810 if fail_early:
1817 if fail_early:
1811 return merge_check
1818 return merge_check
1812
1819
1813 # review status, must be always present
1820 # review status, must be always present
1814 review_status = pull_request.calculated_review_status()
1821 review_status = pull_request.calculated_review_status()
1815 merge_check.review_status = review_status
1822 merge_check.review_status = review_status
1816
1823
1817 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1824 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1818 if not status_approved:
1825 if not status_approved:
1819 log.debug("MergeCheck: cannot merge, approval is pending.")
1826 log.debug("MergeCheck: cannot merge, approval is pending.")
1820
1827
1821 msg = _('Pull request reviewer approval is pending.')
1828 msg = _('Pull request reviewer approval is pending.')
1822
1829
1823 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1830 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1824
1831
1825 if fail_early:
1832 if fail_early:
1826 return merge_check
1833 return merge_check
1827
1834
1828 # left over TODOs
1835 # left over TODOs
1829 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1836 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1830 if todos:
1837 if todos:
1831 log.debug("MergeCheck: cannot merge, {} "
1838 log.debug("MergeCheck: cannot merge, {} "
1832 "unresolved TODOs left.".format(len(todos)))
1839 "unresolved TODOs left.".format(len(todos)))
1833
1840
1834 if len(todos) == 1:
1841 if len(todos) == 1:
1835 msg = _('Cannot merge, {} TODO still not resolved.').format(
1842 msg = _('Cannot merge, {} TODO still not resolved.').format(
1836 len(todos))
1843 len(todos))
1837 else:
1844 else:
1838 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1845 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1839 len(todos))
1846 len(todos))
1840
1847
1841 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1848 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1842
1849
1843 if fail_early:
1850 if fail_early:
1844 return merge_check
1851 return merge_check
1845
1852
1846 # merge possible, here is the filesystem simulation + shadow repo
1853 # merge possible, here is the filesystem simulation + shadow repo
1847 merge_response, merge_status, msg = PullRequestModel().merge_status(
1854 merge_response, merge_status, msg = PullRequestModel().merge_status(
1848 pull_request, translator=translator,
1855 pull_request, translator=translator,
1849 force_shadow_repo_refresh=force_shadow_repo_refresh)
1856 force_shadow_repo_refresh=force_shadow_repo_refresh)
1850
1857
1851 merge_check.merge_possible = merge_status
1858 merge_check.merge_possible = merge_status
1852 merge_check.merge_msg = msg
1859 merge_check.merge_msg = msg
1853 merge_check.merge_response = merge_response
1860 merge_check.merge_response = merge_response
1854
1861
1855 source_ref_id = pull_request.source_ref_parts.commit_id
1862 source_ref_id = pull_request.source_ref_parts.commit_id
1856 target_ref_id = pull_request.target_ref_parts.commit_id
1863 target_ref_id = pull_request.target_ref_parts.commit_id
1857
1864
1858 try:
1865 try:
1859 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
1866 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
1860 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
1867 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
1861 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
1868 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
1862 merge_check.source_commit.current_raw_id = source_commit.raw_id
1869 merge_check.source_commit.current_raw_id = source_commit.raw_id
1863 merge_check.source_commit.previous_raw_id = source_ref_id
1870 merge_check.source_commit.previous_raw_id = source_ref_id
1864
1871
1865 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
1872 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
1866 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
1873 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
1867 merge_check.target_commit.current_raw_id = target_commit.raw_id
1874 merge_check.target_commit.current_raw_id = target_commit.raw_id
1868 merge_check.target_commit.previous_raw_id = target_ref_id
1875 merge_check.target_commit.previous_raw_id = target_ref_id
1869 except (SourceRefMissing, TargetRefMissing):
1876 except (SourceRefMissing, TargetRefMissing):
1870 pass
1877 pass
1871
1878
1872 if not merge_status:
1879 if not merge_status:
1873 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1880 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1874 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1881 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1875
1882
1876 if fail_early:
1883 if fail_early:
1877 return merge_check
1884 return merge_check
1878
1885
1879 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1886 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1880 return merge_check
1887 return merge_check
1881
1888
1882 @classmethod
1889 @classmethod
1883 def get_merge_conditions(cls, pull_request, translator):
1890 def get_merge_conditions(cls, pull_request, translator):
1884 _ = translator
1891 _ = translator
1885 merge_details = {}
1892 merge_details = {}
1886
1893
1887 model = PullRequestModel()
1894 model = PullRequestModel()
1888 use_rebase = model._use_rebase_for_merging(pull_request)
1895 use_rebase = model._use_rebase_for_merging(pull_request)
1889
1896
1890 if use_rebase:
1897 if use_rebase:
1891 merge_details['merge_strategy'] = dict(
1898 merge_details['merge_strategy'] = dict(
1892 details={},
1899 details={},
1893 message=_('Merge strategy: rebase')
1900 message=_('Merge strategy: rebase')
1894 )
1901 )
1895 else:
1902 else:
1896 merge_details['merge_strategy'] = dict(
1903 merge_details['merge_strategy'] = dict(
1897 details={},
1904 details={},
1898 message=_('Merge strategy: explicit merge commit')
1905 message=_('Merge strategy: explicit merge commit')
1899 )
1906 )
1900
1907
1901 close_branch = model._close_branch_before_merging(pull_request)
1908 close_branch = model._close_branch_before_merging(pull_request)
1902 if close_branch:
1909 if close_branch:
1903 repo_type = pull_request.target_repo.repo_type
1910 repo_type = pull_request.target_repo.repo_type
1904 close_msg = ''
1911 close_msg = ''
1905 if repo_type == 'hg':
1912 if repo_type == 'hg':
1906 close_msg = _('Source branch will be closed after merge.')
1913 close_msg = _('Source branch will be closed after merge.')
1907 elif repo_type == 'git':
1914 elif repo_type == 'git':
1908 close_msg = _('Source branch will be deleted after merge.')
1915 close_msg = _('Source branch will be deleted after merge.')
1909
1916
1910 merge_details['close_branch'] = dict(
1917 merge_details['close_branch'] = dict(
1911 details={},
1918 details={},
1912 message=close_msg
1919 message=close_msg
1913 )
1920 )
1914
1921
1915 return merge_details
1922 return merge_details
1916
1923
1917
1924
1918 ChangeTuple = collections.namedtuple(
1925 ChangeTuple = collections.namedtuple(
1919 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1926 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1920
1927
1921 FileChangeTuple = collections.namedtuple(
1928 FileChangeTuple = collections.namedtuple(
1922 'FileChangeTuple', ['added', 'modified', 'removed'])
1929 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,91 +1,144 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <div class="panel panel-default">
3 <div class="panel panel-default">
4 <div class="panel-body">
4 <div class="panel-body">
5 %if c.closed:
5 <div style="height: 35px">
6 ${h.checkbox('show_closed',checked="checked", label=_('Show Closed Pull Requests'))}
6 <%
7 %else:
7 selected_filter = 'all'
8 ${h.checkbox('show_closed',label=_('Show Closed Pull Requests'))}
8 if c.closed:
9 %endif
9 selected_filter = 'all_closed'
10 %>
11
12 <ul class="button-links">
13 <li class="btn ${h.is_active('all', selected_filter)}"><a href="${h.route_path('my_account_pullrequests')}">${_('All')}</a></li>
14 <li class="btn ${h.is_active('all_closed', selected_filter)}"><a href="${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}">${_('All + Closed')}</a></li>
15 </ul>
16
17 <div class="grid-quick-filter">
18 <ul class="grid-filter-box">
19 <li class="grid-filter-box-icon">
20 <i class="icon-search"></i>
21 </li>
22 <li class="grid-filter-box-input">
23 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
24 </li>
25 </ul>
26 </div>
27 </div>
10 </div>
28 </div>
11 </div>
29 </div>
12
30
13 <div class="panel panel-default">
31 <div class="panel panel-default">
14 <div class="panel-heading">
32 <div class="panel-heading">
15 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
16 </div>
34 </div>
17 <div class="panel-body panel-body-min-height">
35 <div class="panel-body panel-body-min-height">
18 <table id="pull_request_list_table" class="display"></table>
36 <table id="pull_request_list_table" class="display"></table>
19 </div>
37 </div>
20 </div>
38 </div>
21
39
22 <script type="text/javascript">
40 <script type="text/javascript">
23 $(document).ready(function() {
41 $(document).ready(function () {
24
42
25 $('#show_closed').on('click', function(e){
26 if($(this).is(":checked")){
27 window.location = "${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}";
28 }
29 else{
30 window.location = "${h.route_path('my_account_pullrequests')}";
31 }
32 });
33
34 var $pullRequestListTable = $('#pull_request_list_table');
43 var $pullRequestListTable = $('#pull_request_list_table');
35
44
36 // participating object list
45 // participating object list
37 $pullRequestListTable.DataTable({
46 $pullRequestListTable.DataTable({
38 processing: true,
47 processing: true,
39 serverSide: true,
48 serverSide: true,
40 ajax: {
49 ajax: {
41 "url": "${h.route_path('my_account_pullrequests_data')}",
50 "url": "${h.route_path('my_account_pullrequests_data')}",
42 "data": function (d) {
51 "data": function (d) {
43 d.closed = "${c.closed}";
52 d.closed = "${c.closed}";
53 },
54 "dataSrc": function (json) {
55 return json.data;
44 }
56 }
45 },
57 },
58
46 dom: 'rtp',
59 dom: 'rtp',
47 pageLength: ${c.visual.dashboard_items},
60 pageLength: ${c.visual.dashboard_items},
48 order: [[ 2, "desc" ]],
61 order: [[2, "desc"]],
49 columns: [
62 columns: [
50 { data: {"_": "target_repo",
63 {
51 "sort": "target_repo"}, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false},
64 data: {
52 { data: {"_": "status",
65 "_": "target_repo",
53 "sort": "status"}, title: "", className: "td-status", orderable: false},
66 "sort": "target_repo"
54 { data: {"_": "name",
67 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
55 "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" },
68 },
56 { data: {"_": "title",
69 {
57 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
70 data: {
58 { data: {"_": "author",
71 "_": "status",
59 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
72 "sort": "status"
60 { data: {"_": "comments",
73 }, title: "", className: "td-status", orderable: false
61 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
74 },
62 { data: {"_": "updated_on",
75 {
63 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
76 data: {
77 "_": "name",
78 "sort": "name_raw"
79 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
80 },
81 {
82 data: {
83 "_": "title",
84 "sort": "title"
85 }, title: "${_('Title')}", className: "td-description"
86 },
87 {
88 data: {
89 "_": "author",
90 "sort": "author_raw"
91 }, title: "${_('Author')}", className: "td-user", orderable: false
92 },
93 {
94 data: {
95 "_": "comments",
96 "sort": "comments_raw"
97 }, title: "", className: "td-comments", orderable: false
98 },
99 {
100 data: {
101 "_": "updated_on",
102 "sort": "updated_on_raw"
103 }, title: "${_('Last Update')}", className: "td-time"
104 }
64 ],
105 ],
65 language: {
106 language: {
66 paginate: DEFAULT_GRID_PAGINATION,
107 paginate: DEFAULT_GRID_PAGINATION,
67 sProcessing: _gettext('loading...'),
108 sProcessing: _gettext('loading...'),
68 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
109 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
69 },
110 },
70 "drawCallback": function( settings, json ) {
111 "drawCallback": function (settings, json) {
71 timeagoActivate();
112 timeagoActivate();
72 tooltipActivate();
113 tooltipActivate();
73 },
114 },
74 "createdRow": function ( row, data, index ) {
115 "createdRow": function (row, data, index) {
75 if (data['closed']) {
116 if (data['closed']) {
76 $(row).addClass('closed');
117 $(row).addClass('closed');
77 }
118 }
78 if (data['owned']) {
119 if (data['owned']) {
79 $(row).addClass('owned');
120 $(row).addClass('owned');
80 }
121 }
81 }
122 }
82 });
123 });
83 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
124 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
84 $pullRequestListTable.css('opacity', 1);
125 $pullRequestListTable.css('opacity', 1);
85 });
126 });
86
127
87 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
128 $pullRequestListTable.on('preXhr.dt', function (e, settings, data) {
88 $pullRequestListTable.css('opacity', 0.3);
129 $pullRequestListTable.css('opacity', 0.3);
89 });
130 });
131
132 // filter
133 $('#q_filter').on('keyup',
134 $.debounce(250, function () {
135 $pullRequestListTable.DataTable().search(
136 $('#q_filter').val()
137 ).draw();
138 })
139 );
140
90 });
141 });
142
143
91 </script>
144 </script>
General Comments 0
You need to be logged in to leave comments. Login now