##// END OF EJS Templates
pull-requests: add indication of state change in list of pull-requests and actually show them in the list.
dan -
r3816:18ef8156 stable
parent child Browse files
Show More
@@ -1,743 +1,743 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import string
23 import string
24
24
25 import formencode
25 import formencode
26 import formencode.htmlfill
26 import formencode.htmlfill
27 import peppercorn
27 import peppercorn
28 from pyramid.httpexceptions import HTTPFound
28 from pyramid.httpexceptions import HTTPFound
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30
30
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode import forms
32 from rhodecode import forms
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.lib import audit_logger
34 from rhodecode.lib import audit_logger
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, \
36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, \
37 HasRepoPermissionAny, HasRepoGroupPermissionAny
37 HasRepoPermissionAny, HasRepoGroupPermissionAny
38 from rhodecode.lib.channelstream import (
38 from rhodecode.lib.channelstream import (
39 channelstream_request, ChannelstreamException)
39 channelstream_request, ChannelstreamException)
40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 IntegrityError, joinedload,
44 IntegrityError, joinedload,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 PullRequest, UserBookmark, RepoGroup)
46 PullRequest, UserBookmark, RepoGroup)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.scm import RepoList
49 from rhodecode.model.scm import RepoList
50 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52 from rhodecode.model.user_group import UserGroupModel
52 from rhodecode.model.user_group import UserGroupModel
53 from rhodecode.model.validation_schema.schemas import user_schema
53 from rhodecode.model.validation_schema.schemas import user_schema
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class MyAccountView(BaseAppView, DataGridAppView):
58 class MyAccountView(BaseAppView, DataGridAppView):
59 ALLOW_SCOPED_TOKENS = False
59 ALLOW_SCOPED_TOKENS = False
60 """
60 """
61 This view has alternative version inside EE, if modified please take a look
61 This view has alternative version inside EE, if modified please take a look
62 in there as well.
62 in there as well.
63 """
63 """
64
64
65 def load_default_context(self):
65 def load_default_context(self):
66 c = self._get_local_tmpl_context()
66 c = self._get_local_tmpl_context()
67 c.user = c.auth_user.get_instance()
67 c.user = c.auth_user.get_instance()
68 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
69
69
70 return c
70 return c
71
71
72 @LoginRequired()
72 @LoginRequired()
73 @NotAnonymous()
73 @NotAnonymous()
74 @view_config(
74 @view_config(
75 route_name='my_account_profile', request_method='GET',
75 route_name='my_account_profile', request_method='GET',
76 renderer='rhodecode:templates/admin/my_account/my_account.mako')
76 renderer='rhodecode:templates/admin/my_account/my_account.mako')
77 def my_account_profile(self):
77 def my_account_profile(self):
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.active = 'profile'
79 c.active = 'profile'
80 return self._get_template_context(c)
80 return self._get_template_context(c)
81
81
82 @LoginRequired()
82 @LoginRequired()
83 @NotAnonymous()
83 @NotAnonymous()
84 @view_config(
84 @view_config(
85 route_name='my_account_password', request_method='GET',
85 route_name='my_account_password', request_method='GET',
86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
87 def my_account_password(self):
87 def my_account_password(self):
88 c = self.load_default_context()
88 c = self.load_default_context()
89 c.active = 'password'
89 c.active = 'password'
90 c.extern_type = c.user.extern_type
90 c.extern_type = c.user.extern_type
91
91
92 schema = user_schema.ChangePasswordSchema().bind(
92 schema = user_schema.ChangePasswordSchema().bind(
93 username=c.user.username)
93 username=c.user.username)
94
94
95 form = forms.Form(
95 form = forms.Form(
96 schema,
96 schema,
97 action=h.route_path('my_account_password_update'),
97 action=h.route_path('my_account_password_update'),
98 buttons=(forms.buttons.save, forms.buttons.reset))
98 buttons=(forms.buttons.save, forms.buttons.reset))
99
99
100 c.form = form
100 c.form = form
101 return self._get_template_context(c)
101 return self._get_template_context(c)
102
102
103 @LoginRequired()
103 @LoginRequired()
104 @NotAnonymous()
104 @NotAnonymous()
105 @CSRFRequired()
105 @CSRFRequired()
106 @view_config(
106 @view_config(
107 route_name='my_account_password_update', request_method='POST',
107 route_name='my_account_password_update', request_method='POST',
108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
109 def my_account_password_update(self):
109 def my_account_password_update(self):
110 _ = self.request.translate
110 _ = self.request.translate
111 c = self.load_default_context()
111 c = self.load_default_context()
112 c.active = 'password'
112 c.active = 'password'
113 c.extern_type = c.user.extern_type
113 c.extern_type = c.user.extern_type
114
114
115 schema = user_schema.ChangePasswordSchema().bind(
115 schema = user_schema.ChangePasswordSchema().bind(
116 username=c.user.username)
116 username=c.user.username)
117
117
118 form = forms.Form(
118 form = forms.Form(
119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
120
120
121 if c.extern_type != 'rhodecode':
121 if c.extern_type != 'rhodecode':
122 raise HTTPFound(self.request.route_path('my_account_password'))
122 raise HTTPFound(self.request.route_path('my_account_password'))
123
123
124 controls = self.request.POST.items()
124 controls = self.request.POST.items()
125 try:
125 try:
126 valid_data = form.validate(controls)
126 valid_data = form.validate(controls)
127 UserModel().update_user(c.user.user_id, **valid_data)
127 UserModel().update_user(c.user.user_id, **valid_data)
128 c.user.update_userdata(force_password_change=False)
128 c.user.update_userdata(force_password_change=False)
129 Session().commit()
129 Session().commit()
130 except forms.ValidationFailure as e:
130 except forms.ValidationFailure as e:
131 c.form = e
131 c.form = e
132 return self._get_template_context(c)
132 return self._get_template_context(c)
133
133
134 except Exception:
134 except Exception:
135 log.exception("Exception updating password")
135 log.exception("Exception updating password")
136 h.flash(_('Error occurred during update of user password'),
136 h.flash(_('Error occurred during update of user password'),
137 category='error')
137 category='error')
138 else:
138 else:
139 instance = c.auth_user.get_instance()
139 instance = c.auth_user.get_instance()
140 self.session.setdefault('rhodecode_user', {}).update(
140 self.session.setdefault('rhodecode_user', {}).update(
141 {'password': md5(instance.password)})
141 {'password': md5(instance.password)})
142 self.session.save()
142 self.session.save()
143 h.flash(_("Successfully updated password"), category='success')
143 h.flash(_("Successfully updated password"), category='success')
144
144
145 raise HTTPFound(self.request.route_path('my_account_password'))
145 raise HTTPFound(self.request.route_path('my_account_password'))
146
146
147 @LoginRequired()
147 @LoginRequired()
148 @NotAnonymous()
148 @NotAnonymous()
149 @view_config(
149 @view_config(
150 route_name='my_account_auth_tokens', request_method='GET',
150 route_name='my_account_auth_tokens', request_method='GET',
151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
152 def my_account_auth_tokens(self):
152 def my_account_auth_tokens(self):
153 _ = self.request.translate
153 _ = self.request.translate
154
154
155 c = self.load_default_context()
155 c = self.load_default_context()
156 c.active = 'auth_tokens'
156 c.active = 'auth_tokens'
157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
158 c.role_values = [
158 c.role_values = [
159 (x, AuthTokenModel.cls._get_role_name(x))
159 (x, AuthTokenModel.cls._get_role_name(x))
160 for x in AuthTokenModel.cls.ROLES]
160 for x in AuthTokenModel.cls.ROLES]
161 c.role_options = [(c.role_values, _("Role"))]
161 c.role_options = [(c.role_values, _("Role"))]
162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
163 c.user.user_id, show_expired=True)
163 c.user.user_id, show_expired=True)
164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
165 return self._get_template_context(c)
165 return self._get_template_context(c)
166
166
167 def maybe_attach_token_scope(self, token):
167 def maybe_attach_token_scope(self, token):
168 # implemented in EE edition
168 # implemented in EE edition
169 pass
169 pass
170
170
171 @LoginRequired()
171 @LoginRequired()
172 @NotAnonymous()
172 @NotAnonymous()
173 @CSRFRequired()
173 @CSRFRequired()
174 @view_config(
174 @view_config(
175 route_name='my_account_auth_tokens_add', request_method='POST',)
175 route_name='my_account_auth_tokens_add', request_method='POST',)
176 def my_account_auth_tokens_add(self):
176 def my_account_auth_tokens_add(self):
177 _ = self.request.translate
177 _ = self.request.translate
178 c = self.load_default_context()
178 c = self.load_default_context()
179
179
180 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
180 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
181 description = self.request.POST.get('description')
181 description = self.request.POST.get('description')
182 role = self.request.POST.get('role')
182 role = self.request.POST.get('role')
183
183
184 token = UserModel().add_auth_token(
184 token = UserModel().add_auth_token(
185 user=c.user.user_id,
185 user=c.user.user_id,
186 lifetime_minutes=lifetime, role=role, description=description,
186 lifetime_minutes=lifetime, role=role, description=description,
187 scope_callback=self.maybe_attach_token_scope)
187 scope_callback=self.maybe_attach_token_scope)
188 token_data = token.get_api_data()
188 token_data = token.get_api_data()
189
189
190 audit_logger.store_web(
190 audit_logger.store_web(
191 'user.edit.token.add', action_data={
191 'user.edit.token.add', action_data={
192 'data': {'token': token_data, 'user': 'self'}},
192 'data': {'token': token_data, 'user': 'self'}},
193 user=self._rhodecode_user, )
193 user=self._rhodecode_user, )
194 Session().commit()
194 Session().commit()
195
195
196 h.flash(_("Auth token successfully created"), category='success')
196 h.flash(_("Auth token successfully created"), category='success')
197 return HTTPFound(h.route_path('my_account_auth_tokens'))
197 return HTTPFound(h.route_path('my_account_auth_tokens'))
198
198
199 @LoginRequired()
199 @LoginRequired()
200 @NotAnonymous()
200 @NotAnonymous()
201 @CSRFRequired()
201 @CSRFRequired()
202 @view_config(
202 @view_config(
203 route_name='my_account_auth_tokens_delete', request_method='POST')
203 route_name='my_account_auth_tokens_delete', request_method='POST')
204 def my_account_auth_tokens_delete(self):
204 def my_account_auth_tokens_delete(self):
205 _ = self.request.translate
205 _ = self.request.translate
206 c = self.load_default_context()
206 c = self.load_default_context()
207
207
208 del_auth_token = self.request.POST.get('del_auth_token')
208 del_auth_token = self.request.POST.get('del_auth_token')
209
209
210 if del_auth_token:
210 if del_auth_token:
211 token = UserApiKeys.get_or_404(del_auth_token)
211 token = UserApiKeys.get_or_404(del_auth_token)
212 token_data = token.get_api_data()
212 token_data = token.get_api_data()
213
213
214 AuthTokenModel().delete(del_auth_token, c.user.user_id)
214 AuthTokenModel().delete(del_auth_token, c.user.user_id)
215 audit_logger.store_web(
215 audit_logger.store_web(
216 'user.edit.token.delete', action_data={
216 'user.edit.token.delete', action_data={
217 'data': {'token': token_data, 'user': 'self'}},
217 'data': {'token': token_data, 'user': 'self'}},
218 user=self._rhodecode_user,)
218 user=self._rhodecode_user,)
219 Session().commit()
219 Session().commit()
220 h.flash(_("Auth token successfully deleted"), category='success')
220 h.flash(_("Auth token successfully deleted"), category='success')
221
221
222 return HTTPFound(h.route_path('my_account_auth_tokens'))
222 return HTTPFound(h.route_path('my_account_auth_tokens'))
223
223
224 @LoginRequired()
224 @LoginRequired()
225 @NotAnonymous()
225 @NotAnonymous()
226 @view_config(
226 @view_config(
227 route_name='my_account_emails', request_method='GET',
227 route_name='my_account_emails', request_method='GET',
228 renderer='rhodecode:templates/admin/my_account/my_account.mako')
228 renderer='rhodecode:templates/admin/my_account/my_account.mako')
229 def my_account_emails(self):
229 def my_account_emails(self):
230 _ = self.request.translate
230 _ = self.request.translate
231
231
232 c = self.load_default_context()
232 c = self.load_default_context()
233 c.active = 'emails'
233 c.active = 'emails'
234
234
235 c.user_email_map = UserEmailMap.query()\
235 c.user_email_map = UserEmailMap.query()\
236 .filter(UserEmailMap.user == c.user).all()
236 .filter(UserEmailMap.user == c.user).all()
237
237
238 schema = user_schema.AddEmailSchema().bind(
238 schema = user_schema.AddEmailSchema().bind(
239 username=c.user.username, user_emails=c.user.emails)
239 username=c.user.username, user_emails=c.user.emails)
240
240
241 form = forms.RcForm(schema,
241 form = forms.RcForm(schema,
242 action=h.route_path('my_account_emails_add'),
242 action=h.route_path('my_account_emails_add'),
243 buttons=(forms.buttons.save, forms.buttons.reset))
243 buttons=(forms.buttons.save, forms.buttons.reset))
244
244
245 c.form = form
245 c.form = form
246 return self._get_template_context(c)
246 return self._get_template_context(c)
247
247
248 @LoginRequired()
248 @LoginRequired()
249 @NotAnonymous()
249 @NotAnonymous()
250 @CSRFRequired()
250 @CSRFRequired()
251 @view_config(
251 @view_config(
252 route_name='my_account_emails_add', request_method='POST',
252 route_name='my_account_emails_add', request_method='POST',
253 renderer='rhodecode:templates/admin/my_account/my_account.mako')
253 renderer='rhodecode:templates/admin/my_account/my_account.mako')
254 def my_account_emails_add(self):
254 def my_account_emails_add(self):
255 _ = self.request.translate
255 _ = self.request.translate
256 c = self.load_default_context()
256 c = self.load_default_context()
257 c.active = 'emails'
257 c.active = 'emails'
258
258
259 schema = user_schema.AddEmailSchema().bind(
259 schema = user_schema.AddEmailSchema().bind(
260 username=c.user.username, user_emails=c.user.emails)
260 username=c.user.username, user_emails=c.user.emails)
261
261
262 form = forms.RcForm(
262 form = forms.RcForm(
263 schema, action=h.route_path('my_account_emails_add'),
263 schema, action=h.route_path('my_account_emails_add'),
264 buttons=(forms.buttons.save, forms.buttons.reset))
264 buttons=(forms.buttons.save, forms.buttons.reset))
265
265
266 controls = self.request.POST.items()
266 controls = self.request.POST.items()
267 try:
267 try:
268 valid_data = form.validate(controls)
268 valid_data = form.validate(controls)
269 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
269 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
270 audit_logger.store_web(
270 audit_logger.store_web(
271 'user.edit.email.add', action_data={
271 'user.edit.email.add', action_data={
272 'data': {'email': valid_data['email'], 'user': 'self'}},
272 'data': {'email': valid_data['email'], 'user': 'self'}},
273 user=self._rhodecode_user,)
273 user=self._rhodecode_user,)
274 Session().commit()
274 Session().commit()
275 except formencode.Invalid as error:
275 except formencode.Invalid as error:
276 h.flash(h.escape(error.error_dict['email']), category='error')
276 h.flash(h.escape(error.error_dict['email']), category='error')
277 except forms.ValidationFailure as e:
277 except forms.ValidationFailure as e:
278 c.user_email_map = UserEmailMap.query() \
278 c.user_email_map = UserEmailMap.query() \
279 .filter(UserEmailMap.user == c.user).all()
279 .filter(UserEmailMap.user == c.user).all()
280 c.form = e
280 c.form = e
281 return self._get_template_context(c)
281 return self._get_template_context(c)
282 except Exception:
282 except Exception:
283 log.exception("Exception adding email")
283 log.exception("Exception adding email")
284 h.flash(_('Error occurred during adding email'),
284 h.flash(_('Error occurred during adding email'),
285 category='error')
285 category='error')
286 else:
286 else:
287 h.flash(_("Successfully added email"), category='success')
287 h.flash(_("Successfully added email"), category='success')
288
288
289 raise HTTPFound(self.request.route_path('my_account_emails'))
289 raise HTTPFound(self.request.route_path('my_account_emails'))
290
290
291 @LoginRequired()
291 @LoginRequired()
292 @NotAnonymous()
292 @NotAnonymous()
293 @CSRFRequired()
293 @CSRFRequired()
294 @view_config(
294 @view_config(
295 route_name='my_account_emails_delete', request_method='POST')
295 route_name='my_account_emails_delete', request_method='POST')
296 def my_account_emails_delete(self):
296 def my_account_emails_delete(self):
297 _ = self.request.translate
297 _ = self.request.translate
298 c = self.load_default_context()
298 c = self.load_default_context()
299
299
300 del_email_id = self.request.POST.get('del_email_id')
300 del_email_id = self.request.POST.get('del_email_id')
301 if del_email_id:
301 if del_email_id:
302 email = UserEmailMap.get_or_404(del_email_id).email
302 email = UserEmailMap.get_or_404(del_email_id).email
303 UserModel().delete_extra_email(c.user.user_id, del_email_id)
303 UserModel().delete_extra_email(c.user.user_id, del_email_id)
304 audit_logger.store_web(
304 audit_logger.store_web(
305 'user.edit.email.delete', action_data={
305 'user.edit.email.delete', action_data={
306 'data': {'email': email, 'user': 'self'}},
306 'data': {'email': email, 'user': 'self'}},
307 user=self._rhodecode_user,)
307 user=self._rhodecode_user,)
308 Session().commit()
308 Session().commit()
309 h.flash(_("Email successfully deleted"),
309 h.flash(_("Email successfully deleted"),
310 category='success')
310 category='success')
311 return HTTPFound(h.route_path('my_account_emails'))
311 return HTTPFound(h.route_path('my_account_emails'))
312
312
313 @LoginRequired()
313 @LoginRequired()
314 @NotAnonymous()
314 @NotAnonymous()
315 @CSRFRequired()
315 @CSRFRequired()
316 @view_config(
316 @view_config(
317 route_name='my_account_notifications_test_channelstream',
317 route_name='my_account_notifications_test_channelstream',
318 request_method='POST', renderer='json_ext')
318 request_method='POST', renderer='json_ext')
319 def my_account_notifications_test_channelstream(self):
319 def my_account_notifications_test_channelstream(self):
320 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
320 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
321 self._rhodecode_user.username, datetime.datetime.now())
321 self._rhodecode_user.username, datetime.datetime.now())
322 payload = {
322 payload = {
323 # 'channel': 'broadcast',
323 # 'channel': 'broadcast',
324 'type': 'message',
324 'type': 'message',
325 'timestamp': datetime.datetime.utcnow(),
325 'timestamp': datetime.datetime.utcnow(),
326 'user': 'system',
326 'user': 'system',
327 'pm_users': [self._rhodecode_user.username],
327 'pm_users': [self._rhodecode_user.username],
328 'message': {
328 'message': {
329 'message': message,
329 'message': message,
330 'level': 'info',
330 'level': 'info',
331 'topic': '/notifications'
331 'topic': '/notifications'
332 }
332 }
333 }
333 }
334
334
335 registry = self.request.registry
335 registry = self.request.registry
336 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
336 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
337 channelstream_config = rhodecode_plugins.get('channelstream', {})
337 channelstream_config = rhodecode_plugins.get('channelstream', {})
338
338
339 try:
339 try:
340 channelstream_request(channelstream_config, [payload], '/message')
340 channelstream_request(channelstream_config, [payload], '/message')
341 except ChannelstreamException as e:
341 except ChannelstreamException as e:
342 log.exception('Failed to send channelstream data')
342 log.exception('Failed to send channelstream data')
343 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
343 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
344 return {"response": 'Channelstream data sent. '
344 return {"response": 'Channelstream data sent. '
345 'You should see a new live message now.'}
345 'You should see a new live message now.'}
346
346
347 def _load_my_repos_data(self, watched=False):
347 def _load_my_repos_data(self, watched=False):
348 if watched:
348 if watched:
349 admin = False
349 admin = False
350 follows_repos = Session().query(UserFollowing)\
350 follows_repos = Session().query(UserFollowing)\
351 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
351 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
352 .options(joinedload(UserFollowing.follows_repository))\
352 .options(joinedload(UserFollowing.follows_repository))\
353 .all()
353 .all()
354 repo_list = [x.follows_repository for x in follows_repos]
354 repo_list = [x.follows_repository for x in follows_repos]
355 else:
355 else:
356 admin = True
356 admin = True
357 repo_list = Repository.get_all_repos(
357 repo_list = Repository.get_all_repos(
358 user_id=self._rhodecode_user.user_id)
358 user_id=self._rhodecode_user.user_id)
359 repo_list = RepoList(repo_list, perm_set=[
359 repo_list = RepoList(repo_list, perm_set=[
360 'repository.read', 'repository.write', 'repository.admin'])
360 'repository.read', 'repository.write', 'repository.admin'])
361
361
362 repos_data = RepoModel().get_repos_as_dict(
362 repos_data = RepoModel().get_repos_as_dict(
363 repo_list=repo_list, admin=admin, short_name=False)
363 repo_list=repo_list, admin=admin, short_name=False)
364 # json used to render the grid
364 # json used to render the grid
365 return json.dumps(repos_data)
365 return json.dumps(repos_data)
366
366
367 @LoginRequired()
367 @LoginRequired()
368 @NotAnonymous()
368 @NotAnonymous()
369 @view_config(
369 @view_config(
370 route_name='my_account_repos', request_method='GET',
370 route_name='my_account_repos', request_method='GET',
371 renderer='rhodecode:templates/admin/my_account/my_account.mako')
371 renderer='rhodecode:templates/admin/my_account/my_account.mako')
372 def my_account_repos(self):
372 def my_account_repos(self):
373 c = self.load_default_context()
373 c = self.load_default_context()
374 c.active = 'repos'
374 c.active = 'repos'
375
375
376 # json used to render the grid
376 # json used to render the grid
377 c.data = self._load_my_repos_data()
377 c.data = self._load_my_repos_data()
378 return self._get_template_context(c)
378 return self._get_template_context(c)
379
379
380 @LoginRequired()
380 @LoginRequired()
381 @NotAnonymous()
381 @NotAnonymous()
382 @view_config(
382 @view_config(
383 route_name='my_account_watched', request_method='GET',
383 route_name='my_account_watched', request_method='GET',
384 renderer='rhodecode:templates/admin/my_account/my_account.mako')
384 renderer='rhodecode:templates/admin/my_account/my_account.mako')
385 def my_account_watched(self):
385 def my_account_watched(self):
386 c = self.load_default_context()
386 c = self.load_default_context()
387 c.active = 'watched'
387 c.active = 'watched'
388
388
389 # json used to render the grid
389 # json used to render the grid
390 c.data = self._load_my_repos_data(watched=True)
390 c.data = self._load_my_repos_data(watched=True)
391 return self._get_template_context(c)
391 return self._get_template_context(c)
392
392
393 @LoginRequired()
393 @LoginRequired()
394 @NotAnonymous()
394 @NotAnonymous()
395 @view_config(
395 @view_config(
396 route_name='my_account_bookmarks', request_method='GET',
396 route_name='my_account_bookmarks', request_method='GET',
397 renderer='rhodecode:templates/admin/my_account/my_account.mako')
397 renderer='rhodecode:templates/admin/my_account/my_account.mako')
398 def my_account_bookmarks(self):
398 def my_account_bookmarks(self):
399 c = self.load_default_context()
399 c = self.load_default_context()
400 c.active = 'bookmarks'
400 c.active = 'bookmarks'
401 return self._get_template_context(c)
401 return self._get_template_context(c)
402
402
403 def _process_entry(self, entry, user_id):
403 def _process_entry(self, entry, user_id):
404 position = safe_int(entry.get('position'))
404 position = safe_int(entry.get('position'))
405 if position is None:
405 if position is None:
406 return
406 return
407
407
408 # check if this is an existing entry
408 # check if this is an existing entry
409 is_new = False
409 is_new = False
410 db_entry = UserBookmark().get_by_position_for_user(position, user_id)
410 db_entry = UserBookmark().get_by_position_for_user(position, user_id)
411
411
412 if db_entry and str2bool(entry.get('remove')):
412 if db_entry and str2bool(entry.get('remove')):
413 log.debug('Marked bookmark %s for deletion', db_entry)
413 log.debug('Marked bookmark %s for deletion', db_entry)
414 Session().delete(db_entry)
414 Session().delete(db_entry)
415 return
415 return
416
416
417 if not db_entry:
417 if not db_entry:
418 # new
418 # new
419 db_entry = UserBookmark()
419 db_entry = UserBookmark()
420 is_new = True
420 is_new = True
421
421
422 should_save = False
422 should_save = False
423 default_redirect_url = ''
423 default_redirect_url = ''
424
424
425 # save repo
425 # save repo
426 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
426 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
427 repo = Repository.get(entry['bookmark_repo'])
427 repo = Repository.get(entry['bookmark_repo'])
428 perm_check = HasRepoPermissionAny(
428 perm_check = HasRepoPermissionAny(
429 'repository.read', 'repository.write', 'repository.admin')
429 'repository.read', 'repository.write', 'repository.admin')
430 if repo and perm_check(repo_name=repo.repo_name):
430 if repo and perm_check(repo_name=repo.repo_name):
431 db_entry.repository = repo
431 db_entry.repository = repo
432 should_save = True
432 should_save = True
433 default_redirect_url = '${repo_url}'
433 default_redirect_url = '${repo_url}'
434 # save repo group
434 # save repo group
435 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
435 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
436 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
436 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
437 perm_check = HasRepoGroupPermissionAny(
437 perm_check = HasRepoGroupPermissionAny(
438 'group.read', 'group.write', 'group.admin')
438 'group.read', 'group.write', 'group.admin')
439
439
440 if repo_group and perm_check(group_name=repo_group.group_name):
440 if repo_group and perm_check(group_name=repo_group.group_name):
441 db_entry.repository_group = repo_group
441 db_entry.repository_group = repo_group
442 should_save = True
442 should_save = True
443 default_redirect_url = '${repo_group_url}'
443 default_redirect_url = '${repo_group_url}'
444 # save generic info
444 # save generic info
445 elif entry.get('title') and entry.get('redirect_url'):
445 elif entry.get('title') and entry.get('redirect_url'):
446 should_save = True
446 should_save = True
447
447
448 if should_save:
448 if should_save:
449 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
449 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
450 # mark user and position
450 # mark user and position
451 db_entry.user_id = user_id
451 db_entry.user_id = user_id
452 db_entry.position = position
452 db_entry.position = position
453 db_entry.title = entry.get('title')
453 db_entry.title = entry.get('title')
454 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
454 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
455
455
456 Session().add(db_entry)
456 Session().add(db_entry)
457
457
458 @LoginRequired()
458 @LoginRequired()
459 @NotAnonymous()
459 @NotAnonymous()
460 @CSRFRequired()
460 @CSRFRequired()
461 @view_config(
461 @view_config(
462 route_name='my_account_bookmarks_update', request_method='POST')
462 route_name='my_account_bookmarks_update', request_method='POST')
463 def my_account_bookmarks_update(self):
463 def my_account_bookmarks_update(self):
464 _ = self.request.translate
464 _ = self.request.translate
465 c = self.load_default_context()
465 c = self.load_default_context()
466 c.active = 'bookmarks'
466 c.active = 'bookmarks'
467
467
468 controls = peppercorn.parse(self.request.POST.items())
468 controls = peppercorn.parse(self.request.POST.items())
469 user_id = c.user.user_id
469 user_id = c.user.user_id
470
470
471 try:
471 try:
472 for entry in controls.get('bookmarks', []):
472 for entry in controls.get('bookmarks', []):
473 self._process_entry(entry, user_id)
473 self._process_entry(entry, user_id)
474
474
475 Session().commit()
475 Session().commit()
476 h.flash(_("Update Bookmarks"), category='success')
476 h.flash(_("Update Bookmarks"), category='success')
477 except IntegrityError:
477 except IntegrityError:
478 h.flash(_("Failed to update bookmarks. "
478 h.flash(_("Failed to update bookmarks. "
479 "Make sure an unique position is used"), category='error')
479 "Make sure an unique position is used"), category='error')
480
480
481 return HTTPFound(h.route_path('my_account_bookmarks'))
481 return HTTPFound(h.route_path('my_account_bookmarks'))
482
482
483 @LoginRequired()
483 @LoginRequired()
484 @NotAnonymous()
484 @NotAnonymous()
485 @view_config(
485 @view_config(
486 route_name='my_account_goto_bookmark', request_method='GET',
486 route_name='my_account_goto_bookmark', request_method='GET',
487 renderer='rhodecode:templates/admin/my_account/my_account.mako')
487 renderer='rhodecode:templates/admin/my_account/my_account.mako')
488 def my_account_goto_bookmark(self):
488 def my_account_goto_bookmark(self):
489
489
490 bookmark_id = self.request.matchdict['bookmark_id']
490 bookmark_id = self.request.matchdict['bookmark_id']
491 user_bookmark = UserBookmark().query()\
491 user_bookmark = UserBookmark().query()\
492 .filter(UserBookmark.user_id == self.request.user.user_id) \
492 .filter(UserBookmark.user_id == self.request.user.user_id) \
493 .filter(UserBookmark.position == bookmark_id).scalar()
493 .filter(UserBookmark.position == bookmark_id).scalar()
494
494
495 redirect_url = h.route_path('my_account_bookmarks')
495 redirect_url = h.route_path('my_account_bookmarks')
496 if not user_bookmark:
496 if not user_bookmark:
497 raise HTTPFound(redirect_url)
497 raise HTTPFound(redirect_url)
498
498
499 # repository set
499 # repository set
500 if user_bookmark.repository:
500 if user_bookmark.repository:
501 repo_name = user_bookmark.repository.repo_name
501 repo_name = user_bookmark.repository.repo_name
502 base_redirect_url = h.route_path(
502 base_redirect_url = h.route_path(
503 'repo_summary', repo_name=repo_name)
503 'repo_summary', repo_name=repo_name)
504 if user_bookmark.redirect_url and \
504 if user_bookmark.redirect_url and \
505 '${repo_url}' in user_bookmark.redirect_url:
505 '${repo_url}' in user_bookmark.redirect_url:
506 redirect_url = string.Template(user_bookmark.redirect_url)\
506 redirect_url = string.Template(user_bookmark.redirect_url)\
507 .safe_substitute({'repo_url': base_redirect_url})
507 .safe_substitute({'repo_url': base_redirect_url})
508 else:
508 else:
509 redirect_url = base_redirect_url
509 redirect_url = base_redirect_url
510 # repository group set
510 # repository group set
511 elif user_bookmark.repository_group:
511 elif user_bookmark.repository_group:
512 repo_group_name = user_bookmark.repository_group.group_name
512 repo_group_name = user_bookmark.repository_group.group_name
513 base_redirect_url = h.route_path(
513 base_redirect_url = h.route_path(
514 'repo_group_home', repo_group_name=repo_group_name)
514 'repo_group_home', repo_group_name=repo_group_name)
515 if user_bookmark.redirect_url and \
515 if user_bookmark.redirect_url and \
516 '${repo_group_url}' in user_bookmark.redirect_url:
516 '${repo_group_url}' in user_bookmark.redirect_url:
517 redirect_url = string.Template(user_bookmark.redirect_url)\
517 redirect_url = string.Template(user_bookmark.redirect_url)\
518 .safe_substitute({'repo_group_url': base_redirect_url})
518 .safe_substitute({'repo_group_url': base_redirect_url})
519 else:
519 else:
520 redirect_url = base_redirect_url
520 redirect_url = base_redirect_url
521 # custom URL set
521 # custom URL set
522 elif user_bookmark.redirect_url:
522 elif user_bookmark.redirect_url:
523 server_url = h.route_url('home').rstrip('/')
523 server_url = h.route_url('home').rstrip('/')
524 redirect_url = string.Template(user_bookmark.redirect_url) \
524 redirect_url = string.Template(user_bookmark.redirect_url) \
525 .safe_substitute({'server_url': server_url})
525 .safe_substitute({'server_url': server_url})
526
526
527 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
527 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
528 raise HTTPFound(redirect_url)
528 raise HTTPFound(redirect_url)
529
529
530 @LoginRequired()
530 @LoginRequired()
531 @NotAnonymous()
531 @NotAnonymous()
532 @view_config(
532 @view_config(
533 route_name='my_account_perms', request_method='GET',
533 route_name='my_account_perms', request_method='GET',
534 renderer='rhodecode:templates/admin/my_account/my_account.mako')
534 renderer='rhodecode:templates/admin/my_account/my_account.mako')
535 def my_account_perms(self):
535 def my_account_perms(self):
536 c = self.load_default_context()
536 c = self.load_default_context()
537 c.active = 'perms'
537 c.active = 'perms'
538
538
539 c.perm_user = c.auth_user
539 c.perm_user = c.auth_user
540 return self._get_template_context(c)
540 return self._get_template_context(c)
541
541
542 @LoginRequired()
542 @LoginRequired()
543 @NotAnonymous()
543 @NotAnonymous()
544 @view_config(
544 @view_config(
545 route_name='my_account_notifications', request_method='GET',
545 route_name='my_account_notifications', request_method='GET',
546 renderer='rhodecode:templates/admin/my_account/my_account.mako')
546 renderer='rhodecode:templates/admin/my_account/my_account.mako')
547 def my_notifications(self):
547 def my_notifications(self):
548 c = self.load_default_context()
548 c = self.load_default_context()
549 c.active = 'notifications'
549 c.active = 'notifications'
550
550
551 return self._get_template_context(c)
551 return self._get_template_context(c)
552
552
553 @LoginRequired()
553 @LoginRequired()
554 @NotAnonymous()
554 @NotAnonymous()
555 @CSRFRequired()
555 @CSRFRequired()
556 @view_config(
556 @view_config(
557 route_name='my_account_notifications_toggle_visibility',
557 route_name='my_account_notifications_toggle_visibility',
558 request_method='POST', renderer='json_ext')
558 request_method='POST', renderer='json_ext')
559 def my_notifications_toggle_visibility(self):
559 def my_notifications_toggle_visibility(self):
560 user = self._rhodecode_db_user
560 user = self._rhodecode_db_user
561 new_status = not user.user_data.get('notification_status', True)
561 new_status = not user.user_data.get('notification_status', True)
562 user.update_userdata(notification_status=new_status)
562 user.update_userdata(notification_status=new_status)
563 Session().commit()
563 Session().commit()
564 return user.user_data['notification_status']
564 return user.user_data['notification_status']
565
565
566 @LoginRequired()
566 @LoginRequired()
567 @NotAnonymous()
567 @NotAnonymous()
568 @view_config(
568 @view_config(
569 route_name='my_account_edit',
569 route_name='my_account_edit',
570 request_method='GET',
570 request_method='GET',
571 renderer='rhodecode:templates/admin/my_account/my_account.mako')
571 renderer='rhodecode:templates/admin/my_account/my_account.mako')
572 def my_account_edit(self):
572 def my_account_edit(self):
573 c = self.load_default_context()
573 c = self.load_default_context()
574 c.active = 'profile_edit'
574 c.active = 'profile_edit'
575 c.extern_type = c.user.extern_type
575 c.extern_type = c.user.extern_type
576 c.extern_name = c.user.extern_name
576 c.extern_name = c.user.extern_name
577
577
578 schema = user_schema.UserProfileSchema().bind(
578 schema = user_schema.UserProfileSchema().bind(
579 username=c.user.username, user_emails=c.user.emails)
579 username=c.user.username, user_emails=c.user.emails)
580 appstruct = {
580 appstruct = {
581 'username': c.user.username,
581 'username': c.user.username,
582 'email': c.user.email,
582 'email': c.user.email,
583 'firstname': c.user.firstname,
583 'firstname': c.user.firstname,
584 'lastname': c.user.lastname,
584 'lastname': c.user.lastname,
585 }
585 }
586 c.form = forms.RcForm(
586 c.form = forms.RcForm(
587 schema, appstruct=appstruct,
587 schema, appstruct=appstruct,
588 action=h.route_path('my_account_update'),
588 action=h.route_path('my_account_update'),
589 buttons=(forms.buttons.save, forms.buttons.reset))
589 buttons=(forms.buttons.save, forms.buttons.reset))
590
590
591 return self._get_template_context(c)
591 return self._get_template_context(c)
592
592
593 @LoginRequired()
593 @LoginRequired()
594 @NotAnonymous()
594 @NotAnonymous()
595 @CSRFRequired()
595 @CSRFRequired()
596 @view_config(
596 @view_config(
597 route_name='my_account_update',
597 route_name='my_account_update',
598 request_method='POST',
598 request_method='POST',
599 renderer='rhodecode:templates/admin/my_account/my_account.mako')
599 renderer='rhodecode:templates/admin/my_account/my_account.mako')
600 def my_account_update(self):
600 def my_account_update(self):
601 _ = self.request.translate
601 _ = self.request.translate
602 c = self.load_default_context()
602 c = self.load_default_context()
603 c.active = 'profile_edit'
603 c.active = 'profile_edit'
604 c.perm_user = c.auth_user
604 c.perm_user = c.auth_user
605 c.extern_type = c.user.extern_type
605 c.extern_type = c.user.extern_type
606 c.extern_name = c.user.extern_name
606 c.extern_name = c.user.extern_name
607
607
608 schema = user_schema.UserProfileSchema().bind(
608 schema = user_schema.UserProfileSchema().bind(
609 username=c.user.username, user_emails=c.user.emails)
609 username=c.user.username, user_emails=c.user.emails)
610 form = forms.RcForm(
610 form = forms.RcForm(
611 schema, buttons=(forms.buttons.save, forms.buttons.reset))
611 schema, buttons=(forms.buttons.save, forms.buttons.reset))
612
612
613 controls = self.request.POST.items()
613 controls = self.request.POST.items()
614 try:
614 try:
615 valid_data = form.validate(controls)
615 valid_data = form.validate(controls)
616 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
616 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
617 'new_password', 'password_confirmation']
617 'new_password', 'password_confirmation']
618 if c.extern_type != "rhodecode":
618 if c.extern_type != "rhodecode":
619 # forbid updating username for external accounts
619 # forbid updating username for external accounts
620 skip_attrs.append('username')
620 skip_attrs.append('username')
621 old_email = c.user.email
621 old_email = c.user.email
622 UserModel().update_user(
622 UserModel().update_user(
623 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
623 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
624 **valid_data)
624 **valid_data)
625 if old_email != valid_data['email']:
625 if old_email != valid_data['email']:
626 old = UserEmailMap.query() \
626 old = UserEmailMap.query() \
627 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
627 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
628 old.email = old_email
628 old.email = old_email
629 h.flash(_('Your account was updated successfully'), category='success')
629 h.flash(_('Your account was updated successfully'), category='success')
630 Session().commit()
630 Session().commit()
631 except forms.ValidationFailure as e:
631 except forms.ValidationFailure as e:
632 c.form = e
632 c.form = e
633 return self._get_template_context(c)
633 return self._get_template_context(c)
634 except Exception:
634 except Exception:
635 log.exception("Exception updating user")
635 log.exception("Exception updating user")
636 h.flash(_('Error occurred during update of user'),
636 h.flash(_('Error occurred during update of user'),
637 category='error')
637 category='error')
638 raise HTTPFound(h.route_path('my_account_profile'))
638 raise HTTPFound(h.route_path('my_account_profile'))
639
639
640 def _get_pull_requests_list(self, statuses):
640 def _get_pull_requests_list(self, statuses):
641 draw, start, limit = self._extract_chunk(self.request)
641 draw, start, limit = self._extract_chunk(self.request)
642 search_q, order_by, order_dir = self._extract_ordering(self.request)
642 search_q, order_by, order_dir = self._extract_ordering(self.request)
643 _render = self.request.get_partial_renderer(
643 _render = self.request.get_partial_renderer(
644 'rhodecode:templates/data_table/_dt_elements.mako')
644 'rhodecode:templates/data_table/_dt_elements.mako')
645
645
646 pull_requests = PullRequestModel().get_im_participating_in(
646 pull_requests = PullRequestModel().get_im_participating_in(
647 user_id=self._rhodecode_user.user_id,
647 user_id=self._rhodecode_user.user_id,
648 statuses=statuses,
648 statuses=statuses,
649 offset=start, length=limit, order_by=order_by,
649 offset=start, length=limit, order_by=order_by,
650 order_dir=order_dir)
650 order_dir=order_dir)
651
651
652 pull_requests_total_count = PullRequestModel().count_im_participating_in(
652 pull_requests_total_count = PullRequestModel().count_im_participating_in(
653 user_id=self._rhodecode_user.user_id, statuses=statuses)
653 user_id=self._rhodecode_user.user_id, statuses=statuses)
654
654
655 data = []
655 data = []
656 comments_model = CommentsModel()
656 comments_model = CommentsModel()
657 for pr in pull_requests:
657 for pr in pull_requests:
658 repo_id = pr.target_repo_id
658 repo_id = pr.target_repo_id
659 comments = comments_model.get_all_comments(
659 comments = comments_model.get_all_comments(
660 repo_id, pull_request=pr)
660 repo_id, pull_request=pr)
661 owned = pr.user_id == self._rhodecode_user.user_id
661 owned = pr.user_id == self._rhodecode_user.user_id
662
662
663 data.append({
663 data.append({
664 'target_repo': _render('pullrequest_target_repo',
664 'target_repo': _render('pullrequest_target_repo',
665 pr.target_repo.repo_name),
665 pr.target_repo.repo_name),
666 'name': _render('pullrequest_name',
666 'name': _render('pullrequest_name',
667 pr.pull_request_id, pr.target_repo.repo_name,
667 pr.pull_request_id, pr.target_repo.repo_name,
668 short=True),
668 short=True),
669 'name_raw': pr.pull_request_id,
669 'name_raw': pr.pull_request_id,
670 'status': _render('pullrequest_status',
670 'status': _render('pullrequest_status',
671 pr.calculated_review_status()),
671 pr.calculated_review_status()),
672 'title': _render(
672 'title': _render('pullrequest_title', pr.title, pr.description),
673 'pullrequest_title', pr.title, pr.description),
674 'description': h.escape(pr.description),
673 'description': h.escape(pr.description),
675 'updated_on': _render('pullrequest_updated_on',
674 'updated_on': _render('pullrequest_updated_on',
676 h.datetime_to_time(pr.updated_on)),
675 h.datetime_to_time(pr.updated_on)),
677 'updated_on_raw': h.datetime_to_time(pr.updated_on),
676 'updated_on_raw': h.datetime_to_time(pr.updated_on),
678 'created_on': _render('pullrequest_updated_on',
677 'created_on': _render('pullrequest_updated_on',
679 h.datetime_to_time(pr.created_on)),
678 h.datetime_to_time(pr.created_on)),
680 'created_on_raw': h.datetime_to_time(pr.created_on),
679 'created_on_raw': h.datetime_to_time(pr.created_on),
680 'state': pr.pull_request_state,
681 'author': _render('pullrequest_author',
681 'author': _render('pullrequest_author',
682 pr.author.full_contact, ),
682 pr.author.full_contact, ),
683 'author_raw': pr.author.full_name,
683 'author_raw': pr.author.full_name,
684 'comments': _render('pullrequest_comments', len(comments)),
684 'comments': _render('pullrequest_comments', len(comments)),
685 'comments_raw': len(comments),
685 'comments_raw': len(comments),
686 'closed': pr.is_closed(),
686 'closed': pr.is_closed(),
687 'owned': owned
687 'owned': owned
688 })
688 })
689
689
690 # json used to render the grid
690 # json used to render the grid
691 data = ({
691 data = ({
692 'draw': draw,
692 'draw': draw,
693 'data': data,
693 'data': data,
694 'recordsTotal': pull_requests_total_count,
694 'recordsTotal': pull_requests_total_count,
695 'recordsFiltered': pull_requests_total_count,
695 'recordsFiltered': pull_requests_total_count,
696 })
696 })
697 return data
697 return data
698
698
699 @LoginRequired()
699 @LoginRequired()
700 @NotAnonymous()
700 @NotAnonymous()
701 @view_config(
701 @view_config(
702 route_name='my_account_pullrequests',
702 route_name='my_account_pullrequests',
703 request_method='GET',
703 request_method='GET',
704 renderer='rhodecode:templates/admin/my_account/my_account.mako')
704 renderer='rhodecode:templates/admin/my_account/my_account.mako')
705 def my_account_pullrequests(self):
705 def my_account_pullrequests(self):
706 c = self.load_default_context()
706 c = self.load_default_context()
707 c.active = 'pullrequests'
707 c.active = 'pullrequests'
708 req_get = self.request.GET
708 req_get = self.request.GET
709
709
710 c.closed = str2bool(req_get.get('pr_show_closed'))
710 c.closed = str2bool(req_get.get('pr_show_closed'))
711
711
712 return self._get_template_context(c)
712 return self._get_template_context(c)
713
713
714 @LoginRequired()
714 @LoginRequired()
715 @NotAnonymous()
715 @NotAnonymous()
716 @view_config(
716 @view_config(
717 route_name='my_account_pullrequests_data',
717 route_name='my_account_pullrequests_data',
718 request_method='GET', renderer='json_ext')
718 request_method='GET', renderer='json_ext')
719 def my_account_pullrequests_data(self):
719 def my_account_pullrequests_data(self):
720 self.load_default_context()
720 self.load_default_context()
721 req_get = self.request.GET
721 req_get = self.request.GET
722 closed = str2bool(req_get.get('closed'))
722 closed = str2bool(req_get.get('closed'))
723
723
724 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
724 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
725 if closed:
725 if closed:
726 statuses += [PullRequest.STATUS_CLOSED]
726 statuses += [PullRequest.STATUS_CLOSED]
727
727
728 data = self._get_pull_requests_list(statuses=statuses)
728 data = self._get_pull_requests_list(statuses=statuses)
729 return data
729 return data
730
730
731 @LoginRequired()
731 @LoginRequired()
732 @NotAnonymous()
732 @NotAnonymous()
733 @view_config(
733 @view_config(
734 route_name='my_account_user_group_membership',
734 route_name='my_account_user_group_membership',
735 request_method='GET',
735 request_method='GET',
736 renderer='rhodecode:templates/admin/my_account/my_account.mako')
736 renderer='rhodecode:templates/admin/my_account/my_account.mako')
737 def my_account_user_group_membership(self):
737 def my_account_user_group_membership(self):
738 c = self.load_default_context()
738 c = self.load_default_context()
739 c.active = 'user_group_membership'
739 c.active = 'user_group_membership'
740 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
740 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
741 for group in self._rhodecode_db_user.group_member]
741 for group in self._rhodecode_db_user.group_member]
742 c.user_groups = json.dumps(groups)
742 c.user_groups = json.dumps(groups)
743 return self._get_template_context(c)
743 return self._get_template_context(c)
@@ -1,1464 +1,1464 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, EmptyRepositoryError)
44 RepositoryRequirementError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
48 ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 # backward compat., we use for OLD PRs a plain renderer
63 # backward compat., we use for OLD PRs a plain renderer
64 c.renderer = 'plain'
64 c.renderer = 'plain'
65 return c
65 return c
66
66
67 def _get_pull_requests_list(
67 def _get_pull_requests_list(
68 self, repo_name, source, filter_type, opened_by, statuses):
68 self, repo_name, source, filter_type, opened_by, statuses):
69
69
70 draw, start, limit = self._extract_chunk(self.request)
70 draw, start, limit = self._extract_chunk(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 _render = self.request.get_partial_renderer(
72 _render = self.request.get_partial_renderer(
73 'rhodecode:templates/data_table/_dt_elements.mako')
73 'rhodecode:templates/data_table/_dt_elements.mako')
74
74
75 # pagination
75 # pagination
76
76
77 if filter_type == 'awaiting_review':
77 if filter_type == 'awaiting_review':
78 pull_requests = PullRequestModel().get_awaiting_review(
78 pull_requests = PullRequestModel().get_awaiting_review(
79 repo_name, source=source, opened_by=opened_by,
79 repo_name, source=source, opened_by=opened_by,
80 statuses=statuses, offset=start, length=limit,
80 statuses=statuses, offset=start, length=limit,
81 order_by=order_by, order_dir=order_dir)
81 order_by=order_by, order_dir=order_dir)
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 repo_name, source=source, statuses=statuses,
83 repo_name, source=source, statuses=statuses,
84 opened_by=opened_by)
84 opened_by=opened_by)
85 elif filter_type == 'awaiting_my_review':
85 elif filter_type == 'awaiting_my_review':
86 pull_requests = PullRequestModel().get_awaiting_my_review(
86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 repo_name, source=source, opened_by=opened_by,
87 repo_name, source=source, opened_by=opened_by,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 offset=start, length=limit, order_by=order_by,
89 offset=start, length=limit, order_by=order_by,
90 order_dir=order_dir)
90 order_dir=order_dir)
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 statuses=statuses, opened_by=opened_by)
93 statuses=statuses, opened_by=opened_by)
94 else:
94 else:
95 pull_requests = PullRequestModel().get_all(
95 pull_requests = PullRequestModel().get_all(
96 repo_name, source=source, opened_by=opened_by,
96 repo_name, source=source, opened_by=opened_by,
97 statuses=statuses, offset=start, length=limit,
97 statuses=statuses, offset=start, length=limit,
98 order_by=order_by, order_dir=order_dir)
98 order_by=order_by, order_dir=order_dir)
99 pull_requests_total_count = PullRequestModel().count_all(
99 pull_requests_total_count = PullRequestModel().count_all(
100 repo_name, source=source, statuses=statuses,
100 repo_name, source=source, statuses=statuses,
101 opened_by=opened_by)
101 opened_by=opened_by)
102
102
103 data = []
103 data = []
104 comments_model = CommentsModel()
104 comments_model = CommentsModel()
105 for pr in pull_requests:
105 for pr in pull_requests:
106 comments = comments_model.get_all_comments(
106 comments = comments_model.get_all_comments(
107 self.db_repo.repo_id, pull_request=pr)
107 self.db_repo.repo_id, pull_request=pr)
108
108
109 data.append({
109 data.append({
110 'name': _render('pullrequest_name',
110 'name': _render('pullrequest_name',
111 pr.pull_request_id, pr.target_repo.repo_name),
111 pr.pull_request_id, pr.target_repo.repo_name),
112 'name_raw': pr.pull_request_id,
112 'name_raw': pr.pull_request_id,
113 'status': _render('pullrequest_status',
113 'status': _render('pullrequest_status',
114 pr.calculated_review_status()),
114 pr.calculated_review_status()),
115 'title': _render(
115 'title': _render('pullrequest_title', pr.title, pr.description),
116 'pullrequest_title', pr.title, pr.description),
117 'description': h.escape(pr.description),
116 'description': h.escape(pr.description),
118 'updated_on': _render('pullrequest_updated_on',
117 'updated_on': _render('pullrequest_updated_on',
119 h.datetime_to_time(pr.updated_on)),
118 h.datetime_to_time(pr.updated_on)),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'created_on': _render('pullrequest_updated_on',
120 'created_on': _render('pullrequest_updated_on',
122 h.datetime_to_time(pr.created_on)),
121 h.datetime_to_time(pr.created_on)),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'state': pr.pull_request_state,
124 'author': _render('pullrequest_author',
124 'author': _render('pullrequest_author',
125 pr.author.full_contact, ),
125 pr.author.full_contact, ),
126 'author_raw': pr.author.full_name,
126 'author_raw': pr.author.full_name,
127 'comments': _render('pullrequest_comments', len(comments)),
127 'comments': _render('pullrequest_comments', len(comments)),
128 'comments_raw': len(comments),
128 'comments_raw': len(comments),
129 'closed': pr.is_closed(),
129 'closed': pr.is_closed(),
130 })
130 })
131
131
132 data = ({
132 data = ({
133 'draw': draw,
133 'draw': draw,
134 'data': data,
134 'data': data,
135 'recordsTotal': pull_requests_total_count,
135 'recordsTotal': pull_requests_total_count,
136 'recordsFiltered': pull_requests_total_count,
136 'recordsFiltered': pull_requests_total_count,
137 })
137 })
138 return data
138 return data
139
139
140 @LoginRequired()
140 @LoginRequired()
141 @HasRepoPermissionAnyDecorator(
141 @HasRepoPermissionAnyDecorator(
142 'repository.read', 'repository.write', 'repository.admin')
142 'repository.read', 'repository.write', 'repository.admin')
143 @view_config(
143 @view_config(
144 route_name='pullrequest_show_all', request_method='GET',
144 route_name='pullrequest_show_all', request_method='GET',
145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 def pull_request_list(self):
146 def pull_request_list(self):
147 c = self.load_default_context()
147 c = self.load_default_context()
148
148
149 req_get = self.request.GET
149 req_get = self.request.GET
150 c.source = str2bool(req_get.get('source'))
150 c.source = str2bool(req_get.get('source'))
151 c.closed = str2bool(req_get.get('closed'))
151 c.closed = str2bool(req_get.get('closed'))
152 c.my = str2bool(req_get.get('my'))
152 c.my = str2bool(req_get.get('my'))
153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155
155
156 c.active = 'open'
156 c.active = 'open'
157 if c.my:
157 if c.my:
158 c.active = 'my'
158 c.active = 'my'
159 if c.closed:
159 if c.closed:
160 c.active = 'closed'
160 c.active = 'closed'
161 if c.awaiting_review and not c.source:
161 if c.awaiting_review and not c.source:
162 c.active = 'awaiting'
162 c.active = 'awaiting'
163 if c.source and not c.awaiting_review:
163 if c.source and not c.awaiting_review:
164 c.active = 'source'
164 c.active = 'source'
165 if c.awaiting_my_review:
165 if c.awaiting_my_review:
166 c.active = 'awaiting_my'
166 c.active = 'awaiting_my'
167
167
168 return self._get_template_context(c)
168 return self._get_template_context(c)
169
169
170 @LoginRequired()
170 @LoginRequired()
171 @HasRepoPermissionAnyDecorator(
171 @HasRepoPermissionAnyDecorator(
172 'repository.read', 'repository.write', 'repository.admin')
172 'repository.read', 'repository.write', 'repository.admin')
173 @view_config(
173 @view_config(
174 route_name='pullrequest_show_all_data', request_method='GET',
174 route_name='pullrequest_show_all_data', request_method='GET',
175 renderer='json_ext', xhr=True)
175 renderer='json_ext', xhr=True)
176 def pull_request_list_data(self):
176 def pull_request_list_data(self):
177 self.load_default_context()
177 self.load_default_context()
178
178
179 # additional filters
179 # additional filters
180 req_get = self.request.GET
180 req_get = self.request.GET
181 source = str2bool(req_get.get('source'))
181 source = str2bool(req_get.get('source'))
182 closed = str2bool(req_get.get('closed'))
182 closed = str2bool(req_get.get('closed'))
183 my = str2bool(req_get.get('my'))
183 my = str2bool(req_get.get('my'))
184 awaiting_review = str2bool(req_get.get('awaiting_review'))
184 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186
186
187 filter_type = 'awaiting_review' if awaiting_review \
187 filter_type = 'awaiting_review' if awaiting_review \
188 else 'awaiting_my_review' if awaiting_my_review \
188 else 'awaiting_my_review' if awaiting_my_review \
189 else None
189 else None
190
190
191 opened_by = None
191 opened_by = None
192 if my:
192 if my:
193 opened_by = [self._rhodecode_user.user_id]
193 opened_by = [self._rhodecode_user.user_id]
194
194
195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 if closed:
196 if closed:
197 statuses = [PullRequest.STATUS_CLOSED]
197 statuses = [PullRequest.STATUS_CLOSED]
198
198
199 data = self._get_pull_requests_list(
199 data = self._get_pull_requests_list(
200 repo_name=self.db_repo_name, source=source,
200 repo_name=self.db_repo_name, source=source,
201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202
202
203 return data
203 return data
204
204
205 def _is_diff_cache_enabled(self, target_repo):
205 def _is_diff_cache_enabled(self, target_repo):
206 caching_enabled = self._get_general_setting(
206 caching_enabled = self._get_general_setting(
207 target_repo, 'rhodecode_diff_cache')
207 target_repo, 'rhodecode_diff_cache')
208 log.debug('Diff caching enabled: %s', caching_enabled)
208 log.debug('Diff caching enabled: %s', caching_enabled)
209 return caching_enabled
209 return caching_enabled
210
210
211 def _get_diffset(self, source_repo_name, source_repo,
211 def _get_diffset(self, source_repo_name, source_repo,
212 source_ref_id, target_ref_id,
212 source_ref_id, target_ref_id,
213 target_commit, source_commit, diff_limit, file_limit,
213 target_commit, source_commit, diff_limit, file_limit,
214 fulldiff, hide_whitespace_changes, diff_context):
214 fulldiff, hide_whitespace_changes, diff_context):
215
215
216 vcs_diff = PullRequestModel().get_diff(
216 vcs_diff = PullRequestModel().get_diff(
217 source_repo, source_ref_id, target_ref_id,
217 source_repo, source_ref_id, target_ref_id,
218 hide_whitespace_changes, diff_context)
218 hide_whitespace_changes, diff_context)
219
219
220 diff_processor = diffs.DiffProcessor(
220 diff_processor = diffs.DiffProcessor(
221 vcs_diff, format='newdiff', diff_limit=diff_limit,
221 vcs_diff, format='newdiff', diff_limit=diff_limit,
222 file_limit=file_limit, show_full_diff=fulldiff)
222 file_limit=file_limit, show_full_diff=fulldiff)
223
223
224 _parsed = diff_processor.prepare()
224 _parsed = diff_processor.prepare()
225
225
226 diffset = codeblocks.DiffSet(
226 diffset = codeblocks.DiffSet(
227 repo_name=self.db_repo_name,
227 repo_name=self.db_repo_name,
228 source_repo_name=source_repo_name,
228 source_repo_name=source_repo_name,
229 source_node_getter=codeblocks.diffset_node_getter(target_commit),
229 source_node_getter=codeblocks.diffset_node_getter(target_commit),
230 target_node_getter=codeblocks.diffset_node_getter(source_commit),
230 target_node_getter=codeblocks.diffset_node_getter(source_commit),
231 )
231 )
232 diffset = self.path_filter.render_patchset_filtered(
232 diffset = self.path_filter.render_patchset_filtered(
233 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
233 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
234
234
235 return diffset
235 return diffset
236
236
237 def _get_range_diffset(self, source_scm, source_repo,
237 def _get_range_diffset(self, source_scm, source_repo,
238 commit1, commit2, diff_limit, file_limit,
238 commit1, commit2, diff_limit, file_limit,
239 fulldiff, hide_whitespace_changes, diff_context):
239 fulldiff, hide_whitespace_changes, diff_context):
240 vcs_diff = source_scm.get_diff(
240 vcs_diff = source_scm.get_diff(
241 commit1, commit2,
241 commit1, commit2,
242 ignore_whitespace=hide_whitespace_changes,
242 ignore_whitespace=hide_whitespace_changes,
243 context=diff_context)
243 context=diff_context)
244
244
245 diff_processor = diffs.DiffProcessor(
245 diff_processor = diffs.DiffProcessor(
246 vcs_diff, format='newdiff', diff_limit=diff_limit,
246 vcs_diff, format='newdiff', diff_limit=diff_limit,
247 file_limit=file_limit, show_full_diff=fulldiff)
247 file_limit=file_limit, show_full_diff=fulldiff)
248
248
249 _parsed = diff_processor.prepare()
249 _parsed = diff_processor.prepare()
250
250
251 diffset = codeblocks.DiffSet(
251 diffset = codeblocks.DiffSet(
252 repo_name=source_repo.repo_name,
252 repo_name=source_repo.repo_name,
253 source_node_getter=codeblocks.diffset_node_getter(commit1),
253 source_node_getter=codeblocks.diffset_node_getter(commit1),
254 target_node_getter=codeblocks.diffset_node_getter(commit2))
254 target_node_getter=codeblocks.diffset_node_getter(commit2))
255
255
256 diffset = self.path_filter.render_patchset_filtered(
256 diffset = self.path_filter.render_patchset_filtered(
257 diffset, _parsed, commit1.raw_id, commit2.raw_id)
257 diffset, _parsed, commit1.raw_id, commit2.raw_id)
258
258
259 return diffset
259 return diffset
260
260
261 @LoginRequired()
261 @LoginRequired()
262 @HasRepoPermissionAnyDecorator(
262 @HasRepoPermissionAnyDecorator(
263 'repository.read', 'repository.write', 'repository.admin')
263 'repository.read', 'repository.write', 'repository.admin')
264 @view_config(
264 @view_config(
265 route_name='pullrequest_show', request_method='GET',
265 route_name='pullrequest_show', request_method='GET',
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 def pull_request_show(self):
267 def pull_request_show(self):
268 _ = self.request.translate
268 _ = self.request.translate
269 c = self.load_default_context()
269 c = self.load_default_context()
270
270
271 pull_request = PullRequest.get_or_404(
271 pull_request = PullRequest.get_or_404(
272 self.request.matchdict['pull_request_id'])
272 self.request.matchdict['pull_request_id'])
273 pull_request_id = pull_request.pull_request_id
273 pull_request_id = pull_request.pull_request_id
274
274
275 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
275 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
276 log.debug('show: forbidden because pull request is in state %s',
276 log.debug('show: forbidden because pull request is in state %s',
277 pull_request.pull_request_state)
277 pull_request.pull_request_state)
278 msg = _(u'Cannot show pull requests in state other than `{}`. '
278 msg = _(u'Cannot show pull requests in state other than `{}`. '
279 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
279 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
280 pull_request.pull_request_state)
280 pull_request.pull_request_state)
281 h.flash(msg, category='error')
281 h.flash(msg, category='error')
282 raise HTTPFound(h.route_path('pullrequest_show_all',
282 raise HTTPFound(h.route_path('pullrequest_show_all',
283 repo_name=self.db_repo_name))
283 repo_name=self.db_repo_name))
284
284
285 version = self.request.GET.get('version')
285 version = self.request.GET.get('version')
286 from_version = self.request.GET.get('from_version') or version
286 from_version = self.request.GET.get('from_version') or version
287 merge_checks = self.request.GET.get('merge_checks')
287 merge_checks = self.request.GET.get('merge_checks')
288 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
288 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
289
289
290 # fetch global flags of ignore ws or context lines
290 # fetch global flags of ignore ws or context lines
291 diff_context = diffs.get_diff_context(self.request)
291 diff_context = diffs.get_diff_context(self.request)
292 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
292 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
293
293
294 force_refresh = str2bool(self.request.GET.get('force_refresh'))
294 force_refresh = str2bool(self.request.GET.get('force_refresh'))
295
295
296 (pull_request_latest,
296 (pull_request_latest,
297 pull_request_at_ver,
297 pull_request_at_ver,
298 pull_request_display_obj,
298 pull_request_display_obj,
299 at_version) = PullRequestModel().get_pr_version(
299 at_version) = PullRequestModel().get_pr_version(
300 pull_request_id, version=version)
300 pull_request_id, version=version)
301 pr_closed = pull_request_latest.is_closed()
301 pr_closed = pull_request_latest.is_closed()
302
302
303 if pr_closed and (version or from_version):
303 if pr_closed and (version or from_version):
304 # not allow to browse versions
304 # not allow to browse versions
305 raise HTTPFound(h.route_path(
305 raise HTTPFound(h.route_path(
306 'pullrequest_show', repo_name=self.db_repo_name,
306 'pullrequest_show', repo_name=self.db_repo_name,
307 pull_request_id=pull_request_id))
307 pull_request_id=pull_request_id))
308
308
309 versions = pull_request_display_obj.versions()
309 versions = pull_request_display_obj.versions()
310 # used to store per-commit range diffs
310 # used to store per-commit range diffs
311 c.changes = collections.OrderedDict()
311 c.changes = collections.OrderedDict()
312 c.range_diff_on = self.request.GET.get('range-diff') == "1"
312 c.range_diff_on = self.request.GET.get('range-diff') == "1"
313
313
314 c.at_version = at_version
314 c.at_version = at_version
315 c.at_version_num = (at_version
315 c.at_version_num = (at_version
316 if at_version and at_version != 'latest'
316 if at_version and at_version != 'latest'
317 else None)
317 else None)
318 c.at_version_pos = ChangesetComment.get_index_from_version(
318 c.at_version_pos = ChangesetComment.get_index_from_version(
319 c.at_version_num, versions)
319 c.at_version_num, versions)
320
320
321 (prev_pull_request_latest,
321 (prev_pull_request_latest,
322 prev_pull_request_at_ver,
322 prev_pull_request_at_ver,
323 prev_pull_request_display_obj,
323 prev_pull_request_display_obj,
324 prev_at_version) = PullRequestModel().get_pr_version(
324 prev_at_version) = PullRequestModel().get_pr_version(
325 pull_request_id, version=from_version)
325 pull_request_id, version=from_version)
326
326
327 c.from_version = prev_at_version
327 c.from_version = prev_at_version
328 c.from_version_num = (prev_at_version
328 c.from_version_num = (prev_at_version
329 if prev_at_version and prev_at_version != 'latest'
329 if prev_at_version and prev_at_version != 'latest'
330 else None)
330 else None)
331 c.from_version_pos = ChangesetComment.get_index_from_version(
331 c.from_version_pos = ChangesetComment.get_index_from_version(
332 c.from_version_num, versions)
332 c.from_version_num, versions)
333
333
334 # define if we're in COMPARE mode or VIEW at version mode
334 # define if we're in COMPARE mode or VIEW at version mode
335 compare = at_version != prev_at_version
335 compare = at_version != prev_at_version
336
336
337 # pull_requests repo_name we opened it against
337 # pull_requests repo_name we opened it against
338 # ie. target_repo must match
338 # ie. target_repo must match
339 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
339 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
340 raise HTTPNotFound()
340 raise HTTPNotFound()
341
341
342 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
342 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
343 pull_request_at_ver)
343 pull_request_at_ver)
344
344
345 c.pull_request = pull_request_display_obj
345 c.pull_request = pull_request_display_obj
346 c.renderer = pull_request_at_ver.description_renderer or c.renderer
346 c.renderer = pull_request_at_ver.description_renderer or c.renderer
347 c.pull_request_latest = pull_request_latest
347 c.pull_request_latest = pull_request_latest
348
348
349 if compare or (at_version and not at_version == 'latest'):
349 if compare or (at_version and not at_version == 'latest'):
350 c.allowed_to_change_status = False
350 c.allowed_to_change_status = False
351 c.allowed_to_update = False
351 c.allowed_to_update = False
352 c.allowed_to_merge = False
352 c.allowed_to_merge = False
353 c.allowed_to_delete = False
353 c.allowed_to_delete = False
354 c.allowed_to_comment = False
354 c.allowed_to_comment = False
355 c.allowed_to_close = False
355 c.allowed_to_close = False
356 else:
356 else:
357 can_change_status = PullRequestModel().check_user_change_status(
357 can_change_status = PullRequestModel().check_user_change_status(
358 pull_request_at_ver, self._rhodecode_user)
358 pull_request_at_ver, self._rhodecode_user)
359 c.allowed_to_change_status = can_change_status and not pr_closed
359 c.allowed_to_change_status = can_change_status and not pr_closed
360
360
361 c.allowed_to_update = PullRequestModel().check_user_update(
361 c.allowed_to_update = PullRequestModel().check_user_update(
362 pull_request_latest, self._rhodecode_user) and not pr_closed
362 pull_request_latest, self._rhodecode_user) and not pr_closed
363 c.allowed_to_merge = PullRequestModel().check_user_merge(
363 c.allowed_to_merge = PullRequestModel().check_user_merge(
364 pull_request_latest, self._rhodecode_user) and not pr_closed
364 pull_request_latest, self._rhodecode_user) and not pr_closed
365 c.allowed_to_delete = PullRequestModel().check_user_delete(
365 c.allowed_to_delete = PullRequestModel().check_user_delete(
366 pull_request_latest, self._rhodecode_user) and not pr_closed
366 pull_request_latest, self._rhodecode_user) and not pr_closed
367 c.allowed_to_comment = not pr_closed
367 c.allowed_to_comment = not pr_closed
368 c.allowed_to_close = c.allowed_to_merge and not pr_closed
368 c.allowed_to_close = c.allowed_to_merge and not pr_closed
369
369
370 c.forbid_adding_reviewers = False
370 c.forbid_adding_reviewers = False
371 c.forbid_author_to_review = False
371 c.forbid_author_to_review = False
372 c.forbid_commit_author_to_review = False
372 c.forbid_commit_author_to_review = False
373
373
374 if pull_request_latest.reviewer_data and \
374 if pull_request_latest.reviewer_data and \
375 'rules' in pull_request_latest.reviewer_data:
375 'rules' in pull_request_latest.reviewer_data:
376 rules = pull_request_latest.reviewer_data['rules'] or {}
376 rules = pull_request_latest.reviewer_data['rules'] or {}
377 try:
377 try:
378 c.forbid_adding_reviewers = rules.get(
378 c.forbid_adding_reviewers = rules.get(
379 'forbid_adding_reviewers')
379 'forbid_adding_reviewers')
380 c.forbid_author_to_review = rules.get(
380 c.forbid_author_to_review = rules.get(
381 'forbid_author_to_review')
381 'forbid_author_to_review')
382 c.forbid_commit_author_to_review = rules.get(
382 c.forbid_commit_author_to_review = rules.get(
383 'forbid_commit_author_to_review')
383 'forbid_commit_author_to_review')
384 except Exception:
384 except Exception:
385 pass
385 pass
386
386
387 # check merge capabilities
387 # check merge capabilities
388 _merge_check = MergeCheck.validate(
388 _merge_check = MergeCheck.validate(
389 pull_request_latest, auth_user=self._rhodecode_user,
389 pull_request_latest, auth_user=self._rhodecode_user,
390 translator=self.request.translate,
390 translator=self.request.translate,
391 force_shadow_repo_refresh=force_refresh)
391 force_shadow_repo_refresh=force_refresh)
392 c.pr_merge_errors = _merge_check.error_details
392 c.pr_merge_errors = _merge_check.error_details
393 c.pr_merge_possible = not _merge_check.failed
393 c.pr_merge_possible = not _merge_check.failed
394 c.pr_merge_message = _merge_check.merge_msg
394 c.pr_merge_message = _merge_check.merge_msg
395
395
396 c.pr_merge_info = MergeCheck.get_merge_conditions(
396 c.pr_merge_info = MergeCheck.get_merge_conditions(
397 pull_request_latest, translator=self.request.translate)
397 pull_request_latest, translator=self.request.translate)
398
398
399 c.pull_request_review_status = _merge_check.review_status
399 c.pull_request_review_status = _merge_check.review_status
400 if merge_checks:
400 if merge_checks:
401 self.request.override_renderer = \
401 self.request.override_renderer = \
402 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
402 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
403 return self._get_template_context(c)
403 return self._get_template_context(c)
404
404
405 comments_model = CommentsModel()
405 comments_model = CommentsModel()
406
406
407 # reviewers and statuses
407 # reviewers and statuses
408 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
408 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
409 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
409 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
410
410
411 # GENERAL COMMENTS with versions #
411 # GENERAL COMMENTS with versions #
412 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
412 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
413 q = q.order_by(ChangesetComment.comment_id.asc())
413 q = q.order_by(ChangesetComment.comment_id.asc())
414 general_comments = q
414 general_comments = q
415
415
416 # pick comments we want to render at current version
416 # pick comments we want to render at current version
417 c.comment_versions = comments_model.aggregate_comments(
417 c.comment_versions = comments_model.aggregate_comments(
418 general_comments, versions, c.at_version_num)
418 general_comments, versions, c.at_version_num)
419 c.comments = c.comment_versions[c.at_version_num]['until']
419 c.comments = c.comment_versions[c.at_version_num]['until']
420
420
421 # INLINE COMMENTS with versions #
421 # INLINE COMMENTS with versions #
422 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
422 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
423 q = q.order_by(ChangesetComment.comment_id.asc())
423 q = q.order_by(ChangesetComment.comment_id.asc())
424 inline_comments = q
424 inline_comments = q
425
425
426 c.inline_versions = comments_model.aggregate_comments(
426 c.inline_versions = comments_model.aggregate_comments(
427 inline_comments, versions, c.at_version_num, inline=True)
427 inline_comments, versions, c.at_version_num, inline=True)
428
428
429 # inject latest version
429 # inject latest version
430 latest_ver = PullRequest.get_pr_display_object(
430 latest_ver = PullRequest.get_pr_display_object(
431 pull_request_latest, pull_request_latest)
431 pull_request_latest, pull_request_latest)
432
432
433 c.versions = versions + [latest_ver]
433 c.versions = versions + [latest_ver]
434
434
435 # if we use version, then do not show later comments
435 # if we use version, then do not show later comments
436 # than current version
436 # than current version
437 display_inline_comments = collections.defaultdict(
437 display_inline_comments = collections.defaultdict(
438 lambda: collections.defaultdict(list))
438 lambda: collections.defaultdict(list))
439 for co in inline_comments:
439 for co in inline_comments:
440 if c.at_version_num:
440 if c.at_version_num:
441 # pick comments that are at least UPTO given version, so we
441 # pick comments that are at least UPTO given version, so we
442 # don't render comments for higher version
442 # don't render comments for higher version
443 should_render = co.pull_request_version_id and \
443 should_render = co.pull_request_version_id and \
444 co.pull_request_version_id <= c.at_version_num
444 co.pull_request_version_id <= c.at_version_num
445 else:
445 else:
446 # showing all, for 'latest'
446 # showing all, for 'latest'
447 should_render = True
447 should_render = True
448
448
449 if should_render:
449 if should_render:
450 display_inline_comments[co.f_path][co.line_no].append(co)
450 display_inline_comments[co.f_path][co.line_no].append(co)
451
451
452 # load diff data into template context, if we use compare mode then
452 # load diff data into template context, if we use compare mode then
453 # diff is calculated based on changes between versions of PR
453 # diff is calculated based on changes between versions of PR
454
454
455 source_repo = pull_request_at_ver.source_repo
455 source_repo = pull_request_at_ver.source_repo
456 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
456 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
457
457
458 target_repo = pull_request_at_ver.target_repo
458 target_repo = pull_request_at_ver.target_repo
459 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
459 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
460
460
461 if compare:
461 if compare:
462 # in compare switch the diff base to latest commit from prev version
462 # in compare switch the diff base to latest commit from prev version
463 target_ref_id = prev_pull_request_display_obj.revisions[0]
463 target_ref_id = prev_pull_request_display_obj.revisions[0]
464
464
465 # despite opening commits for bookmarks/branches/tags, we always
465 # despite opening commits for bookmarks/branches/tags, we always
466 # convert this to rev to prevent changes after bookmark or branch change
466 # convert this to rev to prevent changes after bookmark or branch change
467 c.source_ref_type = 'rev'
467 c.source_ref_type = 'rev'
468 c.source_ref = source_ref_id
468 c.source_ref = source_ref_id
469
469
470 c.target_ref_type = 'rev'
470 c.target_ref_type = 'rev'
471 c.target_ref = target_ref_id
471 c.target_ref = target_ref_id
472
472
473 c.source_repo = source_repo
473 c.source_repo = source_repo
474 c.target_repo = target_repo
474 c.target_repo = target_repo
475
475
476 c.commit_ranges = []
476 c.commit_ranges = []
477 source_commit = EmptyCommit()
477 source_commit = EmptyCommit()
478 target_commit = EmptyCommit()
478 target_commit = EmptyCommit()
479 c.missing_requirements = False
479 c.missing_requirements = False
480
480
481 source_scm = source_repo.scm_instance()
481 source_scm = source_repo.scm_instance()
482 target_scm = target_repo.scm_instance()
482 target_scm = target_repo.scm_instance()
483
483
484 shadow_scm = None
484 shadow_scm = None
485 try:
485 try:
486 shadow_scm = pull_request_latest.get_shadow_repo()
486 shadow_scm = pull_request_latest.get_shadow_repo()
487 except Exception:
487 except Exception:
488 log.debug('Failed to get shadow repo', exc_info=True)
488 log.debug('Failed to get shadow repo', exc_info=True)
489 # try first the existing source_repo, and then shadow
489 # try first the existing source_repo, and then shadow
490 # repo if we can obtain one
490 # repo if we can obtain one
491 commits_source_repo = source_scm or shadow_scm
491 commits_source_repo = source_scm or shadow_scm
492
492
493 c.commits_source_repo = commits_source_repo
493 c.commits_source_repo = commits_source_repo
494 c.ancestor = None # set it to None, to hide it from PR view
494 c.ancestor = None # set it to None, to hide it from PR view
495
495
496 # empty version means latest, so we keep this to prevent
496 # empty version means latest, so we keep this to prevent
497 # double caching
497 # double caching
498 version_normalized = version or 'latest'
498 version_normalized = version or 'latest'
499 from_version_normalized = from_version or 'latest'
499 from_version_normalized = from_version or 'latest'
500
500
501 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
501 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
502 cache_file_path = diff_cache_exist(
502 cache_file_path = diff_cache_exist(
503 cache_path, 'pull_request', pull_request_id, version_normalized,
503 cache_path, 'pull_request', pull_request_id, version_normalized,
504 from_version_normalized, source_ref_id, target_ref_id,
504 from_version_normalized, source_ref_id, target_ref_id,
505 hide_whitespace_changes, diff_context, c.fulldiff)
505 hide_whitespace_changes, diff_context, c.fulldiff)
506
506
507 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
507 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
508 force_recache = self.get_recache_flag()
508 force_recache = self.get_recache_flag()
509
509
510 cached_diff = None
510 cached_diff = None
511 if caching_enabled:
511 if caching_enabled:
512 cached_diff = load_cached_diff(cache_file_path)
512 cached_diff = load_cached_diff(cache_file_path)
513
513
514 has_proper_commit_cache = (
514 has_proper_commit_cache = (
515 cached_diff and cached_diff.get('commits')
515 cached_diff and cached_diff.get('commits')
516 and len(cached_diff.get('commits', [])) == 5
516 and len(cached_diff.get('commits', [])) == 5
517 and cached_diff.get('commits')[0]
517 and cached_diff.get('commits')[0]
518 and cached_diff.get('commits')[3])
518 and cached_diff.get('commits')[3])
519
519
520 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
520 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
521 diff_commit_cache = \
521 diff_commit_cache = \
522 (ancestor_commit, commit_cache, missing_requirements,
522 (ancestor_commit, commit_cache, missing_requirements,
523 source_commit, target_commit) = cached_diff['commits']
523 source_commit, target_commit) = cached_diff['commits']
524 else:
524 else:
525 diff_commit_cache = \
525 diff_commit_cache = \
526 (ancestor_commit, commit_cache, missing_requirements,
526 (ancestor_commit, commit_cache, missing_requirements,
527 source_commit, target_commit) = self.get_commits(
527 source_commit, target_commit) = self.get_commits(
528 commits_source_repo,
528 commits_source_repo,
529 pull_request_at_ver,
529 pull_request_at_ver,
530 source_commit,
530 source_commit,
531 source_ref_id,
531 source_ref_id,
532 source_scm,
532 source_scm,
533 target_commit,
533 target_commit,
534 target_ref_id,
534 target_ref_id,
535 target_scm)
535 target_scm)
536
536
537 # register our commit range
537 # register our commit range
538 for comm in commit_cache.values():
538 for comm in commit_cache.values():
539 c.commit_ranges.append(comm)
539 c.commit_ranges.append(comm)
540
540
541 c.missing_requirements = missing_requirements
541 c.missing_requirements = missing_requirements
542 c.ancestor_commit = ancestor_commit
542 c.ancestor_commit = ancestor_commit
543 c.statuses = source_repo.statuses(
543 c.statuses = source_repo.statuses(
544 [x.raw_id for x in c.commit_ranges])
544 [x.raw_id for x in c.commit_ranges])
545
545
546 # auto collapse if we have more than limit
546 # auto collapse if we have more than limit
547 collapse_limit = diffs.DiffProcessor._collapse_commits_over
547 collapse_limit = diffs.DiffProcessor._collapse_commits_over
548 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
548 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
549 c.compare_mode = compare
549 c.compare_mode = compare
550
550
551 # diff_limit is the old behavior, will cut off the whole diff
551 # diff_limit is the old behavior, will cut off the whole diff
552 # if the limit is applied otherwise will just hide the
552 # if the limit is applied otherwise will just hide the
553 # big files from the front-end
553 # big files from the front-end
554 diff_limit = c.visual.cut_off_limit_diff
554 diff_limit = c.visual.cut_off_limit_diff
555 file_limit = c.visual.cut_off_limit_file
555 file_limit = c.visual.cut_off_limit_file
556
556
557 c.missing_commits = False
557 c.missing_commits = False
558 if (c.missing_requirements
558 if (c.missing_requirements
559 or isinstance(source_commit, EmptyCommit)
559 or isinstance(source_commit, EmptyCommit)
560 or source_commit == target_commit):
560 or source_commit == target_commit):
561
561
562 c.missing_commits = True
562 c.missing_commits = True
563 else:
563 else:
564 c.inline_comments = display_inline_comments
564 c.inline_comments = display_inline_comments
565
565
566 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
566 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
567 if not force_recache and has_proper_diff_cache:
567 if not force_recache and has_proper_diff_cache:
568 c.diffset = cached_diff['diff']
568 c.diffset = cached_diff['diff']
569 (ancestor_commit, commit_cache, missing_requirements,
569 (ancestor_commit, commit_cache, missing_requirements,
570 source_commit, target_commit) = cached_diff['commits']
570 source_commit, target_commit) = cached_diff['commits']
571 else:
571 else:
572 c.diffset = self._get_diffset(
572 c.diffset = self._get_diffset(
573 c.source_repo.repo_name, commits_source_repo,
573 c.source_repo.repo_name, commits_source_repo,
574 source_ref_id, target_ref_id,
574 source_ref_id, target_ref_id,
575 target_commit, source_commit,
575 target_commit, source_commit,
576 diff_limit, file_limit, c.fulldiff,
576 diff_limit, file_limit, c.fulldiff,
577 hide_whitespace_changes, diff_context)
577 hide_whitespace_changes, diff_context)
578
578
579 # save cached diff
579 # save cached diff
580 if caching_enabled:
580 if caching_enabled:
581 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
581 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
582
582
583 c.limited_diff = c.diffset.limited_diff
583 c.limited_diff = c.diffset.limited_diff
584
584
585 # calculate removed files that are bound to comments
585 # calculate removed files that are bound to comments
586 comment_deleted_files = [
586 comment_deleted_files = [
587 fname for fname in display_inline_comments
587 fname for fname in display_inline_comments
588 if fname not in c.diffset.file_stats]
588 if fname not in c.diffset.file_stats]
589
589
590 c.deleted_files_comments = collections.defaultdict(dict)
590 c.deleted_files_comments = collections.defaultdict(dict)
591 for fname, per_line_comments in display_inline_comments.items():
591 for fname, per_line_comments in display_inline_comments.items():
592 if fname in comment_deleted_files:
592 if fname in comment_deleted_files:
593 c.deleted_files_comments[fname]['stats'] = 0
593 c.deleted_files_comments[fname]['stats'] = 0
594 c.deleted_files_comments[fname]['comments'] = list()
594 c.deleted_files_comments[fname]['comments'] = list()
595 for lno, comments in per_line_comments.items():
595 for lno, comments in per_line_comments.items():
596 c.deleted_files_comments[fname]['comments'].extend(comments)
596 c.deleted_files_comments[fname]['comments'].extend(comments)
597
597
598 # maybe calculate the range diff
598 # maybe calculate the range diff
599 if c.range_diff_on:
599 if c.range_diff_on:
600 # TODO(marcink): set whitespace/context
600 # TODO(marcink): set whitespace/context
601 context_lcl = 3
601 context_lcl = 3
602 ign_whitespace_lcl = False
602 ign_whitespace_lcl = False
603
603
604 for commit in c.commit_ranges:
604 for commit in c.commit_ranges:
605 commit2 = commit
605 commit2 = commit
606 commit1 = commit.first_parent
606 commit1 = commit.first_parent
607
607
608 range_diff_cache_file_path = diff_cache_exist(
608 range_diff_cache_file_path = diff_cache_exist(
609 cache_path, 'diff', commit.raw_id,
609 cache_path, 'diff', commit.raw_id,
610 ign_whitespace_lcl, context_lcl, c.fulldiff)
610 ign_whitespace_lcl, context_lcl, c.fulldiff)
611
611
612 cached_diff = None
612 cached_diff = None
613 if caching_enabled:
613 if caching_enabled:
614 cached_diff = load_cached_diff(range_diff_cache_file_path)
614 cached_diff = load_cached_diff(range_diff_cache_file_path)
615
615
616 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
616 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
617 if not force_recache and has_proper_diff_cache:
617 if not force_recache and has_proper_diff_cache:
618 diffset = cached_diff['diff']
618 diffset = cached_diff['diff']
619 else:
619 else:
620 diffset = self._get_range_diffset(
620 diffset = self._get_range_diffset(
621 source_scm, source_repo,
621 source_scm, source_repo,
622 commit1, commit2, diff_limit, file_limit,
622 commit1, commit2, diff_limit, file_limit,
623 c.fulldiff, ign_whitespace_lcl, context_lcl
623 c.fulldiff, ign_whitespace_lcl, context_lcl
624 )
624 )
625
625
626 # save cached diff
626 # save cached diff
627 if caching_enabled:
627 if caching_enabled:
628 cache_diff(range_diff_cache_file_path, diffset, None)
628 cache_diff(range_diff_cache_file_path, diffset, None)
629
629
630 c.changes[commit.raw_id] = diffset
630 c.changes[commit.raw_id] = diffset
631
631
632 # this is a hack to properly display links, when creating PR, the
632 # this is a hack to properly display links, when creating PR, the
633 # compare view and others uses different notation, and
633 # compare view and others uses different notation, and
634 # compare_commits.mako renders links based on the target_repo.
634 # compare_commits.mako renders links based on the target_repo.
635 # We need to swap that here to generate it properly on the html side
635 # We need to swap that here to generate it properly on the html side
636 c.target_repo = c.source_repo
636 c.target_repo = c.source_repo
637
637
638 c.commit_statuses = ChangesetStatus.STATUSES
638 c.commit_statuses = ChangesetStatus.STATUSES
639
639
640 c.show_version_changes = not pr_closed
640 c.show_version_changes = not pr_closed
641 if c.show_version_changes:
641 if c.show_version_changes:
642 cur_obj = pull_request_at_ver
642 cur_obj = pull_request_at_ver
643 prev_obj = prev_pull_request_at_ver
643 prev_obj = prev_pull_request_at_ver
644
644
645 old_commit_ids = prev_obj.revisions
645 old_commit_ids = prev_obj.revisions
646 new_commit_ids = cur_obj.revisions
646 new_commit_ids = cur_obj.revisions
647 commit_changes = PullRequestModel()._calculate_commit_id_changes(
647 commit_changes = PullRequestModel()._calculate_commit_id_changes(
648 old_commit_ids, new_commit_ids)
648 old_commit_ids, new_commit_ids)
649 c.commit_changes_summary = commit_changes
649 c.commit_changes_summary = commit_changes
650
650
651 # calculate the diff for commits between versions
651 # calculate the diff for commits between versions
652 c.commit_changes = []
652 c.commit_changes = []
653 mark = lambda cs, fw: list(
653 mark = lambda cs, fw: list(
654 h.itertools.izip_longest([], cs, fillvalue=fw))
654 h.itertools.izip_longest([], cs, fillvalue=fw))
655 for c_type, raw_id in mark(commit_changes.added, 'a') \
655 for c_type, raw_id in mark(commit_changes.added, 'a') \
656 + mark(commit_changes.removed, 'r') \
656 + mark(commit_changes.removed, 'r') \
657 + mark(commit_changes.common, 'c'):
657 + mark(commit_changes.common, 'c'):
658
658
659 if raw_id in commit_cache:
659 if raw_id in commit_cache:
660 commit = commit_cache[raw_id]
660 commit = commit_cache[raw_id]
661 else:
661 else:
662 try:
662 try:
663 commit = commits_source_repo.get_commit(raw_id)
663 commit = commits_source_repo.get_commit(raw_id)
664 except CommitDoesNotExistError:
664 except CommitDoesNotExistError:
665 # in case we fail extracting still use "dummy" commit
665 # in case we fail extracting still use "dummy" commit
666 # for display in commit diff
666 # for display in commit diff
667 commit = h.AttributeDict(
667 commit = h.AttributeDict(
668 {'raw_id': raw_id,
668 {'raw_id': raw_id,
669 'message': 'EMPTY or MISSING COMMIT'})
669 'message': 'EMPTY or MISSING COMMIT'})
670 c.commit_changes.append([c_type, commit])
670 c.commit_changes.append([c_type, commit])
671
671
672 # current user review statuses for each version
672 # current user review statuses for each version
673 c.review_versions = {}
673 c.review_versions = {}
674 if self._rhodecode_user.user_id in allowed_reviewers:
674 if self._rhodecode_user.user_id in allowed_reviewers:
675 for co in general_comments:
675 for co in general_comments:
676 if co.author.user_id == self._rhodecode_user.user_id:
676 if co.author.user_id == self._rhodecode_user.user_id:
677 status = co.status_change
677 status = co.status_change
678 if status:
678 if status:
679 _ver_pr = status[0].comment.pull_request_version_id
679 _ver_pr = status[0].comment.pull_request_version_id
680 c.review_versions[_ver_pr] = status[0]
680 c.review_versions[_ver_pr] = status[0]
681
681
682 return self._get_template_context(c)
682 return self._get_template_context(c)
683
683
684 def get_commits(
684 def get_commits(
685 self, commits_source_repo, pull_request_at_ver, source_commit,
685 self, commits_source_repo, pull_request_at_ver, source_commit,
686 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
686 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
687 commit_cache = collections.OrderedDict()
687 commit_cache = collections.OrderedDict()
688 missing_requirements = False
688 missing_requirements = False
689 try:
689 try:
690 pre_load = ["author", "branch", "date", "message", "parents"]
690 pre_load = ["author", "branch", "date", "message", "parents"]
691 show_revs = pull_request_at_ver.revisions
691 show_revs = pull_request_at_ver.revisions
692 for rev in show_revs:
692 for rev in show_revs:
693 comm = commits_source_repo.get_commit(
693 comm = commits_source_repo.get_commit(
694 commit_id=rev, pre_load=pre_load)
694 commit_id=rev, pre_load=pre_load)
695 commit_cache[comm.raw_id] = comm
695 commit_cache[comm.raw_id] = comm
696
696
697 # Order here matters, we first need to get target, and then
697 # Order here matters, we first need to get target, and then
698 # the source
698 # the source
699 target_commit = commits_source_repo.get_commit(
699 target_commit = commits_source_repo.get_commit(
700 commit_id=safe_str(target_ref_id))
700 commit_id=safe_str(target_ref_id))
701
701
702 source_commit = commits_source_repo.get_commit(
702 source_commit = commits_source_repo.get_commit(
703 commit_id=safe_str(source_ref_id))
703 commit_id=safe_str(source_ref_id))
704 except CommitDoesNotExistError:
704 except CommitDoesNotExistError:
705 log.warning(
705 log.warning(
706 'Failed to get commit from `{}` repo'.format(
706 'Failed to get commit from `{}` repo'.format(
707 commits_source_repo), exc_info=True)
707 commits_source_repo), exc_info=True)
708 except RepositoryRequirementError:
708 except RepositoryRequirementError:
709 log.warning(
709 log.warning(
710 'Failed to get all required data from repo', exc_info=True)
710 'Failed to get all required data from repo', exc_info=True)
711 missing_requirements = True
711 missing_requirements = True
712 ancestor_commit = None
712 ancestor_commit = None
713 try:
713 try:
714 ancestor_id = source_scm.get_common_ancestor(
714 ancestor_id = source_scm.get_common_ancestor(
715 source_commit.raw_id, target_commit.raw_id, target_scm)
715 source_commit.raw_id, target_commit.raw_id, target_scm)
716 ancestor_commit = source_scm.get_commit(ancestor_id)
716 ancestor_commit = source_scm.get_commit(ancestor_id)
717 except Exception:
717 except Exception:
718 ancestor_commit = None
718 ancestor_commit = None
719 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
719 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
720
720
721 def assure_not_empty_repo(self):
721 def assure_not_empty_repo(self):
722 _ = self.request.translate
722 _ = self.request.translate
723
723
724 try:
724 try:
725 self.db_repo.scm_instance().get_commit()
725 self.db_repo.scm_instance().get_commit()
726 except EmptyRepositoryError:
726 except EmptyRepositoryError:
727 h.flash(h.literal(_('There are no commits yet')),
727 h.flash(h.literal(_('There are no commits yet')),
728 category='warning')
728 category='warning')
729 raise HTTPFound(
729 raise HTTPFound(
730 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
730 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
731
731
732 @LoginRequired()
732 @LoginRequired()
733 @NotAnonymous()
733 @NotAnonymous()
734 @HasRepoPermissionAnyDecorator(
734 @HasRepoPermissionAnyDecorator(
735 'repository.read', 'repository.write', 'repository.admin')
735 'repository.read', 'repository.write', 'repository.admin')
736 @view_config(
736 @view_config(
737 route_name='pullrequest_new', request_method='GET',
737 route_name='pullrequest_new', request_method='GET',
738 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
738 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
739 def pull_request_new(self):
739 def pull_request_new(self):
740 _ = self.request.translate
740 _ = self.request.translate
741 c = self.load_default_context()
741 c = self.load_default_context()
742
742
743 self.assure_not_empty_repo()
743 self.assure_not_empty_repo()
744 source_repo = self.db_repo
744 source_repo = self.db_repo
745
745
746 commit_id = self.request.GET.get('commit')
746 commit_id = self.request.GET.get('commit')
747 branch_ref = self.request.GET.get('branch')
747 branch_ref = self.request.GET.get('branch')
748 bookmark_ref = self.request.GET.get('bookmark')
748 bookmark_ref = self.request.GET.get('bookmark')
749
749
750 try:
750 try:
751 source_repo_data = PullRequestModel().generate_repo_data(
751 source_repo_data = PullRequestModel().generate_repo_data(
752 source_repo, commit_id=commit_id,
752 source_repo, commit_id=commit_id,
753 branch=branch_ref, bookmark=bookmark_ref,
753 branch=branch_ref, bookmark=bookmark_ref,
754 translator=self.request.translate)
754 translator=self.request.translate)
755 except CommitDoesNotExistError as e:
755 except CommitDoesNotExistError as e:
756 log.exception(e)
756 log.exception(e)
757 h.flash(_('Commit does not exist'), 'error')
757 h.flash(_('Commit does not exist'), 'error')
758 raise HTTPFound(
758 raise HTTPFound(
759 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
759 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
760
760
761 default_target_repo = source_repo
761 default_target_repo = source_repo
762
762
763 if source_repo.parent and c.has_origin_repo_read_perm:
763 if source_repo.parent and c.has_origin_repo_read_perm:
764 parent_vcs_obj = source_repo.parent.scm_instance()
764 parent_vcs_obj = source_repo.parent.scm_instance()
765 if parent_vcs_obj and not parent_vcs_obj.is_empty():
765 if parent_vcs_obj and not parent_vcs_obj.is_empty():
766 # change default if we have a parent repo
766 # change default if we have a parent repo
767 default_target_repo = source_repo.parent
767 default_target_repo = source_repo.parent
768
768
769 target_repo_data = PullRequestModel().generate_repo_data(
769 target_repo_data = PullRequestModel().generate_repo_data(
770 default_target_repo, translator=self.request.translate)
770 default_target_repo, translator=self.request.translate)
771
771
772 selected_source_ref = source_repo_data['refs']['selected_ref']
772 selected_source_ref = source_repo_data['refs']['selected_ref']
773 title_source_ref = ''
773 title_source_ref = ''
774 if selected_source_ref:
774 if selected_source_ref:
775 title_source_ref = selected_source_ref.split(':', 2)[1]
775 title_source_ref = selected_source_ref.split(':', 2)[1]
776 c.default_title = PullRequestModel().generate_pullrequest_title(
776 c.default_title = PullRequestModel().generate_pullrequest_title(
777 source=source_repo.repo_name,
777 source=source_repo.repo_name,
778 source_ref=title_source_ref,
778 source_ref=title_source_ref,
779 target=default_target_repo.repo_name
779 target=default_target_repo.repo_name
780 )
780 )
781
781
782 c.default_repo_data = {
782 c.default_repo_data = {
783 'source_repo_name': source_repo.repo_name,
783 'source_repo_name': source_repo.repo_name,
784 'source_refs_json': json.dumps(source_repo_data),
784 'source_refs_json': json.dumps(source_repo_data),
785 'target_repo_name': default_target_repo.repo_name,
785 'target_repo_name': default_target_repo.repo_name,
786 'target_refs_json': json.dumps(target_repo_data),
786 'target_refs_json': json.dumps(target_repo_data),
787 }
787 }
788 c.default_source_ref = selected_source_ref
788 c.default_source_ref = selected_source_ref
789
789
790 return self._get_template_context(c)
790 return self._get_template_context(c)
791
791
792 @LoginRequired()
792 @LoginRequired()
793 @NotAnonymous()
793 @NotAnonymous()
794 @HasRepoPermissionAnyDecorator(
794 @HasRepoPermissionAnyDecorator(
795 'repository.read', 'repository.write', 'repository.admin')
795 'repository.read', 'repository.write', 'repository.admin')
796 @view_config(
796 @view_config(
797 route_name='pullrequest_repo_refs', request_method='GET',
797 route_name='pullrequest_repo_refs', request_method='GET',
798 renderer='json_ext', xhr=True)
798 renderer='json_ext', xhr=True)
799 def pull_request_repo_refs(self):
799 def pull_request_repo_refs(self):
800 self.load_default_context()
800 self.load_default_context()
801 target_repo_name = self.request.matchdict['target_repo_name']
801 target_repo_name = self.request.matchdict['target_repo_name']
802 repo = Repository.get_by_repo_name(target_repo_name)
802 repo = Repository.get_by_repo_name(target_repo_name)
803 if not repo:
803 if not repo:
804 raise HTTPNotFound()
804 raise HTTPNotFound()
805
805
806 target_perm = HasRepoPermissionAny(
806 target_perm = HasRepoPermissionAny(
807 'repository.read', 'repository.write', 'repository.admin')(
807 'repository.read', 'repository.write', 'repository.admin')(
808 target_repo_name)
808 target_repo_name)
809 if not target_perm:
809 if not target_perm:
810 raise HTTPNotFound()
810 raise HTTPNotFound()
811
811
812 return PullRequestModel().generate_repo_data(
812 return PullRequestModel().generate_repo_data(
813 repo, translator=self.request.translate)
813 repo, translator=self.request.translate)
814
814
815 @LoginRequired()
815 @LoginRequired()
816 @NotAnonymous()
816 @NotAnonymous()
817 @HasRepoPermissionAnyDecorator(
817 @HasRepoPermissionAnyDecorator(
818 'repository.read', 'repository.write', 'repository.admin')
818 'repository.read', 'repository.write', 'repository.admin')
819 @view_config(
819 @view_config(
820 route_name='pullrequest_repo_targets', request_method='GET',
820 route_name='pullrequest_repo_targets', request_method='GET',
821 renderer='json_ext', xhr=True)
821 renderer='json_ext', xhr=True)
822 def pullrequest_repo_targets(self):
822 def pullrequest_repo_targets(self):
823 _ = self.request.translate
823 _ = self.request.translate
824 filter_query = self.request.GET.get('query')
824 filter_query = self.request.GET.get('query')
825
825
826 # get the parents
826 # get the parents
827 parent_target_repos = []
827 parent_target_repos = []
828 if self.db_repo.parent:
828 if self.db_repo.parent:
829 parents_query = Repository.query() \
829 parents_query = Repository.query() \
830 .order_by(func.length(Repository.repo_name)) \
830 .order_by(func.length(Repository.repo_name)) \
831 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
831 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
832
832
833 if filter_query:
833 if filter_query:
834 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
834 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
835 parents_query = parents_query.filter(
835 parents_query = parents_query.filter(
836 Repository.repo_name.ilike(ilike_expression))
836 Repository.repo_name.ilike(ilike_expression))
837 parents = parents_query.limit(20).all()
837 parents = parents_query.limit(20).all()
838
838
839 for parent in parents:
839 for parent in parents:
840 parent_vcs_obj = parent.scm_instance()
840 parent_vcs_obj = parent.scm_instance()
841 if parent_vcs_obj and not parent_vcs_obj.is_empty():
841 if parent_vcs_obj and not parent_vcs_obj.is_empty():
842 parent_target_repos.append(parent)
842 parent_target_repos.append(parent)
843
843
844 # get other forks, and repo itself
844 # get other forks, and repo itself
845 query = Repository.query() \
845 query = Repository.query() \
846 .order_by(func.length(Repository.repo_name)) \
846 .order_by(func.length(Repository.repo_name)) \
847 .filter(
847 .filter(
848 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
848 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
849 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
849 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
850 ) \
850 ) \
851 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
851 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
852
852
853 if filter_query:
853 if filter_query:
854 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
854 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
855 query = query.filter(Repository.repo_name.ilike(ilike_expression))
855 query = query.filter(Repository.repo_name.ilike(ilike_expression))
856
856
857 limit = max(20 - len(parent_target_repos), 5) # not less then 5
857 limit = max(20 - len(parent_target_repos), 5) # not less then 5
858 target_repos = query.limit(limit).all()
858 target_repos = query.limit(limit).all()
859
859
860 all_target_repos = target_repos + parent_target_repos
860 all_target_repos = target_repos + parent_target_repos
861
861
862 repos = []
862 repos = []
863 # This checks permissions to the repositories
863 # This checks permissions to the repositories
864 for obj in ScmModel().get_repos(all_target_repos):
864 for obj in ScmModel().get_repos(all_target_repos):
865 repos.append({
865 repos.append({
866 'id': obj['name'],
866 'id': obj['name'],
867 'text': obj['name'],
867 'text': obj['name'],
868 'type': 'repo',
868 'type': 'repo',
869 'repo_id': obj['dbrepo']['repo_id'],
869 'repo_id': obj['dbrepo']['repo_id'],
870 'repo_type': obj['dbrepo']['repo_type'],
870 'repo_type': obj['dbrepo']['repo_type'],
871 'private': obj['dbrepo']['private'],
871 'private': obj['dbrepo']['private'],
872
872
873 })
873 })
874
874
875 data = {
875 data = {
876 'more': False,
876 'more': False,
877 'results': [{
877 'results': [{
878 'text': _('Repositories'),
878 'text': _('Repositories'),
879 'children': repos
879 'children': repos
880 }] if repos else []
880 }] if repos else []
881 }
881 }
882 return data
882 return data
883
883
884 @LoginRequired()
884 @LoginRequired()
885 @NotAnonymous()
885 @NotAnonymous()
886 @HasRepoPermissionAnyDecorator(
886 @HasRepoPermissionAnyDecorator(
887 'repository.read', 'repository.write', 'repository.admin')
887 'repository.read', 'repository.write', 'repository.admin')
888 @CSRFRequired()
888 @CSRFRequired()
889 @view_config(
889 @view_config(
890 route_name='pullrequest_create', request_method='POST',
890 route_name='pullrequest_create', request_method='POST',
891 renderer=None)
891 renderer=None)
892 def pull_request_create(self):
892 def pull_request_create(self):
893 _ = self.request.translate
893 _ = self.request.translate
894 self.assure_not_empty_repo()
894 self.assure_not_empty_repo()
895 self.load_default_context()
895 self.load_default_context()
896
896
897 controls = peppercorn.parse(self.request.POST.items())
897 controls = peppercorn.parse(self.request.POST.items())
898
898
899 try:
899 try:
900 form = PullRequestForm(
900 form = PullRequestForm(
901 self.request.translate, self.db_repo.repo_id)()
901 self.request.translate, self.db_repo.repo_id)()
902 _form = form.to_python(controls)
902 _form = form.to_python(controls)
903 except formencode.Invalid as errors:
903 except formencode.Invalid as errors:
904 if errors.error_dict.get('revisions'):
904 if errors.error_dict.get('revisions'):
905 msg = 'Revisions: %s' % errors.error_dict['revisions']
905 msg = 'Revisions: %s' % errors.error_dict['revisions']
906 elif errors.error_dict.get('pullrequest_title'):
906 elif errors.error_dict.get('pullrequest_title'):
907 msg = errors.error_dict.get('pullrequest_title')
907 msg = errors.error_dict.get('pullrequest_title')
908 else:
908 else:
909 msg = _('Error creating pull request: {}').format(errors)
909 msg = _('Error creating pull request: {}').format(errors)
910 log.exception(msg)
910 log.exception(msg)
911 h.flash(msg, 'error')
911 h.flash(msg, 'error')
912
912
913 # would rather just go back to form ...
913 # would rather just go back to form ...
914 raise HTTPFound(
914 raise HTTPFound(
915 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
915 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
916
916
917 source_repo = _form['source_repo']
917 source_repo = _form['source_repo']
918 source_ref = _form['source_ref']
918 source_ref = _form['source_ref']
919 target_repo = _form['target_repo']
919 target_repo = _form['target_repo']
920 target_ref = _form['target_ref']
920 target_ref = _form['target_ref']
921 commit_ids = _form['revisions'][::-1]
921 commit_ids = _form['revisions'][::-1]
922
922
923 # find the ancestor for this pr
923 # find the ancestor for this pr
924 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
924 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
925 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
925 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
926
926
927 if not (source_db_repo or target_db_repo):
927 if not (source_db_repo or target_db_repo):
928 h.flash(_('source_repo or target repo not found'), category='error')
928 h.flash(_('source_repo or target repo not found'), category='error')
929 raise HTTPFound(
929 raise HTTPFound(
930 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
930 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
931
931
932 # re-check permissions again here
932 # re-check permissions again here
933 # source_repo we must have read permissions
933 # source_repo we must have read permissions
934
934
935 source_perm = HasRepoPermissionAny(
935 source_perm = HasRepoPermissionAny(
936 'repository.read', 'repository.write', 'repository.admin')(
936 'repository.read', 'repository.write', 'repository.admin')(
937 source_db_repo.repo_name)
937 source_db_repo.repo_name)
938 if not source_perm:
938 if not source_perm:
939 msg = _('Not Enough permissions to source repo `{}`.'.format(
939 msg = _('Not Enough permissions to source repo `{}`.'.format(
940 source_db_repo.repo_name))
940 source_db_repo.repo_name))
941 h.flash(msg, category='error')
941 h.flash(msg, category='error')
942 # copy the args back to redirect
942 # copy the args back to redirect
943 org_query = self.request.GET.mixed()
943 org_query = self.request.GET.mixed()
944 raise HTTPFound(
944 raise HTTPFound(
945 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
945 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
946 _query=org_query))
946 _query=org_query))
947
947
948 # target repo we must have read permissions, and also later on
948 # target repo we must have read permissions, and also later on
949 # we want to check branch permissions here
949 # we want to check branch permissions here
950 target_perm = HasRepoPermissionAny(
950 target_perm = HasRepoPermissionAny(
951 'repository.read', 'repository.write', 'repository.admin')(
951 'repository.read', 'repository.write', 'repository.admin')(
952 target_db_repo.repo_name)
952 target_db_repo.repo_name)
953 if not target_perm:
953 if not target_perm:
954 msg = _('Not Enough permissions to target repo `{}`.'.format(
954 msg = _('Not Enough permissions to target repo `{}`.'.format(
955 target_db_repo.repo_name))
955 target_db_repo.repo_name))
956 h.flash(msg, category='error')
956 h.flash(msg, category='error')
957 # copy the args back to redirect
957 # copy the args back to redirect
958 org_query = self.request.GET.mixed()
958 org_query = self.request.GET.mixed()
959 raise HTTPFound(
959 raise HTTPFound(
960 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
960 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
961 _query=org_query))
961 _query=org_query))
962
962
963 source_scm = source_db_repo.scm_instance()
963 source_scm = source_db_repo.scm_instance()
964 target_scm = target_db_repo.scm_instance()
964 target_scm = target_db_repo.scm_instance()
965
965
966 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
966 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
967 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
967 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
968
968
969 ancestor = source_scm.get_common_ancestor(
969 ancestor = source_scm.get_common_ancestor(
970 source_commit.raw_id, target_commit.raw_id, target_scm)
970 source_commit.raw_id, target_commit.raw_id, target_scm)
971
971
972 # recalculate target ref based on ancestor
972 # recalculate target ref based on ancestor
973 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
973 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
974 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
974 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
975
975
976 get_default_reviewers_data, validate_default_reviewers = \
976 get_default_reviewers_data, validate_default_reviewers = \
977 PullRequestModel().get_reviewer_functions()
977 PullRequestModel().get_reviewer_functions()
978
978
979 # recalculate reviewers logic, to make sure we can validate this
979 # recalculate reviewers logic, to make sure we can validate this
980 reviewer_rules = get_default_reviewers_data(
980 reviewer_rules = get_default_reviewers_data(
981 self._rhodecode_db_user, source_db_repo,
981 self._rhodecode_db_user, source_db_repo,
982 source_commit, target_db_repo, target_commit)
982 source_commit, target_db_repo, target_commit)
983
983
984 given_reviewers = _form['review_members']
984 given_reviewers = _form['review_members']
985 reviewers = validate_default_reviewers(
985 reviewers = validate_default_reviewers(
986 given_reviewers, reviewer_rules)
986 given_reviewers, reviewer_rules)
987
987
988 pullrequest_title = _form['pullrequest_title']
988 pullrequest_title = _form['pullrequest_title']
989 title_source_ref = source_ref.split(':', 2)[1]
989 title_source_ref = source_ref.split(':', 2)[1]
990 if not pullrequest_title:
990 if not pullrequest_title:
991 pullrequest_title = PullRequestModel().generate_pullrequest_title(
991 pullrequest_title = PullRequestModel().generate_pullrequest_title(
992 source=source_repo,
992 source=source_repo,
993 source_ref=title_source_ref,
993 source_ref=title_source_ref,
994 target=target_repo
994 target=target_repo
995 )
995 )
996
996
997 description = _form['pullrequest_desc']
997 description = _form['pullrequest_desc']
998 description_renderer = _form['description_renderer']
998 description_renderer = _form['description_renderer']
999
999
1000 try:
1000 try:
1001 pull_request = PullRequestModel().create(
1001 pull_request = PullRequestModel().create(
1002 created_by=self._rhodecode_user.user_id,
1002 created_by=self._rhodecode_user.user_id,
1003 source_repo=source_repo,
1003 source_repo=source_repo,
1004 source_ref=source_ref,
1004 source_ref=source_ref,
1005 target_repo=target_repo,
1005 target_repo=target_repo,
1006 target_ref=target_ref,
1006 target_ref=target_ref,
1007 revisions=commit_ids,
1007 revisions=commit_ids,
1008 reviewers=reviewers,
1008 reviewers=reviewers,
1009 title=pullrequest_title,
1009 title=pullrequest_title,
1010 description=description,
1010 description=description,
1011 description_renderer=description_renderer,
1011 description_renderer=description_renderer,
1012 reviewer_data=reviewer_rules,
1012 reviewer_data=reviewer_rules,
1013 auth_user=self._rhodecode_user
1013 auth_user=self._rhodecode_user
1014 )
1014 )
1015 Session().commit()
1015 Session().commit()
1016
1016
1017 h.flash(_('Successfully opened new pull request'),
1017 h.flash(_('Successfully opened new pull request'),
1018 category='success')
1018 category='success')
1019 except Exception:
1019 except Exception:
1020 msg = _('Error occurred during creation of this pull request.')
1020 msg = _('Error occurred during creation of this pull request.')
1021 log.exception(msg)
1021 log.exception(msg)
1022 h.flash(msg, category='error')
1022 h.flash(msg, category='error')
1023
1023
1024 # copy the args back to redirect
1024 # copy the args back to redirect
1025 org_query = self.request.GET.mixed()
1025 org_query = self.request.GET.mixed()
1026 raise HTTPFound(
1026 raise HTTPFound(
1027 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1027 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1028 _query=org_query))
1028 _query=org_query))
1029
1029
1030 raise HTTPFound(
1030 raise HTTPFound(
1031 h.route_path('pullrequest_show', repo_name=target_repo,
1031 h.route_path('pullrequest_show', repo_name=target_repo,
1032 pull_request_id=pull_request.pull_request_id))
1032 pull_request_id=pull_request.pull_request_id))
1033
1033
1034 @LoginRequired()
1034 @LoginRequired()
1035 @NotAnonymous()
1035 @NotAnonymous()
1036 @HasRepoPermissionAnyDecorator(
1036 @HasRepoPermissionAnyDecorator(
1037 'repository.read', 'repository.write', 'repository.admin')
1037 'repository.read', 'repository.write', 'repository.admin')
1038 @CSRFRequired()
1038 @CSRFRequired()
1039 @view_config(
1039 @view_config(
1040 route_name='pullrequest_update', request_method='POST',
1040 route_name='pullrequest_update', request_method='POST',
1041 renderer='json_ext')
1041 renderer='json_ext')
1042 def pull_request_update(self):
1042 def pull_request_update(self):
1043 pull_request = PullRequest.get_or_404(
1043 pull_request = PullRequest.get_or_404(
1044 self.request.matchdict['pull_request_id'])
1044 self.request.matchdict['pull_request_id'])
1045 _ = self.request.translate
1045 _ = self.request.translate
1046
1046
1047 self.load_default_context()
1047 self.load_default_context()
1048
1048
1049 if pull_request.is_closed():
1049 if pull_request.is_closed():
1050 log.debug('update: forbidden because pull request is closed')
1050 log.debug('update: forbidden because pull request is closed')
1051 msg = _(u'Cannot update closed pull requests.')
1051 msg = _(u'Cannot update closed pull requests.')
1052 h.flash(msg, category='error')
1052 h.flash(msg, category='error')
1053 return True
1053 return True
1054
1054
1055 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1055 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1056 log.debug('update: forbidden because pull request is in state %s',
1056 log.debug('update: forbidden because pull request is in state %s',
1057 pull_request.pull_request_state)
1057 pull_request.pull_request_state)
1058 msg = _(u'Cannot update pull requests in state other than `{}`. '
1058 msg = _(u'Cannot update pull requests in state other than `{}`. '
1059 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1059 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1060 pull_request.pull_request_state)
1060 pull_request.pull_request_state)
1061 h.flash(msg, category='error')
1061 h.flash(msg, category='error')
1062 return True
1062 return True
1063
1063
1064 # only owner or admin can update it
1064 # only owner or admin can update it
1065 allowed_to_update = PullRequestModel().check_user_update(
1065 allowed_to_update = PullRequestModel().check_user_update(
1066 pull_request, self._rhodecode_user)
1066 pull_request, self._rhodecode_user)
1067 if allowed_to_update:
1067 if allowed_to_update:
1068 controls = peppercorn.parse(self.request.POST.items())
1068 controls = peppercorn.parse(self.request.POST.items())
1069
1069
1070 if 'review_members' in controls:
1070 if 'review_members' in controls:
1071 self._update_reviewers(
1071 self._update_reviewers(
1072 pull_request, controls['review_members'],
1072 pull_request, controls['review_members'],
1073 pull_request.reviewer_data)
1073 pull_request.reviewer_data)
1074 elif str2bool(self.request.POST.get('update_commits', 'false')):
1074 elif str2bool(self.request.POST.get('update_commits', 'false')):
1075 self._update_commits(pull_request)
1075 self._update_commits(pull_request)
1076 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1076 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1077 self._edit_pull_request(pull_request)
1077 self._edit_pull_request(pull_request)
1078 else:
1078 else:
1079 raise HTTPBadRequest()
1079 raise HTTPBadRequest()
1080 return True
1080 return True
1081 raise HTTPForbidden()
1081 raise HTTPForbidden()
1082
1082
1083 def _edit_pull_request(self, pull_request):
1083 def _edit_pull_request(self, pull_request):
1084 _ = self.request.translate
1084 _ = self.request.translate
1085
1085
1086 try:
1086 try:
1087 PullRequestModel().edit(
1087 PullRequestModel().edit(
1088 pull_request,
1088 pull_request,
1089 self.request.POST.get('title'),
1089 self.request.POST.get('title'),
1090 self.request.POST.get('description'),
1090 self.request.POST.get('description'),
1091 self.request.POST.get('description_renderer'),
1091 self.request.POST.get('description_renderer'),
1092 self._rhodecode_user)
1092 self._rhodecode_user)
1093 except ValueError:
1093 except ValueError:
1094 msg = _(u'Cannot update closed pull requests.')
1094 msg = _(u'Cannot update closed pull requests.')
1095 h.flash(msg, category='error')
1095 h.flash(msg, category='error')
1096 return
1096 return
1097 else:
1097 else:
1098 Session().commit()
1098 Session().commit()
1099
1099
1100 msg = _(u'Pull request title & description updated.')
1100 msg = _(u'Pull request title & description updated.')
1101 h.flash(msg, category='success')
1101 h.flash(msg, category='success')
1102 return
1102 return
1103
1103
1104 def _update_commits(self, pull_request):
1104 def _update_commits(self, pull_request):
1105 _ = self.request.translate
1105 _ = self.request.translate
1106
1106
1107 with pull_request.set_state(PullRequest.STATE_UPDATING):
1107 with pull_request.set_state(PullRequest.STATE_UPDATING):
1108 resp = PullRequestModel().update_commits(pull_request)
1108 resp = PullRequestModel().update_commits(pull_request)
1109
1109
1110 if resp.executed:
1110 if resp.executed:
1111
1111
1112 if resp.target_changed and resp.source_changed:
1112 if resp.target_changed and resp.source_changed:
1113 changed = 'target and source repositories'
1113 changed = 'target and source repositories'
1114 elif resp.target_changed and not resp.source_changed:
1114 elif resp.target_changed and not resp.source_changed:
1115 changed = 'target repository'
1115 changed = 'target repository'
1116 elif not resp.target_changed and resp.source_changed:
1116 elif not resp.target_changed and resp.source_changed:
1117 changed = 'source repository'
1117 changed = 'source repository'
1118 else:
1118 else:
1119 changed = 'nothing'
1119 changed = 'nothing'
1120
1120
1121 msg = _(u'Pull request updated to "{source_commit_id}" with '
1121 msg = _(u'Pull request updated to "{source_commit_id}" with '
1122 u'{count_added} added, {count_removed} removed commits. '
1122 u'{count_added} added, {count_removed} removed commits. '
1123 u'Source of changes: {change_source}')
1123 u'Source of changes: {change_source}')
1124 msg = msg.format(
1124 msg = msg.format(
1125 source_commit_id=pull_request.source_ref_parts.commit_id,
1125 source_commit_id=pull_request.source_ref_parts.commit_id,
1126 count_added=len(resp.changes.added),
1126 count_added=len(resp.changes.added),
1127 count_removed=len(resp.changes.removed),
1127 count_removed=len(resp.changes.removed),
1128 change_source=changed)
1128 change_source=changed)
1129 h.flash(msg, category='success')
1129 h.flash(msg, category='success')
1130
1130
1131 channel = '/repo${}$/pr/{}'.format(
1131 channel = '/repo${}$/pr/{}'.format(
1132 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1132 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1133 message = msg + (
1133 message = msg + (
1134 ' - <a onclick="window.location.reload()">'
1134 ' - <a onclick="window.location.reload()">'
1135 '<strong>{}</strong></a>'.format(_('Reload page')))
1135 '<strong>{}</strong></a>'.format(_('Reload page')))
1136 channelstream.post_message(
1136 channelstream.post_message(
1137 channel, message, self._rhodecode_user.username,
1137 channel, message, self._rhodecode_user.username,
1138 registry=self.request.registry)
1138 registry=self.request.registry)
1139 else:
1139 else:
1140 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1140 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1141 warning_reasons = [
1141 warning_reasons = [
1142 UpdateFailureReason.NO_CHANGE,
1142 UpdateFailureReason.NO_CHANGE,
1143 UpdateFailureReason.WRONG_REF_TYPE,
1143 UpdateFailureReason.WRONG_REF_TYPE,
1144 ]
1144 ]
1145 category = 'warning' if resp.reason in warning_reasons else 'error'
1145 category = 'warning' if resp.reason in warning_reasons else 'error'
1146 h.flash(msg, category=category)
1146 h.flash(msg, category=category)
1147
1147
1148 @LoginRequired()
1148 @LoginRequired()
1149 @NotAnonymous()
1149 @NotAnonymous()
1150 @HasRepoPermissionAnyDecorator(
1150 @HasRepoPermissionAnyDecorator(
1151 'repository.read', 'repository.write', 'repository.admin')
1151 'repository.read', 'repository.write', 'repository.admin')
1152 @CSRFRequired()
1152 @CSRFRequired()
1153 @view_config(
1153 @view_config(
1154 route_name='pullrequest_merge', request_method='POST',
1154 route_name='pullrequest_merge', request_method='POST',
1155 renderer='json_ext')
1155 renderer='json_ext')
1156 def pull_request_merge(self):
1156 def pull_request_merge(self):
1157 """
1157 """
1158 Merge will perform a server-side merge of the specified
1158 Merge will perform a server-side merge of the specified
1159 pull request, if the pull request is approved and mergeable.
1159 pull request, if the pull request is approved and mergeable.
1160 After successful merging, the pull request is automatically
1160 After successful merging, the pull request is automatically
1161 closed, with a relevant comment.
1161 closed, with a relevant comment.
1162 """
1162 """
1163 pull_request = PullRequest.get_or_404(
1163 pull_request = PullRequest.get_or_404(
1164 self.request.matchdict['pull_request_id'])
1164 self.request.matchdict['pull_request_id'])
1165 _ = self.request.translate
1165 _ = self.request.translate
1166
1166
1167 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1167 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
1168 log.debug('show: forbidden because pull request is in state %s',
1168 log.debug('show: forbidden because pull request is in state %s',
1169 pull_request.pull_request_state)
1169 pull_request.pull_request_state)
1170 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1170 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1171 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1171 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1172 pull_request.pull_request_state)
1172 pull_request.pull_request_state)
1173 h.flash(msg, category='error')
1173 h.flash(msg, category='error')
1174 raise HTTPFound(
1174 raise HTTPFound(
1175 h.route_path('pullrequest_show',
1175 h.route_path('pullrequest_show',
1176 repo_name=pull_request.target_repo.repo_name,
1176 repo_name=pull_request.target_repo.repo_name,
1177 pull_request_id=pull_request.pull_request_id))
1177 pull_request_id=pull_request.pull_request_id))
1178
1178
1179 self.load_default_context()
1179 self.load_default_context()
1180
1180
1181 with pull_request.set_state(PullRequest.STATE_UPDATING):
1181 with pull_request.set_state(PullRequest.STATE_UPDATING):
1182 check = MergeCheck.validate(
1182 check = MergeCheck.validate(
1183 pull_request, auth_user=self._rhodecode_user,
1183 pull_request, auth_user=self._rhodecode_user,
1184 translator=self.request.translate)
1184 translator=self.request.translate)
1185 merge_possible = not check.failed
1185 merge_possible = not check.failed
1186
1186
1187 for err_type, error_msg in check.errors:
1187 for err_type, error_msg in check.errors:
1188 h.flash(error_msg, category=err_type)
1188 h.flash(error_msg, category=err_type)
1189
1189
1190 if merge_possible:
1190 if merge_possible:
1191 log.debug("Pre-conditions checked, trying to merge.")
1191 log.debug("Pre-conditions checked, trying to merge.")
1192 extras = vcs_operation_context(
1192 extras = vcs_operation_context(
1193 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1193 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1194 username=self._rhodecode_db_user.username, action='push',
1194 username=self._rhodecode_db_user.username, action='push',
1195 scm=pull_request.target_repo.repo_type)
1195 scm=pull_request.target_repo.repo_type)
1196 with pull_request.set_state(PullRequest.STATE_UPDATING):
1196 with pull_request.set_state(PullRequest.STATE_UPDATING):
1197 self._merge_pull_request(
1197 self._merge_pull_request(
1198 pull_request, self._rhodecode_db_user, extras)
1198 pull_request, self._rhodecode_db_user, extras)
1199 else:
1199 else:
1200 log.debug("Pre-conditions failed, NOT merging.")
1200 log.debug("Pre-conditions failed, NOT merging.")
1201
1201
1202 raise HTTPFound(
1202 raise HTTPFound(
1203 h.route_path('pullrequest_show',
1203 h.route_path('pullrequest_show',
1204 repo_name=pull_request.target_repo.repo_name,
1204 repo_name=pull_request.target_repo.repo_name,
1205 pull_request_id=pull_request.pull_request_id))
1205 pull_request_id=pull_request.pull_request_id))
1206
1206
1207 def _merge_pull_request(self, pull_request, user, extras):
1207 def _merge_pull_request(self, pull_request, user, extras):
1208 _ = self.request.translate
1208 _ = self.request.translate
1209 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1209 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1210
1210
1211 if merge_resp.executed:
1211 if merge_resp.executed:
1212 log.debug("The merge was successful, closing the pull request.")
1212 log.debug("The merge was successful, closing the pull request.")
1213 PullRequestModel().close_pull_request(
1213 PullRequestModel().close_pull_request(
1214 pull_request.pull_request_id, user)
1214 pull_request.pull_request_id, user)
1215 Session().commit()
1215 Session().commit()
1216 msg = _('Pull request was successfully merged and closed.')
1216 msg = _('Pull request was successfully merged and closed.')
1217 h.flash(msg, category='success')
1217 h.flash(msg, category='success')
1218 else:
1218 else:
1219 log.debug(
1219 log.debug(
1220 "The merge was not successful. Merge response: %s", merge_resp)
1220 "The merge was not successful. Merge response: %s", merge_resp)
1221 msg = merge_resp.merge_status_message
1221 msg = merge_resp.merge_status_message
1222 h.flash(msg, category='error')
1222 h.flash(msg, category='error')
1223
1223
1224 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1224 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1225 _ = self.request.translate
1225 _ = self.request.translate
1226
1226
1227 get_default_reviewers_data, validate_default_reviewers = \
1227 get_default_reviewers_data, validate_default_reviewers = \
1228 PullRequestModel().get_reviewer_functions()
1228 PullRequestModel().get_reviewer_functions()
1229
1229
1230 try:
1230 try:
1231 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1231 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1232 except ValueError as e:
1232 except ValueError as e:
1233 log.error('Reviewers Validation: {}'.format(e))
1233 log.error('Reviewers Validation: {}'.format(e))
1234 h.flash(e, category='error')
1234 h.flash(e, category='error')
1235 return
1235 return
1236
1236
1237 old_calculated_status = pull_request.calculated_review_status()
1237 old_calculated_status = pull_request.calculated_review_status()
1238 PullRequestModel().update_reviewers(
1238 PullRequestModel().update_reviewers(
1239 pull_request, reviewers, self._rhodecode_user)
1239 pull_request, reviewers, self._rhodecode_user)
1240 h.flash(_('Pull request reviewers updated.'), category='success')
1240 h.flash(_('Pull request reviewers updated.'), category='success')
1241 Session().commit()
1241 Session().commit()
1242
1242
1243 # trigger status changed if change in reviewers changes the status
1243 # trigger status changed if change in reviewers changes the status
1244 calculated_status = pull_request.calculated_review_status()
1244 calculated_status = pull_request.calculated_review_status()
1245 if old_calculated_status != calculated_status:
1245 if old_calculated_status != calculated_status:
1246 PullRequestModel().trigger_pull_request_hook(
1246 PullRequestModel().trigger_pull_request_hook(
1247 pull_request, self._rhodecode_user, 'review_status_change',
1247 pull_request, self._rhodecode_user, 'review_status_change',
1248 data={'status': calculated_status})
1248 data={'status': calculated_status})
1249
1249
1250 @LoginRequired()
1250 @LoginRequired()
1251 @NotAnonymous()
1251 @NotAnonymous()
1252 @HasRepoPermissionAnyDecorator(
1252 @HasRepoPermissionAnyDecorator(
1253 'repository.read', 'repository.write', 'repository.admin')
1253 'repository.read', 'repository.write', 'repository.admin')
1254 @CSRFRequired()
1254 @CSRFRequired()
1255 @view_config(
1255 @view_config(
1256 route_name='pullrequest_delete', request_method='POST',
1256 route_name='pullrequest_delete', request_method='POST',
1257 renderer='json_ext')
1257 renderer='json_ext')
1258 def pull_request_delete(self):
1258 def pull_request_delete(self):
1259 _ = self.request.translate
1259 _ = self.request.translate
1260
1260
1261 pull_request = PullRequest.get_or_404(
1261 pull_request = PullRequest.get_or_404(
1262 self.request.matchdict['pull_request_id'])
1262 self.request.matchdict['pull_request_id'])
1263 self.load_default_context()
1263 self.load_default_context()
1264
1264
1265 pr_closed = pull_request.is_closed()
1265 pr_closed = pull_request.is_closed()
1266 allowed_to_delete = PullRequestModel().check_user_delete(
1266 allowed_to_delete = PullRequestModel().check_user_delete(
1267 pull_request, self._rhodecode_user) and not pr_closed
1267 pull_request, self._rhodecode_user) and not pr_closed
1268
1268
1269 # only owner can delete it !
1269 # only owner can delete it !
1270 if allowed_to_delete:
1270 if allowed_to_delete:
1271 PullRequestModel().delete(pull_request, self._rhodecode_user)
1271 PullRequestModel().delete(pull_request, self._rhodecode_user)
1272 Session().commit()
1272 Session().commit()
1273 h.flash(_('Successfully deleted pull request'),
1273 h.flash(_('Successfully deleted pull request'),
1274 category='success')
1274 category='success')
1275 raise HTTPFound(h.route_path('pullrequest_show_all',
1275 raise HTTPFound(h.route_path('pullrequest_show_all',
1276 repo_name=self.db_repo_name))
1276 repo_name=self.db_repo_name))
1277
1277
1278 log.warning('user %s tried to delete pull request without access',
1278 log.warning('user %s tried to delete pull request without access',
1279 self._rhodecode_user)
1279 self._rhodecode_user)
1280 raise HTTPNotFound()
1280 raise HTTPNotFound()
1281
1281
1282 @LoginRequired()
1282 @LoginRequired()
1283 @NotAnonymous()
1283 @NotAnonymous()
1284 @HasRepoPermissionAnyDecorator(
1284 @HasRepoPermissionAnyDecorator(
1285 'repository.read', 'repository.write', 'repository.admin')
1285 'repository.read', 'repository.write', 'repository.admin')
1286 @CSRFRequired()
1286 @CSRFRequired()
1287 @view_config(
1287 @view_config(
1288 route_name='pullrequest_comment_create', request_method='POST',
1288 route_name='pullrequest_comment_create', request_method='POST',
1289 renderer='json_ext')
1289 renderer='json_ext')
1290 def pull_request_comment_create(self):
1290 def pull_request_comment_create(self):
1291 _ = self.request.translate
1291 _ = self.request.translate
1292
1292
1293 pull_request = PullRequest.get_or_404(
1293 pull_request = PullRequest.get_or_404(
1294 self.request.matchdict['pull_request_id'])
1294 self.request.matchdict['pull_request_id'])
1295 pull_request_id = pull_request.pull_request_id
1295 pull_request_id = pull_request.pull_request_id
1296
1296
1297 if pull_request.is_closed():
1297 if pull_request.is_closed():
1298 log.debug('comment: forbidden because pull request is closed')
1298 log.debug('comment: forbidden because pull request is closed')
1299 raise HTTPForbidden()
1299 raise HTTPForbidden()
1300
1300
1301 allowed_to_comment = PullRequestModel().check_user_comment(
1301 allowed_to_comment = PullRequestModel().check_user_comment(
1302 pull_request, self._rhodecode_user)
1302 pull_request, self._rhodecode_user)
1303 if not allowed_to_comment:
1303 if not allowed_to_comment:
1304 log.debug(
1304 log.debug(
1305 'comment: forbidden because pull request is from forbidden repo')
1305 'comment: forbidden because pull request is from forbidden repo')
1306 raise HTTPForbidden()
1306 raise HTTPForbidden()
1307
1307
1308 c = self.load_default_context()
1308 c = self.load_default_context()
1309
1309
1310 status = self.request.POST.get('changeset_status', None)
1310 status = self.request.POST.get('changeset_status', None)
1311 text = self.request.POST.get('text')
1311 text = self.request.POST.get('text')
1312 comment_type = self.request.POST.get('comment_type')
1312 comment_type = self.request.POST.get('comment_type')
1313 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1313 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1314 close_pull_request = self.request.POST.get('close_pull_request')
1314 close_pull_request = self.request.POST.get('close_pull_request')
1315
1315
1316 # the logic here should work like following, if we submit close
1316 # the logic here should work like following, if we submit close
1317 # pr comment, use `close_pull_request_with_comment` function
1317 # pr comment, use `close_pull_request_with_comment` function
1318 # else handle regular comment logic
1318 # else handle regular comment logic
1319
1319
1320 if close_pull_request:
1320 if close_pull_request:
1321 # only owner or admin or person with write permissions
1321 # only owner or admin or person with write permissions
1322 allowed_to_close = PullRequestModel().check_user_update(
1322 allowed_to_close = PullRequestModel().check_user_update(
1323 pull_request, self._rhodecode_user)
1323 pull_request, self._rhodecode_user)
1324 if not allowed_to_close:
1324 if not allowed_to_close:
1325 log.debug('comment: forbidden because not allowed to close '
1325 log.debug('comment: forbidden because not allowed to close '
1326 'pull request %s', pull_request_id)
1326 'pull request %s', pull_request_id)
1327 raise HTTPForbidden()
1327 raise HTTPForbidden()
1328
1328
1329 # This also triggers `review_status_change`
1329 # This also triggers `review_status_change`
1330 comment, status = PullRequestModel().close_pull_request_with_comment(
1330 comment, status = PullRequestModel().close_pull_request_with_comment(
1331 pull_request, self._rhodecode_user, self.db_repo, message=text,
1331 pull_request, self._rhodecode_user, self.db_repo, message=text,
1332 auth_user=self._rhodecode_user)
1332 auth_user=self._rhodecode_user)
1333 Session().flush()
1333 Session().flush()
1334
1334
1335 PullRequestModel().trigger_pull_request_hook(
1335 PullRequestModel().trigger_pull_request_hook(
1336 pull_request, self._rhodecode_user, 'comment',
1336 pull_request, self._rhodecode_user, 'comment',
1337 data={'comment': comment})
1337 data={'comment': comment})
1338
1338
1339 else:
1339 else:
1340 # regular comment case, could be inline, or one with status.
1340 # regular comment case, could be inline, or one with status.
1341 # for that one we check also permissions
1341 # for that one we check also permissions
1342
1342
1343 allowed_to_change_status = PullRequestModel().check_user_change_status(
1343 allowed_to_change_status = PullRequestModel().check_user_change_status(
1344 pull_request, self._rhodecode_user)
1344 pull_request, self._rhodecode_user)
1345
1345
1346 if status and allowed_to_change_status:
1346 if status and allowed_to_change_status:
1347 message = (_('Status change %(transition_icon)s %(status)s')
1347 message = (_('Status change %(transition_icon)s %(status)s')
1348 % {'transition_icon': '>',
1348 % {'transition_icon': '>',
1349 'status': ChangesetStatus.get_status_lbl(status)})
1349 'status': ChangesetStatus.get_status_lbl(status)})
1350 text = text or message
1350 text = text or message
1351
1351
1352 comment = CommentsModel().create(
1352 comment = CommentsModel().create(
1353 text=text,
1353 text=text,
1354 repo=self.db_repo.repo_id,
1354 repo=self.db_repo.repo_id,
1355 user=self._rhodecode_user.user_id,
1355 user=self._rhodecode_user.user_id,
1356 pull_request=pull_request,
1356 pull_request=pull_request,
1357 f_path=self.request.POST.get('f_path'),
1357 f_path=self.request.POST.get('f_path'),
1358 line_no=self.request.POST.get('line'),
1358 line_no=self.request.POST.get('line'),
1359 status_change=(ChangesetStatus.get_status_lbl(status)
1359 status_change=(ChangesetStatus.get_status_lbl(status)
1360 if status and allowed_to_change_status else None),
1360 if status and allowed_to_change_status else None),
1361 status_change_type=(status
1361 status_change_type=(status
1362 if status and allowed_to_change_status else None),
1362 if status and allowed_to_change_status else None),
1363 comment_type=comment_type,
1363 comment_type=comment_type,
1364 resolves_comment_id=resolves_comment_id,
1364 resolves_comment_id=resolves_comment_id,
1365 auth_user=self._rhodecode_user
1365 auth_user=self._rhodecode_user
1366 )
1366 )
1367
1367
1368 if allowed_to_change_status:
1368 if allowed_to_change_status:
1369 # calculate old status before we change it
1369 # calculate old status before we change it
1370 old_calculated_status = pull_request.calculated_review_status()
1370 old_calculated_status = pull_request.calculated_review_status()
1371
1371
1372 # get status if set !
1372 # get status if set !
1373 if status:
1373 if status:
1374 ChangesetStatusModel().set_status(
1374 ChangesetStatusModel().set_status(
1375 self.db_repo.repo_id,
1375 self.db_repo.repo_id,
1376 status,
1376 status,
1377 self._rhodecode_user.user_id,
1377 self._rhodecode_user.user_id,
1378 comment,
1378 comment,
1379 pull_request=pull_request
1379 pull_request=pull_request
1380 )
1380 )
1381
1381
1382 Session().flush()
1382 Session().flush()
1383 # this is somehow required to get access to some relationship
1383 # this is somehow required to get access to some relationship
1384 # loaded on comment
1384 # loaded on comment
1385 Session().refresh(comment)
1385 Session().refresh(comment)
1386
1386
1387 PullRequestModel().trigger_pull_request_hook(
1387 PullRequestModel().trigger_pull_request_hook(
1388 pull_request, self._rhodecode_user, 'comment',
1388 pull_request, self._rhodecode_user, 'comment',
1389 data={'comment': comment})
1389 data={'comment': comment})
1390
1390
1391 # we now calculate the status of pull request, and based on that
1391 # we now calculate the status of pull request, and based on that
1392 # calculation we set the commits status
1392 # calculation we set the commits status
1393 calculated_status = pull_request.calculated_review_status()
1393 calculated_status = pull_request.calculated_review_status()
1394 if old_calculated_status != calculated_status:
1394 if old_calculated_status != calculated_status:
1395 PullRequestModel().trigger_pull_request_hook(
1395 PullRequestModel().trigger_pull_request_hook(
1396 pull_request, self._rhodecode_user, 'review_status_change',
1396 pull_request, self._rhodecode_user, 'review_status_change',
1397 data={'status': calculated_status})
1397 data={'status': calculated_status})
1398
1398
1399 Session().commit()
1399 Session().commit()
1400
1400
1401 data = {
1401 data = {
1402 'target_id': h.safeid(h.safe_unicode(
1402 'target_id': h.safeid(h.safe_unicode(
1403 self.request.POST.get('f_path'))),
1403 self.request.POST.get('f_path'))),
1404 }
1404 }
1405 if comment:
1405 if comment:
1406 c.co = comment
1406 c.co = comment
1407 rendered_comment = render(
1407 rendered_comment = render(
1408 'rhodecode:templates/changeset/changeset_comment_block.mako',
1408 'rhodecode:templates/changeset/changeset_comment_block.mako',
1409 self._get_template_context(c), self.request)
1409 self._get_template_context(c), self.request)
1410
1410
1411 data.update(comment.get_dict())
1411 data.update(comment.get_dict())
1412 data.update({'rendered_text': rendered_comment})
1412 data.update({'rendered_text': rendered_comment})
1413
1413
1414 return data
1414 return data
1415
1415
1416 @LoginRequired()
1416 @LoginRequired()
1417 @NotAnonymous()
1417 @NotAnonymous()
1418 @HasRepoPermissionAnyDecorator(
1418 @HasRepoPermissionAnyDecorator(
1419 'repository.read', 'repository.write', 'repository.admin')
1419 'repository.read', 'repository.write', 'repository.admin')
1420 @CSRFRequired()
1420 @CSRFRequired()
1421 @view_config(
1421 @view_config(
1422 route_name='pullrequest_comment_delete', request_method='POST',
1422 route_name='pullrequest_comment_delete', request_method='POST',
1423 renderer='json_ext')
1423 renderer='json_ext')
1424 def pull_request_comment_delete(self):
1424 def pull_request_comment_delete(self):
1425 pull_request = PullRequest.get_or_404(
1425 pull_request = PullRequest.get_or_404(
1426 self.request.matchdict['pull_request_id'])
1426 self.request.matchdict['pull_request_id'])
1427
1427
1428 comment = ChangesetComment.get_or_404(
1428 comment = ChangesetComment.get_or_404(
1429 self.request.matchdict['comment_id'])
1429 self.request.matchdict['comment_id'])
1430 comment_id = comment.comment_id
1430 comment_id = comment.comment_id
1431
1431
1432 if pull_request.is_closed():
1432 if pull_request.is_closed():
1433 log.debug('comment: forbidden because pull request is closed')
1433 log.debug('comment: forbidden because pull request is closed')
1434 raise HTTPForbidden()
1434 raise HTTPForbidden()
1435
1435
1436 if not comment:
1436 if not comment:
1437 log.debug('Comment with id:%s not found, skipping', comment_id)
1437 log.debug('Comment with id:%s not found, skipping', comment_id)
1438 # comment already deleted in another call probably
1438 # comment already deleted in another call probably
1439 return True
1439 return True
1440
1440
1441 if comment.pull_request.is_closed():
1441 if comment.pull_request.is_closed():
1442 # don't allow deleting comments on closed pull request
1442 # don't allow deleting comments on closed pull request
1443 raise HTTPForbidden()
1443 raise HTTPForbidden()
1444
1444
1445 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1445 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1446 super_admin = h.HasPermissionAny('hg.admin')()
1446 super_admin = h.HasPermissionAny('hg.admin')()
1447 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1447 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1448 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1448 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1449 comment_repo_admin = is_repo_admin and is_repo_comment
1449 comment_repo_admin = is_repo_admin and is_repo_comment
1450
1450
1451 if super_admin or comment_owner or comment_repo_admin:
1451 if super_admin or comment_owner or comment_repo_admin:
1452 old_calculated_status = comment.pull_request.calculated_review_status()
1452 old_calculated_status = comment.pull_request.calculated_review_status()
1453 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1453 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1454 Session().commit()
1454 Session().commit()
1455 calculated_status = comment.pull_request.calculated_review_status()
1455 calculated_status = comment.pull_request.calculated_review_status()
1456 if old_calculated_status != calculated_status:
1456 if old_calculated_status != calculated_status:
1457 PullRequestModel().trigger_pull_request_hook(
1457 PullRequestModel().trigger_pull_request_hook(
1458 comment.pull_request, self._rhodecode_user, 'review_status_change',
1458 comment.pull_request, self._rhodecode_user, 'review_status_change',
1459 data={'status': calculated_status})
1459 data={'status': calculated_status})
1460 return True
1460 return True
1461 else:
1461 else:
1462 log.warning('No permissions for user %s to delete comment_id: %s',
1462 log.warning('No permissions for user %s to delete comment_id: %s',
1463 self._rhodecode_db_user, comment_id)
1463 self._rhodecode_db_user, comment_id)
1464 raise HTTPNotFound()
1464 raise HTTPNotFound()
@@ -1,1742 +1,1742 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31 import collections
31 import collections
32
32
33 from pyramid import compat
33 from pyramid import compat
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35
35
36 from rhodecode import events
36 from rhodecode import events
37 from rhodecode.translation import lazy_ugettext
37 from rhodecode.translation import lazy_ugettext
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
58 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
61
61
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
67 # request update.
67 # request update.
68 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 UpdateResponse = collections.namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
71
71
72
72
73 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
74
74
75 cls = PullRequest
75 cls = PullRequest
76
76
77 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
77 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
78
78
79 UPDATE_STATUS_MESSAGES = {
79 UPDATE_STATUS_MESSAGES = {
80 UpdateFailureReason.NONE: lazy_ugettext(
80 UpdateFailureReason.NONE: lazy_ugettext(
81 'Pull request update successful.'),
81 'Pull request update successful.'),
82 UpdateFailureReason.UNKNOWN: lazy_ugettext(
82 UpdateFailureReason.UNKNOWN: lazy_ugettext(
83 'Pull request update failed because of an unknown error.'),
83 'Pull request update failed because of an unknown error.'),
84 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
84 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
85 'No update needed because the source and target have not changed.'),
85 'No update needed because the source and target have not changed.'),
86 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
86 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
87 'Pull request cannot be updated because the reference type is '
87 'Pull request cannot be updated because the reference type is '
88 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
88 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
89 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
89 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
90 'This pull request cannot be updated because the target '
90 'This pull request cannot be updated because the target '
91 'reference is missing.'),
91 'reference is missing.'),
92 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
92 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
93 'This pull request cannot be updated because the source '
93 'This pull request cannot be updated because the source '
94 'reference is missing.'),
94 'reference is missing.'),
95 }
95 }
96 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
96 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
97 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
97 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
98
98
99 def __get_pull_request(self, pull_request):
99 def __get_pull_request(self, pull_request):
100 return self._get_instance((
100 return self._get_instance((
101 PullRequest, PullRequestVersion), pull_request)
101 PullRequest, PullRequestVersion), pull_request)
102
102
103 def _check_perms(self, perms, pull_request, user, api=False):
103 def _check_perms(self, perms, pull_request, user, api=False):
104 if not api:
104 if not api:
105 return h.HasRepoPermissionAny(*perms)(
105 return h.HasRepoPermissionAny(*perms)(
106 user=user, repo_name=pull_request.target_repo.repo_name)
106 user=user, repo_name=pull_request.target_repo.repo_name)
107 else:
107 else:
108 return h.HasRepoPermissionAnyApi(*perms)(
108 return h.HasRepoPermissionAnyApi(*perms)(
109 user=user, repo_name=pull_request.target_repo.repo_name)
109 user=user, repo_name=pull_request.target_repo.repo_name)
110
110
111 def check_user_read(self, pull_request, user, api=False):
111 def check_user_read(self, pull_request, user, api=False):
112 _perms = ('repository.admin', 'repository.write', 'repository.read',)
112 _perms = ('repository.admin', 'repository.write', 'repository.read',)
113 return self._check_perms(_perms, pull_request, user, api)
113 return self._check_perms(_perms, pull_request, user, api)
114
114
115 def check_user_merge(self, pull_request, user, api=False):
115 def check_user_merge(self, pull_request, user, api=False):
116 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
116 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
117 return self._check_perms(_perms, pull_request, user, api)
117 return self._check_perms(_perms, pull_request, user, api)
118
118
119 def check_user_update(self, pull_request, user, api=False):
119 def check_user_update(self, pull_request, user, api=False):
120 owner = user.user_id == pull_request.user_id
120 owner = user.user_id == pull_request.user_id
121 return self.check_user_merge(pull_request, user, api) or owner
121 return self.check_user_merge(pull_request, user, api) or owner
122
122
123 def check_user_delete(self, pull_request, user):
123 def check_user_delete(self, pull_request, user):
124 owner = user.user_id == pull_request.user_id
124 owner = user.user_id == pull_request.user_id
125 _perms = ('repository.admin',)
125 _perms = ('repository.admin',)
126 return self._check_perms(_perms, pull_request, user) or owner
126 return self._check_perms(_perms, pull_request, user) or owner
127
127
128 def check_user_change_status(self, pull_request, user, api=False):
128 def check_user_change_status(self, pull_request, user, api=False):
129 reviewer = user.user_id in [x.user_id for x in
129 reviewer = user.user_id in [x.user_id for x in
130 pull_request.reviewers]
130 pull_request.reviewers]
131 return self.check_user_update(pull_request, user, api) or reviewer
131 return self.check_user_update(pull_request, user, api) or reviewer
132
132
133 def check_user_comment(self, pull_request, user):
133 def check_user_comment(self, pull_request, user):
134 owner = user.user_id == pull_request.user_id
134 owner = user.user_id == pull_request.user_id
135 return self.check_user_read(pull_request, user) or owner
135 return self.check_user_read(pull_request, user) or owner
136
136
137 def get(self, pull_request):
137 def get(self, pull_request):
138 return self.__get_pull_request(pull_request)
138 return self.__get_pull_request(pull_request)
139
139
140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
141 opened_by=None, order_by=None,
141 opened_by=None, order_by=None,
142 order_dir='desc', only_created=True):
142 order_dir='desc', only_created=False):
143 repo = None
143 repo = None
144 if repo_name:
144 if repo_name:
145 repo = self._get_repo(repo_name)
145 repo = self._get_repo(repo_name)
146
146
147 q = PullRequest.query()
147 q = PullRequest.query()
148
148
149 # source or target
149 # source or target
150 if repo and source:
150 if repo and source:
151 q = q.filter(PullRequest.source_repo == repo)
151 q = q.filter(PullRequest.source_repo == repo)
152 elif repo:
152 elif repo:
153 q = q.filter(PullRequest.target_repo == repo)
153 q = q.filter(PullRequest.target_repo == repo)
154
154
155 # closed,opened
155 # closed,opened
156 if statuses:
156 if statuses:
157 q = q.filter(PullRequest.status.in_(statuses))
157 q = q.filter(PullRequest.status.in_(statuses))
158
158
159 # opened by filter
159 # opened by filter
160 if opened_by:
160 if opened_by:
161 q = q.filter(PullRequest.user_id.in_(opened_by))
161 q = q.filter(PullRequest.user_id.in_(opened_by))
162
162
163 # only get those that are in "created" state
163 # only get those that are in "created" state
164 if only_created:
164 if only_created:
165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
166
166
167 if order_by:
167 if order_by:
168 order_map = {
168 order_map = {
169 'name_raw': PullRequest.pull_request_id,
169 'name_raw': PullRequest.pull_request_id,
170 'id': PullRequest.pull_request_id,
170 'id': PullRequest.pull_request_id,
171 'title': PullRequest.title,
171 'title': PullRequest.title,
172 'updated_on_raw': PullRequest.updated_on,
172 'updated_on_raw': PullRequest.updated_on,
173 'target_repo': PullRequest.target_repo_id
173 'target_repo': PullRequest.target_repo_id
174 }
174 }
175 if order_dir == 'asc':
175 if order_dir == 'asc':
176 q = q.order_by(order_map[order_by].asc())
176 q = q.order_by(order_map[order_by].asc())
177 else:
177 else:
178 q = q.order_by(order_map[order_by].desc())
178 q = q.order_by(order_map[order_by].desc())
179
179
180 return q
180 return q
181
181
182 def count_all(self, repo_name, source=False, statuses=None,
182 def count_all(self, repo_name, source=False, statuses=None,
183 opened_by=None):
183 opened_by=None):
184 """
184 """
185 Count the number of pull requests for a specific repository.
185 Count the number of pull requests for a specific repository.
186
186
187 :param repo_name: target or source repo
187 :param repo_name: target or source repo
188 :param source: boolean flag to specify if repo_name refers to source
188 :param source: boolean flag to specify if repo_name refers to source
189 :param statuses: list of pull request statuses
189 :param statuses: list of pull request statuses
190 :param opened_by: author user of the pull request
190 :param opened_by: author user of the pull request
191 :returns: int number of pull requests
191 :returns: int number of pull requests
192 """
192 """
193 q = self._prepare_get_all_query(
193 q = self._prepare_get_all_query(
194 repo_name, source=source, statuses=statuses, opened_by=opened_by)
194 repo_name, source=source, statuses=statuses, opened_by=opened_by)
195
195
196 return q.count()
196 return q.count()
197
197
198 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
198 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
199 offset=0, length=None, order_by=None, order_dir='desc'):
199 offset=0, length=None, order_by=None, order_dir='desc'):
200 """
200 """
201 Get all pull requests for a specific repository.
201 Get all pull requests for a specific repository.
202
202
203 :param repo_name: target or source repo
203 :param repo_name: target or source repo
204 :param source: boolean flag to specify if repo_name refers to source
204 :param source: boolean flag to specify if repo_name refers to source
205 :param statuses: list of pull request statuses
205 :param statuses: list of pull request statuses
206 :param opened_by: author user of the pull request
206 :param opened_by: author user of the pull request
207 :param offset: pagination offset
207 :param offset: pagination offset
208 :param length: length of returned list
208 :param length: length of returned list
209 :param order_by: order of the returned list
209 :param order_by: order of the returned list
210 :param order_dir: 'asc' or 'desc' ordering direction
210 :param order_dir: 'asc' or 'desc' ordering direction
211 :returns: list of pull requests
211 :returns: list of pull requests
212 """
212 """
213 q = self._prepare_get_all_query(
213 q = self._prepare_get_all_query(
214 repo_name, source=source, statuses=statuses, opened_by=opened_by,
214 repo_name, source=source, statuses=statuses, opened_by=opened_by,
215 order_by=order_by, order_dir=order_dir)
215 order_by=order_by, order_dir=order_dir)
216
216
217 if length:
217 if length:
218 pull_requests = q.limit(length).offset(offset).all()
218 pull_requests = q.limit(length).offset(offset).all()
219 else:
219 else:
220 pull_requests = q.all()
220 pull_requests = q.all()
221
221
222 return pull_requests
222 return pull_requests
223
223
224 def count_awaiting_review(self, repo_name, source=False, statuses=None,
224 def count_awaiting_review(self, repo_name, source=False, statuses=None,
225 opened_by=None):
225 opened_by=None):
226 """
226 """
227 Count the number of pull requests for a specific repository that are
227 Count the number of pull requests for a specific repository that are
228 awaiting review.
228 awaiting review.
229
229
230 :param repo_name: target or source repo
230 :param repo_name: target or source repo
231 :param source: boolean flag to specify if repo_name refers to source
231 :param source: boolean flag to specify if repo_name refers to source
232 :param statuses: list of pull request statuses
232 :param statuses: list of pull request statuses
233 :param opened_by: author user of the pull request
233 :param opened_by: author user of the pull request
234 :returns: int number of pull requests
234 :returns: int number of pull requests
235 """
235 """
236 pull_requests = self.get_awaiting_review(
236 pull_requests = self.get_awaiting_review(
237 repo_name, source=source, statuses=statuses, opened_by=opened_by)
237 repo_name, source=source, statuses=statuses, opened_by=opened_by)
238
238
239 return len(pull_requests)
239 return len(pull_requests)
240
240
241 def get_awaiting_review(self, repo_name, source=False, statuses=None,
241 def get_awaiting_review(self, repo_name, source=False, statuses=None,
242 opened_by=None, offset=0, length=None,
242 opened_by=None, offset=0, length=None,
243 order_by=None, order_dir='desc'):
243 order_by=None, order_dir='desc'):
244 """
244 """
245 Get all pull requests for a specific repository that are awaiting
245 Get all pull requests for a specific repository that are awaiting
246 review.
246 review.
247
247
248 :param repo_name: target or source repo
248 :param repo_name: target or source repo
249 :param source: boolean flag to specify if repo_name refers to source
249 :param source: boolean flag to specify if repo_name refers to source
250 :param statuses: list of pull request statuses
250 :param statuses: list of pull request statuses
251 :param opened_by: author user of the pull request
251 :param opened_by: author user of the pull request
252 :param offset: pagination offset
252 :param offset: pagination offset
253 :param length: length of returned list
253 :param length: length of returned list
254 :param order_by: order of the returned list
254 :param order_by: order of the returned list
255 :param order_dir: 'asc' or 'desc' ordering direction
255 :param order_dir: 'asc' or 'desc' ordering direction
256 :returns: list of pull requests
256 :returns: list of pull requests
257 """
257 """
258 pull_requests = self.get_all(
258 pull_requests = self.get_all(
259 repo_name, source=source, statuses=statuses, opened_by=opened_by,
259 repo_name, source=source, statuses=statuses, opened_by=opened_by,
260 order_by=order_by, order_dir=order_dir)
260 order_by=order_by, order_dir=order_dir)
261
261
262 _filtered_pull_requests = []
262 _filtered_pull_requests = []
263 for pr in pull_requests:
263 for pr in pull_requests:
264 status = pr.calculated_review_status()
264 status = pr.calculated_review_status()
265 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
265 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
266 ChangesetStatus.STATUS_UNDER_REVIEW]:
266 ChangesetStatus.STATUS_UNDER_REVIEW]:
267 _filtered_pull_requests.append(pr)
267 _filtered_pull_requests.append(pr)
268 if length:
268 if length:
269 return _filtered_pull_requests[offset:offset+length]
269 return _filtered_pull_requests[offset:offset+length]
270 else:
270 else:
271 return _filtered_pull_requests
271 return _filtered_pull_requests
272
272
273 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
273 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
274 opened_by=None, user_id=None):
274 opened_by=None, user_id=None):
275 """
275 """
276 Count the number of pull requests for a specific repository that are
276 Count the number of pull requests for a specific repository that are
277 awaiting review from a specific user.
277 awaiting review from a specific user.
278
278
279 :param repo_name: target or source repo
279 :param repo_name: target or source repo
280 :param source: boolean flag to specify if repo_name refers to source
280 :param source: boolean flag to specify if repo_name refers to source
281 :param statuses: list of pull request statuses
281 :param statuses: list of pull request statuses
282 :param opened_by: author user of the pull request
282 :param opened_by: author user of the pull request
283 :param user_id: reviewer user of the pull request
283 :param user_id: reviewer user of the pull request
284 :returns: int number of pull requests
284 :returns: int number of pull requests
285 """
285 """
286 pull_requests = self.get_awaiting_my_review(
286 pull_requests = self.get_awaiting_my_review(
287 repo_name, source=source, statuses=statuses, opened_by=opened_by,
287 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 user_id=user_id)
288 user_id=user_id)
289
289
290 return len(pull_requests)
290 return len(pull_requests)
291
291
292 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
292 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
293 opened_by=None, user_id=None, offset=0,
293 opened_by=None, user_id=None, offset=0,
294 length=None, order_by=None, order_dir='desc'):
294 length=None, order_by=None, order_dir='desc'):
295 """
295 """
296 Get all pull requests for a specific repository that are awaiting
296 Get all pull requests for a specific repository that are awaiting
297 review from a specific user.
297 review from a specific user.
298
298
299 :param repo_name: target or source repo
299 :param repo_name: target or source repo
300 :param source: boolean flag to specify if repo_name refers to source
300 :param source: boolean flag to specify if repo_name refers to source
301 :param statuses: list of pull request statuses
301 :param statuses: list of pull request statuses
302 :param opened_by: author user of the pull request
302 :param opened_by: author user of the pull request
303 :param user_id: reviewer user of the pull request
303 :param user_id: reviewer user of the pull request
304 :param offset: pagination offset
304 :param offset: pagination offset
305 :param length: length of returned list
305 :param length: length of returned list
306 :param order_by: order of the returned list
306 :param order_by: order of the returned list
307 :param order_dir: 'asc' or 'desc' ordering direction
307 :param order_dir: 'asc' or 'desc' ordering direction
308 :returns: list of pull requests
308 :returns: list of pull requests
309 """
309 """
310 pull_requests = self.get_all(
310 pull_requests = self.get_all(
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 order_by=order_by, order_dir=order_dir)
312 order_by=order_by, order_dir=order_dir)
313
313
314 _my = PullRequestModel().get_not_reviewed(user_id)
314 _my = PullRequestModel().get_not_reviewed(user_id)
315 my_participation = []
315 my_participation = []
316 for pr in pull_requests:
316 for pr in pull_requests:
317 if pr in _my:
317 if pr in _my:
318 my_participation.append(pr)
318 my_participation.append(pr)
319 _filtered_pull_requests = my_participation
319 _filtered_pull_requests = my_participation
320 if length:
320 if length:
321 return _filtered_pull_requests[offset:offset+length]
321 return _filtered_pull_requests[offset:offset+length]
322 else:
322 else:
323 return _filtered_pull_requests
323 return _filtered_pull_requests
324
324
325 def get_not_reviewed(self, user_id):
325 def get_not_reviewed(self, user_id):
326 return [
326 return [
327 x.pull_request for x in PullRequestReviewers.query().filter(
327 x.pull_request for x in PullRequestReviewers.query().filter(
328 PullRequestReviewers.user_id == user_id).all()
328 PullRequestReviewers.user_id == user_id).all()
329 ]
329 ]
330
330
331 def _prepare_participating_query(self, user_id=None, statuses=None,
331 def _prepare_participating_query(self, user_id=None, statuses=None,
332 order_by=None, order_dir='desc'):
332 order_by=None, order_dir='desc'):
333 q = PullRequest.query()
333 q = PullRequest.query()
334 if user_id:
334 if user_id:
335 reviewers_subquery = Session().query(
335 reviewers_subquery = Session().query(
336 PullRequestReviewers.pull_request_id).filter(
336 PullRequestReviewers.pull_request_id).filter(
337 PullRequestReviewers.user_id == user_id).subquery()
337 PullRequestReviewers.user_id == user_id).subquery()
338 user_filter = or_(
338 user_filter = or_(
339 PullRequest.user_id == user_id,
339 PullRequest.user_id == user_id,
340 PullRequest.pull_request_id.in_(reviewers_subquery)
340 PullRequest.pull_request_id.in_(reviewers_subquery)
341 )
341 )
342 q = PullRequest.query().filter(user_filter)
342 q = PullRequest.query().filter(user_filter)
343
343
344 # closed,opened
344 # closed,opened
345 if statuses:
345 if statuses:
346 q = q.filter(PullRequest.status.in_(statuses))
346 q = q.filter(PullRequest.status.in_(statuses))
347
347
348 if order_by:
348 if order_by:
349 order_map = {
349 order_map = {
350 'name_raw': PullRequest.pull_request_id,
350 'name_raw': PullRequest.pull_request_id,
351 'title': PullRequest.title,
351 'title': PullRequest.title,
352 'updated_on_raw': PullRequest.updated_on,
352 'updated_on_raw': PullRequest.updated_on,
353 'target_repo': PullRequest.target_repo_id
353 'target_repo': PullRequest.target_repo_id
354 }
354 }
355 if order_dir == 'asc':
355 if order_dir == 'asc':
356 q = q.order_by(order_map[order_by].asc())
356 q = q.order_by(order_map[order_by].asc())
357 else:
357 else:
358 q = q.order_by(order_map[order_by].desc())
358 q = q.order_by(order_map[order_by].desc())
359
359
360 return q
360 return q
361
361
362 def count_im_participating_in(self, user_id=None, statuses=None):
362 def count_im_participating_in(self, user_id=None, statuses=None):
363 q = self._prepare_participating_query(user_id, statuses=statuses)
363 q = self._prepare_participating_query(user_id, statuses=statuses)
364 return q.count()
364 return q.count()
365
365
366 def get_im_participating_in(
366 def get_im_participating_in(
367 self, user_id=None, statuses=None, offset=0,
367 self, user_id=None, statuses=None, offset=0,
368 length=None, order_by=None, order_dir='desc'):
368 length=None, order_by=None, order_dir='desc'):
369 """
369 """
370 Get all Pull requests that i'm participating in, or i have opened
370 Get all Pull requests that i'm participating in, or i have opened
371 """
371 """
372
372
373 q = self._prepare_participating_query(
373 q = self._prepare_participating_query(
374 user_id, statuses=statuses, order_by=order_by,
374 user_id, statuses=statuses, order_by=order_by,
375 order_dir=order_dir)
375 order_dir=order_dir)
376
376
377 if length:
377 if length:
378 pull_requests = q.limit(length).offset(offset).all()
378 pull_requests = q.limit(length).offset(offset).all()
379 else:
379 else:
380 pull_requests = q.all()
380 pull_requests = q.all()
381
381
382 return pull_requests
382 return pull_requests
383
383
384 def get_versions(self, pull_request):
384 def get_versions(self, pull_request):
385 """
385 """
386 returns version of pull request sorted by ID descending
386 returns version of pull request sorted by ID descending
387 """
387 """
388 return PullRequestVersion.query()\
388 return PullRequestVersion.query()\
389 .filter(PullRequestVersion.pull_request == pull_request)\
389 .filter(PullRequestVersion.pull_request == pull_request)\
390 .order_by(PullRequestVersion.pull_request_version_id.asc())\
390 .order_by(PullRequestVersion.pull_request_version_id.asc())\
391 .all()
391 .all()
392
392
393 def get_pr_version(self, pull_request_id, version=None):
393 def get_pr_version(self, pull_request_id, version=None):
394 at_version = None
394 at_version = None
395
395
396 if version and version == 'latest':
396 if version and version == 'latest':
397 pull_request_ver = PullRequest.get(pull_request_id)
397 pull_request_ver = PullRequest.get(pull_request_id)
398 pull_request_obj = pull_request_ver
398 pull_request_obj = pull_request_ver
399 _org_pull_request_obj = pull_request_obj
399 _org_pull_request_obj = pull_request_obj
400 at_version = 'latest'
400 at_version = 'latest'
401 elif version:
401 elif version:
402 pull_request_ver = PullRequestVersion.get_or_404(version)
402 pull_request_ver = PullRequestVersion.get_or_404(version)
403 pull_request_obj = pull_request_ver
403 pull_request_obj = pull_request_ver
404 _org_pull_request_obj = pull_request_ver.pull_request
404 _org_pull_request_obj = pull_request_ver.pull_request
405 at_version = pull_request_ver.pull_request_version_id
405 at_version = pull_request_ver.pull_request_version_id
406 else:
406 else:
407 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
407 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
408 pull_request_id)
408 pull_request_id)
409
409
410 pull_request_display_obj = PullRequest.get_pr_display_object(
410 pull_request_display_obj = PullRequest.get_pr_display_object(
411 pull_request_obj, _org_pull_request_obj)
411 pull_request_obj, _org_pull_request_obj)
412
412
413 return _org_pull_request_obj, pull_request_obj, \
413 return _org_pull_request_obj, pull_request_obj, \
414 pull_request_display_obj, at_version
414 pull_request_display_obj, at_version
415
415
416 def create(self, created_by, source_repo, source_ref, target_repo,
416 def create(self, created_by, source_repo, source_ref, target_repo,
417 target_ref, revisions, reviewers, title, description=None,
417 target_ref, revisions, reviewers, title, description=None,
418 description_renderer=None,
418 description_renderer=None,
419 reviewer_data=None, translator=None, auth_user=None):
419 reviewer_data=None, translator=None, auth_user=None):
420 translator = translator or get_current_request().translate
420 translator = translator or get_current_request().translate
421
421
422 created_by_user = self._get_user(created_by)
422 created_by_user = self._get_user(created_by)
423 auth_user = auth_user or created_by_user.AuthUser()
423 auth_user = auth_user or created_by_user.AuthUser()
424 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
425 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
426
426
427 pull_request = PullRequest()
427 pull_request = PullRequest()
428 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
429 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
430 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
431 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
432 pull_request.revisions = revisions
432 pull_request.revisions = revisions
433 pull_request.title = title
433 pull_request.title = title
434 pull_request.description = description
434 pull_request.description = description
435 pull_request.description_renderer = description_renderer
435 pull_request.description_renderer = description_renderer
436 pull_request.author = created_by_user
436 pull_request.author = created_by_user
437 pull_request.reviewer_data = reviewer_data
437 pull_request.reviewer_data = reviewer_data
438 pull_request.pull_request_state = pull_request.STATE_CREATING
438 pull_request.pull_request_state = pull_request.STATE_CREATING
439 Session().add(pull_request)
439 Session().add(pull_request)
440 Session().flush()
440 Session().flush()
441
441
442 reviewer_ids = set()
442 reviewer_ids = set()
443 # members / reviewers
443 # members / reviewers
444 for reviewer_object in reviewers:
444 for reviewer_object in reviewers:
445 user_id, reasons, mandatory, rules = reviewer_object
445 user_id, reasons, mandatory, rules = reviewer_object
446 user = self._get_user(user_id)
446 user = self._get_user(user_id)
447
447
448 # skip duplicates
448 # skip duplicates
449 if user.user_id in reviewer_ids:
449 if user.user_id in reviewer_ids:
450 continue
450 continue
451
451
452 reviewer_ids.add(user.user_id)
452 reviewer_ids.add(user.user_id)
453
453
454 reviewer = PullRequestReviewers()
454 reviewer = PullRequestReviewers()
455 reviewer.user = user
455 reviewer.user = user
456 reviewer.pull_request = pull_request
456 reviewer.pull_request = pull_request
457 reviewer.reasons = reasons
457 reviewer.reasons = reasons
458 reviewer.mandatory = mandatory
458 reviewer.mandatory = mandatory
459
459
460 # NOTE(marcink): pick only first rule for now
460 # NOTE(marcink): pick only first rule for now
461 rule_id = list(rules)[0] if rules else None
461 rule_id = list(rules)[0] if rules else None
462 rule = RepoReviewRule.get(rule_id) if rule_id else None
462 rule = RepoReviewRule.get(rule_id) if rule_id else None
463 if rule:
463 if rule:
464 review_group = rule.user_group_vote_rule(user_id)
464 review_group = rule.user_group_vote_rule(user_id)
465 # we check if this particular reviewer is member of a voting group
465 # we check if this particular reviewer is member of a voting group
466 if review_group:
466 if review_group:
467 # NOTE(marcink):
467 # NOTE(marcink):
468 # can be that user is member of more but we pick the first same,
468 # can be that user is member of more but we pick the first same,
469 # same as default reviewers algo
469 # same as default reviewers algo
470 review_group = review_group[0]
470 review_group = review_group[0]
471
471
472 rule_data = {
472 rule_data = {
473 'rule_name':
473 'rule_name':
474 rule.review_rule_name,
474 rule.review_rule_name,
475 'rule_user_group_entry_id':
475 'rule_user_group_entry_id':
476 review_group.repo_review_rule_users_group_id,
476 review_group.repo_review_rule_users_group_id,
477 'rule_user_group_name':
477 'rule_user_group_name':
478 review_group.users_group.users_group_name,
478 review_group.users_group.users_group_name,
479 'rule_user_group_members':
479 'rule_user_group_members':
480 [x.user.username for x in review_group.users_group.members],
480 [x.user.username for x in review_group.users_group.members],
481 'rule_user_group_members_id':
481 'rule_user_group_members_id':
482 [x.user.user_id for x in review_group.users_group.members],
482 [x.user.user_id for x in review_group.users_group.members],
483 }
483 }
484 # e.g {'vote_rule': -1, 'mandatory': True}
484 # e.g {'vote_rule': -1, 'mandatory': True}
485 rule_data.update(review_group.rule_data())
485 rule_data.update(review_group.rule_data())
486
486
487 reviewer.rule_data = rule_data
487 reviewer.rule_data = rule_data
488
488
489 Session().add(reviewer)
489 Session().add(reviewer)
490 Session().flush()
490 Session().flush()
491
491
492 # Set approval status to "Under Review" for all commits which are
492 # Set approval status to "Under Review" for all commits which are
493 # part of this pull request.
493 # part of this pull request.
494 ChangesetStatusModel().set_status(
494 ChangesetStatusModel().set_status(
495 repo=target_repo,
495 repo=target_repo,
496 status=ChangesetStatus.STATUS_UNDER_REVIEW,
496 status=ChangesetStatus.STATUS_UNDER_REVIEW,
497 user=created_by_user,
497 user=created_by_user,
498 pull_request=pull_request
498 pull_request=pull_request
499 )
499 )
500 # we commit early at this point. This has to do with a fact
500 # we commit early at this point. This has to do with a fact
501 # that before queries do some row-locking. And because of that
501 # that before queries do some row-locking. And because of that
502 # we need to commit and finish transaction before below validate call
502 # we need to commit and finish transaction before below validate call
503 # that for large repos could be long resulting in long row locks
503 # that for large repos could be long resulting in long row locks
504 Session().commit()
504 Session().commit()
505
505
506 # prepare workspace, and run initial merge simulation. Set state during that
506 # prepare workspace, and run initial merge simulation. Set state during that
507 # operation
507 # operation
508 pull_request = PullRequest.get(pull_request.pull_request_id)
508 pull_request = PullRequest.get(pull_request.pull_request_id)
509
509
510 # set as merging, for simulation, and if finished to created so we mark
510 # set as merging, for simulation, and if finished to created so we mark
511 # simulation is working fine
511 # simulation is working fine
512 with pull_request.set_state(PullRequest.STATE_MERGING,
512 with pull_request.set_state(PullRequest.STATE_MERGING,
513 final_state=PullRequest.STATE_CREATED):
513 final_state=PullRequest.STATE_CREATED):
514 MergeCheck.validate(
514 MergeCheck.validate(
515 pull_request, auth_user=auth_user, translator=translator)
515 pull_request, auth_user=auth_user, translator=translator)
516
516
517 self.notify_reviewers(pull_request, reviewer_ids)
517 self.notify_reviewers(pull_request, reviewer_ids)
518 self.trigger_pull_request_hook(
518 self.trigger_pull_request_hook(
519 pull_request, created_by_user, 'create')
519 pull_request, created_by_user, 'create')
520
520
521 creation_data = pull_request.get_api_data(with_merge_state=False)
521 creation_data = pull_request.get_api_data(with_merge_state=False)
522 self._log_audit_action(
522 self._log_audit_action(
523 'repo.pull_request.create', {'data': creation_data},
523 'repo.pull_request.create', {'data': creation_data},
524 auth_user, pull_request)
524 auth_user, pull_request)
525
525
526 return pull_request
526 return pull_request
527
527
528 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
528 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
529 pull_request = self.__get_pull_request(pull_request)
529 pull_request = self.__get_pull_request(pull_request)
530 target_scm = pull_request.target_repo.scm_instance()
530 target_scm = pull_request.target_repo.scm_instance()
531 if action == 'create':
531 if action == 'create':
532 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
532 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
533 elif action == 'merge':
533 elif action == 'merge':
534 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
534 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
535 elif action == 'close':
535 elif action == 'close':
536 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
536 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
537 elif action == 'review_status_change':
537 elif action == 'review_status_change':
538 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
538 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
539 elif action == 'update':
539 elif action == 'update':
540 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
540 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
541 elif action == 'comment':
541 elif action == 'comment':
542 # dummy hook ! for comment. We want this function to handle all cases
542 # dummy hook ! for comment. We want this function to handle all cases
543 def trigger_hook(*args, **kwargs):
543 def trigger_hook(*args, **kwargs):
544 pass
544 pass
545 comment = data['comment']
545 comment = data['comment']
546 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
546 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
547 else:
547 else:
548 return
548 return
549
549
550 trigger_hook(
550 trigger_hook(
551 username=user.username,
551 username=user.username,
552 repo_name=pull_request.target_repo.repo_name,
552 repo_name=pull_request.target_repo.repo_name,
553 repo_alias=target_scm.alias,
553 repo_alias=target_scm.alias,
554 pull_request=pull_request,
554 pull_request=pull_request,
555 data=data)
555 data=data)
556
556
557 def _get_commit_ids(self, pull_request):
557 def _get_commit_ids(self, pull_request):
558 """
558 """
559 Return the commit ids of the merged pull request.
559 Return the commit ids of the merged pull request.
560
560
561 This method is not dealing correctly yet with the lack of autoupdates
561 This method is not dealing correctly yet with the lack of autoupdates
562 nor with the implicit target updates.
562 nor with the implicit target updates.
563 For example: if a commit in the source repo is already in the target it
563 For example: if a commit in the source repo is already in the target it
564 will be reported anyways.
564 will be reported anyways.
565 """
565 """
566 merge_rev = pull_request.merge_rev
566 merge_rev = pull_request.merge_rev
567 if merge_rev is None:
567 if merge_rev is None:
568 raise ValueError('This pull request was not merged yet')
568 raise ValueError('This pull request was not merged yet')
569
569
570 commit_ids = list(pull_request.revisions)
570 commit_ids = list(pull_request.revisions)
571 if merge_rev not in commit_ids:
571 if merge_rev not in commit_ids:
572 commit_ids.append(merge_rev)
572 commit_ids.append(merge_rev)
573
573
574 return commit_ids
574 return commit_ids
575
575
576 def merge_repo(self, pull_request, user, extras):
576 def merge_repo(self, pull_request, user, extras):
577 log.debug("Merging pull request %s", pull_request.pull_request_id)
577 log.debug("Merging pull request %s", pull_request.pull_request_id)
578 extras['user_agent'] = 'internal-merge'
578 extras['user_agent'] = 'internal-merge'
579 merge_state = self._merge_pull_request(pull_request, user, extras)
579 merge_state = self._merge_pull_request(pull_request, user, extras)
580 if merge_state.executed:
580 if merge_state.executed:
581 log.debug("Merge was successful, updating the pull request comments.")
581 log.debug("Merge was successful, updating the pull request comments.")
582 self._comment_and_close_pr(pull_request, user, merge_state)
582 self._comment_and_close_pr(pull_request, user, merge_state)
583
583
584 self._log_audit_action(
584 self._log_audit_action(
585 'repo.pull_request.merge',
585 'repo.pull_request.merge',
586 {'merge_state': merge_state.__dict__},
586 {'merge_state': merge_state.__dict__},
587 user, pull_request)
587 user, pull_request)
588
588
589 else:
589 else:
590 log.warn("Merge failed, not updating the pull request.")
590 log.warn("Merge failed, not updating the pull request.")
591 return merge_state
591 return merge_state
592
592
593 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
593 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
594 target_vcs = pull_request.target_repo.scm_instance()
594 target_vcs = pull_request.target_repo.scm_instance()
595 source_vcs = pull_request.source_repo.scm_instance()
595 source_vcs = pull_request.source_repo.scm_instance()
596
596
597 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
597 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
598 pr_id=pull_request.pull_request_id,
598 pr_id=pull_request.pull_request_id,
599 pr_title=pull_request.title,
599 pr_title=pull_request.title,
600 source_repo=source_vcs.name,
600 source_repo=source_vcs.name,
601 source_ref_name=pull_request.source_ref_parts.name,
601 source_ref_name=pull_request.source_ref_parts.name,
602 target_repo=target_vcs.name,
602 target_repo=target_vcs.name,
603 target_ref_name=pull_request.target_ref_parts.name,
603 target_ref_name=pull_request.target_ref_parts.name,
604 )
604 )
605
605
606 workspace_id = self._workspace_id(pull_request)
606 workspace_id = self._workspace_id(pull_request)
607 repo_id = pull_request.target_repo.repo_id
607 repo_id = pull_request.target_repo.repo_id
608 use_rebase = self._use_rebase_for_merging(pull_request)
608 use_rebase = self._use_rebase_for_merging(pull_request)
609 close_branch = self._close_branch_before_merging(pull_request)
609 close_branch = self._close_branch_before_merging(pull_request)
610
610
611 target_ref = self._refresh_reference(
611 target_ref = self._refresh_reference(
612 pull_request.target_ref_parts, target_vcs)
612 pull_request.target_ref_parts, target_vcs)
613
613
614 callback_daemon, extras = prepare_callback_daemon(
614 callback_daemon, extras = prepare_callback_daemon(
615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
616 host=vcs_settings.HOOKS_HOST,
616 host=vcs_settings.HOOKS_HOST,
617 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
617 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
618
618
619 with callback_daemon:
619 with callback_daemon:
620 # TODO: johbo: Implement a clean way to run a config_override
620 # TODO: johbo: Implement a clean way to run a config_override
621 # for a single call.
621 # for a single call.
622 target_vcs.config.set(
622 target_vcs.config.set(
623 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
623 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
624
624
625 user_name = user.short_contact
625 user_name = user.short_contact
626 merge_state = target_vcs.merge(
626 merge_state = target_vcs.merge(
627 repo_id, workspace_id, target_ref, source_vcs,
627 repo_id, workspace_id, target_ref, source_vcs,
628 pull_request.source_ref_parts,
628 pull_request.source_ref_parts,
629 user_name=user_name, user_email=user.email,
629 user_name=user_name, user_email=user.email,
630 message=message, use_rebase=use_rebase,
630 message=message, use_rebase=use_rebase,
631 close_branch=close_branch)
631 close_branch=close_branch)
632 return merge_state
632 return merge_state
633
633
634 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
634 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
635 pull_request.merge_rev = merge_state.merge_ref.commit_id
635 pull_request.merge_rev = merge_state.merge_ref.commit_id
636 pull_request.updated_on = datetime.datetime.now()
636 pull_request.updated_on = datetime.datetime.now()
637 close_msg = close_msg or 'Pull request merged and closed'
637 close_msg = close_msg or 'Pull request merged and closed'
638
638
639 CommentsModel().create(
639 CommentsModel().create(
640 text=safe_unicode(close_msg),
640 text=safe_unicode(close_msg),
641 repo=pull_request.target_repo.repo_id,
641 repo=pull_request.target_repo.repo_id,
642 user=user.user_id,
642 user=user.user_id,
643 pull_request=pull_request.pull_request_id,
643 pull_request=pull_request.pull_request_id,
644 f_path=None,
644 f_path=None,
645 line_no=None,
645 line_no=None,
646 closing_pr=True
646 closing_pr=True
647 )
647 )
648
648
649 Session().add(pull_request)
649 Session().add(pull_request)
650 Session().flush()
650 Session().flush()
651 # TODO: paris: replace invalidation with less radical solution
651 # TODO: paris: replace invalidation with less radical solution
652 ScmModel().mark_for_invalidation(
652 ScmModel().mark_for_invalidation(
653 pull_request.target_repo.repo_name)
653 pull_request.target_repo.repo_name)
654 self.trigger_pull_request_hook(pull_request, user, 'merge')
654 self.trigger_pull_request_hook(pull_request, user, 'merge')
655
655
656 def has_valid_update_type(self, pull_request):
656 def has_valid_update_type(self, pull_request):
657 source_ref_type = pull_request.source_ref_parts.type
657 source_ref_type = pull_request.source_ref_parts.type
658 return source_ref_type in self.REF_TYPES
658 return source_ref_type in self.REF_TYPES
659
659
660 def update_commits(self, pull_request):
660 def update_commits(self, pull_request):
661 """
661 """
662 Get the updated list of commits for the pull request
662 Get the updated list of commits for the pull request
663 and return the new pull request version and the list
663 and return the new pull request version and the list
664 of commits processed by this update action
664 of commits processed by this update action
665 """
665 """
666 pull_request = self.__get_pull_request(pull_request)
666 pull_request = self.__get_pull_request(pull_request)
667 source_ref_type = pull_request.source_ref_parts.type
667 source_ref_type = pull_request.source_ref_parts.type
668 source_ref_name = pull_request.source_ref_parts.name
668 source_ref_name = pull_request.source_ref_parts.name
669 source_ref_id = pull_request.source_ref_parts.commit_id
669 source_ref_id = pull_request.source_ref_parts.commit_id
670
670
671 target_ref_type = pull_request.target_ref_parts.type
671 target_ref_type = pull_request.target_ref_parts.type
672 target_ref_name = pull_request.target_ref_parts.name
672 target_ref_name = pull_request.target_ref_parts.name
673 target_ref_id = pull_request.target_ref_parts.commit_id
673 target_ref_id = pull_request.target_ref_parts.commit_id
674
674
675 if not self.has_valid_update_type(pull_request):
675 if not self.has_valid_update_type(pull_request):
676 log.debug("Skipping update of pull request %s due to ref type: %s",
676 log.debug("Skipping update of pull request %s due to ref type: %s",
677 pull_request, source_ref_type)
677 pull_request, source_ref_type)
678 return UpdateResponse(
678 return UpdateResponse(
679 executed=False,
679 executed=False,
680 reason=UpdateFailureReason.WRONG_REF_TYPE,
680 reason=UpdateFailureReason.WRONG_REF_TYPE,
681 old=pull_request, new=None, changes=None,
681 old=pull_request, new=None, changes=None,
682 source_changed=False, target_changed=False)
682 source_changed=False, target_changed=False)
683
683
684 # source repo
684 # source repo
685 source_repo = pull_request.source_repo.scm_instance()
685 source_repo = pull_request.source_repo.scm_instance()
686
686
687 try:
687 try:
688 source_commit = source_repo.get_commit(commit_id=source_ref_name)
688 source_commit = source_repo.get_commit(commit_id=source_ref_name)
689 except CommitDoesNotExistError:
689 except CommitDoesNotExistError:
690 return UpdateResponse(
690 return UpdateResponse(
691 executed=False,
691 executed=False,
692 reason=UpdateFailureReason.MISSING_SOURCE_REF,
692 reason=UpdateFailureReason.MISSING_SOURCE_REF,
693 old=pull_request, new=None, changes=None,
693 old=pull_request, new=None, changes=None,
694 source_changed=False, target_changed=False)
694 source_changed=False, target_changed=False)
695
695
696 source_changed = source_ref_id != source_commit.raw_id
696 source_changed = source_ref_id != source_commit.raw_id
697
697
698 # target repo
698 # target repo
699 target_repo = pull_request.target_repo.scm_instance()
699 target_repo = pull_request.target_repo.scm_instance()
700
700
701 try:
701 try:
702 target_commit = target_repo.get_commit(commit_id=target_ref_name)
702 target_commit = target_repo.get_commit(commit_id=target_ref_name)
703 except CommitDoesNotExistError:
703 except CommitDoesNotExistError:
704 return UpdateResponse(
704 return UpdateResponse(
705 executed=False,
705 executed=False,
706 reason=UpdateFailureReason.MISSING_TARGET_REF,
706 reason=UpdateFailureReason.MISSING_TARGET_REF,
707 old=pull_request, new=None, changes=None,
707 old=pull_request, new=None, changes=None,
708 source_changed=False, target_changed=False)
708 source_changed=False, target_changed=False)
709 target_changed = target_ref_id != target_commit.raw_id
709 target_changed = target_ref_id != target_commit.raw_id
710
710
711 if not (source_changed or target_changed):
711 if not (source_changed or target_changed):
712 log.debug("Nothing changed in pull request %s", pull_request)
712 log.debug("Nothing changed in pull request %s", pull_request)
713 return UpdateResponse(
713 return UpdateResponse(
714 executed=False,
714 executed=False,
715 reason=UpdateFailureReason.NO_CHANGE,
715 reason=UpdateFailureReason.NO_CHANGE,
716 old=pull_request, new=None, changes=None,
716 old=pull_request, new=None, changes=None,
717 source_changed=target_changed, target_changed=source_changed)
717 source_changed=target_changed, target_changed=source_changed)
718
718
719 change_in_found = 'target repo' if target_changed else 'source repo'
719 change_in_found = 'target repo' if target_changed else 'source repo'
720 log.debug('Updating pull request because of change in %s detected',
720 log.debug('Updating pull request because of change in %s detected',
721 change_in_found)
721 change_in_found)
722
722
723 # Finally there is a need for an update, in case of source change
723 # Finally there is a need for an update, in case of source change
724 # we create a new version, else just an update
724 # we create a new version, else just an update
725 if source_changed:
725 if source_changed:
726 pull_request_version = self._create_version_from_snapshot(pull_request)
726 pull_request_version = self._create_version_from_snapshot(pull_request)
727 self._link_comments_to_version(pull_request_version)
727 self._link_comments_to_version(pull_request_version)
728 else:
728 else:
729 try:
729 try:
730 ver = pull_request.versions[-1]
730 ver = pull_request.versions[-1]
731 except IndexError:
731 except IndexError:
732 ver = None
732 ver = None
733
733
734 pull_request.pull_request_version_id = \
734 pull_request.pull_request_version_id = \
735 ver.pull_request_version_id if ver else None
735 ver.pull_request_version_id if ver else None
736 pull_request_version = pull_request
736 pull_request_version = pull_request
737
737
738 try:
738 try:
739 if target_ref_type in self.REF_TYPES:
739 if target_ref_type in self.REF_TYPES:
740 target_commit = target_repo.get_commit(target_ref_name)
740 target_commit = target_repo.get_commit(target_ref_name)
741 else:
741 else:
742 target_commit = target_repo.get_commit(target_ref_id)
742 target_commit = target_repo.get_commit(target_ref_id)
743 except CommitDoesNotExistError:
743 except CommitDoesNotExistError:
744 return UpdateResponse(
744 return UpdateResponse(
745 executed=False,
745 executed=False,
746 reason=UpdateFailureReason.MISSING_TARGET_REF,
746 reason=UpdateFailureReason.MISSING_TARGET_REF,
747 old=pull_request, new=None, changes=None,
747 old=pull_request, new=None, changes=None,
748 source_changed=source_changed, target_changed=target_changed)
748 source_changed=source_changed, target_changed=target_changed)
749
749
750 # re-compute commit ids
750 # re-compute commit ids
751 old_commit_ids = pull_request.revisions
751 old_commit_ids = pull_request.revisions
752 pre_load = ["author", "branch", "date", "message"]
752 pre_load = ["author", "branch", "date", "message"]
753 commit_ranges = target_repo.compare(
753 commit_ranges = target_repo.compare(
754 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
754 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
755 pre_load=pre_load)
755 pre_load=pre_load)
756
756
757 ancestor = source_repo.get_common_ancestor(
757 ancestor = source_repo.get_common_ancestor(
758 source_commit.raw_id, target_commit.raw_id, target_repo)
758 source_commit.raw_id, target_commit.raw_id, target_repo)
759
759
760 pull_request.source_ref = '%s:%s:%s' % (
760 pull_request.source_ref = '%s:%s:%s' % (
761 source_ref_type, source_ref_name, source_commit.raw_id)
761 source_ref_type, source_ref_name, source_commit.raw_id)
762 pull_request.target_ref = '%s:%s:%s' % (
762 pull_request.target_ref = '%s:%s:%s' % (
763 target_ref_type, target_ref_name, ancestor)
763 target_ref_type, target_ref_name, ancestor)
764
764
765 pull_request.revisions = [
765 pull_request.revisions = [
766 commit.raw_id for commit in reversed(commit_ranges)]
766 commit.raw_id for commit in reversed(commit_ranges)]
767 pull_request.updated_on = datetime.datetime.now()
767 pull_request.updated_on = datetime.datetime.now()
768 Session().add(pull_request)
768 Session().add(pull_request)
769 new_commit_ids = pull_request.revisions
769 new_commit_ids = pull_request.revisions
770
770
771 old_diff_data, new_diff_data = self._generate_update_diffs(
771 old_diff_data, new_diff_data = self._generate_update_diffs(
772 pull_request, pull_request_version)
772 pull_request, pull_request_version)
773
773
774 # calculate commit and file changes
774 # calculate commit and file changes
775 changes = self._calculate_commit_id_changes(
775 changes = self._calculate_commit_id_changes(
776 old_commit_ids, new_commit_ids)
776 old_commit_ids, new_commit_ids)
777 file_changes = self._calculate_file_changes(
777 file_changes = self._calculate_file_changes(
778 old_diff_data, new_diff_data)
778 old_diff_data, new_diff_data)
779
779
780 # set comments as outdated if DIFFS changed
780 # set comments as outdated if DIFFS changed
781 CommentsModel().outdate_comments(
781 CommentsModel().outdate_comments(
782 pull_request, old_diff_data=old_diff_data,
782 pull_request, old_diff_data=old_diff_data,
783 new_diff_data=new_diff_data)
783 new_diff_data=new_diff_data)
784
784
785 commit_changes = (changes.added or changes.removed)
785 commit_changes = (changes.added or changes.removed)
786 file_node_changes = (
786 file_node_changes = (
787 file_changes.added or file_changes.modified or file_changes.removed)
787 file_changes.added or file_changes.modified or file_changes.removed)
788 pr_has_changes = commit_changes or file_node_changes
788 pr_has_changes = commit_changes or file_node_changes
789
789
790 # Add an automatic comment to the pull request, in case
790 # Add an automatic comment to the pull request, in case
791 # anything has changed
791 # anything has changed
792 if pr_has_changes:
792 if pr_has_changes:
793 update_comment = CommentsModel().create(
793 update_comment = CommentsModel().create(
794 text=self._render_update_message(changes, file_changes),
794 text=self._render_update_message(changes, file_changes),
795 repo=pull_request.target_repo,
795 repo=pull_request.target_repo,
796 user=pull_request.author,
796 user=pull_request.author,
797 pull_request=pull_request,
797 pull_request=pull_request,
798 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
798 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
799
799
800 # Update status to "Under Review" for added commits
800 # Update status to "Under Review" for added commits
801 for commit_id in changes.added:
801 for commit_id in changes.added:
802 ChangesetStatusModel().set_status(
802 ChangesetStatusModel().set_status(
803 repo=pull_request.source_repo,
803 repo=pull_request.source_repo,
804 status=ChangesetStatus.STATUS_UNDER_REVIEW,
804 status=ChangesetStatus.STATUS_UNDER_REVIEW,
805 comment=update_comment,
805 comment=update_comment,
806 user=pull_request.author,
806 user=pull_request.author,
807 pull_request=pull_request,
807 pull_request=pull_request,
808 revision=commit_id)
808 revision=commit_id)
809
809
810 log.debug(
810 log.debug(
811 'Updated pull request %s, added_ids: %s, common_ids: %s, '
811 'Updated pull request %s, added_ids: %s, common_ids: %s, '
812 'removed_ids: %s', pull_request.pull_request_id,
812 'removed_ids: %s', pull_request.pull_request_id,
813 changes.added, changes.common, changes.removed)
813 changes.added, changes.common, changes.removed)
814 log.debug(
814 log.debug(
815 'Updated pull request with the following file changes: %s',
815 'Updated pull request with the following file changes: %s',
816 file_changes)
816 file_changes)
817
817
818 log.info(
818 log.info(
819 "Updated pull request %s from commit %s to commit %s, "
819 "Updated pull request %s from commit %s to commit %s, "
820 "stored new version %s of this pull request.",
820 "stored new version %s of this pull request.",
821 pull_request.pull_request_id, source_ref_id,
821 pull_request.pull_request_id, source_ref_id,
822 pull_request.source_ref_parts.commit_id,
822 pull_request.source_ref_parts.commit_id,
823 pull_request_version.pull_request_version_id)
823 pull_request_version.pull_request_version_id)
824 Session().commit()
824 Session().commit()
825 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
825 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
826
826
827 return UpdateResponse(
827 return UpdateResponse(
828 executed=True, reason=UpdateFailureReason.NONE,
828 executed=True, reason=UpdateFailureReason.NONE,
829 old=pull_request, new=pull_request_version, changes=changes,
829 old=pull_request, new=pull_request_version, changes=changes,
830 source_changed=source_changed, target_changed=target_changed)
830 source_changed=source_changed, target_changed=target_changed)
831
831
832 def _create_version_from_snapshot(self, pull_request):
832 def _create_version_from_snapshot(self, pull_request):
833 version = PullRequestVersion()
833 version = PullRequestVersion()
834 version.title = pull_request.title
834 version.title = pull_request.title
835 version.description = pull_request.description
835 version.description = pull_request.description
836 version.status = pull_request.status
836 version.status = pull_request.status
837 version.pull_request_state = pull_request.pull_request_state
837 version.pull_request_state = pull_request.pull_request_state
838 version.created_on = datetime.datetime.now()
838 version.created_on = datetime.datetime.now()
839 version.updated_on = pull_request.updated_on
839 version.updated_on = pull_request.updated_on
840 version.user_id = pull_request.user_id
840 version.user_id = pull_request.user_id
841 version.source_repo = pull_request.source_repo
841 version.source_repo = pull_request.source_repo
842 version.source_ref = pull_request.source_ref
842 version.source_ref = pull_request.source_ref
843 version.target_repo = pull_request.target_repo
843 version.target_repo = pull_request.target_repo
844 version.target_ref = pull_request.target_ref
844 version.target_ref = pull_request.target_ref
845
845
846 version._last_merge_source_rev = pull_request._last_merge_source_rev
846 version._last_merge_source_rev = pull_request._last_merge_source_rev
847 version._last_merge_target_rev = pull_request._last_merge_target_rev
847 version._last_merge_target_rev = pull_request._last_merge_target_rev
848 version.last_merge_status = pull_request.last_merge_status
848 version.last_merge_status = pull_request.last_merge_status
849 version.shadow_merge_ref = pull_request.shadow_merge_ref
849 version.shadow_merge_ref = pull_request.shadow_merge_ref
850 version.merge_rev = pull_request.merge_rev
850 version.merge_rev = pull_request.merge_rev
851 version.reviewer_data = pull_request.reviewer_data
851 version.reviewer_data = pull_request.reviewer_data
852
852
853 version.revisions = pull_request.revisions
853 version.revisions = pull_request.revisions
854 version.pull_request = pull_request
854 version.pull_request = pull_request
855 Session().add(version)
855 Session().add(version)
856 Session().flush()
856 Session().flush()
857
857
858 return version
858 return version
859
859
860 def _generate_update_diffs(self, pull_request, pull_request_version):
860 def _generate_update_diffs(self, pull_request, pull_request_version):
861
861
862 diff_context = (
862 diff_context = (
863 self.DIFF_CONTEXT +
863 self.DIFF_CONTEXT +
864 CommentsModel.needed_extra_diff_context())
864 CommentsModel.needed_extra_diff_context())
865 hide_whitespace_changes = False
865 hide_whitespace_changes = False
866 source_repo = pull_request_version.source_repo
866 source_repo = pull_request_version.source_repo
867 source_ref_id = pull_request_version.source_ref_parts.commit_id
867 source_ref_id = pull_request_version.source_ref_parts.commit_id
868 target_ref_id = pull_request_version.target_ref_parts.commit_id
868 target_ref_id = pull_request_version.target_ref_parts.commit_id
869 old_diff = self._get_diff_from_pr_or_version(
869 old_diff = self._get_diff_from_pr_or_version(
870 source_repo, source_ref_id, target_ref_id,
870 source_repo, source_ref_id, target_ref_id,
871 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
871 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
872
872
873 source_repo = pull_request.source_repo
873 source_repo = pull_request.source_repo
874 source_ref_id = pull_request.source_ref_parts.commit_id
874 source_ref_id = pull_request.source_ref_parts.commit_id
875 target_ref_id = pull_request.target_ref_parts.commit_id
875 target_ref_id = pull_request.target_ref_parts.commit_id
876
876
877 new_diff = self._get_diff_from_pr_or_version(
877 new_diff = self._get_diff_from_pr_or_version(
878 source_repo, source_ref_id, target_ref_id,
878 source_repo, source_ref_id, target_ref_id,
879 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
879 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
880
880
881 old_diff_data = diffs.DiffProcessor(old_diff)
881 old_diff_data = diffs.DiffProcessor(old_diff)
882 old_diff_data.prepare()
882 old_diff_data.prepare()
883 new_diff_data = diffs.DiffProcessor(new_diff)
883 new_diff_data = diffs.DiffProcessor(new_diff)
884 new_diff_data.prepare()
884 new_diff_data.prepare()
885
885
886 return old_diff_data, new_diff_data
886 return old_diff_data, new_diff_data
887
887
888 def _link_comments_to_version(self, pull_request_version):
888 def _link_comments_to_version(self, pull_request_version):
889 """
889 """
890 Link all unlinked comments of this pull request to the given version.
890 Link all unlinked comments of this pull request to the given version.
891
891
892 :param pull_request_version: The `PullRequestVersion` to which
892 :param pull_request_version: The `PullRequestVersion` to which
893 the comments shall be linked.
893 the comments shall be linked.
894
894
895 """
895 """
896 pull_request = pull_request_version.pull_request
896 pull_request = pull_request_version.pull_request
897 comments = ChangesetComment.query()\
897 comments = ChangesetComment.query()\
898 .filter(
898 .filter(
899 # TODO: johbo: Should we query for the repo at all here?
899 # TODO: johbo: Should we query for the repo at all here?
900 # Pending decision on how comments of PRs are to be related
900 # Pending decision on how comments of PRs are to be related
901 # to either the source repo, the target repo or no repo at all.
901 # to either the source repo, the target repo or no repo at all.
902 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
902 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
903 ChangesetComment.pull_request == pull_request,
903 ChangesetComment.pull_request == pull_request,
904 ChangesetComment.pull_request_version == None)\
904 ChangesetComment.pull_request_version == None)\
905 .order_by(ChangesetComment.comment_id.asc())
905 .order_by(ChangesetComment.comment_id.asc())
906
906
907 # TODO: johbo: Find out why this breaks if it is done in a bulk
907 # TODO: johbo: Find out why this breaks if it is done in a bulk
908 # operation.
908 # operation.
909 for comment in comments:
909 for comment in comments:
910 comment.pull_request_version_id = (
910 comment.pull_request_version_id = (
911 pull_request_version.pull_request_version_id)
911 pull_request_version.pull_request_version_id)
912 Session().add(comment)
912 Session().add(comment)
913
913
914 def _calculate_commit_id_changes(self, old_ids, new_ids):
914 def _calculate_commit_id_changes(self, old_ids, new_ids):
915 added = [x for x in new_ids if x not in old_ids]
915 added = [x for x in new_ids if x not in old_ids]
916 common = [x for x in new_ids if x in old_ids]
916 common = [x for x in new_ids if x in old_ids]
917 removed = [x for x in old_ids if x not in new_ids]
917 removed = [x for x in old_ids if x not in new_ids]
918 total = new_ids
918 total = new_ids
919 return ChangeTuple(added, common, removed, total)
919 return ChangeTuple(added, common, removed, total)
920
920
921 def _calculate_file_changes(self, old_diff_data, new_diff_data):
921 def _calculate_file_changes(self, old_diff_data, new_diff_data):
922
922
923 old_files = OrderedDict()
923 old_files = OrderedDict()
924 for diff_data in old_diff_data.parsed_diff:
924 for diff_data in old_diff_data.parsed_diff:
925 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
925 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
926
926
927 added_files = []
927 added_files = []
928 modified_files = []
928 modified_files = []
929 removed_files = []
929 removed_files = []
930 for diff_data in new_diff_data.parsed_diff:
930 for diff_data in new_diff_data.parsed_diff:
931 new_filename = diff_data['filename']
931 new_filename = diff_data['filename']
932 new_hash = md5_safe(diff_data['raw_diff'])
932 new_hash = md5_safe(diff_data['raw_diff'])
933
933
934 old_hash = old_files.get(new_filename)
934 old_hash = old_files.get(new_filename)
935 if not old_hash:
935 if not old_hash:
936 # file is not present in old diff, means it's added
936 # file is not present in old diff, means it's added
937 added_files.append(new_filename)
937 added_files.append(new_filename)
938 else:
938 else:
939 if new_hash != old_hash:
939 if new_hash != old_hash:
940 modified_files.append(new_filename)
940 modified_files.append(new_filename)
941 # now remove a file from old, since we have seen it already
941 # now remove a file from old, since we have seen it already
942 del old_files[new_filename]
942 del old_files[new_filename]
943
943
944 # removed files is when there are present in old, but not in NEW,
944 # removed files is when there are present in old, but not in NEW,
945 # since we remove old files that are present in new diff, left-overs
945 # since we remove old files that are present in new diff, left-overs
946 # if any should be the removed files
946 # if any should be the removed files
947 removed_files.extend(old_files.keys())
947 removed_files.extend(old_files.keys())
948
948
949 return FileChangeTuple(added_files, modified_files, removed_files)
949 return FileChangeTuple(added_files, modified_files, removed_files)
950
950
951 def _render_update_message(self, changes, file_changes):
951 def _render_update_message(self, changes, file_changes):
952 """
952 """
953 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
953 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
954 so it's always looking the same disregarding on which default
954 so it's always looking the same disregarding on which default
955 renderer system is using.
955 renderer system is using.
956
956
957 :param changes: changes named tuple
957 :param changes: changes named tuple
958 :param file_changes: file changes named tuple
958 :param file_changes: file changes named tuple
959
959
960 """
960 """
961 new_status = ChangesetStatus.get_status_lbl(
961 new_status = ChangesetStatus.get_status_lbl(
962 ChangesetStatus.STATUS_UNDER_REVIEW)
962 ChangesetStatus.STATUS_UNDER_REVIEW)
963
963
964 changed_files = (
964 changed_files = (
965 file_changes.added + file_changes.modified + file_changes.removed)
965 file_changes.added + file_changes.modified + file_changes.removed)
966
966
967 params = {
967 params = {
968 'under_review_label': new_status,
968 'under_review_label': new_status,
969 'added_commits': changes.added,
969 'added_commits': changes.added,
970 'removed_commits': changes.removed,
970 'removed_commits': changes.removed,
971 'changed_files': changed_files,
971 'changed_files': changed_files,
972 'added_files': file_changes.added,
972 'added_files': file_changes.added,
973 'modified_files': file_changes.modified,
973 'modified_files': file_changes.modified,
974 'removed_files': file_changes.removed,
974 'removed_files': file_changes.removed,
975 }
975 }
976 renderer = RstTemplateRenderer()
976 renderer = RstTemplateRenderer()
977 return renderer.render('pull_request_update.mako', **params)
977 return renderer.render('pull_request_update.mako', **params)
978
978
979 def edit(self, pull_request, title, description, description_renderer, user):
979 def edit(self, pull_request, title, description, description_renderer, user):
980 pull_request = self.__get_pull_request(pull_request)
980 pull_request = self.__get_pull_request(pull_request)
981 old_data = pull_request.get_api_data(with_merge_state=False)
981 old_data = pull_request.get_api_data(with_merge_state=False)
982 if pull_request.is_closed():
982 if pull_request.is_closed():
983 raise ValueError('This pull request is closed')
983 raise ValueError('This pull request is closed')
984 if title:
984 if title:
985 pull_request.title = title
985 pull_request.title = title
986 pull_request.description = description
986 pull_request.description = description
987 pull_request.updated_on = datetime.datetime.now()
987 pull_request.updated_on = datetime.datetime.now()
988 pull_request.description_renderer = description_renderer
988 pull_request.description_renderer = description_renderer
989 Session().add(pull_request)
989 Session().add(pull_request)
990 self._log_audit_action(
990 self._log_audit_action(
991 'repo.pull_request.edit', {'old_data': old_data},
991 'repo.pull_request.edit', {'old_data': old_data},
992 user, pull_request)
992 user, pull_request)
993
993
994 def update_reviewers(self, pull_request, reviewer_data, user):
994 def update_reviewers(self, pull_request, reviewer_data, user):
995 """
995 """
996 Update the reviewers in the pull request
996 Update the reviewers in the pull request
997
997
998 :param pull_request: the pr to update
998 :param pull_request: the pr to update
999 :param reviewer_data: list of tuples
999 :param reviewer_data: list of tuples
1000 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1000 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1001 """
1001 """
1002 pull_request = self.__get_pull_request(pull_request)
1002 pull_request = self.__get_pull_request(pull_request)
1003 if pull_request.is_closed():
1003 if pull_request.is_closed():
1004 raise ValueError('This pull request is closed')
1004 raise ValueError('This pull request is closed')
1005
1005
1006 reviewers = {}
1006 reviewers = {}
1007 for user_id, reasons, mandatory, rules in reviewer_data:
1007 for user_id, reasons, mandatory, rules in reviewer_data:
1008 if isinstance(user_id, (int, compat.string_types)):
1008 if isinstance(user_id, (int, compat.string_types)):
1009 user_id = self._get_user(user_id).user_id
1009 user_id = self._get_user(user_id).user_id
1010 reviewers[user_id] = {
1010 reviewers[user_id] = {
1011 'reasons': reasons, 'mandatory': mandatory}
1011 'reasons': reasons, 'mandatory': mandatory}
1012
1012
1013 reviewers_ids = set(reviewers.keys())
1013 reviewers_ids = set(reviewers.keys())
1014 current_reviewers = PullRequestReviewers.query()\
1014 current_reviewers = PullRequestReviewers.query()\
1015 .filter(PullRequestReviewers.pull_request ==
1015 .filter(PullRequestReviewers.pull_request ==
1016 pull_request).all()
1016 pull_request).all()
1017 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1017 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1018
1018
1019 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1019 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1020 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1020 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1021
1021
1022 log.debug("Adding %s reviewers", ids_to_add)
1022 log.debug("Adding %s reviewers", ids_to_add)
1023 log.debug("Removing %s reviewers", ids_to_remove)
1023 log.debug("Removing %s reviewers", ids_to_remove)
1024 changed = False
1024 changed = False
1025 added_audit_reviewers = []
1025 added_audit_reviewers = []
1026 removed_audit_reviewers = []
1026 removed_audit_reviewers = []
1027
1027
1028 for uid in ids_to_add:
1028 for uid in ids_to_add:
1029 changed = True
1029 changed = True
1030 _usr = self._get_user(uid)
1030 _usr = self._get_user(uid)
1031 reviewer = PullRequestReviewers()
1031 reviewer = PullRequestReviewers()
1032 reviewer.user = _usr
1032 reviewer.user = _usr
1033 reviewer.pull_request = pull_request
1033 reviewer.pull_request = pull_request
1034 reviewer.reasons = reviewers[uid]['reasons']
1034 reviewer.reasons = reviewers[uid]['reasons']
1035 # NOTE(marcink): mandatory shouldn't be changed now
1035 # NOTE(marcink): mandatory shouldn't be changed now
1036 # reviewer.mandatory = reviewers[uid]['reasons']
1036 # reviewer.mandatory = reviewers[uid]['reasons']
1037 Session().add(reviewer)
1037 Session().add(reviewer)
1038 added_audit_reviewers.append(reviewer.get_dict())
1038 added_audit_reviewers.append(reviewer.get_dict())
1039
1039
1040 for uid in ids_to_remove:
1040 for uid in ids_to_remove:
1041 changed = True
1041 changed = True
1042 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1042 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1043 # that prevents and fixes cases that we added the same reviewer twice.
1043 # that prevents and fixes cases that we added the same reviewer twice.
1044 # this CAN happen due to the lack of DB checks
1044 # this CAN happen due to the lack of DB checks
1045 reviewers = PullRequestReviewers.query()\
1045 reviewers = PullRequestReviewers.query()\
1046 .filter(PullRequestReviewers.user_id == uid,
1046 .filter(PullRequestReviewers.user_id == uid,
1047 PullRequestReviewers.pull_request == pull_request)\
1047 PullRequestReviewers.pull_request == pull_request)\
1048 .all()
1048 .all()
1049
1049
1050 for obj in reviewers:
1050 for obj in reviewers:
1051 added_audit_reviewers.append(obj.get_dict())
1051 added_audit_reviewers.append(obj.get_dict())
1052 Session().delete(obj)
1052 Session().delete(obj)
1053
1053
1054 if changed:
1054 if changed:
1055 Session().expire_all()
1055 Session().expire_all()
1056 pull_request.updated_on = datetime.datetime.now()
1056 pull_request.updated_on = datetime.datetime.now()
1057 Session().add(pull_request)
1057 Session().add(pull_request)
1058
1058
1059 # finally store audit logs
1059 # finally store audit logs
1060 for user_data in added_audit_reviewers:
1060 for user_data in added_audit_reviewers:
1061 self._log_audit_action(
1061 self._log_audit_action(
1062 'repo.pull_request.reviewer.add', {'data': user_data},
1062 'repo.pull_request.reviewer.add', {'data': user_data},
1063 user, pull_request)
1063 user, pull_request)
1064 for user_data in removed_audit_reviewers:
1064 for user_data in removed_audit_reviewers:
1065 self._log_audit_action(
1065 self._log_audit_action(
1066 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1066 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1067 user, pull_request)
1067 user, pull_request)
1068
1068
1069 self.notify_reviewers(pull_request, ids_to_add)
1069 self.notify_reviewers(pull_request, ids_to_add)
1070 return ids_to_add, ids_to_remove
1070 return ids_to_add, ids_to_remove
1071
1071
1072 def get_url(self, pull_request, request=None, permalink=False):
1072 def get_url(self, pull_request, request=None, permalink=False):
1073 if not request:
1073 if not request:
1074 request = get_current_request()
1074 request = get_current_request()
1075
1075
1076 if permalink:
1076 if permalink:
1077 return request.route_url(
1077 return request.route_url(
1078 'pull_requests_global',
1078 'pull_requests_global',
1079 pull_request_id=pull_request.pull_request_id,)
1079 pull_request_id=pull_request.pull_request_id,)
1080 else:
1080 else:
1081 return request.route_url('pullrequest_show',
1081 return request.route_url('pullrequest_show',
1082 repo_name=safe_str(pull_request.target_repo.repo_name),
1082 repo_name=safe_str(pull_request.target_repo.repo_name),
1083 pull_request_id=pull_request.pull_request_id,)
1083 pull_request_id=pull_request.pull_request_id,)
1084
1084
1085 def get_shadow_clone_url(self, pull_request, request=None):
1085 def get_shadow_clone_url(self, pull_request, request=None):
1086 """
1086 """
1087 Returns qualified url pointing to the shadow repository. If this pull
1087 Returns qualified url pointing to the shadow repository. If this pull
1088 request is closed there is no shadow repository and ``None`` will be
1088 request is closed there is no shadow repository and ``None`` will be
1089 returned.
1089 returned.
1090 """
1090 """
1091 if pull_request.is_closed():
1091 if pull_request.is_closed():
1092 return None
1092 return None
1093 else:
1093 else:
1094 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1094 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1095 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1095 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1096
1096
1097 def notify_reviewers(self, pull_request, reviewers_ids):
1097 def notify_reviewers(self, pull_request, reviewers_ids):
1098 # notification to reviewers
1098 # notification to reviewers
1099 if not reviewers_ids:
1099 if not reviewers_ids:
1100 return
1100 return
1101
1101
1102 pull_request_obj = pull_request
1102 pull_request_obj = pull_request
1103 # get the current participants of this pull request
1103 # get the current participants of this pull request
1104 recipients = reviewers_ids
1104 recipients = reviewers_ids
1105 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1105 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1106
1106
1107 pr_source_repo = pull_request_obj.source_repo
1107 pr_source_repo = pull_request_obj.source_repo
1108 pr_target_repo = pull_request_obj.target_repo
1108 pr_target_repo = pull_request_obj.target_repo
1109
1109
1110 pr_url = h.route_url('pullrequest_show',
1110 pr_url = h.route_url('pullrequest_show',
1111 repo_name=pr_target_repo.repo_name,
1111 repo_name=pr_target_repo.repo_name,
1112 pull_request_id=pull_request_obj.pull_request_id,)
1112 pull_request_id=pull_request_obj.pull_request_id,)
1113
1113
1114 # set some variables for email notification
1114 # set some variables for email notification
1115 pr_target_repo_url = h.route_url(
1115 pr_target_repo_url = h.route_url(
1116 'repo_summary', repo_name=pr_target_repo.repo_name)
1116 'repo_summary', repo_name=pr_target_repo.repo_name)
1117
1117
1118 pr_source_repo_url = h.route_url(
1118 pr_source_repo_url = h.route_url(
1119 'repo_summary', repo_name=pr_source_repo.repo_name)
1119 'repo_summary', repo_name=pr_source_repo.repo_name)
1120
1120
1121 # pull request specifics
1121 # pull request specifics
1122 pull_request_commits = [
1122 pull_request_commits = [
1123 (x.raw_id, x.message)
1123 (x.raw_id, x.message)
1124 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1124 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1125
1125
1126 kwargs = {
1126 kwargs = {
1127 'user': pull_request.author,
1127 'user': pull_request.author,
1128 'pull_request': pull_request_obj,
1128 'pull_request': pull_request_obj,
1129 'pull_request_commits': pull_request_commits,
1129 'pull_request_commits': pull_request_commits,
1130
1130
1131 'pull_request_target_repo': pr_target_repo,
1131 'pull_request_target_repo': pr_target_repo,
1132 'pull_request_target_repo_url': pr_target_repo_url,
1132 'pull_request_target_repo_url': pr_target_repo_url,
1133
1133
1134 'pull_request_source_repo': pr_source_repo,
1134 'pull_request_source_repo': pr_source_repo,
1135 'pull_request_source_repo_url': pr_source_repo_url,
1135 'pull_request_source_repo_url': pr_source_repo_url,
1136
1136
1137 'pull_request_url': pr_url,
1137 'pull_request_url': pr_url,
1138 }
1138 }
1139
1139
1140 # pre-generate the subject for notification itself
1140 # pre-generate the subject for notification itself
1141 (subject,
1141 (subject,
1142 _h, _e, # we don't care about those
1142 _h, _e, # we don't care about those
1143 body_plaintext) = EmailNotificationModel().render_email(
1143 body_plaintext) = EmailNotificationModel().render_email(
1144 notification_type, **kwargs)
1144 notification_type, **kwargs)
1145
1145
1146 # create notification objects, and emails
1146 # create notification objects, and emails
1147 NotificationModel().create(
1147 NotificationModel().create(
1148 created_by=pull_request.author,
1148 created_by=pull_request.author,
1149 notification_subject=subject,
1149 notification_subject=subject,
1150 notification_body=body_plaintext,
1150 notification_body=body_plaintext,
1151 notification_type=notification_type,
1151 notification_type=notification_type,
1152 recipients=recipients,
1152 recipients=recipients,
1153 email_kwargs=kwargs,
1153 email_kwargs=kwargs,
1154 )
1154 )
1155
1155
1156 def delete(self, pull_request, user):
1156 def delete(self, pull_request, user):
1157 pull_request = self.__get_pull_request(pull_request)
1157 pull_request = self.__get_pull_request(pull_request)
1158 old_data = pull_request.get_api_data(with_merge_state=False)
1158 old_data = pull_request.get_api_data(with_merge_state=False)
1159 self._cleanup_merge_workspace(pull_request)
1159 self._cleanup_merge_workspace(pull_request)
1160 self._log_audit_action(
1160 self._log_audit_action(
1161 'repo.pull_request.delete', {'old_data': old_data},
1161 'repo.pull_request.delete', {'old_data': old_data},
1162 user, pull_request)
1162 user, pull_request)
1163 Session().delete(pull_request)
1163 Session().delete(pull_request)
1164
1164
1165 def close_pull_request(self, pull_request, user):
1165 def close_pull_request(self, pull_request, user):
1166 pull_request = self.__get_pull_request(pull_request)
1166 pull_request = self.__get_pull_request(pull_request)
1167 self._cleanup_merge_workspace(pull_request)
1167 self._cleanup_merge_workspace(pull_request)
1168 pull_request.status = PullRequest.STATUS_CLOSED
1168 pull_request.status = PullRequest.STATUS_CLOSED
1169 pull_request.updated_on = datetime.datetime.now()
1169 pull_request.updated_on = datetime.datetime.now()
1170 Session().add(pull_request)
1170 Session().add(pull_request)
1171 self.trigger_pull_request_hook(
1171 self.trigger_pull_request_hook(
1172 pull_request, pull_request.author, 'close')
1172 pull_request, pull_request.author, 'close')
1173
1173
1174 pr_data = pull_request.get_api_data(with_merge_state=False)
1174 pr_data = pull_request.get_api_data(with_merge_state=False)
1175 self._log_audit_action(
1175 self._log_audit_action(
1176 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1176 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1177
1177
1178 def close_pull_request_with_comment(
1178 def close_pull_request_with_comment(
1179 self, pull_request, user, repo, message=None, auth_user=None):
1179 self, pull_request, user, repo, message=None, auth_user=None):
1180
1180
1181 pull_request_review_status = pull_request.calculated_review_status()
1181 pull_request_review_status = pull_request.calculated_review_status()
1182
1182
1183 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1183 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1184 # approved only if we have voting consent
1184 # approved only if we have voting consent
1185 status = ChangesetStatus.STATUS_APPROVED
1185 status = ChangesetStatus.STATUS_APPROVED
1186 else:
1186 else:
1187 status = ChangesetStatus.STATUS_REJECTED
1187 status = ChangesetStatus.STATUS_REJECTED
1188 status_lbl = ChangesetStatus.get_status_lbl(status)
1188 status_lbl = ChangesetStatus.get_status_lbl(status)
1189
1189
1190 default_message = (
1190 default_message = (
1191 'Closing with status change {transition_icon} {status}.'
1191 'Closing with status change {transition_icon} {status}.'
1192 ).format(transition_icon='>', status=status_lbl)
1192 ).format(transition_icon='>', status=status_lbl)
1193 text = message or default_message
1193 text = message or default_message
1194
1194
1195 # create a comment, and link it to new status
1195 # create a comment, and link it to new status
1196 comment = CommentsModel().create(
1196 comment = CommentsModel().create(
1197 text=text,
1197 text=text,
1198 repo=repo.repo_id,
1198 repo=repo.repo_id,
1199 user=user.user_id,
1199 user=user.user_id,
1200 pull_request=pull_request.pull_request_id,
1200 pull_request=pull_request.pull_request_id,
1201 status_change=status_lbl,
1201 status_change=status_lbl,
1202 status_change_type=status,
1202 status_change_type=status,
1203 closing_pr=True,
1203 closing_pr=True,
1204 auth_user=auth_user,
1204 auth_user=auth_user,
1205 )
1205 )
1206
1206
1207 # calculate old status before we change it
1207 # calculate old status before we change it
1208 old_calculated_status = pull_request.calculated_review_status()
1208 old_calculated_status = pull_request.calculated_review_status()
1209 ChangesetStatusModel().set_status(
1209 ChangesetStatusModel().set_status(
1210 repo.repo_id,
1210 repo.repo_id,
1211 status,
1211 status,
1212 user.user_id,
1212 user.user_id,
1213 comment=comment,
1213 comment=comment,
1214 pull_request=pull_request.pull_request_id
1214 pull_request=pull_request.pull_request_id
1215 )
1215 )
1216
1216
1217 Session().flush()
1217 Session().flush()
1218 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1218 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1219 # we now calculate the status of pull request again, and based on that
1219 # we now calculate the status of pull request again, and based on that
1220 # calculation trigger status change. This might happen in cases
1220 # calculation trigger status change. This might happen in cases
1221 # that non-reviewer admin closes a pr, which means his vote doesn't
1221 # that non-reviewer admin closes a pr, which means his vote doesn't
1222 # change the status, while if he's a reviewer this might change it.
1222 # change the status, while if he's a reviewer this might change it.
1223 calculated_status = pull_request.calculated_review_status()
1223 calculated_status = pull_request.calculated_review_status()
1224 if old_calculated_status != calculated_status:
1224 if old_calculated_status != calculated_status:
1225 self.trigger_pull_request_hook(
1225 self.trigger_pull_request_hook(
1226 pull_request, user, 'review_status_change',
1226 pull_request, user, 'review_status_change',
1227 data={'status': calculated_status})
1227 data={'status': calculated_status})
1228
1228
1229 # finally close the PR
1229 # finally close the PR
1230 PullRequestModel().close_pull_request(
1230 PullRequestModel().close_pull_request(
1231 pull_request.pull_request_id, user)
1231 pull_request.pull_request_id, user)
1232
1232
1233 return comment, status
1233 return comment, status
1234
1234
1235 def merge_status(self, pull_request, translator=None,
1235 def merge_status(self, pull_request, translator=None,
1236 force_shadow_repo_refresh=False):
1236 force_shadow_repo_refresh=False):
1237 _ = translator or get_current_request().translate
1237 _ = translator or get_current_request().translate
1238
1238
1239 if not self._is_merge_enabled(pull_request):
1239 if not self._is_merge_enabled(pull_request):
1240 return False, _('Server-side pull request merging is disabled.')
1240 return False, _('Server-side pull request merging is disabled.')
1241 if pull_request.is_closed():
1241 if pull_request.is_closed():
1242 return False, _('This pull request is closed.')
1242 return False, _('This pull request is closed.')
1243 merge_possible, msg = self._check_repo_requirements(
1243 merge_possible, msg = self._check_repo_requirements(
1244 target=pull_request.target_repo, source=pull_request.source_repo,
1244 target=pull_request.target_repo, source=pull_request.source_repo,
1245 translator=_)
1245 translator=_)
1246 if not merge_possible:
1246 if not merge_possible:
1247 return merge_possible, msg
1247 return merge_possible, msg
1248
1248
1249 try:
1249 try:
1250 resp = self._try_merge(
1250 resp = self._try_merge(
1251 pull_request,
1251 pull_request,
1252 force_shadow_repo_refresh=force_shadow_repo_refresh)
1252 force_shadow_repo_refresh=force_shadow_repo_refresh)
1253 log.debug("Merge response: %s", resp)
1253 log.debug("Merge response: %s", resp)
1254 status = resp.possible, resp.merge_status_message
1254 status = resp.possible, resp.merge_status_message
1255 except NotImplementedError:
1255 except NotImplementedError:
1256 status = False, _('Pull request merging is not supported.')
1256 status = False, _('Pull request merging is not supported.')
1257
1257
1258 return status
1258 return status
1259
1259
1260 def _check_repo_requirements(self, target, source, translator):
1260 def _check_repo_requirements(self, target, source, translator):
1261 """
1261 """
1262 Check if `target` and `source` have compatible requirements.
1262 Check if `target` and `source` have compatible requirements.
1263
1263
1264 Currently this is just checking for largefiles.
1264 Currently this is just checking for largefiles.
1265 """
1265 """
1266 _ = translator
1266 _ = translator
1267 target_has_largefiles = self._has_largefiles(target)
1267 target_has_largefiles = self._has_largefiles(target)
1268 source_has_largefiles = self._has_largefiles(source)
1268 source_has_largefiles = self._has_largefiles(source)
1269 merge_possible = True
1269 merge_possible = True
1270 message = u''
1270 message = u''
1271
1271
1272 if target_has_largefiles != source_has_largefiles:
1272 if target_has_largefiles != source_has_largefiles:
1273 merge_possible = False
1273 merge_possible = False
1274 if source_has_largefiles:
1274 if source_has_largefiles:
1275 message = _(
1275 message = _(
1276 'Target repository large files support is disabled.')
1276 'Target repository large files support is disabled.')
1277 else:
1277 else:
1278 message = _(
1278 message = _(
1279 'Source repository large files support is disabled.')
1279 'Source repository large files support is disabled.')
1280
1280
1281 return merge_possible, message
1281 return merge_possible, message
1282
1282
1283 def _has_largefiles(self, repo):
1283 def _has_largefiles(self, repo):
1284 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1284 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1285 'extensions', 'largefiles')
1285 'extensions', 'largefiles')
1286 return largefiles_ui and largefiles_ui[0].active
1286 return largefiles_ui and largefiles_ui[0].active
1287
1287
1288 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1288 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1289 """
1289 """
1290 Try to merge the pull request and return the merge status.
1290 Try to merge the pull request and return the merge status.
1291 """
1291 """
1292 log.debug(
1292 log.debug(
1293 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1293 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1294 pull_request.pull_request_id, force_shadow_repo_refresh)
1294 pull_request.pull_request_id, force_shadow_repo_refresh)
1295 target_vcs = pull_request.target_repo.scm_instance()
1295 target_vcs = pull_request.target_repo.scm_instance()
1296 # Refresh the target reference.
1296 # Refresh the target reference.
1297 try:
1297 try:
1298 target_ref = self._refresh_reference(
1298 target_ref = self._refresh_reference(
1299 pull_request.target_ref_parts, target_vcs)
1299 pull_request.target_ref_parts, target_vcs)
1300 except CommitDoesNotExistError:
1300 except CommitDoesNotExistError:
1301 merge_state = MergeResponse(
1301 merge_state = MergeResponse(
1302 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1302 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1303 metadata={'target_ref': pull_request.target_ref_parts})
1303 metadata={'target_ref': pull_request.target_ref_parts})
1304 return merge_state
1304 return merge_state
1305
1305
1306 target_locked = pull_request.target_repo.locked
1306 target_locked = pull_request.target_repo.locked
1307 if target_locked and target_locked[0]:
1307 if target_locked and target_locked[0]:
1308 locked_by = 'user:{}'.format(target_locked[0])
1308 locked_by = 'user:{}'.format(target_locked[0])
1309 log.debug("The target repository is locked by %s.", locked_by)
1309 log.debug("The target repository is locked by %s.", locked_by)
1310 merge_state = MergeResponse(
1310 merge_state = MergeResponse(
1311 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1311 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1312 metadata={'locked_by': locked_by})
1312 metadata={'locked_by': locked_by})
1313 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1313 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1314 pull_request, target_ref):
1314 pull_request, target_ref):
1315 log.debug("Refreshing the merge status of the repository.")
1315 log.debug("Refreshing the merge status of the repository.")
1316 merge_state = self._refresh_merge_state(
1316 merge_state = self._refresh_merge_state(
1317 pull_request, target_vcs, target_ref)
1317 pull_request, target_vcs, target_ref)
1318 else:
1318 else:
1319 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1319 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1320 metadata = {
1320 metadata = {
1321 'target_ref': pull_request.target_ref_parts,
1321 'target_ref': pull_request.target_ref_parts,
1322 'source_ref': pull_request.source_ref_parts,
1322 'source_ref': pull_request.source_ref_parts,
1323 }
1323 }
1324 if not possible and target_ref.type == 'branch':
1324 if not possible and target_ref.type == 'branch':
1325 # NOTE(marcink): case for mercurial multiple heads on branch
1325 # NOTE(marcink): case for mercurial multiple heads on branch
1326 heads = target_vcs._heads(target_ref.name)
1326 heads = target_vcs._heads(target_ref.name)
1327 if len(heads) != 1:
1327 if len(heads) != 1:
1328 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1328 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1329 metadata.update({
1329 metadata.update({
1330 'heads': heads
1330 'heads': heads
1331 })
1331 })
1332 merge_state = MergeResponse(
1332 merge_state = MergeResponse(
1333 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1333 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1334
1334
1335 return merge_state
1335 return merge_state
1336
1336
1337 def _refresh_reference(self, reference, vcs_repository):
1337 def _refresh_reference(self, reference, vcs_repository):
1338 if reference.type in self.UPDATABLE_REF_TYPES:
1338 if reference.type in self.UPDATABLE_REF_TYPES:
1339 name_or_id = reference.name
1339 name_or_id = reference.name
1340 else:
1340 else:
1341 name_or_id = reference.commit_id
1341 name_or_id = reference.commit_id
1342
1342
1343 refreshed_commit = vcs_repository.get_commit(name_or_id)
1343 refreshed_commit = vcs_repository.get_commit(name_or_id)
1344 refreshed_reference = Reference(
1344 refreshed_reference = Reference(
1345 reference.type, reference.name, refreshed_commit.raw_id)
1345 reference.type, reference.name, refreshed_commit.raw_id)
1346 return refreshed_reference
1346 return refreshed_reference
1347
1347
1348 def _needs_merge_state_refresh(self, pull_request, target_reference):
1348 def _needs_merge_state_refresh(self, pull_request, target_reference):
1349 return not(
1349 return not(
1350 pull_request.revisions and
1350 pull_request.revisions and
1351 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1351 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1352 target_reference.commit_id == pull_request._last_merge_target_rev)
1352 target_reference.commit_id == pull_request._last_merge_target_rev)
1353
1353
1354 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1354 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1355 workspace_id = self._workspace_id(pull_request)
1355 workspace_id = self._workspace_id(pull_request)
1356 source_vcs = pull_request.source_repo.scm_instance()
1356 source_vcs = pull_request.source_repo.scm_instance()
1357 repo_id = pull_request.target_repo.repo_id
1357 repo_id = pull_request.target_repo.repo_id
1358 use_rebase = self._use_rebase_for_merging(pull_request)
1358 use_rebase = self._use_rebase_for_merging(pull_request)
1359 close_branch = self._close_branch_before_merging(pull_request)
1359 close_branch = self._close_branch_before_merging(pull_request)
1360 merge_state = target_vcs.merge(
1360 merge_state = target_vcs.merge(
1361 repo_id, workspace_id,
1361 repo_id, workspace_id,
1362 target_reference, source_vcs, pull_request.source_ref_parts,
1362 target_reference, source_vcs, pull_request.source_ref_parts,
1363 dry_run=True, use_rebase=use_rebase,
1363 dry_run=True, use_rebase=use_rebase,
1364 close_branch=close_branch)
1364 close_branch=close_branch)
1365
1365
1366 # Do not store the response if there was an unknown error.
1366 # Do not store the response if there was an unknown error.
1367 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1367 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1368 pull_request._last_merge_source_rev = \
1368 pull_request._last_merge_source_rev = \
1369 pull_request.source_ref_parts.commit_id
1369 pull_request.source_ref_parts.commit_id
1370 pull_request._last_merge_target_rev = target_reference.commit_id
1370 pull_request._last_merge_target_rev = target_reference.commit_id
1371 pull_request.last_merge_status = merge_state.failure_reason
1371 pull_request.last_merge_status = merge_state.failure_reason
1372 pull_request.shadow_merge_ref = merge_state.merge_ref
1372 pull_request.shadow_merge_ref = merge_state.merge_ref
1373 Session().add(pull_request)
1373 Session().add(pull_request)
1374 Session().commit()
1374 Session().commit()
1375
1375
1376 return merge_state
1376 return merge_state
1377
1377
1378 def _workspace_id(self, pull_request):
1378 def _workspace_id(self, pull_request):
1379 workspace_id = 'pr-%s' % pull_request.pull_request_id
1379 workspace_id = 'pr-%s' % pull_request.pull_request_id
1380 return workspace_id
1380 return workspace_id
1381
1381
1382 def generate_repo_data(self, repo, commit_id=None, branch=None,
1382 def generate_repo_data(self, repo, commit_id=None, branch=None,
1383 bookmark=None, translator=None):
1383 bookmark=None, translator=None):
1384 from rhodecode.model.repo import RepoModel
1384 from rhodecode.model.repo import RepoModel
1385
1385
1386 all_refs, selected_ref = \
1386 all_refs, selected_ref = \
1387 self._get_repo_pullrequest_sources(
1387 self._get_repo_pullrequest_sources(
1388 repo.scm_instance(), commit_id=commit_id,
1388 repo.scm_instance(), commit_id=commit_id,
1389 branch=branch, bookmark=bookmark, translator=translator)
1389 branch=branch, bookmark=bookmark, translator=translator)
1390
1390
1391 refs_select2 = []
1391 refs_select2 = []
1392 for element in all_refs:
1392 for element in all_refs:
1393 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1393 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1394 refs_select2.append({'text': element[1], 'children': children})
1394 refs_select2.append({'text': element[1], 'children': children})
1395
1395
1396 return {
1396 return {
1397 'user': {
1397 'user': {
1398 'user_id': repo.user.user_id,
1398 'user_id': repo.user.user_id,
1399 'username': repo.user.username,
1399 'username': repo.user.username,
1400 'firstname': repo.user.first_name,
1400 'firstname': repo.user.first_name,
1401 'lastname': repo.user.last_name,
1401 'lastname': repo.user.last_name,
1402 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1402 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1403 },
1403 },
1404 'name': repo.repo_name,
1404 'name': repo.repo_name,
1405 'link': RepoModel().get_url(repo),
1405 'link': RepoModel().get_url(repo),
1406 'description': h.chop_at_smart(repo.description_safe, '\n'),
1406 'description': h.chop_at_smart(repo.description_safe, '\n'),
1407 'refs': {
1407 'refs': {
1408 'all_refs': all_refs,
1408 'all_refs': all_refs,
1409 'selected_ref': selected_ref,
1409 'selected_ref': selected_ref,
1410 'select2_refs': refs_select2
1410 'select2_refs': refs_select2
1411 }
1411 }
1412 }
1412 }
1413
1413
1414 def generate_pullrequest_title(self, source, source_ref, target):
1414 def generate_pullrequest_title(self, source, source_ref, target):
1415 return u'{source}#{at_ref} to {target}'.format(
1415 return u'{source}#{at_ref} to {target}'.format(
1416 source=source,
1416 source=source,
1417 at_ref=source_ref,
1417 at_ref=source_ref,
1418 target=target,
1418 target=target,
1419 )
1419 )
1420
1420
1421 def _cleanup_merge_workspace(self, pull_request):
1421 def _cleanup_merge_workspace(self, pull_request):
1422 # Merging related cleanup
1422 # Merging related cleanup
1423 repo_id = pull_request.target_repo.repo_id
1423 repo_id = pull_request.target_repo.repo_id
1424 target_scm = pull_request.target_repo.scm_instance()
1424 target_scm = pull_request.target_repo.scm_instance()
1425 workspace_id = self._workspace_id(pull_request)
1425 workspace_id = self._workspace_id(pull_request)
1426
1426
1427 try:
1427 try:
1428 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1428 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1429 except NotImplementedError:
1429 except NotImplementedError:
1430 pass
1430 pass
1431
1431
1432 def _get_repo_pullrequest_sources(
1432 def _get_repo_pullrequest_sources(
1433 self, repo, commit_id=None, branch=None, bookmark=None,
1433 self, repo, commit_id=None, branch=None, bookmark=None,
1434 translator=None):
1434 translator=None):
1435 """
1435 """
1436 Return a structure with repo's interesting commits, suitable for
1436 Return a structure with repo's interesting commits, suitable for
1437 the selectors in pullrequest controller
1437 the selectors in pullrequest controller
1438
1438
1439 :param commit_id: a commit that must be in the list somehow
1439 :param commit_id: a commit that must be in the list somehow
1440 and selected by default
1440 and selected by default
1441 :param branch: a branch that must be in the list and selected
1441 :param branch: a branch that must be in the list and selected
1442 by default - even if closed
1442 by default - even if closed
1443 :param bookmark: a bookmark that must be in the list and selected
1443 :param bookmark: a bookmark that must be in the list and selected
1444 """
1444 """
1445 _ = translator or get_current_request().translate
1445 _ = translator or get_current_request().translate
1446
1446
1447 commit_id = safe_str(commit_id) if commit_id else None
1447 commit_id = safe_str(commit_id) if commit_id else None
1448 branch = safe_unicode(branch) if branch else None
1448 branch = safe_unicode(branch) if branch else None
1449 bookmark = safe_unicode(bookmark) if bookmark else None
1449 bookmark = safe_unicode(bookmark) if bookmark else None
1450
1450
1451 selected = None
1451 selected = None
1452
1452
1453 # order matters: first source that has commit_id in it will be selected
1453 # order matters: first source that has commit_id in it will be selected
1454 sources = []
1454 sources = []
1455 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1455 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1456 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1456 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1457
1457
1458 if commit_id:
1458 if commit_id:
1459 ref_commit = (h.short_id(commit_id), commit_id)
1459 ref_commit = (h.short_id(commit_id), commit_id)
1460 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1460 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1461
1461
1462 sources.append(
1462 sources.append(
1463 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1463 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1464 )
1464 )
1465
1465
1466 groups = []
1466 groups = []
1467
1467
1468 for group_key, ref_list, group_name, match in sources:
1468 for group_key, ref_list, group_name, match in sources:
1469 group_refs = []
1469 group_refs = []
1470 for ref_name, ref_id in ref_list:
1470 for ref_name, ref_id in ref_list:
1471 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1471 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1472 group_refs.append((ref_key, ref_name))
1472 group_refs.append((ref_key, ref_name))
1473
1473
1474 if not selected:
1474 if not selected:
1475 if set([commit_id, match]) & set([ref_id, ref_name]):
1475 if set([commit_id, match]) & set([ref_id, ref_name]):
1476 selected = ref_key
1476 selected = ref_key
1477
1477
1478 if group_refs:
1478 if group_refs:
1479 groups.append((group_refs, group_name))
1479 groups.append((group_refs, group_name))
1480
1480
1481 if not selected:
1481 if not selected:
1482 ref = commit_id or branch or bookmark
1482 ref = commit_id or branch or bookmark
1483 if ref:
1483 if ref:
1484 raise CommitDoesNotExistError(
1484 raise CommitDoesNotExistError(
1485 u'No commit refs could be found matching: {}'.format(ref))
1485 u'No commit refs could be found matching: {}'.format(ref))
1486 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1486 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1487 selected = u'branch:{}:{}'.format(
1487 selected = u'branch:{}:{}'.format(
1488 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1488 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1489 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1489 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1490 )
1490 )
1491 elif repo.commit_ids:
1491 elif repo.commit_ids:
1492 # make the user select in this case
1492 # make the user select in this case
1493 selected = None
1493 selected = None
1494 else:
1494 else:
1495 raise EmptyRepositoryError()
1495 raise EmptyRepositoryError()
1496 return groups, selected
1496 return groups, selected
1497
1497
1498 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1498 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1499 hide_whitespace_changes, diff_context):
1499 hide_whitespace_changes, diff_context):
1500
1500
1501 return self._get_diff_from_pr_or_version(
1501 return self._get_diff_from_pr_or_version(
1502 source_repo, source_ref_id, target_ref_id,
1502 source_repo, source_ref_id, target_ref_id,
1503 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1503 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1504
1504
1505 def _get_diff_from_pr_or_version(
1505 def _get_diff_from_pr_or_version(
1506 self, source_repo, source_ref_id, target_ref_id,
1506 self, source_repo, source_ref_id, target_ref_id,
1507 hide_whitespace_changes, diff_context):
1507 hide_whitespace_changes, diff_context):
1508
1508
1509 target_commit = source_repo.get_commit(
1509 target_commit = source_repo.get_commit(
1510 commit_id=safe_str(target_ref_id))
1510 commit_id=safe_str(target_ref_id))
1511 source_commit = source_repo.get_commit(
1511 source_commit = source_repo.get_commit(
1512 commit_id=safe_str(source_ref_id))
1512 commit_id=safe_str(source_ref_id))
1513 if isinstance(source_repo, Repository):
1513 if isinstance(source_repo, Repository):
1514 vcs_repo = source_repo.scm_instance()
1514 vcs_repo = source_repo.scm_instance()
1515 else:
1515 else:
1516 vcs_repo = source_repo
1516 vcs_repo = source_repo
1517
1517
1518 # TODO: johbo: In the context of an update, we cannot reach
1518 # TODO: johbo: In the context of an update, we cannot reach
1519 # the old commit anymore with our normal mechanisms. It needs
1519 # the old commit anymore with our normal mechanisms. It needs
1520 # some sort of special support in the vcs layer to avoid this
1520 # some sort of special support in the vcs layer to avoid this
1521 # workaround.
1521 # workaround.
1522 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1522 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1523 vcs_repo.alias == 'git'):
1523 vcs_repo.alias == 'git'):
1524 source_commit.raw_id = safe_str(source_ref_id)
1524 source_commit.raw_id = safe_str(source_ref_id)
1525
1525
1526 log.debug('calculating diff between '
1526 log.debug('calculating diff between '
1527 'source_ref:%s and target_ref:%s for repo `%s`',
1527 'source_ref:%s and target_ref:%s for repo `%s`',
1528 target_ref_id, source_ref_id,
1528 target_ref_id, source_ref_id,
1529 safe_unicode(vcs_repo.path))
1529 safe_unicode(vcs_repo.path))
1530
1530
1531 vcs_diff = vcs_repo.get_diff(
1531 vcs_diff = vcs_repo.get_diff(
1532 commit1=target_commit, commit2=source_commit,
1532 commit1=target_commit, commit2=source_commit,
1533 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1533 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1534 return vcs_diff
1534 return vcs_diff
1535
1535
1536 def _is_merge_enabled(self, pull_request):
1536 def _is_merge_enabled(self, pull_request):
1537 return self._get_general_setting(
1537 return self._get_general_setting(
1538 pull_request, 'rhodecode_pr_merge_enabled')
1538 pull_request, 'rhodecode_pr_merge_enabled')
1539
1539
1540 def _use_rebase_for_merging(self, pull_request):
1540 def _use_rebase_for_merging(self, pull_request):
1541 repo_type = pull_request.target_repo.repo_type
1541 repo_type = pull_request.target_repo.repo_type
1542 if repo_type == 'hg':
1542 if repo_type == 'hg':
1543 return self._get_general_setting(
1543 return self._get_general_setting(
1544 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1544 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1545 elif repo_type == 'git':
1545 elif repo_type == 'git':
1546 return self._get_general_setting(
1546 return self._get_general_setting(
1547 pull_request, 'rhodecode_git_use_rebase_for_merging')
1547 pull_request, 'rhodecode_git_use_rebase_for_merging')
1548
1548
1549 return False
1549 return False
1550
1550
1551 def _close_branch_before_merging(self, pull_request):
1551 def _close_branch_before_merging(self, pull_request):
1552 repo_type = pull_request.target_repo.repo_type
1552 repo_type = pull_request.target_repo.repo_type
1553 if repo_type == 'hg':
1553 if repo_type == 'hg':
1554 return self._get_general_setting(
1554 return self._get_general_setting(
1555 pull_request, 'rhodecode_hg_close_branch_before_merging')
1555 pull_request, 'rhodecode_hg_close_branch_before_merging')
1556 elif repo_type == 'git':
1556 elif repo_type == 'git':
1557 return self._get_general_setting(
1557 return self._get_general_setting(
1558 pull_request, 'rhodecode_git_close_branch_before_merging')
1558 pull_request, 'rhodecode_git_close_branch_before_merging')
1559
1559
1560 return False
1560 return False
1561
1561
1562 def _get_general_setting(self, pull_request, settings_key, default=False):
1562 def _get_general_setting(self, pull_request, settings_key, default=False):
1563 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1563 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1564 settings = settings_model.get_general_settings()
1564 settings = settings_model.get_general_settings()
1565 return settings.get(settings_key, default)
1565 return settings.get(settings_key, default)
1566
1566
1567 def _log_audit_action(self, action, action_data, user, pull_request):
1567 def _log_audit_action(self, action, action_data, user, pull_request):
1568 audit_logger.store(
1568 audit_logger.store(
1569 action=action,
1569 action=action,
1570 action_data=action_data,
1570 action_data=action_data,
1571 user=user,
1571 user=user,
1572 repo=pull_request.target_repo)
1572 repo=pull_request.target_repo)
1573
1573
1574 def get_reviewer_functions(self):
1574 def get_reviewer_functions(self):
1575 """
1575 """
1576 Fetches functions for validation and fetching default reviewers.
1576 Fetches functions for validation and fetching default reviewers.
1577 If available we use the EE package, else we fallback to CE
1577 If available we use the EE package, else we fallback to CE
1578 package functions
1578 package functions
1579 """
1579 """
1580 try:
1580 try:
1581 from rc_reviewers.utils import get_default_reviewers_data
1581 from rc_reviewers.utils import get_default_reviewers_data
1582 from rc_reviewers.utils import validate_default_reviewers
1582 from rc_reviewers.utils import validate_default_reviewers
1583 except ImportError:
1583 except ImportError:
1584 from rhodecode.apps.repository.utils import get_default_reviewers_data
1584 from rhodecode.apps.repository.utils import get_default_reviewers_data
1585 from rhodecode.apps.repository.utils import validate_default_reviewers
1585 from rhodecode.apps.repository.utils import validate_default_reviewers
1586
1586
1587 return get_default_reviewers_data, validate_default_reviewers
1587 return get_default_reviewers_data, validate_default_reviewers
1588
1588
1589
1589
1590 class MergeCheck(object):
1590 class MergeCheck(object):
1591 """
1591 """
1592 Perform Merge Checks and returns a check object which stores information
1592 Perform Merge Checks and returns a check object which stores information
1593 about merge errors, and merge conditions
1593 about merge errors, and merge conditions
1594 """
1594 """
1595 TODO_CHECK = 'todo'
1595 TODO_CHECK = 'todo'
1596 PERM_CHECK = 'perm'
1596 PERM_CHECK = 'perm'
1597 REVIEW_CHECK = 'review'
1597 REVIEW_CHECK = 'review'
1598 MERGE_CHECK = 'merge'
1598 MERGE_CHECK = 'merge'
1599
1599
1600 def __init__(self):
1600 def __init__(self):
1601 self.review_status = None
1601 self.review_status = None
1602 self.merge_possible = None
1602 self.merge_possible = None
1603 self.merge_msg = ''
1603 self.merge_msg = ''
1604 self.failed = None
1604 self.failed = None
1605 self.errors = []
1605 self.errors = []
1606 self.error_details = OrderedDict()
1606 self.error_details = OrderedDict()
1607
1607
1608 def push_error(self, error_type, message, error_key, details):
1608 def push_error(self, error_type, message, error_key, details):
1609 self.failed = True
1609 self.failed = True
1610 self.errors.append([error_type, message])
1610 self.errors.append([error_type, message])
1611 self.error_details[error_key] = dict(
1611 self.error_details[error_key] = dict(
1612 details=details,
1612 details=details,
1613 error_type=error_type,
1613 error_type=error_type,
1614 message=message
1614 message=message
1615 )
1615 )
1616
1616
1617 @classmethod
1617 @classmethod
1618 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1618 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1619 force_shadow_repo_refresh=False):
1619 force_shadow_repo_refresh=False):
1620 _ = translator
1620 _ = translator
1621 merge_check = cls()
1621 merge_check = cls()
1622
1622
1623 # permissions to merge
1623 # permissions to merge
1624 user_allowed_to_merge = PullRequestModel().check_user_merge(
1624 user_allowed_to_merge = PullRequestModel().check_user_merge(
1625 pull_request, auth_user)
1625 pull_request, auth_user)
1626 if not user_allowed_to_merge:
1626 if not user_allowed_to_merge:
1627 log.debug("MergeCheck: cannot merge, approval is pending.")
1627 log.debug("MergeCheck: cannot merge, approval is pending.")
1628
1628
1629 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1629 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1630 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1630 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1631 if fail_early:
1631 if fail_early:
1632 return merge_check
1632 return merge_check
1633
1633
1634 # permission to merge into the target branch
1634 # permission to merge into the target branch
1635 target_commit_id = pull_request.target_ref_parts.commit_id
1635 target_commit_id = pull_request.target_ref_parts.commit_id
1636 if pull_request.target_ref_parts.type == 'branch':
1636 if pull_request.target_ref_parts.type == 'branch':
1637 branch_name = pull_request.target_ref_parts.name
1637 branch_name = pull_request.target_ref_parts.name
1638 else:
1638 else:
1639 # for mercurial we can always figure out the branch from the commit
1639 # for mercurial we can always figure out the branch from the commit
1640 # in case of bookmark
1640 # in case of bookmark
1641 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1641 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1642 branch_name = target_commit.branch
1642 branch_name = target_commit.branch
1643
1643
1644 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1644 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1645 pull_request.target_repo.repo_name, branch_name)
1645 pull_request.target_repo.repo_name, branch_name)
1646 if branch_perm and branch_perm == 'branch.none':
1646 if branch_perm and branch_perm == 'branch.none':
1647 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1647 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1648 branch_name, rule)
1648 branch_name, rule)
1649 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1649 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1650 if fail_early:
1650 if fail_early:
1651 return merge_check
1651 return merge_check
1652
1652
1653 # review status, must be always present
1653 # review status, must be always present
1654 review_status = pull_request.calculated_review_status()
1654 review_status = pull_request.calculated_review_status()
1655 merge_check.review_status = review_status
1655 merge_check.review_status = review_status
1656
1656
1657 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1657 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1658 if not status_approved:
1658 if not status_approved:
1659 log.debug("MergeCheck: cannot merge, approval is pending.")
1659 log.debug("MergeCheck: cannot merge, approval is pending.")
1660
1660
1661 msg = _('Pull request reviewer approval is pending.')
1661 msg = _('Pull request reviewer approval is pending.')
1662
1662
1663 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1663 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1664
1664
1665 if fail_early:
1665 if fail_early:
1666 return merge_check
1666 return merge_check
1667
1667
1668 # left over TODOs
1668 # left over TODOs
1669 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1669 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1670 if todos:
1670 if todos:
1671 log.debug("MergeCheck: cannot merge, {} "
1671 log.debug("MergeCheck: cannot merge, {} "
1672 "unresolved TODOs left.".format(len(todos)))
1672 "unresolved TODOs left.".format(len(todos)))
1673
1673
1674 if len(todos) == 1:
1674 if len(todos) == 1:
1675 msg = _('Cannot merge, {} TODO still not resolved.').format(
1675 msg = _('Cannot merge, {} TODO still not resolved.').format(
1676 len(todos))
1676 len(todos))
1677 else:
1677 else:
1678 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1678 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1679 len(todos))
1679 len(todos))
1680
1680
1681 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1681 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1682
1682
1683 if fail_early:
1683 if fail_early:
1684 return merge_check
1684 return merge_check
1685
1685
1686 # merge possible, here is the filesystem simulation + shadow repo
1686 # merge possible, here is the filesystem simulation + shadow repo
1687 merge_status, msg = PullRequestModel().merge_status(
1687 merge_status, msg = PullRequestModel().merge_status(
1688 pull_request, translator=translator,
1688 pull_request, translator=translator,
1689 force_shadow_repo_refresh=force_shadow_repo_refresh)
1689 force_shadow_repo_refresh=force_shadow_repo_refresh)
1690 merge_check.merge_possible = merge_status
1690 merge_check.merge_possible = merge_status
1691 merge_check.merge_msg = msg
1691 merge_check.merge_msg = msg
1692 if not merge_status:
1692 if not merge_status:
1693 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1693 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1694 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1694 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1695
1695
1696 if fail_early:
1696 if fail_early:
1697 return merge_check
1697 return merge_check
1698
1698
1699 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1699 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1700 return merge_check
1700 return merge_check
1701
1701
1702 @classmethod
1702 @classmethod
1703 def get_merge_conditions(cls, pull_request, translator):
1703 def get_merge_conditions(cls, pull_request, translator):
1704 _ = translator
1704 _ = translator
1705 merge_details = {}
1705 merge_details = {}
1706
1706
1707 model = PullRequestModel()
1707 model = PullRequestModel()
1708 use_rebase = model._use_rebase_for_merging(pull_request)
1708 use_rebase = model._use_rebase_for_merging(pull_request)
1709
1709
1710 if use_rebase:
1710 if use_rebase:
1711 merge_details['merge_strategy'] = dict(
1711 merge_details['merge_strategy'] = dict(
1712 details={},
1712 details={},
1713 message=_('Merge strategy: rebase')
1713 message=_('Merge strategy: rebase')
1714 )
1714 )
1715 else:
1715 else:
1716 merge_details['merge_strategy'] = dict(
1716 merge_details['merge_strategy'] = dict(
1717 details={},
1717 details={},
1718 message=_('Merge strategy: explicit merge commit')
1718 message=_('Merge strategy: explicit merge commit')
1719 )
1719 )
1720
1720
1721 close_branch = model._close_branch_before_merging(pull_request)
1721 close_branch = model._close_branch_before_merging(pull_request)
1722 if close_branch:
1722 if close_branch:
1723 repo_type = pull_request.target_repo.repo_type
1723 repo_type = pull_request.target_repo.repo_type
1724 close_msg = ''
1724 close_msg = ''
1725 if repo_type == 'hg':
1725 if repo_type == 'hg':
1726 close_msg = _('Source branch will be closed after merge.')
1726 close_msg = _('Source branch will be closed after merge.')
1727 elif repo_type == 'git':
1727 elif repo_type == 'git':
1728 close_msg = _('Source branch will be deleted after merge.')
1728 close_msg = _('Source branch will be deleted after merge.')
1729
1729
1730 merge_details['close_branch'] = dict(
1730 merge_details['close_branch'] = dict(
1731 details={},
1731 details={},
1732 message=close_msg
1732 message=close_msg
1733 )
1733 )
1734
1734
1735 return merge_details
1735 return merge_details
1736
1736
1737
1737
1738 ChangeTuple = collections.namedtuple(
1738 ChangeTuple = collections.namedtuple(
1739 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1739 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1740
1740
1741 FileChangeTuple = collections.namedtuple(
1741 FileChangeTuple = collections.namedtuple(
1742 'FileChangeTuple', ['added', 'modified', 'removed'])
1742 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,2796 +1,2804 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'variables';
8 @import 'variables';
9 @import 'bootstrap-variables';
9 @import 'bootstrap-variables';
10 @import 'form-bootstrap';
10 @import 'form-bootstrap';
11 @import 'codemirror';
11 @import 'codemirror';
12 @import 'legacy_code_styles';
12 @import 'legacy_code_styles';
13 @import 'readme-box';
13 @import 'readme-box';
14 @import 'progress-bar';
14 @import 'progress-bar';
15
15
16 @import 'type';
16 @import 'type';
17 @import 'alerts';
17 @import 'alerts';
18 @import 'buttons';
18 @import 'buttons';
19 @import 'tags';
19 @import 'tags';
20 @import 'code-block';
20 @import 'code-block';
21 @import 'examples';
21 @import 'examples';
22 @import 'login';
22 @import 'login';
23 @import 'main-content';
23 @import 'main-content';
24 @import 'select2';
24 @import 'select2';
25 @import 'comments';
25 @import 'comments';
26 @import 'panels-bootstrap';
26 @import 'panels-bootstrap';
27 @import 'panels';
27 @import 'panels';
28 @import 'deform';
28 @import 'deform';
29
29
30 //--- BASE ------------------//
30 //--- BASE ------------------//
31 .noscript-error {
31 .noscript-error {
32 top: 0;
32 top: 0;
33 left: 0;
33 left: 0;
34 width: 100%;
34 width: 100%;
35 z-index: 101;
35 z-index: 101;
36 text-align: center;
36 text-align: center;
37 font-size: 120%;
37 font-size: 120%;
38 color: white;
38 color: white;
39 background-color: @alert2;
39 background-color: @alert2;
40 padding: 5px 0 5px 0;
40 padding: 5px 0 5px 0;
41 font-weight: @text-semibold-weight;
41 font-weight: @text-semibold-weight;
42 font-family: @text-semibold;
42 font-family: @text-semibold;
43 }
43 }
44
44
45 html {
45 html {
46 display: table;
46 display: table;
47 height: 100%;
47 height: 100%;
48 width: 100%;
48 width: 100%;
49 }
49 }
50
50
51 body {
51 body {
52 display: table-cell;
52 display: table-cell;
53 width: 100%;
53 width: 100%;
54 }
54 }
55
55
56 //--- LAYOUT ------------------//
56 //--- LAYOUT ------------------//
57
57
58 .hidden{
58 .hidden{
59 display: none !important;
59 display: none !important;
60 }
60 }
61
61
62 .box{
62 .box{
63 float: left;
63 float: left;
64 width: 100%;
64 width: 100%;
65 }
65 }
66
66
67 .browser-header {
67 .browser-header {
68 clear: both;
68 clear: both;
69 }
69 }
70 .main {
70 .main {
71 clear: both;
71 clear: both;
72 padding:0 0 @pagepadding;
72 padding:0 0 @pagepadding;
73 height: auto;
73 height: auto;
74
74
75 &:after { //clearfix
75 &:after { //clearfix
76 content:"";
76 content:"";
77 clear:both;
77 clear:both;
78 width:100%;
78 width:100%;
79 display:block;
79 display:block;
80 }
80 }
81 }
81 }
82
82
83 .action-link{
83 .action-link{
84 margin-left: @padding;
84 margin-left: @padding;
85 padding-left: @padding;
85 padding-left: @padding;
86 border-left: @border-thickness solid @border-default-color;
86 border-left: @border-thickness solid @border-default-color;
87 }
87 }
88
88
89 input + .action-link, .action-link.first{
89 input + .action-link, .action-link.first{
90 border-left: none;
90 border-left: none;
91 }
91 }
92
92
93 .action-link.last{
93 .action-link.last{
94 margin-right: @padding;
94 margin-right: @padding;
95 padding-right: @padding;
95 padding-right: @padding;
96 }
96 }
97
97
98 .action-link.active,
98 .action-link.active,
99 .action-link.active a{
99 .action-link.active a{
100 color: @grey4;
100 color: @grey4;
101 }
101 }
102
102
103 .action-link.disabled {
103 .action-link.disabled {
104 color: @grey4;
104 color: @grey4;
105 cursor: inherit;
105 cursor: inherit;
106 }
106 }
107
107
108 .clipboard-action {
108 .clipboard-action {
109 cursor: pointer;
109 cursor: pointer;
110 color: @grey4;
110 color: @grey4;
111 margin-left: 5px;
111 margin-left: 5px;
112
112
113 &:hover {
113 &:hover {
114 color: @grey2;
114 color: @grey2;
115 }
115 }
116 }
116 }
117
117
118 ul.simple-list{
118 ul.simple-list{
119 list-style: none;
119 list-style: none;
120 margin: 0;
120 margin: 0;
121 padding: 0;
121 padding: 0;
122 }
122 }
123
123
124 .main-content {
124 .main-content {
125 padding-bottom: @pagepadding;
125 padding-bottom: @pagepadding;
126 }
126 }
127
127
128 .wide-mode-wrapper {
128 .wide-mode-wrapper {
129 max-width:4000px !important;
129 max-width:4000px !important;
130 }
130 }
131
131
132 .wrapper {
132 .wrapper {
133 position: relative;
133 position: relative;
134 max-width: @wrapper-maxwidth;
134 max-width: @wrapper-maxwidth;
135 margin: 0 auto;
135 margin: 0 auto;
136 }
136 }
137
137
138 #content {
138 #content {
139 clear: both;
139 clear: both;
140 padding: 0 @contentpadding;
140 padding: 0 @contentpadding;
141 }
141 }
142
142
143 .advanced-settings-fields{
143 .advanced-settings-fields{
144 input{
144 input{
145 margin-left: @textmargin;
145 margin-left: @textmargin;
146 margin-right: @padding/2;
146 margin-right: @padding/2;
147 }
147 }
148 }
148 }
149
149
150 .cs_files_title {
150 .cs_files_title {
151 margin: @pagepadding 0 0;
151 margin: @pagepadding 0 0;
152 }
152 }
153
153
154 input.inline[type="file"] {
154 input.inline[type="file"] {
155 display: inline;
155 display: inline;
156 }
156 }
157
157
158 .error_page {
158 .error_page {
159 margin: 10% auto;
159 margin: 10% auto;
160
160
161 h1 {
161 h1 {
162 color: @grey2;
162 color: @grey2;
163 }
163 }
164
164
165 .alert {
165 .alert {
166 margin: @padding 0;
166 margin: @padding 0;
167 }
167 }
168
168
169 .error-branding {
169 .error-branding {
170 color: @grey4;
170 color: @grey4;
171 font-weight: @text-semibold-weight;
171 font-weight: @text-semibold-weight;
172 font-family: @text-semibold;
172 font-family: @text-semibold;
173 }
173 }
174
174
175 .error_message {
175 .error_message {
176 font-family: @text-regular;
176 font-family: @text-regular;
177 }
177 }
178
178
179 .sidebar {
179 .sidebar {
180 min-height: 275px;
180 min-height: 275px;
181 margin: 0;
181 margin: 0;
182 padding: 0 0 @sidebarpadding @sidebarpadding;
182 padding: 0 0 @sidebarpadding @sidebarpadding;
183 border: none;
183 border: none;
184 }
184 }
185
185
186 .main-content {
186 .main-content {
187 position: relative;
187 position: relative;
188 margin: 0 @sidebarpadding @sidebarpadding;
188 margin: 0 @sidebarpadding @sidebarpadding;
189 padding: 0 0 0 @sidebarpadding;
189 padding: 0 0 0 @sidebarpadding;
190 border-left: @border-thickness solid @grey5;
190 border-left: @border-thickness solid @grey5;
191
191
192 @media (max-width:767px) {
192 @media (max-width:767px) {
193 clear: both;
193 clear: both;
194 width: 100%;
194 width: 100%;
195 margin: 0;
195 margin: 0;
196 border: none;
196 border: none;
197 }
197 }
198 }
198 }
199
199
200 .inner-column {
200 .inner-column {
201 float: left;
201 float: left;
202 width: 29.75%;
202 width: 29.75%;
203 min-height: 150px;
203 min-height: 150px;
204 margin: @sidebarpadding 2% 0 0;
204 margin: @sidebarpadding 2% 0 0;
205 padding: 0 2% 0 0;
205 padding: 0 2% 0 0;
206 border-right: @border-thickness solid @grey5;
206 border-right: @border-thickness solid @grey5;
207
207
208 @media (max-width:767px) {
208 @media (max-width:767px) {
209 clear: both;
209 clear: both;
210 width: 100%;
210 width: 100%;
211 border: none;
211 border: none;
212 }
212 }
213
213
214 ul {
214 ul {
215 padding-left: 1.25em;
215 padding-left: 1.25em;
216 }
216 }
217
217
218 &:last-child {
218 &:last-child {
219 margin: @sidebarpadding 0 0;
219 margin: @sidebarpadding 0 0;
220 border: none;
220 border: none;
221 }
221 }
222
222
223 h4 {
223 h4 {
224 margin: 0 0 @padding;
224 margin: 0 0 @padding;
225 font-weight: @text-semibold-weight;
225 font-weight: @text-semibold-weight;
226 font-family: @text-semibold;
226 font-family: @text-semibold;
227 }
227 }
228 }
228 }
229 }
229 }
230 .error-page-logo {
230 .error-page-logo {
231 width: 130px;
231 width: 130px;
232 height: 160px;
232 height: 160px;
233 }
233 }
234
234
235 // HEADER
235 // HEADER
236 .header {
236 .header {
237
237
238 // TODO: johbo: Fix login pages, so that they work without a min-height
238 // TODO: johbo: Fix login pages, so that they work without a min-height
239 // for the header and then remove the min-height. I chose a smaller value
239 // for the header and then remove the min-height. I chose a smaller value
240 // intentionally here to avoid rendering issues in the main navigation.
240 // intentionally here to avoid rendering issues in the main navigation.
241 min-height: 49px;
241 min-height: 49px;
242
242
243 position: relative;
243 position: relative;
244 vertical-align: bottom;
244 vertical-align: bottom;
245 padding: 0 @header-padding;
245 padding: 0 @header-padding;
246 background-color: @grey1;
246 background-color: @grey1;
247 color: @grey5;
247 color: @grey5;
248
248
249 .title {
249 .title {
250 overflow: visible;
250 overflow: visible;
251 }
251 }
252
252
253 &:before,
253 &:before,
254 &:after {
254 &:after {
255 content: "";
255 content: "";
256 clear: both;
256 clear: both;
257 width: 100%;
257 width: 100%;
258 }
258 }
259
259
260 // TODO: johbo: Avoids breaking "Repositories" chooser
260 // TODO: johbo: Avoids breaking "Repositories" chooser
261 .select2-container .select2-choice .select2-arrow {
261 .select2-container .select2-choice .select2-arrow {
262 display: none;
262 display: none;
263 }
263 }
264 }
264 }
265
265
266 #header-inner {
266 #header-inner {
267 &.title {
267 &.title {
268 margin: 0;
268 margin: 0;
269 }
269 }
270 &:before,
270 &:before,
271 &:after {
271 &:after {
272 content: "";
272 content: "";
273 clear: both;
273 clear: both;
274 }
274 }
275 }
275 }
276
276
277 // Gists
277 // Gists
278 #files_data {
278 #files_data {
279 clear: both; //for firefox
279 clear: both; //for firefox
280 padding-top: 10px;
280 padding-top: 10px;
281 }
281 }
282
282
283 #gistid {
283 #gistid {
284 margin-right: @padding;
284 margin-right: @padding;
285 }
285 }
286
286
287 // Global Settings Editor
287 // Global Settings Editor
288 .textarea.editor {
288 .textarea.editor {
289 float: left;
289 float: left;
290 position: relative;
290 position: relative;
291 max-width: @texteditor-width;
291 max-width: @texteditor-width;
292
292
293 select {
293 select {
294 position: absolute;
294 position: absolute;
295 top:10px;
295 top:10px;
296 right:0;
296 right:0;
297 }
297 }
298
298
299 .CodeMirror {
299 .CodeMirror {
300 margin: 0;
300 margin: 0;
301 }
301 }
302
302
303 .help-block {
303 .help-block {
304 margin: 0 0 @padding;
304 margin: 0 0 @padding;
305 padding:.5em;
305 padding:.5em;
306 background-color: @grey6;
306 background-color: @grey6;
307 &.pre-formatting {
307 &.pre-formatting {
308 white-space: pre;
308 white-space: pre;
309 }
309 }
310 }
310 }
311 }
311 }
312
312
313 ul.auth_plugins {
313 ul.auth_plugins {
314 margin: @padding 0 @padding @legend-width;
314 margin: @padding 0 @padding @legend-width;
315 padding: 0;
315 padding: 0;
316
316
317 li {
317 li {
318 margin-bottom: @padding;
318 margin-bottom: @padding;
319 line-height: 1em;
319 line-height: 1em;
320 list-style-type: none;
320 list-style-type: none;
321
321
322 .auth_buttons .btn {
322 .auth_buttons .btn {
323 margin-right: @padding;
323 margin-right: @padding;
324 }
324 }
325
325
326 }
326 }
327 }
327 }
328
328
329
329
330 // My Account PR list
330 // My Account PR list
331
331
332 #show_closed {
332 #show_closed {
333 margin: 0 1em 0 0;
333 margin: 0 1em 0 0;
334 }
334 }
335
335
336 .pullrequestlist {
336 #pull_request_list_table {
337 .closed {
337 .closed {
338 background-color: @grey6;
338 background-color: @grey6;
339 }
339 }
340
341 .state-creating,
342 .state-updating,
343 .state-merging
344 {
345 background-color: @grey6;
346 }
347
340 .td-status {
348 .td-status {
341 padding-left: .5em;
349 padding-left: .5em;
342 }
350 }
343 .log-container .truncate {
351 .log-container .truncate {
344 height: 2.75em;
352 height: 2.75em;
345 white-space: pre-line;
353 white-space: pre-line;
346 }
354 }
347 table.rctable .user {
355 table.rctable .user {
348 padding-left: 0;
356 padding-left: 0;
349 }
357 }
350 table.rctable {
358 table.rctable {
351 td.td-description,
359 td.td-description,
352 .rc-user {
360 .rc-user {
353 min-width: auto;
361 min-width: auto;
354 }
362 }
355 }
363 }
356 }
364 }
357
365
358 // Pull Requests
366 // Pull Requests
359
367
360 .pullrequests_section_head {
368 .pullrequests_section_head {
361 display: block;
369 display: block;
362 clear: both;
370 clear: both;
363 margin: @padding 0;
371 margin: @padding 0;
364 font-weight: @text-bold-weight;
372 font-weight: @text-bold-weight;
365 font-family: @text-bold;
373 font-family: @text-bold;
366 }
374 }
367
375
368 .pr-origininfo, .pr-targetinfo {
376 .pr-origininfo, .pr-targetinfo {
369 position: relative;
377 position: relative;
370
378
371 .tag {
379 .tag {
372 display: inline-block;
380 display: inline-block;
373 margin: 0 1em .5em 0;
381 margin: 0 1em .5em 0;
374 }
382 }
375
383
376 .clone-url {
384 .clone-url {
377 display: inline-block;
385 display: inline-block;
378 margin: 0 0 .5em 0;
386 margin: 0 0 .5em 0;
379 padding: 0;
387 padding: 0;
380 line-height: 1.2em;
388 line-height: 1.2em;
381 }
389 }
382 }
390 }
383
391
384 .pr-mergeinfo {
392 .pr-mergeinfo {
385 min-width: 95% !important;
393 min-width: 95% !important;
386 padding: 0 !important;
394 padding: 0 !important;
387 border: 0;
395 border: 0;
388 }
396 }
389 .pr-mergeinfo-copy {
397 .pr-mergeinfo-copy {
390 padding: 0 0;
398 padding: 0 0;
391 }
399 }
392
400
393 .pr-pullinfo {
401 .pr-pullinfo {
394 min-width: 95% !important;
402 min-width: 95% !important;
395 padding: 0 !important;
403 padding: 0 !important;
396 border: 0;
404 border: 0;
397 }
405 }
398 .pr-pullinfo-copy {
406 .pr-pullinfo-copy {
399 padding: 0 0;
407 padding: 0 0;
400 }
408 }
401
409
402
410
403 #pr-title-input {
411 #pr-title-input {
404 width: 72%;
412 width: 72%;
405 font-size: 1em;
413 font-size: 1em;
406 margin: 0;
414 margin: 0;
407 padding: 0 0 0 @padding/4;
415 padding: 0 0 0 @padding/4;
408 line-height: 1.7em;
416 line-height: 1.7em;
409 color: @text-color;
417 color: @text-color;
410 letter-spacing: .02em;
418 letter-spacing: .02em;
411 font-weight: @text-bold-weight;
419 font-weight: @text-bold-weight;
412 font-family: @text-bold;
420 font-family: @text-bold;
413 }
421 }
414
422
415 #pullrequest_title {
423 #pullrequest_title {
416 width: 100%;
424 width: 100%;
417 box-sizing: border-box;
425 box-sizing: border-box;
418 }
426 }
419
427
420 #pr_open_message {
428 #pr_open_message {
421 border: @border-thickness solid #fff;
429 border: @border-thickness solid #fff;
422 border-radius: @border-radius;
430 border-radius: @border-radius;
423 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
431 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
424 text-align: left;
432 text-align: left;
425 overflow: hidden;
433 overflow: hidden;
426 }
434 }
427
435
428 .pr-submit-button {
436 .pr-submit-button {
429 float: right;
437 float: right;
430 margin: 0 0 0 5px;
438 margin: 0 0 0 5px;
431 }
439 }
432
440
433 .pr-spacing-container {
441 .pr-spacing-container {
434 padding: 20px;
442 padding: 20px;
435 clear: both
443 clear: both
436 }
444 }
437
445
438 #pr-description-input {
446 #pr-description-input {
439 margin-bottom: 0;
447 margin-bottom: 0;
440 }
448 }
441
449
442 .pr-description-label {
450 .pr-description-label {
443 vertical-align: top;
451 vertical-align: top;
444 }
452 }
445
453
446 .perms_section_head {
454 .perms_section_head {
447 min-width: 625px;
455 min-width: 625px;
448
456
449 h2 {
457 h2 {
450 margin-bottom: 0;
458 margin-bottom: 0;
451 }
459 }
452
460
453 .label-checkbox {
461 .label-checkbox {
454 float: left;
462 float: left;
455 }
463 }
456
464
457 &.field {
465 &.field {
458 margin: @space 0 @padding;
466 margin: @space 0 @padding;
459 }
467 }
460
468
461 &:first-child.field {
469 &:first-child.field {
462 margin-top: 0;
470 margin-top: 0;
463
471
464 .label {
472 .label {
465 margin-top: 0;
473 margin-top: 0;
466 padding-top: 0;
474 padding-top: 0;
467 }
475 }
468
476
469 .radios {
477 .radios {
470 padding-top: 0;
478 padding-top: 0;
471 }
479 }
472 }
480 }
473
481
474 .radios {
482 .radios {
475 position: relative;
483 position: relative;
476 width: 505px;
484 width: 505px;
477 }
485 }
478 }
486 }
479
487
480 //--- MODULES ------------------//
488 //--- MODULES ------------------//
481
489
482
490
483 // Server Announcement
491 // Server Announcement
484 #server-announcement {
492 #server-announcement {
485 width: 95%;
493 width: 95%;
486 margin: @padding auto;
494 margin: @padding auto;
487 padding: @padding;
495 padding: @padding;
488 border-width: 2px;
496 border-width: 2px;
489 border-style: solid;
497 border-style: solid;
490 .border-radius(2px);
498 .border-radius(2px);
491 font-weight: @text-bold-weight;
499 font-weight: @text-bold-weight;
492 font-family: @text-bold;
500 font-family: @text-bold;
493
501
494 &.info { border-color: @alert4; background-color: @alert4-inner; }
502 &.info { border-color: @alert4; background-color: @alert4-inner; }
495 &.warning { border-color: @alert3; background-color: @alert3-inner; }
503 &.warning { border-color: @alert3; background-color: @alert3-inner; }
496 &.error { border-color: @alert2; background-color: @alert2-inner; }
504 &.error { border-color: @alert2; background-color: @alert2-inner; }
497 &.success { border-color: @alert1; background-color: @alert1-inner; }
505 &.success { border-color: @alert1; background-color: @alert1-inner; }
498 &.neutral { border-color: @grey3; background-color: @grey6; }
506 &.neutral { border-color: @grey3; background-color: @grey6; }
499 }
507 }
500
508
501 // Fixed Sidebar Column
509 // Fixed Sidebar Column
502 .sidebar-col-wrapper {
510 .sidebar-col-wrapper {
503 padding-left: @sidebar-all-width;
511 padding-left: @sidebar-all-width;
504
512
505 .sidebar {
513 .sidebar {
506 width: @sidebar-width;
514 width: @sidebar-width;
507 margin-left: -@sidebar-all-width;
515 margin-left: -@sidebar-all-width;
508 }
516 }
509 }
517 }
510
518
511 .sidebar-col-wrapper.scw-small {
519 .sidebar-col-wrapper.scw-small {
512 padding-left: @sidebar-small-all-width;
520 padding-left: @sidebar-small-all-width;
513
521
514 .sidebar {
522 .sidebar {
515 width: @sidebar-small-width;
523 width: @sidebar-small-width;
516 margin-left: -@sidebar-small-all-width;
524 margin-left: -@sidebar-small-all-width;
517 }
525 }
518 }
526 }
519
527
520
528
521 // FOOTER
529 // FOOTER
522 #footer {
530 #footer {
523 padding: 0;
531 padding: 0;
524 text-align: center;
532 text-align: center;
525 vertical-align: middle;
533 vertical-align: middle;
526 color: @grey2;
534 color: @grey2;
527 font-size: 11px;
535 font-size: 11px;
528
536
529 p {
537 p {
530 margin: 0;
538 margin: 0;
531 padding: 1em;
539 padding: 1em;
532 line-height: 1em;
540 line-height: 1em;
533 }
541 }
534
542
535 .server-instance { //server instance
543 .server-instance { //server instance
536 display: none;
544 display: none;
537 }
545 }
538
546
539 .title {
547 .title {
540 float: none;
548 float: none;
541 margin: 0 auto;
549 margin: 0 auto;
542 }
550 }
543 }
551 }
544
552
545 button.close {
553 button.close {
546 padding: 0;
554 padding: 0;
547 cursor: pointer;
555 cursor: pointer;
548 background: transparent;
556 background: transparent;
549 border: 0;
557 border: 0;
550 .box-shadow(none);
558 .box-shadow(none);
551 -webkit-appearance: none;
559 -webkit-appearance: none;
552 }
560 }
553
561
554 .close {
562 .close {
555 float: right;
563 float: right;
556 font-size: 21px;
564 font-size: 21px;
557 font-family: @text-bootstrap;
565 font-family: @text-bootstrap;
558 line-height: 1em;
566 line-height: 1em;
559 font-weight: bold;
567 font-weight: bold;
560 color: @grey2;
568 color: @grey2;
561
569
562 &:hover,
570 &:hover,
563 &:focus {
571 &:focus {
564 color: @grey1;
572 color: @grey1;
565 text-decoration: none;
573 text-decoration: none;
566 cursor: pointer;
574 cursor: pointer;
567 }
575 }
568 }
576 }
569
577
570 // GRID
578 // GRID
571 .sorting,
579 .sorting,
572 .sorting_desc,
580 .sorting_desc,
573 .sorting_asc {
581 .sorting_asc {
574 cursor: pointer;
582 cursor: pointer;
575 }
583 }
576 .sorting_desc:after {
584 .sorting_desc:after {
577 content: "\00A0\25B2";
585 content: "\00A0\25B2";
578 font-size: .75em;
586 font-size: .75em;
579 }
587 }
580 .sorting_asc:after {
588 .sorting_asc:after {
581 content: "\00A0\25BC";
589 content: "\00A0\25BC";
582 font-size: .68em;
590 font-size: .68em;
583 }
591 }
584
592
585
593
586 .user_auth_tokens {
594 .user_auth_tokens {
587
595
588 &.truncate {
596 &.truncate {
589 white-space: nowrap;
597 white-space: nowrap;
590 overflow: hidden;
598 overflow: hidden;
591 text-overflow: ellipsis;
599 text-overflow: ellipsis;
592 }
600 }
593
601
594 .fields .field .input {
602 .fields .field .input {
595 margin: 0;
603 margin: 0;
596 }
604 }
597
605
598 input#description {
606 input#description {
599 width: 100px;
607 width: 100px;
600 margin: 0;
608 margin: 0;
601 }
609 }
602
610
603 .drop-menu {
611 .drop-menu {
604 // TODO: johbo: Remove this, should work out of the box when
612 // TODO: johbo: Remove this, should work out of the box when
605 // having multiple inputs inline
613 // having multiple inputs inline
606 margin: 0 0 0 5px;
614 margin: 0 0 0 5px;
607 }
615 }
608 }
616 }
609 #user_list_table {
617 #user_list_table {
610 .closed {
618 .closed {
611 background-color: @grey6;
619 background-color: @grey6;
612 }
620 }
613 }
621 }
614
622
615
623
616 input, textarea {
624 input, textarea {
617 &.disabled {
625 &.disabled {
618 opacity: .5;
626 opacity: .5;
619 }
627 }
620
628
621 &:hover {
629 &:hover {
622 border-color: @grey3;
630 border-color: @grey3;
623 box-shadow: @button-shadow;
631 box-shadow: @button-shadow;
624 }
632 }
625
633
626 &:focus {
634 &:focus {
627 border-color: @rcblue;
635 border-color: @rcblue;
628 box-shadow: @button-shadow;
636 box-shadow: @button-shadow;
629 }
637 }
630 }
638 }
631
639
632 // remove extra padding in firefox
640 // remove extra padding in firefox
633 input::-moz-focus-inner { border:0; padding:0 }
641 input::-moz-focus-inner { border:0; padding:0 }
634
642
635 .adjacent input {
643 .adjacent input {
636 margin-bottom: @padding;
644 margin-bottom: @padding;
637 }
645 }
638
646
639 .permissions_boxes {
647 .permissions_boxes {
640 display: block;
648 display: block;
641 }
649 }
642
650
643 //FORMS
651 //FORMS
644
652
645 .medium-inline,
653 .medium-inline,
646 input#description.medium-inline {
654 input#description.medium-inline {
647 display: inline;
655 display: inline;
648 width: @medium-inline-input-width;
656 width: @medium-inline-input-width;
649 min-width: 100px;
657 min-width: 100px;
650 }
658 }
651
659
652 select {
660 select {
653 //reset
661 //reset
654 -webkit-appearance: none;
662 -webkit-appearance: none;
655 -moz-appearance: none;
663 -moz-appearance: none;
656
664
657 display: inline-block;
665 display: inline-block;
658 height: 28px;
666 height: 28px;
659 width: auto;
667 width: auto;
660 margin: 0 @padding @padding 0;
668 margin: 0 @padding @padding 0;
661 padding: 0 18px 0 8px;
669 padding: 0 18px 0 8px;
662 line-height:1em;
670 line-height:1em;
663 font-size: @basefontsize;
671 font-size: @basefontsize;
664 border: @border-thickness solid @grey5;
672 border: @border-thickness solid @grey5;
665 border-radius: @border-radius;
673 border-radius: @border-radius;
666 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
674 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
667 color: @grey4;
675 color: @grey4;
668 box-shadow: @button-shadow;
676 box-shadow: @button-shadow;
669
677
670 &:after {
678 &:after {
671 content: "\00A0\25BE";
679 content: "\00A0\25BE";
672 }
680 }
673
681
674 &:focus, &:hover {
682 &:focus, &:hover {
675 outline: none;
683 outline: none;
676 border-color: @grey4;
684 border-color: @grey4;
677 color: @rcdarkblue;
685 color: @rcdarkblue;
678 }
686 }
679 }
687 }
680
688
681 option {
689 option {
682 &:focus {
690 &:focus {
683 outline: none;
691 outline: none;
684 }
692 }
685 }
693 }
686
694
687 input,
695 input,
688 textarea {
696 textarea {
689 padding: @input-padding;
697 padding: @input-padding;
690 border: @input-border-thickness solid @border-highlight-color;
698 border: @input-border-thickness solid @border-highlight-color;
691 .border-radius (@border-radius);
699 .border-radius (@border-radius);
692 font-family: @text-light;
700 font-family: @text-light;
693 font-size: @basefontsize;
701 font-size: @basefontsize;
694
702
695 &.input-sm {
703 &.input-sm {
696 padding: 5px;
704 padding: 5px;
697 }
705 }
698
706
699 &#description {
707 &#description {
700 min-width: @input-description-minwidth;
708 min-width: @input-description-minwidth;
701 min-height: 1em;
709 min-height: 1em;
702 padding: 10px;
710 padding: 10px;
703 }
711 }
704 }
712 }
705
713
706 .field-sm {
714 .field-sm {
707 input,
715 input,
708 textarea {
716 textarea {
709 padding: 5px;
717 padding: 5px;
710 }
718 }
711 }
719 }
712
720
713 textarea {
721 textarea {
714 display: block;
722 display: block;
715 clear: both;
723 clear: both;
716 width: 100%;
724 width: 100%;
717 min-height: 100px;
725 min-height: 100px;
718 margin-bottom: @padding;
726 margin-bottom: @padding;
719 .box-sizing(border-box);
727 .box-sizing(border-box);
720 overflow: auto;
728 overflow: auto;
721 }
729 }
722
730
723 label {
731 label {
724 font-family: @text-light;
732 font-family: @text-light;
725 }
733 }
726
734
727 // GRAVATARS
735 // GRAVATARS
728 // centers gravatar on username to the right
736 // centers gravatar on username to the right
729
737
730 .gravatar {
738 .gravatar {
731 display: inline;
739 display: inline;
732 min-width: 16px;
740 min-width: 16px;
733 min-height: 16px;
741 min-height: 16px;
734 margin: -5px 0;
742 margin: -5px 0;
735 padding: 0;
743 padding: 0;
736 line-height: 1em;
744 line-height: 1em;
737 box-sizing: content-box;
745 box-sizing: content-box;
738 border-radius: 50%;
746 border-radius: 50%;
739
747
740 &.gravatar-large {
748 &.gravatar-large {
741 margin: -0.5em .25em -0.5em 0;
749 margin: -0.5em .25em -0.5em 0;
742 }
750 }
743
751
744 & + .user {
752 & + .user {
745 display: inline;
753 display: inline;
746 margin: 0;
754 margin: 0;
747 padding: 0 0 0 .17em;
755 padding: 0 0 0 .17em;
748 line-height: 1em;
756 line-height: 1em;
749 }
757 }
750 }
758 }
751
759
752 .user-inline-data {
760 .user-inline-data {
753 display: inline-block;
761 display: inline-block;
754 float: left;
762 float: left;
755 padding-left: .5em;
763 padding-left: .5em;
756 line-height: 1.3em;
764 line-height: 1.3em;
757 }
765 }
758
766
759 .rc-user { // gravatar + user wrapper
767 .rc-user { // gravatar + user wrapper
760 float: left;
768 float: left;
761 position: relative;
769 position: relative;
762 min-width: 100px;
770 min-width: 100px;
763 max-width: 200px;
771 max-width: 200px;
764 min-height: (@gravatar-size + @border-thickness * 2); // account for border
772 min-height: (@gravatar-size + @border-thickness * 2); // account for border
765 display: block;
773 display: block;
766 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
774 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
767
775
768
776
769 .gravatar {
777 .gravatar {
770 display: block;
778 display: block;
771 position: absolute;
779 position: absolute;
772 top: 0;
780 top: 0;
773 left: 0;
781 left: 0;
774 min-width: @gravatar-size;
782 min-width: @gravatar-size;
775 min-height: @gravatar-size;
783 min-height: @gravatar-size;
776 margin: 0;
784 margin: 0;
777 }
785 }
778
786
779 .user {
787 .user {
780 display: block;
788 display: block;
781 max-width: 175px;
789 max-width: 175px;
782 padding-top: 2px;
790 padding-top: 2px;
783 overflow: hidden;
791 overflow: hidden;
784 text-overflow: ellipsis;
792 text-overflow: ellipsis;
785 }
793 }
786 }
794 }
787
795
788 .gist-gravatar,
796 .gist-gravatar,
789 .journal_container {
797 .journal_container {
790 .gravatar-large {
798 .gravatar-large {
791 margin: 0 .5em -10px 0;
799 margin: 0 .5em -10px 0;
792 }
800 }
793 }
801 }
794
802
795
803
796 // ADMIN SETTINGS
804 // ADMIN SETTINGS
797
805
798 // Tag Patterns
806 // Tag Patterns
799 .tag_patterns {
807 .tag_patterns {
800 .tag_input {
808 .tag_input {
801 margin-bottom: @padding;
809 margin-bottom: @padding;
802 }
810 }
803 }
811 }
804
812
805 .locked_input {
813 .locked_input {
806 position: relative;
814 position: relative;
807
815
808 input {
816 input {
809 display: inline;
817 display: inline;
810 margin: 3px 5px 0px 0px;
818 margin: 3px 5px 0px 0px;
811 }
819 }
812
820
813 br {
821 br {
814 display: none;
822 display: none;
815 }
823 }
816
824
817 .error-message {
825 .error-message {
818 float: left;
826 float: left;
819 width: 100%;
827 width: 100%;
820 }
828 }
821
829
822 .lock_input_button {
830 .lock_input_button {
823 display: inline;
831 display: inline;
824 }
832 }
825
833
826 .help-block {
834 .help-block {
827 clear: both;
835 clear: both;
828 }
836 }
829 }
837 }
830
838
831 // Notifications
839 // Notifications
832
840
833 .notifications_buttons {
841 .notifications_buttons {
834 margin: 0 0 @space 0;
842 margin: 0 0 @space 0;
835 padding: 0;
843 padding: 0;
836
844
837 .btn {
845 .btn {
838 display: inline-block;
846 display: inline-block;
839 }
847 }
840 }
848 }
841
849
842 .notification-list {
850 .notification-list {
843
851
844 div {
852 div {
845 display: inline-block;
853 display: inline-block;
846 vertical-align: middle;
854 vertical-align: middle;
847 }
855 }
848
856
849 .container {
857 .container {
850 display: block;
858 display: block;
851 margin: 0 0 @padding 0;
859 margin: 0 0 @padding 0;
852 }
860 }
853
861
854 .delete-notifications {
862 .delete-notifications {
855 margin-left: @padding;
863 margin-left: @padding;
856 text-align: right;
864 text-align: right;
857 cursor: pointer;
865 cursor: pointer;
858 }
866 }
859
867
860 .read-notifications {
868 .read-notifications {
861 margin-left: @padding/2;
869 margin-left: @padding/2;
862 text-align: right;
870 text-align: right;
863 width: 35px;
871 width: 35px;
864 cursor: pointer;
872 cursor: pointer;
865 }
873 }
866
874
867 .icon-minus-sign {
875 .icon-minus-sign {
868 color: @alert2;
876 color: @alert2;
869 }
877 }
870
878
871 .icon-ok-sign {
879 .icon-ok-sign {
872 color: @alert1;
880 color: @alert1;
873 }
881 }
874 }
882 }
875
883
876 .user_settings {
884 .user_settings {
877 float: left;
885 float: left;
878 clear: both;
886 clear: both;
879 display: block;
887 display: block;
880 width: 100%;
888 width: 100%;
881
889
882 .gravatar_box {
890 .gravatar_box {
883 margin-bottom: @padding;
891 margin-bottom: @padding;
884
892
885 &:after {
893 &:after {
886 content: " ";
894 content: " ";
887 clear: both;
895 clear: both;
888 width: 100%;
896 width: 100%;
889 }
897 }
890 }
898 }
891
899
892 .fields .field {
900 .fields .field {
893 clear: both;
901 clear: both;
894 }
902 }
895 }
903 }
896
904
897 .advanced_settings {
905 .advanced_settings {
898 margin-bottom: @space;
906 margin-bottom: @space;
899
907
900 .help-block {
908 .help-block {
901 margin-left: 0;
909 margin-left: 0;
902 }
910 }
903
911
904 button + .help-block {
912 button + .help-block {
905 margin-top: @padding;
913 margin-top: @padding;
906 }
914 }
907 }
915 }
908
916
909 // admin settings radio buttons and labels
917 // admin settings radio buttons and labels
910 .label-2 {
918 .label-2 {
911 float: left;
919 float: left;
912 width: @label2-width;
920 width: @label2-width;
913
921
914 label {
922 label {
915 color: @grey1;
923 color: @grey1;
916 }
924 }
917 }
925 }
918 .checkboxes {
926 .checkboxes {
919 float: left;
927 float: left;
920 width: @checkboxes-width;
928 width: @checkboxes-width;
921 margin-bottom: @padding;
929 margin-bottom: @padding;
922
930
923 .checkbox {
931 .checkbox {
924 width: 100%;
932 width: 100%;
925
933
926 label {
934 label {
927 margin: 0;
935 margin: 0;
928 padding: 0;
936 padding: 0;
929 }
937 }
930 }
938 }
931
939
932 .checkbox + .checkbox {
940 .checkbox + .checkbox {
933 display: inline-block;
941 display: inline-block;
934 }
942 }
935
943
936 label {
944 label {
937 margin-right: 1em;
945 margin-right: 1em;
938 }
946 }
939 }
947 }
940
948
941 // CHANGELOG
949 // CHANGELOG
942 .container_header {
950 .container_header {
943 float: left;
951 float: left;
944 display: block;
952 display: block;
945 width: 100%;
953 width: 100%;
946 margin: @padding 0 @padding;
954 margin: @padding 0 @padding;
947
955
948 #filter_changelog {
956 #filter_changelog {
949 float: left;
957 float: left;
950 margin-right: @padding;
958 margin-right: @padding;
951 }
959 }
952
960
953 .breadcrumbs_light {
961 .breadcrumbs_light {
954 display: inline-block;
962 display: inline-block;
955 }
963 }
956 }
964 }
957
965
958 .info_box {
966 .info_box {
959 float: right;
967 float: right;
960 }
968 }
961
969
962
970
963
971
964 #graph_content{
972 #graph_content{
965
973
966 // adjust for table headers so that graph renders properly
974 // adjust for table headers so that graph renders properly
967 // #graph_nodes padding - table cell padding
975 // #graph_nodes padding - table cell padding
968 padding-top: (@space - (@basefontsize * 2.4));
976 padding-top: (@space - (@basefontsize * 2.4));
969
977
970 &.graph_full_width {
978 &.graph_full_width {
971 width: 100%;
979 width: 100%;
972 max-width: 100%;
980 max-width: 100%;
973 }
981 }
974 }
982 }
975
983
976 #graph {
984 #graph {
977 .flag_status {
985 .flag_status {
978 margin: 0;
986 margin: 0;
979 }
987 }
980
988
981 .pagination-left {
989 .pagination-left {
982 float: left;
990 float: left;
983 clear: both;
991 clear: both;
984 }
992 }
985
993
986 .log-container {
994 .log-container {
987 max-width: 345px;
995 max-width: 345px;
988
996
989 .message{
997 .message{
990 max-width: 340px;
998 max-width: 340px;
991 }
999 }
992 }
1000 }
993
1001
994 .graph-col-wrapper {
1002 .graph-col-wrapper {
995
1003
996 #graph_nodes {
1004 #graph_nodes {
997 width: 100px;
1005 width: 100px;
998 position: absolute;
1006 position: absolute;
999 left: 70px;
1007 left: 70px;
1000 z-index: -1;
1008 z-index: -1;
1001 }
1009 }
1002 }
1010 }
1003
1011
1004 .load-more-commits {
1012 .load-more-commits {
1005 text-align: center;
1013 text-align: center;
1006 }
1014 }
1007 .load-more-commits:hover {
1015 .load-more-commits:hover {
1008 background-color: @grey7;
1016 background-color: @grey7;
1009 }
1017 }
1010 .load-more-commits {
1018 .load-more-commits {
1011 a {
1019 a {
1012 display: block;
1020 display: block;
1013 }
1021 }
1014 }
1022 }
1015 }
1023 }
1016
1024
1017 .obsolete-toggle {
1025 .obsolete-toggle {
1018 line-height: 30px;
1026 line-height: 30px;
1019 margin-left: -15px;
1027 margin-left: -15px;
1020 }
1028 }
1021
1029
1022 #rev_range_container, #rev_range_clear, #rev_range_more {
1030 #rev_range_container, #rev_range_clear, #rev_range_more {
1023 margin-top: -5px;
1031 margin-top: -5px;
1024 margin-bottom: -5px;
1032 margin-bottom: -5px;
1025 }
1033 }
1026
1034
1027 #filter_changelog {
1035 #filter_changelog {
1028 float: left;
1036 float: left;
1029 }
1037 }
1030
1038
1031
1039
1032 //--- THEME ------------------//
1040 //--- THEME ------------------//
1033
1041
1034 #logo {
1042 #logo {
1035 float: left;
1043 float: left;
1036 margin: 9px 0 0 0;
1044 margin: 9px 0 0 0;
1037
1045
1038 .header {
1046 .header {
1039 background-color: transparent;
1047 background-color: transparent;
1040 }
1048 }
1041
1049
1042 a {
1050 a {
1043 display: inline-block;
1051 display: inline-block;
1044 }
1052 }
1045
1053
1046 img {
1054 img {
1047 height:30px;
1055 height:30px;
1048 }
1056 }
1049 }
1057 }
1050
1058
1051 .logo-wrapper {
1059 .logo-wrapper {
1052 float:left;
1060 float:left;
1053 }
1061 }
1054
1062
1055 .branding {
1063 .branding {
1056 float: left;
1064 float: left;
1057 padding: 9px 2px;
1065 padding: 9px 2px;
1058 line-height: 1em;
1066 line-height: 1em;
1059 font-size: @navigation-fontsize;
1067 font-size: @navigation-fontsize;
1060
1068
1061 a {
1069 a {
1062 color: @grey5
1070 color: @grey5
1063 }
1071 }
1064 }
1072 }
1065
1073
1066 img {
1074 img {
1067 border: none;
1075 border: none;
1068 outline: none;
1076 outline: none;
1069 }
1077 }
1070 user-profile-header
1078 user-profile-header
1071 label {
1079 label {
1072
1080
1073 input[type="checkbox"] {
1081 input[type="checkbox"] {
1074 margin-right: 1em;
1082 margin-right: 1em;
1075 }
1083 }
1076 input[type="radio"] {
1084 input[type="radio"] {
1077 margin-right: 1em;
1085 margin-right: 1em;
1078 }
1086 }
1079 }
1087 }
1080
1088
1081 .flag_status {
1089 .flag_status {
1082 margin: 2px;
1090 margin: 2px;
1083 &.under_review {
1091 &.under_review {
1084 .circle(5px, @alert3);
1092 .circle(5px, @alert3);
1085 }
1093 }
1086 &.approved {
1094 &.approved {
1087 .circle(5px, @alert1);
1095 .circle(5px, @alert1);
1088 }
1096 }
1089 &.rejected,
1097 &.rejected,
1090 &.forced_closed{
1098 &.forced_closed{
1091 .circle(5px, @alert2);
1099 .circle(5px, @alert2);
1092 }
1100 }
1093 &.not_reviewed {
1101 &.not_reviewed {
1094 .circle(5px, @grey5);
1102 .circle(5px, @grey5);
1095 }
1103 }
1096 }
1104 }
1097
1105
1098 .flag_status_comment_box {
1106 .flag_status_comment_box {
1099 margin: 5px 6px 0px 2px;
1107 margin: 5px 6px 0px 2px;
1100 }
1108 }
1101 .test_pattern_preview {
1109 .test_pattern_preview {
1102 margin: @space 0;
1110 margin: @space 0;
1103
1111
1104 p {
1112 p {
1105 margin-bottom: 0;
1113 margin-bottom: 0;
1106 border-bottom: @border-thickness solid @border-default-color;
1114 border-bottom: @border-thickness solid @border-default-color;
1107 color: @grey3;
1115 color: @grey3;
1108 }
1116 }
1109
1117
1110 .btn {
1118 .btn {
1111 margin-bottom: @padding;
1119 margin-bottom: @padding;
1112 }
1120 }
1113 }
1121 }
1114 #test_pattern_result {
1122 #test_pattern_result {
1115 display: none;
1123 display: none;
1116 &:extend(pre);
1124 &:extend(pre);
1117 padding: .9em;
1125 padding: .9em;
1118 color: @grey3;
1126 color: @grey3;
1119 background-color: @grey7;
1127 background-color: @grey7;
1120 border-right: @border-thickness solid @border-default-color;
1128 border-right: @border-thickness solid @border-default-color;
1121 border-bottom: @border-thickness solid @border-default-color;
1129 border-bottom: @border-thickness solid @border-default-color;
1122 border-left: @border-thickness solid @border-default-color;
1130 border-left: @border-thickness solid @border-default-color;
1123 }
1131 }
1124
1132
1125 #repo_vcs_settings {
1133 #repo_vcs_settings {
1126 #inherit_overlay_vcs_default {
1134 #inherit_overlay_vcs_default {
1127 display: none;
1135 display: none;
1128 }
1136 }
1129 #inherit_overlay_vcs_custom {
1137 #inherit_overlay_vcs_custom {
1130 display: custom;
1138 display: custom;
1131 }
1139 }
1132 &.inherited {
1140 &.inherited {
1133 #inherit_overlay_vcs_default {
1141 #inherit_overlay_vcs_default {
1134 display: block;
1142 display: block;
1135 }
1143 }
1136 #inherit_overlay_vcs_custom {
1144 #inherit_overlay_vcs_custom {
1137 display: none;
1145 display: none;
1138 }
1146 }
1139 }
1147 }
1140 }
1148 }
1141
1149
1142 .issue-tracker-link {
1150 .issue-tracker-link {
1143 color: @rcblue;
1151 color: @rcblue;
1144 }
1152 }
1145
1153
1146 // Issue Tracker Table Show/Hide
1154 // Issue Tracker Table Show/Hide
1147 #repo_issue_tracker {
1155 #repo_issue_tracker {
1148 #inherit_overlay {
1156 #inherit_overlay {
1149 display: none;
1157 display: none;
1150 }
1158 }
1151 #custom_overlay {
1159 #custom_overlay {
1152 display: custom;
1160 display: custom;
1153 }
1161 }
1154 &.inherited {
1162 &.inherited {
1155 #inherit_overlay {
1163 #inherit_overlay {
1156 display: block;
1164 display: block;
1157 }
1165 }
1158 #custom_overlay {
1166 #custom_overlay {
1159 display: none;
1167 display: none;
1160 }
1168 }
1161 }
1169 }
1162 }
1170 }
1163 table.issuetracker {
1171 table.issuetracker {
1164 &.readonly {
1172 &.readonly {
1165 tr, td {
1173 tr, td {
1166 color: @grey3;
1174 color: @grey3;
1167 }
1175 }
1168 }
1176 }
1169 .edit {
1177 .edit {
1170 display: none;
1178 display: none;
1171 }
1179 }
1172 .editopen {
1180 .editopen {
1173 .edit {
1181 .edit {
1174 display: inline;
1182 display: inline;
1175 }
1183 }
1176 .entry {
1184 .entry {
1177 display: none;
1185 display: none;
1178 }
1186 }
1179 }
1187 }
1180 tr td.td-action {
1188 tr td.td-action {
1181 min-width: 117px;
1189 min-width: 117px;
1182 }
1190 }
1183 td input {
1191 td input {
1184 max-width: none;
1192 max-width: none;
1185 min-width: 30px;
1193 min-width: 30px;
1186 width: 80%;
1194 width: 80%;
1187 }
1195 }
1188 .issuetracker_pref input {
1196 .issuetracker_pref input {
1189 width: 40%;
1197 width: 40%;
1190 }
1198 }
1191 input.edit_issuetracker_update {
1199 input.edit_issuetracker_update {
1192 margin-right: 0;
1200 margin-right: 0;
1193 width: auto;
1201 width: auto;
1194 }
1202 }
1195 }
1203 }
1196
1204
1197 table.integrations {
1205 table.integrations {
1198 .td-icon {
1206 .td-icon {
1199 width: 20px;
1207 width: 20px;
1200 .integration-icon {
1208 .integration-icon {
1201 height: 20px;
1209 height: 20px;
1202 width: 20px;
1210 width: 20px;
1203 }
1211 }
1204 }
1212 }
1205 }
1213 }
1206
1214
1207 .integrations {
1215 .integrations {
1208 a.integration-box {
1216 a.integration-box {
1209 color: @text-color;
1217 color: @text-color;
1210 &:hover {
1218 &:hover {
1211 .panel {
1219 .panel {
1212 background: #fbfbfb;
1220 background: #fbfbfb;
1213 }
1221 }
1214 }
1222 }
1215 .integration-icon {
1223 .integration-icon {
1216 width: 30px;
1224 width: 30px;
1217 height: 30px;
1225 height: 30px;
1218 margin-right: 20px;
1226 margin-right: 20px;
1219 float: left;
1227 float: left;
1220 }
1228 }
1221
1229
1222 .panel-body {
1230 .panel-body {
1223 padding: 10px;
1231 padding: 10px;
1224 }
1232 }
1225 .panel {
1233 .panel {
1226 margin-bottom: 10px;
1234 margin-bottom: 10px;
1227 }
1235 }
1228 h2 {
1236 h2 {
1229 display: inline-block;
1237 display: inline-block;
1230 margin: 0;
1238 margin: 0;
1231 min-width: 140px;
1239 min-width: 140px;
1232 }
1240 }
1233 }
1241 }
1234 a.integration-box.dummy-integration {
1242 a.integration-box.dummy-integration {
1235 color: @grey4
1243 color: @grey4
1236 }
1244 }
1237 }
1245 }
1238
1246
1239 //Permissions Settings
1247 //Permissions Settings
1240 #add_perm {
1248 #add_perm {
1241 margin: 0 0 @padding;
1249 margin: 0 0 @padding;
1242 cursor: pointer;
1250 cursor: pointer;
1243 }
1251 }
1244
1252
1245 .perm_ac {
1253 .perm_ac {
1246 input {
1254 input {
1247 width: 95%;
1255 width: 95%;
1248 }
1256 }
1249 }
1257 }
1250
1258
1251 .autocomplete-suggestions {
1259 .autocomplete-suggestions {
1252 width: auto !important; // overrides autocomplete.js
1260 width: auto !important; // overrides autocomplete.js
1253 min-width: 278px;
1261 min-width: 278px;
1254 margin: 0;
1262 margin: 0;
1255 border: @border-thickness solid @grey5;
1263 border: @border-thickness solid @grey5;
1256 border-radius: @border-radius;
1264 border-radius: @border-radius;
1257 color: @grey2;
1265 color: @grey2;
1258 background-color: white;
1266 background-color: white;
1259 }
1267 }
1260
1268
1261 .autocomplete-qfilter-suggestions {
1269 .autocomplete-qfilter-suggestions {
1262 width: auto !important; // overrides autocomplete.js
1270 width: auto !important; // overrides autocomplete.js
1263 max-height: 100% !important;
1271 max-height: 100% !important;
1264 min-width: 376px;
1272 min-width: 376px;
1265 margin: 0;
1273 margin: 0;
1266 border: @border-thickness solid @grey5;
1274 border: @border-thickness solid @grey5;
1267 color: @grey2;
1275 color: @grey2;
1268 background-color: white;
1276 background-color: white;
1269 }
1277 }
1270
1278
1271 .autocomplete-selected {
1279 .autocomplete-selected {
1272 background: #F0F0F0;
1280 background: #F0F0F0;
1273 }
1281 }
1274
1282
1275 .ac-container-wrap {
1283 .ac-container-wrap {
1276 margin: 0;
1284 margin: 0;
1277 padding: 8px;
1285 padding: 8px;
1278 border-bottom: @border-thickness solid @grey5;
1286 border-bottom: @border-thickness solid @grey5;
1279 list-style-type: none;
1287 list-style-type: none;
1280 cursor: pointer;
1288 cursor: pointer;
1281
1289
1282 &:hover {
1290 &:hover {
1283 background-color: @grey7;
1291 background-color: @grey7;
1284 }
1292 }
1285
1293
1286 img {
1294 img {
1287 height: @gravatar-size;
1295 height: @gravatar-size;
1288 width: @gravatar-size;
1296 width: @gravatar-size;
1289 margin-right: 1em;
1297 margin-right: 1em;
1290 }
1298 }
1291
1299
1292 strong {
1300 strong {
1293 font-weight: normal;
1301 font-weight: normal;
1294 }
1302 }
1295 }
1303 }
1296
1304
1297 // Settings Dropdown
1305 // Settings Dropdown
1298 .user-menu .container {
1306 .user-menu .container {
1299 padding: 0 4px;
1307 padding: 0 4px;
1300 margin: 0;
1308 margin: 0;
1301 }
1309 }
1302
1310
1303 .user-menu .gravatar {
1311 .user-menu .gravatar {
1304 cursor: pointer;
1312 cursor: pointer;
1305 }
1313 }
1306
1314
1307 .codeblock {
1315 .codeblock {
1308 margin-bottom: @padding;
1316 margin-bottom: @padding;
1309 clear: both;
1317 clear: both;
1310
1318
1311 .stats {
1319 .stats {
1312 overflow: hidden;
1320 overflow: hidden;
1313 }
1321 }
1314
1322
1315 .message{
1323 .message{
1316 textarea{
1324 textarea{
1317 margin: 0;
1325 margin: 0;
1318 }
1326 }
1319 }
1327 }
1320
1328
1321 .code-header {
1329 .code-header {
1322 .stats {
1330 .stats {
1323 line-height: 2em;
1331 line-height: 2em;
1324
1332
1325 .revision_id {
1333 .revision_id {
1326 margin-left: 0;
1334 margin-left: 0;
1327 }
1335 }
1328 .buttons {
1336 .buttons {
1329 padding-right: 0;
1337 padding-right: 0;
1330 }
1338 }
1331 }
1339 }
1332
1340
1333 .item{
1341 .item{
1334 margin-right: 0.5em;
1342 margin-right: 0.5em;
1335 }
1343 }
1336 }
1344 }
1337
1345
1338 #editor_container {
1346 #editor_container {
1339 position: relative;
1347 position: relative;
1340 margin: @padding 10px;
1348 margin: @padding 10px;
1341 }
1349 }
1342 }
1350 }
1343
1351
1344 #file_history_container {
1352 #file_history_container {
1345 display: none;
1353 display: none;
1346 }
1354 }
1347
1355
1348 .file-history-inner {
1356 .file-history-inner {
1349 margin-bottom: 10px;
1357 margin-bottom: 10px;
1350 }
1358 }
1351
1359
1352 // Pull Requests
1360 // Pull Requests
1353 .summary-details {
1361 .summary-details {
1354 width: 72%;
1362 width: 72%;
1355 }
1363 }
1356 .pr-summary {
1364 .pr-summary {
1357 border-bottom: @border-thickness solid @grey5;
1365 border-bottom: @border-thickness solid @grey5;
1358 margin-bottom: @space;
1366 margin-bottom: @space;
1359 }
1367 }
1360 .reviewers-title {
1368 .reviewers-title {
1361 width: 25%;
1369 width: 25%;
1362 min-width: 200px;
1370 min-width: 200px;
1363 }
1371 }
1364 .reviewers {
1372 .reviewers {
1365 width: 25%;
1373 width: 25%;
1366 min-width: 200px;
1374 min-width: 200px;
1367 }
1375 }
1368 .reviewers ul li {
1376 .reviewers ul li {
1369 position: relative;
1377 position: relative;
1370 width: 100%;
1378 width: 100%;
1371 padding-bottom: 8px;
1379 padding-bottom: 8px;
1372 list-style-type: none;
1380 list-style-type: none;
1373 }
1381 }
1374
1382
1375 .reviewer_entry {
1383 .reviewer_entry {
1376 min-height: 55px;
1384 min-height: 55px;
1377 }
1385 }
1378
1386
1379 .reviewers_member {
1387 .reviewers_member {
1380 width: 100%;
1388 width: 100%;
1381 overflow: auto;
1389 overflow: auto;
1382 }
1390 }
1383 .reviewer_reason {
1391 .reviewer_reason {
1384 padding-left: 20px;
1392 padding-left: 20px;
1385 line-height: 1.5em;
1393 line-height: 1.5em;
1386 }
1394 }
1387 .reviewer_status {
1395 .reviewer_status {
1388 display: inline-block;
1396 display: inline-block;
1389 vertical-align: top;
1397 vertical-align: top;
1390 width: 25px;
1398 width: 25px;
1391 min-width: 25px;
1399 min-width: 25px;
1392 height: 1.2em;
1400 height: 1.2em;
1393 margin-top: 3px;
1401 margin-top: 3px;
1394 line-height: 1em;
1402 line-height: 1em;
1395 }
1403 }
1396
1404
1397 .reviewer_name {
1405 .reviewer_name {
1398 display: inline-block;
1406 display: inline-block;
1399 max-width: 83%;
1407 max-width: 83%;
1400 padding-right: 20px;
1408 padding-right: 20px;
1401 vertical-align: middle;
1409 vertical-align: middle;
1402 line-height: 1;
1410 line-height: 1;
1403
1411
1404 .rc-user {
1412 .rc-user {
1405 min-width: 0;
1413 min-width: 0;
1406 margin: -2px 1em 0 0;
1414 margin: -2px 1em 0 0;
1407 }
1415 }
1408
1416
1409 .reviewer {
1417 .reviewer {
1410 float: left;
1418 float: left;
1411 }
1419 }
1412 }
1420 }
1413
1421
1414 .reviewer_member_mandatory {
1422 .reviewer_member_mandatory {
1415 position: absolute;
1423 position: absolute;
1416 left: 15px;
1424 left: 15px;
1417 top: 8px;
1425 top: 8px;
1418 width: 16px;
1426 width: 16px;
1419 font-size: 11px;
1427 font-size: 11px;
1420 margin: 0;
1428 margin: 0;
1421 padding: 0;
1429 padding: 0;
1422 color: black;
1430 color: black;
1423 }
1431 }
1424
1432
1425 .reviewer_member_mandatory_remove,
1433 .reviewer_member_mandatory_remove,
1426 .reviewer_member_remove {
1434 .reviewer_member_remove {
1427 position: absolute;
1435 position: absolute;
1428 right: 0;
1436 right: 0;
1429 top: 0;
1437 top: 0;
1430 width: 16px;
1438 width: 16px;
1431 margin-bottom: 10px;
1439 margin-bottom: 10px;
1432 padding: 0;
1440 padding: 0;
1433 color: black;
1441 color: black;
1434 }
1442 }
1435
1443
1436 .reviewer_member_mandatory_remove {
1444 .reviewer_member_mandatory_remove {
1437 color: @grey4;
1445 color: @grey4;
1438 }
1446 }
1439
1447
1440 .reviewer_member_status {
1448 .reviewer_member_status {
1441 margin-top: 5px;
1449 margin-top: 5px;
1442 }
1450 }
1443 .pr-summary #summary{
1451 .pr-summary #summary{
1444 width: 100%;
1452 width: 100%;
1445 }
1453 }
1446 .pr-summary .action_button:hover {
1454 .pr-summary .action_button:hover {
1447 border: 0;
1455 border: 0;
1448 cursor: pointer;
1456 cursor: pointer;
1449 }
1457 }
1450 .pr-details-title {
1458 .pr-details-title {
1451 padding-bottom: 8px;
1459 padding-bottom: 8px;
1452 border-bottom: @border-thickness solid @grey5;
1460 border-bottom: @border-thickness solid @grey5;
1453
1461
1454 .action_button.disabled {
1462 .action_button.disabled {
1455 color: @grey4;
1463 color: @grey4;
1456 cursor: inherit;
1464 cursor: inherit;
1457 }
1465 }
1458 .action_button {
1466 .action_button {
1459 color: @rcblue;
1467 color: @rcblue;
1460 }
1468 }
1461 }
1469 }
1462 .pr-details-content {
1470 .pr-details-content {
1463 margin-top: @textmargin;
1471 margin-top: @textmargin;
1464 margin-bottom: @textmargin;
1472 margin-bottom: @textmargin;
1465 }
1473 }
1466
1474
1467 .pr-reviewer-rules {
1475 .pr-reviewer-rules {
1468 padding: 10px 0px 20px 0px;
1476 padding: 10px 0px 20px 0px;
1469 }
1477 }
1470
1478
1471 .group_members {
1479 .group_members {
1472 margin-top: 0;
1480 margin-top: 0;
1473 padding: 0;
1481 padding: 0;
1474 list-style: outside none none;
1482 list-style: outside none none;
1475
1483
1476 img {
1484 img {
1477 height: @gravatar-size;
1485 height: @gravatar-size;
1478 width: @gravatar-size;
1486 width: @gravatar-size;
1479 margin-right: .5em;
1487 margin-right: .5em;
1480 margin-left: 3px;
1488 margin-left: 3px;
1481 }
1489 }
1482
1490
1483 .to-delete {
1491 .to-delete {
1484 .user {
1492 .user {
1485 text-decoration: line-through;
1493 text-decoration: line-through;
1486 }
1494 }
1487 }
1495 }
1488 }
1496 }
1489
1497
1490 .compare_view_commits_title {
1498 .compare_view_commits_title {
1491 .disabled {
1499 .disabled {
1492 cursor: inherit;
1500 cursor: inherit;
1493 &:hover{
1501 &:hover{
1494 background-color: inherit;
1502 background-color: inherit;
1495 color: inherit;
1503 color: inherit;
1496 }
1504 }
1497 }
1505 }
1498 }
1506 }
1499
1507
1500 .subtitle-compare {
1508 .subtitle-compare {
1501 margin: -15px 0px 0px 0px;
1509 margin: -15px 0px 0px 0px;
1502 }
1510 }
1503
1511
1504 .comments-summary-td {
1512 .comments-summary-td {
1505 border-top: 1px dashed @grey5;
1513 border-top: 1px dashed @grey5;
1506 }
1514 }
1507
1515
1508 // new entry in group_members
1516 // new entry in group_members
1509 .td-author-new-entry {
1517 .td-author-new-entry {
1510 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1518 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1511 }
1519 }
1512
1520
1513 .usergroup_member_remove {
1521 .usergroup_member_remove {
1514 width: 16px;
1522 width: 16px;
1515 margin-bottom: 10px;
1523 margin-bottom: 10px;
1516 padding: 0;
1524 padding: 0;
1517 color: black !important;
1525 color: black !important;
1518 cursor: pointer;
1526 cursor: pointer;
1519 }
1527 }
1520
1528
1521 .reviewer_ac .ac-input {
1529 .reviewer_ac .ac-input {
1522 width: 92%;
1530 width: 92%;
1523 margin-bottom: 1em;
1531 margin-bottom: 1em;
1524 }
1532 }
1525
1533
1526 .compare_view_commits tr{
1534 .compare_view_commits tr{
1527 height: 20px;
1535 height: 20px;
1528 }
1536 }
1529 .compare_view_commits td {
1537 .compare_view_commits td {
1530 vertical-align: top;
1538 vertical-align: top;
1531 padding-top: 10px;
1539 padding-top: 10px;
1532 }
1540 }
1533 .compare_view_commits .author {
1541 .compare_view_commits .author {
1534 margin-left: 5px;
1542 margin-left: 5px;
1535 }
1543 }
1536
1544
1537 .compare_view_commits {
1545 .compare_view_commits {
1538 .color-a {
1546 .color-a {
1539 color: @alert1;
1547 color: @alert1;
1540 }
1548 }
1541
1549
1542 .color-c {
1550 .color-c {
1543 color: @color3;
1551 color: @color3;
1544 }
1552 }
1545
1553
1546 .color-r {
1554 .color-r {
1547 color: @color5;
1555 color: @color5;
1548 }
1556 }
1549
1557
1550 .color-a-bg {
1558 .color-a-bg {
1551 background-color: @alert1;
1559 background-color: @alert1;
1552 }
1560 }
1553
1561
1554 .color-c-bg {
1562 .color-c-bg {
1555 background-color: @alert3;
1563 background-color: @alert3;
1556 }
1564 }
1557
1565
1558 .color-r-bg {
1566 .color-r-bg {
1559 background-color: @alert2;
1567 background-color: @alert2;
1560 }
1568 }
1561
1569
1562 .color-a-border {
1570 .color-a-border {
1563 border: 1px solid @alert1;
1571 border: 1px solid @alert1;
1564 }
1572 }
1565
1573
1566 .color-c-border {
1574 .color-c-border {
1567 border: 1px solid @alert3;
1575 border: 1px solid @alert3;
1568 }
1576 }
1569
1577
1570 .color-r-border {
1578 .color-r-border {
1571 border: 1px solid @alert2;
1579 border: 1px solid @alert2;
1572 }
1580 }
1573
1581
1574 .commit-change-indicator {
1582 .commit-change-indicator {
1575 width: 15px;
1583 width: 15px;
1576 height: 15px;
1584 height: 15px;
1577 position: relative;
1585 position: relative;
1578 left: 15px;
1586 left: 15px;
1579 }
1587 }
1580
1588
1581 .commit-change-content {
1589 .commit-change-content {
1582 text-align: center;
1590 text-align: center;
1583 vertical-align: middle;
1591 vertical-align: middle;
1584 line-height: 15px;
1592 line-height: 15px;
1585 }
1593 }
1586 }
1594 }
1587
1595
1588 .compare_view_filepath {
1596 .compare_view_filepath {
1589 color: @grey1;
1597 color: @grey1;
1590 }
1598 }
1591
1599
1592 .show_more {
1600 .show_more {
1593 display: inline-block;
1601 display: inline-block;
1594 width: 0;
1602 width: 0;
1595 height: 0;
1603 height: 0;
1596 vertical-align: middle;
1604 vertical-align: middle;
1597 content: "";
1605 content: "";
1598 border: 4px solid;
1606 border: 4px solid;
1599 border-right-color: transparent;
1607 border-right-color: transparent;
1600 border-bottom-color: transparent;
1608 border-bottom-color: transparent;
1601 border-left-color: transparent;
1609 border-left-color: transparent;
1602 font-size: 0;
1610 font-size: 0;
1603 }
1611 }
1604
1612
1605 .journal_more .show_more {
1613 .journal_more .show_more {
1606 display: inline;
1614 display: inline;
1607
1615
1608 &:after {
1616 &:after {
1609 content: none;
1617 content: none;
1610 }
1618 }
1611 }
1619 }
1612
1620
1613 .compare_view_commits .collapse_commit:after {
1621 .compare_view_commits .collapse_commit:after {
1614 cursor: pointer;
1622 cursor: pointer;
1615 content: "\00A0\25B4";
1623 content: "\00A0\25B4";
1616 margin-left: -3px;
1624 margin-left: -3px;
1617 font-size: 17px;
1625 font-size: 17px;
1618 color: @grey4;
1626 color: @grey4;
1619 }
1627 }
1620
1628
1621 .diff_links {
1629 .diff_links {
1622 margin-left: 8px;
1630 margin-left: 8px;
1623 }
1631 }
1624
1632
1625 #pull_request_overview {
1633 #pull_request_overview {
1626 div.ancestor {
1634 div.ancestor {
1627 margin: -33px 0;
1635 margin: -33px 0;
1628 }
1636 }
1629 }
1637 }
1630
1638
1631 div.ancestor {
1639 div.ancestor {
1632 line-height: 33px;
1640 line-height: 33px;
1633 }
1641 }
1634
1642
1635 .cs_icon_td input[type="checkbox"] {
1643 .cs_icon_td input[type="checkbox"] {
1636 display: none;
1644 display: none;
1637 }
1645 }
1638
1646
1639 .cs_icon_td .expand_file_icon:after {
1647 .cs_icon_td .expand_file_icon:after {
1640 cursor: pointer;
1648 cursor: pointer;
1641 content: "\00A0\25B6";
1649 content: "\00A0\25B6";
1642 font-size: 12px;
1650 font-size: 12px;
1643 color: @grey4;
1651 color: @grey4;
1644 }
1652 }
1645
1653
1646 .cs_icon_td .collapse_file_icon:after {
1654 .cs_icon_td .collapse_file_icon:after {
1647 cursor: pointer;
1655 cursor: pointer;
1648 content: "\00A0\25BC";
1656 content: "\00A0\25BC";
1649 font-size: 12px;
1657 font-size: 12px;
1650 color: @grey4;
1658 color: @grey4;
1651 }
1659 }
1652
1660
1653 /*new binary
1661 /*new binary
1654 NEW_FILENODE = 1
1662 NEW_FILENODE = 1
1655 DEL_FILENODE = 2
1663 DEL_FILENODE = 2
1656 MOD_FILENODE = 3
1664 MOD_FILENODE = 3
1657 RENAMED_FILENODE = 4
1665 RENAMED_FILENODE = 4
1658 COPIED_FILENODE = 5
1666 COPIED_FILENODE = 5
1659 CHMOD_FILENODE = 6
1667 CHMOD_FILENODE = 6
1660 BIN_FILENODE = 7
1668 BIN_FILENODE = 7
1661 */
1669 */
1662 .cs_files_expand {
1670 .cs_files_expand {
1663 font-size: @basefontsize + 5px;
1671 font-size: @basefontsize + 5px;
1664 line-height: 1.8em;
1672 line-height: 1.8em;
1665 float: right;
1673 float: right;
1666 }
1674 }
1667
1675
1668 .cs_files_expand span{
1676 .cs_files_expand span{
1669 color: @rcblue;
1677 color: @rcblue;
1670 cursor: pointer;
1678 cursor: pointer;
1671 }
1679 }
1672 .cs_files {
1680 .cs_files {
1673 clear: both;
1681 clear: both;
1674 padding-bottom: @padding;
1682 padding-bottom: @padding;
1675
1683
1676 .cur_cs {
1684 .cur_cs {
1677 margin: 10px 2px;
1685 margin: 10px 2px;
1678 font-weight: bold;
1686 font-weight: bold;
1679 }
1687 }
1680
1688
1681 .node {
1689 .node {
1682 float: left;
1690 float: left;
1683 }
1691 }
1684
1692
1685 .changes {
1693 .changes {
1686 float: right;
1694 float: right;
1687 color: white;
1695 color: white;
1688 font-size: @basefontsize - 4px;
1696 font-size: @basefontsize - 4px;
1689 margin-top: 4px;
1697 margin-top: 4px;
1690 opacity: 0.6;
1698 opacity: 0.6;
1691 filter: Alpha(opacity=60); /* IE8 and earlier */
1699 filter: Alpha(opacity=60); /* IE8 and earlier */
1692
1700
1693 .added {
1701 .added {
1694 background-color: @alert1;
1702 background-color: @alert1;
1695 float: left;
1703 float: left;
1696 text-align: center;
1704 text-align: center;
1697 }
1705 }
1698
1706
1699 .deleted {
1707 .deleted {
1700 background-color: @alert2;
1708 background-color: @alert2;
1701 float: left;
1709 float: left;
1702 text-align: center;
1710 text-align: center;
1703 }
1711 }
1704
1712
1705 .bin {
1713 .bin {
1706 background-color: @alert1;
1714 background-color: @alert1;
1707 text-align: center;
1715 text-align: center;
1708 }
1716 }
1709
1717
1710 /*new binary*/
1718 /*new binary*/
1711 .bin.bin1 {
1719 .bin.bin1 {
1712 background-color: @alert1;
1720 background-color: @alert1;
1713 text-align: center;
1721 text-align: center;
1714 }
1722 }
1715
1723
1716 /*deleted binary*/
1724 /*deleted binary*/
1717 .bin.bin2 {
1725 .bin.bin2 {
1718 background-color: @alert2;
1726 background-color: @alert2;
1719 text-align: center;
1727 text-align: center;
1720 }
1728 }
1721
1729
1722 /*mod binary*/
1730 /*mod binary*/
1723 .bin.bin3 {
1731 .bin.bin3 {
1724 background-color: @grey2;
1732 background-color: @grey2;
1725 text-align: center;
1733 text-align: center;
1726 }
1734 }
1727
1735
1728 /*rename file*/
1736 /*rename file*/
1729 .bin.bin4 {
1737 .bin.bin4 {
1730 background-color: @alert4;
1738 background-color: @alert4;
1731 text-align: center;
1739 text-align: center;
1732 }
1740 }
1733
1741
1734 /*copied file*/
1742 /*copied file*/
1735 .bin.bin5 {
1743 .bin.bin5 {
1736 background-color: @alert4;
1744 background-color: @alert4;
1737 text-align: center;
1745 text-align: center;
1738 }
1746 }
1739
1747
1740 /*chmod file*/
1748 /*chmod file*/
1741 .bin.bin6 {
1749 .bin.bin6 {
1742 background-color: @grey2;
1750 background-color: @grey2;
1743 text-align: center;
1751 text-align: center;
1744 }
1752 }
1745 }
1753 }
1746 }
1754 }
1747
1755
1748 .cs_files .cs_added, .cs_files .cs_A,
1756 .cs_files .cs_added, .cs_files .cs_A,
1749 .cs_files .cs_added, .cs_files .cs_M,
1757 .cs_files .cs_added, .cs_files .cs_M,
1750 .cs_files .cs_added, .cs_files .cs_D {
1758 .cs_files .cs_added, .cs_files .cs_D {
1751 height: 16px;
1759 height: 16px;
1752 padding-right: 10px;
1760 padding-right: 10px;
1753 margin-top: 7px;
1761 margin-top: 7px;
1754 text-align: left;
1762 text-align: left;
1755 }
1763 }
1756
1764
1757 .cs_icon_td {
1765 .cs_icon_td {
1758 min-width: 16px;
1766 min-width: 16px;
1759 width: 16px;
1767 width: 16px;
1760 }
1768 }
1761
1769
1762 .pull-request-merge {
1770 .pull-request-merge {
1763 border: 1px solid @grey5;
1771 border: 1px solid @grey5;
1764 padding: 10px 0px 20px;
1772 padding: 10px 0px 20px;
1765 margin-top: 10px;
1773 margin-top: 10px;
1766 margin-bottom: 20px;
1774 margin-bottom: 20px;
1767 }
1775 }
1768
1776
1769 .pull-request-merge ul {
1777 .pull-request-merge ul {
1770 padding: 0px 0px;
1778 padding: 0px 0px;
1771 }
1779 }
1772
1780
1773 .pull-request-merge li {
1781 .pull-request-merge li {
1774 list-style-type: none;
1782 list-style-type: none;
1775 }
1783 }
1776
1784
1777 .pull-request-merge .pull-request-wrap {
1785 .pull-request-merge .pull-request-wrap {
1778 height: auto;
1786 height: auto;
1779 padding: 0px 0px;
1787 padding: 0px 0px;
1780 text-align: right;
1788 text-align: right;
1781 }
1789 }
1782
1790
1783 .pull-request-merge span {
1791 .pull-request-merge span {
1784 margin-right: 5px;
1792 margin-right: 5px;
1785 }
1793 }
1786
1794
1787 .pull-request-merge-actions {
1795 .pull-request-merge-actions {
1788 min-height: 30px;
1796 min-height: 30px;
1789 padding: 0px 0px;
1797 padding: 0px 0px;
1790 }
1798 }
1791
1799
1792 .pull-request-merge-info {
1800 .pull-request-merge-info {
1793 padding: 0px 5px 5px 0px;
1801 padding: 0px 5px 5px 0px;
1794 }
1802 }
1795
1803
1796 .merge-status {
1804 .merge-status {
1797 margin-right: 5px;
1805 margin-right: 5px;
1798 }
1806 }
1799
1807
1800 .merge-message {
1808 .merge-message {
1801 font-size: 1.2em
1809 font-size: 1.2em
1802 }
1810 }
1803
1811
1804 .merge-message.success i,
1812 .merge-message.success i,
1805 .merge-icon.success i {
1813 .merge-icon.success i {
1806 color:@alert1;
1814 color:@alert1;
1807 }
1815 }
1808
1816
1809 .merge-message.warning i,
1817 .merge-message.warning i,
1810 .merge-icon.warning i {
1818 .merge-icon.warning i {
1811 color: @alert3;
1819 color: @alert3;
1812 }
1820 }
1813
1821
1814 .merge-message.error i,
1822 .merge-message.error i,
1815 .merge-icon.error i {
1823 .merge-icon.error i {
1816 color:@alert2;
1824 color:@alert2;
1817 }
1825 }
1818
1826
1819 .pr-versions {
1827 .pr-versions {
1820 font-size: 1.1em;
1828 font-size: 1.1em;
1821
1829
1822 table {
1830 table {
1823 padding: 0px 5px;
1831 padding: 0px 5px;
1824 }
1832 }
1825
1833
1826 td {
1834 td {
1827 line-height: 15px;
1835 line-height: 15px;
1828 }
1836 }
1829
1837
1830 .flag_status {
1838 .flag_status {
1831 margin: 0;
1839 margin: 0;
1832 }
1840 }
1833
1841
1834 .compare-radio-button {
1842 .compare-radio-button {
1835 position: relative;
1843 position: relative;
1836 top: -3px;
1844 top: -3px;
1837 }
1845 }
1838 }
1846 }
1839
1847
1840
1848
1841 #close_pull_request {
1849 #close_pull_request {
1842 margin-right: 0px;
1850 margin-right: 0px;
1843 }
1851 }
1844
1852
1845 .empty_data {
1853 .empty_data {
1846 color: @grey4;
1854 color: @grey4;
1847 }
1855 }
1848
1856
1849 #changeset_compare_view_content {
1857 #changeset_compare_view_content {
1850 clear: both;
1858 clear: both;
1851 width: 100%;
1859 width: 100%;
1852 box-sizing: border-box;
1860 box-sizing: border-box;
1853 .border-radius(@border-radius);
1861 .border-radius(@border-radius);
1854
1862
1855 .help-block {
1863 .help-block {
1856 margin: @padding 0;
1864 margin: @padding 0;
1857 color: @text-color;
1865 color: @text-color;
1858 &.pre-formatting {
1866 &.pre-formatting {
1859 white-space: pre;
1867 white-space: pre;
1860 }
1868 }
1861 }
1869 }
1862
1870
1863 .empty_data {
1871 .empty_data {
1864 margin: @padding 0;
1872 margin: @padding 0;
1865 }
1873 }
1866
1874
1867 .alert {
1875 .alert {
1868 margin-bottom: @space;
1876 margin-bottom: @space;
1869 }
1877 }
1870 }
1878 }
1871
1879
1872 .table_disp {
1880 .table_disp {
1873 .status {
1881 .status {
1874 width: auto;
1882 width: auto;
1875
1883
1876 .flag_status {
1884 .flag_status {
1877 float: left;
1885 float: left;
1878 }
1886 }
1879 }
1887 }
1880 }
1888 }
1881
1889
1882
1890
1883 .creation_in_progress {
1891 .creation_in_progress {
1884 color: @grey4
1892 color: @grey4
1885 }
1893 }
1886
1894
1887 .status_box_menu {
1895 .status_box_menu {
1888 margin: 0;
1896 margin: 0;
1889 }
1897 }
1890
1898
1891 .notification-table{
1899 .notification-table{
1892 margin-bottom: @space;
1900 margin-bottom: @space;
1893 display: table;
1901 display: table;
1894 width: 100%;
1902 width: 100%;
1895
1903
1896 .container{
1904 .container{
1897 display: table-row;
1905 display: table-row;
1898
1906
1899 .notification-header{
1907 .notification-header{
1900 border-bottom: @border-thickness solid @border-default-color;
1908 border-bottom: @border-thickness solid @border-default-color;
1901 }
1909 }
1902
1910
1903 .notification-subject{
1911 .notification-subject{
1904 display: table-cell;
1912 display: table-cell;
1905 }
1913 }
1906 }
1914 }
1907 }
1915 }
1908
1916
1909 // Notifications
1917 // Notifications
1910 .notification-header{
1918 .notification-header{
1911 display: table;
1919 display: table;
1912 width: 100%;
1920 width: 100%;
1913 padding: floor(@basefontsize/2) 0;
1921 padding: floor(@basefontsize/2) 0;
1914 line-height: 1em;
1922 line-height: 1em;
1915
1923
1916 .desc, .delete-notifications, .read-notifications{
1924 .desc, .delete-notifications, .read-notifications{
1917 display: table-cell;
1925 display: table-cell;
1918 text-align: left;
1926 text-align: left;
1919 }
1927 }
1920
1928
1921 .desc{
1929 .desc{
1922 width: 1163px;
1930 width: 1163px;
1923 }
1931 }
1924
1932
1925 .delete-notifications, .read-notifications{
1933 .delete-notifications, .read-notifications{
1926 width: 35px;
1934 width: 35px;
1927 min-width: 35px; //fixes when only one button is displayed
1935 min-width: 35px; //fixes when only one button is displayed
1928 }
1936 }
1929 }
1937 }
1930
1938
1931 .notification-body {
1939 .notification-body {
1932 .markdown-block,
1940 .markdown-block,
1933 .rst-block {
1941 .rst-block {
1934 padding: @padding 0;
1942 padding: @padding 0;
1935 }
1943 }
1936
1944
1937 .notification-subject {
1945 .notification-subject {
1938 padding: @textmargin 0;
1946 padding: @textmargin 0;
1939 border-bottom: @border-thickness solid @border-default-color;
1947 border-bottom: @border-thickness solid @border-default-color;
1940 }
1948 }
1941 }
1949 }
1942
1950
1943
1951
1944 .notifications_buttons{
1952 .notifications_buttons{
1945 float: right;
1953 float: right;
1946 }
1954 }
1947
1955
1948 #notification-status{
1956 #notification-status{
1949 display: inline;
1957 display: inline;
1950 }
1958 }
1951
1959
1952 // Repositories
1960 // Repositories
1953
1961
1954 #summary.fields{
1962 #summary.fields{
1955 display: table;
1963 display: table;
1956
1964
1957 .field{
1965 .field{
1958 display: table-row;
1966 display: table-row;
1959
1967
1960 .label-summary{
1968 .label-summary{
1961 display: table-cell;
1969 display: table-cell;
1962 min-width: @label-summary-minwidth;
1970 min-width: @label-summary-minwidth;
1963 padding-top: @padding/2;
1971 padding-top: @padding/2;
1964 padding-bottom: @padding/2;
1972 padding-bottom: @padding/2;
1965 padding-right: @padding/2;
1973 padding-right: @padding/2;
1966 }
1974 }
1967
1975
1968 .input{
1976 .input{
1969 display: table-cell;
1977 display: table-cell;
1970 padding: @padding/2;
1978 padding: @padding/2;
1971
1979
1972 input{
1980 input{
1973 min-width: 29em;
1981 min-width: 29em;
1974 padding: @padding/4;
1982 padding: @padding/4;
1975 }
1983 }
1976 }
1984 }
1977 .statistics, .downloads{
1985 .statistics, .downloads{
1978 .disabled{
1986 .disabled{
1979 color: @grey4;
1987 color: @grey4;
1980 }
1988 }
1981 }
1989 }
1982 }
1990 }
1983 }
1991 }
1984
1992
1985 #summary{
1993 #summary{
1986 width: 70%;
1994 width: 70%;
1987 }
1995 }
1988
1996
1989
1997
1990 // Journal
1998 // Journal
1991 .journal.title {
1999 .journal.title {
1992 h5 {
2000 h5 {
1993 float: left;
2001 float: left;
1994 margin: 0;
2002 margin: 0;
1995 width: 70%;
2003 width: 70%;
1996 }
2004 }
1997
2005
1998 ul {
2006 ul {
1999 float: right;
2007 float: right;
2000 display: inline-block;
2008 display: inline-block;
2001 margin: 0;
2009 margin: 0;
2002 width: 30%;
2010 width: 30%;
2003 text-align: right;
2011 text-align: right;
2004
2012
2005 li {
2013 li {
2006 display: inline;
2014 display: inline;
2007 font-size: @journal-fontsize;
2015 font-size: @journal-fontsize;
2008 line-height: 1em;
2016 line-height: 1em;
2009
2017
2010 list-style-type: none;
2018 list-style-type: none;
2011 }
2019 }
2012 }
2020 }
2013 }
2021 }
2014
2022
2015 .filterexample {
2023 .filterexample {
2016 position: absolute;
2024 position: absolute;
2017 top: 95px;
2025 top: 95px;
2018 left: @contentpadding;
2026 left: @contentpadding;
2019 color: @rcblue;
2027 color: @rcblue;
2020 font-size: 11px;
2028 font-size: 11px;
2021 font-family: @text-regular;
2029 font-family: @text-regular;
2022 cursor: help;
2030 cursor: help;
2023
2031
2024 &:hover {
2032 &:hover {
2025 color: @rcdarkblue;
2033 color: @rcdarkblue;
2026 }
2034 }
2027
2035
2028 @media (max-width:768px) {
2036 @media (max-width:768px) {
2029 position: relative;
2037 position: relative;
2030 top: auto;
2038 top: auto;
2031 left: auto;
2039 left: auto;
2032 display: block;
2040 display: block;
2033 }
2041 }
2034 }
2042 }
2035
2043
2036
2044
2037 #journal{
2045 #journal{
2038 margin-bottom: @space;
2046 margin-bottom: @space;
2039
2047
2040 .journal_day{
2048 .journal_day{
2041 margin-bottom: @textmargin/2;
2049 margin-bottom: @textmargin/2;
2042 padding-bottom: @textmargin/2;
2050 padding-bottom: @textmargin/2;
2043 font-size: @journal-fontsize;
2051 font-size: @journal-fontsize;
2044 border-bottom: @border-thickness solid @border-default-color;
2052 border-bottom: @border-thickness solid @border-default-color;
2045 }
2053 }
2046
2054
2047 .journal_container{
2055 .journal_container{
2048 margin-bottom: @space;
2056 margin-bottom: @space;
2049
2057
2050 .journal_user{
2058 .journal_user{
2051 display: inline-block;
2059 display: inline-block;
2052 }
2060 }
2053 .journal_action_container{
2061 .journal_action_container{
2054 display: block;
2062 display: block;
2055 margin-top: @textmargin;
2063 margin-top: @textmargin;
2056
2064
2057 div{
2065 div{
2058 display: inline;
2066 display: inline;
2059 }
2067 }
2060
2068
2061 div.journal_action_params{
2069 div.journal_action_params{
2062 display: block;
2070 display: block;
2063 }
2071 }
2064
2072
2065 div.journal_repo:after{
2073 div.journal_repo:after{
2066 content: "\A";
2074 content: "\A";
2067 white-space: pre;
2075 white-space: pre;
2068 }
2076 }
2069
2077
2070 div.date{
2078 div.date{
2071 display: block;
2079 display: block;
2072 margin-bottom: @textmargin;
2080 margin-bottom: @textmargin;
2073 }
2081 }
2074 }
2082 }
2075 }
2083 }
2076 }
2084 }
2077
2085
2078 // Files
2086 // Files
2079 .edit-file-title {
2087 .edit-file-title {
2080 font-size: 16px;
2088 font-size: 16px;
2081
2089
2082 .title-heading {
2090 .title-heading {
2083 padding: 2px;
2091 padding: 2px;
2084 }
2092 }
2085 }
2093 }
2086
2094
2087 .edit-file-fieldset {
2095 .edit-file-fieldset {
2088 margin: @sidebarpadding 0;
2096 margin: @sidebarpadding 0;
2089
2097
2090 .fieldset {
2098 .fieldset {
2091 .left-label {
2099 .left-label {
2092 width: 13%;
2100 width: 13%;
2093 }
2101 }
2094 .right-content {
2102 .right-content {
2095 width: 87%;
2103 width: 87%;
2096 max-width: 100%;
2104 max-width: 100%;
2097 }
2105 }
2098 .filename-label {
2106 .filename-label {
2099 margin-top: 13px;
2107 margin-top: 13px;
2100 }
2108 }
2101 .commit-message-label {
2109 .commit-message-label {
2102 margin-top: 4px;
2110 margin-top: 4px;
2103 }
2111 }
2104 .file-upload-input {
2112 .file-upload-input {
2105 input {
2113 input {
2106 display: none;
2114 display: none;
2107 }
2115 }
2108 margin-top: 10px;
2116 margin-top: 10px;
2109 }
2117 }
2110 .file-upload-label {
2118 .file-upload-label {
2111 margin-top: 10px;
2119 margin-top: 10px;
2112 }
2120 }
2113 p {
2121 p {
2114 margin-top: 5px;
2122 margin-top: 5px;
2115 }
2123 }
2116
2124
2117 }
2125 }
2118 .custom-path-link {
2126 .custom-path-link {
2119 margin-left: 5px;
2127 margin-left: 5px;
2120 }
2128 }
2121 #commit {
2129 #commit {
2122 resize: vertical;
2130 resize: vertical;
2123 }
2131 }
2124 }
2132 }
2125
2133
2126 .delete-file-preview {
2134 .delete-file-preview {
2127 max-height: 250px;
2135 max-height: 250px;
2128 }
2136 }
2129
2137
2130 .new-file,
2138 .new-file,
2131 #filter_activate,
2139 #filter_activate,
2132 #filter_deactivate {
2140 #filter_deactivate {
2133 float: right;
2141 float: right;
2134 margin: 0 0 0 10px;
2142 margin: 0 0 0 10px;
2135 }
2143 }
2136
2144
2137 .file-upload-transaction-wrapper {
2145 .file-upload-transaction-wrapper {
2138 margin-top: 57px;
2146 margin-top: 57px;
2139 clear: both;
2147 clear: both;
2140 }
2148 }
2141
2149
2142 .file-upload-transaction-wrapper .error {
2150 .file-upload-transaction-wrapper .error {
2143 color: @color5;
2151 color: @color5;
2144 }
2152 }
2145
2153
2146 .file-upload-transaction {
2154 .file-upload-transaction {
2147 min-height: 200px;
2155 min-height: 200px;
2148 padding: 54px;
2156 padding: 54px;
2149 border: 1px solid @grey5;
2157 border: 1px solid @grey5;
2150 text-align: center;
2158 text-align: center;
2151 clear: both;
2159 clear: both;
2152 }
2160 }
2153
2161
2154 .file-upload-transaction i {
2162 .file-upload-transaction i {
2155 font-size: 48px
2163 font-size: 48px
2156 }
2164 }
2157
2165
2158 h3.files_location{
2166 h3.files_location{
2159 line-height: 2.4em;
2167 line-height: 2.4em;
2160 }
2168 }
2161
2169
2162 .browser-nav {
2170 .browser-nav {
2163 width: 100%;
2171 width: 100%;
2164 display: table;
2172 display: table;
2165 margin-bottom: 20px;
2173 margin-bottom: 20px;
2166
2174
2167 .info_box {
2175 .info_box {
2168 float: left;
2176 float: left;
2169 display: inline-table;
2177 display: inline-table;
2170 height: 2.5em;
2178 height: 2.5em;
2171
2179
2172 .browser-cur-rev, .info_box_elem {
2180 .browser-cur-rev, .info_box_elem {
2173 display: table-cell;
2181 display: table-cell;
2174 vertical-align: middle;
2182 vertical-align: middle;
2175 }
2183 }
2176
2184
2177 .drop-menu {
2185 .drop-menu {
2178 margin: 0 10px;
2186 margin: 0 10px;
2179 }
2187 }
2180
2188
2181 .info_box_elem {
2189 .info_box_elem {
2182 border-top: @border-thickness solid @grey5;
2190 border-top: @border-thickness solid @grey5;
2183 border-bottom: @border-thickness solid @grey5;
2191 border-bottom: @border-thickness solid @grey5;
2184 box-shadow: @button-shadow;
2192 box-shadow: @button-shadow;
2185
2193
2186 #at_rev, a {
2194 #at_rev, a {
2187 padding: 0.6em 0.4em;
2195 padding: 0.6em 0.4em;
2188 margin: 0;
2196 margin: 0;
2189 .box-shadow(none);
2197 .box-shadow(none);
2190 border: 0;
2198 border: 0;
2191 height: 12px;
2199 height: 12px;
2192 color: @grey2;
2200 color: @grey2;
2193 }
2201 }
2194
2202
2195 input#at_rev {
2203 input#at_rev {
2196 max-width: 50px;
2204 max-width: 50px;
2197 text-align: center;
2205 text-align: center;
2198 }
2206 }
2199
2207
2200 &.previous {
2208 &.previous {
2201 border: @border-thickness solid @grey5;
2209 border: @border-thickness solid @grey5;
2202 border-top-left-radius: @border-radius;
2210 border-top-left-radius: @border-radius;
2203 border-bottom-left-radius: @border-radius;
2211 border-bottom-left-radius: @border-radius;
2204
2212
2205 &:hover {
2213 &:hover {
2206 border-color: @grey4;
2214 border-color: @grey4;
2207 }
2215 }
2208
2216
2209 .disabled {
2217 .disabled {
2210 color: @grey5;
2218 color: @grey5;
2211 cursor: not-allowed;
2219 cursor: not-allowed;
2212 opacity: 0.5;
2220 opacity: 0.5;
2213 }
2221 }
2214 }
2222 }
2215
2223
2216 &.next {
2224 &.next {
2217 border: @border-thickness solid @grey5;
2225 border: @border-thickness solid @grey5;
2218 border-top-right-radius: @border-radius;
2226 border-top-right-radius: @border-radius;
2219 border-bottom-right-radius: @border-radius;
2227 border-bottom-right-radius: @border-radius;
2220
2228
2221 &:hover {
2229 &:hover {
2222 border-color: @grey4;
2230 border-color: @grey4;
2223 }
2231 }
2224
2232
2225 .disabled {
2233 .disabled {
2226 color: @grey5;
2234 color: @grey5;
2227 cursor: not-allowed;
2235 cursor: not-allowed;
2228 opacity: 0.5;
2236 opacity: 0.5;
2229 }
2237 }
2230 }
2238 }
2231 }
2239 }
2232
2240
2233 .browser-cur-rev {
2241 .browser-cur-rev {
2234
2242
2235 span{
2243 span{
2236 margin: 0;
2244 margin: 0;
2237 color: @rcblue;
2245 color: @rcblue;
2238 height: 12px;
2246 height: 12px;
2239 display: inline-block;
2247 display: inline-block;
2240 padding: 0.7em 1em ;
2248 padding: 0.7em 1em ;
2241 border: @border-thickness solid @rcblue;
2249 border: @border-thickness solid @rcblue;
2242 margin-right: @padding;
2250 margin-right: @padding;
2243 }
2251 }
2244 }
2252 }
2245
2253
2246 }
2254 }
2247
2255
2248 .select-index-number {
2256 .select-index-number {
2249 margin: 0 0 0 20px;
2257 margin: 0 0 0 20px;
2250 color: @grey3;
2258 color: @grey3;
2251 }
2259 }
2252
2260
2253 .search_activate {
2261 .search_activate {
2254 display: table-cell;
2262 display: table-cell;
2255 vertical-align: middle;
2263 vertical-align: middle;
2256
2264
2257 input, label{
2265 input, label{
2258 margin: 0;
2266 margin: 0;
2259 padding: 0;
2267 padding: 0;
2260 }
2268 }
2261
2269
2262 input{
2270 input{
2263 margin-left: @textmargin;
2271 margin-left: @textmargin;
2264 }
2272 }
2265
2273
2266 }
2274 }
2267 }
2275 }
2268
2276
2269 .browser-cur-rev{
2277 .browser-cur-rev{
2270 margin-bottom: @textmargin;
2278 margin-bottom: @textmargin;
2271 }
2279 }
2272
2280
2273 #node_filter_box_loading{
2281 #node_filter_box_loading{
2274 .info_text;
2282 .info_text;
2275 }
2283 }
2276
2284
2277 .browser-search {
2285 .browser-search {
2278 margin: -25px 0px 5px 0px;
2286 margin: -25px 0px 5px 0px;
2279 }
2287 }
2280
2288
2281 .files-quick-filter {
2289 .files-quick-filter {
2282 float: right;
2290 float: right;
2283 width: 180px;
2291 width: 180px;
2284 position: relative;
2292 position: relative;
2285 }
2293 }
2286
2294
2287 .files-filter-box {
2295 .files-filter-box {
2288 display: flex;
2296 display: flex;
2289 padding: 0px;
2297 padding: 0px;
2290 border-radius: 3px;
2298 border-radius: 3px;
2291 margin-bottom: 0;
2299 margin-bottom: 0;
2292
2300
2293 a {
2301 a {
2294 border: none !important;
2302 border: none !important;
2295 }
2303 }
2296
2304
2297 li {
2305 li {
2298 list-style-type: none
2306 list-style-type: none
2299 }
2307 }
2300 }
2308 }
2301
2309
2302 .files-filter-box-path {
2310 .files-filter-box-path {
2303 line-height: 33px;
2311 line-height: 33px;
2304 padding: 0;
2312 padding: 0;
2305 width: 20px;
2313 width: 20px;
2306 position: absolute;
2314 position: absolute;
2307 z-index: 11;
2315 z-index: 11;
2308 left: 5px;
2316 left: 5px;
2309 }
2317 }
2310
2318
2311 .files-filter-box-input {
2319 .files-filter-box-input {
2312 margin-right: 0;
2320 margin-right: 0;
2313
2321
2314 input {
2322 input {
2315 border: 1px solid @white;
2323 border: 1px solid @white;
2316 padding-left: 25px;
2324 padding-left: 25px;
2317 width: 145px;
2325 width: 145px;
2318
2326
2319 &:hover {
2327 &:hover {
2320 border-color: @grey6;
2328 border-color: @grey6;
2321 }
2329 }
2322
2330
2323 &:focus {
2331 &:focus {
2324 border-color: @grey5;
2332 border-color: @grey5;
2325 }
2333 }
2326 }
2334 }
2327 }
2335 }
2328
2336
2329 .browser-result{
2337 .browser-result{
2330 td a{
2338 td a{
2331 margin-left: 0.5em;
2339 margin-left: 0.5em;
2332 display: inline-block;
2340 display: inline-block;
2333
2341
2334 em {
2342 em {
2335 font-weight: @text-bold-weight;
2343 font-weight: @text-bold-weight;
2336 font-family: @text-bold;
2344 font-family: @text-bold;
2337 }
2345 }
2338 }
2346 }
2339 }
2347 }
2340
2348
2341 .browser-highlight{
2349 .browser-highlight{
2342 background-color: @grey5-alpha;
2350 background-color: @grey5-alpha;
2343 }
2351 }
2344
2352
2345
2353
2346 .edit-file-fieldset #location,
2354 .edit-file-fieldset #location,
2347 .edit-file-fieldset #filename {
2355 .edit-file-fieldset #filename {
2348 display: flex;
2356 display: flex;
2349 width: -moz-available; /* WebKit-based browsers will ignore this. */
2357 width: -moz-available; /* WebKit-based browsers will ignore this. */
2350 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2358 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2351 width: fill-available;
2359 width: fill-available;
2352 border: 0;
2360 border: 0;
2353 }
2361 }
2354
2362
2355 .path-items {
2363 .path-items {
2356 display: flex;
2364 display: flex;
2357 padding: 0;
2365 padding: 0;
2358 border: 1px solid #eeeeee;
2366 border: 1px solid #eeeeee;
2359 width: 100%;
2367 width: 100%;
2360 float: left;
2368 float: left;
2361
2369
2362 .breadcrumb-path {
2370 .breadcrumb-path {
2363 line-height: 30px;
2371 line-height: 30px;
2364 padding: 0 4px;
2372 padding: 0 4px;
2365 white-space: nowrap;
2373 white-space: nowrap;
2366 }
2374 }
2367
2375
2368 .location-path {
2376 .location-path {
2369 width: -moz-available; /* WebKit-based browsers will ignore this. */
2377 width: -moz-available; /* WebKit-based browsers will ignore this. */
2370 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2378 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2371 width: fill-available;
2379 width: fill-available;
2372
2380
2373 .file-name-input {
2381 .file-name-input {
2374 padding: 0.5em 0;
2382 padding: 0.5em 0;
2375 }
2383 }
2376
2384
2377 }
2385 }
2378
2386
2379 ul {
2387 ul {
2380 display: flex;
2388 display: flex;
2381 margin: 0;
2389 margin: 0;
2382 padding: 0;
2390 padding: 0;
2383 width: 100%;
2391 width: 100%;
2384 }
2392 }
2385
2393
2386 li {
2394 li {
2387 list-style-type: none;
2395 list-style-type: none;
2388 }
2396 }
2389
2397
2390 }
2398 }
2391
2399
2392 .editor-items {
2400 .editor-items {
2393 height: 40px;
2401 height: 40px;
2394 margin: 10px 0 -17px 10px;
2402 margin: 10px 0 -17px 10px;
2395
2403
2396 .editor-action {
2404 .editor-action {
2397 cursor: pointer;
2405 cursor: pointer;
2398 }
2406 }
2399
2407
2400 .editor-action.active {
2408 .editor-action.active {
2401 border-bottom: 2px solid #5C5C5C;
2409 border-bottom: 2px solid #5C5C5C;
2402 }
2410 }
2403
2411
2404 li {
2412 li {
2405 list-style-type: none;
2413 list-style-type: none;
2406 }
2414 }
2407 }
2415 }
2408
2416
2409 .edit-file-fieldset .message textarea {
2417 .edit-file-fieldset .message textarea {
2410 border: 1px solid #eeeeee;
2418 border: 1px solid #eeeeee;
2411 }
2419 }
2412
2420
2413 #files_data .codeblock {
2421 #files_data .codeblock {
2414 background-color: #F5F5F5;
2422 background-color: #F5F5F5;
2415 }
2423 }
2416
2424
2417 #editor_preview {
2425 #editor_preview {
2418 background: white;
2426 background: white;
2419 }
2427 }
2420
2428
2421 .show-editor {
2429 .show-editor {
2422 padding: 10px;
2430 padding: 10px;
2423 background-color: white;
2431 background-color: white;
2424
2432
2425 }
2433 }
2426
2434
2427 .show-preview {
2435 .show-preview {
2428 padding: 10px;
2436 padding: 10px;
2429 background-color: white;
2437 background-color: white;
2430 border-left: 1px solid #eeeeee;
2438 border-left: 1px solid #eeeeee;
2431 }
2439 }
2432 // quick filter
2440 // quick filter
2433 .grid-quick-filter {
2441 .grid-quick-filter {
2434 float: right;
2442 float: right;
2435 position: relative;
2443 position: relative;
2436 }
2444 }
2437
2445
2438 .grid-filter-box {
2446 .grid-filter-box {
2439 display: flex;
2447 display: flex;
2440 padding: 0px;
2448 padding: 0px;
2441 border-radius: 3px;
2449 border-radius: 3px;
2442 margin-bottom: 0;
2450 margin-bottom: 0;
2443
2451
2444 a {
2452 a {
2445 border: none !important;
2453 border: none !important;
2446 }
2454 }
2447
2455
2448 li {
2456 li {
2449 list-style-type: none
2457 list-style-type: none
2450 }
2458 }
2451 }
2459 }
2452
2460
2453 .grid-filter-box-icon {
2461 .grid-filter-box-icon {
2454 line-height: 33px;
2462 line-height: 33px;
2455 padding: 0;
2463 padding: 0;
2456 width: 20px;
2464 width: 20px;
2457 position: absolute;
2465 position: absolute;
2458 z-index: 11;
2466 z-index: 11;
2459 left: 5px;
2467 left: 5px;
2460 }
2468 }
2461
2469
2462 .grid-filter-box-input {
2470 .grid-filter-box-input {
2463 margin-right: 0;
2471 margin-right: 0;
2464
2472
2465 input {
2473 input {
2466 border: 1px solid @white;
2474 border: 1px solid @white;
2467 padding-left: 25px;
2475 padding-left: 25px;
2468 width: 145px;
2476 width: 145px;
2469
2477
2470 &:hover {
2478 &:hover {
2471 border-color: @grey6;
2479 border-color: @grey6;
2472 }
2480 }
2473
2481
2474 &:focus {
2482 &:focus {
2475 border-color: @grey5;
2483 border-color: @grey5;
2476 }
2484 }
2477 }
2485 }
2478 }
2486 }
2479
2487
2480
2488
2481
2489
2482 // Search
2490 // Search
2483
2491
2484 .search-form{
2492 .search-form{
2485 #q {
2493 #q {
2486 width: @search-form-width;
2494 width: @search-form-width;
2487 }
2495 }
2488 .fields{
2496 .fields{
2489 margin: 0 0 @space;
2497 margin: 0 0 @space;
2490 }
2498 }
2491
2499
2492 label{
2500 label{
2493 display: inline-block;
2501 display: inline-block;
2494 margin-right: @textmargin;
2502 margin-right: @textmargin;
2495 padding-top: 0.25em;
2503 padding-top: 0.25em;
2496 }
2504 }
2497
2505
2498
2506
2499 .results{
2507 .results{
2500 clear: both;
2508 clear: both;
2501 margin: 0 0 @padding;
2509 margin: 0 0 @padding;
2502 }
2510 }
2503
2511
2504 .search-tags {
2512 .search-tags {
2505 padding: 5px 0;
2513 padding: 5px 0;
2506 }
2514 }
2507 }
2515 }
2508
2516
2509 div.search-feedback-items {
2517 div.search-feedback-items {
2510 display: inline-block;
2518 display: inline-block;
2511 }
2519 }
2512
2520
2513 div.search-code-body {
2521 div.search-code-body {
2514 background-color: #ffffff; padding: 5px 0 5px 10px;
2522 background-color: #ffffff; padding: 5px 0 5px 10px;
2515 pre {
2523 pre {
2516 .match { background-color: #faffa6;}
2524 .match { background-color: #faffa6;}
2517 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2525 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2518 }
2526 }
2519 }
2527 }
2520
2528
2521 .expand_commit.search {
2529 .expand_commit.search {
2522 .show_more.open {
2530 .show_more.open {
2523 height: auto;
2531 height: auto;
2524 max-height: none;
2532 max-height: none;
2525 }
2533 }
2526 }
2534 }
2527
2535
2528 .search-results {
2536 .search-results {
2529
2537
2530 h2 {
2538 h2 {
2531 margin-bottom: 0;
2539 margin-bottom: 0;
2532 }
2540 }
2533 .codeblock {
2541 .codeblock {
2534 border: none;
2542 border: none;
2535 background: transparent;
2543 background: transparent;
2536 }
2544 }
2537
2545
2538 .codeblock-header {
2546 .codeblock-header {
2539 border: none;
2547 border: none;
2540 background: transparent;
2548 background: transparent;
2541 }
2549 }
2542
2550
2543 .code-body {
2551 .code-body {
2544 border: @border-thickness solid @grey6;
2552 border: @border-thickness solid @grey6;
2545 .border-radius(@border-radius);
2553 .border-radius(@border-radius);
2546 }
2554 }
2547
2555
2548 .td-commit {
2556 .td-commit {
2549 &:extend(pre);
2557 &:extend(pre);
2550 border-bottom: @border-thickness solid @border-default-color;
2558 border-bottom: @border-thickness solid @border-default-color;
2551 }
2559 }
2552
2560
2553 .message {
2561 .message {
2554 height: auto;
2562 height: auto;
2555 max-width: 350px;
2563 max-width: 350px;
2556 white-space: normal;
2564 white-space: normal;
2557 text-overflow: initial;
2565 text-overflow: initial;
2558 overflow: visible;
2566 overflow: visible;
2559
2567
2560 .match { background-color: #faffa6;}
2568 .match { background-color: #faffa6;}
2561 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2569 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2562 }
2570 }
2563
2571
2564 .path {
2572 .path {
2565 border-bottom: none !important;
2573 border-bottom: none !important;
2566 border-left: 1px solid @grey6 !important;
2574 border-left: 1px solid @grey6 !important;
2567 border-right: 1px solid @grey6 !important;
2575 border-right: 1px solid @grey6 !important;
2568 }
2576 }
2569 }
2577 }
2570
2578
2571 table.rctable td.td-search-results div {
2579 table.rctable td.td-search-results div {
2572 max-width: 100%;
2580 max-width: 100%;
2573 }
2581 }
2574
2582
2575 #tip-box, .tip-box{
2583 #tip-box, .tip-box{
2576 padding: @menupadding/2;
2584 padding: @menupadding/2;
2577 display: block;
2585 display: block;
2578 border: @border-thickness solid @border-highlight-color;
2586 border: @border-thickness solid @border-highlight-color;
2579 .border-radius(@border-radius);
2587 .border-radius(@border-radius);
2580 background-color: white;
2588 background-color: white;
2581 z-index: 99;
2589 z-index: 99;
2582 white-space: pre-wrap;
2590 white-space: pre-wrap;
2583 }
2591 }
2584
2592
2585 #linktt {
2593 #linktt {
2586 width: 79px;
2594 width: 79px;
2587 }
2595 }
2588
2596
2589 #help_kb .modal-content{
2597 #help_kb .modal-content{
2590 max-width: 750px;
2598 max-width: 750px;
2591 margin: 10% auto;
2599 margin: 10% auto;
2592
2600
2593 table{
2601 table{
2594 td,th{
2602 td,th{
2595 border-bottom: none;
2603 border-bottom: none;
2596 line-height: 2.5em;
2604 line-height: 2.5em;
2597 }
2605 }
2598 th{
2606 th{
2599 padding-bottom: @textmargin/2;
2607 padding-bottom: @textmargin/2;
2600 }
2608 }
2601 td.keys{
2609 td.keys{
2602 text-align: center;
2610 text-align: center;
2603 }
2611 }
2604 }
2612 }
2605
2613
2606 .block-left{
2614 .block-left{
2607 width: 45%;
2615 width: 45%;
2608 margin-right: 5%;
2616 margin-right: 5%;
2609 }
2617 }
2610 .modal-footer{
2618 .modal-footer{
2611 clear: both;
2619 clear: both;
2612 }
2620 }
2613 .key.tag{
2621 .key.tag{
2614 padding: 0.5em;
2622 padding: 0.5em;
2615 background-color: @rcblue;
2623 background-color: @rcblue;
2616 color: white;
2624 color: white;
2617 border-color: @rcblue;
2625 border-color: @rcblue;
2618 .box-shadow(none);
2626 .box-shadow(none);
2619 }
2627 }
2620 }
2628 }
2621
2629
2622
2630
2623
2631
2624 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2632 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2625
2633
2626 @import 'statistics-graph';
2634 @import 'statistics-graph';
2627 @import 'tables';
2635 @import 'tables';
2628 @import 'forms';
2636 @import 'forms';
2629 @import 'diff';
2637 @import 'diff';
2630 @import 'summary';
2638 @import 'summary';
2631 @import 'navigation';
2639 @import 'navigation';
2632
2640
2633 //--- SHOW/HIDE SECTIONS --//
2641 //--- SHOW/HIDE SECTIONS --//
2634
2642
2635 .btn-collapse {
2643 .btn-collapse {
2636 float: right;
2644 float: right;
2637 text-align: right;
2645 text-align: right;
2638 font-family: @text-light;
2646 font-family: @text-light;
2639 font-size: @basefontsize;
2647 font-size: @basefontsize;
2640 cursor: pointer;
2648 cursor: pointer;
2641 border: none;
2649 border: none;
2642 color: @rcblue;
2650 color: @rcblue;
2643 }
2651 }
2644
2652
2645 table.rctable,
2653 table.rctable,
2646 table.dataTable {
2654 table.dataTable {
2647 .btn-collapse {
2655 .btn-collapse {
2648 float: right;
2656 float: right;
2649 text-align: right;
2657 text-align: right;
2650 }
2658 }
2651 }
2659 }
2652
2660
2653 table.rctable {
2661 table.rctable {
2654 &.permissions {
2662 &.permissions {
2655
2663
2656 th.td-owner {
2664 th.td-owner {
2657 padding: 0;
2665 padding: 0;
2658 }
2666 }
2659
2667
2660 th {
2668 th {
2661 font-weight: normal;
2669 font-weight: normal;
2662 padding: 0 5px;
2670 padding: 0 5px;
2663 }
2671 }
2664
2672
2665 }
2673 }
2666 }
2674 }
2667
2675
2668
2676
2669 // TODO: johbo: Fix for IE10, this avoids that we see a border
2677 // TODO: johbo: Fix for IE10, this avoids that we see a border
2670 // and padding around checkboxes and radio boxes. Move to the right place,
2678 // and padding around checkboxes and radio boxes. Move to the right place,
2671 // or better: Remove this once we did the form refactoring.
2679 // or better: Remove this once we did the form refactoring.
2672 input[type=checkbox],
2680 input[type=checkbox],
2673 input[type=radio] {
2681 input[type=radio] {
2674 padding: 0;
2682 padding: 0;
2675 border: none;
2683 border: none;
2676 }
2684 }
2677
2685
2678 .toggle-ajax-spinner{
2686 .toggle-ajax-spinner{
2679 height: 16px;
2687 height: 16px;
2680 width: 16px;
2688 width: 16px;
2681 }
2689 }
2682
2690
2683
2691
2684 .markup-form .clearfix {
2692 .markup-form .clearfix {
2685 .border-radius(@border-radius);
2693 .border-radius(@border-radius);
2686 margin: 0px;
2694 margin: 0px;
2687 }
2695 }
2688
2696
2689 .markup-form-area {
2697 .markup-form-area {
2690 padding: 8px 12px;
2698 padding: 8px 12px;
2691 border: 1px solid @grey4;
2699 border: 1px solid @grey4;
2692 .border-radius(@border-radius);
2700 .border-radius(@border-radius);
2693 }
2701 }
2694
2702
2695 .markup-form-area-header .nav-links {
2703 .markup-form-area-header .nav-links {
2696 display: flex;
2704 display: flex;
2697 flex-flow: row wrap;
2705 flex-flow: row wrap;
2698 -webkit-flex-flow: row wrap;
2706 -webkit-flex-flow: row wrap;
2699 width: 100%;
2707 width: 100%;
2700 }
2708 }
2701
2709
2702 .markup-form-area-footer {
2710 .markup-form-area-footer {
2703 display: flex;
2711 display: flex;
2704 }
2712 }
2705
2713
2706 .markup-form-area-footer .toolbar {
2714 .markup-form-area-footer .toolbar {
2707
2715
2708 }
2716 }
2709
2717
2710 // markup Form
2718 // markup Form
2711 div.markup-form {
2719 div.markup-form {
2712 margin-top: 20px;
2720 margin-top: 20px;
2713 }
2721 }
2714
2722
2715 .markup-form strong {
2723 .markup-form strong {
2716 display: block;
2724 display: block;
2717 margin-bottom: 15px;
2725 margin-bottom: 15px;
2718 }
2726 }
2719
2727
2720 .markup-form textarea {
2728 .markup-form textarea {
2721 width: 100%;
2729 width: 100%;
2722 height: 100px;
2730 height: 100px;
2723 font-family: @text-monospace;
2731 font-family: @text-monospace;
2724 }
2732 }
2725
2733
2726 form.markup-form {
2734 form.markup-form {
2727 margin-top: 10px;
2735 margin-top: 10px;
2728 margin-left: 10px;
2736 margin-left: 10px;
2729 }
2737 }
2730
2738
2731 .markup-form .comment-block-ta,
2739 .markup-form .comment-block-ta,
2732 .markup-form .preview-box {
2740 .markup-form .preview-box {
2733 .border-radius(@border-radius);
2741 .border-radius(@border-radius);
2734 .box-sizing(border-box);
2742 .box-sizing(border-box);
2735 background-color: white;
2743 background-color: white;
2736 }
2744 }
2737
2745
2738 .markup-form .preview-box.unloaded {
2746 .markup-form .preview-box.unloaded {
2739 height: 50px;
2747 height: 50px;
2740 text-align: center;
2748 text-align: center;
2741 padding: 20px;
2749 padding: 20px;
2742 background-color: white;
2750 background-color: white;
2743 }
2751 }
2744
2752
2745
2753
2746 .dropzone-wrapper {
2754 .dropzone-wrapper {
2747 border: 1px solid @grey5;
2755 border: 1px solid @grey5;
2748 padding: 20px;
2756 padding: 20px;
2749 }
2757 }
2750
2758
2751 .dropzone,
2759 .dropzone,
2752 .dropzone-pure {
2760 .dropzone-pure {
2753 border: 2px dashed @grey5;
2761 border: 2px dashed @grey5;
2754 border-radius: 5px;
2762 border-radius: 5px;
2755 background: white;
2763 background: white;
2756 min-height: 200px;
2764 min-height: 200px;
2757 padding: 54px;
2765 padding: 54px;
2758
2766
2759 .dz-message {
2767 .dz-message {
2760 font-weight: 700;
2768 font-weight: 700;
2761 text-align: center;
2769 text-align: center;
2762 margin: 2em 0;
2770 margin: 2em 0;
2763 }
2771 }
2764
2772
2765 }
2773 }
2766
2774
2767 .dz-preview {
2775 .dz-preview {
2768 margin: 10px 0 !important;
2776 margin: 10px 0 !important;
2769 position: relative;
2777 position: relative;
2770 vertical-align: top;
2778 vertical-align: top;
2771 padding: 10px;
2779 padding: 10px;
2772 border-bottom: 1px solid @grey5;
2780 border-bottom: 1px solid @grey5;
2773 }
2781 }
2774
2782
2775 .dz-filename {
2783 .dz-filename {
2776 font-weight: 700;
2784 font-weight: 700;
2777 float:left;
2785 float:left;
2778 }
2786 }
2779
2787
2780 .dz-sending {
2788 .dz-sending {
2781 float: right;
2789 float: right;
2782 }
2790 }
2783
2791
2784 .dz-response {
2792 .dz-response {
2785 clear:both
2793 clear:both
2786 }
2794 }
2787
2795
2788 .dz-filename-size {
2796 .dz-filename-size {
2789 float:right
2797 float:right
2790 }
2798 }
2791
2799
2792 .dz-error-message {
2800 .dz-error-message {
2793 color: @alert2;
2801 color: @alert2;
2794 padding-top: 10px;
2802 padding-top: 10px;
2795 clear: both;
2803 clear: both;
2796 }
2804 }
@@ -1,90 +1,93 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <div class="panel panel-default">
3 <div class="panel panel-default">
4 <div class="panel-body">
4 <div class="panel-body">
5 %if c.closed:
5 %if c.closed:
6 ${h.checkbox('show_closed',checked="checked", label=_('Show Closed Pull Requests'))}
6 ${h.checkbox('show_closed',checked="checked", label=_('Show Closed Pull Requests'))}
7 %else:
7 %else:
8 ${h.checkbox('show_closed',label=_('Show Closed Pull Requests'))}
8 ${h.checkbox('show_closed',label=_('Show Closed Pull Requests'))}
9 %endif
9 %endif
10 </div>
10 </div>
11 </div>
11 </div>
12
12
13 <div class="panel panel-default">
13 <div class="panel panel-default">
14 <div class="panel-heading">
14 <div class="panel-heading">
15 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
15 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
16 </div>
16 </div>
17 <div class="panel-body panel-body-min-height">
17 <div class="panel-body panel-body-min-height">
18 <table id="pull_request_list_table" class="display"></table>
18 <table id="pull_request_list_table" class="display"></table>
19 </div>
19 </div>
20 </div>
20 </div>
21
21
22 <script type="text/javascript">
22 <script type="text/javascript">
23 $(document).ready(function() {
23 $(document).ready(function() {
24
24
25 $('#show_closed').on('click', function(e){
25 $('#show_closed').on('click', function(e){
26 if($(this).is(":checked")){
26 if($(this).is(":checked")){
27 window.location = "${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}";
27 window.location = "${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}";
28 }
28 }
29 else{
29 else{
30 window.location = "${h.route_path('my_account_pullrequests')}";
30 window.location = "${h.route_path('my_account_pullrequests')}";
31 }
31 }
32 });
32 });
33
33
34 var $pullRequestListTable = $('#pull_request_list_table');
34 var $pullRequestListTable = $('#pull_request_list_table');
35
35
36 // participating object list
36 // participating object list
37 $pullRequestListTable.DataTable({
37 $pullRequestListTable.DataTable({
38 processing: true,
38 processing: true,
39 serverSide: true,
39 serverSide: true,
40 ajax: {
40 ajax: {
41 "url": "${h.route_path('my_account_pullrequests_data')}",
41 "url": "${h.route_path('my_account_pullrequests_data')}",
42 "data": function (d) {
42 "data": function (d) {
43 d.closed = "${c.closed}";
43 d.closed = "${c.closed}";
44 }
44 }
45 },
45 },
46 dom: 'rtp',
46 dom: 'rtp',
47 pageLength: ${c.visual.dashboard_items},
47 pageLength: ${c.visual.dashboard_items},
48 order: [[ 2, "desc" ]],
48 order: [[ 2, "desc" ]],
49 columns: [
49 columns: [
50 { data: {"_": "status",
50 { data: {"_": "status",
51 "sort": "status"}, title: "", className: "td-status", orderable: false},
51 "sort": "status"}, title: "", className: "td-status", orderable: false},
52 { data: {"_": "target_repo",
52 { data: {"_": "target_repo",
53 "sort": "target_repo"}, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false},
53 "sort": "target_repo"}, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false},
54 { data: {"_": "name",
54 { data: {"_": "name",
55 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
55 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
56 { data: {"_": "author",
56 { data: {"_": "author",
57 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
57 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
58 { data: {"_": "title",
58 { data: {"_": "title",
59 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
59 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
60 { data: {"_": "comments",
60 { data: {"_": "comments",
61 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
61 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
62 { data: {"_": "updated_on",
62 { data: {"_": "updated_on",
63 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
63 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
64 ],
64 ],
65 language: {
65 language: {
66 paginate: DEFAULT_GRID_PAGINATION,
66 paginate: DEFAULT_GRID_PAGINATION,
67 sProcessing: _gettext('loading...'),
67 sProcessing: _gettext('loading...'),
68 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
68 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
69 },
69 },
70 "drawCallback": function( settings, json ) {
70 "drawCallback": function( settings, json ) {
71 timeagoActivate();
71 timeagoActivate();
72 },
72 },
73 "createdRow": function ( row, data, index ) {
73 "createdRow": function ( row, data, index ) {
74 if (data['closed']) {
74 if (data['closed']) {
75 $(row).addClass('closed');
75 $(row).addClass('closed');
76 }
76 }
77 if (data['owned']) {
77 if (data['owned']) {
78 $(row).addClass('owned');
78 $(row).addClass('owned');
79 }
79 }
80 if (data['state'] !== 'created') {
81 $(row).addClass('state-' + data['state']);
82 }
80 }
83 }
81 });
84 });
82 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
85 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
83 $pullRequestListTable.css('opacity', 1);
86 $pullRequestListTable.css('opacity', 1);
84 });
87 });
85
88
86 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
89 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
87 $pullRequestListTable.css('opacity', 0.3);
90 $pullRequestListTable.css('opacity', 0.3);
88 });
91 });
89 });
92 });
90 </script>
93 </script>
@@ -1,117 +1,120 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Pull Requests') % c.repo_name}
4 ${_('%s Pull Requests') % c.repo_name}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()"></%def>
10 <%def name="breadcrumbs_links()"></%def>
11
11
12 <%def name="menu_bar_nav()">
12 <%def name="menu_bar_nav()">
13 ${self.menu_items(active='repositories')}
13 ${self.menu_items(active='repositories')}
14 </%def>
14 </%def>
15
15
16
16
17 <%def name="menu_bar_subnav()">
17 <%def name="menu_bar_subnav()">
18 ${self.repo_menu(active='showpullrequest')}
18 ${self.repo_menu(active='showpullrequest')}
19 </%def>
19 </%def>
20
20
21
21
22 <%def name="main()">
22 <%def name="main()">
23
23
24 <div class="box">
24 <div class="box">
25 <div class="title">
25 <div class="title">
26 <ul class="button-links">
26 <ul class="button-links">
27 <li class="btn ${('active' if c.active=='open' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
27 <li class="btn ${('active' if c.active=='open' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
28 <li class="btn ${('active' if c.active=='my' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
28 <li class="btn ${('active' if c.active=='my' else '')}"><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 ${('active' if c.active=='awaiting' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
29 <li class="btn ${('active' if c.active=='awaiting' else '')}"><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 ${('active' if c.active=='awaiting_my' else '')}"><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>
30 <li class="btn ${('active' if c.active=='awaiting_my' else '')}"><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 ${('active' if c.active=='closed' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
31 <li class="btn ${('active' if c.active=='closed' else '')}"><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 ${('active' if c.active=='source' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
32 <li class="btn ${('active' if c.active=='source' else '')}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 </ul>
33 </ul>
34
34
35 <ul class="links">
35 <ul class="links">
36 % if c.rhodecode_user.username != h.DEFAULT_USER:
36 % if c.rhodecode_user.username != h.DEFAULT_USER:
37 <li>
37 <li>
38 <span>
38 <span>
39 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
39 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
40 ${_('Open new Pull Request')}
40 ${_('Open new Pull Request')}
41 </a>
41 </a>
42 </span>
42 </span>
43 </li>
43 </li>
44 % endif
44 % endif
45 </ul>
45 </ul>
46
46
47 </div>
47 </div>
48
48
49 <div class="main-content-full-width">
49 <div class="main-content-full-width">
50 <table id="pull_request_list_table" class="display"></table>
50 <table id="pull_request_list_table" class="display"></table>
51 </div>
51 </div>
52
52
53 </div>
53 </div>
54
54
55 <script type="text/javascript">
55 <script type="text/javascript">
56 $(document).ready(function() {
56 $(document).ready(function() {
57
57
58 var $pullRequestListTable = $('#pull_request_list_table');
58 var $pullRequestListTable = $('#pull_request_list_table');
59
59
60 // object list
60 // object list
61 $pullRequestListTable.DataTable({
61 $pullRequestListTable.DataTable({
62 processing: true,
62 processing: true,
63 serverSide: true,
63 serverSide: true,
64 ajax: {
64 ajax: {
65 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
65 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
66 "data": function (d) {
66 "data": function (d) {
67 d.source = "${c.source}";
67 d.source = "${c.source}";
68 d.closed = "${c.closed}";
68 d.closed = "${c.closed}";
69 d.my = "${c.my}";
69 d.my = "${c.my}";
70 d.awaiting_review = "${c.awaiting_review}";
70 d.awaiting_review = "${c.awaiting_review}";
71 d.awaiting_my_review = "${c.awaiting_my_review}";
71 d.awaiting_my_review = "${c.awaiting_my_review}";
72 }
72 }
73 },
73 },
74 dom: 'rtp',
74 dom: 'rtp',
75 pageLength: ${c.visual.dashboard_items},
75 pageLength: ${c.visual.dashboard_items},
76 order: [[ 1, "desc" ]],
76 order: [[ 1, "desc" ]],
77 columns: [
77 columns: [
78 { data: {"_": "status",
78 { data: {"_": "status",
79 "sort": "status"}, title: "", className: "td-status", orderable: false},
79 "sort": "status"}, title: "", className: "td-status", orderable: false},
80 { data: {"_": "name",
80 { data: {"_": "name",
81 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
81 "sort": "name_raw"}, title: "${_('Name')}", className: "td-componentname", "type": "num" },
82 { data: {"_": "author",
82 { data: {"_": "author",
83 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
83 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
84 { data: {"_": "title",
84 { data: {"_": "title",
85 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
85 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
86 { data: {"_": "comments",
86 { data: {"_": "comments",
87 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
87 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
88 { data: {"_": "updated_on",
88 { data: {"_": "updated_on",
89 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
89 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
90 ],
90 ],
91 language: {
91 language: {
92 paginate: DEFAULT_GRID_PAGINATION,
92 paginate: DEFAULT_GRID_PAGINATION,
93 sProcessing: _gettext('loading...'),
93 sProcessing: _gettext('loading...'),
94 emptyTable: _gettext("No pull requests available yet.")
94 emptyTable: _gettext("No pull requests available yet.")
95 },
95 },
96 "drawCallback": function( settings, json ) {
96 "drawCallback": function( settings, json ) {
97 timeagoActivate();
97 timeagoActivate();
98 },
98 },
99 "createdRow": function ( row, data, index ) {
99 "createdRow": function ( row, data, index ) {
100 if (data['closed']) {
100 if (data['closed']) {
101 $(row).addClass('closed');
101 $(row).addClass('closed');
102 }
103 if (data['state'] !== 'created') {
104 $(row).addClass('state-' + data['state']);
102 }
105 }
103 }
106 }
104 });
107 });
105
108
106 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
109 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
107 $pullRequestListTable.css('opacity', 1);
110 $pullRequestListTable.css('opacity', 1);
108 });
111 });
109
112
110 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
113 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
111 $pullRequestListTable.css('opacity', 0.3);
114 $pullRequestListTable.css('opacity', 0.3);
112 });
115 });
113
116
114 });
117 });
115
118
116 </script>
119 </script>
117 </%def>
120 </%def>
General Comments 0
You need to be logged in to leave comments. Login now