##// END OF EJS Templates
pull-requests: added awaiting my review filter for users pull-requests....
super-admin -
r4690:2e951f8d stable
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,204 +1,208 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 # -*- coding: utf-8 -*-
21 21
22 22 # Copyright (C) 2016-2020 RhodeCode GmbH
23 23 #
24 24 # This program is free software: you can redistribute it and/or modify
25 25 # it under the terms of the GNU Affero General Public License, version 3
26 26 # (only), as published by the Free Software Foundation.
27 27 #
28 28 # This program is distributed in the hope that it will be useful,
29 29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 31 # GNU General Public License for more details.
32 32 #
33 33 # You should have received a copy of the GNU Affero General Public License
34 34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 35 #
36 36 # This program is dual-licensed. If you wish to learn more about the
37 37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 38 # and proprietary license terms, please see https://rhodecode.com/licenses/
39 39
40 40 import pytest
41 41
42 42 from rhodecode.model.db import User
43 43 from rhodecode.tests import TestController, assert_session_flash
44 44 from rhodecode.lib import helpers as h
45 45
46 46
47 47 def route_path(name, params=None, **kwargs):
48 48 import urllib
49 49 from rhodecode.apps._base import ADMIN_PREFIX
50 50
51 51 base_url = {
52 52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
53 53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
54 54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
55 55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
56 56 }[name].format(**kwargs)
57 57
58 58 if params:
59 59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 60 return base_url
61 61
62 62
63 63 class TestMyAccountEdit(TestController):
64 64
65 65 def test_my_account_edit(self):
66 66 self.log_user()
67 67 response = self.app.get(route_path('my_account_edit'))
68 68
69 69 response.mustcontain('value="test_admin')
70 70
71 71 @pytest.mark.backends("git", "hg")
72 72 def test_my_account_my_pullrequests(self, pr_util):
73 73 self.log_user()
74 74 response = self.app.get(route_path('my_account_pullrequests'))
75 75 response.mustcontain('There are currently no open pull '
76 76 'requests requiring your participation.')
77 77
78 78 @pytest.mark.backends("git", "hg")
79 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header):
79 @pytest.mark.parametrize('params, expected_title', [
80 ({'closed': 1}, 'Closed'),
81 ({'awaiting_my_review': 1}, 'Awaiting my review'),
82 ])
83 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
80 84 self.log_user()
81 85 response = self.app.get(route_path('my_account_pullrequests_data'),
82 86 extra_environ=xhr_header)
83 87 assert response.json == {
84 88 u'data': [], u'draw': None,
85 89 u'recordsFiltered': 0, u'recordsTotal': 0}
86 90
87 91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
88 92 expected = {
89 93 'author_raw': 'RhodeCode Admin',
90 94 'name_raw': pr.pull_request_id
91 95 }
92 96 response = self.app.get(route_path('my_account_pullrequests_data'),
93 97 extra_environ=xhr_header)
94 98 assert response.json['recordsTotal'] == 1
95 99 assert response.json['data'][0]['author_raw'] == expected['author_raw']
96 100
97 101 assert response.json['data'][0]['author_raw'] == expected['author_raw']
98 102 assert response.json['data'][0]['name_raw'] == expected['name_raw']
99 103
100 104 @pytest.mark.parametrize(
101 105 "name, attrs", [
102 106 ('firstname', {'firstname': 'new_username'}),
103 107 ('lastname', {'lastname': 'new_username'}),
104 108 ('admin', {'admin': True}),
105 109 ('admin', {'admin': False}),
106 110 ('extern_type', {'extern_type': 'ldap'}),
107 111 ('extern_type', {'extern_type': None}),
108 112 # ('extern_name', {'extern_name': 'test'}),
109 113 # ('extern_name', {'extern_name': None}),
110 114 ('active', {'active': False}),
111 115 ('active', {'active': True}),
112 116 ('email', {'email': u'some@email.com'}),
113 117 ])
114 118 def test_my_account_update(self, name, attrs, user_util):
115 119 usr = user_util.create_user(password='qweqwe')
116 120 params = usr.get_api_data() # current user data
117 121 user_id = usr.user_id
118 122 self.log_user(
119 123 username=usr.username, password='qweqwe')
120 124
121 125 params.update({'password_confirmation': ''})
122 126 params.update({'new_password': ''})
123 127 params.update({'extern_type': u'rhodecode'})
124 128 params.update({'extern_name': u'rhodecode'})
125 129 params.update({'csrf_token': self.csrf_token})
126 130
127 131 params.update(attrs)
128 132 # my account page cannot set language param yet, only for admins
129 133 del params['language']
130 134 if name == 'email':
131 135 uem = user_util.create_additional_user_email(usr, attrs['email'])
132 136 email_before = User.get(user_id).email
133 137
134 138 response = self.app.post(route_path('my_account_update'), params)
135 139
136 140 assert_session_flash(
137 141 response, 'Your account was updated successfully')
138 142
139 143 del params['csrf_token']
140 144
141 145 updated_user = User.get(user_id)
142 146 updated_params = updated_user.get_api_data()
143 147 updated_params.update({'password_confirmation': ''})
144 148 updated_params.update({'new_password': ''})
145 149
146 150 params['last_login'] = updated_params['last_login']
147 151 params['last_activity'] = updated_params['last_activity']
148 152 # my account page cannot set language param yet, only for admins
149 153 # but we get this info from API anyway
150 154 params['language'] = updated_params['language']
151 155
152 156 if name == 'email':
153 157 params['emails'] = [attrs['email'], email_before]
154 158 if name == 'extern_type':
155 159 # cannot update this via form, expected value is original one
156 160 params['extern_type'] = "rhodecode"
157 161 if name == 'extern_name':
158 162 # cannot update this via form, expected value is original one
159 163 params['extern_name'] = str(user_id)
160 164 if name == 'active':
161 165 # my account cannot deactivate account
162 166 params['active'] = True
163 167 if name == 'admin':
164 168 # my account cannot make you an admin !
165 169 params['admin'] = False
166 170
167 171 assert params == updated_params
168 172
169 173 def test_my_account_update_err_email_not_exists_in_emails(self):
170 174 self.log_user()
171 175
172 176 new_email = 'test_regular@mail.com' # not in emails
173 177 params = {
174 178 'username': 'test_admin',
175 179 'new_password': 'test12',
176 180 'password_confirmation': 'test122',
177 181 'firstname': 'NewName',
178 182 'lastname': 'NewLastname',
179 183 'email': new_email,
180 184 'csrf_token': self.csrf_token,
181 185 }
182 186
183 187 response = self.app.post(route_path('my_account_update'),
184 188 params=params)
185 189
186 190 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
187 191
188 192 def test_my_account_update_bad_email_address(self):
189 193 self.log_user('test_regular2', 'test12')
190 194
191 195 new_email = 'newmail.pl'
192 196 params = {
193 197 'username': 'test_admin',
194 198 'new_password': 'test12',
195 199 'password_confirmation': 'test122',
196 200 'firstname': 'NewName',
197 201 'lastname': 'NewLastname',
198 202 'email': new_email,
199 203 'csrf_token': self.csrf_token,
200 204 }
201 205 response = self.app.post(route_path('my_account_update'),
202 206 params=params)
203 207
204 208 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
@@ -1,752 +1,783 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import string
24 24
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import peppercorn
28 28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29 29
30 30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 31 from rhodecode import forms
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib import audit_logger
34 34 from rhodecode.lib.ext_json import json
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, NotAnonymous, CSRFRequired,
37 37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 38 from rhodecode.lib.channelstream import (
39 39 channelstream_request, ChannelstreamException)
40 40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.comment import CommentsModel
43 43 from rhodecode.model.db import (
44 44 IntegrityError, or_, in_filter_generator,
45 45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 PullRequest, UserBookmark, RepoGroup)
46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.pull_request import PullRequestModel
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.model.user_group import UserGroupModel
51 51 from rhodecode.model.validation_schema.schemas import user_schema
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 class MyAccountView(BaseAppView, DataGridAppView):
57 57 ALLOW_SCOPED_TOKENS = False
58 58 """
59 59 This view has alternative version inside EE, if modified please take a look
60 60 in there as well.
61 61 """
62 62
63 63 def load_default_context(self):
64 64 c = self._get_local_tmpl_context()
65 65 c.user = c.auth_user.get_instance()
66 66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 67 return c
68 68
69 69 @LoginRequired()
70 70 @NotAnonymous()
71 71 def my_account_profile(self):
72 72 c = self.load_default_context()
73 73 c.active = 'profile'
74 74 c.extern_type = c.user.extern_type
75 75 return self._get_template_context(c)
76 76
77 77 @LoginRequired()
78 78 @NotAnonymous()
79 79 def my_account_edit(self):
80 80 c = self.load_default_context()
81 81 c.active = 'profile_edit'
82 82 c.extern_type = c.user.extern_type
83 83 c.extern_name = c.user.extern_name
84 84
85 85 schema = user_schema.UserProfileSchema().bind(
86 86 username=c.user.username, user_emails=c.user.emails)
87 87 appstruct = {
88 88 'username': c.user.username,
89 89 'email': c.user.email,
90 90 'firstname': c.user.firstname,
91 91 'lastname': c.user.lastname,
92 92 'description': c.user.description,
93 93 }
94 94 c.form = forms.RcForm(
95 95 schema, appstruct=appstruct,
96 96 action=h.route_path('my_account_update'),
97 97 buttons=(forms.buttons.save, forms.buttons.reset))
98 98
99 99 return self._get_template_context(c)
100 100
101 101 @LoginRequired()
102 102 @NotAnonymous()
103 103 @CSRFRequired()
104 104 def my_account_update(self):
105 105 _ = self.request.translate
106 106 c = self.load_default_context()
107 107 c.active = 'profile_edit'
108 108 c.perm_user = c.auth_user
109 109 c.extern_type = c.user.extern_type
110 110 c.extern_name = c.user.extern_name
111 111
112 112 schema = user_schema.UserProfileSchema().bind(
113 113 username=c.user.username, user_emails=c.user.emails)
114 114 form = forms.RcForm(
115 115 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116 116
117 117 controls = self.request.POST.items()
118 118 try:
119 119 valid_data = form.validate(controls)
120 120 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
121 121 'new_password', 'password_confirmation']
122 122 if c.extern_type != "rhodecode":
123 123 # forbid updating username for external accounts
124 124 skip_attrs.append('username')
125 125 old_email = c.user.email
126 126 UserModel().update_user(
127 127 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
128 128 **valid_data)
129 129 if old_email != valid_data['email']:
130 130 old = UserEmailMap.query() \
131 131 .filter(UserEmailMap.user == c.user)\
132 132 .filter(UserEmailMap.email == valid_data['email'])\
133 133 .first()
134 134 old.email = old_email
135 135 h.flash(_('Your account was updated successfully'), category='success')
136 136 Session().commit()
137 137 except forms.ValidationFailure as e:
138 138 c.form = e
139 139 return self._get_template_context(c)
140 140 except Exception:
141 141 log.exception("Exception updating user")
142 142 h.flash(_('Error occurred during update of user'),
143 143 category='error')
144 144 raise HTTPFound(h.route_path('my_account_profile'))
145 145
146 146 @LoginRequired()
147 147 @NotAnonymous()
148 148 def my_account_password(self):
149 149 c = self.load_default_context()
150 150 c.active = 'password'
151 151 c.extern_type = c.user.extern_type
152 152
153 153 schema = user_schema.ChangePasswordSchema().bind(
154 154 username=c.user.username)
155 155
156 156 form = forms.Form(
157 157 schema,
158 158 action=h.route_path('my_account_password_update'),
159 159 buttons=(forms.buttons.save, forms.buttons.reset))
160 160
161 161 c.form = form
162 162 return self._get_template_context(c)
163 163
164 164 @LoginRequired()
165 165 @NotAnonymous()
166 166 @CSRFRequired()
167 167 def my_account_password_update(self):
168 168 _ = self.request.translate
169 169 c = self.load_default_context()
170 170 c.active = 'password'
171 171 c.extern_type = c.user.extern_type
172 172
173 173 schema = user_schema.ChangePasswordSchema().bind(
174 174 username=c.user.username)
175 175
176 176 form = forms.Form(
177 177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178 178
179 179 if c.extern_type != 'rhodecode':
180 180 raise HTTPFound(self.request.route_path('my_account_password'))
181 181
182 182 controls = self.request.POST.items()
183 183 try:
184 184 valid_data = form.validate(controls)
185 185 UserModel().update_user(c.user.user_id, **valid_data)
186 186 c.user.update_userdata(force_password_change=False)
187 187 Session().commit()
188 188 except forms.ValidationFailure as e:
189 189 c.form = e
190 190 return self._get_template_context(c)
191 191
192 192 except Exception:
193 193 log.exception("Exception updating password")
194 194 h.flash(_('Error occurred during update of user password'),
195 195 category='error')
196 196 else:
197 197 instance = c.auth_user.get_instance()
198 198 self.session.setdefault('rhodecode_user', {}).update(
199 199 {'password': md5(instance.password)})
200 200 self.session.save()
201 201 h.flash(_("Successfully updated password"), category='success')
202 202
203 203 raise HTTPFound(self.request.route_path('my_account_password'))
204 204
205 205 @LoginRequired()
206 206 @NotAnonymous()
207 207 def my_account_auth_tokens(self):
208 208 _ = self.request.translate
209 209
210 210 c = self.load_default_context()
211 211 c.active = 'auth_tokens'
212 212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
213 213 c.role_values = [
214 214 (x, AuthTokenModel.cls._get_role_name(x))
215 215 for x in AuthTokenModel.cls.ROLES]
216 216 c.role_options = [(c.role_values, _("Role"))]
217 217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
218 218 c.user.user_id, show_expired=True)
219 219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
220 220 return self._get_template_context(c)
221 221
222 222 @LoginRequired()
223 223 @NotAnonymous()
224 224 @CSRFRequired()
225 225 def my_account_auth_tokens_view(self):
226 226 _ = self.request.translate
227 227 c = self.load_default_context()
228 228
229 229 auth_token_id = self.request.POST.get('auth_token_id')
230 230
231 231 if auth_token_id:
232 232 token = UserApiKeys.get_or_404(auth_token_id)
233 233 if token.user.user_id != c.user.user_id:
234 234 raise HTTPNotFound()
235 235
236 236 return {
237 237 'auth_token': token.api_key
238 238 }
239 239
240 240 def maybe_attach_token_scope(self, token):
241 241 # implemented in EE edition
242 242 pass
243 243
244 244 @LoginRequired()
245 245 @NotAnonymous()
246 246 @CSRFRequired()
247 247 def my_account_auth_tokens_add(self):
248 248 _ = self.request.translate
249 249 c = self.load_default_context()
250 250
251 251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
252 252 description = self.request.POST.get('description')
253 253 role = self.request.POST.get('role')
254 254
255 255 token = UserModel().add_auth_token(
256 256 user=c.user.user_id,
257 257 lifetime_minutes=lifetime, role=role, description=description,
258 258 scope_callback=self.maybe_attach_token_scope)
259 259 token_data = token.get_api_data()
260 260
261 261 audit_logger.store_web(
262 262 'user.edit.token.add', action_data={
263 263 'data': {'token': token_data, 'user': 'self'}},
264 264 user=self._rhodecode_user, )
265 265 Session().commit()
266 266
267 267 h.flash(_("Auth token successfully created"), category='success')
268 268 return HTTPFound(h.route_path('my_account_auth_tokens'))
269 269
270 270 @LoginRequired()
271 271 @NotAnonymous()
272 272 @CSRFRequired()
273 273 def my_account_auth_tokens_delete(self):
274 274 _ = self.request.translate
275 275 c = self.load_default_context()
276 276
277 277 del_auth_token = self.request.POST.get('del_auth_token')
278 278
279 279 if del_auth_token:
280 280 token = UserApiKeys.get_or_404(del_auth_token)
281 281 token_data = token.get_api_data()
282 282
283 283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
284 284 audit_logger.store_web(
285 285 'user.edit.token.delete', action_data={
286 286 'data': {'token': token_data, 'user': 'self'}},
287 287 user=self._rhodecode_user,)
288 288 Session().commit()
289 289 h.flash(_("Auth token successfully deleted"), category='success')
290 290
291 291 return HTTPFound(h.route_path('my_account_auth_tokens'))
292 292
293 293 @LoginRequired()
294 294 @NotAnonymous()
295 295 def my_account_emails(self):
296 296 _ = self.request.translate
297 297
298 298 c = self.load_default_context()
299 299 c.active = 'emails'
300 300
301 301 c.user_email_map = UserEmailMap.query()\
302 302 .filter(UserEmailMap.user == c.user).all()
303 303
304 304 schema = user_schema.AddEmailSchema().bind(
305 305 username=c.user.username, user_emails=c.user.emails)
306 306
307 307 form = forms.RcForm(schema,
308 308 action=h.route_path('my_account_emails_add'),
309 309 buttons=(forms.buttons.save, forms.buttons.reset))
310 310
311 311 c.form = form
312 312 return self._get_template_context(c)
313 313
314 314 @LoginRequired()
315 315 @NotAnonymous()
316 316 @CSRFRequired()
317 317 def my_account_emails_add(self):
318 318 _ = self.request.translate
319 319 c = self.load_default_context()
320 320 c.active = 'emails'
321 321
322 322 schema = user_schema.AddEmailSchema().bind(
323 323 username=c.user.username, user_emails=c.user.emails)
324 324
325 325 form = forms.RcForm(
326 326 schema, action=h.route_path('my_account_emails_add'),
327 327 buttons=(forms.buttons.save, forms.buttons.reset))
328 328
329 329 controls = self.request.POST.items()
330 330 try:
331 331 valid_data = form.validate(controls)
332 332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
333 333 audit_logger.store_web(
334 334 'user.edit.email.add', action_data={
335 335 'data': {'email': valid_data['email'], 'user': 'self'}},
336 336 user=self._rhodecode_user,)
337 337 Session().commit()
338 338 except formencode.Invalid as error:
339 339 h.flash(h.escape(error.error_dict['email']), category='error')
340 340 except forms.ValidationFailure as e:
341 341 c.user_email_map = UserEmailMap.query() \
342 342 .filter(UserEmailMap.user == c.user).all()
343 343 c.form = e
344 344 return self._get_template_context(c)
345 345 except Exception:
346 346 log.exception("Exception adding email")
347 347 h.flash(_('Error occurred during adding email'),
348 348 category='error')
349 349 else:
350 350 h.flash(_("Successfully added email"), category='success')
351 351
352 352 raise HTTPFound(self.request.route_path('my_account_emails'))
353 353
354 354 @LoginRequired()
355 355 @NotAnonymous()
356 356 @CSRFRequired()
357 357 def my_account_emails_delete(self):
358 358 _ = self.request.translate
359 359 c = self.load_default_context()
360 360
361 361 del_email_id = self.request.POST.get('del_email_id')
362 362 if del_email_id:
363 363 email = UserEmailMap.get_or_404(del_email_id).email
364 364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
365 365 audit_logger.store_web(
366 366 'user.edit.email.delete', action_data={
367 367 'data': {'email': email, 'user': 'self'}},
368 368 user=self._rhodecode_user,)
369 369 Session().commit()
370 370 h.flash(_("Email successfully deleted"),
371 371 category='success')
372 372 return HTTPFound(h.route_path('my_account_emails'))
373 373
374 374 @LoginRequired()
375 375 @NotAnonymous()
376 376 @CSRFRequired()
377 377 def my_account_notifications_test_channelstream(self):
378 378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
379 379 self._rhodecode_user.username, datetime.datetime.now())
380 380 payload = {
381 381 # 'channel': 'broadcast',
382 382 'type': 'message',
383 383 'timestamp': datetime.datetime.utcnow(),
384 384 'user': 'system',
385 385 'pm_users': [self._rhodecode_user.username],
386 386 'message': {
387 387 'message': message,
388 388 'level': 'info',
389 389 'topic': '/notifications'
390 390 }
391 391 }
392 392
393 393 registry = self.request.registry
394 394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
395 395 channelstream_config = rhodecode_plugins.get('channelstream', {})
396 396
397 397 try:
398 398 channelstream_request(channelstream_config, [payload], '/message')
399 399 except ChannelstreamException as e:
400 400 log.exception('Failed to send channelstream data')
401 401 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
402 402 return {"response": 'Channelstream data sent. '
403 403 'You should see a new live message now.'}
404 404
405 405 def _load_my_repos_data(self, watched=False):
406 406
407 407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
408 408
409 409 if watched:
410 410 # repos user watch
411 411 repo_list = Session().query(
412 412 Repository
413 413 ) \
414 414 .join(
415 415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
416 416 ) \
417 417 .filter(
418 418 UserFollowing.user_id == self._rhodecode_user.user_id
419 419 ) \
420 420 .filter(or_(
421 421 # generate multiple IN to fix limitation problems
422 422 *in_filter_generator(Repository.repo_id, allowed_ids))
423 423 ) \
424 424 .order_by(Repository.repo_name) \
425 425 .all()
426 426
427 427 else:
428 428 # repos user is owner of
429 429 repo_list = Session().query(
430 430 Repository
431 431 ) \
432 432 .filter(
433 433 Repository.user_id == self._rhodecode_user.user_id
434 434 ) \
435 435 .filter(or_(
436 436 # generate multiple IN to fix limitation problems
437 437 *in_filter_generator(Repository.repo_id, allowed_ids))
438 438 ) \
439 439 .order_by(Repository.repo_name) \
440 440 .all()
441 441
442 442 _render = self.request.get_partial_renderer(
443 443 'rhodecode:templates/data_table/_dt_elements.mako')
444 444
445 445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
446 446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
447 447 short_name=False, admin=False)
448 448
449 449 repos_data = []
450 450 for repo in repo_list:
451 451 row = {
452 452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
453 453 repo.private, repo.archived, repo.fork),
454 454 "name_raw": repo.repo_name.lower(),
455 455 }
456 456
457 457 repos_data.append(row)
458 458
459 459 # json used to render the grid
460 460 return json.dumps(repos_data)
461 461
462 462 @LoginRequired()
463 463 @NotAnonymous()
464 464 def my_account_repos(self):
465 465 c = self.load_default_context()
466 466 c.active = 'repos'
467 467
468 468 # json used to render the grid
469 469 c.data = self._load_my_repos_data()
470 470 return self._get_template_context(c)
471 471
472 472 @LoginRequired()
473 473 @NotAnonymous()
474 474 def my_account_watched(self):
475 475 c = self.load_default_context()
476 476 c.active = 'watched'
477 477
478 478 # json used to render the grid
479 479 c.data = self._load_my_repos_data(watched=True)
480 480 return self._get_template_context(c)
481 481
482 482 @LoginRequired()
483 483 @NotAnonymous()
484 484 def my_account_bookmarks(self):
485 485 c = self.load_default_context()
486 486 c.active = 'bookmarks'
487 487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
488 488 self._rhodecode_db_user.user_id, cache=False)
489 489 return self._get_template_context(c)
490 490
491 491 def _process_bookmark_entry(self, entry, user_id):
492 492 position = safe_int(entry.get('position'))
493 493 cur_position = safe_int(entry.get('cur_position'))
494 494 if position is None:
495 495 return
496 496
497 497 # check if this is an existing entry
498 498 is_new = False
499 499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
500 500
501 501 if db_entry and str2bool(entry.get('remove')):
502 502 log.debug('Marked bookmark %s for deletion', db_entry)
503 503 Session().delete(db_entry)
504 504 return
505 505
506 506 if not db_entry:
507 507 # new
508 508 db_entry = UserBookmark()
509 509 is_new = True
510 510
511 511 should_save = False
512 512 default_redirect_url = ''
513 513
514 514 # save repo
515 515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
516 516 repo = Repository.get(entry['bookmark_repo'])
517 517 perm_check = HasRepoPermissionAny(
518 518 'repository.read', 'repository.write', 'repository.admin')
519 519 if repo and perm_check(repo_name=repo.repo_name):
520 520 db_entry.repository = repo
521 521 should_save = True
522 522 default_redirect_url = '${repo_url}'
523 523 # save repo group
524 524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
525 525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
526 526 perm_check = HasRepoGroupPermissionAny(
527 527 'group.read', 'group.write', 'group.admin')
528 528
529 529 if repo_group and perm_check(group_name=repo_group.group_name):
530 530 db_entry.repository_group = repo_group
531 531 should_save = True
532 532 default_redirect_url = '${repo_group_url}'
533 533 # save generic info
534 534 elif entry.get('title') and entry.get('redirect_url'):
535 535 should_save = True
536 536
537 537 if should_save:
538 538 # mark user and position
539 539 db_entry.user_id = user_id
540 540 db_entry.position = position
541 541 db_entry.title = entry.get('title')
542 542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
543 543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
544 544
545 545 Session().add(db_entry)
546 546
547 547 @LoginRequired()
548 548 @NotAnonymous()
549 549 @CSRFRequired()
550 550 def my_account_bookmarks_update(self):
551 551 _ = self.request.translate
552 552 c = self.load_default_context()
553 553 c.active = 'bookmarks'
554 554
555 555 controls = peppercorn.parse(self.request.POST.items())
556 556 user_id = c.user.user_id
557 557
558 558 # validate positions
559 559 positions = {}
560 560 for entry in controls.get('bookmarks', []):
561 561 position = safe_int(entry['position'])
562 562 if position is None:
563 563 continue
564 564
565 565 if position in positions:
566 566 h.flash(_("Position {} is defined twice. "
567 567 "Please correct this error.").format(position), category='error')
568 568 return HTTPFound(h.route_path('my_account_bookmarks'))
569 569
570 570 entry['position'] = position
571 571 entry['cur_position'] = safe_int(entry.get('cur_position'))
572 572 positions[position] = entry
573 573
574 574 try:
575 575 for entry in positions.values():
576 576 self._process_bookmark_entry(entry, user_id)
577 577
578 578 Session().commit()
579 579 h.flash(_("Update Bookmarks"), category='success')
580 580 except IntegrityError:
581 581 h.flash(_("Failed to update bookmarks. "
582 582 "Make sure an unique position is used."), category='error')
583 583
584 584 return HTTPFound(h.route_path('my_account_bookmarks'))
585 585
586 586 @LoginRequired()
587 587 @NotAnonymous()
588 588 def my_account_goto_bookmark(self):
589 589
590 590 bookmark_id = self.request.matchdict['bookmark_id']
591 591 user_bookmark = UserBookmark().query()\
592 592 .filter(UserBookmark.user_id == self.request.user.user_id) \
593 593 .filter(UserBookmark.position == bookmark_id).scalar()
594 594
595 595 redirect_url = h.route_path('my_account_bookmarks')
596 596 if not user_bookmark:
597 597 raise HTTPFound(redirect_url)
598 598
599 599 # repository set
600 600 if user_bookmark.repository:
601 601 repo_name = user_bookmark.repository.repo_name
602 602 base_redirect_url = h.route_path(
603 603 'repo_summary', repo_name=repo_name)
604 604 if user_bookmark.redirect_url and \
605 605 '${repo_url}' in user_bookmark.redirect_url:
606 606 redirect_url = string.Template(user_bookmark.redirect_url)\
607 607 .safe_substitute({'repo_url': base_redirect_url})
608 608 else:
609 609 redirect_url = base_redirect_url
610 610 # repository group set
611 611 elif user_bookmark.repository_group:
612 612 repo_group_name = user_bookmark.repository_group.group_name
613 613 base_redirect_url = h.route_path(
614 614 'repo_group_home', repo_group_name=repo_group_name)
615 615 if user_bookmark.redirect_url and \
616 616 '${repo_group_url}' in user_bookmark.redirect_url:
617 617 redirect_url = string.Template(user_bookmark.redirect_url)\
618 618 .safe_substitute({'repo_group_url': base_redirect_url})
619 619 else:
620 620 redirect_url = base_redirect_url
621 621 # custom URL set
622 622 elif user_bookmark.redirect_url:
623 623 server_url = h.route_url('home').rstrip('/')
624 624 redirect_url = string.Template(user_bookmark.redirect_url) \
625 625 .safe_substitute({'server_url': server_url})
626 626
627 627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
628 628 raise HTTPFound(redirect_url)
629 629
630 630 @LoginRequired()
631 631 @NotAnonymous()
632 632 def my_account_perms(self):
633 633 c = self.load_default_context()
634 634 c.active = 'perms'
635 635
636 636 c.perm_user = c.auth_user
637 637 return self._get_template_context(c)
638 638
639 639 @LoginRequired()
640 640 @NotAnonymous()
641 641 def my_notifications(self):
642 642 c = self.load_default_context()
643 643 c.active = 'notifications'
644 644
645 645 return self._get_template_context(c)
646 646
647 647 @LoginRequired()
648 648 @NotAnonymous()
649 649 @CSRFRequired()
650 650 def my_notifications_toggle_visibility(self):
651 651 user = self._rhodecode_db_user
652 652 new_status = not user.user_data.get('notification_status', True)
653 653 user.update_userdata(notification_status=new_status)
654 654 Session().commit()
655 655 return user.user_data['notification_status']
656 656
657 def _get_pull_requests_list(self, statuses):
657 def _get_pull_requests_list(self, statuses, filter_type=None):
658 658 draw, start, limit = self._extract_chunk(self.request)
659 659 search_q, order_by, order_dir = self._extract_ordering(self.request)
660 660
661 661 _render = self.request.get_partial_renderer(
662 662 'rhodecode:templates/data_table/_dt_elements.mako')
663 663
664 if filter_type == 'awaiting_my_review':
665 pull_requests = PullRequestModel().get_im_participating_in_for_review(
666 user_id=self._rhodecode_user.user_id,
667 statuses=statuses, query=search_q,
668 offset=start, length=limit, order_by=order_by,
669 order_dir=order_dir)
670
671 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
672 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
673 else:
664 674 pull_requests = PullRequestModel().get_im_participating_in(
665 675 user_id=self._rhodecode_user.user_id,
666 676 statuses=statuses, query=search_q,
667 677 offset=start, length=limit, order_by=order_by,
668 678 order_dir=order_dir)
669 679
670 680 pull_requests_total_count = PullRequestModel().count_im_participating_in(
671 681 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
672 682
673 683 data = []
674 684 comments_model = CommentsModel()
675 685 for pr in pull_requests:
676 686 repo_id = pr.target_repo_id
677 687 comments_count = comments_model.get_all_comments(
678 688 repo_id, pull_request=pr, include_drafts=False, count_only=True)
679 689 owned = pr.user_id == self._rhodecode_user.user_id
680 690
691 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
692 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
693 if review_statuses and review_statuses[4]:
694 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
695 my_review_status = statuses[0][1].status
696
681 697 data.append({
682 698 'target_repo': _render('pullrequest_target_repo',
683 699 pr.target_repo.repo_name),
684 700 'name': _render('pullrequest_name',
685 701 pr.pull_request_id, pr.pull_request_state,
686 702 pr.work_in_progress, pr.target_repo.repo_name,
687 703 short=True),
688 704 'name_raw': pr.pull_request_id,
689 705 'status': _render('pullrequest_status',
690 706 pr.calculated_review_status()),
707 'my_status': _render('pullrequest_status',
708 my_review_status),
691 709 'title': _render('pullrequest_title', pr.title, pr.description),
692 710 'description': h.escape(pr.description),
693 711 'updated_on': _render('pullrequest_updated_on',
694 712 h.datetime_to_time(pr.updated_on),
695 713 pr.versions_count),
696 714 'updated_on_raw': h.datetime_to_time(pr.updated_on),
697 715 'created_on': _render('pullrequest_updated_on',
698 716 h.datetime_to_time(pr.created_on)),
699 717 'created_on_raw': h.datetime_to_time(pr.created_on),
700 718 'state': pr.pull_request_state,
701 719 'author': _render('pullrequest_author',
702 720 pr.author.full_contact, ),
703 721 'author_raw': pr.author.full_name,
704 722 'comments': _render('pullrequest_comments', comments_count),
705 723 'comments_raw': comments_count,
706 724 'closed': pr.is_closed(),
707 725 'owned': owned
708 726 })
709 727
710 728 # json used to render the grid
711 729 data = ({
712 730 'draw': draw,
713 731 'data': data,
714 732 'recordsTotal': pull_requests_total_count,
715 733 'recordsFiltered': pull_requests_total_count,
716 734 })
717 735 return data
718 736
719 737 @LoginRequired()
720 738 @NotAnonymous()
721 739 def my_account_pullrequests(self):
722 740 c = self.load_default_context()
723 741 c.active = 'pullrequests'
724 742 req_get = self.request.GET
725 743
726 c.closed = str2bool(req_get.get('pr_show_closed'))
744 c.closed = str2bool(req_get.get('closed'))
745 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
746
747 c.selected_filter = 'all'
748 if c.closed:
749 c.selected_filter = 'all_closed'
750 if c.awaiting_my_review:
751 c.selected_filter = 'awaiting_my_review'
727 752
728 753 return self._get_template_context(c)
729 754
730 755 @LoginRequired()
731 756 @NotAnonymous()
732 757 def my_account_pullrequests_data(self):
733 758 self.load_default_context()
734 759 req_get = self.request.GET
760
761 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
735 762 closed = str2bool(req_get.get('closed'))
736 763
737 764 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
738 765 if closed:
739 766 statuses += [PullRequest.STATUS_CLOSED]
740 767
741 data = self._get_pull_requests_list(statuses=statuses)
768 filter_type = \
769 'awaiting_my_review' if awaiting_my_review \
770 else None
771
772 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
742 773 return data
743 774
744 775 @LoginRequired()
745 776 @NotAnonymous()
746 777 def my_account_user_group_membership(self):
747 778 c = self.load_default_context()
748 779 c.active = 'user_group_membership'
749 780 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
750 781 for group in self._rhodecode_db_user.group_member]
751 782 c.user_groups = json.dumps(groups)
752 783 return self._get_template_context(c)
@@ -1,84 +1,84 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22 from rhodecode.model.db import Repository
23 23
24 24
25 25 def route_path(name, params=None, **kwargs):
26 26 import urllib
27 27
28 28 base_url = {
29 29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 31 }[name].format(**kwargs)
32 32
33 33 if params:
34 34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 35 return base_url
36 36
37 37
38 38 @pytest.mark.backends("git", "hg")
39 39 @pytest.mark.usefixtures('autologin_user', 'app')
40 40 class TestPullRequestList(object):
41 41
42 42 @pytest.mark.parametrize('params, expected_title', [
43 43 ({'source': 0, 'closed': 1}, 'Closed'),
44 ({'source': 0, 'my': 1}, 'Opened by me'),
44 ({'source': 0, 'my': 1}, 'Created by me'),
45 45 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
46 46 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
47 47 ({'source': 1}, 'From this repo'),
48 48 ])
49 49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 50 pull_request = pr_util.create_pull_request()
51 51
52 52 response = self.app.get(
53 53 route_path('pullrequest_show_all',
54 54 repo_name=pull_request.target_repo.repo_name,
55 55 params=params))
56 56
57 57 assert_response = response.assert_response()
58 58
59 59 element = assert_response.get_element('.title .active')
60 60 element_text = element.text_content()
61 61 assert expected_title == element_text
62 62
63 63 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
64 64 pull_request = pr_util.create_pull_request()
65 65 response = self.app.get(
66 66 route_path('pullrequest_show_all_data',
67 67 repo_name=pull_request.target_repo.repo_name),
68 68 extra_environ=xhr_header)
69 69
70 70 assert response.json['recordsTotal'] == 1
71 71 assert response.json['data'][0]['description'] == 'Description'
72 72
73 73 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
74 74 xss_description = "<script>alert('Hi!')</script>"
75 75 pull_request = pr_util.create_pull_request(description=xss_description)
76 76
77 77 response = self.app.get(
78 78 route_path('pullrequest_show_all_data',
79 79 repo_name=pull_request.target_repo.repo_name),
80 80 extra_environ=xhr_header)
81 81
82 82 assert response.json['recordsTotal'] == 1
83 83 assert response.json['data'][0]['description'] == \
84 84 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -1,1861 +1,1868 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 statuses=statuses, offset=start, length=limit,
84 order_by=order_by, order_dir=order_dir)
82 repo_name,
83 search_q=search_q, statuses=statuses,
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 opened_by=opened_by)
86 repo_name,
87 search_q=search_q, statuses=statuses)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 offset=start, length=limit, order_by=order_by,
93 order_dir=order_dir)
90 repo_name, self._rhodecode_user.user_id,
91 search_q=search_q, statuses=statuses,
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
94 93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 statuses=statuses, opened_by=opened_by)
94 repo_name, self._rhodecode_user.user_id,
95 search_q=search_q, statuses=statuses)
97 96 else:
98 97 pull_requests = PullRequestModel().get_all(
99 98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 99 statuses=statuses, offset=start, length=limit,
101 100 order_by=order_by, order_dir=order_dir)
102 101 pull_requests_total_count = PullRequestModel().count_all(
103 102 repo_name, search_q=search_q, source=source, statuses=statuses,
104 103 opened_by=opened_by)
105 104
106 105 data = []
107 106 comments_model = CommentsModel()
108 107 for pr in pull_requests:
109 108 comments_count = comments_model.get_all_comments(
110 109 self.db_repo.repo_id, pull_request=pr,
111 110 include_drafts=False, count_only=True)
112 111
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 if review_statuses and review_statuses[4]:
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 my_review_status = statuses[0][1].status
117
113 118 data.append({
114 119 'name': _render('pullrequest_name',
115 120 pr.pull_request_id, pr.pull_request_state,
116 121 pr.work_in_progress, pr.target_repo.repo_name,
117 122 short=True),
118 123 'name_raw': pr.pull_request_id,
119 124 'status': _render('pullrequest_status',
120 125 pr.calculated_review_status()),
126 'my_status': _render('pullrequest_status',
127 my_review_status),
121 128 'title': _render('pullrequest_title', pr.title, pr.description),
122 129 'description': h.escape(pr.description),
123 130 'updated_on': _render('pullrequest_updated_on',
124 131 h.datetime_to_time(pr.updated_on),
125 132 pr.versions_count),
126 133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
127 134 'created_on': _render('pullrequest_updated_on',
128 135 h.datetime_to_time(pr.created_on)),
129 136 'created_on_raw': h.datetime_to_time(pr.created_on),
130 137 'state': pr.pull_request_state,
131 138 'author': _render('pullrequest_author',
132 139 pr.author.full_contact, ),
133 140 'author_raw': pr.author.full_name,
134 141 'comments': _render('pullrequest_comments', comments_count),
135 142 'comments_raw': comments_count,
136 143 'closed': pr.is_closed(),
137 144 })
138 145
139 146 data = ({
140 147 'draw': draw,
141 148 'data': data,
142 149 'recordsTotal': pull_requests_total_count,
143 150 'recordsFiltered': pull_requests_total_count,
144 151 })
145 152 return data
146 153
147 154 @LoginRequired()
148 155 @HasRepoPermissionAnyDecorator(
149 156 'repository.read', 'repository.write', 'repository.admin')
150 157 def pull_request_list(self):
151 158 c = self.load_default_context()
152 159
153 160 req_get = self.request.GET
154 161 c.source = str2bool(req_get.get('source'))
155 162 c.closed = str2bool(req_get.get('closed'))
156 163 c.my = str2bool(req_get.get('my'))
157 164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
158 165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
159 166
160 167 c.active = 'open'
161 168 if c.my:
162 169 c.active = 'my'
163 170 if c.closed:
164 171 c.active = 'closed'
165 172 if c.awaiting_review and not c.source:
166 173 c.active = 'awaiting'
167 174 if c.source and not c.awaiting_review:
168 175 c.active = 'source'
169 176 if c.awaiting_my_review:
170 177 c.active = 'awaiting_my'
171 178
172 179 return self._get_template_context(c)
173 180
174 181 @LoginRequired()
175 182 @HasRepoPermissionAnyDecorator(
176 183 'repository.read', 'repository.write', 'repository.admin')
177 184 def pull_request_list_data(self):
178 185 self.load_default_context()
179 186
180 187 # additional filters
181 188 req_get = self.request.GET
182 189 source = str2bool(req_get.get('source'))
183 190 closed = str2bool(req_get.get('closed'))
184 191 my = str2bool(req_get.get('my'))
185 192 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187 194
188 195 filter_type = 'awaiting_review' if awaiting_review \
189 196 else 'awaiting_my_review' if awaiting_my_review \
190 197 else None
191 198
192 199 opened_by = None
193 200 if my:
194 201 opened_by = [self._rhodecode_user.user_id]
195 202
196 203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 204 if closed:
198 205 statuses = [PullRequest.STATUS_CLOSED]
199 206
200 207 data = self._get_pull_requests_list(
201 208 repo_name=self.db_repo_name, source=source,
202 209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203 210
204 211 return data
205 212
206 213 def _is_diff_cache_enabled(self, target_repo):
207 214 caching_enabled = self._get_general_setting(
208 215 target_repo, 'rhodecode_diff_cache')
209 216 log.debug('Diff caching enabled: %s', caching_enabled)
210 217 return caching_enabled
211 218
212 219 def _get_diffset(self, source_repo_name, source_repo,
213 220 ancestor_commit,
214 221 source_ref_id, target_ref_id,
215 222 target_commit, source_commit, diff_limit, file_limit,
216 223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
217 224
218 225 target_commit_final = target_commit
219 226 source_commit_final = source_commit
220 227
221 228 if use_ancestor:
222 229 # we might want to not use it for versions
223 230 target_ref_id = ancestor_commit.raw_id
224 231 target_commit_final = ancestor_commit
225 232
226 233 vcs_diff = PullRequestModel().get_diff(
227 234 source_repo, source_ref_id, target_ref_id,
228 235 hide_whitespace_changes, diff_context)
229 236
230 237 diff_processor = diffs.DiffProcessor(
231 238 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 239 file_limit=file_limit, show_full_diff=fulldiff)
233 240
234 241 _parsed = diff_processor.prepare()
235 242
236 243 diffset = codeblocks.DiffSet(
237 244 repo_name=self.db_repo_name,
238 245 source_repo_name=source_repo_name,
239 246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
240 247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
241 248 )
242 249 diffset = self.path_filter.render_patchset_filtered(
243 250 diffset, _parsed, target_ref_id, source_ref_id)
244 251
245 252 return diffset
246 253
247 254 def _get_range_diffset(self, source_scm, source_repo,
248 255 commit1, commit2, diff_limit, file_limit,
249 256 fulldiff, hide_whitespace_changes, diff_context):
250 257 vcs_diff = source_scm.get_diff(
251 258 commit1, commit2,
252 259 ignore_whitespace=hide_whitespace_changes,
253 260 context=diff_context)
254 261
255 262 diff_processor = diffs.DiffProcessor(
256 263 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 264 file_limit=file_limit, show_full_diff=fulldiff)
258 265
259 266 _parsed = diff_processor.prepare()
260 267
261 268 diffset = codeblocks.DiffSet(
262 269 repo_name=source_repo.repo_name,
263 270 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 271 target_node_getter=codeblocks.diffset_node_getter(commit2))
265 272
266 273 diffset = self.path_filter.render_patchset_filtered(
267 274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268 275
269 276 return diffset
270 277
271 278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 279 comments_model = CommentsModel()
273 280
274 281 # GENERAL COMMENTS with versions #
275 282 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 283 q = q.order_by(ChangesetComment.comment_id.asc())
277 284 if not include_drafts:
278 285 q = q.filter(ChangesetComment.draft == false())
279 286 general_comments = q
280 287
281 288 # pick comments we want to render at current version
282 289 c.comment_versions = comments_model.aggregate_comments(
283 290 general_comments, versions, c.at_version_num)
284 291
285 292 # INLINE COMMENTS with versions #
286 293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
287 294 q = q.order_by(ChangesetComment.comment_id.asc())
288 295 if not include_drafts:
289 296 q = q.filter(ChangesetComment.draft == false())
290 297 inline_comments = q
291 298
292 299 c.inline_versions = comments_model.aggregate_comments(
293 300 inline_comments, versions, c.at_version_num, inline=True)
294 301
295 302 # Comments inline+general
296 303 if c.at_version:
297 304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
298 305 c.comments = c.comment_versions[c.at_version_num]['display']
299 306 else:
300 307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
301 308 c.comments = c.comment_versions[c.at_version_num]['until']
302 309
303 310 return general_comments, inline_comments
304 311
305 312 @LoginRequired()
306 313 @HasRepoPermissionAnyDecorator(
307 314 'repository.read', 'repository.write', 'repository.admin')
308 315 def pull_request_show(self):
309 316 _ = self.request.translate
310 317 c = self.load_default_context()
311 318
312 319 pull_request = PullRequest.get_or_404(
313 320 self.request.matchdict['pull_request_id'])
314 321 pull_request_id = pull_request.pull_request_id
315 322
316 323 c.state_progressing = pull_request.is_state_changing()
317 324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
318 325
319 326 _new_state = {
320 327 'created': PullRequest.STATE_CREATED,
321 328 }.get(self.request.GET.get('force_state'))
322 329
323 330 if c.is_super_admin and _new_state:
324 331 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
325 332 h.flash(
326 333 _('Pull Request state was force changed to `{}`').format(_new_state),
327 334 category='success')
328 335 Session().commit()
329 336
330 337 raise HTTPFound(h.route_path(
331 338 'pullrequest_show', repo_name=self.db_repo_name,
332 339 pull_request_id=pull_request_id))
333 340
334 341 version = self.request.GET.get('version')
335 342 from_version = self.request.GET.get('from_version') or version
336 343 merge_checks = self.request.GET.get('merge_checks')
337 344 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
338 345 force_refresh = str2bool(self.request.GET.get('force_refresh'))
339 346 c.range_diff_on = self.request.GET.get('range-diff') == "1"
340 347
341 348 # fetch global flags of ignore ws or context lines
342 349 diff_context = diffs.get_diff_context(self.request)
343 350 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
344 351
345 352 (pull_request_latest,
346 353 pull_request_at_ver,
347 354 pull_request_display_obj,
348 355 at_version) = PullRequestModel().get_pr_version(
349 356 pull_request_id, version=version)
350 357
351 358 pr_closed = pull_request_latest.is_closed()
352 359
353 360 if pr_closed and (version or from_version):
354 361 # not allow to browse versions for closed PR
355 362 raise HTTPFound(h.route_path(
356 363 'pullrequest_show', repo_name=self.db_repo_name,
357 364 pull_request_id=pull_request_id))
358 365
359 366 versions = pull_request_display_obj.versions()
360 367
361 368 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
362 369
363 370 # used to store per-commit range diffs
364 371 c.changes = collections.OrderedDict()
365 372
366 373 c.at_version = at_version
367 374 c.at_version_num = (at_version
368 375 if at_version and at_version != PullRequest.LATEST_VER
369 376 else None)
370 377
371 378 c.at_version_index = ChangesetComment.get_index_from_version(
372 379 c.at_version_num, versions)
373 380
374 381 (prev_pull_request_latest,
375 382 prev_pull_request_at_ver,
376 383 prev_pull_request_display_obj,
377 384 prev_at_version) = PullRequestModel().get_pr_version(
378 385 pull_request_id, version=from_version)
379 386
380 387 c.from_version = prev_at_version
381 388 c.from_version_num = (prev_at_version
382 389 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
383 390 else None)
384 391 c.from_version_index = ChangesetComment.get_index_from_version(
385 392 c.from_version_num, versions)
386 393
387 394 # define if we're in COMPARE mode or VIEW at version mode
388 395 compare = at_version != prev_at_version
389 396
390 397 # pull_requests repo_name we opened it against
391 398 # ie. target_repo must match
392 399 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
393 400 log.warning('Mismatch between the current repo: %s, and target %s',
394 401 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
395 402 raise HTTPNotFound()
396 403
397 404 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
398 405
399 406 c.pull_request = pull_request_display_obj
400 407 c.renderer = pull_request_at_ver.description_renderer or c.renderer
401 408 c.pull_request_latest = pull_request_latest
402 409
403 410 # inject latest version
404 411 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
405 412 c.versions = versions + [latest_ver]
406 413
407 414 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
408 415 c.allowed_to_change_status = False
409 416 c.allowed_to_update = False
410 417 c.allowed_to_merge = False
411 418 c.allowed_to_delete = False
412 419 c.allowed_to_comment = False
413 420 c.allowed_to_close = False
414 421 else:
415 422 can_change_status = PullRequestModel().check_user_change_status(
416 423 pull_request_at_ver, self._rhodecode_user)
417 424 c.allowed_to_change_status = can_change_status and not pr_closed
418 425
419 426 c.allowed_to_update = PullRequestModel().check_user_update(
420 427 pull_request_latest, self._rhodecode_user) and not pr_closed
421 428 c.allowed_to_merge = PullRequestModel().check_user_merge(
422 429 pull_request_latest, self._rhodecode_user) and not pr_closed
423 430 c.allowed_to_delete = PullRequestModel().check_user_delete(
424 431 pull_request_latest, self._rhodecode_user) and not pr_closed
425 432 c.allowed_to_comment = not pr_closed
426 433 c.allowed_to_close = c.allowed_to_merge and not pr_closed
427 434
428 435 c.forbid_adding_reviewers = False
429 436
430 437 if pull_request_latest.reviewer_data and \
431 438 'rules' in pull_request_latest.reviewer_data:
432 439 rules = pull_request_latest.reviewer_data['rules'] or {}
433 440 try:
434 441 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
435 442 except Exception:
436 443 pass
437 444
438 445 # check merge capabilities
439 446 _merge_check = MergeCheck.validate(
440 447 pull_request_latest, auth_user=self._rhodecode_user,
441 448 translator=self.request.translate,
442 449 force_shadow_repo_refresh=force_refresh)
443 450
444 451 c.pr_merge_errors = _merge_check.error_details
445 452 c.pr_merge_possible = not _merge_check.failed
446 453 c.pr_merge_message = _merge_check.merge_msg
447 454 c.pr_merge_source_commit = _merge_check.source_commit
448 455 c.pr_merge_target_commit = _merge_check.target_commit
449 456
450 457 c.pr_merge_info = MergeCheck.get_merge_conditions(
451 458 pull_request_latest, translator=self.request.translate)
452 459
453 460 c.pull_request_review_status = _merge_check.review_status
454 461 if merge_checks:
455 462 self.request.override_renderer = \
456 463 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
457 464 return self._get_template_context(c)
458 465
459 466 c.reviewers_count = pull_request.reviewers_count
460 467 c.observers_count = pull_request.observers_count
461 468
462 469 # reviewers and statuses
463 470 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
464 471 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
465 472 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
466 473
467 474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
468 475 member_reviewer = h.reviewer_as_json(
469 476 member, reasons=reasons, mandatory=mandatory,
470 477 role=review_obj.role,
471 478 user_group=review_obj.rule_user_group_data()
472 479 )
473 480
474 481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
475 482 member_reviewer['review_status'] = current_review_status
476 483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
477 484 member_reviewer['allowed_to_update'] = c.allowed_to_update
478 485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
479 486
480 487 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
481 488
482 489 for observer_obj, member in pull_request_at_ver.observers():
483 490 member_observer = h.reviewer_as_json(
484 491 member, reasons=[], mandatory=False,
485 492 role=observer_obj.role,
486 493 user_group=observer_obj.rule_user_group_data()
487 494 )
488 495 member_observer['allowed_to_update'] = c.allowed_to_update
489 496 c.pull_request_set_observers_data_json['observers'].append(member_observer)
490 497
491 498 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
492 499
493 500 general_comments, inline_comments = \
494 501 self.register_comments_vars(c, pull_request_latest, versions)
495 502
496 503 # TODOs
497 504 c.unresolved_comments = CommentsModel() \
498 505 .get_pull_request_unresolved_todos(pull_request_latest)
499 506 c.resolved_comments = CommentsModel() \
500 507 .get_pull_request_resolved_todos(pull_request_latest)
501 508
502 509 # Drafts
503 510 c.draft_comments = CommentsModel().get_pull_request_drafts(
504 511 self._rhodecode_db_user.user_id,
505 512 pull_request_latest)
506 513
507 514 # if we use version, then do not show later comments
508 515 # than current version
509 516 display_inline_comments = collections.defaultdict(
510 517 lambda: collections.defaultdict(list))
511 518 for co in inline_comments:
512 519 if c.at_version_num:
513 520 # pick comments that are at least UPTO given version, so we
514 521 # don't render comments for higher version
515 522 should_render = co.pull_request_version_id and \
516 523 co.pull_request_version_id <= c.at_version_num
517 524 else:
518 525 # showing all, for 'latest'
519 526 should_render = True
520 527
521 528 if should_render:
522 529 display_inline_comments[co.f_path][co.line_no].append(co)
523 530
524 531 # load diff data into template context, if we use compare mode then
525 532 # diff is calculated based on changes between versions of PR
526 533
527 534 source_repo = pull_request_at_ver.source_repo
528 535 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
529 536
530 537 target_repo = pull_request_at_ver.target_repo
531 538 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
532 539
533 540 if compare:
534 541 # in compare switch the diff base to latest commit from prev version
535 542 target_ref_id = prev_pull_request_display_obj.revisions[0]
536 543
537 544 # despite opening commits for bookmarks/branches/tags, we always
538 545 # convert this to rev to prevent changes after bookmark or branch change
539 546 c.source_ref_type = 'rev'
540 547 c.source_ref = source_ref_id
541 548
542 549 c.target_ref_type = 'rev'
543 550 c.target_ref = target_ref_id
544 551
545 552 c.source_repo = source_repo
546 553 c.target_repo = target_repo
547 554
548 555 c.commit_ranges = []
549 556 source_commit = EmptyCommit()
550 557 target_commit = EmptyCommit()
551 558 c.missing_requirements = False
552 559
553 560 source_scm = source_repo.scm_instance()
554 561 target_scm = target_repo.scm_instance()
555 562
556 563 shadow_scm = None
557 564 try:
558 565 shadow_scm = pull_request_latest.get_shadow_repo()
559 566 except Exception:
560 567 log.debug('Failed to get shadow repo', exc_info=True)
561 568 # try first the existing source_repo, and then shadow
562 569 # repo if we can obtain one
563 570 commits_source_repo = source_scm
564 571 if shadow_scm:
565 572 commits_source_repo = shadow_scm
566 573
567 574 c.commits_source_repo = commits_source_repo
568 575 c.ancestor = None # set it to None, to hide it from PR view
569 576
570 577 # empty version means latest, so we keep this to prevent
571 578 # double caching
572 579 version_normalized = version or PullRequest.LATEST_VER
573 580 from_version_normalized = from_version or PullRequest.LATEST_VER
574 581
575 582 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
576 583 cache_file_path = diff_cache_exist(
577 584 cache_path, 'pull_request', pull_request_id, version_normalized,
578 585 from_version_normalized, source_ref_id, target_ref_id,
579 586 hide_whitespace_changes, diff_context, c.fulldiff)
580 587
581 588 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
582 589 force_recache = self.get_recache_flag()
583 590
584 591 cached_diff = None
585 592 if caching_enabled:
586 593 cached_diff = load_cached_diff(cache_file_path)
587 594
588 595 has_proper_commit_cache = (
589 596 cached_diff and cached_diff.get('commits')
590 597 and len(cached_diff.get('commits', [])) == 5
591 598 and cached_diff.get('commits')[0]
592 599 and cached_diff.get('commits')[3])
593 600
594 601 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
595 602 diff_commit_cache = \
596 603 (ancestor_commit, commit_cache, missing_requirements,
597 604 source_commit, target_commit) = cached_diff['commits']
598 605 else:
599 606 # NOTE(marcink): we reach potentially unreachable errors when a PR has
600 607 # merge errors resulting in potentially hidden commits in the shadow repo.
601 608 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
602 609 and _merge_check.merge_response
603 610 maybe_unreachable = maybe_unreachable \
604 611 and _merge_check.merge_response.metadata.get('unresolved_files')
605 612 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
606 613 diff_commit_cache = \
607 614 (ancestor_commit, commit_cache, missing_requirements,
608 615 source_commit, target_commit) = self.get_commits(
609 616 commits_source_repo,
610 617 pull_request_at_ver,
611 618 source_commit,
612 619 source_ref_id,
613 620 source_scm,
614 621 target_commit,
615 622 target_ref_id,
616 623 target_scm,
617 624 maybe_unreachable=maybe_unreachable)
618 625
619 626 # register our commit range
620 627 for comm in commit_cache.values():
621 628 c.commit_ranges.append(comm)
622 629
623 630 c.missing_requirements = missing_requirements
624 631 c.ancestor_commit = ancestor_commit
625 632 c.statuses = source_repo.statuses(
626 633 [x.raw_id for x in c.commit_ranges])
627 634
628 635 # auto collapse if we have more than limit
629 636 collapse_limit = diffs.DiffProcessor._collapse_commits_over
630 637 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
631 638 c.compare_mode = compare
632 639
633 640 # diff_limit is the old behavior, will cut off the whole diff
634 641 # if the limit is applied otherwise will just hide the
635 642 # big files from the front-end
636 643 diff_limit = c.visual.cut_off_limit_diff
637 644 file_limit = c.visual.cut_off_limit_file
638 645
639 646 c.missing_commits = False
640 647 if (c.missing_requirements
641 648 or isinstance(source_commit, EmptyCommit)
642 649 or source_commit == target_commit):
643 650
644 651 c.missing_commits = True
645 652 else:
646 653 c.inline_comments = display_inline_comments
647 654
648 655 use_ancestor = True
649 656 if from_version_normalized != version_normalized:
650 657 use_ancestor = False
651 658
652 659 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
653 660 if not force_recache and has_proper_diff_cache:
654 661 c.diffset = cached_diff['diff']
655 662 else:
656 663 try:
657 664 c.diffset = self._get_diffset(
658 665 c.source_repo.repo_name, commits_source_repo,
659 666 c.ancestor_commit,
660 667 source_ref_id, target_ref_id,
661 668 target_commit, source_commit,
662 669 diff_limit, file_limit, c.fulldiff,
663 670 hide_whitespace_changes, diff_context,
664 671 use_ancestor=use_ancestor
665 672 )
666 673
667 674 # save cached diff
668 675 if caching_enabled:
669 676 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
670 677 except CommitDoesNotExistError:
671 678 log.exception('Failed to generate diffset')
672 679 c.missing_commits = True
673 680
674 681 if not c.missing_commits:
675 682
676 683 c.limited_diff = c.diffset.limited_diff
677 684
678 685 # calculate removed files that are bound to comments
679 686 comment_deleted_files = [
680 687 fname for fname in display_inline_comments
681 688 if fname not in c.diffset.file_stats]
682 689
683 690 c.deleted_files_comments = collections.defaultdict(dict)
684 691 for fname, per_line_comments in display_inline_comments.items():
685 692 if fname in comment_deleted_files:
686 693 c.deleted_files_comments[fname]['stats'] = 0
687 694 c.deleted_files_comments[fname]['comments'] = list()
688 695 for lno, comments in per_line_comments.items():
689 696 c.deleted_files_comments[fname]['comments'].extend(comments)
690 697
691 698 # maybe calculate the range diff
692 699 if c.range_diff_on:
693 700 # TODO(marcink): set whitespace/context
694 701 context_lcl = 3
695 702 ign_whitespace_lcl = False
696 703
697 704 for commit in c.commit_ranges:
698 705 commit2 = commit
699 706 commit1 = commit.first_parent
700 707
701 708 range_diff_cache_file_path = diff_cache_exist(
702 709 cache_path, 'diff', commit.raw_id,
703 710 ign_whitespace_lcl, context_lcl, c.fulldiff)
704 711
705 712 cached_diff = None
706 713 if caching_enabled:
707 714 cached_diff = load_cached_diff(range_diff_cache_file_path)
708 715
709 716 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
710 717 if not force_recache and has_proper_diff_cache:
711 718 diffset = cached_diff['diff']
712 719 else:
713 720 diffset = self._get_range_diffset(
714 721 commits_source_repo, source_repo,
715 722 commit1, commit2, diff_limit, file_limit,
716 723 c.fulldiff, ign_whitespace_lcl, context_lcl
717 724 )
718 725
719 726 # save cached diff
720 727 if caching_enabled:
721 728 cache_diff(range_diff_cache_file_path, diffset, None)
722 729
723 730 c.changes[commit.raw_id] = diffset
724 731
725 732 # this is a hack to properly display links, when creating PR, the
726 733 # compare view and others uses different notation, and
727 734 # compare_commits.mako renders links based on the target_repo.
728 735 # We need to swap that here to generate it properly on the html side
729 736 c.target_repo = c.source_repo
730 737
731 738 c.commit_statuses = ChangesetStatus.STATUSES
732 739
733 740 c.show_version_changes = not pr_closed
734 741 if c.show_version_changes:
735 742 cur_obj = pull_request_at_ver
736 743 prev_obj = prev_pull_request_at_ver
737 744
738 745 old_commit_ids = prev_obj.revisions
739 746 new_commit_ids = cur_obj.revisions
740 747 commit_changes = PullRequestModel()._calculate_commit_id_changes(
741 748 old_commit_ids, new_commit_ids)
742 749 c.commit_changes_summary = commit_changes
743 750
744 751 # calculate the diff for commits between versions
745 752 c.commit_changes = []
746 753
747 754 def mark(cs, fw):
748 755 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
749 756
750 757 for c_type, raw_id in mark(commit_changes.added, 'a') \
751 758 + mark(commit_changes.removed, 'r') \
752 759 + mark(commit_changes.common, 'c'):
753 760
754 761 if raw_id in commit_cache:
755 762 commit = commit_cache[raw_id]
756 763 else:
757 764 try:
758 765 commit = commits_source_repo.get_commit(raw_id)
759 766 except CommitDoesNotExistError:
760 767 # in case we fail extracting still use "dummy" commit
761 768 # for display in commit diff
762 769 commit = h.AttributeDict(
763 770 {'raw_id': raw_id,
764 771 'message': 'EMPTY or MISSING COMMIT'})
765 772 c.commit_changes.append([c_type, commit])
766 773
767 774 # current user review statuses for each version
768 775 c.review_versions = {}
769 776 is_reviewer = PullRequestModel().is_user_reviewer(
770 777 pull_request, self._rhodecode_user)
771 778 if is_reviewer:
772 779 for co in general_comments:
773 780 if co.author.user_id == self._rhodecode_user.user_id:
774 781 status = co.status_change
775 782 if status:
776 783 _ver_pr = status[0].comment.pull_request_version_id
777 784 c.review_versions[_ver_pr] = status[0]
778 785
779 786 return self._get_template_context(c)
780 787
781 788 def get_commits(
782 789 self, commits_source_repo, pull_request_at_ver, source_commit,
783 790 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
784 791 maybe_unreachable=False):
785 792
786 793 commit_cache = collections.OrderedDict()
787 794 missing_requirements = False
788 795
789 796 try:
790 797 pre_load = ["author", "date", "message", "branch", "parents"]
791 798
792 799 pull_request_commits = pull_request_at_ver.revisions
793 800 log.debug('Loading %s commits from %s',
794 801 len(pull_request_commits), commits_source_repo)
795 802
796 803 for rev in pull_request_commits:
797 804 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
798 805 maybe_unreachable=maybe_unreachable)
799 806 commit_cache[comm.raw_id] = comm
800 807
801 808 # Order here matters, we first need to get target, and then
802 809 # the source
803 810 target_commit = commits_source_repo.get_commit(
804 811 commit_id=safe_str(target_ref_id))
805 812
806 813 source_commit = commits_source_repo.get_commit(
807 814 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
808 815 except CommitDoesNotExistError:
809 816 log.warning('Failed to get commit from `{}` repo'.format(
810 817 commits_source_repo), exc_info=True)
811 818 except RepositoryRequirementError:
812 819 log.warning('Failed to get all required data from repo', exc_info=True)
813 820 missing_requirements = True
814 821
815 822 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
816 823
817 824 try:
818 825 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
819 826 except Exception:
820 827 ancestor_commit = None
821 828
822 829 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
823 830
824 831 def assure_not_empty_repo(self):
825 832 _ = self.request.translate
826 833
827 834 try:
828 835 self.db_repo.scm_instance().get_commit()
829 836 except EmptyRepositoryError:
830 837 h.flash(h.literal(_('There are no commits yet')),
831 838 category='warning')
832 839 raise HTTPFound(
833 840 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
834 841
835 842 @LoginRequired()
836 843 @NotAnonymous()
837 844 @HasRepoPermissionAnyDecorator(
838 845 'repository.read', 'repository.write', 'repository.admin')
839 846 def pull_request_new(self):
840 847 _ = self.request.translate
841 848 c = self.load_default_context()
842 849
843 850 self.assure_not_empty_repo()
844 851 source_repo = self.db_repo
845 852
846 853 commit_id = self.request.GET.get('commit')
847 854 branch_ref = self.request.GET.get('branch')
848 855 bookmark_ref = self.request.GET.get('bookmark')
849 856
850 857 try:
851 858 source_repo_data = PullRequestModel().generate_repo_data(
852 859 source_repo, commit_id=commit_id,
853 860 branch=branch_ref, bookmark=bookmark_ref,
854 861 translator=self.request.translate)
855 862 except CommitDoesNotExistError as e:
856 863 log.exception(e)
857 864 h.flash(_('Commit does not exist'), 'error')
858 865 raise HTTPFound(
859 866 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
860 867
861 868 default_target_repo = source_repo
862 869
863 870 if source_repo.parent and c.has_origin_repo_read_perm:
864 871 parent_vcs_obj = source_repo.parent.scm_instance()
865 872 if parent_vcs_obj and not parent_vcs_obj.is_empty():
866 873 # change default if we have a parent repo
867 874 default_target_repo = source_repo.parent
868 875
869 876 target_repo_data = PullRequestModel().generate_repo_data(
870 877 default_target_repo, translator=self.request.translate)
871 878
872 879 selected_source_ref = source_repo_data['refs']['selected_ref']
873 880 title_source_ref = ''
874 881 if selected_source_ref:
875 882 title_source_ref = selected_source_ref.split(':', 2)[1]
876 883 c.default_title = PullRequestModel().generate_pullrequest_title(
877 884 source=source_repo.repo_name,
878 885 source_ref=title_source_ref,
879 886 target=default_target_repo.repo_name
880 887 )
881 888
882 889 c.default_repo_data = {
883 890 'source_repo_name': source_repo.repo_name,
884 891 'source_refs_json': json.dumps(source_repo_data),
885 892 'target_repo_name': default_target_repo.repo_name,
886 893 'target_refs_json': json.dumps(target_repo_data),
887 894 }
888 895 c.default_source_ref = selected_source_ref
889 896
890 897 return self._get_template_context(c)
891 898
892 899 @LoginRequired()
893 900 @NotAnonymous()
894 901 @HasRepoPermissionAnyDecorator(
895 902 'repository.read', 'repository.write', 'repository.admin')
896 903 def pull_request_repo_refs(self):
897 904 self.load_default_context()
898 905 target_repo_name = self.request.matchdict['target_repo_name']
899 906 repo = Repository.get_by_repo_name(target_repo_name)
900 907 if not repo:
901 908 raise HTTPNotFound()
902 909
903 910 target_perm = HasRepoPermissionAny(
904 911 'repository.read', 'repository.write', 'repository.admin')(
905 912 target_repo_name)
906 913 if not target_perm:
907 914 raise HTTPNotFound()
908 915
909 916 return PullRequestModel().generate_repo_data(
910 917 repo, translator=self.request.translate)
911 918
912 919 @LoginRequired()
913 920 @NotAnonymous()
914 921 @HasRepoPermissionAnyDecorator(
915 922 'repository.read', 'repository.write', 'repository.admin')
916 923 def pullrequest_repo_targets(self):
917 924 _ = self.request.translate
918 925 filter_query = self.request.GET.get('query')
919 926
920 927 # get the parents
921 928 parent_target_repos = []
922 929 if self.db_repo.parent:
923 930 parents_query = Repository.query() \
924 931 .order_by(func.length(Repository.repo_name)) \
925 932 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
926 933
927 934 if filter_query:
928 935 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
929 936 parents_query = parents_query.filter(
930 937 Repository.repo_name.ilike(ilike_expression))
931 938 parents = parents_query.limit(20).all()
932 939
933 940 for parent in parents:
934 941 parent_vcs_obj = parent.scm_instance()
935 942 if parent_vcs_obj and not parent_vcs_obj.is_empty():
936 943 parent_target_repos.append(parent)
937 944
938 945 # get other forks, and repo itself
939 946 query = Repository.query() \
940 947 .order_by(func.length(Repository.repo_name)) \
941 948 .filter(
942 949 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
943 950 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
944 951 ) \
945 952 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
946 953
947 954 if filter_query:
948 955 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
949 956 query = query.filter(Repository.repo_name.ilike(ilike_expression))
950 957
951 958 limit = max(20 - len(parent_target_repos), 5) # not less then 5
952 959 target_repos = query.limit(limit).all()
953 960
954 961 all_target_repos = target_repos + parent_target_repos
955 962
956 963 repos = []
957 964 # This checks permissions to the repositories
958 965 for obj in ScmModel().get_repos(all_target_repos):
959 966 repos.append({
960 967 'id': obj['name'],
961 968 'text': obj['name'],
962 969 'type': 'repo',
963 970 'repo_id': obj['dbrepo']['repo_id'],
964 971 'repo_type': obj['dbrepo']['repo_type'],
965 972 'private': obj['dbrepo']['private'],
966 973
967 974 })
968 975
969 976 data = {
970 977 'more': False,
971 978 'results': [{
972 979 'text': _('Repositories'),
973 980 'children': repos
974 981 }] if repos else []
975 982 }
976 983 return data
977 984
978 985 @classmethod
979 986 def get_comment_ids(cls, post_data):
980 987 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
981 988
982 989 @LoginRequired()
983 990 @NotAnonymous()
984 991 @HasRepoPermissionAnyDecorator(
985 992 'repository.read', 'repository.write', 'repository.admin')
986 993 def pullrequest_comments(self):
987 994 self.load_default_context()
988 995
989 996 pull_request = PullRequest.get_or_404(
990 997 self.request.matchdict['pull_request_id'])
991 998 pull_request_id = pull_request.pull_request_id
992 999 version = self.request.GET.get('version')
993 1000
994 1001 _render = self.request.get_partial_renderer(
995 1002 'rhodecode:templates/base/sidebar.mako')
996 1003 c = _render.get_call_context()
997 1004
998 1005 (pull_request_latest,
999 1006 pull_request_at_ver,
1000 1007 pull_request_display_obj,
1001 1008 at_version) = PullRequestModel().get_pr_version(
1002 1009 pull_request_id, version=version)
1003 1010 versions = pull_request_display_obj.versions()
1004 1011 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1005 1012 c.versions = versions + [latest_ver]
1006 1013
1007 1014 c.at_version = at_version
1008 1015 c.at_version_num = (at_version
1009 1016 if at_version and at_version != PullRequest.LATEST_VER
1010 1017 else None)
1011 1018
1012 1019 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1013 1020 all_comments = c.inline_comments_flat + c.comments
1014 1021
1015 1022 existing_ids = self.get_comment_ids(self.request.POST)
1016 1023 return _render('comments_table', all_comments, len(all_comments),
1017 1024 existing_ids=existing_ids)
1018 1025
1019 1026 @LoginRequired()
1020 1027 @NotAnonymous()
1021 1028 @HasRepoPermissionAnyDecorator(
1022 1029 'repository.read', 'repository.write', 'repository.admin')
1023 1030 def pullrequest_todos(self):
1024 1031 self.load_default_context()
1025 1032
1026 1033 pull_request = PullRequest.get_or_404(
1027 1034 self.request.matchdict['pull_request_id'])
1028 1035 pull_request_id = pull_request.pull_request_id
1029 1036 version = self.request.GET.get('version')
1030 1037
1031 1038 _render = self.request.get_partial_renderer(
1032 1039 'rhodecode:templates/base/sidebar.mako')
1033 1040 c = _render.get_call_context()
1034 1041 (pull_request_latest,
1035 1042 pull_request_at_ver,
1036 1043 pull_request_display_obj,
1037 1044 at_version) = PullRequestModel().get_pr_version(
1038 1045 pull_request_id, version=version)
1039 1046 versions = pull_request_display_obj.versions()
1040 1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1041 1048 c.versions = versions + [latest_ver]
1042 1049
1043 1050 c.at_version = at_version
1044 1051 c.at_version_num = (at_version
1045 1052 if at_version and at_version != PullRequest.LATEST_VER
1046 1053 else None)
1047 1054
1048 1055 c.unresolved_comments = CommentsModel() \
1049 1056 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1050 1057 c.resolved_comments = CommentsModel() \
1051 1058 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1052 1059
1053 1060 all_comments = c.unresolved_comments + c.resolved_comments
1054 1061 existing_ids = self.get_comment_ids(self.request.POST)
1055 1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1056 1063 todo_comments=True, existing_ids=existing_ids)
1057 1064
1058 1065 @LoginRequired()
1059 1066 @NotAnonymous()
1060 1067 @HasRepoPermissionAnyDecorator(
1061 1068 'repository.read', 'repository.write', 'repository.admin')
1062 1069 def pullrequest_drafts(self):
1063 1070 self.load_default_context()
1064 1071
1065 1072 pull_request = PullRequest.get_or_404(
1066 1073 self.request.matchdict['pull_request_id'])
1067 1074 pull_request_id = pull_request.pull_request_id
1068 1075 version = self.request.GET.get('version')
1069 1076
1070 1077 _render = self.request.get_partial_renderer(
1071 1078 'rhodecode:templates/base/sidebar.mako')
1072 1079 c = _render.get_call_context()
1073 1080
1074 1081 (pull_request_latest,
1075 1082 pull_request_at_ver,
1076 1083 pull_request_display_obj,
1077 1084 at_version) = PullRequestModel().get_pr_version(
1078 1085 pull_request_id, version=version)
1079 1086 versions = pull_request_display_obj.versions()
1080 1087 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1081 1088 c.versions = versions + [latest_ver]
1082 1089
1083 1090 c.at_version = at_version
1084 1091 c.at_version_num = (at_version
1085 1092 if at_version and at_version != PullRequest.LATEST_VER
1086 1093 else None)
1087 1094
1088 1095 c.draft_comments = CommentsModel() \
1089 1096 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1090 1097
1091 1098 all_comments = c.draft_comments
1092 1099
1093 1100 existing_ids = self.get_comment_ids(self.request.POST)
1094 1101 return _render('comments_table', all_comments, len(all_comments),
1095 1102 existing_ids=existing_ids, draft_comments=True)
1096 1103
1097 1104 @LoginRequired()
1098 1105 @NotAnonymous()
1099 1106 @HasRepoPermissionAnyDecorator(
1100 1107 'repository.read', 'repository.write', 'repository.admin')
1101 1108 @CSRFRequired()
1102 1109 def pull_request_create(self):
1103 1110 _ = self.request.translate
1104 1111 self.assure_not_empty_repo()
1105 1112 self.load_default_context()
1106 1113
1107 1114 controls = peppercorn.parse(self.request.POST.items())
1108 1115
1109 1116 try:
1110 1117 form = PullRequestForm(
1111 1118 self.request.translate, self.db_repo.repo_id)()
1112 1119 _form = form.to_python(controls)
1113 1120 except formencode.Invalid as errors:
1114 1121 if errors.error_dict.get('revisions'):
1115 1122 msg = 'Revisions: %s' % errors.error_dict['revisions']
1116 1123 elif errors.error_dict.get('pullrequest_title'):
1117 1124 msg = errors.error_dict.get('pullrequest_title')
1118 1125 else:
1119 1126 msg = _('Error creating pull request: {}').format(errors)
1120 1127 log.exception(msg)
1121 1128 h.flash(msg, 'error')
1122 1129
1123 1130 # would rather just go back to form ...
1124 1131 raise HTTPFound(
1125 1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1126 1133
1127 1134 source_repo = _form['source_repo']
1128 1135 source_ref = _form['source_ref']
1129 1136 target_repo = _form['target_repo']
1130 1137 target_ref = _form['target_ref']
1131 1138 commit_ids = _form['revisions'][::-1]
1132 1139 common_ancestor_id = _form['common_ancestor']
1133 1140
1134 1141 # find the ancestor for this pr
1135 1142 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1136 1143 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1137 1144
1138 1145 if not (source_db_repo or target_db_repo):
1139 1146 h.flash(_('source_repo or target repo not found'), category='error')
1140 1147 raise HTTPFound(
1141 1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1142 1149
1143 1150 # re-check permissions again here
1144 1151 # source_repo we must have read permissions
1145 1152
1146 1153 source_perm = HasRepoPermissionAny(
1147 1154 'repository.read', 'repository.write', 'repository.admin')(
1148 1155 source_db_repo.repo_name)
1149 1156 if not source_perm:
1150 1157 msg = _('Not Enough permissions to source repo `{}`.'.format(
1151 1158 source_db_repo.repo_name))
1152 1159 h.flash(msg, category='error')
1153 1160 # copy the args back to redirect
1154 1161 org_query = self.request.GET.mixed()
1155 1162 raise HTTPFound(
1156 1163 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1157 1164 _query=org_query))
1158 1165
1159 1166 # target repo we must have read permissions, and also later on
1160 1167 # we want to check branch permissions here
1161 1168 target_perm = HasRepoPermissionAny(
1162 1169 'repository.read', 'repository.write', 'repository.admin')(
1163 1170 target_db_repo.repo_name)
1164 1171 if not target_perm:
1165 1172 msg = _('Not Enough permissions to target repo `{}`.'.format(
1166 1173 target_db_repo.repo_name))
1167 1174 h.flash(msg, category='error')
1168 1175 # copy the args back to redirect
1169 1176 org_query = self.request.GET.mixed()
1170 1177 raise HTTPFound(
1171 1178 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1172 1179 _query=org_query))
1173 1180
1174 1181 source_scm = source_db_repo.scm_instance()
1175 1182 target_scm = target_db_repo.scm_instance()
1176 1183
1177 1184 source_ref_obj = unicode_to_reference(source_ref)
1178 1185 target_ref_obj = unicode_to_reference(target_ref)
1179 1186
1180 1187 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1181 1188 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1182 1189
1183 1190 ancestor = source_scm.get_common_ancestor(
1184 1191 source_commit.raw_id, target_commit.raw_id, target_scm)
1185 1192
1186 1193 # recalculate target ref based on ancestor
1187 1194 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1188 1195
1189 1196 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1190 1197 PullRequestModel().get_reviewer_functions()
1191 1198
1192 1199 # recalculate reviewers logic, to make sure we can validate this
1193 1200 reviewer_rules = get_default_reviewers_data(
1194 1201 self._rhodecode_db_user,
1195 1202 source_db_repo,
1196 1203 source_ref_obj,
1197 1204 target_db_repo,
1198 1205 target_ref_obj,
1199 1206 include_diff_info=False)
1200 1207
1201 1208 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1202 1209 observers = validate_observers(_form['observer_members'], reviewer_rules)
1203 1210
1204 1211 pullrequest_title = _form['pullrequest_title']
1205 1212 title_source_ref = source_ref_obj.name
1206 1213 if not pullrequest_title:
1207 1214 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1208 1215 source=source_repo,
1209 1216 source_ref=title_source_ref,
1210 1217 target=target_repo
1211 1218 )
1212 1219
1213 1220 description = _form['pullrequest_desc']
1214 1221 description_renderer = _form['description_renderer']
1215 1222
1216 1223 try:
1217 1224 pull_request = PullRequestModel().create(
1218 1225 created_by=self._rhodecode_user.user_id,
1219 1226 source_repo=source_repo,
1220 1227 source_ref=source_ref,
1221 1228 target_repo=target_repo,
1222 1229 target_ref=target_ref,
1223 1230 revisions=commit_ids,
1224 1231 common_ancestor_id=common_ancestor_id,
1225 1232 reviewers=reviewers,
1226 1233 observers=observers,
1227 1234 title=pullrequest_title,
1228 1235 description=description,
1229 1236 description_renderer=description_renderer,
1230 1237 reviewer_data=reviewer_rules,
1231 1238 auth_user=self._rhodecode_user
1232 1239 )
1233 1240 Session().commit()
1234 1241
1235 1242 h.flash(_('Successfully opened new pull request'),
1236 1243 category='success')
1237 1244 except Exception:
1238 1245 msg = _('Error occurred during creation of this pull request.')
1239 1246 log.exception(msg)
1240 1247 h.flash(msg, category='error')
1241 1248
1242 1249 # copy the args back to redirect
1243 1250 org_query = self.request.GET.mixed()
1244 1251 raise HTTPFound(
1245 1252 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1246 1253 _query=org_query))
1247 1254
1248 1255 raise HTTPFound(
1249 1256 h.route_path('pullrequest_show', repo_name=target_repo,
1250 1257 pull_request_id=pull_request.pull_request_id))
1251 1258
1252 1259 @LoginRequired()
1253 1260 @NotAnonymous()
1254 1261 @HasRepoPermissionAnyDecorator(
1255 1262 'repository.read', 'repository.write', 'repository.admin')
1256 1263 @CSRFRequired()
1257 1264 def pull_request_update(self):
1258 1265 pull_request = PullRequest.get_or_404(
1259 1266 self.request.matchdict['pull_request_id'])
1260 1267 _ = self.request.translate
1261 1268
1262 1269 c = self.load_default_context()
1263 1270 redirect_url = None
1264 1271
1265 1272 if pull_request.is_closed():
1266 1273 log.debug('update: forbidden because pull request is closed')
1267 1274 msg = _(u'Cannot update closed pull requests.')
1268 1275 h.flash(msg, category='error')
1269 1276 return {'response': True,
1270 1277 'redirect_url': redirect_url}
1271 1278
1272 1279 is_state_changing = pull_request.is_state_changing()
1273 1280 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1274 1281
1275 1282 # only owner or admin can update it
1276 1283 allowed_to_update = PullRequestModel().check_user_update(
1277 1284 pull_request, self._rhodecode_user)
1278 1285
1279 1286 if allowed_to_update:
1280 1287 controls = peppercorn.parse(self.request.POST.items())
1281 1288 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1282 1289
1283 1290 if 'review_members' in controls:
1284 1291 self._update_reviewers(
1285 1292 c,
1286 1293 pull_request, controls['review_members'],
1287 1294 pull_request.reviewer_data,
1288 1295 PullRequestReviewers.ROLE_REVIEWER)
1289 1296 elif 'observer_members' in controls:
1290 1297 self._update_reviewers(
1291 1298 c,
1292 1299 pull_request, controls['observer_members'],
1293 1300 pull_request.reviewer_data,
1294 1301 PullRequestReviewers.ROLE_OBSERVER)
1295 1302 elif str2bool(self.request.POST.get('update_commits', 'false')):
1296 1303 if is_state_changing:
1297 1304 log.debug('commits update: forbidden because pull request is in state %s',
1298 1305 pull_request.pull_request_state)
1299 1306 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1300 1307 u'Current state is: `{}`').format(
1301 1308 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1302 1309 h.flash(msg, category='error')
1303 1310 return {'response': True,
1304 1311 'redirect_url': redirect_url}
1305 1312
1306 1313 self._update_commits(c, pull_request)
1307 1314 if force_refresh:
1308 1315 redirect_url = h.route_path(
1309 1316 'pullrequest_show', repo_name=self.db_repo_name,
1310 1317 pull_request_id=pull_request.pull_request_id,
1311 1318 _query={"force_refresh": 1})
1312 1319 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1313 1320 self._edit_pull_request(pull_request)
1314 1321 else:
1315 1322 log.error('Unhandled update data.')
1316 1323 raise HTTPBadRequest()
1317 1324
1318 1325 return {'response': True,
1319 1326 'redirect_url': redirect_url}
1320 1327 raise HTTPForbidden()
1321 1328
1322 1329 def _edit_pull_request(self, pull_request):
1323 1330 """
1324 1331 Edit title and description
1325 1332 """
1326 1333 _ = self.request.translate
1327 1334
1328 1335 try:
1329 1336 PullRequestModel().edit(
1330 1337 pull_request,
1331 1338 self.request.POST.get('title'),
1332 1339 self.request.POST.get('description'),
1333 1340 self.request.POST.get('description_renderer'),
1334 1341 self._rhodecode_user)
1335 1342 except ValueError:
1336 1343 msg = _(u'Cannot update closed pull requests.')
1337 1344 h.flash(msg, category='error')
1338 1345 return
1339 1346 else:
1340 1347 Session().commit()
1341 1348
1342 1349 msg = _(u'Pull request title & description updated.')
1343 1350 h.flash(msg, category='success')
1344 1351 return
1345 1352
1346 1353 def _update_commits(self, c, pull_request):
1347 1354 _ = self.request.translate
1348 1355
1349 1356 with pull_request.set_state(PullRequest.STATE_UPDATING):
1350 1357 resp = PullRequestModel().update_commits(
1351 1358 pull_request, self._rhodecode_db_user)
1352 1359
1353 1360 if resp.executed:
1354 1361
1355 1362 if resp.target_changed and resp.source_changed:
1356 1363 changed = 'target and source repositories'
1357 1364 elif resp.target_changed and not resp.source_changed:
1358 1365 changed = 'target repository'
1359 1366 elif not resp.target_changed and resp.source_changed:
1360 1367 changed = 'source repository'
1361 1368 else:
1362 1369 changed = 'nothing'
1363 1370
1364 1371 msg = _(u'Pull request updated to "{source_commit_id}" with '
1365 1372 u'{count_added} added, {count_removed} removed commits. '
1366 1373 u'Source of changes: {change_source}.')
1367 1374 msg = msg.format(
1368 1375 source_commit_id=pull_request.source_ref_parts.commit_id,
1369 1376 count_added=len(resp.changes.added),
1370 1377 count_removed=len(resp.changes.removed),
1371 1378 change_source=changed)
1372 1379 h.flash(msg, category='success')
1373 1380 channelstream.pr_update_channelstream_push(
1374 1381 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1375 1382 else:
1376 1383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1377 1384 warning_reasons = [
1378 1385 UpdateFailureReason.NO_CHANGE,
1379 1386 UpdateFailureReason.WRONG_REF_TYPE,
1380 1387 ]
1381 1388 category = 'warning' if resp.reason in warning_reasons else 'error'
1382 1389 h.flash(msg, category=category)
1383 1390
1384 1391 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1385 1392 _ = self.request.translate
1386 1393
1387 1394 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1388 1395 PullRequestModel().get_reviewer_functions()
1389 1396
1390 1397 if role == PullRequestReviewers.ROLE_REVIEWER:
1391 1398 try:
1392 1399 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1393 1400 except ValueError as e:
1394 1401 log.error('Reviewers Validation: {}'.format(e))
1395 1402 h.flash(e, category='error')
1396 1403 return
1397 1404
1398 1405 old_calculated_status = pull_request.calculated_review_status()
1399 1406 PullRequestModel().update_reviewers(
1400 1407 pull_request, reviewers, self._rhodecode_db_user)
1401 1408
1402 1409 Session().commit()
1403 1410
1404 1411 msg = _('Pull request reviewers updated.')
1405 1412 h.flash(msg, category='success')
1406 1413 channelstream.pr_update_channelstream_push(
1407 1414 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1408 1415
1409 1416 # trigger status changed if change in reviewers changes the status
1410 1417 calculated_status = pull_request.calculated_review_status()
1411 1418 if old_calculated_status != calculated_status:
1412 1419 PullRequestModel().trigger_pull_request_hook(
1413 1420 pull_request, self._rhodecode_user, 'review_status_change',
1414 1421 data={'status': calculated_status})
1415 1422
1416 1423 elif role == PullRequestReviewers.ROLE_OBSERVER:
1417 1424 try:
1418 1425 observers = validate_observers(review_members, reviewer_rules)
1419 1426 except ValueError as e:
1420 1427 log.error('Observers Validation: {}'.format(e))
1421 1428 h.flash(e, category='error')
1422 1429 return
1423 1430
1424 1431 PullRequestModel().update_observers(
1425 1432 pull_request, observers, self._rhodecode_db_user)
1426 1433
1427 1434 Session().commit()
1428 1435 msg = _('Pull request observers updated.')
1429 1436 h.flash(msg, category='success')
1430 1437 channelstream.pr_update_channelstream_push(
1431 1438 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1432 1439
1433 1440 @LoginRequired()
1434 1441 @NotAnonymous()
1435 1442 @HasRepoPermissionAnyDecorator(
1436 1443 'repository.read', 'repository.write', 'repository.admin')
1437 1444 @CSRFRequired()
1438 1445 def pull_request_merge(self):
1439 1446 """
1440 1447 Merge will perform a server-side merge of the specified
1441 1448 pull request, if the pull request is approved and mergeable.
1442 1449 After successful merging, the pull request is automatically
1443 1450 closed, with a relevant comment.
1444 1451 """
1445 1452 pull_request = PullRequest.get_or_404(
1446 1453 self.request.matchdict['pull_request_id'])
1447 1454 _ = self.request.translate
1448 1455
1449 1456 if pull_request.is_state_changing():
1450 1457 log.debug('show: forbidden because pull request is in state %s',
1451 1458 pull_request.pull_request_state)
1452 1459 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1453 1460 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1454 1461 pull_request.pull_request_state)
1455 1462 h.flash(msg, category='error')
1456 1463 raise HTTPFound(
1457 1464 h.route_path('pullrequest_show',
1458 1465 repo_name=pull_request.target_repo.repo_name,
1459 1466 pull_request_id=pull_request.pull_request_id))
1460 1467
1461 1468 self.load_default_context()
1462 1469
1463 1470 with pull_request.set_state(PullRequest.STATE_UPDATING):
1464 1471 check = MergeCheck.validate(
1465 1472 pull_request, auth_user=self._rhodecode_user,
1466 1473 translator=self.request.translate)
1467 1474 merge_possible = not check.failed
1468 1475
1469 1476 for err_type, error_msg in check.errors:
1470 1477 h.flash(error_msg, category=err_type)
1471 1478
1472 1479 if merge_possible:
1473 1480 log.debug("Pre-conditions checked, trying to merge.")
1474 1481 extras = vcs_operation_context(
1475 1482 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1476 1483 username=self._rhodecode_db_user.username, action='push',
1477 1484 scm=pull_request.target_repo.repo_type)
1478 1485 with pull_request.set_state(PullRequest.STATE_UPDATING):
1479 1486 self._merge_pull_request(
1480 1487 pull_request, self._rhodecode_db_user, extras)
1481 1488 else:
1482 1489 log.debug("Pre-conditions failed, NOT merging.")
1483 1490
1484 1491 raise HTTPFound(
1485 1492 h.route_path('pullrequest_show',
1486 1493 repo_name=pull_request.target_repo.repo_name,
1487 1494 pull_request_id=pull_request.pull_request_id))
1488 1495
1489 1496 def _merge_pull_request(self, pull_request, user, extras):
1490 1497 _ = self.request.translate
1491 1498 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1492 1499
1493 1500 if merge_resp.executed:
1494 1501 log.debug("The merge was successful, closing the pull request.")
1495 1502 PullRequestModel().close_pull_request(
1496 1503 pull_request.pull_request_id, user)
1497 1504 Session().commit()
1498 1505 msg = _('Pull request was successfully merged and closed.')
1499 1506 h.flash(msg, category='success')
1500 1507 else:
1501 1508 log.debug(
1502 1509 "The merge was not successful. Merge response: %s", merge_resp)
1503 1510 msg = merge_resp.merge_status_message
1504 1511 h.flash(msg, category='error')
1505 1512
1506 1513 @LoginRequired()
1507 1514 @NotAnonymous()
1508 1515 @HasRepoPermissionAnyDecorator(
1509 1516 'repository.read', 'repository.write', 'repository.admin')
1510 1517 @CSRFRequired()
1511 1518 def pull_request_delete(self):
1512 1519 _ = self.request.translate
1513 1520
1514 1521 pull_request = PullRequest.get_or_404(
1515 1522 self.request.matchdict['pull_request_id'])
1516 1523 self.load_default_context()
1517 1524
1518 1525 pr_closed = pull_request.is_closed()
1519 1526 allowed_to_delete = PullRequestModel().check_user_delete(
1520 1527 pull_request, self._rhodecode_user) and not pr_closed
1521 1528
1522 1529 # only owner can delete it !
1523 1530 if allowed_to_delete:
1524 1531 PullRequestModel().delete(pull_request, self._rhodecode_user)
1525 1532 Session().commit()
1526 1533 h.flash(_('Successfully deleted pull request'),
1527 1534 category='success')
1528 1535 raise HTTPFound(h.route_path('pullrequest_show_all',
1529 1536 repo_name=self.db_repo_name))
1530 1537
1531 1538 log.warning('user %s tried to delete pull request without access',
1532 1539 self._rhodecode_user)
1533 1540 raise HTTPNotFound()
1534 1541
1535 1542 def _pull_request_comments_create(self, pull_request, comments):
1536 1543 _ = self.request.translate
1537 1544 data = {}
1538 1545 if not comments:
1539 1546 return
1540 1547 pull_request_id = pull_request.pull_request_id
1541 1548
1542 1549 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1543 1550
1544 1551 for entry in comments:
1545 1552 c = self.load_default_context()
1546 1553 comment_type = entry['comment_type']
1547 1554 text = entry['text']
1548 1555 status = entry['status']
1549 1556 is_draft = str2bool(entry['is_draft'])
1550 1557 resolves_comment_id = entry['resolves_comment_id']
1551 1558 close_pull_request = entry['close_pull_request']
1552 1559 f_path = entry['f_path']
1553 1560 line_no = entry['line']
1554 1561 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1555 1562
1556 1563 # the logic here should work like following, if we submit close
1557 1564 # pr comment, use `close_pull_request_with_comment` function
1558 1565 # else handle regular comment logic
1559 1566
1560 1567 if close_pull_request:
1561 1568 # only owner or admin or person with write permissions
1562 1569 allowed_to_close = PullRequestModel().check_user_update(
1563 1570 pull_request, self._rhodecode_user)
1564 1571 if not allowed_to_close:
1565 1572 log.debug('comment: forbidden because not allowed to close '
1566 1573 'pull request %s', pull_request_id)
1567 1574 raise HTTPForbidden()
1568 1575
1569 1576 # This also triggers `review_status_change`
1570 1577 comment, status = PullRequestModel().close_pull_request_with_comment(
1571 1578 pull_request, self._rhodecode_user, self.db_repo, message=text,
1572 1579 auth_user=self._rhodecode_user)
1573 1580 Session().flush()
1574 1581 is_inline = comment.is_inline
1575 1582
1576 1583 PullRequestModel().trigger_pull_request_hook(
1577 1584 pull_request, self._rhodecode_user, 'comment',
1578 1585 data={'comment': comment})
1579 1586
1580 1587 else:
1581 1588 # regular comment case, could be inline, or one with status.
1582 1589 # for that one we check also permissions
1583 1590 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1584 1591 allowed_to_change_status = PullRequestModel().check_user_change_status(
1585 1592 pull_request, self._rhodecode_user) and not is_draft
1586 1593
1587 1594 if status and allowed_to_change_status:
1588 1595 message = (_('Status change %(transition_icon)s %(status)s')
1589 1596 % {'transition_icon': '>',
1590 1597 'status': ChangesetStatus.get_status_lbl(status)})
1591 1598 text = text or message
1592 1599
1593 1600 comment = CommentsModel().create(
1594 1601 text=text,
1595 1602 repo=self.db_repo.repo_id,
1596 1603 user=self._rhodecode_user.user_id,
1597 1604 pull_request=pull_request,
1598 1605 f_path=f_path,
1599 1606 line_no=line_no,
1600 1607 status_change=(ChangesetStatus.get_status_lbl(status)
1601 1608 if status and allowed_to_change_status else None),
1602 1609 status_change_type=(status
1603 1610 if status and allowed_to_change_status else None),
1604 1611 comment_type=comment_type,
1605 1612 is_draft=is_draft,
1606 1613 resolves_comment_id=resolves_comment_id,
1607 1614 auth_user=self._rhodecode_user,
1608 1615 send_email=not is_draft, # skip notification for draft comments
1609 1616 )
1610 1617 is_inline = comment.is_inline
1611 1618
1612 1619 if allowed_to_change_status:
1613 1620 # calculate old status before we change it
1614 1621 old_calculated_status = pull_request.calculated_review_status()
1615 1622
1616 1623 # get status if set !
1617 1624 if status:
1618 1625 ChangesetStatusModel().set_status(
1619 1626 self.db_repo.repo_id,
1620 1627 status,
1621 1628 self._rhodecode_user.user_id,
1622 1629 comment,
1623 1630 pull_request=pull_request
1624 1631 )
1625 1632
1626 1633 Session().flush()
1627 1634 # this is somehow required to get access to some relationship
1628 1635 # loaded on comment
1629 1636 Session().refresh(comment)
1630 1637
1631 1638 # skip notifications for drafts
1632 1639 if not is_draft:
1633 1640 PullRequestModel().trigger_pull_request_hook(
1634 1641 pull_request, self._rhodecode_user, 'comment',
1635 1642 data={'comment': comment})
1636 1643
1637 1644 # we now calculate the status of pull request, and based on that
1638 1645 # calculation we set the commits status
1639 1646 calculated_status = pull_request.calculated_review_status()
1640 1647 if old_calculated_status != calculated_status:
1641 1648 PullRequestModel().trigger_pull_request_hook(
1642 1649 pull_request, self._rhodecode_user, 'review_status_change',
1643 1650 data={'status': calculated_status})
1644 1651
1645 1652 comment_id = comment.comment_id
1646 1653 data[comment_id] = {
1647 1654 'target_id': target_elem_id
1648 1655 }
1649 1656 Session().flush()
1650 1657
1651 1658 c.co = comment
1652 1659 c.at_version_num = None
1653 1660 c.is_new = True
1654 1661 rendered_comment = render(
1655 1662 'rhodecode:templates/changeset/changeset_comment_block.mako',
1656 1663 self._get_template_context(c), self.request)
1657 1664
1658 1665 data[comment_id].update(comment.get_dict())
1659 1666 data[comment_id].update({'rendered_text': rendered_comment})
1660 1667
1661 1668 Session().commit()
1662 1669
1663 1670 # skip channelstream for draft comments
1664 1671 if not all_drafts:
1665 1672 comment_broadcast_channel = channelstream.comment_channel(
1666 1673 self.db_repo_name, pull_request_obj=pull_request)
1667 1674
1668 1675 comment_data = data
1669 1676 posted_comment_type = 'inline' if is_inline else 'general'
1670 1677 if len(data) == 1:
1671 1678 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1672 1679 else:
1673 1680 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1674 1681
1675 1682 channelstream.comment_channelstream_push(
1676 1683 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1677 1684 comment_data=comment_data)
1678 1685
1679 1686 return data
1680 1687
1681 1688 @LoginRequired()
1682 1689 @NotAnonymous()
1683 1690 @HasRepoPermissionAnyDecorator(
1684 1691 'repository.read', 'repository.write', 'repository.admin')
1685 1692 @CSRFRequired()
1686 1693 def pull_request_comment_create(self):
1687 1694 _ = self.request.translate
1688 1695
1689 1696 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1690 1697
1691 1698 if pull_request.is_closed():
1692 1699 log.debug('comment: forbidden because pull request is closed')
1693 1700 raise HTTPForbidden()
1694 1701
1695 1702 allowed_to_comment = PullRequestModel().check_user_comment(
1696 1703 pull_request, self._rhodecode_user)
1697 1704 if not allowed_to_comment:
1698 1705 log.debug('comment: forbidden because pull request is from forbidden repo')
1699 1706 raise HTTPForbidden()
1700 1707
1701 1708 comment_data = {
1702 1709 'comment_type': self.request.POST.get('comment_type'),
1703 1710 'text': self.request.POST.get('text'),
1704 1711 'status': self.request.POST.get('changeset_status', None),
1705 1712 'is_draft': self.request.POST.get('draft'),
1706 1713 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1707 1714 'close_pull_request': self.request.POST.get('close_pull_request'),
1708 1715 'f_path': self.request.POST.get('f_path'),
1709 1716 'line': self.request.POST.get('line'),
1710 1717 }
1711 1718 data = self._pull_request_comments_create(pull_request, [comment_data])
1712 1719
1713 1720 return data
1714 1721
1715 1722 @LoginRequired()
1716 1723 @NotAnonymous()
1717 1724 @HasRepoPermissionAnyDecorator(
1718 1725 'repository.read', 'repository.write', 'repository.admin')
1719 1726 @CSRFRequired()
1720 1727 def pull_request_comment_delete(self):
1721 1728 pull_request = PullRequest.get_or_404(
1722 1729 self.request.matchdict['pull_request_id'])
1723 1730
1724 1731 comment = ChangesetComment.get_or_404(
1725 1732 self.request.matchdict['comment_id'])
1726 1733 comment_id = comment.comment_id
1727 1734
1728 1735 if comment.immutable:
1729 1736 # don't allow deleting comments that are immutable
1730 1737 raise HTTPForbidden()
1731 1738
1732 1739 if pull_request.is_closed():
1733 1740 log.debug('comment: forbidden because pull request is closed')
1734 1741 raise HTTPForbidden()
1735 1742
1736 1743 if not comment:
1737 1744 log.debug('Comment with id:%s not found, skipping', comment_id)
1738 1745 # comment already deleted in another call probably
1739 1746 return True
1740 1747
1741 1748 if comment.pull_request.is_closed():
1742 1749 # don't allow deleting comments on closed pull request
1743 1750 raise HTTPForbidden()
1744 1751
1745 1752 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1746 1753 super_admin = h.HasPermissionAny('hg.admin')()
1747 1754 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1748 1755 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1749 1756 comment_repo_admin = is_repo_admin and is_repo_comment
1750 1757
1751 1758 if comment.draft and not comment_owner:
1752 1759 # We never allow to delete draft comments for other than owners
1753 1760 raise HTTPNotFound()
1754 1761
1755 1762 if super_admin or comment_owner or comment_repo_admin:
1756 1763 old_calculated_status = comment.pull_request.calculated_review_status()
1757 1764 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1758 1765 Session().commit()
1759 1766 calculated_status = comment.pull_request.calculated_review_status()
1760 1767 if old_calculated_status != calculated_status:
1761 1768 PullRequestModel().trigger_pull_request_hook(
1762 1769 comment.pull_request, self._rhodecode_user, 'review_status_change',
1763 1770 data={'status': calculated_status})
1764 1771 return True
1765 1772 else:
1766 1773 log.warning('No permissions for user %s to delete comment_id: %s',
1767 1774 self._rhodecode_db_user, comment_id)
1768 1775 raise HTTPNotFound()
1769 1776
1770 1777 @LoginRequired()
1771 1778 @NotAnonymous()
1772 1779 @HasRepoPermissionAnyDecorator(
1773 1780 'repository.read', 'repository.write', 'repository.admin')
1774 1781 @CSRFRequired()
1775 1782 def pull_request_comment_edit(self):
1776 1783 self.load_default_context()
1777 1784
1778 1785 pull_request = PullRequest.get_or_404(
1779 1786 self.request.matchdict['pull_request_id']
1780 1787 )
1781 1788 comment = ChangesetComment.get_or_404(
1782 1789 self.request.matchdict['comment_id']
1783 1790 )
1784 1791 comment_id = comment.comment_id
1785 1792
1786 1793 if comment.immutable:
1787 1794 # don't allow deleting comments that are immutable
1788 1795 raise HTTPForbidden()
1789 1796
1790 1797 if pull_request.is_closed():
1791 1798 log.debug('comment: forbidden because pull request is closed')
1792 1799 raise HTTPForbidden()
1793 1800
1794 1801 if comment.pull_request.is_closed():
1795 1802 # don't allow deleting comments on closed pull request
1796 1803 raise HTTPForbidden()
1797 1804
1798 1805 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1799 1806 super_admin = h.HasPermissionAny('hg.admin')()
1800 1807 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1801 1808 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1802 1809 comment_repo_admin = is_repo_admin and is_repo_comment
1803 1810
1804 1811 if super_admin or comment_owner or comment_repo_admin:
1805 1812 text = self.request.POST.get('text')
1806 1813 version = self.request.POST.get('version')
1807 1814 if text == comment.text:
1808 1815 log.warning(
1809 1816 'Comment(PR): '
1810 1817 'Trying to create new version '
1811 1818 'with the same comment body {}'.format(
1812 1819 comment_id,
1813 1820 )
1814 1821 )
1815 1822 raise HTTPNotFound()
1816 1823
1817 1824 if version.isdigit():
1818 1825 version = int(version)
1819 1826 else:
1820 1827 log.warning(
1821 1828 'Comment(PR): Wrong version type {} {} '
1822 1829 'for comment {}'.format(
1823 1830 version,
1824 1831 type(version),
1825 1832 comment_id,
1826 1833 )
1827 1834 )
1828 1835 raise HTTPNotFound()
1829 1836
1830 1837 try:
1831 1838 comment_history = CommentsModel().edit(
1832 1839 comment_id=comment_id,
1833 1840 text=text,
1834 1841 auth_user=self._rhodecode_user,
1835 1842 version=version,
1836 1843 )
1837 1844 except CommentVersionMismatch:
1838 1845 raise HTTPConflict()
1839 1846
1840 1847 if not comment_history:
1841 1848 raise HTTPNotFound()
1842 1849
1843 1850 Session().commit()
1844 1851 if not comment.draft:
1845 1852 PullRequestModel().trigger_pull_request_hook(
1846 1853 pull_request, self._rhodecode_user, 'comment_edit',
1847 1854 data={'comment': comment})
1848 1855
1849 1856 return {
1850 1857 'comment_history_id': comment_history.comment_history_id,
1851 1858 'comment_id': comment.comment_id,
1852 1859 'comment_version': comment_history.version,
1853 1860 'comment_author_username': comment_history.author.username,
1854 1861 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1855 1862 'comment_created_on': h.age_component(comment_history.created_on,
1856 1863 time_is_local=True),
1857 1864 }
1858 1865 else:
1859 1866 log.warning('No permissions for user %s to edit comment_id: %s',
1860 1867 self._rhodecode_db_user, comment_id)
1861 1868 raise HTTPNotFound()
@@ -1,396 +1,403 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import itertools
23 23 import logging
24 24 import collections
25 25
26 26 from rhodecode.model import BaseModel
27 27 from rhodecode.model.db import (
28 28 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
29 29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 30 from rhodecode.lib.markup_renderer import (
31 31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class ChangesetStatusModel(BaseModel):
37 37
38 38 cls = ChangesetStatus
39 39
40 40 def __get_changeset_status(self, changeset_status):
41 41 return self._get_instance(ChangesetStatus, changeset_status)
42 42
43 43 def __get_pull_request(self, pull_request):
44 44 return self._get_instance(PullRequest, pull_request)
45 45
46 46 def _get_status_query(self, repo, revision, pull_request,
47 47 with_revisions=False):
48 48 repo = self._get_repo(repo)
49 49
50 50 q = ChangesetStatus.query()\
51 51 .filter(ChangesetStatus.repo == repo)
52 52 if not with_revisions:
53 53 q = q.filter(ChangesetStatus.version == 0)
54 54
55 55 if revision:
56 56 q = q.filter(ChangesetStatus.revision == revision)
57 57 elif pull_request:
58 58 pull_request = self.__get_pull_request(pull_request)
59 59 # TODO: johbo: Think about the impact of this join, there must
60 60 # be a reason why ChangesetStatus and ChanagesetComment is linked
61 61 # to the pull request. Might be that we want to do the same for
62 62 # the pull_request_version_id.
63 63 q = q.join(ChangesetComment).filter(
64 64 ChangesetStatus.pull_request == pull_request,
65 65 ChangesetComment.pull_request_version_id == None)
66 66 else:
67 67 raise Exception('Please specify revision or pull_request')
68 68 q = q.order_by(ChangesetStatus.version.asc())
69 69 return q
70 70
71 71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
72 72 trim_votes=True):
73 73 """
74 74 Calculate status based on given group members, and voting rule
75 75
76 76
77 77 group1 - 4 members, 3 required for approval
78 78 user1 - approved
79 79 user2 - reject
80 80 user3 - approved
81 81 user4 - rejected
82 82
83 83 final_state: rejected, reasons not at least 3 votes
84 84
85 85
86 86 group1 - 4 members, 2 required for approval
87 87 user1 - approved
88 88 user2 - reject
89 89 user3 - approved
90 90 user4 - rejected
91 91
92 92 final_state: approved, reasons got at least 2 approvals
93 93
94 94 group1 - 4 members, ALL required for approval
95 95 user1 - approved
96 96 user2 - reject
97 97 user3 - approved
98 98 user4 - rejected
99 99
100 100 final_state: rejected, reasons not all approvals
101 101
102 102
103 103 group1 - 4 members, ALL required for approval
104 104 user1 - approved
105 105 user2 - approved
106 106 user3 - approved
107 107 user4 - approved
108 108
109 109 final_state: approved, reason all approvals received
110 110
111 111 group1 - 4 members, 5 required for approval
112 112 (approval should be shorted to number of actual members)
113 113
114 114 user1 - approved
115 115 user2 - approved
116 116 user3 - approved
117 117 user4 - approved
118 118
119 119 final_state: approved, reason all approvals received
120 120
121 121 """
122 122 group_vote_data = {}
123 123 got_rule = False
124 124 members = collections.OrderedDict()
125 125 for review_obj, user, reasons, mandatory, statuses \
126 126 in group_statuses_by_reviewers:
127 127
128 128 if not got_rule:
129 129 group_vote_data = review_obj.rule_user_group_data()
130 130 got_rule = bool(group_vote_data)
131 131
132 132 members[user.user_id] = statuses
133 133
134 134 if not group_vote_data:
135 135 return []
136 136
137 137 required_votes = group_vote_data['vote_rule']
138 138 if required_votes == -1:
139 139 # -1 means all required, so we replace it with how many people
140 140 # are in the members
141 141 required_votes = len(members)
142 142
143 143 if trim_votes and required_votes > len(members):
144 144 # we require more votes than we have members in the group
145 145 # in this case we trim the required votes to the number of members
146 146 required_votes = len(members)
147 147
148 148 approvals = sum([
149 149 1 for statuses in members.values()
150 150 if statuses and
151 151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
152 152
153 153 calculated_votes = []
154 154 # we have all votes from users, now check if we have enough votes
155 155 # to fill other
156 156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
157 157 if approvals >= required_votes:
158 158 fill_in = ChangesetStatus.STATUS_APPROVED
159 159
160 160 for member, statuses in members.items():
161 161 if statuses:
162 162 ver, latest = statuses[0]
163 163 if fill_in == ChangesetStatus.STATUS_APPROVED:
164 164 calculated_votes.append(fill_in)
165 165 else:
166 166 calculated_votes.append(latest.status)
167 167 else:
168 168 calculated_votes.append(fill_in)
169 169
170 170 return calculated_votes
171 171
172 172 def calculate_status(self, statuses_by_reviewers):
173 173 """
174 174 Given the approval statuses from reviewers, calculates final approval
175 175 status. There can only be 3 results, all approved, all rejected. If
176 176 there is no consensus the PR is under review.
177 177
178 178 :param statuses_by_reviewers:
179 179 """
180 180
181 181 def group_rule(element):
182 182 review_obj = element[0]
183 183 rule_data = review_obj.rule_user_group_data()
184 184 if rule_data and rule_data['id']:
185 185 return rule_data['id']
186 186
187 187 voting_groups = itertools.groupby(
188 188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
189 189
190 190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
191 191
192 192 reviewers_number = len(statuses_by_reviewers)
193 193 votes = collections.defaultdict(int)
194 194 for group, group_statuses_by_reviewers in voting_by_groups:
195 195 if group:
196 196 # calculate how the "group" voted
197 197 for vote_status in self.calculate_group_vote(
198 198 group, group_statuses_by_reviewers):
199 199 votes[vote_status] += 1
200 200 else:
201 201
202 202 for review_obj, user, reasons, mandatory, statuses \
203 203 in group_statuses_by_reviewers:
204 204 # individual vote
205 205 if statuses:
206 206 ver, latest = statuses[0]
207 207 votes[latest.status] += 1
208 208
209 209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
210 210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
211 211
212 212 # TODO(marcink): with group voting, how does rejected work,
213 213 # do we ever get rejected state ?
214 214
215 215 if approved_votes_count and (approved_votes_count == reviewers_number):
216 216 return ChangesetStatus.STATUS_APPROVED
217 217
218 218 if rejected_votes_count and (rejected_votes_count == reviewers_number):
219 219 return ChangesetStatus.STATUS_REJECTED
220 220
221 221 return ChangesetStatus.STATUS_UNDER_REVIEW
222 222
223 223 def get_statuses(self, repo, revision=None, pull_request=None,
224 224 with_revisions=False):
225 225 q = self._get_status_query(repo, revision, pull_request,
226 226 with_revisions)
227 227 return q.all()
228 228
229 229 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
230 230 """
231 231 Returns latest status of changeset for given revision or for given
232 232 pull request. Statuses are versioned inside a table itself and
233 233 version == 0 is always the current one
234 234
235 235 :param repo:
236 236 :param revision: 40char hash or None
237 237 :param pull_request: pull_request reference
238 238 :param as_str: return status as string not object
239 239 """
240 240 q = self._get_status_query(repo, revision, pull_request)
241 241
242 242 # need to use first here since there can be multiple statuses
243 243 # returned from pull_request
244 244 status = q.first()
245 245 if as_str:
246 246 status = status.status if status else status
247 247 st = status or ChangesetStatus.DEFAULT
248 248 return str(st)
249 249 return status
250 250
251 251 def _render_auto_status_message(
252 252 self, status, commit_id=None, pull_request=None):
253 253 """
254 254 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
255 255 so it's always looking the same disregarding on which default
256 256 renderer system is using.
257 257
258 258 :param status: status text to change into
259 259 :param commit_id: the commit_id we change the status for
260 260 :param pull_request: the pull request we change the status for
261 261 """
262 262
263 263 new_status = ChangesetStatus.get_status_lbl(status)
264 264
265 265 params = {
266 266 'new_status_label': new_status,
267 267 'pull_request': pull_request,
268 268 'commit_id': commit_id,
269 269 }
270 270 renderer = RstTemplateRenderer()
271 271 return renderer.render('auto_status_change.mako', **params)
272 272
273 273 def set_status(self, repo, status, user, comment=None, revision=None,
274 274 pull_request=None, dont_allow_on_closed_pull_request=False):
275 275 """
276 276 Creates new status for changeset or updates the old ones bumping their
277 277 version, leaving the current status at
278 278
279 279 :param repo:
280 280 :param revision:
281 281 :param status:
282 282 :param user:
283 283 :param comment:
284 284 :param dont_allow_on_closed_pull_request: don't allow a status change
285 285 if last status was for pull request and it's closed. We shouldn't
286 286 mess around this manually
287 287 """
288 288 repo = self._get_repo(repo)
289 289
290 290 q = ChangesetStatus.query()
291 291
292 292 if revision:
293 293 q = q.filter(ChangesetStatus.repo == repo)
294 294 q = q.filter(ChangesetStatus.revision == revision)
295 295 elif pull_request:
296 296 pull_request = self.__get_pull_request(pull_request)
297 297 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
298 298 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
299 299 cur_statuses = q.all()
300 300
301 301 # if statuses exists and last is associated with a closed pull request
302 302 # we need to check if we can allow this status change
303 303 if (dont_allow_on_closed_pull_request and cur_statuses
304 304 and getattr(cur_statuses[0].pull_request, 'status', '')
305 305 == PullRequest.STATUS_CLOSED):
306 306 raise StatusChangeOnClosedPullRequestError(
307 307 'Changing status on closed pull request is not allowed'
308 308 )
309 309
310 310 # update all current statuses with older version
311 311 if cur_statuses:
312 312 for st in cur_statuses:
313 313 st.version += 1
314 314 Session().add(st)
315 315 Session().flush()
316 316
317 317 def _create_status(user, repo, status, comment, revision, pull_request):
318 318 new_status = ChangesetStatus()
319 319 new_status.author = self._get_user(user)
320 320 new_status.repo = self._get_repo(repo)
321 321 new_status.status = status
322 322 new_status.comment = comment
323 323 new_status.revision = revision
324 324 new_status.pull_request = pull_request
325 325 return new_status
326 326
327 327 if not comment:
328 328 from rhodecode.model.comment import CommentsModel
329 329 comment = CommentsModel().create(
330 330 text=self._render_auto_status_message(
331 331 status, commit_id=revision, pull_request=pull_request),
332 332 repo=repo,
333 333 user=user,
334 334 pull_request=pull_request,
335 335 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
336 336 )
337 337
338 338 if revision:
339 339 new_status = _create_status(
340 340 user=user, repo=repo, status=status, comment=comment,
341 341 revision=revision, pull_request=pull_request)
342 342 Session().add(new_status)
343 343 return new_status
344 344 elif pull_request:
345 345 # pull request can have more than one revision associated to it
346 346 # we need to create new version for each one
347 347 new_statuses = []
348 348 repo = pull_request.source_repo
349 349 for rev in pull_request.revisions:
350 350 new_status = _create_status(
351 351 user=user, repo=repo, status=status, comment=comment,
352 352 revision=rev, pull_request=pull_request)
353 353 new_statuses.append(new_status)
354 354 Session().add(new_status)
355 355 return new_statuses
356 356
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data):
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data, user=None):
358 358
359 359 commit_statuses_map = collections.defaultdict(list)
360 360 for st in commit_statuses:
361 361 commit_statuses_map[st.author.username] += [st]
362 362
363 363 reviewers = []
364 364
365 365 def version(commit_status):
366 366 return commit_status.version
367 367
368 368 for obj in reviewers_data:
369 369 if not obj.user:
370 370 continue
371 if user and obj.user.username != user.username:
372 # single user filter
373 continue
374
371 375 statuses = commit_statuses_map.get(obj.user.username, None)
372 376 if statuses:
373 377 status_groups = itertools.groupby(
374 378 sorted(statuses, key=version), version)
375 379 statuses = [(x, list(y)[0]) for x, y in status_groups]
376 380
377 381 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
378 382
383 if user:
384 return reviewers[0] if reviewers else reviewers
385 else:
379 386 return reviewers
380 387
381 def reviewers_statuses(self, pull_request):
388 def reviewers_statuses(self, pull_request, user=None):
382 389 _commit_statuses = self.get_statuses(
383 390 pull_request.source_repo,
384 391 pull_request=pull_request,
385 392 with_revisions=True)
386 393 reviewers = pull_request.get_pull_request_reviewers(
387 394 role=PullRequestReviewers.ROLE_REVIEWER)
388 return self.aggregate_votes_by_user(_commit_statuses, reviewers)
395 return self.aggregate_votes_by_user(_commit_statuses, reviewers, user=user)
389 396
390 397 def calculated_review_status(self, pull_request):
391 398 """
392 399 calculate pull request status based on reviewers, it should be a list
393 400 of two element lists.
394 401 """
395 402 reviewers = self.reviewers_statuses(pull_request)
396 403 return self.calculate_status(reviewers)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,2249 +1,2372 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26
27 27 import json
28 28 import logging
29 29 import os
30 30
31 31 import datetime
32 32 import urllib
33 33 import collections
34 34
35 35 from pyramid import compat
36 36 from pyramid.threadlocal import get_current_request
37 37
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39 from rhodecode.translation import lazy_ugettext
40 40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 41 from rhodecode.lib import audit_logger
42 42 from rhodecode.lib.compat import OrderedDict
43 43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 44 from rhodecode.lib.markup_renderer import (
45 45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 46 from rhodecode.lib.utils2 import (
47 47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 48 get_current_rhodecode_user)
49 49 from rhodecode.lib.vcs.backends.base import (
50 50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
51 51 TargetRefMissing, SourceRefMissing)
52 52 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 CommitDoesNotExistError, EmptyRepositoryError)
55 55 from rhodecode.model import BaseModel
56 56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 57 from rhodecode.model.comment import CommentsModel
58 58 from rhodecode.model.db import (
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 aliased, null, lazyload, and_, or_, func, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 61 from rhodecode.model.meta import Session
62 62 from rhodecode.model.notification import NotificationModel, \
63 63 EmailNotificationModel
64 64 from rhodecode.model.scm import ScmModel
65 65 from rhodecode.model.settings import VcsSettingsModel
66 66
67 67
68 68 log = logging.getLogger(__name__)
69 69
70 70
71 71 # Data structure to hold the response data when updating commits during a pull
72 72 # request update.
73 73 class UpdateResponse(object):
74 74
75 75 def __init__(self, executed, reason, new, old, common_ancestor_id,
76 76 commit_changes, source_changed, target_changed):
77 77
78 78 self.executed = executed
79 79 self.reason = reason
80 80 self.new = new
81 81 self.old = old
82 82 self.common_ancestor_id = common_ancestor_id
83 83 self.changes = commit_changes
84 84 self.source_changed = source_changed
85 85 self.target_changed = target_changed
86 86
87 87
88 88 def get_diff_info(
89 89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
90 90 get_commit_authors=True):
91 91 """
92 92 Calculates detailed diff information for usage in preview of creation of a pull-request.
93 93 This is also used for default reviewers logic
94 94 """
95 95
96 96 source_scm = source_repo.scm_instance()
97 97 target_scm = target_repo.scm_instance()
98 98
99 99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
100 100 if not ancestor_id:
101 101 raise ValueError(
102 102 'cannot calculate diff info without a common ancestor. '
103 103 'Make sure both repositories are related, and have a common forking commit.')
104 104
105 105 # case here is that want a simple diff without incoming commits,
106 106 # previewing what will be merged based only on commits in the source.
107 107 log.debug('Using ancestor %s as source_ref instead of %s',
108 108 ancestor_id, source_ref)
109 109
110 110 # source of changes now is the common ancestor
111 111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
112 112 # target commit becomes the source ref as it is the last commit
113 113 # for diff generation this logic gives proper diff
114 114 target_commit = source_scm.get_commit(commit_id=source_ref)
115 115
116 116 vcs_diff = \
117 117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
118 118 ignore_whitespace=False, context=3)
119 119
120 120 diff_processor = diffs.DiffProcessor(
121 121 vcs_diff, format='newdiff', diff_limit=None,
122 122 file_limit=None, show_full_diff=True)
123 123
124 124 _parsed = diff_processor.prepare()
125 125
126 126 all_files = []
127 127 all_files_changes = []
128 128 changed_lines = {}
129 129 stats = [0, 0]
130 130 for f in _parsed:
131 131 all_files.append(f['filename'])
132 132 all_files_changes.append({
133 133 'filename': f['filename'],
134 134 'stats': f['stats']
135 135 })
136 136 stats[0] += f['stats']['added']
137 137 stats[1] += f['stats']['deleted']
138 138
139 139 changed_lines[f['filename']] = []
140 140 if len(f['chunks']) < 2:
141 141 continue
142 142 # first line is "context" information
143 143 for chunks in f['chunks'][1:]:
144 144 for chunk in chunks['lines']:
145 145 if chunk['action'] not in ('del', 'mod'):
146 146 continue
147 147 changed_lines[f['filename']].append(chunk['old_lineno'])
148 148
149 149 commit_authors = []
150 150 user_counts = {}
151 151 email_counts = {}
152 152 author_counts = {}
153 153 _commit_cache = {}
154 154
155 155 commits = []
156 156 if get_commit_authors:
157 157 log.debug('Obtaining commit authors from set of commits')
158 158 _compare_data = target_scm.compare(
159 159 target_ref, source_ref, source_scm, merge=True,
160 160 pre_load=["author", "date", "message"]
161 161 )
162 162
163 163 for commit in _compare_data:
164 164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
165 165 # at this function which is later called via JSON serialization
166 166 serialized_commit = dict(
167 167 author=commit.author,
168 168 date=commit.date,
169 169 message=commit.message,
170 170 commit_id=commit.raw_id,
171 171 raw_id=commit.raw_id
172 172 )
173 173 commits.append(serialized_commit)
174 174 user = User.get_from_cs_author(serialized_commit['author'])
175 175 if user and user not in commit_authors:
176 176 commit_authors.append(user)
177 177
178 178 # lines
179 179 if get_authors:
180 180 log.debug('Calculating authors of changed files')
181 181 target_commit = source_repo.get_commit(ancestor_id)
182 182
183 183 for fname, lines in changed_lines.items():
184 184
185 185 try:
186 186 node = target_commit.get_node(fname, pre_load=["is_binary"])
187 187 except Exception:
188 188 log.exception("Failed to load node with path %s", fname)
189 189 continue
190 190
191 191 if not isinstance(node, FileNode):
192 192 continue
193 193
194 194 # NOTE(marcink): for binary node we don't do annotation, just use last author
195 195 if node.is_binary:
196 196 author = node.last_commit.author
197 197 email = node.last_commit.author_email
198 198
199 199 user = User.get_from_cs_author(author)
200 200 if user:
201 201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
202 202 author_counts[author] = author_counts.get(author, 0) + 1
203 203 email_counts[email] = email_counts.get(email, 0) + 1
204 204
205 205 continue
206 206
207 207 for annotation in node.annotate:
208 208 line_no, commit_id, get_commit_func, line_text = annotation
209 209 if line_no in lines:
210 210 if commit_id not in _commit_cache:
211 211 _commit_cache[commit_id] = get_commit_func()
212 212 commit = _commit_cache[commit_id]
213 213 author = commit.author
214 214 email = commit.author_email
215 215 user = User.get_from_cs_author(author)
216 216 if user:
217 217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
218 218 author_counts[author] = author_counts.get(author, 0) + 1
219 219 email_counts[email] = email_counts.get(email, 0) + 1
220 220
221 221 log.debug('Default reviewers processing finished')
222 222
223 223 return {
224 224 'commits': commits,
225 225 'files': all_files_changes,
226 226 'stats': stats,
227 227 'ancestor': ancestor_id,
228 228 # original authors of modified files
229 229 'original_authors': {
230 230 'users': user_counts,
231 231 'authors': author_counts,
232 232 'emails': email_counts,
233 233 },
234 234 'commit_authors': commit_authors
235 235 }
236 236
237 237
238 238 class PullRequestModel(BaseModel):
239 239
240 240 cls = PullRequest
241 241
242 242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
243 243
244 244 UPDATE_STATUS_MESSAGES = {
245 245 UpdateFailureReason.NONE: lazy_ugettext(
246 246 'Pull request update successful.'),
247 247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
248 248 'Pull request update failed because of an unknown error.'),
249 249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
250 250 'No update needed because the source and target have not changed.'),
251 251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
252 252 'Pull request cannot be updated because the reference type is '
253 253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
254 254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
255 255 'This pull request cannot be updated because the target '
256 256 'reference is missing.'),
257 257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
258 258 'This pull request cannot be updated because the source '
259 259 'reference is missing.'),
260 260 }
261 261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
262 262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
263 263
264 264 def __get_pull_request(self, pull_request):
265 265 return self._get_instance((
266 266 PullRequest, PullRequestVersion), pull_request)
267 267
268 268 def _check_perms(self, perms, pull_request, user, api=False):
269 269 if not api:
270 270 return h.HasRepoPermissionAny(*perms)(
271 271 user=user, repo_name=pull_request.target_repo.repo_name)
272 272 else:
273 273 return h.HasRepoPermissionAnyApi(*perms)(
274 274 user=user, repo_name=pull_request.target_repo.repo_name)
275 275
276 276 def check_user_read(self, pull_request, user, api=False):
277 277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
278 278 return self._check_perms(_perms, pull_request, user, api)
279 279
280 280 def check_user_merge(self, pull_request, user, api=False):
281 281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
282 282 return self._check_perms(_perms, pull_request, user, api)
283 283
284 284 def check_user_update(self, pull_request, user, api=False):
285 285 owner = user.user_id == pull_request.user_id
286 286 return self.check_user_merge(pull_request, user, api) or owner
287 287
288 288 def check_user_delete(self, pull_request, user):
289 289 owner = user.user_id == pull_request.user_id
290 290 _perms = ('repository.admin',)
291 291 return self._check_perms(_perms, pull_request, user) or owner
292 292
293 293 def is_user_reviewer(self, pull_request, user):
294 294 return user.user_id in [
295 295 x.user_id for x in
296 296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
297 297 if x.user
298 298 ]
299 299
300 300 def check_user_change_status(self, pull_request, user, api=False):
301 301 return self.check_user_update(pull_request, user, api) \
302 302 or self.is_user_reviewer(pull_request, user)
303 303
304 304 def check_user_comment(self, pull_request, user):
305 305 owner = user.user_id == pull_request.user_id
306 306 return self.check_user_read(pull_request, user) or owner
307 307
308 308 def get(self, pull_request):
309 309 return self.__get_pull_request(pull_request)
310 310
311 311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
312 312 statuses=None, opened_by=None, order_by=None,
313 313 order_dir='desc', only_created=False):
314 314 repo = None
315 315 if repo_name:
316 316 repo = self._get_repo(repo_name)
317 317
318 318 q = PullRequest.query()
319 319
320 320 if search_q:
321 321 like_expression = u'%{}%'.format(safe_unicode(search_q))
322 q = q.join(User)
322 q = q.join(User, User.user_id == PullRequest.user_id)
323 323 q = q.filter(or_(
324 324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
325 325 User.username.ilike(like_expression),
326 326 PullRequest.title.ilike(like_expression),
327 327 PullRequest.description.ilike(like_expression),
328 328 ))
329 329
330 330 # source or target
331 331 if repo and source:
332 332 q = q.filter(PullRequest.source_repo == repo)
333 333 elif repo:
334 334 q = q.filter(PullRequest.target_repo == repo)
335 335
336 336 # closed,opened
337 337 if statuses:
338 338 q = q.filter(PullRequest.status.in_(statuses))
339 339
340 340 # opened by filter
341 341 if opened_by:
342 342 q = q.filter(PullRequest.user_id.in_(opened_by))
343 343
344 344 # only get those that are in "created" state
345 345 if only_created:
346 346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
347 347
348 348 if order_by:
349 349 order_map = {
350 350 'name_raw': PullRequest.pull_request_id,
351 351 'id': PullRequest.pull_request_id,
352 352 'title': PullRequest.title,
353 353 'updated_on_raw': PullRequest.updated_on,
354 354 'target_repo': PullRequest.target_repo_id
355 355 }
356 356 if order_dir == 'asc':
357 357 q = q.order_by(order_map[order_by].asc())
358 358 else:
359 359 q = q.order_by(order_map[order_by].desc())
360 360
361 361 return q
362 362
363 363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
364 364 opened_by=None):
365 365 """
366 366 Count the number of pull requests for a specific repository.
367 367
368 368 :param repo_name: target or source repo
369 369 :param search_q: filter by text
370 370 :param source: boolean flag to specify if repo_name refers to source
371 371 :param statuses: list of pull request statuses
372 372 :param opened_by: author user of the pull request
373 373 :returns: int number of pull requests
374 374 """
375 375 q = self._prepare_get_all_query(
376 376 repo_name, search_q=search_q, source=source, statuses=statuses,
377 377 opened_by=opened_by)
378 378
379 379 return q.count()
380 380
381 381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
382 382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
383 383 """
384 384 Get all pull requests for a specific repository.
385 385
386 386 :param repo_name: target or source repo
387 387 :param search_q: filter by text
388 388 :param source: boolean flag to specify if repo_name refers to source
389 389 :param statuses: list of pull request statuses
390 390 :param opened_by: author user of the pull request
391 391 :param offset: pagination offset
392 392 :param length: length of returned list
393 393 :param order_by: order of the returned list
394 394 :param order_dir: 'asc' or 'desc' ordering direction
395 395 :returns: list of pull requests
396 396 """
397 397 q = self._prepare_get_all_query(
398 398 repo_name, search_q=search_q, source=source, statuses=statuses,
399 399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
400 400
401 401 if length:
402 402 pull_requests = q.limit(length).offset(offset).all()
403 403 else:
404 404 pull_requests = q.all()
405 405
406 406 return pull_requests
407 407
408 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
409 opened_by=None):
408 def count_awaiting_review(self, repo_name, search_q=None, statuses=None):
410 409 """
411 410 Count the number of pull requests for a specific repository that are
412 411 awaiting review.
413 412
414 413 :param repo_name: target or source repo
415 414 :param search_q: filter by text
416 :param source: boolean flag to specify if repo_name refers to source
417 415 :param statuses: list of pull request statuses
418 :param opened_by: author user of the pull request
419 416 :returns: int number of pull requests
420 417 """
421 418 pull_requests = self.get_awaiting_review(
422 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
419 repo_name, search_q=search_q, statuses=statuses)
423 420
424 421 return len(pull_requests)
425 422
426 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
427 opened_by=None, offset=0, length=None,
428 order_by=None, order_dir='desc'):
423 def get_awaiting_review(self, repo_name, search_q=None, statuses=None,
424 offset=0, length=None, order_by=None, order_dir='desc'):
429 425 """
430 426 Get all pull requests for a specific repository that are awaiting
431 427 review.
432 428
433 429 :param repo_name: target or source repo
434 430 :param search_q: filter by text
435 :param source: boolean flag to specify if repo_name refers to source
436 431 :param statuses: list of pull request statuses
437 :param opened_by: author user of the pull request
438 432 :param offset: pagination offset
439 433 :param length: length of returned list
440 434 :param order_by: order of the returned list
441 435 :param order_dir: 'asc' or 'desc' ordering direction
442 436 :returns: list of pull requests
443 437 """
444 438 pull_requests = self.get_all(
445 repo_name, search_q=search_q, source=source, statuses=statuses,
446 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
439 repo_name, search_q=search_q, statuses=statuses,
440 order_by=order_by, order_dir=order_dir)
447 441
448 442 _filtered_pull_requests = []
449 443 for pr in pull_requests:
450 444 status = pr.calculated_review_status()
451 445 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
452 446 ChangesetStatus.STATUS_UNDER_REVIEW]:
453 447 _filtered_pull_requests.append(pr)
454 448 if length:
455 449 return _filtered_pull_requests[offset:offset+length]
456 450 else:
457 451 return _filtered_pull_requests
458 452
459 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
460 opened_by=None, user_id=None):
453 def _prepare_awaiting_my_review_review_query(
454 self, repo_name, user_id, search_q=None, statuses=None,
455 order_by=None, order_dir='desc'):
456
457 for_review_statuses = [
458 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
459 ]
460
461 pull_request_alias = aliased(PullRequest)
462 status_alias = aliased(ChangesetStatus)
463 reviewers_alias = aliased(PullRequestReviewers)
464 repo_alias = aliased(Repository)
465
466 last_ver_subq = Session()\
467 .query(func.min(ChangesetStatus.version)) \
468 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
469 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
470 .subquery()
471
472 q = Session().query(pull_request_alias) \
473 .options(lazyload(pull_request_alias.author)) \
474 .join(reviewers_alias,
475 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
476 .join(repo_alias,
477 repo_alias.repo_id == pull_request_alias.target_repo_id) \
478 .outerjoin(status_alias,
479 and_(status_alias.user_id == reviewers_alias.user_id,
480 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
481 .filter(or_(status_alias.version == null(),
482 status_alias.version == last_ver_subq)) \
483 .filter(reviewers_alias.user_id == user_id) \
484 .filter(repo_alias.repo_name == repo_name) \
485 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
486 .group_by(pull_request_alias)
487
488 # closed,opened
489 if statuses:
490 q = q.filter(pull_request_alias.status.in_(statuses))
491
492 if search_q:
493 like_expression = u'%{}%'.format(safe_unicode(search_q))
494 q = q.join(User, User.user_id == pull_request_alias.user_id)
495 q = q.filter(or_(
496 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
497 User.username.ilike(like_expression),
498 pull_request_alias.title.ilike(like_expression),
499 pull_request_alias.description.ilike(like_expression),
500 ))
501
502 if order_by:
503 order_map = {
504 'name_raw': pull_request_alias.pull_request_id,
505 'title': pull_request_alias.title,
506 'updated_on_raw': pull_request_alias.updated_on,
507 'target_repo': pull_request_alias.target_repo_id
508 }
509 if order_dir == 'asc':
510 q = q.order_by(order_map[order_by].asc())
511 else:
512 q = q.order_by(order_map[order_by].desc())
513
514 return q
515
516 def count_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None):
461 517 """
462 518 Count the number of pull requests for a specific repository that are
463 519 awaiting review from a specific user.
464 520
465 521 :param repo_name: target or source repo
522 :param user_id: reviewer user of the pull request
466 523 :param search_q: filter by text
467 :param source: boolean flag to specify if repo_name refers to source
468 524 :param statuses: list of pull request statuses
469 :param opened_by: author user of the pull request
470 :param user_id: reviewer user of the pull request
471 525 :returns: int number of pull requests
472 526 """
473 pull_requests = self.get_awaiting_my_review(
474 repo_name, search_q=search_q, source=source, statuses=statuses,
475 opened_by=opened_by, user_id=user_id)
527 q = self._prepare_awaiting_my_review_review_query(
528 repo_name, user_id, search_q=search_q, statuses=statuses)
529 return q.count()
476 530
477 return len(pull_requests)
478
479 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
480 opened_by=None, user_id=None, offset=0,
481 length=None, order_by=None, order_dir='desc'):
531 def get_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None,
532 offset=0, length=None, order_by=None, order_dir='desc'):
482 533 """
483 534 Get all pull requests for a specific repository that are awaiting
484 535 review from a specific user.
485 536
486 537 :param repo_name: target or source repo
538 :param user_id: reviewer user of the pull request
487 539 :param search_q: filter by text
488 :param source: boolean flag to specify if repo_name refers to source
489 540 :param statuses: list of pull request statuses
490 :param opened_by: author user of the pull request
491 :param user_id: reviewer user of the pull request
492 541 :param offset: pagination offset
493 542 :param length: length of returned list
494 543 :param order_by: order of the returned list
495 544 :param order_dir: 'asc' or 'desc' ordering direction
496 545 :returns: list of pull requests
497 546 """
498 pull_requests = self.get_all(
499 repo_name, search_q=search_q, source=source, statuses=statuses,
500 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
501 547
502 _my = PullRequestModel().get_not_reviewed(user_id)
503 my_participation = []
504 for pr in pull_requests:
505 if pr in _my:
506 my_participation.append(pr)
507 _filtered_pull_requests = my_participation
548 q = self._prepare_awaiting_my_review_review_query(
549 repo_name, user_id, search_q=search_q, statuses=statuses,
550 order_by=order_by, order_dir=order_dir)
551
508 552 if length:
509 return _filtered_pull_requests[offset:offset+length]
553 pull_requests = q.limit(length).offset(offset).all()
510 554 else:
511 return _filtered_pull_requests
555 pull_requests = q.all()
556
557 return pull_requests
512 558
513 def get_not_reviewed(self, user_id):
514 return [
515 x.pull_request for x in PullRequestReviewers.query().filter(
516 PullRequestReviewers.user_id == user_id).all()
517 ]
518
519 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
559 def _prepare_im_participating_query(self, user_id=None, statuses=None, query='',
520 560 order_by=None, order_dir='desc'):
561 """
562 return a query of pull-requests user is an creator, or he's added as a reviewer
563 """
521 564 q = PullRequest.query()
522 565 if user_id:
523 566 reviewers_subquery = Session().query(
524 567 PullRequestReviewers.pull_request_id).filter(
525 568 PullRequestReviewers.user_id == user_id).subquery()
526 569 user_filter = or_(
527 570 PullRequest.user_id == user_id,
528 571 PullRequest.pull_request_id.in_(reviewers_subquery)
529 572 )
530 573 q = PullRequest.query().filter(user_filter)
531 574
532 575 # closed,opened
533 576 if statuses:
534 577 q = q.filter(PullRequest.status.in_(statuses))
535 578
536 579 if query:
537 580 like_expression = u'%{}%'.format(safe_unicode(query))
538 q = q.join(User)
581 q = q.join(User, User.user_id == PullRequest.user_id)
539 582 q = q.filter(or_(
540 583 cast(PullRequest.pull_request_id, String).ilike(like_expression),
541 584 User.username.ilike(like_expression),
542 585 PullRequest.title.ilike(like_expression),
543 586 PullRequest.description.ilike(like_expression),
544 587 ))
545 588 if order_by:
546 589 order_map = {
547 590 'name_raw': PullRequest.pull_request_id,
548 591 'title': PullRequest.title,
549 592 'updated_on_raw': PullRequest.updated_on,
550 593 'target_repo': PullRequest.target_repo_id
551 594 }
552 595 if order_dir == 'asc':
553 596 q = q.order_by(order_map[order_by].asc())
554 597 else:
555 598 q = q.order_by(order_map[order_by].desc())
556 599
557 600 return q
558 601
559 602 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
560 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
603 q = self._prepare_im_participating_query(user_id, statuses=statuses, query=query)
561 604 return q.count()
562 605
563 606 def get_im_participating_in(
564 607 self, user_id=None, statuses=None, query='', offset=0,
565 608 length=None, order_by=None, order_dir='desc'):
566 609 """
567 Get all Pull requests that i'm participating in, or i have opened
610 Get all Pull requests that i'm participating in as a reviewer, or i have opened
568 611 """
569 612
570 q = self._prepare_participating_query(
613 q = self._prepare_im_participating_query(
614 user_id, statuses=statuses, query=query, order_by=order_by,
615 order_dir=order_dir)
616
617 if length:
618 pull_requests = q.limit(length).offset(offset).all()
619 else:
620 pull_requests = q.all()
621
622 return pull_requests
623
624 def _prepare_participating_in_for_review_query(
625 self, user_id, statuses=None, query='', order_by=None, order_dir='desc'):
626
627 for_review_statuses = [
628 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
629 ]
630
631 pull_request_alias = aliased(PullRequest)
632 status_alias = aliased(ChangesetStatus)
633 reviewers_alias = aliased(PullRequestReviewers)
634
635 last_ver_subq = Session()\
636 .query(func.min(ChangesetStatus.version)) \
637 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
638 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
639 .subquery()
640
641 q = Session().query(pull_request_alias) \
642 .options(lazyload(pull_request_alias.author)) \
643 .join(reviewers_alias,
644 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
645 .outerjoin(status_alias,
646 and_(status_alias.user_id == reviewers_alias.user_id,
647 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
648 .filter(or_(status_alias.version == null(),
649 status_alias.version == last_ver_subq)) \
650 .filter(reviewers_alias.user_id == user_id) \
651 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
652 .group_by(pull_request_alias)
653
654 # closed,opened
655 if statuses:
656 q = q.filter(pull_request_alias.status.in_(statuses))
657
658 if query:
659 like_expression = u'%{}%'.format(safe_unicode(query))
660 q = q.join(User, User.user_id == pull_request_alias.user_id)
661 q = q.filter(or_(
662 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
663 User.username.ilike(like_expression),
664 pull_request_alias.title.ilike(like_expression),
665 pull_request_alias.description.ilike(like_expression),
666 ))
667
668 if order_by:
669 order_map = {
670 'name_raw': pull_request_alias.pull_request_id,
671 'title': pull_request_alias.title,
672 'updated_on_raw': pull_request_alias.updated_on,
673 'target_repo': pull_request_alias.target_repo_id
674 }
675 if order_dir == 'asc':
676 q = q.order_by(order_map[order_by].asc())
677 else:
678 q = q.order_by(order_map[order_by].desc())
679
680 return q
681
682 def count_im_participating_in_for_review(self, user_id, statuses=None, query=''):
683 q = self._prepare_participating_in_for_review_query(user_id, statuses=statuses, query=query)
684 return q.count()
685
686 def get_im_participating_in_for_review(
687 self, user_id, statuses=None, query='', offset=0,
688 length=None, order_by=None, order_dir='desc'):
689 """
690 Get all Pull requests that needs user approval or rejection
691 """
692
693 q = self._prepare_participating_in_for_review_query(
571 694 user_id, statuses=statuses, query=query, order_by=order_by,
572 695 order_dir=order_dir)
573 696
574 697 if length:
575 698 pull_requests = q.limit(length).offset(offset).all()
576 699 else:
577 700 pull_requests = q.all()
578 701
579 702 return pull_requests
580 703
581 704 def get_versions(self, pull_request):
582 705 """
583 706 returns version of pull request sorted by ID descending
584 707 """
585 708 return PullRequestVersion.query()\
586 709 .filter(PullRequestVersion.pull_request == pull_request)\
587 710 .order_by(PullRequestVersion.pull_request_version_id.asc())\
588 711 .all()
589 712
590 713 def get_pr_version(self, pull_request_id, version=None):
591 714 at_version = None
592 715
593 716 if version and version == 'latest':
594 717 pull_request_ver = PullRequest.get(pull_request_id)
595 718 pull_request_obj = pull_request_ver
596 719 _org_pull_request_obj = pull_request_obj
597 720 at_version = 'latest'
598 721 elif version:
599 722 pull_request_ver = PullRequestVersion.get_or_404(version)
600 723 pull_request_obj = pull_request_ver
601 724 _org_pull_request_obj = pull_request_ver.pull_request
602 725 at_version = pull_request_ver.pull_request_version_id
603 726 else:
604 727 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
605 728 pull_request_id)
606 729
607 730 pull_request_display_obj = PullRequest.get_pr_display_object(
608 731 pull_request_obj, _org_pull_request_obj)
609 732
610 733 return _org_pull_request_obj, pull_request_obj, \
611 734 pull_request_display_obj, at_version
612 735
613 736 def pr_commits_versions(self, versions):
614 737 """
615 738 Maps the pull-request commits into all known PR versions. This way we can obtain
616 739 each pr version the commit was introduced in.
617 740 """
618 741 commit_versions = collections.defaultdict(list)
619 742 num_versions = [x.pull_request_version_id for x in versions]
620 743 for ver in versions:
621 744 for commit_id in ver.revisions:
622 745 ver_idx = ChangesetComment.get_index_from_version(
623 746 ver.pull_request_version_id, num_versions=num_versions)
624 747 commit_versions[commit_id].append(ver_idx)
625 748 return commit_versions
626 749
627 750 def create(self, created_by, source_repo, source_ref, target_repo,
628 751 target_ref, revisions, reviewers, observers, title, description=None,
629 752 common_ancestor_id=None,
630 753 description_renderer=None,
631 754 reviewer_data=None, translator=None, auth_user=None):
632 755 translator = translator or get_current_request().translate
633 756
634 757 created_by_user = self._get_user(created_by)
635 758 auth_user = auth_user or created_by_user.AuthUser()
636 759 source_repo = self._get_repo(source_repo)
637 760 target_repo = self._get_repo(target_repo)
638 761
639 762 pull_request = PullRequest()
640 763 pull_request.source_repo = source_repo
641 764 pull_request.source_ref = source_ref
642 765 pull_request.target_repo = target_repo
643 766 pull_request.target_ref = target_ref
644 767 pull_request.revisions = revisions
645 768 pull_request.title = title
646 769 pull_request.description = description
647 770 pull_request.description_renderer = description_renderer
648 771 pull_request.author = created_by_user
649 772 pull_request.reviewer_data = reviewer_data
650 773 pull_request.pull_request_state = pull_request.STATE_CREATING
651 774 pull_request.common_ancestor_id = common_ancestor_id
652 775
653 776 Session().add(pull_request)
654 777 Session().flush()
655 778
656 779 reviewer_ids = set()
657 780 # members / reviewers
658 781 for reviewer_object in reviewers:
659 782 user_id, reasons, mandatory, role, rules = reviewer_object
660 783 user = self._get_user(user_id)
661 784
662 785 # skip duplicates
663 786 if user.user_id in reviewer_ids:
664 787 continue
665 788
666 789 reviewer_ids.add(user.user_id)
667 790
668 791 reviewer = PullRequestReviewers()
669 792 reviewer.user = user
670 793 reviewer.pull_request = pull_request
671 794 reviewer.reasons = reasons
672 795 reviewer.mandatory = mandatory
673 796 reviewer.role = role
674 797
675 798 # NOTE(marcink): pick only first rule for now
676 799 rule_id = list(rules)[0] if rules else None
677 800 rule = RepoReviewRule.get(rule_id) if rule_id else None
678 801 if rule:
679 802 review_group = rule.user_group_vote_rule(user_id)
680 803 # we check if this particular reviewer is member of a voting group
681 804 if review_group:
682 805 # NOTE(marcink):
683 806 # can be that user is member of more but we pick the first same,
684 807 # same as default reviewers algo
685 808 review_group = review_group[0]
686 809
687 810 rule_data = {
688 811 'rule_name':
689 812 rule.review_rule_name,
690 813 'rule_user_group_entry_id':
691 814 review_group.repo_review_rule_users_group_id,
692 815 'rule_user_group_name':
693 816 review_group.users_group.users_group_name,
694 817 'rule_user_group_members':
695 818 [x.user.username for x in review_group.users_group.members],
696 819 'rule_user_group_members_id':
697 820 [x.user.user_id for x in review_group.users_group.members],
698 821 }
699 822 # e.g {'vote_rule': -1, 'mandatory': True}
700 823 rule_data.update(review_group.rule_data())
701 824
702 825 reviewer.rule_data = rule_data
703 826
704 827 Session().add(reviewer)
705 828 Session().flush()
706 829
707 830 for observer_object in observers:
708 831 user_id, reasons, mandatory, role, rules = observer_object
709 832 user = self._get_user(user_id)
710 833
711 834 # skip duplicates from reviewers
712 835 if user.user_id in reviewer_ids:
713 836 continue
714 837
715 838 #reviewer_ids.add(user.user_id)
716 839
717 840 observer = PullRequestReviewers()
718 841 observer.user = user
719 842 observer.pull_request = pull_request
720 843 observer.reasons = reasons
721 844 observer.mandatory = mandatory
722 845 observer.role = role
723 846
724 847 # NOTE(marcink): pick only first rule for now
725 848 rule_id = list(rules)[0] if rules else None
726 849 rule = RepoReviewRule.get(rule_id) if rule_id else None
727 850 if rule:
728 851 # TODO(marcink): do we need this for observers ??
729 852 pass
730 853
731 854 Session().add(observer)
732 855 Session().flush()
733 856
734 857 # Set approval status to "Under Review" for all commits which are
735 858 # part of this pull request.
736 859 ChangesetStatusModel().set_status(
737 860 repo=target_repo,
738 861 status=ChangesetStatus.STATUS_UNDER_REVIEW,
739 862 user=created_by_user,
740 863 pull_request=pull_request
741 864 )
742 865 # we commit early at this point. This has to do with a fact
743 866 # that before queries do some row-locking. And because of that
744 867 # we need to commit and finish transaction before below validate call
745 868 # that for large repos could be long resulting in long row locks
746 869 Session().commit()
747 870
748 871 # prepare workspace, and run initial merge simulation. Set state during that
749 872 # operation
750 873 pull_request = PullRequest.get(pull_request.pull_request_id)
751 874
752 875 # set as merging, for merge simulation, and if finished to created so we mark
753 876 # simulation is working fine
754 877 with pull_request.set_state(PullRequest.STATE_MERGING,
755 878 final_state=PullRequest.STATE_CREATED) as state_obj:
756 879 MergeCheck.validate(
757 880 pull_request, auth_user=auth_user, translator=translator)
758 881
759 882 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
760 883 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
761 884
762 885 creation_data = pull_request.get_api_data(with_merge_state=False)
763 886 self._log_audit_action(
764 887 'repo.pull_request.create', {'data': creation_data},
765 888 auth_user, pull_request)
766 889
767 890 return pull_request
768 891
769 892 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
770 893 pull_request = self.__get_pull_request(pull_request)
771 894 target_scm = pull_request.target_repo.scm_instance()
772 895 if action == 'create':
773 896 trigger_hook = hooks_utils.trigger_create_pull_request_hook
774 897 elif action == 'merge':
775 898 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
776 899 elif action == 'close':
777 900 trigger_hook = hooks_utils.trigger_close_pull_request_hook
778 901 elif action == 'review_status_change':
779 902 trigger_hook = hooks_utils.trigger_review_pull_request_hook
780 903 elif action == 'update':
781 904 trigger_hook = hooks_utils.trigger_update_pull_request_hook
782 905 elif action == 'comment':
783 906 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
784 907 elif action == 'comment_edit':
785 908 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
786 909 else:
787 910 return
788 911
789 912 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
790 913 pull_request, action, trigger_hook)
791 914 trigger_hook(
792 915 username=user.username,
793 916 repo_name=pull_request.target_repo.repo_name,
794 917 repo_type=target_scm.alias,
795 918 pull_request=pull_request,
796 919 data=data)
797 920
798 921 def _get_commit_ids(self, pull_request):
799 922 """
800 923 Return the commit ids of the merged pull request.
801 924
802 925 This method is not dealing correctly yet with the lack of autoupdates
803 926 nor with the implicit target updates.
804 927 For example: if a commit in the source repo is already in the target it
805 928 will be reported anyways.
806 929 """
807 930 merge_rev = pull_request.merge_rev
808 931 if merge_rev is None:
809 932 raise ValueError('This pull request was not merged yet')
810 933
811 934 commit_ids = list(pull_request.revisions)
812 935 if merge_rev not in commit_ids:
813 936 commit_ids.append(merge_rev)
814 937
815 938 return commit_ids
816 939
817 940 def merge_repo(self, pull_request, user, extras):
818 941 log.debug("Merging pull request %s", pull_request.pull_request_id)
819 942 extras['user_agent'] = 'internal-merge'
820 943 merge_state = self._merge_pull_request(pull_request, user, extras)
821 944 if merge_state.executed:
822 945 log.debug("Merge was successful, updating the pull request comments.")
823 946 self._comment_and_close_pr(pull_request, user, merge_state)
824 947
825 948 self._log_audit_action(
826 949 'repo.pull_request.merge',
827 950 {'merge_state': merge_state.__dict__},
828 951 user, pull_request)
829 952
830 953 else:
831 954 log.warn("Merge failed, not updating the pull request.")
832 955 return merge_state
833 956
834 957 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
835 958 target_vcs = pull_request.target_repo.scm_instance()
836 959 source_vcs = pull_request.source_repo.scm_instance()
837 960
838 961 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
839 962 pr_id=pull_request.pull_request_id,
840 963 pr_title=pull_request.title,
841 964 source_repo=source_vcs.name,
842 965 source_ref_name=pull_request.source_ref_parts.name,
843 966 target_repo=target_vcs.name,
844 967 target_ref_name=pull_request.target_ref_parts.name,
845 968 )
846 969
847 970 workspace_id = self._workspace_id(pull_request)
848 971 repo_id = pull_request.target_repo.repo_id
849 972 use_rebase = self._use_rebase_for_merging(pull_request)
850 973 close_branch = self._close_branch_before_merging(pull_request)
851 974 user_name = self._user_name_for_merging(pull_request, user)
852 975
853 976 target_ref = self._refresh_reference(
854 977 pull_request.target_ref_parts, target_vcs)
855 978
856 979 callback_daemon, extras = prepare_callback_daemon(
857 980 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
858 981 host=vcs_settings.HOOKS_HOST,
859 982 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
860 983
861 984 with callback_daemon:
862 985 # TODO: johbo: Implement a clean way to run a config_override
863 986 # for a single call.
864 987 target_vcs.config.set(
865 988 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
866 989
867 990 merge_state = target_vcs.merge(
868 991 repo_id, workspace_id, target_ref, source_vcs,
869 992 pull_request.source_ref_parts,
870 993 user_name=user_name, user_email=user.email,
871 994 message=message, use_rebase=use_rebase,
872 995 close_branch=close_branch)
873 996 return merge_state
874 997
875 998 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
876 999 pull_request.merge_rev = merge_state.merge_ref.commit_id
877 1000 pull_request.updated_on = datetime.datetime.now()
878 1001 close_msg = close_msg or 'Pull request merged and closed'
879 1002
880 1003 CommentsModel().create(
881 1004 text=safe_unicode(close_msg),
882 1005 repo=pull_request.target_repo.repo_id,
883 1006 user=user.user_id,
884 1007 pull_request=pull_request.pull_request_id,
885 1008 f_path=None,
886 1009 line_no=None,
887 1010 closing_pr=True
888 1011 )
889 1012
890 1013 Session().add(pull_request)
891 1014 Session().flush()
892 1015 # TODO: paris: replace invalidation with less radical solution
893 1016 ScmModel().mark_for_invalidation(
894 1017 pull_request.target_repo.repo_name)
895 1018 self.trigger_pull_request_hook(pull_request, user, 'merge')
896 1019
897 1020 def has_valid_update_type(self, pull_request):
898 1021 source_ref_type = pull_request.source_ref_parts.type
899 1022 return source_ref_type in self.REF_TYPES
900 1023
901 1024 def get_flow_commits(self, pull_request):
902 1025
903 1026 # source repo
904 1027 source_ref_name = pull_request.source_ref_parts.name
905 1028 source_ref_type = pull_request.source_ref_parts.type
906 1029 source_ref_id = pull_request.source_ref_parts.commit_id
907 1030 source_repo = pull_request.source_repo.scm_instance()
908 1031
909 1032 try:
910 1033 if source_ref_type in self.REF_TYPES:
911 1034 source_commit = source_repo.get_commit(
912 1035 source_ref_name, reference_obj=pull_request.source_ref_parts)
913 1036 else:
914 1037 source_commit = source_repo.get_commit(source_ref_id)
915 1038 except CommitDoesNotExistError:
916 1039 raise SourceRefMissing()
917 1040
918 1041 # target repo
919 1042 target_ref_name = pull_request.target_ref_parts.name
920 1043 target_ref_type = pull_request.target_ref_parts.type
921 1044 target_ref_id = pull_request.target_ref_parts.commit_id
922 1045 target_repo = pull_request.target_repo.scm_instance()
923 1046
924 1047 try:
925 1048 if target_ref_type in self.REF_TYPES:
926 1049 target_commit = target_repo.get_commit(
927 1050 target_ref_name, reference_obj=pull_request.target_ref_parts)
928 1051 else:
929 1052 target_commit = target_repo.get_commit(target_ref_id)
930 1053 except CommitDoesNotExistError:
931 1054 raise TargetRefMissing()
932 1055
933 1056 return source_commit, target_commit
934 1057
935 1058 def update_commits(self, pull_request, updating_user):
936 1059 """
937 1060 Get the updated list of commits for the pull request
938 1061 and return the new pull request version and the list
939 1062 of commits processed by this update action
940 1063
941 1064 updating_user is the user_object who triggered the update
942 1065 """
943 1066 pull_request = self.__get_pull_request(pull_request)
944 1067 source_ref_type = pull_request.source_ref_parts.type
945 1068 source_ref_name = pull_request.source_ref_parts.name
946 1069 source_ref_id = pull_request.source_ref_parts.commit_id
947 1070
948 1071 target_ref_type = pull_request.target_ref_parts.type
949 1072 target_ref_name = pull_request.target_ref_parts.name
950 1073 target_ref_id = pull_request.target_ref_parts.commit_id
951 1074
952 1075 if not self.has_valid_update_type(pull_request):
953 1076 log.debug("Skipping update of pull request %s due to ref type: %s",
954 1077 pull_request, source_ref_type)
955 1078 return UpdateResponse(
956 1079 executed=False,
957 1080 reason=UpdateFailureReason.WRONG_REF_TYPE,
958 1081 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
959 1082 source_changed=False, target_changed=False)
960 1083
961 1084 try:
962 1085 source_commit, target_commit = self.get_flow_commits(pull_request)
963 1086 except SourceRefMissing:
964 1087 return UpdateResponse(
965 1088 executed=False,
966 1089 reason=UpdateFailureReason.MISSING_SOURCE_REF,
967 1090 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
968 1091 source_changed=False, target_changed=False)
969 1092 except TargetRefMissing:
970 1093 return UpdateResponse(
971 1094 executed=False,
972 1095 reason=UpdateFailureReason.MISSING_TARGET_REF,
973 1096 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
974 1097 source_changed=False, target_changed=False)
975 1098
976 1099 source_changed = source_ref_id != source_commit.raw_id
977 1100 target_changed = target_ref_id != target_commit.raw_id
978 1101
979 1102 if not (source_changed or target_changed):
980 1103 log.debug("Nothing changed in pull request %s", pull_request)
981 1104 return UpdateResponse(
982 1105 executed=False,
983 1106 reason=UpdateFailureReason.NO_CHANGE,
984 1107 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
985 1108 source_changed=target_changed, target_changed=source_changed)
986 1109
987 1110 change_in_found = 'target repo' if target_changed else 'source repo'
988 1111 log.debug('Updating pull request because of change in %s detected',
989 1112 change_in_found)
990 1113
991 1114 # Finally there is a need for an update, in case of source change
992 1115 # we create a new version, else just an update
993 1116 if source_changed:
994 1117 pull_request_version = self._create_version_from_snapshot(pull_request)
995 1118 self._link_comments_to_version(pull_request_version)
996 1119 else:
997 1120 try:
998 1121 ver = pull_request.versions[-1]
999 1122 except IndexError:
1000 1123 ver = None
1001 1124
1002 1125 pull_request.pull_request_version_id = \
1003 1126 ver.pull_request_version_id if ver else None
1004 1127 pull_request_version = pull_request
1005 1128
1006 1129 source_repo = pull_request.source_repo.scm_instance()
1007 1130 target_repo = pull_request.target_repo.scm_instance()
1008 1131
1009 1132 # re-compute commit ids
1010 1133 old_commit_ids = pull_request.revisions
1011 1134 pre_load = ["author", "date", "message", "branch"]
1012 1135 commit_ranges = target_repo.compare(
1013 1136 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
1014 1137 pre_load=pre_load)
1015 1138
1016 1139 target_ref = target_commit.raw_id
1017 1140 source_ref = source_commit.raw_id
1018 1141 ancestor_commit_id = target_repo.get_common_ancestor(
1019 1142 target_ref, source_ref, source_repo)
1020 1143
1021 1144 if not ancestor_commit_id:
1022 1145 raise ValueError(
1023 1146 'cannot calculate diff info without a common ancestor. '
1024 1147 'Make sure both repositories are related, and have a common forking commit.')
1025 1148
1026 1149 pull_request.common_ancestor_id = ancestor_commit_id
1027 1150
1028 1151 pull_request.source_ref = '%s:%s:%s' % (
1029 1152 source_ref_type, source_ref_name, source_commit.raw_id)
1030 1153 pull_request.target_ref = '%s:%s:%s' % (
1031 1154 target_ref_type, target_ref_name, ancestor_commit_id)
1032 1155
1033 1156 pull_request.revisions = [
1034 1157 commit.raw_id for commit in reversed(commit_ranges)]
1035 1158 pull_request.updated_on = datetime.datetime.now()
1036 1159 Session().add(pull_request)
1037 1160 new_commit_ids = pull_request.revisions
1038 1161
1039 1162 old_diff_data, new_diff_data = self._generate_update_diffs(
1040 1163 pull_request, pull_request_version)
1041 1164
1042 1165 # calculate commit and file changes
1043 1166 commit_changes = self._calculate_commit_id_changes(
1044 1167 old_commit_ids, new_commit_ids)
1045 1168 file_changes = self._calculate_file_changes(
1046 1169 old_diff_data, new_diff_data)
1047 1170
1048 1171 # set comments as outdated if DIFFS changed
1049 1172 CommentsModel().outdate_comments(
1050 1173 pull_request, old_diff_data=old_diff_data,
1051 1174 new_diff_data=new_diff_data)
1052 1175
1053 1176 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1054 1177 file_node_changes = (
1055 1178 file_changes.added or file_changes.modified or file_changes.removed)
1056 1179 pr_has_changes = valid_commit_changes or file_node_changes
1057 1180
1058 1181 # Add an automatic comment to the pull request, in case
1059 1182 # anything has changed
1060 1183 if pr_has_changes:
1061 1184 update_comment = CommentsModel().create(
1062 1185 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1063 1186 repo=pull_request.target_repo,
1064 1187 user=pull_request.author,
1065 1188 pull_request=pull_request,
1066 1189 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1067 1190
1068 1191 # Update status to "Under Review" for added commits
1069 1192 for commit_id in commit_changes.added:
1070 1193 ChangesetStatusModel().set_status(
1071 1194 repo=pull_request.source_repo,
1072 1195 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1073 1196 comment=update_comment,
1074 1197 user=pull_request.author,
1075 1198 pull_request=pull_request,
1076 1199 revision=commit_id)
1077 1200
1078 1201 # send update email to users
1079 1202 try:
1080 1203 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1081 1204 ancestor_commit_id=ancestor_commit_id,
1082 1205 commit_changes=commit_changes,
1083 1206 file_changes=file_changes)
1084 1207 except Exception:
1085 1208 log.exception('Failed to send email notification to users')
1086 1209
1087 1210 log.debug(
1088 1211 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1089 1212 'removed_ids: %s', pull_request.pull_request_id,
1090 1213 commit_changes.added, commit_changes.common, commit_changes.removed)
1091 1214 log.debug(
1092 1215 'Updated pull request with the following file changes: %s',
1093 1216 file_changes)
1094 1217
1095 1218 log.info(
1096 1219 "Updated pull request %s from commit %s to commit %s, "
1097 1220 "stored new version %s of this pull request.",
1098 1221 pull_request.pull_request_id, source_ref_id,
1099 1222 pull_request.source_ref_parts.commit_id,
1100 1223 pull_request_version.pull_request_version_id)
1101 1224 Session().commit()
1102 1225 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1103 1226
1104 1227 return UpdateResponse(
1105 1228 executed=True, reason=UpdateFailureReason.NONE,
1106 1229 old=pull_request, new=pull_request_version,
1107 1230 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1108 1231 source_changed=source_changed, target_changed=target_changed)
1109 1232
1110 1233 def _create_version_from_snapshot(self, pull_request):
1111 1234 version = PullRequestVersion()
1112 1235 version.title = pull_request.title
1113 1236 version.description = pull_request.description
1114 1237 version.status = pull_request.status
1115 1238 version.pull_request_state = pull_request.pull_request_state
1116 1239 version.created_on = datetime.datetime.now()
1117 1240 version.updated_on = pull_request.updated_on
1118 1241 version.user_id = pull_request.user_id
1119 1242 version.source_repo = pull_request.source_repo
1120 1243 version.source_ref = pull_request.source_ref
1121 1244 version.target_repo = pull_request.target_repo
1122 1245 version.target_ref = pull_request.target_ref
1123 1246
1124 1247 version._last_merge_source_rev = pull_request._last_merge_source_rev
1125 1248 version._last_merge_target_rev = pull_request._last_merge_target_rev
1126 1249 version.last_merge_status = pull_request.last_merge_status
1127 1250 version.last_merge_metadata = pull_request.last_merge_metadata
1128 1251 version.shadow_merge_ref = pull_request.shadow_merge_ref
1129 1252 version.merge_rev = pull_request.merge_rev
1130 1253 version.reviewer_data = pull_request.reviewer_data
1131 1254
1132 1255 version.revisions = pull_request.revisions
1133 1256 version.common_ancestor_id = pull_request.common_ancestor_id
1134 1257 version.pull_request = pull_request
1135 1258 Session().add(version)
1136 1259 Session().flush()
1137 1260
1138 1261 return version
1139 1262
1140 1263 def _generate_update_diffs(self, pull_request, pull_request_version):
1141 1264
1142 1265 diff_context = (
1143 1266 self.DIFF_CONTEXT +
1144 1267 CommentsModel.needed_extra_diff_context())
1145 1268 hide_whitespace_changes = False
1146 1269 source_repo = pull_request_version.source_repo
1147 1270 source_ref_id = pull_request_version.source_ref_parts.commit_id
1148 1271 target_ref_id = pull_request_version.target_ref_parts.commit_id
1149 1272 old_diff = self._get_diff_from_pr_or_version(
1150 1273 source_repo, source_ref_id, target_ref_id,
1151 1274 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1152 1275
1153 1276 source_repo = pull_request.source_repo
1154 1277 source_ref_id = pull_request.source_ref_parts.commit_id
1155 1278 target_ref_id = pull_request.target_ref_parts.commit_id
1156 1279
1157 1280 new_diff = self._get_diff_from_pr_or_version(
1158 1281 source_repo, source_ref_id, target_ref_id,
1159 1282 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1160 1283
1161 1284 old_diff_data = diffs.DiffProcessor(old_diff)
1162 1285 old_diff_data.prepare()
1163 1286 new_diff_data = diffs.DiffProcessor(new_diff)
1164 1287 new_diff_data.prepare()
1165 1288
1166 1289 return old_diff_data, new_diff_data
1167 1290
1168 1291 def _link_comments_to_version(self, pull_request_version):
1169 1292 """
1170 1293 Link all unlinked comments of this pull request to the given version.
1171 1294
1172 1295 :param pull_request_version: The `PullRequestVersion` to which
1173 1296 the comments shall be linked.
1174 1297
1175 1298 """
1176 1299 pull_request = pull_request_version.pull_request
1177 1300 comments = ChangesetComment.query()\
1178 1301 .filter(
1179 1302 # TODO: johbo: Should we query for the repo at all here?
1180 1303 # Pending decision on how comments of PRs are to be related
1181 1304 # to either the source repo, the target repo or no repo at all.
1182 1305 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1183 1306 ChangesetComment.pull_request == pull_request,
1184 1307 ChangesetComment.pull_request_version == None)\
1185 1308 .order_by(ChangesetComment.comment_id.asc())
1186 1309
1187 1310 # TODO: johbo: Find out why this breaks if it is done in a bulk
1188 1311 # operation.
1189 1312 for comment in comments:
1190 1313 comment.pull_request_version_id = (
1191 1314 pull_request_version.pull_request_version_id)
1192 1315 Session().add(comment)
1193 1316
1194 1317 def _calculate_commit_id_changes(self, old_ids, new_ids):
1195 1318 added = [x for x in new_ids if x not in old_ids]
1196 1319 common = [x for x in new_ids if x in old_ids]
1197 1320 removed = [x for x in old_ids if x not in new_ids]
1198 1321 total = new_ids
1199 1322 return ChangeTuple(added, common, removed, total)
1200 1323
1201 1324 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1202 1325
1203 1326 old_files = OrderedDict()
1204 1327 for diff_data in old_diff_data.parsed_diff:
1205 1328 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1206 1329
1207 1330 added_files = []
1208 1331 modified_files = []
1209 1332 removed_files = []
1210 1333 for diff_data in new_diff_data.parsed_diff:
1211 1334 new_filename = diff_data['filename']
1212 1335 new_hash = md5_safe(diff_data['raw_diff'])
1213 1336
1214 1337 old_hash = old_files.get(new_filename)
1215 1338 if not old_hash:
1216 1339 # file is not present in old diff, we have to figure out from parsed diff
1217 1340 # operation ADD/REMOVE
1218 1341 operations_dict = diff_data['stats']['ops']
1219 1342 if diffs.DEL_FILENODE in operations_dict:
1220 1343 removed_files.append(new_filename)
1221 1344 else:
1222 1345 added_files.append(new_filename)
1223 1346 else:
1224 1347 if new_hash != old_hash:
1225 1348 modified_files.append(new_filename)
1226 1349 # now remove a file from old, since we have seen it already
1227 1350 del old_files[new_filename]
1228 1351
1229 1352 # removed files is when there are present in old, but not in NEW,
1230 1353 # since we remove old files that are present in new diff, left-overs
1231 1354 # if any should be the removed files
1232 1355 removed_files.extend(old_files.keys())
1233 1356
1234 1357 return FileChangeTuple(added_files, modified_files, removed_files)
1235 1358
1236 1359 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1237 1360 """
1238 1361 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1239 1362 so it's always looking the same disregarding on which default
1240 1363 renderer system is using.
1241 1364
1242 1365 :param ancestor_commit_id: ancestor raw_id
1243 1366 :param changes: changes named tuple
1244 1367 :param file_changes: file changes named tuple
1245 1368
1246 1369 """
1247 1370 new_status = ChangesetStatus.get_status_lbl(
1248 1371 ChangesetStatus.STATUS_UNDER_REVIEW)
1249 1372
1250 1373 changed_files = (
1251 1374 file_changes.added + file_changes.modified + file_changes.removed)
1252 1375
1253 1376 params = {
1254 1377 'under_review_label': new_status,
1255 1378 'added_commits': changes.added,
1256 1379 'removed_commits': changes.removed,
1257 1380 'changed_files': changed_files,
1258 1381 'added_files': file_changes.added,
1259 1382 'modified_files': file_changes.modified,
1260 1383 'removed_files': file_changes.removed,
1261 1384 'ancestor_commit_id': ancestor_commit_id
1262 1385 }
1263 1386 renderer = RstTemplateRenderer()
1264 1387 return renderer.render('pull_request_update.mako', **params)
1265 1388
1266 1389 def edit(self, pull_request, title, description, description_renderer, user):
1267 1390 pull_request = self.__get_pull_request(pull_request)
1268 1391 old_data = pull_request.get_api_data(with_merge_state=False)
1269 1392 if pull_request.is_closed():
1270 1393 raise ValueError('This pull request is closed')
1271 1394 if title:
1272 1395 pull_request.title = title
1273 1396 pull_request.description = description
1274 1397 pull_request.updated_on = datetime.datetime.now()
1275 1398 pull_request.description_renderer = description_renderer
1276 1399 Session().add(pull_request)
1277 1400 self._log_audit_action(
1278 1401 'repo.pull_request.edit', {'old_data': old_data},
1279 1402 user, pull_request)
1280 1403
1281 1404 def update_reviewers(self, pull_request, reviewer_data, user):
1282 1405 """
1283 1406 Update the reviewers in the pull request
1284 1407
1285 1408 :param pull_request: the pr to update
1286 1409 :param reviewer_data: list of tuples
1287 1410 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1288 1411 :param user: current use who triggers this action
1289 1412 """
1290 1413
1291 1414 pull_request = self.__get_pull_request(pull_request)
1292 1415 if pull_request.is_closed():
1293 1416 raise ValueError('This pull request is closed')
1294 1417
1295 1418 reviewers = {}
1296 1419 for user_id, reasons, mandatory, role, rules in reviewer_data:
1297 1420 if isinstance(user_id, (int, compat.string_types)):
1298 1421 user_id = self._get_user(user_id).user_id
1299 1422 reviewers[user_id] = {
1300 1423 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1301 1424
1302 1425 reviewers_ids = set(reviewers.keys())
1303 1426 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1304 1427 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1305 1428
1306 1429 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1307 1430
1308 1431 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1309 1432 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1310 1433
1311 1434 log.debug("Adding %s reviewers", ids_to_add)
1312 1435 log.debug("Removing %s reviewers", ids_to_remove)
1313 1436 changed = False
1314 1437 added_audit_reviewers = []
1315 1438 removed_audit_reviewers = []
1316 1439
1317 1440 for uid in ids_to_add:
1318 1441 changed = True
1319 1442 _usr = self._get_user(uid)
1320 1443 reviewer = PullRequestReviewers()
1321 1444 reviewer.user = _usr
1322 1445 reviewer.pull_request = pull_request
1323 1446 reviewer.reasons = reviewers[uid]['reasons']
1324 1447 # NOTE(marcink): mandatory shouldn't be changed now
1325 1448 # reviewer.mandatory = reviewers[uid]['reasons']
1326 1449 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1327 1450 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1328 1451 Session().add(reviewer)
1329 1452 added_audit_reviewers.append(reviewer.get_dict())
1330 1453
1331 1454 for uid in ids_to_remove:
1332 1455 changed = True
1333 1456 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1334 1457 # This is an edge case that handles previous state of having the same reviewer twice.
1335 1458 # this CAN happen due to the lack of DB checks
1336 1459 reviewers = PullRequestReviewers.query()\
1337 1460 .filter(PullRequestReviewers.user_id == uid,
1338 1461 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1339 1462 PullRequestReviewers.pull_request == pull_request)\
1340 1463 .all()
1341 1464
1342 1465 for obj in reviewers:
1343 1466 added_audit_reviewers.append(obj.get_dict())
1344 1467 Session().delete(obj)
1345 1468
1346 1469 if changed:
1347 1470 Session().expire_all()
1348 1471 pull_request.updated_on = datetime.datetime.now()
1349 1472 Session().add(pull_request)
1350 1473
1351 1474 # finally store audit logs
1352 1475 for user_data in added_audit_reviewers:
1353 1476 self._log_audit_action(
1354 1477 'repo.pull_request.reviewer.add', {'data': user_data},
1355 1478 user, pull_request)
1356 1479 for user_data in removed_audit_reviewers:
1357 1480 self._log_audit_action(
1358 1481 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1359 1482 user, pull_request)
1360 1483
1361 1484 self.notify_reviewers(pull_request, ids_to_add, user)
1362 1485 return ids_to_add, ids_to_remove
1363 1486
1364 1487 def update_observers(self, pull_request, observer_data, user):
1365 1488 """
1366 1489 Update the observers in the pull request
1367 1490
1368 1491 :param pull_request: the pr to update
1369 1492 :param observer_data: list of tuples
1370 1493 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1371 1494 :param user: current use who triggers this action
1372 1495 """
1373 1496 pull_request = self.__get_pull_request(pull_request)
1374 1497 if pull_request.is_closed():
1375 1498 raise ValueError('This pull request is closed')
1376 1499
1377 1500 observers = {}
1378 1501 for user_id, reasons, mandatory, role, rules in observer_data:
1379 1502 if isinstance(user_id, (int, compat.string_types)):
1380 1503 user_id = self._get_user(user_id).user_id
1381 1504 observers[user_id] = {
1382 1505 'reasons': reasons, 'observers': mandatory, 'role': role}
1383 1506
1384 1507 observers_ids = set(observers.keys())
1385 1508 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1386 1509 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1387 1510
1388 1511 current_observers_ids = set([x.user.user_id for x in current_observers])
1389 1512
1390 1513 ids_to_add = observers_ids.difference(current_observers_ids)
1391 1514 ids_to_remove = current_observers_ids.difference(observers_ids)
1392 1515
1393 1516 log.debug("Adding %s observer", ids_to_add)
1394 1517 log.debug("Removing %s observer", ids_to_remove)
1395 1518 changed = False
1396 1519 added_audit_observers = []
1397 1520 removed_audit_observers = []
1398 1521
1399 1522 for uid in ids_to_add:
1400 1523 changed = True
1401 1524 _usr = self._get_user(uid)
1402 1525 observer = PullRequestReviewers()
1403 1526 observer.user = _usr
1404 1527 observer.pull_request = pull_request
1405 1528 observer.reasons = observers[uid]['reasons']
1406 1529 # NOTE(marcink): mandatory shouldn't be changed now
1407 1530 # observer.mandatory = observer[uid]['reasons']
1408 1531
1409 1532 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1410 1533 observer.role = PullRequestReviewers.ROLE_OBSERVER
1411 1534 Session().add(observer)
1412 1535 added_audit_observers.append(observer.get_dict())
1413 1536
1414 1537 for uid in ids_to_remove:
1415 1538 changed = True
1416 1539 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1417 1540 # This is an edge case that handles previous state of having the same reviewer twice.
1418 1541 # this CAN happen due to the lack of DB checks
1419 1542 observers = PullRequestReviewers.query()\
1420 1543 .filter(PullRequestReviewers.user_id == uid,
1421 1544 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1422 1545 PullRequestReviewers.pull_request == pull_request)\
1423 1546 .all()
1424 1547
1425 1548 for obj in observers:
1426 1549 added_audit_observers.append(obj.get_dict())
1427 1550 Session().delete(obj)
1428 1551
1429 1552 if changed:
1430 1553 Session().expire_all()
1431 1554 pull_request.updated_on = datetime.datetime.now()
1432 1555 Session().add(pull_request)
1433 1556
1434 1557 # finally store audit logs
1435 1558 for user_data in added_audit_observers:
1436 1559 self._log_audit_action(
1437 1560 'repo.pull_request.observer.add', {'data': user_data},
1438 1561 user, pull_request)
1439 1562 for user_data in removed_audit_observers:
1440 1563 self._log_audit_action(
1441 1564 'repo.pull_request.observer.delete', {'old_data': user_data},
1442 1565 user, pull_request)
1443 1566
1444 1567 self.notify_observers(pull_request, ids_to_add, user)
1445 1568 return ids_to_add, ids_to_remove
1446 1569
1447 1570 def get_url(self, pull_request, request=None, permalink=False):
1448 1571 if not request:
1449 1572 request = get_current_request()
1450 1573
1451 1574 if permalink:
1452 1575 return request.route_url(
1453 1576 'pull_requests_global',
1454 1577 pull_request_id=pull_request.pull_request_id,)
1455 1578 else:
1456 1579 return request.route_url('pullrequest_show',
1457 1580 repo_name=safe_str(pull_request.target_repo.repo_name),
1458 1581 pull_request_id=pull_request.pull_request_id,)
1459 1582
1460 1583 def get_shadow_clone_url(self, pull_request, request=None):
1461 1584 """
1462 1585 Returns qualified url pointing to the shadow repository. If this pull
1463 1586 request is closed there is no shadow repository and ``None`` will be
1464 1587 returned.
1465 1588 """
1466 1589 if pull_request.is_closed():
1467 1590 return None
1468 1591 else:
1469 1592 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1470 1593 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1471 1594
1472 1595 def _notify_reviewers(self, pull_request, user_ids, role, user):
1473 1596 # notification to reviewers/observers
1474 1597 if not user_ids:
1475 1598 return
1476 1599
1477 1600 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1478 1601
1479 1602 pull_request_obj = pull_request
1480 1603 # get the current participants of this pull request
1481 1604 recipients = user_ids
1482 1605 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1483 1606
1484 1607 pr_source_repo = pull_request_obj.source_repo
1485 1608 pr_target_repo = pull_request_obj.target_repo
1486 1609
1487 1610 pr_url = h.route_url('pullrequest_show',
1488 1611 repo_name=pr_target_repo.repo_name,
1489 1612 pull_request_id=pull_request_obj.pull_request_id,)
1490 1613
1491 1614 # set some variables for email notification
1492 1615 pr_target_repo_url = h.route_url(
1493 1616 'repo_summary', repo_name=pr_target_repo.repo_name)
1494 1617
1495 1618 pr_source_repo_url = h.route_url(
1496 1619 'repo_summary', repo_name=pr_source_repo.repo_name)
1497 1620
1498 1621 # pull request specifics
1499 1622 pull_request_commits = [
1500 1623 (x.raw_id, x.message)
1501 1624 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1502 1625
1503 1626 current_rhodecode_user = user
1504 1627 kwargs = {
1505 1628 'user': current_rhodecode_user,
1506 1629 'pull_request_author': pull_request.author,
1507 1630 'pull_request': pull_request_obj,
1508 1631 'pull_request_commits': pull_request_commits,
1509 1632
1510 1633 'pull_request_target_repo': pr_target_repo,
1511 1634 'pull_request_target_repo_url': pr_target_repo_url,
1512 1635
1513 1636 'pull_request_source_repo': pr_source_repo,
1514 1637 'pull_request_source_repo_url': pr_source_repo_url,
1515 1638
1516 1639 'pull_request_url': pr_url,
1517 1640 'thread_ids': [pr_url],
1518 1641 'user_role': role
1519 1642 }
1520 1643
1521 1644 # create notification objects, and emails
1522 1645 NotificationModel().create(
1523 1646 created_by=current_rhodecode_user,
1524 1647 notification_subject='', # Filled in based on the notification_type
1525 1648 notification_body='', # Filled in based on the notification_type
1526 1649 notification_type=notification_type,
1527 1650 recipients=recipients,
1528 1651 email_kwargs=kwargs,
1529 1652 )
1530 1653
1531 1654 def notify_reviewers(self, pull_request, reviewers_ids, user):
1532 1655 return self._notify_reviewers(pull_request, reviewers_ids,
1533 1656 PullRequestReviewers.ROLE_REVIEWER, user)
1534 1657
1535 1658 def notify_observers(self, pull_request, observers_ids, user):
1536 1659 return self._notify_reviewers(pull_request, observers_ids,
1537 1660 PullRequestReviewers.ROLE_OBSERVER, user)
1538 1661
1539 1662 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1540 1663 commit_changes, file_changes):
1541 1664
1542 1665 updating_user_id = updating_user.user_id
1543 1666 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1544 1667 # NOTE(marcink): send notification to all other users except to
1545 1668 # person who updated the PR
1546 1669 recipients = reviewers.difference(set([updating_user_id]))
1547 1670
1548 1671 log.debug('Notify following recipients about pull-request update %s', recipients)
1549 1672
1550 1673 pull_request_obj = pull_request
1551 1674
1552 1675 # send email about the update
1553 1676 changed_files = (
1554 1677 file_changes.added + file_changes.modified + file_changes.removed)
1555 1678
1556 1679 pr_source_repo = pull_request_obj.source_repo
1557 1680 pr_target_repo = pull_request_obj.target_repo
1558 1681
1559 1682 pr_url = h.route_url('pullrequest_show',
1560 1683 repo_name=pr_target_repo.repo_name,
1561 1684 pull_request_id=pull_request_obj.pull_request_id,)
1562 1685
1563 1686 # set some variables for email notification
1564 1687 pr_target_repo_url = h.route_url(
1565 1688 'repo_summary', repo_name=pr_target_repo.repo_name)
1566 1689
1567 1690 pr_source_repo_url = h.route_url(
1568 1691 'repo_summary', repo_name=pr_source_repo.repo_name)
1569 1692
1570 1693 email_kwargs = {
1571 1694 'date': datetime.datetime.now(),
1572 1695 'updating_user': updating_user,
1573 1696
1574 1697 'pull_request': pull_request_obj,
1575 1698
1576 1699 'pull_request_target_repo': pr_target_repo,
1577 1700 'pull_request_target_repo_url': pr_target_repo_url,
1578 1701
1579 1702 'pull_request_source_repo': pr_source_repo,
1580 1703 'pull_request_source_repo_url': pr_source_repo_url,
1581 1704
1582 1705 'pull_request_url': pr_url,
1583 1706
1584 1707 'ancestor_commit_id': ancestor_commit_id,
1585 1708 'added_commits': commit_changes.added,
1586 1709 'removed_commits': commit_changes.removed,
1587 1710 'changed_files': changed_files,
1588 1711 'added_files': file_changes.added,
1589 1712 'modified_files': file_changes.modified,
1590 1713 'removed_files': file_changes.removed,
1591 1714 'thread_ids': [pr_url],
1592 1715 }
1593 1716
1594 1717 # create notification objects, and emails
1595 1718 NotificationModel().create(
1596 1719 created_by=updating_user,
1597 1720 notification_subject='', # Filled in based on the notification_type
1598 1721 notification_body='', # Filled in based on the notification_type
1599 1722 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1600 1723 recipients=recipients,
1601 1724 email_kwargs=email_kwargs,
1602 1725 )
1603 1726
1604 1727 def delete(self, pull_request, user=None):
1605 1728 if not user:
1606 1729 user = getattr(get_current_rhodecode_user(), 'username', None)
1607 1730
1608 1731 pull_request = self.__get_pull_request(pull_request)
1609 1732 old_data = pull_request.get_api_data(with_merge_state=False)
1610 1733 self._cleanup_merge_workspace(pull_request)
1611 1734 self._log_audit_action(
1612 1735 'repo.pull_request.delete', {'old_data': old_data},
1613 1736 user, pull_request)
1614 1737 Session().delete(pull_request)
1615 1738
1616 1739 def close_pull_request(self, pull_request, user):
1617 1740 pull_request = self.__get_pull_request(pull_request)
1618 1741 self._cleanup_merge_workspace(pull_request)
1619 1742 pull_request.status = PullRequest.STATUS_CLOSED
1620 1743 pull_request.updated_on = datetime.datetime.now()
1621 1744 Session().add(pull_request)
1622 1745 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1623 1746
1624 1747 pr_data = pull_request.get_api_data(with_merge_state=False)
1625 1748 self._log_audit_action(
1626 1749 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1627 1750
1628 1751 def close_pull_request_with_comment(
1629 1752 self, pull_request, user, repo, message=None, auth_user=None):
1630 1753
1631 1754 pull_request_review_status = pull_request.calculated_review_status()
1632 1755
1633 1756 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1634 1757 # approved only if we have voting consent
1635 1758 status = ChangesetStatus.STATUS_APPROVED
1636 1759 else:
1637 1760 status = ChangesetStatus.STATUS_REJECTED
1638 1761 status_lbl = ChangesetStatus.get_status_lbl(status)
1639 1762
1640 1763 default_message = (
1641 1764 'Closing with status change {transition_icon} {status}.'
1642 1765 ).format(transition_icon='>', status=status_lbl)
1643 1766 text = message or default_message
1644 1767
1645 1768 # create a comment, and link it to new status
1646 1769 comment = CommentsModel().create(
1647 1770 text=text,
1648 1771 repo=repo.repo_id,
1649 1772 user=user.user_id,
1650 1773 pull_request=pull_request.pull_request_id,
1651 1774 status_change=status_lbl,
1652 1775 status_change_type=status,
1653 1776 closing_pr=True,
1654 1777 auth_user=auth_user,
1655 1778 )
1656 1779
1657 1780 # calculate old status before we change it
1658 1781 old_calculated_status = pull_request.calculated_review_status()
1659 1782 ChangesetStatusModel().set_status(
1660 1783 repo.repo_id,
1661 1784 status,
1662 1785 user.user_id,
1663 1786 comment=comment,
1664 1787 pull_request=pull_request.pull_request_id
1665 1788 )
1666 1789
1667 1790 Session().flush()
1668 1791
1669 1792 self.trigger_pull_request_hook(pull_request, user, 'comment',
1670 1793 data={'comment': comment})
1671 1794
1672 1795 # we now calculate the status of pull request again, and based on that
1673 1796 # calculation trigger status change. This might happen in cases
1674 1797 # that non-reviewer admin closes a pr, which means his vote doesn't
1675 1798 # change the status, while if he's a reviewer this might change it.
1676 1799 calculated_status = pull_request.calculated_review_status()
1677 1800 if old_calculated_status != calculated_status:
1678 1801 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1679 1802 data={'status': calculated_status})
1680 1803
1681 1804 # finally close the PR
1682 1805 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1683 1806
1684 1807 return comment, status
1685 1808
1686 1809 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1687 1810 _ = translator or get_current_request().translate
1688 1811
1689 1812 if not self._is_merge_enabled(pull_request):
1690 1813 return None, False, _('Server-side pull request merging is disabled.')
1691 1814
1692 1815 if pull_request.is_closed():
1693 1816 return None, False, _('This pull request is closed.')
1694 1817
1695 1818 merge_possible, msg = self._check_repo_requirements(
1696 1819 target=pull_request.target_repo, source=pull_request.source_repo,
1697 1820 translator=_)
1698 1821 if not merge_possible:
1699 1822 return None, merge_possible, msg
1700 1823
1701 1824 try:
1702 1825 merge_response = self._try_merge(
1703 1826 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1704 1827 log.debug("Merge response: %s", merge_response)
1705 1828 return merge_response, merge_response.possible, merge_response.merge_status_message
1706 1829 except NotImplementedError:
1707 1830 return None, False, _('Pull request merging is not supported.')
1708 1831
1709 1832 def _check_repo_requirements(self, target, source, translator):
1710 1833 """
1711 1834 Check if `target` and `source` have compatible requirements.
1712 1835
1713 1836 Currently this is just checking for largefiles.
1714 1837 """
1715 1838 _ = translator
1716 1839 target_has_largefiles = self._has_largefiles(target)
1717 1840 source_has_largefiles = self._has_largefiles(source)
1718 1841 merge_possible = True
1719 1842 message = u''
1720 1843
1721 1844 if target_has_largefiles != source_has_largefiles:
1722 1845 merge_possible = False
1723 1846 if source_has_largefiles:
1724 1847 message = _(
1725 1848 'Target repository large files support is disabled.')
1726 1849 else:
1727 1850 message = _(
1728 1851 'Source repository large files support is disabled.')
1729 1852
1730 1853 return merge_possible, message
1731 1854
1732 1855 def _has_largefiles(self, repo):
1733 1856 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1734 1857 'extensions', 'largefiles')
1735 1858 return largefiles_ui and largefiles_ui[0].active
1736 1859
1737 1860 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1738 1861 """
1739 1862 Try to merge the pull request and return the merge status.
1740 1863 """
1741 1864 log.debug(
1742 1865 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1743 1866 pull_request.pull_request_id, force_shadow_repo_refresh)
1744 1867 target_vcs = pull_request.target_repo.scm_instance()
1745 1868 # Refresh the target reference.
1746 1869 try:
1747 1870 target_ref = self._refresh_reference(
1748 1871 pull_request.target_ref_parts, target_vcs)
1749 1872 except CommitDoesNotExistError:
1750 1873 merge_state = MergeResponse(
1751 1874 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1752 1875 metadata={'target_ref': pull_request.target_ref_parts})
1753 1876 return merge_state
1754 1877
1755 1878 target_locked = pull_request.target_repo.locked
1756 1879 if target_locked and target_locked[0]:
1757 1880 locked_by = 'user:{}'.format(target_locked[0])
1758 1881 log.debug("The target repository is locked by %s.", locked_by)
1759 1882 merge_state = MergeResponse(
1760 1883 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1761 1884 metadata={'locked_by': locked_by})
1762 1885 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1763 1886 pull_request, target_ref):
1764 1887 log.debug("Refreshing the merge status of the repository.")
1765 1888 merge_state = self._refresh_merge_state(
1766 1889 pull_request, target_vcs, target_ref)
1767 1890 else:
1768 1891 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1769 1892 metadata = {
1770 1893 'unresolved_files': '',
1771 1894 'target_ref': pull_request.target_ref_parts,
1772 1895 'source_ref': pull_request.source_ref_parts,
1773 1896 }
1774 1897 if pull_request.last_merge_metadata:
1775 1898 metadata.update(pull_request.last_merge_metadata_parsed)
1776 1899
1777 1900 if not possible and target_ref.type == 'branch':
1778 1901 # NOTE(marcink): case for mercurial multiple heads on branch
1779 1902 heads = target_vcs._heads(target_ref.name)
1780 1903 if len(heads) != 1:
1781 1904 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1782 1905 metadata.update({
1783 1906 'heads': heads
1784 1907 })
1785 1908
1786 1909 merge_state = MergeResponse(
1787 1910 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1788 1911
1789 1912 return merge_state
1790 1913
1791 1914 def _refresh_reference(self, reference, vcs_repository):
1792 1915 if reference.type in self.UPDATABLE_REF_TYPES:
1793 1916 name_or_id = reference.name
1794 1917 else:
1795 1918 name_or_id = reference.commit_id
1796 1919
1797 1920 refreshed_commit = vcs_repository.get_commit(name_or_id)
1798 1921 refreshed_reference = Reference(
1799 1922 reference.type, reference.name, refreshed_commit.raw_id)
1800 1923 return refreshed_reference
1801 1924
1802 1925 def _needs_merge_state_refresh(self, pull_request, target_reference):
1803 1926 return not(
1804 1927 pull_request.revisions and
1805 1928 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1806 1929 target_reference.commit_id == pull_request._last_merge_target_rev)
1807 1930
1808 1931 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1809 1932 workspace_id = self._workspace_id(pull_request)
1810 1933 source_vcs = pull_request.source_repo.scm_instance()
1811 1934 repo_id = pull_request.target_repo.repo_id
1812 1935 use_rebase = self._use_rebase_for_merging(pull_request)
1813 1936 close_branch = self._close_branch_before_merging(pull_request)
1814 1937 merge_state = target_vcs.merge(
1815 1938 repo_id, workspace_id,
1816 1939 target_reference, source_vcs, pull_request.source_ref_parts,
1817 1940 dry_run=True, use_rebase=use_rebase,
1818 1941 close_branch=close_branch)
1819 1942
1820 1943 # Do not store the response if there was an unknown error.
1821 1944 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1822 1945 pull_request._last_merge_source_rev = \
1823 1946 pull_request.source_ref_parts.commit_id
1824 1947 pull_request._last_merge_target_rev = target_reference.commit_id
1825 1948 pull_request.last_merge_status = merge_state.failure_reason
1826 1949 pull_request.last_merge_metadata = merge_state.metadata
1827 1950
1828 1951 pull_request.shadow_merge_ref = merge_state.merge_ref
1829 1952 Session().add(pull_request)
1830 1953 Session().commit()
1831 1954
1832 1955 return merge_state
1833 1956
1834 1957 def _workspace_id(self, pull_request):
1835 1958 workspace_id = 'pr-%s' % pull_request.pull_request_id
1836 1959 return workspace_id
1837 1960
1838 1961 def generate_repo_data(self, repo, commit_id=None, branch=None,
1839 1962 bookmark=None, translator=None):
1840 1963 from rhodecode.model.repo import RepoModel
1841 1964
1842 1965 all_refs, selected_ref = \
1843 1966 self._get_repo_pullrequest_sources(
1844 1967 repo.scm_instance(), commit_id=commit_id,
1845 1968 branch=branch, bookmark=bookmark, translator=translator)
1846 1969
1847 1970 refs_select2 = []
1848 1971 for element in all_refs:
1849 1972 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1850 1973 refs_select2.append({'text': element[1], 'children': children})
1851 1974
1852 1975 return {
1853 1976 'user': {
1854 1977 'user_id': repo.user.user_id,
1855 1978 'username': repo.user.username,
1856 1979 'firstname': repo.user.first_name,
1857 1980 'lastname': repo.user.last_name,
1858 1981 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1859 1982 },
1860 1983 'name': repo.repo_name,
1861 1984 'link': RepoModel().get_url(repo),
1862 1985 'description': h.chop_at_smart(repo.description_safe, '\n'),
1863 1986 'refs': {
1864 1987 'all_refs': all_refs,
1865 1988 'selected_ref': selected_ref,
1866 1989 'select2_refs': refs_select2
1867 1990 }
1868 1991 }
1869 1992
1870 1993 def generate_pullrequest_title(self, source, source_ref, target):
1871 1994 return u'{source}#{at_ref} to {target}'.format(
1872 1995 source=source,
1873 1996 at_ref=source_ref,
1874 1997 target=target,
1875 1998 )
1876 1999
1877 2000 def _cleanup_merge_workspace(self, pull_request):
1878 2001 # Merging related cleanup
1879 2002 repo_id = pull_request.target_repo.repo_id
1880 2003 target_scm = pull_request.target_repo.scm_instance()
1881 2004 workspace_id = self._workspace_id(pull_request)
1882 2005
1883 2006 try:
1884 2007 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1885 2008 except NotImplementedError:
1886 2009 pass
1887 2010
1888 2011 def _get_repo_pullrequest_sources(
1889 2012 self, repo, commit_id=None, branch=None, bookmark=None,
1890 2013 translator=None):
1891 2014 """
1892 2015 Return a structure with repo's interesting commits, suitable for
1893 2016 the selectors in pullrequest controller
1894 2017
1895 2018 :param commit_id: a commit that must be in the list somehow
1896 2019 and selected by default
1897 2020 :param branch: a branch that must be in the list and selected
1898 2021 by default - even if closed
1899 2022 :param bookmark: a bookmark that must be in the list and selected
1900 2023 """
1901 2024 _ = translator or get_current_request().translate
1902 2025
1903 2026 commit_id = safe_str(commit_id) if commit_id else None
1904 2027 branch = safe_unicode(branch) if branch else None
1905 2028 bookmark = safe_unicode(bookmark) if bookmark else None
1906 2029
1907 2030 selected = None
1908 2031
1909 2032 # order matters: first source that has commit_id in it will be selected
1910 2033 sources = []
1911 2034 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1912 2035 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1913 2036
1914 2037 if commit_id:
1915 2038 ref_commit = (h.short_id(commit_id), commit_id)
1916 2039 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1917 2040
1918 2041 sources.append(
1919 2042 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1920 2043 )
1921 2044
1922 2045 groups = []
1923 2046
1924 2047 for group_key, ref_list, group_name, match in sources:
1925 2048 group_refs = []
1926 2049 for ref_name, ref_id in ref_list:
1927 2050 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1928 2051 group_refs.append((ref_key, ref_name))
1929 2052
1930 2053 if not selected:
1931 2054 if set([commit_id, match]) & set([ref_id, ref_name]):
1932 2055 selected = ref_key
1933 2056
1934 2057 if group_refs:
1935 2058 groups.append((group_refs, group_name))
1936 2059
1937 2060 if not selected:
1938 2061 ref = commit_id or branch or bookmark
1939 2062 if ref:
1940 2063 raise CommitDoesNotExistError(
1941 2064 u'No commit refs could be found matching: {}'.format(ref))
1942 2065 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1943 2066 selected = u'branch:{}:{}'.format(
1944 2067 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1945 2068 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1946 2069 )
1947 2070 elif repo.commit_ids:
1948 2071 # make the user select in this case
1949 2072 selected = None
1950 2073 else:
1951 2074 raise EmptyRepositoryError()
1952 2075 return groups, selected
1953 2076
1954 2077 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1955 2078 hide_whitespace_changes, diff_context):
1956 2079
1957 2080 return self._get_diff_from_pr_or_version(
1958 2081 source_repo, source_ref_id, target_ref_id,
1959 2082 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1960 2083
1961 2084 def _get_diff_from_pr_or_version(
1962 2085 self, source_repo, source_ref_id, target_ref_id,
1963 2086 hide_whitespace_changes, diff_context):
1964 2087
1965 2088 target_commit = source_repo.get_commit(
1966 2089 commit_id=safe_str(target_ref_id))
1967 2090 source_commit = source_repo.get_commit(
1968 2091 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1969 2092 if isinstance(source_repo, Repository):
1970 2093 vcs_repo = source_repo.scm_instance()
1971 2094 else:
1972 2095 vcs_repo = source_repo
1973 2096
1974 2097 # TODO: johbo: In the context of an update, we cannot reach
1975 2098 # the old commit anymore with our normal mechanisms. It needs
1976 2099 # some sort of special support in the vcs layer to avoid this
1977 2100 # workaround.
1978 2101 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1979 2102 vcs_repo.alias == 'git'):
1980 2103 source_commit.raw_id = safe_str(source_ref_id)
1981 2104
1982 2105 log.debug('calculating diff between '
1983 2106 'source_ref:%s and target_ref:%s for repo `%s`',
1984 2107 target_ref_id, source_ref_id,
1985 2108 safe_unicode(vcs_repo.path))
1986 2109
1987 2110 vcs_diff = vcs_repo.get_diff(
1988 2111 commit1=target_commit, commit2=source_commit,
1989 2112 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1990 2113 return vcs_diff
1991 2114
1992 2115 def _is_merge_enabled(self, pull_request):
1993 2116 return self._get_general_setting(
1994 2117 pull_request, 'rhodecode_pr_merge_enabled')
1995 2118
1996 2119 def _use_rebase_for_merging(self, pull_request):
1997 2120 repo_type = pull_request.target_repo.repo_type
1998 2121 if repo_type == 'hg':
1999 2122 return self._get_general_setting(
2000 2123 pull_request, 'rhodecode_hg_use_rebase_for_merging')
2001 2124 elif repo_type == 'git':
2002 2125 return self._get_general_setting(
2003 2126 pull_request, 'rhodecode_git_use_rebase_for_merging')
2004 2127
2005 2128 return False
2006 2129
2007 2130 def _user_name_for_merging(self, pull_request, user):
2008 2131 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
2009 2132 if env_user_name_attr and hasattr(user, env_user_name_attr):
2010 2133 user_name_attr = env_user_name_attr
2011 2134 else:
2012 2135 user_name_attr = 'short_contact'
2013 2136
2014 2137 user_name = getattr(user, user_name_attr)
2015 2138 return user_name
2016 2139
2017 2140 def _close_branch_before_merging(self, pull_request):
2018 2141 repo_type = pull_request.target_repo.repo_type
2019 2142 if repo_type == 'hg':
2020 2143 return self._get_general_setting(
2021 2144 pull_request, 'rhodecode_hg_close_branch_before_merging')
2022 2145 elif repo_type == 'git':
2023 2146 return self._get_general_setting(
2024 2147 pull_request, 'rhodecode_git_close_branch_before_merging')
2025 2148
2026 2149 return False
2027 2150
2028 2151 def _get_general_setting(self, pull_request, settings_key, default=False):
2029 2152 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2030 2153 settings = settings_model.get_general_settings()
2031 2154 return settings.get(settings_key, default)
2032 2155
2033 2156 def _log_audit_action(self, action, action_data, user, pull_request):
2034 2157 audit_logger.store(
2035 2158 action=action,
2036 2159 action_data=action_data,
2037 2160 user=user,
2038 2161 repo=pull_request.target_repo)
2039 2162
2040 2163 def get_reviewer_functions(self):
2041 2164 """
2042 2165 Fetches functions for validation and fetching default reviewers.
2043 2166 If available we use the EE package, else we fallback to CE
2044 2167 package functions
2045 2168 """
2046 2169 try:
2047 2170 from rc_reviewers.utils import get_default_reviewers_data
2048 2171 from rc_reviewers.utils import validate_default_reviewers
2049 2172 from rc_reviewers.utils import validate_observers
2050 2173 except ImportError:
2051 2174 from rhodecode.apps.repository.utils import get_default_reviewers_data
2052 2175 from rhodecode.apps.repository.utils import validate_default_reviewers
2053 2176 from rhodecode.apps.repository.utils import validate_observers
2054 2177
2055 2178 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2056 2179
2057 2180
2058 2181 class MergeCheck(object):
2059 2182 """
2060 2183 Perform Merge Checks and returns a check object which stores information
2061 2184 about merge errors, and merge conditions
2062 2185 """
2063 2186 TODO_CHECK = 'todo'
2064 2187 PERM_CHECK = 'perm'
2065 2188 REVIEW_CHECK = 'review'
2066 2189 MERGE_CHECK = 'merge'
2067 2190 WIP_CHECK = 'wip'
2068 2191
2069 2192 def __init__(self):
2070 2193 self.review_status = None
2071 2194 self.merge_possible = None
2072 2195 self.merge_msg = ''
2073 2196 self.merge_response = None
2074 2197 self.failed = None
2075 2198 self.errors = []
2076 2199 self.error_details = OrderedDict()
2077 2200 self.source_commit = AttributeDict()
2078 2201 self.target_commit = AttributeDict()
2079 2202 self.reviewers_count = 0
2080 2203 self.observers_count = 0
2081 2204
2082 2205 def __repr__(self):
2083 2206 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2084 2207 self.merge_possible, self.failed, self.errors)
2085 2208
2086 2209 def push_error(self, error_type, message, error_key, details):
2087 2210 self.failed = True
2088 2211 self.errors.append([error_type, message])
2089 2212 self.error_details[error_key] = dict(
2090 2213 details=details,
2091 2214 error_type=error_type,
2092 2215 message=message
2093 2216 )
2094 2217
2095 2218 @classmethod
2096 2219 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2097 2220 force_shadow_repo_refresh=False):
2098 2221 _ = translator
2099 2222 merge_check = cls()
2100 2223
2101 2224 # title has WIP:
2102 2225 if pull_request.work_in_progress:
2103 2226 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2104 2227
2105 2228 msg = _('WIP marker in title prevents from accidental merge.')
2106 2229 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2107 2230 if fail_early:
2108 2231 return merge_check
2109 2232
2110 2233 # permissions to merge
2111 2234 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2112 2235 if not user_allowed_to_merge:
2113 2236 log.debug("MergeCheck: cannot merge, approval is pending.")
2114 2237
2115 2238 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2116 2239 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2117 2240 if fail_early:
2118 2241 return merge_check
2119 2242
2120 2243 # permission to merge into the target branch
2121 2244 target_commit_id = pull_request.target_ref_parts.commit_id
2122 2245 if pull_request.target_ref_parts.type == 'branch':
2123 2246 branch_name = pull_request.target_ref_parts.name
2124 2247 else:
2125 2248 # for mercurial we can always figure out the branch from the commit
2126 2249 # in case of bookmark
2127 2250 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2128 2251 branch_name = target_commit.branch
2129 2252
2130 2253 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2131 2254 pull_request.target_repo.repo_name, branch_name)
2132 2255 if branch_perm and branch_perm == 'branch.none':
2133 2256 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2134 2257 branch_name, rule)
2135 2258 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2136 2259 if fail_early:
2137 2260 return merge_check
2138 2261
2139 2262 # review status, must be always present
2140 2263 review_status = pull_request.calculated_review_status()
2141 2264 merge_check.review_status = review_status
2142 2265 merge_check.reviewers_count = pull_request.reviewers_count
2143 2266 merge_check.observers_count = pull_request.observers_count
2144 2267
2145 2268 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2146 2269 if not status_approved and merge_check.reviewers_count:
2147 2270 log.debug("MergeCheck: cannot merge, approval is pending.")
2148 2271 msg = _('Pull request reviewer approval is pending.')
2149 2272
2150 2273 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2151 2274
2152 2275 if fail_early:
2153 2276 return merge_check
2154 2277
2155 2278 # left over TODOs
2156 2279 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2157 2280 if todos:
2158 2281 log.debug("MergeCheck: cannot merge, {} "
2159 2282 "unresolved TODOs left.".format(len(todos)))
2160 2283
2161 2284 if len(todos) == 1:
2162 2285 msg = _('Cannot merge, {} TODO still not resolved.').format(
2163 2286 len(todos))
2164 2287 else:
2165 2288 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2166 2289 len(todos))
2167 2290
2168 2291 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2169 2292
2170 2293 if fail_early:
2171 2294 return merge_check
2172 2295
2173 2296 # merge possible, here is the filesystem simulation + shadow repo
2174 2297 merge_response, merge_status, msg = PullRequestModel().merge_status(
2175 2298 pull_request, translator=translator,
2176 2299 force_shadow_repo_refresh=force_shadow_repo_refresh)
2177 2300
2178 2301 merge_check.merge_possible = merge_status
2179 2302 merge_check.merge_msg = msg
2180 2303 merge_check.merge_response = merge_response
2181 2304
2182 2305 source_ref_id = pull_request.source_ref_parts.commit_id
2183 2306 target_ref_id = pull_request.target_ref_parts.commit_id
2184 2307
2185 2308 try:
2186 2309 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2187 2310 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2188 2311 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2189 2312 merge_check.source_commit.current_raw_id = source_commit.raw_id
2190 2313 merge_check.source_commit.previous_raw_id = source_ref_id
2191 2314
2192 2315 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2193 2316 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2194 2317 merge_check.target_commit.current_raw_id = target_commit.raw_id
2195 2318 merge_check.target_commit.previous_raw_id = target_ref_id
2196 2319 except (SourceRefMissing, TargetRefMissing):
2197 2320 pass
2198 2321
2199 2322 if not merge_status:
2200 2323 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2201 2324 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2202 2325
2203 2326 if fail_early:
2204 2327 return merge_check
2205 2328
2206 2329 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2207 2330 return merge_check
2208 2331
2209 2332 @classmethod
2210 2333 def get_merge_conditions(cls, pull_request, translator):
2211 2334 _ = translator
2212 2335 merge_details = {}
2213 2336
2214 2337 model = PullRequestModel()
2215 2338 use_rebase = model._use_rebase_for_merging(pull_request)
2216 2339
2217 2340 if use_rebase:
2218 2341 merge_details['merge_strategy'] = dict(
2219 2342 details={},
2220 2343 message=_('Merge strategy: rebase')
2221 2344 )
2222 2345 else:
2223 2346 merge_details['merge_strategy'] = dict(
2224 2347 details={},
2225 2348 message=_('Merge strategy: explicit merge commit')
2226 2349 )
2227 2350
2228 2351 close_branch = model._close_branch_before_merging(pull_request)
2229 2352 if close_branch:
2230 2353 repo_type = pull_request.target_repo.repo_type
2231 2354 close_msg = ''
2232 2355 if repo_type == 'hg':
2233 2356 close_msg = _('Source branch will be closed before the merge.')
2234 2357 elif repo_type == 'git':
2235 2358 close_msg = _('Source branch will be deleted after the merge.')
2236 2359
2237 2360 merge_details['close_branch'] = dict(
2238 2361 details={},
2239 2362 message=close_msg
2240 2363 )
2241 2364
2242 2365 return merge_details
2243 2366
2244 2367
2245 2368 ChangeTuple = collections.namedtuple(
2246 2369 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2247 2370
2248 2371 FileChangeTuple = collections.namedtuple(
2249 2372 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,630 +1,630 b''
1 1
2 2
3 3 //BUTTONS
4 4 button,
5 5 .btn,
6 6 input[type="button"] {
7 7 -webkit-appearance: none;
8 8 display: inline-block;
9 9 margin: 0 @padding/3 0 0;
10 10 padding: @button-padding;
11 11 text-align: center;
12 12 font-size: @basefontsize;
13 13 line-height: 1em;
14 14 font-family: @text-light;
15 15 text-decoration: none;
16 16 text-shadow: none;
17 17 color: @grey2;
18 18 background-color: white;
19 19 background-image: none;
20 20 border: none;
21 21 .border ( @border-thickness-buttons, @grey5 );
22 22 .border-radius (@border-radius);
23 23 cursor: pointer;
24 24 white-space: nowrap;
25 25 -webkit-transition: background .3s,color .3s;
26 26 -moz-transition: background .3s,color .3s;
27 27 -o-transition: background .3s,color .3s;
28 28 transition: background .3s,color .3s;
29 29 box-shadow: @button-shadow;
30 30 -webkit-box-shadow: @button-shadow;
31 31
32 32
33 33
34 34 a {
35 35 display: block;
36 36 margin: 0;
37 37 padding: 0;
38 38 color: inherit;
39 39 text-decoration: none;
40 40
41 41 &:hover {
42 42 text-decoration: none;
43 43 }
44 44 }
45 45
46 46 &:focus,
47 47 &:active {
48 48 outline:none;
49 49 }
50 50
51 51 &:hover {
52 52 color: @rcdarkblue;
53 53 background-color: @grey6;
54 54
55 55 }
56 56
57 57 &.btn-active {
58 58 color: @rcdarkblue;
59 59 background-color: @grey6;
60 60 }
61 61
62 62 .icon-remove {
63 63 display: none;
64 64 }
65 65
66 66 //disabled buttons
67 67 //last; overrides any other styles
68 68 &:disabled {
69 69 opacity: .7;
70 70 cursor: auto;
71 71 background-color: white;
72 72 color: @grey4;
73 73 text-shadow: none;
74 74 }
75 75
76 76 &.no-margin {
77 77 margin: 0 0 0 0;
78 78 }
79 79
80 80
81 81
82 82 }
83 83
84 84
85 85 .btn-default {
86 86 border: @border-thickness solid @grey5;
87 87 background-image: none;
88 88 color: @grey2;
89 89
90 90 a {
91 91 color: @grey2;
92 92 }
93 93
94 94 &:hover,
95 95 &.active {
96 96 color: @rcdarkblue;
97 97 background-color: @white;
98 98 .border ( @border-thickness, @grey4 );
99 99
100 100 a {
101 101 color: @grey2;
102 102 }
103 103 }
104 104 &:disabled {
105 105 .border ( @border-thickness-buttons, @grey5 );
106 106 background-color: transparent;
107 107 }
108 108 &.btn-active {
109 109 color: @rcdarkblue;
110 110 background-color: @white;
111 111 .border ( @border-thickness, @rcdarkblue );
112 112 }
113 113 }
114 114
115 115 .btn-primary,
116 116 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
117 117 .btn-success {
118 118 .border ( @border-thickness, @rcblue );
119 119 background-color: @rcblue;
120 120 color: white;
121 121
122 122 a {
123 123 color: white;
124 124 }
125 125
126 126 &:hover,
127 127 &.active {
128 128 .border ( @border-thickness, @rcdarkblue );
129 129 color: white;
130 130 background-color: @rcdarkblue;
131 131
132 132 a {
133 133 color: white;
134 134 }
135 135 }
136 136 &:disabled {
137 137 background-color: @rcblue;
138 138 }
139 139 }
140 140
141 141 .btn-secondary {
142 142 &:extend(.btn-default);
143 143
144 144 background-color: white;
145 145
146 146 &:focus {
147 147 outline: 0;
148 148 }
149 149
150 150 &:hover {
151 151 &:extend(.btn-default:hover);
152 152 }
153 153
154 154 &.btn-link {
155 155 &:extend(.btn-link);
156 156 color: @rcblue;
157 157 }
158 158
159 159 &:disabled {
160 160 color: @rcblue;
161 161 background-color: white;
162 162 }
163 163 }
164 164
165 165 .btn-danger,
166 166 .revoke_perm,
167 167 .btn-x,
168 168 .form .action_button.btn-x {
169 169 .border ( @border-thickness, @alert2 );
170 170 background-color: white;
171 171 color: @alert2;
172 172
173 173 a {
174 174 color: @alert2;
175 175 }
176 176
177 177 &:hover,
178 178 &.active {
179 179 .border ( @border-thickness, @alert2 );
180 180 color: white;
181 181 background-color: @alert2;
182 182
183 183 a {
184 184 color: white;
185 185 }
186 186 }
187 187
188 188 i {
189 189 display:none;
190 190 }
191 191
192 192 &:disabled {
193 193 background-color: white;
194 194 color: @alert2;
195 195 }
196 196 }
197 197
198 198 .btn-warning {
199 199 .border ( @border-thickness, @alert3 );
200 200 background-color: white;
201 201 color: @alert3;
202 202
203 203 a {
204 204 color: @alert3;
205 205 }
206 206
207 207 &:hover,
208 208 &.active {
209 209 .border ( @border-thickness, @alert3 );
210 210 color: white;
211 211 background-color: @alert3;
212 212
213 213 a {
214 214 color: white;
215 215 }
216 216 }
217 217
218 218 i {
219 219 display:none;
220 220 }
221 221
222 222 &:disabled {
223 223 background-color: white;
224 224 color: @alert3;
225 225 }
226 226 }
227 227
228 228
229 229 .btn-approved-status {
230 230 .border ( @border-thickness, @alert1 );
231 231 background-color: white;
232 232 color: @alert1;
233 233
234 234 }
235 235
236 236 .btn-rejected-status {
237 237 .border ( @border-thickness, @alert2 );
238 238 background-color: white;
239 239 color: @alert2;
240 240 }
241 241
242 242 .btn-sm,
243 243 .btn-mini,
244 244 .field-sm .btn {
245 245 padding: @padding/3;
246 246 }
247 247
248 248 .btn-xs {
249 249 padding: @padding/4;
250 250 }
251 251
252 252 .btn-lg {
253 253 padding: @padding * 1.2;
254 254 }
255 255
256 256 .btn-group {
257 257 display: inline-block;
258 258 .btn {
259 259 float: left;
260 260 margin: 0 0 0 0;
261 261 // first item
262 262 &:first-of-type:not(:last-of-type) {
263 263 border-radius: @border-radius 0 0 @border-radius;
264 264
265 265 }
266 266 // 2nd, if only 2 elements are there
267 267 &:nth-of-type(2) {
268 268 border-left-width: 0;
269 269 }
270 270 // middle elements
271 271 &:not(:first-of-type):not(:last-of-type) {
272 272 border-radius: 0;
273 273 border-left-width: 0;
274 274 border-right-width: 0;
275 275 }
276 276 // last item
277 277 &:last-of-type:not(:first-of-type) {
278 278 border-radius: 0 @border-radius @border-radius 0;
279 279 }
280 280
281 281 &:only-child {
282 282 border-radius: @border-radius;
283 283 }
284 284 }
285 285
286 286 }
287 287
288 288
289 289 .btn-group-actions {
290 290 position: relative;
291 291 z-index: 50;
292 292
293 293 &:not(.open) .btn-action-switcher-container {
294 294 display: none;
295 295 }
296 296
297 297 .btn-more-option {
298 298 margin-left: -1px;
299 299 padding-left: 2px;
300 300 padding-right: 2px;
301 301 }
302 302 }
303 303
304 304
305 305 .btn-action-switcher-container {
306 306 position: absolute;
307 307 top: 100%;
308 308
309 309 &.left-align {
310 310 left: 0;
311 311 }
312 312 &.right-align {
313 313 right: 0;
314 314 }
315 315
316 316 }
317 317
318 318 .btn-action-switcher {
319 319 display: block;
320 320 position: relative;
321 321 z-index: 300;
322 322 max-width: 600px;
323 323 margin-top: 4px;
324 324 margin-bottom: 24px;
325 325 font-size: 14px;
326 326 font-weight: 400;
327 327 padding: 8px 0;
328 328 background-color: #fff;
329 329 border: 1px solid @grey4;
330 330 border-radius: 3px;
331 331 box-shadow: @dropdown-shadow;
332 332 overflow: auto;
333 333
334 334 li {
335 335 display: block;
336 336 text-align: left;
337 337 list-style: none;
338 338 padding: 5px 10px;
339 339 }
340 340
341 341 li .action-help-block {
342 342 font-size: 10px;
343 343 line-height: normal;
344 344 color: @grey4;
345 345 }
346 346
347 347 }
348 348
349 349 .btn-link {
350 350 background: transparent;
351 351 border: none;
352 352 padding: 0;
353 353 color: @rcblue;
354 354
355 355 &:hover {
356 356 background: transparent;
357 357 border: none;
358 358 color: @rcdarkblue;
359 359 }
360 360
361 361 //disabled buttons
362 362 //last; overrides any other styles
363 363 &:disabled {
364 364 opacity: .7;
365 365 cursor: auto;
366 366 background-color: white;
367 367 color: @grey4;
368 368 text-shadow: none;
369 369 }
370 370
371 371 // TODO: johbo: Check if we can avoid this, indicates that the structure
372 372 // is not yet good.
373 373 // lisa: The button CSS reflects the button HTML; both need a cleanup.
374 374 &.btn-danger {
375 375 color: @alert2;
376 376
377 377 &:hover {
378 378 color: darken(@alert2, 30%);
379 379 }
380 380
381 381 &:disabled {
382 382 color: @alert2;
383 383 }
384 384 }
385 385 }
386 386
387 387 .btn-social {
388 388 &:extend(.btn-default);
389 389 margin: 5px 5px 5px 0px;
390 390 min-width: 160px;
391 391 }
392 392
393 393 // TODO: johbo: check these exceptions
394 394
395 395 .links {
396 396
397 397 .btn + .btn {
398 398 margin-top: @padding;
399 399 }
400 400 }
401 401
402 402
403 403 .action_button {
404 404 display:inline;
405 405 margin: 0;
406 406 padding: 0 1em 0 0;
407 407 font-size: inherit;
408 408 color: @rcblue;
409 409 border: none;
410 410 border-radius: 0;
411 411 background-color: transparent;
412 412
413 413 &.last-item {
414 414 border: none;
415 415 padding: 0 0 0 0;
416 416 }
417 417
418 418 &:last-child {
419 419 border: none;
420 420 padding: 0 0 0 0;
421 421 }
422 422
423 423 &:hover {
424 424 color: @rcdarkblue;
425 425 background-color: transparent;
426 426 border: none;
427 427 }
428 428 .noselect
429 429 }
430 430
431 431 .grid_delete {
432 432 .action_button {
433 433 border: none;
434 434 }
435 435 }
436 436
437 437 input[type="submit"].btn-draft {
438 438 .border ( @border-thickness, @rcblue );
439 439 background-color: white;
440 440 color: @rcblue;
441 441
442 442 a {
443 443 color: @rcblue;
444 444 }
445 445
446 446 &:hover,
447 447 &.active {
448 448 .border ( @border-thickness, @rcdarkblue );
449 449 background-color: white;
450 450 color: @rcdarkblue;
451 451
452 452 a {
453 453 color: @rcdarkblue;
454 454 }
455 455 }
456 456
457 457 &:disabled {
458 458 background-color: white;
459 459 color: @rcblue;
460 460 }
461 461 }
462 462
463 463 input[type="submit"].btn-warning {
464 464 &:extend(.btn-warning);
465 465
466 466 &:focus {
467 467 outline: 0;
468 468 }
469 469
470 470 &:hover {
471 471 &:extend(.btn-warning:hover);
472 472 }
473 473
474 474 &.btn-link {
475 475 &:extend(.btn-link);
476 476 color: @alert3;
477 477
478 478 &:disabled {
479 479 color: @alert3;
480 480 background-color: transparent;
481 481 }
482 482 }
483 483
484 484 &:disabled {
485 485 .border ( @border-thickness-buttons, @alert3 );
486 486 background-color: white;
487 487 color: @alert3;
488 488 opacity: 0.5;
489 489 }
490 490 }
491 491
492 492
493 493
494 494 // TODO: johbo: Form button tweaks, check if we can use the classes instead
495 495 input[type="submit"] {
496 496 &:extend(.btn-primary);
497 497
498 498 &:focus {
499 499 outline: 0;
500 500 }
501 501
502 502 &:hover {
503 503 &:extend(.btn-primary:hover);
504 504 }
505 505
506 506 &.btn-link {
507 507 &:extend(.btn-link);
508 508 color: @rcblue;
509 509
510 510 &:disabled {
511 511 color: @rcblue;
512 512 background-color: transparent;
513 513 }
514 514 }
515 515
516 516 &:disabled {
517 517 .border ( @border-thickness-buttons, @rcblue );
518 518 background-color: @rcblue;
519 519 color: white;
520 520 opacity: 0.5;
521 521 }
522 522 }
523 523
524 524 input[type="reset"] {
525 525 &:extend(.btn-default);
526 526
527 527 // TODO: johbo: Check if this tweak can be avoided.
528 528 background: transparent;
529 529
530 530 &:focus {
531 531 outline: 0;
532 532 }
533 533
534 534 &:hover {
535 535 &:extend(.btn-default:hover);
536 536 }
537 537
538 538 &.btn-link {
539 539 &:extend(.btn-link);
540 540 color: @rcblue;
541 541
542 542 &:disabled {
543 543 border: none;
544 544 }
545 545 }
546 546
547 547 &:disabled {
548 548 .border ( @border-thickness-buttons, @rcblue );
549 549 background-color: white;
550 550 color: @rcblue;
551 551 }
552 552 }
553 553
554 554 input[type="submit"],
555 555 input[type="reset"] {
556 556 &.btn-danger {
557 557 &:extend(.btn-danger);
558 558
559 559 &:focus {
560 560 outline: 0;
561 561 }
562 562
563 563 &:hover {
564 564 &:extend(.btn-danger:hover);
565 565 }
566 566
567 567 &.btn-link {
568 568 &:extend(.btn-link);
569 569 color: @alert2;
570 570
571 571 &:hover {
572 572 color: darken(@alert2,30%);
573 573 }
574 574 }
575 575
576 576 &:disabled {
577 577 color: @alert2;
578 578 background-color: white;
579 579 }
580 580 }
581 581 &.btn-danger-action {
582 582 .border ( @border-thickness, @alert2 );
583 583 background-color: @alert2;
584 584 color: white;
585 585
586 586 a {
587 587 color: white;
588 588 }
589 589
590 590 &:hover {
591 591 background-color: darken(@alert2,20%);
592 592 }
593 593
594 594 &.active {
595 595 .border ( @border-thickness, @alert2 );
596 596 color: white;
597 597 background-color: @alert2;
598 598
599 599 a {
600 600 color: white;
601 601 }
602 602 }
603 603
604 604 &:disabled {
605 605 background-color: white;
606 606 color: @alert2;
607 607 }
608 608 }
609 609 }
610 610
611 611
612 612 .button-links {
613 613 float: left;
614 614 display: inline;
615 615 margin: 0;
616 616 padding-left: 0;
617 617 list-style: none;
618 618 text-align: right;
619 619
620 620 li {
621
622
621 list-style: none;
622 text-align: right;
623 display: inline-block;
623 624 }
624 625
625 li.active {
626 background-color: @grey6;
627 .border ( @border-thickness, @grey4 );
626 a.active {
627 border: 2px solid @rcblue;
628 628 }
629 629
630 630 }
@@ -1,85 +1,85 b''
1 1 // See panels-bootstrap.less
2 2 // These provide overrides for custom styling of Bootstrap panels
3 3
4 4 .panel {
5 5 &:extend(.clearfix);
6 6
7 7 width: 100%;
8 8 margin: 0 0 25px 0;
9 9 .border-radius(@border-radius);
10 10 .box-shadow(none);
11 11
12 12 .permalink {
13 13 visibility: hidden;
14 14 }
15 15
16 16 &:hover .permalink {
17 17 visibility: visible;
18 18 color: @rcblue;
19 19 }
20 20
21 21 .panel-heading {
22 22 position: relative;
23 23 min-height: 1em;
24 24 padding: @padding @panel-padding;
25 25 border-bottom: none;
26 26
27 27 .panel-title,
28 28 h3.panel-title {
29 29 float: left;
30 30 padding: 0 @padding 0 0;
31 31 line-height: 1;
32 32 font-size: @panel-title;
33 33 color: @grey1;
34 34 }
35 35
36 36 .panel-edit {
37 37 float: right;
38 38 line-height: 1;
39 39 font-size: @panel-title;
40 40 }
41 41 }
42 42
43 43 .panel-body {
44 44 padding: @panel-padding;
45 45
46 46 &.panel-body-min-height {
47 min-height: 150px
47 min-height: 200px
48 48 }
49 49 }
50 50
51 51 .panel-footer {
52 52 background-color: white;
53 53 padding: .65em @panel-padding .5em;
54 54 font-size: @panel-footer;
55 55 color: @text-muted;
56 56 }
57 57
58 58 .q_filter_box {
59 59 min-width: 40%;
60 60 }
61 61
62 62 // special cases
63 63 &.user-profile {
64 64 float: left;
65 65
66 66 .panel-body {
67 67 &:extend(.clearfix);
68 68 }
69 69 }
70 70 }
71 71
72 72 .main-content h3.panel-title {
73 73 font-size: @panel-title;
74 74 color: @grey1;
75 75 }
76 76
77 77 .panel-body-title-text {
78 78 margin: 0 0 20px 0;
79 79 }
80 80
81 81 // play nice with the current form and field css
82 82 .field.panel-default,
83 83 .form.panel-default {
84 84 width: auto;
85 85 } No newline at end of file
@@ -1,151 +1,164 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default">
4 <div class="panel-body">
5 <div style="height: 35px">
6 <%
7 selected_filter = 'all'
8 if c.closed:
9 selected_filter = 'all_closed'
10 %>
4 <div class="panel-heading">
5 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
6 </div>
11 7
8 <div class="panel-body panel-body-min-height">
9 <div class="title">
12 10 <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>
11 <li><a class="btn ${h.is_active('all', c.selected_filter)}"
12 href="${h.route_path('my_account_pullrequests', _query={})}">
13 ${_('Open')}
14 </a>
15 </li>
16 <li><a class="btn ${h.is_active('all_closed', c.selected_filter)}"
17 href="${h.route_path('my_account_pullrequests', _query={'closed':1})}">
18 ${_('All + Closed')}
19 </a>
20 </li>
21 <li><a class="btn ${h.is_active('awaiting_my_review', c.selected_filter)}"
22 href="${h.route_path('my_account_pullrequests', _query={'awaiting_my_review':1})}">
23
24 ${_('Awaiting my review')}
25 </a>
26 </li>
15 27 </ul>
16 28
17 29 <div class="grid-quick-filter">
18 30 <ul class="grid-filter-box">
19 31 <li class="grid-filter-box-icon">
20 32 <i class="icon-search"></i>
21 33 </li>
22 34 <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=""/>
35 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter"
36 placeholder="${_('quick filter...')}" value=""/>
24 37 </li>
25 38 </ul>
26 39 </div>
27 40 </div>
28 </div>
29 </div>
30 41
31 <div class="panel panel-default">
32 <div class="panel-heading">
33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
34 </div>
35 <div class="panel-body panel-body-min-height">
36 42 <table id="pull_request_list_table" class="rctable table-bordered"></table>
37 43 </div>
38 44 </div>
39 45
40 46 <script type="text/javascript">
41 47 $(document).ready(function () {
42 48
43 49 var $pullRequestListTable = $('#pull_request_list_table');
44 50
45 51 // participating object list
46 52 $pullRequestListTable.DataTable({
47 53 processing: true,
48 54 serverSide: true,
49 55 stateSave: true,
50 56 stateDuration: -1,
51 57 ajax: {
52 58 "url": "${h.route_path('my_account_pullrequests_data')}",
53 59 "data": function (d) {
54 60 d.closed = "${c.closed}";
61 d.awaiting_my_review = "${c.awaiting_my_review}";
55 62 },
56 63 "dataSrc": function (json) {
57 64 return json.data;
58 65 }
59 66 },
60 67
61 68 dom: 'rtp',
62 69 pageLength: ${c.visual.dashboard_items},
63 order: [[1, "desc"]],
70 order: [[2, "desc"]],
64 71 columns: [
65 72 {
66 73 data: {
67 74 "_": "status",
68 75 "sort": "status"
69 }, title: "", className: "td-status", orderable: false
76 }, title: "PR", className: "td-status", orderable: false
77 },
78 {
79 data: {
80 "_": "my_status",
81 "sort": "status"
82 }, title: "You", className: "td-status", orderable: false
70 83 },
71 84 {
72 85 data: {
73 86 "_": "name",
74 87 "sort": "name_raw"
75 88 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
76 89 },
77 90 {
78 91 data: {
79 92 "_": "title",
80 93 "sort": "title"
81 94 }, title: "${_('Title')}", className: "td-description"
82 95 },
83 96 {
84 97 data: {
85 98 "_": "author",
86 99 "sort": "author_raw"
87 100 }, title: "${_('Author')}", className: "td-user", orderable: false
88 101 },
89 102 {
90 103 data: {
91 104 "_": "comments",
92 105 "sort": "comments_raw"
93 106 }, title: "", className: "td-comments", orderable: false
94 107 },
95 108 {
96 109 data: {
97 110 "_": "updated_on",
98 111 "sort": "updated_on_raw"
99 112 }, title: "${_('Last Update')}", className: "td-time"
100 113 },
101 114 {
102 115 data: {
103 116 "_": "target_repo",
104 117 "sort": "target_repo"
105 118 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
106 119 },
107 120 ],
108 121 language: {
109 122 paginate: DEFAULT_GRID_PAGINATION,
110 123 sProcessing: _gettext('loading...'),
111 124 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
112 125 },
113 126 "drawCallback": function (settings, json) {
114 127 timeagoActivate();
115 128 tooltipActivate();
116 129 },
117 130 "createdRow": function (row, data, index) {
118 131 if (data['closed']) {
119 132 $(row).addClass('closed');
120 133 }
121 134 if (data['owned']) {
122 135 $(row).addClass('owned');
123 136 }
124 137 },
125 138 "stateSaveParams": function (settings, data) {
126 139 data.search.search = ""; // Don't save search
127 140 data.start = 0; // don't save pagination
128 141 }
129 142 });
130 143 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
131 144 $pullRequestListTable.css('opacity', 1);
132 145 });
133 146
134 147 $pullRequestListTable.on('preXhr.dt', function (e, settings, data) {
135 148 $pullRequestListTable.css('opacity', 0.3);
136 149 });
137 150
138 151
139 152 // filter
140 153 $('#q_filter').on('keyup',
141 154 $.debounce(250, function () {
142 155 $pullRequestListTable.DataTable().search(
143 156 $('#q_filter').val()
144 157 ).draw();
145 158 })
146 159 );
147 160
148 161 });
149 162
150 163
151 164 </script>
@@ -1,146 +1,177 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('{} Pull Requests').format(c.repo_name)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()"></%def>
11 11
12 12 <%def name="menu_bar_nav()">
13 13 ${self.menu_items(active='repositories')}
14 14 </%def>
15 15
16 16
17 17 <%def name="menu_bar_subnav()">
18 18 ${self.repo_menu(active='showpullrequest')}
19 19 </%def>
20 20
21 21
22 22 <%def name="main()">
23 23
24 24 <div class="box">
25 25 <div class="title">
26
26 27 <ul class="button-links">
27 <li class="btn ${h.is_active('open', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
28 <li class="btn ${h.is_active('my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
29 <li class="btn ${h.is_active('awaiting', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
30 <li class="btn ${h.is_active('awaiting_my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
31 <li class="btn ${h.is_active('closed', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
32 <li class="btn ${h.is_active('source', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
28 <li><a class="btn ${h.is_active('open', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'open':1})}">${_('Open')}</a></li>
29 <li><a class="btn ${h.is_active('my', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Created by me')}</a></li>
30 <li><a class="btn ${h.is_active('awaiting', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
31 <li><a class="btn ${h.is_active('awaiting_my', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
32 <li><a class="btn ${h.is_active('closed', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
33 <li><a class="btn ${h.is_active('source', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 34 </ul>
34 35
35 36 <ul class="links">
36 37 % if c.rhodecode_user.username != h.DEFAULT_USER:
37 38 <li>
38 39 <span>
39 40 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
40 41 ${_('Open new Pull Request')}
41 42 </a>
42 43 </span>
43 44 </li>
44 45 % endif
45 46
46 47 <li>
47 48 <div class="grid-quick-filter">
48 49 <ul class="grid-filter-box">
49 50 <li class="grid-filter-box-icon">
50 51 <i class="icon-search"></i>
51 52 </li>
52 53 <li class="grid-filter-box-input">
53 54 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
54 55 </li>
55 56 </ul>
56 57 </div>
57 58 </li>
58 59
59 60 </ul>
60 61
61 62 </div>
62 63
63 64 <div class="main-content-full-width">
64 65 <table id="pull_request_list_table" class="rctable table-bordered"></table>
65 66 </div>
66 67
67 68 </div>
68 69
69 70 <script type="text/javascript">
70 71 $(document).ready(function() {
71 72 var $pullRequestListTable = $('#pull_request_list_table');
72 73
73 74 // object list
74 75 $pullRequestListTable.DataTable({
75 76 processing: true,
76 77 serverSide: true,
77 78 stateSave: true,
78 79 stateDuration: -1,
79 80 ajax: {
80 81 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
81 82 "data": function (d) {
82 83 d.source = "${c.source}";
83 84 d.closed = "${c.closed}";
84 85 d.my = "${c.my}";
85 86 d.awaiting_review = "${c.awaiting_review}";
86 87 d.awaiting_my_review = "${c.awaiting_my_review}";
87 88 }
88 89 },
89 90 dom: 'rtp',
90 91 pageLength: ${c.visual.dashboard_items},
91 order: [[ 1, "desc" ]],
92 order: [[ 2, "desc" ]],
92 93 columns: [
93 { data: {"_": "status",
94 "sort": "status"}, title: "", className: "td-status", orderable: false},
95 { data: {"_": "name",
96 "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" },
97 { data: {"_": "title",
98 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
99 { data: {"_": "author",
100 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
101 { data: {"_": "comments",
102 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
103 { data: {"_": "updated_on",
104 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
94 {
95 data: {
96 "_": "status",
97 "sort": "status"
98 }, title: "PR", className: "td-status", orderable: false
99 },
100 {
101 data: {
102 "_": "my_status",
103 "sort": "status"
104 }, title: "You", className: "td-status", orderable: false
105 },
106 {
107 data: {
108 "_": "name",
109 "sort": "name_raw"
110 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
111 },
112 {
113 data: {
114 "_": "title",
115 "sort": "title"
116 }, title: "${_('Title')}", className: "td-description"
117 },
118 {
119 data: {
120 "_": "author",
121 "sort": "author_raw"
122 }, title: "${_('Author')}", className: "td-user", orderable: false
123 },
124 {
125 data: {
126 "_": "comments",
127 "sort": "comments_raw"
128 }, title: "", className: "td-comments", orderable: false
129 },
130 {
131 data: {
132 "_": "updated_on",
133 "sort": "updated_on_raw"
134 }, title: "${_('Last Update')}", className: "td-time"
135 }
105 136 ],
106 137 language: {
107 138 paginate: DEFAULT_GRID_PAGINATION,
108 139 sProcessing: _gettext('loading...'),
109 140 emptyTable: _gettext("No pull requests available yet.")
110 141 },
111 142 "drawCallback": function( settings, json ) {
112 143 timeagoActivate();
113 144 tooltipActivate();
114 145 },
115 146 "createdRow": function ( row, data, index ) {
116 147 if (data['closed']) {
117 148 $(row).addClass('closed');
118 149 }
119 150 },
120 151 "stateSaveParams": function (settings, data) {
121 152 data.search.search = ""; // Don't save search
122 153 data.start = 0; // don't save pagination
123 154 }
124 155 });
125 156
126 157 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
127 158 $pullRequestListTable.css('opacity', 1);
128 159 });
129 160
130 161 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
131 162 $pullRequestListTable.css('opacity', 0.3);
132 163 });
133 164
134 165 // filter
135 166 $('#q_filter').on('keyup',
136 167 $.debounce(250, function() {
137 168 $pullRequestListTable.DataTable().search(
138 169 $('#q_filter').val()
139 170 ).draw();
140 171 })
141 172 );
142 173
143 174 });
144 175
145 176 </script>
146 177 </%def>
General Comments 1
Under Review
author

Auto status change to "Under Review"

You need to be logged in to leave comments. Login now