##// END OF EJS Templates
config: Use hooks protocol and direc calls seting from vcs settings module.
Martin Bornhold -
r590:cb08eb7b default
parent child Browse files
Show More
@@ -1,441 +1,442 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 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 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import logging
27 import logging
28 import importlib
28 import importlib
29 from functools import wraps
29 from functools import wraps
30
30
31 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
31 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 from webob.exc import (
32 from webob.exc import (
33 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
33 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34
34
35 import rhodecode
35 import rhodecode
36 from rhodecode.authentication.base import authenticate, VCS_TYPE
36 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
37 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
38 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
39 from rhodecode.lib.exceptions import (
39 from rhodecode.lib.exceptions import (
40 HTTPLockedRC, HTTPRequirementError, UserCreationError,
40 HTTPLockedRC, HTTPRequirementError, UserCreationError,
41 NotAllowedToCreateUserError)
41 NotAllowedToCreateUserError)
42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.middleware import appenlight
43 from rhodecode.lib.middleware import appenlight
44 from rhodecode.lib.middleware.utils import scm_app
44 from rhodecode.lib.middleware.utils import scm_app
45 from rhodecode.lib.utils import (
45 from rhodecode.lib.utils import (
46 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
46 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
47 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
47 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
48 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.model import meta
49 from rhodecode.model import meta
49 from rhodecode.model.db import User, Repository
50 from rhodecode.model.db import User, Repository
50 from rhodecode.model.scm import ScmModel
51 from rhodecode.model.scm import ScmModel
51 from rhodecode.model.settings import SettingsModel
52 from rhodecode.model.settings import SettingsModel
52
53
53 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
54
55
55
56
56 def initialize_generator(factory):
57 def initialize_generator(factory):
57 """
58 """
58 Initializes the returned generator by draining its first element.
59 Initializes the returned generator by draining its first element.
59
60
60 This can be used to give a generator an initializer, which is the code
61 This can be used to give a generator an initializer, which is the code
61 up to the first yield statement. This decorator enforces that the first
62 up to the first yield statement. This decorator enforces that the first
62 produced element has the value ``"__init__"`` to make its special
63 produced element has the value ``"__init__"`` to make its special
63 purpose very explicit in the using code.
64 purpose very explicit in the using code.
64 """
65 """
65
66
66 @wraps(factory)
67 @wraps(factory)
67 def wrapper(*args, **kwargs):
68 def wrapper(*args, **kwargs):
68 gen = factory(*args, **kwargs)
69 gen = factory(*args, **kwargs)
69 try:
70 try:
70 init = gen.next()
71 init = gen.next()
71 except StopIteration:
72 except StopIteration:
72 raise ValueError('Generator must yield at least one element.')
73 raise ValueError('Generator must yield at least one element.')
73 if init != "__init__":
74 if init != "__init__":
74 raise ValueError('First yielded element must be "__init__".')
75 raise ValueError('First yielded element must be "__init__".')
75 return gen
76 return gen
76 return wrapper
77 return wrapper
77
78
78
79
79 class SimpleVCS(object):
80 class SimpleVCS(object):
80 """Common functionality for SCM HTTP handlers."""
81 """Common functionality for SCM HTTP handlers."""
81
82
82 SCM = 'unknown'
83 SCM = 'unknown'
83
84
84 def __init__(self, application, config):
85 def __init__(self, application, config):
85 self.application = application
86 self.application = application
86 self.config = config
87 self.config = config
87 # base path of repo locations
88 # base path of repo locations
88 self.basepath = get_rhodecode_base_path()
89 self.basepath = get_rhodecode_base_path()
89 # authenticate this VCS request using authfunc
90 # authenticate this VCS request using authfunc
90 auth_ret_code_detection = \
91 auth_ret_code_detection = \
91 str2bool(self.config.get('auth_ret_code_detection', False))
92 str2bool(self.config.get('auth_ret_code_detection', False))
92 self.authenticate = BasicAuth('', authenticate,
93 self.authenticate = BasicAuth('', authenticate,
93 config.get('auth_ret_code'),
94 config.get('auth_ret_code'),
94 auth_ret_code_detection)
95 auth_ret_code_detection)
95 self.ip_addr = '0.0.0.0'
96 self.ip_addr = '0.0.0.0'
96
97
97 @property
98 @property
98 def scm_app(self):
99 def scm_app(self):
99 custom_implementation = self.config.get('vcs.scm_app_implementation')
100 custom_implementation = self.config.get('vcs.scm_app_implementation')
100 if custom_implementation:
101 if custom_implementation:
101 log.info(
102 log.info(
102 "Using custom implementation of scm_app: %s",
103 "Using custom implementation of scm_app: %s",
103 custom_implementation)
104 custom_implementation)
104 scm_app_impl = importlib.import_module(custom_implementation)
105 scm_app_impl = importlib.import_module(custom_implementation)
105 else:
106 else:
106 scm_app_impl = scm_app
107 scm_app_impl = scm_app
107 return scm_app_impl
108 return scm_app_impl
108
109
109 def _get_by_id(self, repo_name):
110 def _get_by_id(self, repo_name):
110 """
111 """
111 Gets a special pattern _<ID> from clone url and tries to replace it
112 Gets a special pattern _<ID> from clone url and tries to replace it
112 with a repository_name for support of _<ID> non changable urls
113 with a repository_name for support of _<ID> non changable urls
113
114
114 :param repo_name:
115 :param repo_name:
115 """
116 """
116
117
117 data = repo_name.split('/')
118 data = repo_name.split('/')
118 if len(data) >= 2:
119 if len(data) >= 2:
119 from rhodecode.model.repo import RepoModel
120 from rhodecode.model.repo import RepoModel
120 by_id_match = RepoModel().get_repo_by_id(repo_name)
121 by_id_match = RepoModel().get_repo_by_id(repo_name)
121 if by_id_match:
122 if by_id_match:
122 data[1] = by_id_match.repo_name
123 data[1] = by_id_match.repo_name
123
124
124 return safe_str('/'.join(data))
125 return safe_str('/'.join(data))
125
126
126 def _invalidate_cache(self, repo_name):
127 def _invalidate_cache(self, repo_name):
127 """
128 """
128 Set's cache for this repository for invalidation on next access
129 Set's cache for this repository for invalidation on next access
129
130
130 :param repo_name: full repo name, also a cache key
131 :param repo_name: full repo name, also a cache key
131 """
132 """
132 ScmModel().mark_for_invalidation(repo_name)
133 ScmModel().mark_for_invalidation(repo_name)
133
134
134 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
135 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
135 db_repo = Repository.get_by_repo_name(repo_name)
136 db_repo = Repository.get_by_repo_name(repo_name)
136 if not db_repo:
137 if not db_repo:
137 log.debug('Repository `%s` not found inside the database.',
138 log.debug('Repository `%s` not found inside the database.',
138 repo_name)
139 repo_name)
139 return False
140 return False
140
141
141 if db_repo.repo_type != scm_type:
142 if db_repo.repo_type != scm_type:
142 log.warning(
143 log.warning(
143 'Repository `%s` have incorrect scm_type, expected %s got %s',
144 'Repository `%s` have incorrect scm_type, expected %s got %s',
144 repo_name, db_repo.repo_type, scm_type)
145 repo_name, db_repo.repo_type, scm_type)
145 return False
146 return False
146
147
147 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
148 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
148
149
149 def valid_and_active_user(self, user):
150 def valid_and_active_user(self, user):
150 """
151 """
151 Checks if that user is not empty, and if it's actually object it checks
152 Checks if that user is not empty, and if it's actually object it checks
152 if he's active.
153 if he's active.
153
154
154 :param user: user object or None
155 :param user: user object or None
155 :return: boolean
156 :return: boolean
156 """
157 """
157 if user is None:
158 if user is None:
158 return False
159 return False
159
160
160 elif user.active:
161 elif user.active:
161 return True
162 return True
162
163
163 return False
164 return False
164
165
165 def _check_permission(self, action, user, repo_name, ip_addr=None):
166 def _check_permission(self, action, user, repo_name, ip_addr=None):
166 """
167 """
167 Checks permissions using action (push/pull) user and repository
168 Checks permissions using action (push/pull) user and repository
168 name
169 name
169
170
170 :param action: push or pull action
171 :param action: push or pull action
171 :param user: user instance
172 :param user: user instance
172 :param repo_name: repository name
173 :param repo_name: repository name
173 """
174 """
174 # check IP
175 # check IP
175 inherit = user.inherit_default_permissions
176 inherit = user.inherit_default_permissions
176 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
177 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
177 inherit_from_default=inherit)
178 inherit_from_default=inherit)
178 if ip_allowed:
179 if ip_allowed:
179 log.info('Access for IP:%s allowed', ip_addr)
180 log.info('Access for IP:%s allowed', ip_addr)
180 else:
181 else:
181 return False
182 return False
182
183
183 if action == 'push':
184 if action == 'push':
184 if not HasPermissionAnyMiddleware('repository.write',
185 if not HasPermissionAnyMiddleware('repository.write',
185 'repository.admin')(user,
186 'repository.admin')(user,
186 repo_name):
187 repo_name):
187 return False
188 return False
188
189
189 else:
190 else:
190 # any other action need at least read permission
191 # any other action need at least read permission
191 if not HasPermissionAnyMiddleware('repository.read',
192 if not HasPermissionAnyMiddleware('repository.read',
192 'repository.write',
193 'repository.write',
193 'repository.admin')(user,
194 'repository.admin')(user,
194 repo_name):
195 repo_name):
195 return False
196 return False
196
197
197 return True
198 return True
198
199
199 def _check_ssl(self, environ, start_response):
200 def _check_ssl(self, environ, start_response):
200 """
201 """
201 Checks the SSL check flag and returns False if SSL is not present
202 Checks the SSL check flag and returns False if SSL is not present
202 and required True otherwise
203 and required True otherwise
203 """
204 """
204 org_proto = environ['wsgi._org_proto']
205 org_proto = environ['wsgi._org_proto']
205 # check if we have SSL required ! if not it's a bad request !
206 # check if we have SSL required ! if not it's a bad request !
206 require_ssl = str2bool(
207 require_ssl = str2bool(
207 SettingsModel().get_ui_by_key('push_ssl').ui_value)
208 SettingsModel().get_ui_by_key('push_ssl').ui_value)
208 if require_ssl and org_proto == 'http':
209 if require_ssl and org_proto == 'http':
209 log.debug('proto is %s and SSL is required BAD REQUEST !',
210 log.debug('proto is %s and SSL is required BAD REQUEST !',
210 org_proto)
211 org_proto)
211 return False
212 return False
212 return True
213 return True
213
214
214 def __call__(self, environ, start_response):
215 def __call__(self, environ, start_response):
215 try:
216 try:
216 return self._handle_request(environ, start_response)
217 return self._handle_request(environ, start_response)
217 except Exception:
218 except Exception:
218 log.exception("Exception while handling request")
219 log.exception("Exception while handling request")
219 appenlight.track_exception(environ)
220 appenlight.track_exception(environ)
220 return HTTPInternalServerError()(environ, start_response)
221 return HTTPInternalServerError()(environ, start_response)
221 finally:
222 finally:
222 meta.Session.remove()
223 meta.Session.remove()
223
224
224 def _handle_request(self, environ, start_response):
225 def _handle_request(self, environ, start_response):
225
226
226 if not self._check_ssl(environ, start_response):
227 if not self._check_ssl(environ, start_response):
227 reason = ('SSL required, while RhodeCode was unable '
228 reason = ('SSL required, while RhodeCode was unable '
228 'to detect this as SSL request')
229 'to detect this as SSL request')
229 log.debug('User not allowed to proceed, %s', reason)
230 log.debug('User not allowed to proceed, %s', reason)
230 return HTTPNotAcceptable(reason)(environ, start_response)
231 return HTTPNotAcceptable(reason)(environ, start_response)
231
232
232 ip_addr = get_ip_addr(environ)
233 ip_addr = get_ip_addr(environ)
233 username = None
234 username = None
234
235
235 # skip passing error to error controller
236 # skip passing error to error controller
236 environ['pylons.status_code_redirect'] = True
237 environ['pylons.status_code_redirect'] = True
237
238
238 # ======================================================================
239 # ======================================================================
239 # EXTRACT REPOSITORY NAME FROM ENV
240 # EXTRACT REPOSITORY NAME FROM ENV
240 # ======================================================================
241 # ======================================================================
241 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
242 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
242 repo_name = self._get_repository_name(environ)
243 repo_name = self._get_repository_name(environ)
243 environ['REPO_NAME'] = repo_name
244 environ['REPO_NAME'] = repo_name
244 log.debug('Extracted repo name is %s', repo_name)
245 log.debug('Extracted repo name is %s', repo_name)
245
246
246 # check for type, presence in database and on filesystem
247 # check for type, presence in database and on filesystem
247 if not self.is_valid_and_existing_repo(
248 if not self.is_valid_and_existing_repo(
248 repo_name, self.basepath, self.SCM):
249 repo_name, self.basepath, self.SCM):
249 return HTTPNotFound()(environ, start_response)
250 return HTTPNotFound()(environ, start_response)
250
251
251 # ======================================================================
252 # ======================================================================
252 # GET ACTION PULL or PUSH
253 # GET ACTION PULL or PUSH
253 # ======================================================================
254 # ======================================================================
254 action = self._get_action(environ)
255 action = self._get_action(environ)
255
256
256 # ======================================================================
257 # ======================================================================
257 # CHECK ANONYMOUS PERMISSION
258 # CHECK ANONYMOUS PERMISSION
258 # ======================================================================
259 # ======================================================================
259 if action in ['pull', 'push']:
260 if action in ['pull', 'push']:
260 anonymous_user = User.get_default_user()
261 anonymous_user = User.get_default_user()
261 username = anonymous_user.username
262 username = anonymous_user.username
262 if anonymous_user.active:
263 if anonymous_user.active:
263 # ONLY check permissions if the user is activated
264 # ONLY check permissions if the user is activated
264 anonymous_perm = self._check_permission(
265 anonymous_perm = self._check_permission(
265 action, anonymous_user, repo_name, ip_addr)
266 action, anonymous_user, repo_name, ip_addr)
266 else:
267 else:
267 anonymous_perm = False
268 anonymous_perm = False
268
269
269 if not anonymous_user.active or not anonymous_perm:
270 if not anonymous_user.active or not anonymous_perm:
270 if not anonymous_user.active:
271 if not anonymous_user.active:
271 log.debug('Anonymous access is disabled, running '
272 log.debug('Anonymous access is disabled, running '
272 'authentication')
273 'authentication')
273
274
274 if not anonymous_perm:
275 if not anonymous_perm:
275 log.debug('Not enough credentials to access this '
276 log.debug('Not enough credentials to access this '
276 'repository as anonymous user')
277 'repository as anonymous user')
277
278
278 username = None
279 username = None
279 # ==============================================================
280 # ==============================================================
280 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
281 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
281 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
282 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
282 # ==============================================================
283 # ==============================================================
283
284
284 # try to auth based on environ, container auth methods
285 # try to auth based on environ, container auth methods
285 log.debug('Running PRE-AUTH for container based authentication')
286 log.debug('Running PRE-AUTH for container based authentication')
286 pre_auth = authenticate('', '', environ,VCS_TYPE)
287 pre_auth = authenticate('', '', environ,VCS_TYPE)
287 if pre_auth and pre_auth.get('username'):
288 if pre_auth and pre_auth.get('username'):
288 username = pre_auth['username']
289 username = pre_auth['username']
289 log.debug('PRE-AUTH got %s as username', username)
290 log.debug('PRE-AUTH got %s as username', username)
290
291
291 # If not authenticated by the container, running basic auth
292 # If not authenticated by the container, running basic auth
292 if not username:
293 if not username:
293 self.authenticate.realm = get_rhodecode_realm()
294 self.authenticate.realm = get_rhodecode_realm()
294
295
295 try:
296 try:
296 result = self.authenticate(environ)
297 result = self.authenticate(environ)
297 except (UserCreationError, NotAllowedToCreateUserError) as e:
298 except (UserCreationError, NotAllowedToCreateUserError) as e:
298 log.error(e)
299 log.error(e)
299 reason = safe_str(e)
300 reason = safe_str(e)
300 return HTTPNotAcceptable(reason)(environ, start_response)
301 return HTTPNotAcceptable(reason)(environ, start_response)
301
302
302 if isinstance(result, str):
303 if isinstance(result, str):
303 AUTH_TYPE.update(environ, 'basic')
304 AUTH_TYPE.update(environ, 'basic')
304 REMOTE_USER.update(environ, result)
305 REMOTE_USER.update(environ, result)
305 username = result
306 username = result
306 else:
307 else:
307 return result.wsgi_application(environ, start_response)
308 return result.wsgi_application(environ, start_response)
308
309
309 # ==============================================================
310 # ==============================================================
310 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
311 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
311 # ==============================================================
312 # ==============================================================
312 user = User.get_by_username(username)
313 user = User.get_by_username(username)
313 if not self.valid_and_active_user(user):
314 if not self.valid_and_active_user(user):
314 return HTTPForbidden()(environ, start_response)
315 return HTTPForbidden()(environ, start_response)
315 username = user.username
316 username = user.username
316 user.update_lastactivity()
317 user.update_lastactivity()
317 meta.Session().commit()
318 meta.Session().commit()
318
319
319 # check user attributes for password change flag
320 # check user attributes for password change flag
320 user_obj = user
321 user_obj = user
321 if user_obj and user_obj.username != User.DEFAULT_USER and \
322 if user_obj and user_obj.username != User.DEFAULT_USER and \
322 user_obj.user_data.get('force_password_change'):
323 user_obj.user_data.get('force_password_change'):
323 reason = 'password change required'
324 reason = 'password change required'
324 log.debug('User not allowed to authenticate, %s', reason)
325 log.debug('User not allowed to authenticate, %s', reason)
325 return HTTPNotAcceptable(reason)(environ, start_response)
326 return HTTPNotAcceptable(reason)(environ, start_response)
326
327
327 # check permissions for this repository
328 # check permissions for this repository
328 perm = self._check_permission(action, user, repo_name, ip_addr)
329 perm = self._check_permission(action, user, repo_name, ip_addr)
329 if not perm:
330 if not perm:
330 return HTTPForbidden()(environ, start_response)
331 return HTTPForbidden()(environ, start_response)
331
332
332 # extras are injected into UI object and later available
333 # extras are injected into UI object and later available
333 # in hooks executed by rhodecode
334 # in hooks executed by rhodecode
334 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
335 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
335 extras = vcs_operation_context(
336 extras = vcs_operation_context(
336 environ, repo_name=repo_name, username=username,
337 environ, repo_name=repo_name, username=username,
337 action=action, scm=self.SCM,
338 action=action, scm=self.SCM,
338 check_locking=check_locking)
339 check_locking=check_locking)
339
340
340 # ======================================================================
341 # ======================================================================
341 # REQUEST HANDLING
342 # REQUEST HANDLING
342 # ======================================================================
343 # ======================================================================
343 str_repo_name = safe_str(repo_name)
344 str_repo_name = safe_str(repo_name)
344 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
345 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
345 log.debug('Repository path is %s', repo_path)
346 log.debug('Repository path is %s', repo_path)
346
347
347 fix_PATH()
348 fix_PATH()
348
349
349 log.info(
350 log.info(
350 '%s action on %s repo "%s" by "%s" from %s',
351 '%s action on %s repo "%s" by "%s" from %s',
351 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
352 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
352 return self._generate_vcs_response(
353 return self._generate_vcs_response(
353 environ, start_response, repo_path, repo_name, extras, action)
354 environ, start_response, repo_path, repo_name, extras, action)
354
355
355 @initialize_generator
356 @initialize_generator
356 def _generate_vcs_response(
357 def _generate_vcs_response(
357 self, environ, start_response, repo_path, repo_name, extras,
358 self, environ, start_response, repo_path, repo_name, extras,
358 action):
359 action):
359 """
360 """
360 Returns a generator for the response content.
361 Returns a generator for the response content.
361
362
362 This method is implemented as a generator, so that it can trigger
363 This method is implemented as a generator, so that it can trigger
363 the cache validation after all content sent back to the client. It
364 the cache validation after all content sent back to the client. It
364 also handles the locking exceptions which will be triggered when
365 also handles the locking exceptions which will be triggered when
365 the first chunk is produced by the underlying WSGI application.
366 the first chunk is produced by the underlying WSGI application.
366 """
367 """
367 callback_daemon, extras = self._prepare_callback_daemon(extras)
368 callback_daemon, extras = self._prepare_callback_daemon(extras)
368 config = self._create_config(extras, repo_name)
369 config = self._create_config(extras, repo_name)
369 log.debug('HOOKS extras is %s', extras)
370 log.debug('HOOKS extras is %s', extras)
370 app = self._create_wsgi_app(repo_path, repo_name, config)
371 app = self._create_wsgi_app(repo_path, repo_name, config)
371
372
372 try:
373 try:
373 with callback_daemon:
374 with callback_daemon:
374 try:
375 try:
375 response = app(environ, start_response)
376 response = app(environ, start_response)
376 finally:
377 finally:
377 # This statement works together with the decorator
378 # This statement works together with the decorator
378 # "initialize_generator" above. The decorator ensures that
379 # "initialize_generator" above. The decorator ensures that
379 # we hit the first yield statement before the generator is
380 # we hit the first yield statement before the generator is
380 # returned back to the WSGI server. This is needed to
381 # returned back to the WSGI server. This is needed to
381 # ensure that the call to "app" above triggers the
382 # ensure that the call to "app" above triggers the
382 # needed callback to "start_response" before the
383 # needed callback to "start_response" before the
383 # generator is actually used.
384 # generator is actually used.
384 yield "__init__"
385 yield "__init__"
385
386
386 for chunk in response:
387 for chunk in response:
387 yield chunk
388 yield chunk
388 except Exception as exc:
389 except Exception as exc:
389 # TODO: johbo: Improve "translating" back the exception.
390 # TODO: johbo: Improve "translating" back the exception.
390 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
391 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
391 exc = HTTPLockedRC(*exc.args)
392 exc = HTTPLockedRC(*exc.args)
392 _code = rhodecode.CONFIG.get('lock_ret_code')
393 _code = rhodecode.CONFIG.get('lock_ret_code')
393 log.debug('Repository LOCKED ret code %s!', (_code,))
394 log.debug('Repository LOCKED ret code %s!', (_code,))
394 elif getattr(exc, '_vcs_kind', None) == 'requirement':
395 elif getattr(exc, '_vcs_kind', None) == 'requirement':
395 log.debug(
396 log.debug(
396 'Repository requires features unknown to this Mercurial')
397 'Repository requires features unknown to this Mercurial')
397 exc = HTTPRequirementError(*exc.args)
398 exc = HTTPRequirementError(*exc.args)
398 else:
399 else:
399 raise
400 raise
400
401
401 for chunk in exc(environ, start_response):
402 for chunk in exc(environ, start_response):
402 yield chunk
403 yield chunk
403 finally:
404 finally:
404 # invalidate cache on push
405 # invalidate cache on push
405 if action == 'push':
406 if action == 'push':
406 self._invalidate_cache(repo_name)
407 self._invalidate_cache(repo_name)
407
408
408 def _get_repository_name(self, environ):
409 def _get_repository_name(self, environ):
409 """Get repository name out of the environmnent
410 """Get repository name out of the environmnent
410
411
411 :param environ: WSGI environment
412 :param environ: WSGI environment
412 """
413 """
413 raise NotImplementedError()
414 raise NotImplementedError()
414
415
415 def _get_action(self, environ):
416 def _get_action(self, environ):
416 """Map request commands into a pull or push command.
417 """Map request commands into a pull or push command.
417
418
418 :param environ: WSGI environment
419 :param environ: WSGI environment
419 """
420 """
420 raise NotImplementedError()
421 raise NotImplementedError()
421
422
422 def _create_wsgi_app(self, repo_path, repo_name, config):
423 def _create_wsgi_app(self, repo_path, repo_name, config):
423 """Return the WSGI app that will finally handle the request."""
424 """Return the WSGI app that will finally handle the request."""
424 raise NotImplementedError()
425 raise NotImplementedError()
425
426
426 def _create_config(self, extras, repo_name):
427 def _create_config(self, extras, repo_name):
427 """Create a Pyro safe config representation."""
428 """Create a Pyro safe config representation."""
428 raise NotImplementedError()
429 raise NotImplementedError()
429
430
430 def _prepare_callback_daemon(self, extras):
431 def _prepare_callback_daemon(self, extras):
431 return prepare_callback_daemon(
432 return prepare_callback_daemon(
432 extras, protocol=self.config.get('vcs.hooks.protocol'),
433 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
433 use_direct_calls=self.config.get('vcs.hooks.direct_calls'))
434 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
434
435
435
436
436 def _should_check_locking(query_string):
437 def _should_check_locking(query_string):
437 # this is kind of hacky, but due to how mercurial handles client-server
438 # this is kind of hacky, but due to how mercurial handles client-server
438 # server see all operation on commit; bookmarks, phases and
439 # server see all operation on commit; bookmarks, phases and
439 # obsolescence marker in different transaction, we don't want to check
440 # obsolescence marker in different transaction, we don't want to check
440 # locking on those
441 # locking on those
441 return query_string not in ['cmd=listkeys']
442 return query_string not in ['cmd=listkeys']
@@ -1,84 +1,87 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 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 Internal settings for vcs-lib
22 Internal settings for vcs-lib
23 """
23 """
24
24
25 # list of default encoding used in safe_unicode/safe_str methods
25 # list of default encoding used in safe_unicode/safe_str methods
26 DEFAULT_ENCODINGS = ['utf8']
26 DEFAULT_ENCODINGS = ['utf8']
27
27
28 # Optional arguments to rev-filter, it has to be a list
28 # Optional arguments to rev-filter, it has to be a list
29 # It can also be ['--branches', '--tags']
29 # It can also be ['--branches', '--tags']
30 GIT_REV_FILTER = ['--all']
30 GIT_REV_FILTER = ['--all']
31
31
32 # Compatibility version when creating SVN repositories. None means newest.
32 # Compatibility version when creating SVN repositories. None means newest.
33 # Other available options are: pre-1.4-compatible, pre-1.5-compatible,
33 # Other available options are: pre-1.4-compatible, pre-1.5-compatible,
34 # pre-1.6-compatible, pre-1.8-compatible
34 # pre-1.6-compatible, pre-1.8-compatible
35 SVN_COMPATIBLE_VERSION = None
35 SVN_COMPATIBLE_VERSION = None
36
36
37 ALIASES = ['hg', 'git', 'svn']
37 ALIASES = ['hg', 'git', 'svn']
38
38
39 BACKENDS = {
39 BACKENDS = {
40 'hg': 'rhodecode.lib.vcs.backends.hg.MercurialRepository',
40 'hg': 'rhodecode.lib.vcs.backends.hg.MercurialRepository',
41 'git': 'rhodecode.lib.vcs.backends.git.GitRepository',
41 'git': 'rhodecode.lib.vcs.backends.git.GitRepository',
42 'svn': 'rhodecode.lib.vcs.backends.svn.SubversionRepository',
42 'svn': 'rhodecode.lib.vcs.backends.svn.SubversionRepository',
43 }
43 }
44
44
45 # TODO: Remove once controllers/files.py is adjusted
45 # TODO: Remove once controllers/files.py is adjusted
46 ARCHIVE_SPECS = {
46 ARCHIVE_SPECS = {
47 'tbz2': ('application/x-bzip2', '.tar.bz2'),
47 'tbz2': ('application/x-bzip2', '.tar.bz2'),
48 'tgz': ('application/x-gzip', '.tar.gz'),
48 'tgz': ('application/x-gzip', '.tar.gz'),
49 'zip': ('application/zip', '.zip'),
49 'zip': ('application/zip', '.zip'),
50 }
50 }
51
51
52 HOOKS_PROTOCOL = None
53 HOOKS_DIRECT_CALLS = False
54
52 PYRO_PORT = 9900
55 PYRO_PORT = 9900
53
56
54 PYRO_GIT = 'git_remote'
57 PYRO_GIT = 'git_remote'
55 PYRO_HG = 'hg_remote'
58 PYRO_HG = 'hg_remote'
56 PYRO_SVN = 'svn_remote'
59 PYRO_SVN = 'svn_remote'
57 PYRO_VCSSERVER = 'vcs_server'
60 PYRO_VCSSERVER = 'vcs_server'
58 PYRO_GIT_REMOTE_WSGI = 'git_remote_wsgi'
61 PYRO_GIT_REMOTE_WSGI = 'git_remote_wsgi'
59 PYRO_HG_REMOTE_WSGI = 'hg_remote_wsgi'
62 PYRO_HG_REMOTE_WSGI = 'hg_remote_wsgi'
60
63
61 PYRO_RECONNECT_TRIES = 15
64 PYRO_RECONNECT_TRIES = 15
62 """
65 """
63 How many retries to reconnect will be performed if the connection was lost.
66 How many retries to reconnect will be performed if the connection was lost.
64
67
65 Each try takes 2s. Doing 15 means that we will give it up to 30s for a
68 Each try takes 2s. Doing 15 means that we will give it up to 30s for a
66 connection to be re-established.
69 connection to be re-established.
67 """
70 """
68
71
69
72
70 def pyro_remote(object_id, server_and_port):
73 def pyro_remote(object_id, server_and_port):
71 return "PYRO:%s@%s" % (object_id, server_and_port)
74 return "PYRO:%s@%s" % (object_id, server_and_port)
72
75
73
76
74 def available_aliases():
77 def available_aliases():
75 """
78 """
76 Mercurial is required for the system to work, so in case vcs.backends does
79 Mercurial is required for the system to work, so in case vcs.backends does
77 not include it, we make sure it will be available internally
80 not include it, we make sure it will be available internally
78 TODO: anderson: refactor vcs.backends so it won't be necessary, VCS server
81 TODO: anderson: refactor vcs.backends so it won't be necessary, VCS server
79 should be responsible to dictate available backends.
82 should be responsible to dictate available backends.
80 """
83 """
81 aliases = ALIASES[:]
84 aliases = ALIASES[:]
82 if 'hg' not in aliases:
85 if 'hg' not in aliases:
83 aliases += ['hg']
86 aliases += ['hg']
84 return aliases
87 return aliases
@@ -1,1155 +1,1154 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 from collections import namedtuple
26 from collections import namedtuple
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pylons.i18n.translation import lazy_ugettext
32 from pylons.i18n.translation import lazy_ugettext
33
33
34 import rhodecode
35 from rhodecode.lib import helpers as h, hooks_utils, diffs
34 from rhodecode.lib import helpers as h, hooks_utils, diffs
36 from rhodecode.lib.compat import OrderedDict
35 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
36 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
38 from rhodecode.lib.markup_renderer import (
37 from rhodecode.lib.markup_renderer import (
39 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
38 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
40 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils import action_logger
41 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
40 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
42 from rhodecode.lib.vcs.backends.base import (
41 from rhodecode.lib.vcs.backends.base import (
43 Reference, MergeResponse, MergeFailureReason)
42 Reference, MergeResponse, MergeFailureReason)
43 from rhodecode.lib.vcs.conf import settings as vcs_settings
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, EmptyRepositoryError)
45 CommitDoesNotExistError, EmptyRepositoryError)
46 from rhodecode.model import BaseModel
46 from rhodecode.model import BaseModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import ChangesetCommentsModel
48 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 PullRequest, PullRequestReviewers, Notification, ChangesetStatus,
50 PullRequest, PullRequestReviewers, ChangesetStatus,
51 PullRequestVersion, ChangesetComment)
51 PullRequestVersion, ChangesetComment)
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.notification import NotificationModel, \
53 from rhodecode.model.notification import NotificationModel, \
54 EmailNotificationModel
54 EmailNotificationModel
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.settings import VcsSettingsModel
56 from rhodecode.model.settings import VcsSettingsModel
57
57
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 class PullRequestModel(BaseModel):
62 class PullRequestModel(BaseModel):
63
63
64 cls = PullRequest
64 cls = PullRequest
65
65
66 DIFF_CONTEXT = 3
66 DIFF_CONTEXT = 3
67
67
68 MERGE_STATUS_MESSAGES = {
68 MERGE_STATUS_MESSAGES = {
69 MergeFailureReason.NONE: lazy_ugettext(
69 MergeFailureReason.NONE: lazy_ugettext(
70 'This pull request can be automatically merged.'),
70 'This pull request can be automatically merged.'),
71 MergeFailureReason.UNKNOWN: lazy_ugettext(
71 MergeFailureReason.UNKNOWN: lazy_ugettext(
72 'This pull request cannot be merged because of an unhandled'
72 'This pull request cannot be merged because of an unhandled'
73 ' exception.'),
73 ' exception.'),
74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
75 'This pull request cannot be merged because of conflicts.'),
75 'This pull request cannot be merged because of conflicts.'),
76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
77 'This pull request could not be merged because push to target'
77 'This pull request could not be merged because push to target'
78 ' failed.'),
78 ' failed.'),
79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
80 'This pull request cannot be merged because the target is not a'
80 'This pull request cannot be merged because the target is not a'
81 ' head.'),
81 ' head.'),
82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
83 'This pull request cannot be merged because the source contains'
83 'This pull request cannot be merged because the source contains'
84 ' more branches than the target.'),
84 ' more branches than the target.'),
85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
86 'This pull request cannot be merged because the target has'
86 'This pull request cannot be merged because the target has'
87 ' multiple heads.'),
87 ' multiple heads.'),
88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
89 'This pull request cannot be merged because the target repository'
89 'This pull request cannot be merged because the target repository'
90 ' is locked.'),
90 ' is locked.'),
91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
92 'This pull request cannot be merged because the target or the '
92 'This pull request cannot be merged because the target or the '
93 'source reference is missing.'),
93 'source reference is missing.'),
94 }
94 }
95
95
96 def __get_pull_request(self, pull_request):
96 def __get_pull_request(self, pull_request):
97 return self._get_instance(PullRequest, pull_request)
97 return self._get_instance(PullRequest, pull_request)
98
98
99 def _check_perms(self, perms, pull_request, user, api=False):
99 def _check_perms(self, perms, pull_request, user, api=False):
100 if not api:
100 if not api:
101 return h.HasRepoPermissionAny(*perms)(
101 return h.HasRepoPermissionAny(*perms)(
102 user=user, repo_name=pull_request.target_repo.repo_name)
102 user=user, repo_name=pull_request.target_repo.repo_name)
103 else:
103 else:
104 return h.HasRepoPermissionAnyApi(*perms)(
104 return h.HasRepoPermissionAnyApi(*perms)(
105 user=user, repo_name=pull_request.target_repo.repo_name)
105 user=user, repo_name=pull_request.target_repo.repo_name)
106
106
107 def check_user_read(self, pull_request, user, api=False):
107 def check_user_read(self, pull_request, user, api=False):
108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
109 return self._check_perms(_perms, pull_request, user, api)
109 return self._check_perms(_perms, pull_request, user, api)
110
110
111 def check_user_merge(self, pull_request, user, api=False):
111 def check_user_merge(self, pull_request, user, api=False):
112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
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_update(self, pull_request, user, api=False):
115 def check_user_update(self, pull_request, user, api=False):
116 owner = user.user_id == pull_request.user_id
116 owner = user.user_id == pull_request.user_id
117 return self.check_user_merge(pull_request, user, api) or owner
117 return self.check_user_merge(pull_request, user, api) or owner
118
118
119 def check_user_change_status(self, pull_request, user, api=False):
119 def check_user_change_status(self, pull_request, user, api=False):
120 reviewer = user.user_id in [x.user_id for x in
120 reviewer = user.user_id in [x.user_id for x in
121 pull_request.reviewers]
121 pull_request.reviewers]
122 return self.check_user_update(pull_request, user, api) or reviewer
122 return self.check_user_update(pull_request, user, api) or reviewer
123
123
124 def get(self, pull_request):
124 def get(self, pull_request):
125 return self.__get_pull_request(pull_request)
125 return self.__get_pull_request(pull_request)
126
126
127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
128 opened_by=None, order_by=None,
128 opened_by=None, order_by=None,
129 order_dir='desc'):
129 order_dir='desc'):
130 repo = self._get_repo(repo_name)
130 repo = self._get_repo(repo_name)
131 q = PullRequest.query()
131 q = PullRequest.query()
132 # source or target
132 # source or target
133 if source:
133 if source:
134 q = q.filter(PullRequest.source_repo == repo)
134 q = q.filter(PullRequest.source_repo == repo)
135 else:
135 else:
136 q = q.filter(PullRequest.target_repo == repo)
136 q = q.filter(PullRequest.target_repo == repo)
137
137
138 # closed,opened
138 # closed,opened
139 if statuses:
139 if statuses:
140 q = q.filter(PullRequest.status.in_(statuses))
140 q = q.filter(PullRequest.status.in_(statuses))
141
141
142 # opened by filter
142 # opened by filter
143 if opened_by:
143 if opened_by:
144 q = q.filter(PullRequest.user_id.in_(opened_by))
144 q = q.filter(PullRequest.user_id.in_(opened_by))
145
145
146 if order_by:
146 if order_by:
147 order_map = {
147 order_map = {
148 'name_raw': PullRequest.pull_request_id,
148 'name_raw': PullRequest.pull_request_id,
149 'title': PullRequest.title,
149 'title': PullRequest.title,
150 'updated_on_raw': PullRequest.updated_on
150 'updated_on_raw': PullRequest.updated_on
151 }
151 }
152 if order_dir == 'asc':
152 if order_dir == 'asc':
153 q = q.order_by(order_map[order_by].asc())
153 q = q.order_by(order_map[order_by].asc())
154 else:
154 else:
155 q = q.order_by(order_map[order_by].desc())
155 q = q.order_by(order_map[order_by].desc())
156
156
157 return q
157 return q
158
158
159 def count_all(self, repo_name, source=False, statuses=None,
159 def count_all(self, repo_name, source=False, statuses=None,
160 opened_by=None):
160 opened_by=None):
161 """
161 """
162 Count the number of pull requests for a specific repository.
162 Count the number of pull requests for a specific repository.
163
163
164 :param repo_name: target or source repo
164 :param repo_name: target or source repo
165 :param source: boolean flag to specify if repo_name refers to source
165 :param source: boolean flag to specify if repo_name refers to source
166 :param statuses: list of pull request statuses
166 :param statuses: list of pull request statuses
167 :param opened_by: author user of the pull request
167 :param opened_by: author user of the pull request
168 :returns: int number of pull requests
168 :returns: int number of pull requests
169 """
169 """
170 q = self._prepare_get_all_query(
170 q = self._prepare_get_all_query(
171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
172
172
173 return q.count()
173 return q.count()
174
174
175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
176 offset=0, length=None, order_by=None, order_dir='desc'):
176 offset=0, length=None, order_by=None, order_dir='desc'):
177 """
177 """
178 Get all pull requests for a specific repository.
178 Get all pull requests for a specific repository.
179
179
180 :param repo_name: target or source repo
180 :param repo_name: target or source repo
181 :param source: boolean flag to specify if repo_name refers to source
181 :param source: boolean flag to specify if repo_name refers to source
182 :param statuses: list of pull request statuses
182 :param statuses: list of pull request statuses
183 :param opened_by: author user of the pull request
183 :param opened_by: author user of the pull request
184 :param offset: pagination offset
184 :param offset: pagination offset
185 :param length: length of returned list
185 :param length: length of returned list
186 :param order_by: order of the returned list
186 :param order_by: order of the returned list
187 :param order_dir: 'asc' or 'desc' ordering direction
187 :param order_dir: 'asc' or 'desc' ordering direction
188 :returns: list of pull requests
188 :returns: list of pull requests
189 """
189 """
190 q = self._prepare_get_all_query(
190 q = self._prepare_get_all_query(
191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
192 order_by=order_by, order_dir=order_dir)
192 order_by=order_by, order_dir=order_dir)
193
193
194 if length:
194 if length:
195 pull_requests = q.limit(length).offset(offset).all()
195 pull_requests = q.limit(length).offset(offset).all()
196 else:
196 else:
197 pull_requests = q.all()
197 pull_requests = q.all()
198
198
199 return pull_requests
199 return pull_requests
200
200
201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
202 opened_by=None):
202 opened_by=None):
203 """
203 """
204 Count the number of pull requests for a specific repository that are
204 Count the number of pull requests for a specific repository that are
205 awaiting review.
205 awaiting review.
206
206
207 :param repo_name: target or source repo
207 :param repo_name: target or source repo
208 :param source: boolean flag to specify if repo_name refers to source
208 :param source: boolean flag to specify if repo_name refers to source
209 :param statuses: list of pull request statuses
209 :param statuses: list of pull request statuses
210 :param opened_by: author user of the pull request
210 :param opened_by: author user of the pull request
211 :returns: int number of pull requests
211 :returns: int number of pull requests
212 """
212 """
213 pull_requests = self.get_awaiting_review(
213 pull_requests = self.get_awaiting_review(
214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
215
215
216 return len(pull_requests)
216 return len(pull_requests)
217
217
218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
219 opened_by=None, offset=0, length=None,
219 opened_by=None, offset=0, length=None,
220 order_by=None, order_dir='desc'):
220 order_by=None, order_dir='desc'):
221 """
221 """
222 Get all pull requests for a specific repository that are awaiting
222 Get all pull requests for a specific repository that are awaiting
223 review.
223 review.
224
224
225 :param repo_name: target or source repo
225 :param repo_name: target or source repo
226 :param source: boolean flag to specify if repo_name refers to source
226 :param source: boolean flag to specify if repo_name refers to source
227 :param statuses: list of pull request statuses
227 :param statuses: list of pull request statuses
228 :param opened_by: author user of the pull request
228 :param opened_by: author user of the pull request
229 :param offset: pagination offset
229 :param offset: pagination offset
230 :param length: length of returned list
230 :param length: length of returned list
231 :param order_by: order of the returned list
231 :param order_by: order of the returned list
232 :param order_dir: 'asc' or 'desc' ordering direction
232 :param order_dir: 'asc' or 'desc' ordering direction
233 :returns: list of pull requests
233 :returns: list of pull requests
234 """
234 """
235 pull_requests = self.get_all(
235 pull_requests = self.get_all(
236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
237 order_by=order_by, order_dir=order_dir)
237 order_by=order_by, order_dir=order_dir)
238
238
239 _filtered_pull_requests = []
239 _filtered_pull_requests = []
240 for pr in pull_requests:
240 for pr in pull_requests:
241 status = pr.calculated_review_status()
241 status = pr.calculated_review_status()
242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
243 ChangesetStatus.STATUS_UNDER_REVIEW]:
243 ChangesetStatus.STATUS_UNDER_REVIEW]:
244 _filtered_pull_requests.append(pr)
244 _filtered_pull_requests.append(pr)
245 if length:
245 if length:
246 return _filtered_pull_requests[offset:offset+length]
246 return _filtered_pull_requests[offset:offset+length]
247 else:
247 else:
248 return _filtered_pull_requests
248 return _filtered_pull_requests
249
249
250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
251 opened_by=None, user_id=None):
251 opened_by=None, user_id=None):
252 """
252 """
253 Count the number of pull requests for a specific repository that are
253 Count the number of pull requests for a specific repository that are
254 awaiting review from a specific user.
254 awaiting review from a specific user.
255
255
256 :param repo_name: target or source repo
256 :param repo_name: target or source repo
257 :param source: boolean flag to specify if repo_name refers to source
257 :param source: boolean flag to specify if repo_name refers to source
258 :param statuses: list of pull request statuses
258 :param statuses: list of pull request statuses
259 :param opened_by: author user of the pull request
259 :param opened_by: author user of the pull request
260 :param user_id: reviewer user of the pull request
260 :param user_id: reviewer user of the pull request
261 :returns: int number of pull requests
261 :returns: int number of pull requests
262 """
262 """
263 pull_requests = self.get_awaiting_my_review(
263 pull_requests = self.get_awaiting_my_review(
264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
265 user_id=user_id)
265 user_id=user_id)
266
266
267 return len(pull_requests)
267 return len(pull_requests)
268
268
269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
270 opened_by=None, user_id=None, offset=0,
270 opened_by=None, user_id=None, offset=0,
271 length=None, order_by=None, order_dir='desc'):
271 length=None, order_by=None, order_dir='desc'):
272 """
272 """
273 Get all pull requests for a specific repository that are awaiting
273 Get all pull requests for a specific repository that are awaiting
274 review from a specific user.
274 review from a specific user.
275
275
276 :param repo_name: target or source repo
276 :param repo_name: target or source repo
277 :param source: boolean flag to specify if repo_name refers to source
277 :param source: boolean flag to specify if repo_name refers to source
278 :param statuses: list of pull request statuses
278 :param statuses: list of pull request statuses
279 :param opened_by: author user of the pull request
279 :param opened_by: author user of the pull request
280 :param user_id: reviewer user of the pull request
280 :param user_id: reviewer user of the pull request
281 :param offset: pagination offset
281 :param offset: pagination offset
282 :param length: length of returned list
282 :param length: length of returned list
283 :param order_by: order of the returned list
283 :param order_by: order of the returned list
284 :param order_dir: 'asc' or 'desc' ordering direction
284 :param order_dir: 'asc' or 'desc' ordering direction
285 :returns: list of pull requests
285 :returns: list of pull requests
286 """
286 """
287 pull_requests = self.get_all(
287 pull_requests = self.get_all(
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 order_by=order_by, order_dir=order_dir)
289 order_by=order_by, order_dir=order_dir)
290
290
291 _my = PullRequestModel().get_not_reviewed(user_id)
291 _my = PullRequestModel().get_not_reviewed(user_id)
292 my_participation = []
292 my_participation = []
293 for pr in pull_requests:
293 for pr in pull_requests:
294 if pr in _my:
294 if pr in _my:
295 my_participation.append(pr)
295 my_participation.append(pr)
296 _filtered_pull_requests = my_participation
296 _filtered_pull_requests = my_participation
297 if length:
297 if length:
298 return _filtered_pull_requests[offset:offset+length]
298 return _filtered_pull_requests[offset:offset+length]
299 else:
299 else:
300 return _filtered_pull_requests
300 return _filtered_pull_requests
301
301
302 def get_not_reviewed(self, user_id):
302 def get_not_reviewed(self, user_id):
303 return [
303 return [
304 x.pull_request for x in PullRequestReviewers.query().filter(
304 x.pull_request for x in PullRequestReviewers.query().filter(
305 PullRequestReviewers.user_id == user_id).all()
305 PullRequestReviewers.user_id == user_id).all()
306 ]
306 ]
307
307
308 def get_versions(self, pull_request):
308 def get_versions(self, pull_request):
309 """
309 """
310 returns version of pull request sorted by ID descending
310 returns version of pull request sorted by ID descending
311 """
311 """
312 return PullRequestVersion.query()\
312 return PullRequestVersion.query()\
313 .filter(PullRequestVersion.pull_request == pull_request)\
313 .filter(PullRequestVersion.pull_request == pull_request)\
314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
315 .all()
315 .all()
316
316
317 def create(self, created_by, source_repo, source_ref, target_repo,
317 def create(self, created_by, source_repo, source_ref, target_repo,
318 target_ref, revisions, reviewers, title, description=None):
318 target_ref, revisions, reviewers, title, description=None):
319 created_by_user = self._get_user(created_by)
319 created_by_user = self._get_user(created_by)
320 source_repo = self._get_repo(source_repo)
320 source_repo = self._get_repo(source_repo)
321 target_repo = self._get_repo(target_repo)
321 target_repo = self._get_repo(target_repo)
322
322
323 pull_request = PullRequest()
323 pull_request = PullRequest()
324 pull_request.source_repo = source_repo
324 pull_request.source_repo = source_repo
325 pull_request.source_ref = source_ref
325 pull_request.source_ref = source_ref
326 pull_request.target_repo = target_repo
326 pull_request.target_repo = target_repo
327 pull_request.target_ref = target_ref
327 pull_request.target_ref = target_ref
328 pull_request.revisions = revisions
328 pull_request.revisions = revisions
329 pull_request.title = title
329 pull_request.title = title
330 pull_request.description = description
330 pull_request.description = description
331 pull_request.author = created_by_user
331 pull_request.author = created_by_user
332
332
333 Session().add(pull_request)
333 Session().add(pull_request)
334 Session().flush()
334 Session().flush()
335
335
336 # members / reviewers
336 # members / reviewers
337 for user_id in set(reviewers):
337 for user_id in set(reviewers):
338 user = self._get_user(user_id)
338 user = self._get_user(user_id)
339 reviewer = PullRequestReviewers(user, pull_request)
339 reviewer = PullRequestReviewers(user, pull_request)
340 Session().add(reviewer)
340 Session().add(reviewer)
341
341
342 # Set approval status to "Under Review" for all commits which are
342 # Set approval status to "Under Review" for all commits which are
343 # part of this pull request.
343 # part of this pull request.
344 ChangesetStatusModel().set_status(
344 ChangesetStatusModel().set_status(
345 repo=target_repo,
345 repo=target_repo,
346 status=ChangesetStatus.STATUS_UNDER_REVIEW,
346 status=ChangesetStatus.STATUS_UNDER_REVIEW,
347 user=created_by_user,
347 user=created_by_user,
348 pull_request=pull_request
348 pull_request=pull_request
349 )
349 )
350
350
351 self.notify_reviewers(pull_request, reviewers)
351 self.notify_reviewers(pull_request, reviewers)
352 self._trigger_pull_request_hook(
352 self._trigger_pull_request_hook(
353 pull_request, created_by_user, 'create')
353 pull_request, created_by_user, 'create')
354
354
355 return pull_request
355 return pull_request
356
356
357 def _trigger_pull_request_hook(self, pull_request, user, action):
357 def _trigger_pull_request_hook(self, pull_request, user, action):
358 pull_request = self.__get_pull_request(pull_request)
358 pull_request = self.__get_pull_request(pull_request)
359 target_scm = pull_request.target_repo.scm_instance()
359 target_scm = pull_request.target_repo.scm_instance()
360 if action == 'create':
360 if action == 'create':
361 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
361 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
362 elif action == 'merge':
362 elif action == 'merge':
363 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
363 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
364 elif action == 'close':
364 elif action == 'close':
365 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
365 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
366 elif action == 'review_status_change':
366 elif action == 'review_status_change':
367 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
367 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
368 elif action == 'update':
368 elif action == 'update':
369 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
369 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
370 else:
370 else:
371 return
371 return
372
372
373 trigger_hook(
373 trigger_hook(
374 username=user.username,
374 username=user.username,
375 repo_name=pull_request.target_repo.repo_name,
375 repo_name=pull_request.target_repo.repo_name,
376 repo_alias=target_scm.alias,
376 repo_alias=target_scm.alias,
377 pull_request=pull_request)
377 pull_request=pull_request)
378
378
379 def _get_commit_ids(self, pull_request):
379 def _get_commit_ids(self, pull_request):
380 """
380 """
381 Return the commit ids of the merged pull request.
381 Return the commit ids of the merged pull request.
382
382
383 This method is not dealing correctly yet with the lack of autoupdates
383 This method is not dealing correctly yet with the lack of autoupdates
384 nor with the implicit target updates.
384 nor with the implicit target updates.
385 For example: if a commit in the source repo is already in the target it
385 For example: if a commit in the source repo is already in the target it
386 will be reported anyways.
386 will be reported anyways.
387 """
387 """
388 merge_rev = pull_request.merge_rev
388 merge_rev = pull_request.merge_rev
389 if merge_rev is None:
389 if merge_rev is None:
390 raise ValueError('This pull request was not merged yet')
390 raise ValueError('This pull request was not merged yet')
391
391
392 commit_ids = list(pull_request.revisions)
392 commit_ids = list(pull_request.revisions)
393 if merge_rev not in commit_ids:
393 if merge_rev not in commit_ids:
394 commit_ids.append(merge_rev)
394 commit_ids.append(merge_rev)
395
395
396 return commit_ids
396 return commit_ids
397
397
398 def merge(self, pull_request, user, extras):
398 def merge(self, pull_request, user, extras):
399 log.debug("Merging pull request %s", pull_request.pull_request_id)
399 log.debug("Merging pull request %s", pull_request.pull_request_id)
400 merge_state = self._merge_pull_request(pull_request, user, extras)
400 merge_state = self._merge_pull_request(pull_request, user, extras)
401 if merge_state.executed:
401 if merge_state.executed:
402 log.debug(
402 log.debug(
403 "Merge was successful, updating the pull request comments.")
403 "Merge was successful, updating the pull request comments.")
404 self._comment_and_close_pr(pull_request, user, merge_state)
404 self._comment_and_close_pr(pull_request, user, merge_state)
405 self._log_action('user_merged_pull_request', user, pull_request)
405 self._log_action('user_merged_pull_request', user, pull_request)
406 else:
406 else:
407 log.warn("Merge failed, not updating the pull request.")
407 log.warn("Merge failed, not updating the pull request.")
408 return merge_state
408 return merge_state
409
409
410 def _merge_pull_request(self, pull_request, user, extras):
410 def _merge_pull_request(self, pull_request, user, extras):
411 target_vcs = pull_request.target_repo.scm_instance()
411 target_vcs = pull_request.target_repo.scm_instance()
412 source_vcs = pull_request.source_repo.scm_instance()
412 source_vcs = pull_request.source_repo.scm_instance()
413 target_ref = self._refresh_reference(
413 target_ref = self._refresh_reference(
414 pull_request.target_ref_parts, target_vcs)
414 pull_request.target_ref_parts, target_vcs)
415
415
416 message = _(
416 message = _(
417 'Merge pull request #%(pr_id)s from '
417 'Merge pull request #%(pr_id)s from '
418 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
418 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
419 'pr_id': pull_request.pull_request_id,
419 'pr_id': pull_request.pull_request_id,
420 'source_repo': source_vcs.name,
420 'source_repo': source_vcs.name,
421 'source_ref_name': pull_request.source_ref_parts.name,
421 'source_ref_name': pull_request.source_ref_parts.name,
422 'pr_title': pull_request.title
422 'pr_title': pull_request.title
423 }
423 }
424
424
425 workspace_id = self._workspace_id(pull_request)
425 workspace_id = self._workspace_id(pull_request)
426 protocol = rhodecode.CONFIG.get('vcs.hooks.protocol')
427 use_direct_calls = rhodecode.CONFIG.get('vcs.hooks.direct_calls')
428 use_rebase = self._use_rebase_for_merging(pull_request)
426 use_rebase = self._use_rebase_for_merging(pull_request)
429
427
430 callback_daemon, extras = prepare_callback_daemon(
428 callback_daemon, extras = prepare_callback_daemon(
431 extras, protocol=protocol, use_direct_calls=use_direct_calls)
429 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
430 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
432
431
433 with callback_daemon:
432 with callback_daemon:
434 # TODO: johbo: Implement a clean way to run a config_override
433 # TODO: johbo: Implement a clean way to run a config_override
435 # for a single call.
434 # for a single call.
436 target_vcs.config.set(
435 target_vcs.config.set(
437 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
436 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
438 merge_state = target_vcs.merge(
437 merge_state = target_vcs.merge(
439 target_ref, source_vcs, pull_request.source_ref_parts,
438 target_ref, source_vcs, pull_request.source_ref_parts,
440 workspace_id, user_name=user.username,
439 workspace_id, user_name=user.username,
441 user_email=user.email, message=message, use_rebase=use_rebase)
440 user_email=user.email, message=message, use_rebase=use_rebase)
442 return merge_state
441 return merge_state
443
442
444 def _comment_and_close_pr(self, pull_request, user, merge_state):
443 def _comment_and_close_pr(self, pull_request, user, merge_state):
445 pull_request.merge_rev = merge_state.merge_commit_id
444 pull_request.merge_rev = merge_state.merge_commit_id
446 pull_request.updated_on = datetime.datetime.now()
445 pull_request.updated_on = datetime.datetime.now()
447
446
448 ChangesetCommentsModel().create(
447 ChangesetCommentsModel().create(
449 text=unicode(_('Pull request merged and closed')),
448 text=unicode(_('Pull request merged and closed')),
450 repo=pull_request.target_repo.repo_id,
449 repo=pull_request.target_repo.repo_id,
451 user=user.user_id,
450 user=user.user_id,
452 pull_request=pull_request.pull_request_id,
451 pull_request=pull_request.pull_request_id,
453 f_path=None,
452 f_path=None,
454 line_no=None,
453 line_no=None,
455 closing_pr=True
454 closing_pr=True
456 )
455 )
457
456
458 Session().add(pull_request)
457 Session().add(pull_request)
459 Session().flush()
458 Session().flush()
460 # TODO: paris: replace invalidation with less radical solution
459 # TODO: paris: replace invalidation with less radical solution
461 ScmModel().mark_for_invalidation(
460 ScmModel().mark_for_invalidation(
462 pull_request.target_repo.repo_name)
461 pull_request.target_repo.repo_name)
463 self._trigger_pull_request_hook(pull_request, user, 'merge')
462 self._trigger_pull_request_hook(pull_request, user, 'merge')
464
463
465 def has_valid_update_type(self, pull_request):
464 def has_valid_update_type(self, pull_request):
466 source_ref_type = pull_request.source_ref_parts.type
465 source_ref_type = pull_request.source_ref_parts.type
467 return source_ref_type in ['book', 'branch', 'tag']
466 return source_ref_type in ['book', 'branch', 'tag']
468
467
469 def update_commits(self, pull_request):
468 def update_commits(self, pull_request):
470 """
469 """
471 Get the updated list of commits for the pull request
470 Get the updated list of commits for the pull request
472 and return the new pull request version and the list
471 and return the new pull request version and the list
473 of commits processed by this update action
472 of commits processed by this update action
474 """
473 """
475
474
476 pull_request = self.__get_pull_request(pull_request)
475 pull_request = self.__get_pull_request(pull_request)
477 source_ref_type = pull_request.source_ref_parts.type
476 source_ref_type = pull_request.source_ref_parts.type
478 source_ref_name = pull_request.source_ref_parts.name
477 source_ref_name = pull_request.source_ref_parts.name
479 source_ref_id = pull_request.source_ref_parts.commit_id
478 source_ref_id = pull_request.source_ref_parts.commit_id
480
479
481 if not self.has_valid_update_type(pull_request):
480 if not self.has_valid_update_type(pull_request):
482 log.debug(
481 log.debug(
483 "Skipping update of pull request %s due to ref type: %s",
482 "Skipping update of pull request %s due to ref type: %s",
484 pull_request, source_ref_type)
483 pull_request, source_ref_type)
485 return (None, None)
484 return (None, None)
486
485
487 source_repo = pull_request.source_repo.scm_instance()
486 source_repo = pull_request.source_repo.scm_instance()
488 source_commit = source_repo.get_commit(commit_id=source_ref_name)
487 source_commit = source_repo.get_commit(commit_id=source_ref_name)
489 if source_ref_id == source_commit.raw_id:
488 if source_ref_id == source_commit.raw_id:
490 log.debug("Nothing changed in pull request %s", pull_request)
489 log.debug("Nothing changed in pull request %s", pull_request)
491 return (None, None)
490 return (None, None)
492
491
493 # Finally there is a need for an update
492 # Finally there is a need for an update
494 pull_request_version = self._create_version_from_snapshot(pull_request)
493 pull_request_version = self._create_version_from_snapshot(pull_request)
495 self._link_comments_to_version(pull_request_version)
494 self._link_comments_to_version(pull_request_version)
496
495
497 target_ref_type = pull_request.target_ref_parts.type
496 target_ref_type = pull_request.target_ref_parts.type
498 target_ref_name = pull_request.target_ref_parts.name
497 target_ref_name = pull_request.target_ref_parts.name
499 target_ref_id = pull_request.target_ref_parts.commit_id
498 target_ref_id = pull_request.target_ref_parts.commit_id
500 target_repo = pull_request.target_repo.scm_instance()
499 target_repo = pull_request.target_repo.scm_instance()
501
500
502 if target_ref_type in ('tag', 'branch', 'book'):
501 if target_ref_type in ('tag', 'branch', 'book'):
503 target_commit = target_repo.get_commit(target_ref_name)
502 target_commit = target_repo.get_commit(target_ref_name)
504 else:
503 else:
505 target_commit = target_repo.get_commit(target_ref_id)
504 target_commit = target_repo.get_commit(target_ref_id)
506
505
507 # re-compute commit ids
506 # re-compute commit ids
508 old_commit_ids = set(pull_request.revisions)
507 old_commit_ids = set(pull_request.revisions)
509 pre_load = ["author", "branch", "date", "message"]
508 pre_load = ["author", "branch", "date", "message"]
510 commit_ranges = target_repo.compare(
509 commit_ranges = target_repo.compare(
511 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
510 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
512 pre_load=pre_load)
511 pre_load=pre_load)
513
512
514 ancestor = target_repo.get_common_ancestor(
513 ancestor = target_repo.get_common_ancestor(
515 target_commit.raw_id, source_commit.raw_id, source_repo)
514 target_commit.raw_id, source_commit.raw_id, source_repo)
516
515
517 pull_request.source_ref = '%s:%s:%s' % (
516 pull_request.source_ref = '%s:%s:%s' % (
518 source_ref_type, source_ref_name, source_commit.raw_id)
517 source_ref_type, source_ref_name, source_commit.raw_id)
519 pull_request.target_ref = '%s:%s:%s' % (
518 pull_request.target_ref = '%s:%s:%s' % (
520 target_ref_type, target_ref_name, ancestor)
519 target_ref_type, target_ref_name, ancestor)
521 pull_request.revisions = [
520 pull_request.revisions = [
522 commit.raw_id for commit in reversed(commit_ranges)]
521 commit.raw_id for commit in reversed(commit_ranges)]
523 pull_request.updated_on = datetime.datetime.now()
522 pull_request.updated_on = datetime.datetime.now()
524 Session().add(pull_request)
523 Session().add(pull_request)
525 new_commit_ids = set(pull_request.revisions)
524 new_commit_ids = set(pull_request.revisions)
526
525
527 changes = self._calculate_commit_id_changes(
526 changes = self._calculate_commit_id_changes(
528 old_commit_ids, new_commit_ids)
527 old_commit_ids, new_commit_ids)
529
528
530 old_diff_data, new_diff_data = self._generate_update_diffs(
529 old_diff_data, new_diff_data = self._generate_update_diffs(
531 pull_request, pull_request_version)
530 pull_request, pull_request_version)
532
531
533 ChangesetCommentsModel().outdate_comments(
532 ChangesetCommentsModel().outdate_comments(
534 pull_request, old_diff_data=old_diff_data,
533 pull_request, old_diff_data=old_diff_data,
535 new_diff_data=new_diff_data)
534 new_diff_data=new_diff_data)
536
535
537 file_changes = self._calculate_file_changes(
536 file_changes = self._calculate_file_changes(
538 old_diff_data, new_diff_data)
537 old_diff_data, new_diff_data)
539
538
540 # Add an automatic comment to the pull request
539 # Add an automatic comment to the pull request
541 update_comment = ChangesetCommentsModel().create(
540 update_comment = ChangesetCommentsModel().create(
542 text=self._render_update_message(changes, file_changes),
541 text=self._render_update_message(changes, file_changes),
543 repo=pull_request.target_repo,
542 repo=pull_request.target_repo,
544 user=pull_request.author,
543 user=pull_request.author,
545 pull_request=pull_request,
544 pull_request=pull_request,
546 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
545 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
547
546
548 # Update status to "Under Review" for added commits
547 # Update status to "Under Review" for added commits
549 for commit_id in changes.added:
548 for commit_id in changes.added:
550 ChangesetStatusModel().set_status(
549 ChangesetStatusModel().set_status(
551 repo=pull_request.source_repo,
550 repo=pull_request.source_repo,
552 status=ChangesetStatus.STATUS_UNDER_REVIEW,
551 status=ChangesetStatus.STATUS_UNDER_REVIEW,
553 comment=update_comment,
552 comment=update_comment,
554 user=pull_request.author,
553 user=pull_request.author,
555 pull_request=pull_request,
554 pull_request=pull_request,
556 revision=commit_id)
555 revision=commit_id)
557
556
558 log.debug(
557 log.debug(
559 'Updated pull request %s, added_ids: %s, common_ids: %s, '
558 'Updated pull request %s, added_ids: %s, common_ids: %s, '
560 'removed_ids: %s', pull_request.pull_request_id,
559 'removed_ids: %s', pull_request.pull_request_id,
561 changes.added, changes.common, changes.removed)
560 changes.added, changes.common, changes.removed)
562 log.debug('Updated pull request with the following file changes: %s',
561 log.debug('Updated pull request with the following file changes: %s',
563 file_changes)
562 file_changes)
564
563
565 log.info(
564 log.info(
566 "Updated pull request %s from commit %s to commit %s, "
565 "Updated pull request %s from commit %s to commit %s, "
567 "stored new version %s of this pull request.",
566 "stored new version %s of this pull request.",
568 pull_request.pull_request_id, source_ref_id,
567 pull_request.pull_request_id, source_ref_id,
569 pull_request.source_ref_parts.commit_id,
568 pull_request.source_ref_parts.commit_id,
570 pull_request_version.pull_request_version_id)
569 pull_request_version.pull_request_version_id)
571 Session().commit()
570 Session().commit()
572 self._trigger_pull_request_hook(pull_request, pull_request.author,
571 self._trigger_pull_request_hook(pull_request, pull_request.author,
573 'update')
572 'update')
574 return (pull_request_version, changes)
573 return (pull_request_version, changes)
575
574
576 def _create_version_from_snapshot(self, pull_request):
575 def _create_version_from_snapshot(self, pull_request):
577 version = PullRequestVersion()
576 version = PullRequestVersion()
578 version.title = pull_request.title
577 version.title = pull_request.title
579 version.description = pull_request.description
578 version.description = pull_request.description
580 version.status = pull_request.status
579 version.status = pull_request.status
581 version.created_on = pull_request.created_on
580 version.created_on = pull_request.created_on
582 version.updated_on = pull_request.updated_on
581 version.updated_on = pull_request.updated_on
583 version.user_id = pull_request.user_id
582 version.user_id = pull_request.user_id
584 version.source_repo = pull_request.source_repo
583 version.source_repo = pull_request.source_repo
585 version.source_ref = pull_request.source_ref
584 version.source_ref = pull_request.source_ref
586 version.target_repo = pull_request.target_repo
585 version.target_repo = pull_request.target_repo
587 version.target_ref = pull_request.target_ref
586 version.target_ref = pull_request.target_ref
588
587
589 version._last_merge_source_rev = pull_request._last_merge_source_rev
588 version._last_merge_source_rev = pull_request._last_merge_source_rev
590 version._last_merge_target_rev = pull_request._last_merge_target_rev
589 version._last_merge_target_rev = pull_request._last_merge_target_rev
591 version._last_merge_status = pull_request._last_merge_status
590 version._last_merge_status = pull_request._last_merge_status
592 version.merge_rev = pull_request.merge_rev
591 version.merge_rev = pull_request.merge_rev
593
592
594 version.revisions = pull_request.revisions
593 version.revisions = pull_request.revisions
595 version.pull_request = pull_request
594 version.pull_request = pull_request
596 Session().add(version)
595 Session().add(version)
597 Session().flush()
596 Session().flush()
598
597
599 return version
598 return version
600
599
601 def _generate_update_diffs(self, pull_request, pull_request_version):
600 def _generate_update_diffs(self, pull_request, pull_request_version):
602 diff_context = (
601 diff_context = (
603 self.DIFF_CONTEXT +
602 self.DIFF_CONTEXT +
604 ChangesetCommentsModel.needed_extra_diff_context())
603 ChangesetCommentsModel.needed_extra_diff_context())
605 old_diff = self._get_diff_from_pr_or_version(
604 old_diff = self._get_diff_from_pr_or_version(
606 pull_request_version, context=diff_context)
605 pull_request_version, context=diff_context)
607 new_diff = self._get_diff_from_pr_or_version(
606 new_diff = self._get_diff_from_pr_or_version(
608 pull_request, context=diff_context)
607 pull_request, context=diff_context)
609
608
610 old_diff_data = diffs.DiffProcessor(old_diff)
609 old_diff_data = diffs.DiffProcessor(old_diff)
611 old_diff_data.prepare()
610 old_diff_data.prepare()
612 new_diff_data = diffs.DiffProcessor(new_diff)
611 new_diff_data = diffs.DiffProcessor(new_diff)
613 new_diff_data.prepare()
612 new_diff_data.prepare()
614
613
615 return old_diff_data, new_diff_data
614 return old_diff_data, new_diff_data
616
615
617 def _link_comments_to_version(self, pull_request_version):
616 def _link_comments_to_version(self, pull_request_version):
618 """
617 """
619 Link all unlinked comments of this pull request to the given version.
618 Link all unlinked comments of this pull request to the given version.
620
619
621 :param pull_request_version: The `PullRequestVersion` to which
620 :param pull_request_version: The `PullRequestVersion` to which
622 the comments shall be linked.
621 the comments shall be linked.
623
622
624 """
623 """
625 pull_request = pull_request_version.pull_request
624 pull_request = pull_request_version.pull_request
626 comments = ChangesetComment.query().filter(
625 comments = ChangesetComment.query().filter(
627 # TODO: johbo: Should we query for the repo at all here?
626 # TODO: johbo: Should we query for the repo at all here?
628 # Pending decision on how comments of PRs are to be related
627 # Pending decision on how comments of PRs are to be related
629 # to either the source repo, the target repo or no repo at all.
628 # to either the source repo, the target repo or no repo at all.
630 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
629 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
631 ChangesetComment.pull_request == pull_request,
630 ChangesetComment.pull_request == pull_request,
632 ChangesetComment.pull_request_version == None)
631 ChangesetComment.pull_request_version == None)
633
632
634 # TODO: johbo: Find out why this breaks if it is done in a bulk
633 # TODO: johbo: Find out why this breaks if it is done in a bulk
635 # operation.
634 # operation.
636 for comment in comments:
635 for comment in comments:
637 comment.pull_request_version_id = (
636 comment.pull_request_version_id = (
638 pull_request_version.pull_request_version_id)
637 pull_request_version.pull_request_version_id)
639 Session().add(comment)
638 Session().add(comment)
640
639
641 def _calculate_commit_id_changes(self, old_ids, new_ids):
640 def _calculate_commit_id_changes(self, old_ids, new_ids):
642 added = new_ids.difference(old_ids)
641 added = new_ids.difference(old_ids)
643 common = old_ids.intersection(new_ids)
642 common = old_ids.intersection(new_ids)
644 removed = old_ids.difference(new_ids)
643 removed = old_ids.difference(new_ids)
645 return ChangeTuple(added, common, removed)
644 return ChangeTuple(added, common, removed)
646
645
647 def _calculate_file_changes(self, old_diff_data, new_diff_data):
646 def _calculate_file_changes(self, old_diff_data, new_diff_data):
648
647
649 old_files = OrderedDict()
648 old_files = OrderedDict()
650 for diff_data in old_diff_data.parsed_diff:
649 for diff_data in old_diff_data.parsed_diff:
651 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
650 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
652
651
653 added_files = []
652 added_files = []
654 modified_files = []
653 modified_files = []
655 removed_files = []
654 removed_files = []
656 for diff_data in new_diff_data.parsed_diff:
655 for diff_data in new_diff_data.parsed_diff:
657 new_filename = diff_data['filename']
656 new_filename = diff_data['filename']
658 new_hash = md5_safe(diff_data['raw_diff'])
657 new_hash = md5_safe(diff_data['raw_diff'])
659
658
660 old_hash = old_files.get(new_filename)
659 old_hash = old_files.get(new_filename)
661 if not old_hash:
660 if not old_hash:
662 # file is not present in old diff, means it's added
661 # file is not present in old diff, means it's added
663 added_files.append(new_filename)
662 added_files.append(new_filename)
664 else:
663 else:
665 if new_hash != old_hash:
664 if new_hash != old_hash:
666 modified_files.append(new_filename)
665 modified_files.append(new_filename)
667 # now remove a file from old, since we have seen it already
666 # now remove a file from old, since we have seen it already
668 del old_files[new_filename]
667 del old_files[new_filename]
669
668
670 # removed files is when there are present in old, but not in NEW,
669 # removed files is when there are present in old, but not in NEW,
671 # since we remove old files that are present in new diff, left-overs
670 # since we remove old files that are present in new diff, left-overs
672 # if any should be the removed files
671 # if any should be the removed files
673 removed_files.extend(old_files.keys())
672 removed_files.extend(old_files.keys())
674
673
675 return FileChangeTuple(added_files, modified_files, removed_files)
674 return FileChangeTuple(added_files, modified_files, removed_files)
676
675
677 def _render_update_message(self, changes, file_changes):
676 def _render_update_message(self, changes, file_changes):
678 """
677 """
679 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
678 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
680 so it's always looking the same disregarding on which default
679 so it's always looking the same disregarding on which default
681 renderer system is using.
680 renderer system is using.
682
681
683 :param changes: changes named tuple
682 :param changes: changes named tuple
684 :param file_changes: file changes named tuple
683 :param file_changes: file changes named tuple
685
684
686 """
685 """
687 new_status = ChangesetStatus.get_status_lbl(
686 new_status = ChangesetStatus.get_status_lbl(
688 ChangesetStatus.STATUS_UNDER_REVIEW)
687 ChangesetStatus.STATUS_UNDER_REVIEW)
689
688
690 changed_files = (
689 changed_files = (
691 file_changes.added + file_changes.modified + file_changes.removed)
690 file_changes.added + file_changes.modified + file_changes.removed)
692
691
693 params = {
692 params = {
694 'under_review_label': new_status,
693 'under_review_label': new_status,
695 'added_commits': changes.added,
694 'added_commits': changes.added,
696 'removed_commits': changes.removed,
695 'removed_commits': changes.removed,
697 'changed_files': changed_files,
696 'changed_files': changed_files,
698 'added_files': file_changes.added,
697 'added_files': file_changes.added,
699 'modified_files': file_changes.modified,
698 'modified_files': file_changes.modified,
700 'removed_files': file_changes.removed,
699 'removed_files': file_changes.removed,
701 }
700 }
702 renderer = RstTemplateRenderer()
701 renderer = RstTemplateRenderer()
703 return renderer.render('pull_request_update.mako', **params)
702 return renderer.render('pull_request_update.mako', **params)
704
703
705 def edit(self, pull_request, title, description):
704 def edit(self, pull_request, title, description):
706 pull_request = self.__get_pull_request(pull_request)
705 pull_request = self.__get_pull_request(pull_request)
707 if pull_request.is_closed():
706 if pull_request.is_closed():
708 raise ValueError('This pull request is closed')
707 raise ValueError('This pull request is closed')
709 if title:
708 if title:
710 pull_request.title = title
709 pull_request.title = title
711 pull_request.description = description
710 pull_request.description = description
712 pull_request.updated_on = datetime.datetime.now()
711 pull_request.updated_on = datetime.datetime.now()
713 Session().add(pull_request)
712 Session().add(pull_request)
714
713
715 def update_reviewers(self, pull_request, reviewers_ids):
714 def update_reviewers(self, pull_request, reviewers_ids):
716 reviewers_ids = set(reviewers_ids)
715 reviewers_ids = set(reviewers_ids)
717 pull_request = self.__get_pull_request(pull_request)
716 pull_request = self.__get_pull_request(pull_request)
718 current_reviewers = PullRequestReviewers.query()\
717 current_reviewers = PullRequestReviewers.query()\
719 .filter(PullRequestReviewers.pull_request ==
718 .filter(PullRequestReviewers.pull_request ==
720 pull_request).all()
719 pull_request).all()
721 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
720 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
722
721
723 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
722 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
724 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
723 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
725
724
726 log.debug("Adding %s reviewers", ids_to_add)
725 log.debug("Adding %s reviewers", ids_to_add)
727 log.debug("Removing %s reviewers", ids_to_remove)
726 log.debug("Removing %s reviewers", ids_to_remove)
728 changed = False
727 changed = False
729 for uid in ids_to_add:
728 for uid in ids_to_add:
730 changed = True
729 changed = True
731 _usr = self._get_user(uid)
730 _usr = self._get_user(uid)
732 reviewer = PullRequestReviewers(_usr, pull_request)
731 reviewer = PullRequestReviewers(_usr, pull_request)
733 Session().add(reviewer)
732 Session().add(reviewer)
734
733
735 self.notify_reviewers(pull_request, ids_to_add)
734 self.notify_reviewers(pull_request, ids_to_add)
736
735
737 for uid in ids_to_remove:
736 for uid in ids_to_remove:
738 changed = True
737 changed = True
739 reviewer = PullRequestReviewers.query()\
738 reviewer = PullRequestReviewers.query()\
740 .filter(PullRequestReviewers.user_id == uid,
739 .filter(PullRequestReviewers.user_id == uid,
741 PullRequestReviewers.pull_request == pull_request)\
740 PullRequestReviewers.pull_request == pull_request)\
742 .scalar()
741 .scalar()
743 if reviewer:
742 if reviewer:
744 Session().delete(reviewer)
743 Session().delete(reviewer)
745 if changed:
744 if changed:
746 pull_request.updated_on = datetime.datetime.now()
745 pull_request.updated_on = datetime.datetime.now()
747 Session().add(pull_request)
746 Session().add(pull_request)
748
747
749 return ids_to_add, ids_to_remove
748 return ids_to_add, ids_to_remove
750
749
751 def get_url(self, pull_request):
750 def get_url(self, pull_request):
752 return h.url('pullrequest_show',
751 return h.url('pullrequest_show',
753 repo_name=safe_str(pull_request.target_repo.repo_name),
752 repo_name=safe_str(pull_request.target_repo.repo_name),
754 pull_request_id=pull_request.pull_request_id,
753 pull_request_id=pull_request.pull_request_id,
755 qualified=True)
754 qualified=True)
756
755
757 def notify_reviewers(self, pull_request, reviewers_ids):
756 def notify_reviewers(self, pull_request, reviewers_ids):
758 # notification to reviewers
757 # notification to reviewers
759 if not reviewers_ids:
758 if not reviewers_ids:
760 return
759 return
761
760
762 pull_request_obj = pull_request
761 pull_request_obj = pull_request
763 # get the current participants of this pull request
762 # get the current participants of this pull request
764 recipients = reviewers_ids
763 recipients = reviewers_ids
765 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
764 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
766
765
767 pr_source_repo = pull_request_obj.source_repo
766 pr_source_repo = pull_request_obj.source_repo
768 pr_target_repo = pull_request_obj.target_repo
767 pr_target_repo = pull_request_obj.target_repo
769
768
770 pr_url = h.url(
769 pr_url = h.url(
771 'pullrequest_show',
770 'pullrequest_show',
772 repo_name=pr_target_repo.repo_name,
771 repo_name=pr_target_repo.repo_name,
773 pull_request_id=pull_request_obj.pull_request_id,
772 pull_request_id=pull_request_obj.pull_request_id,
774 qualified=True,)
773 qualified=True,)
775
774
776 # set some variables for email notification
775 # set some variables for email notification
777 pr_target_repo_url = h.url(
776 pr_target_repo_url = h.url(
778 'summary_home',
777 'summary_home',
779 repo_name=pr_target_repo.repo_name,
778 repo_name=pr_target_repo.repo_name,
780 qualified=True)
779 qualified=True)
781
780
782 pr_source_repo_url = h.url(
781 pr_source_repo_url = h.url(
783 'summary_home',
782 'summary_home',
784 repo_name=pr_source_repo.repo_name,
783 repo_name=pr_source_repo.repo_name,
785 qualified=True)
784 qualified=True)
786
785
787 # pull request specifics
786 # pull request specifics
788 pull_request_commits = [
787 pull_request_commits = [
789 (x.raw_id, x.message)
788 (x.raw_id, x.message)
790 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
789 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
791
790
792 kwargs = {
791 kwargs = {
793 'user': pull_request.author,
792 'user': pull_request.author,
794 'pull_request': pull_request_obj,
793 'pull_request': pull_request_obj,
795 'pull_request_commits': pull_request_commits,
794 'pull_request_commits': pull_request_commits,
796
795
797 'pull_request_target_repo': pr_target_repo,
796 'pull_request_target_repo': pr_target_repo,
798 'pull_request_target_repo_url': pr_target_repo_url,
797 'pull_request_target_repo_url': pr_target_repo_url,
799
798
800 'pull_request_source_repo': pr_source_repo,
799 'pull_request_source_repo': pr_source_repo,
801 'pull_request_source_repo_url': pr_source_repo_url,
800 'pull_request_source_repo_url': pr_source_repo_url,
802
801
803 'pull_request_url': pr_url,
802 'pull_request_url': pr_url,
804 }
803 }
805
804
806 # pre-generate the subject for notification itself
805 # pre-generate the subject for notification itself
807 (subject,
806 (subject,
808 _h, _e, # we don't care about those
807 _h, _e, # we don't care about those
809 body_plaintext) = EmailNotificationModel().render_email(
808 body_plaintext) = EmailNotificationModel().render_email(
810 notification_type, **kwargs)
809 notification_type, **kwargs)
811
810
812 # create notification objects, and emails
811 # create notification objects, and emails
813 NotificationModel().create(
812 NotificationModel().create(
814 created_by=pull_request.author,
813 created_by=pull_request.author,
815 notification_subject=subject,
814 notification_subject=subject,
816 notification_body=body_plaintext,
815 notification_body=body_plaintext,
817 notification_type=notification_type,
816 notification_type=notification_type,
818 recipients=recipients,
817 recipients=recipients,
819 email_kwargs=kwargs,
818 email_kwargs=kwargs,
820 )
819 )
821
820
822 def delete(self, pull_request):
821 def delete(self, pull_request):
823 pull_request = self.__get_pull_request(pull_request)
822 pull_request = self.__get_pull_request(pull_request)
824 self._cleanup_merge_workspace(pull_request)
823 self._cleanup_merge_workspace(pull_request)
825 Session().delete(pull_request)
824 Session().delete(pull_request)
826
825
827 def close_pull_request(self, pull_request, user):
826 def close_pull_request(self, pull_request, user):
828 pull_request = self.__get_pull_request(pull_request)
827 pull_request = self.__get_pull_request(pull_request)
829 self._cleanup_merge_workspace(pull_request)
828 self._cleanup_merge_workspace(pull_request)
830 pull_request.status = PullRequest.STATUS_CLOSED
829 pull_request.status = PullRequest.STATUS_CLOSED
831 pull_request.updated_on = datetime.datetime.now()
830 pull_request.updated_on = datetime.datetime.now()
832 Session().add(pull_request)
831 Session().add(pull_request)
833 self._trigger_pull_request_hook(
832 self._trigger_pull_request_hook(
834 pull_request, pull_request.author, 'close')
833 pull_request, pull_request.author, 'close')
835 self._log_action('user_closed_pull_request', user, pull_request)
834 self._log_action('user_closed_pull_request', user, pull_request)
836
835
837 def close_pull_request_with_comment(self, pull_request, user, repo,
836 def close_pull_request_with_comment(self, pull_request, user, repo,
838 message=None):
837 message=None):
839 status = ChangesetStatus.STATUS_REJECTED
838 status = ChangesetStatus.STATUS_REJECTED
840
839
841 if not message:
840 if not message:
842 message = (
841 message = (
843 _('Status change %(transition_icon)s %(status)s') % {
842 _('Status change %(transition_icon)s %(status)s') % {
844 'transition_icon': '>',
843 'transition_icon': '>',
845 'status': ChangesetStatus.get_status_lbl(status)})
844 'status': ChangesetStatus.get_status_lbl(status)})
846
845
847 internal_message = _('Closing with') + ' ' + message
846 internal_message = _('Closing with') + ' ' + message
848
847
849 comm = ChangesetCommentsModel().create(
848 comm = ChangesetCommentsModel().create(
850 text=internal_message,
849 text=internal_message,
851 repo=repo.repo_id,
850 repo=repo.repo_id,
852 user=user.user_id,
851 user=user.user_id,
853 pull_request=pull_request.pull_request_id,
852 pull_request=pull_request.pull_request_id,
854 f_path=None,
853 f_path=None,
855 line_no=None,
854 line_no=None,
856 status_change=ChangesetStatus.get_status_lbl(status),
855 status_change=ChangesetStatus.get_status_lbl(status),
857 status_change_type=status,
856 status_change_type=status,
858 closing_pr=True
857 closing_pr=True
859 )
858 )
860
859
861 ChangesetStatusModel().set_status(
860 ChangesetStatusModel().set_status(
862 repo.repo_id,
861 repo.repo_id,
863 status,
862 status,
864 user.user_id,
863 user.user_id,
865 comm,
864 comm,
866 pull_request=pull_request.pull_request_id
865 pull_request=pull_request.pull_request_id
867 )
866 )
868 Session().flush()
867 Session().flush()
869
868
870 PullRequestModel().close_pull_request(
869 PullRequestModel().close_pull_request(
871 pull_request.pull_request_id, user)
870 pull_request.pull_request_id, user)
872
871
873 def merge_status(self, pull_request):
872 def merge_status(self, pull_request):
874 if not self._is_merge_enabled(pull_request):
873 if not self._is_merge_enabled(pull_request):
875 return False, _('Server-side pull request merging is disabled.')
874 return False, _('Server-side pull request merging is disabled.')
876 if pull_request.is_closed():
875 if pull_request.is_closed():
877 return False, _('This pull request is closed.')
876 return False, _('This pull request is closed.')
878 merge_possible, msg = self._check_repo_requirements(
877 merge_possible, msg = self._check_repo_requirements(
879 target=pull_request.target_repo, source=pull_request.source_repo)
878 target=pull_request.target_repo, source=pull_request.source_repo)
880 if not merge_possible:
879 if not merge_possible:
881 return merge_possible, msg
880 return merge_possible, msg
882
881
883 try:
882 try:
884 resp = self._try_merge(pull_request)
883 resp = self._try_merge(pull_request)
885 status = resp.possible, self.merge_status_message(
884 status = resp.possible, self.merge_status_message(
886 resp.failure_reason)
885 resp.failure_reason)
887 except NotImplementedError:
886 except NotImplementedError:
888 status = False, _('Pull request merging is not supported.')
887 status = False, _('Pull request merging is not supported.')
889
888
890 return status
889 return status
891
890
892 def _check_repo_requirements(self, target, source):
891 def _check_repo_requirements(self, target, source):
893 """
892 """
894 Check if `target` and `source` have compatible requirements.
893 Check if `target` and `source` have compatible requirements.
895
894
896 Currently this is just checking for largefiles.
895 Currently this is just checking for largefiles.
897 """
896 """
898 target_has_largefiles = self._has_largefiles(target)
897 target_has_largefiles = self._has_largefiles(target)
899 source_has_largefiles = self._has_largefiles(source)
898 source_has_largefiles = self._has_largefiles(source)
900 merge_possible = True
899 merge_possible = True
901 message = u''
900 message = u''
902
901
903 if target_has_largefiles != source_has_largefiles:
902 if target_has_largefiles != source_has_largefiles:
904 merge_possible = False
903 merge_possible = False
905 if source_has_largefiles:
904 if source_has_largefiles:
906 message = _(
905 message = _(
907 'Target repository large files support is disabled.')
906 'Target repository large files support is disabled.')
908 else:
907 else:
909 message = _(
908 message = _(
910 'Source repository large files support is disabled.')
909 'Source repository large files support is disabled.')
911
910
912 return merge_possible, message
911 return merge_possible, message
913
912
914 def _has_largefiles(self, repo):
913 def _has_largefiles(self, repo):
915 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
914 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
916 'extensions', 'largefiles')
915 'extensions', 'largefiles')
917 return largefiles_ui and largefiles_ui[0].active
916 return largefiles_ui and largefiles_ui[0].active
918
917
919 def _try_merge(self, pull_request):
918 def _try_merge(self, pull_request):
920 """
919 """
921 Try to merge the pull request and return the merge status.
920 Try to merge the pull request and return the merge status.
922 """
921 """
923 log.debug(
922 log.debug(
924 "Trying out if the pull request %s can be merged.",
923 "Trying out if the pull request %s can be merged.",
925 pull_request.pull_request_id)
924 pull_request.pull_request_id)
926 target_vcs = pull_request.target_repo.scm_instance()
925 target_vcs = pull_request.target_repo.scm_instance()
927 target_ref = self._refresh_reference(
926 target_ref = self._refresh_reference(
928 pull_request.target_ref_parts, target_vcs)
927 pull_request.target_ref_parts, target_vcs)
929
928
930 target_locked = pull_request.target_repo.locked
929 target_locked = pull_request.target_repo.locked
931 if target_locked and target_locked[0]:
930 if target_locked and target_locked[0]:
932 log.debug("The target repository is locked.")
931 log.debug("The target repository is locked.")
933 merge_state = MergeResponse(
932 merge_state = MergeResponse(
934 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
933 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
935 elif self._needs_merge_state_refresh(pull_request, target_ref):
934 elif self._needs_merge_state_refresh(pull_request, target_ref):
936 log.debug("Refreshing the merge status of the repository.")
935 log.debug("Refreshing the merge status of the repository.")
937 merge_state = self._refresh_merge_state(
936 merge_state = self._refresh_merge_state(
938 pull_request, target_vcs, target_ref)
937 pull_request, target_vcs, target_ref)
939 else:
938 else:
940 possible = pull_request.\
939 possible = pull_request.\
941 _last_merge_status == MergeFailureReason.NONE
940 _last_merge_status == MergeFailureReason.NONE
942 merge_state = MergeResponse(
941 merge_state = MergeResponse(
943 possible, False, None, pull_request._last_merge_status)
942 possible, False, None, pull_request._last_merge_status)
944 log.debug("Merge response: %s", merge_state)
943 log.debug("Merge response: %s", merge_state)
945 return merge_state
944 return merge_state
946
945
947 def _refresh_reference(self, reference, vcs_repository):
946 def _refresh_reference(self, reference, vcs_repository):
948 if reference.type in ('branch', 'book'):
947 if reference.type in ('branch', 'book'):
949 name_or_id = reference.name
948 name_or_id = reference.name
950 else:
949 else:
951 name_or_id = reference.commit_id
950 name_or_id = reference.commit_id
952 refreshed_commit = vcs_repository.get_commit(name_or_id)
951 refreshed_commit = vcs_repository.get_commit(name_or_id)
953 refreshed_reference = Reference(
952 refreshed_reference = Reference(
954 reference.type, reference.name, refreshed_commit.raw_id)
953 reference.type, reference.name, refreshed_commit.raw_id)
955 return refreshed_reference
954 return refreshed_reference
956
955
957 def _needs_merge_state_refresh(self, pull_request, target_reference):
956 def _needs_merge_state_refresh(self, pull_request, target_reference):
958 return not(
957 return not(
959 pull_request.revisions and
958 pull_request.revisions and
960 pull_request.revisions[0] == pull_request._last_merge_source_rev and
959 pull_request.revisions[0] == pull_request._last_merge_source_rev and
961 target_reference.commit_id == pull_request._last_merge_target_rev)
960 target_reference.commit_id == pull_request._last_merge_target_rev)
962
961
963 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
962 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
964 workspace_id = self._workspace_id(pull_request)
963 workspace_id = self._workspace_id(pull_request)
965 source_vcs = pull_request.source_repo.scm_instance()
964 source_vcs = pull_request.source_repo.scm_instance()
966 use_rebase = self._use_rebase_for_merging(pull_request)
965 use_rebase = self._use_rebase_for_merging(pull_request)
967 merge_state = target_vcs.merge(
966 merge_state = target_vcs.merge(
968 target_reference, source_vcs, pull_request.source_ref_parts,
967 target_reference, source_vcs, pull_request.source_ref_parts,
969 workspace_id, dry_run=True, use_rebase=use_rebase)
968 workspace_id, dry_run=True, use_rebase=use_rebase)
970
969
971 # Do not store the response if there was an unknown error.
970 # Do not store the response if there was an unknown error.
972 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
971 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
973 pull_request._last_merge_source_rev = pull_request.\
972 pull_request._last_merge_source_rev = pull_request.\
974 source_ref_parts.commit_id
973 source_ref_parts.commit_id
975 pull_request._last_merge_target_rev = target_reference.commit_id
974 pull_request._last_merge_target_rev = target_reference.commit_id
976 pull_request._last_merge_status = (
975 pull_request._last_merge_status = (
977 merge_state.failure_reason)
976 merge_state.failure_reason)
978 Session().add(pull_request)
977 Session().add(pull_request)
979 Session().flush()
978 Session().flush()
980
979
981 return merge_state
980 return merge_state
982
981
983 def _workspace_id(self, pull_request):
982 def _workspace_id(self, pull_request):
984 workspace_id = 'pr-%s' % pull_request.pull_request_id
983 workspace_id = 'pr-%s' % pull_request.pull_request_id
985 return workspace_id
984 return workspace_id
986
985
987 def merge_status_message(self, status_code):
986 def merge_status_message(self, status_code):
988 """
987 """
989 Return a human friendly error message for the given merge status code.
988 Return a human friendly error message for the given merge status code.
990 """
989 """
991 return self.MERGE_STATUS_MESSAGES[status_code]
990 return self.MERGE_STATUS_MESSAGES[status_code]
992
991
993 def generate_repo_data(self, repo, commit_id=None, branch=None,
992 def generate_repo_data(self, repo, commit_id=None, branch=None,
994 bookmark=None):
993 bookmark=None):
995 all_refs, selected_ref = \
994 all_refs, selected_ref = \
996 self._get_repo_pullrequest_sources(
995 self._get_repo_pullrequest_sources(
997 repo.scm_instance(), commit_id=commit_id,
996 repo.scm_instance(), commit_id=commit_id,
998 branch=branch, bookmark=bookmark)
997 branch=branch, bookmark=bookmark)
999
998
1000 refs_select2 = []
999 refs_select2 = []
1001 for element in all_refs:
1000 for element in all_refs:
1002 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1001 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1003 refs_select2.append({'text': element[1], 'children': children})
1002 refs_select2.append({'text': element[1], 'children': children})
1004
1003
1005 return {
1004 return {
1006 'user': {
1005 'user': {
1007 'user_id': repo.user.user_id,
1006 'user_id': repo.user.user_id,
1008 'username': repo.user.username,
1007 'username': repo.user.username,
1009 'firstname': repo.user.firstname,
1008 'firstname': repo.user.firstname,
1010 'lastname': repo.user.lastname,
1009 'lastname': repo.user.lastname,
1011 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1010 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1012 },
1011 },
1013 'description': h.chop_at_smart(repo.description, '\n'),
1012 'description': h.chop_at_smart(repo.description, '\n'),
1014 'refs': {
1013 'refs': {
1015 'all_refs': all_refs,
1014 'all_refs': all_refs,
1016 'selected_ref': selected_ref,
1015 'selected_ref': selected_ref,
1017 'select2_refs': refs_select2
1016 'select2_refs': refs_select2
1018 }
1017 }
1019 }
1018 }
1020
1019
1021 def generate_pullrequest_title(self, source, source_ref, target):
1020 def generate_pullrequest_title(self, source, source_ref, target):
1022 return '{source}#{at_ref} to {target}'.format(
1021 return '{source}#{at_ref} to {target}'.format(
1023 source=source,
1022 source=source,
1024 at_ref=source_ref,
1023 at_ref=source_ref,
1025 target=target,
1024 target=target,
1026 )
1025 )
1027
1026
1028 def _cleanup_merge_workspace(self, pull_request):
1027 def _cleanup_merge_workspace(self, pull_request):
1029 # Merging related cleanup
1028 # Merging related cleanup
1030 target_scm = pull_request.target_repo.scm_instance()
1029 target_scm = pull_request.target_repo.scm_instance()
1031 workspace_id = 'pr-%s' % pull_request.pull_request_id
1030 workspace_id = 'pr-%s' % pull_request.pull_request_id
1032
1031
1033 try:
1032 try:
1034 target_scm.cleanup_merge_workspace(workspace_id)
1033 target_scm.cleanup_merge_workspace(workspace_id)
1035 except NotImplementedError:
1034 except NotImplementedError:
1036 pass
1035 pass
1037
1036
1038 def _get_repo_pullrequest_sources(
1037 def _get_repo_pullrequest_sources(
1039 self, repo, commit_id=None, branch=None, bookmark=None):
1038 self, repo, commit_id=None, branch=None, bookmark=None):
1040 """
1039 """
1041 Return a structure with repo's interesting commits, suitable for
1040 Return a structure with repo's interesting commits, suitable for
1042 the selectors in pullrequest controller
1041 the selectors in pullrequest controller
1043
1042
1044 :param commit_id: a commit that must be in the list somehow
1043 :param commit_id: a commit that must be in the list somehow
1045 and selected by default
1044 and selected by default
1046 :param branch: a branch that must be in the list and selected
1045 :param branch: a branch that must be in the list and selected
1047 by default - even if closed
1046 by default - even if closed
1048 :param bookmark: a bookmark that must be in the list and selected
1047 :param bookmark: a bookmark that must be in the list and selected
1049 """
1048 """
1050
1049
1051 commit_id = safe_str(commit_id) if commit_id else None
1050 commit_id = safe_str(commit_id) if commit_id else None
1052 branch = safe_str(branch) if branch else None
1051 branch = safe_str(branch) if branch else None
1053 bookmark = safe_str(bookmark) if bookmark else None
1052 bookmark = safe_str(bookmark) if bookmark else None
1054
1053
1055 selected = None
1054 selected = None
1056
1055
1057 # order matters: first source that has commit_id in it will be selected
1056 # order matters: first source that has commit_id in it will be selected
1058 sources = []
1057 sources = []
1059 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1058 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1060 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1059 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1061
1060
1062 if commit_id:
1061 if commit_id:
1063 ref_commit = (h.short_id(commit_id), commit_id)
1062 ref_commit = (h.short_id(commit_id), commit_id)
1064 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1063 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1065
1064
1066 sources.append(
1065 sources.append(
1067 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1066 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1068 )
1067 )
1069
1068
1070 groups = []
1069 groups = []
1071 for group_key, ref_list, group_name, match in sources:
1070 for group_key, ref_list, group_name, match in sources:
1072 group_refs = []
1071 group_refs = []
1073 for ref_name, ref_id in ref_list:
1072 for ref_name, ref_id in ref_list:
1074 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1073 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1075 group_refs.append((ref_key, ref_name))
1074 group_refs.append((ref_key, ref_name))
1076
1075
1077 if not selected:
1076 if not selected:
1078 if set([commit_id, match]) & set([ref_id, ref_name]):
1077 if set([commit_id, match]) & set([ref_id, ref_name]):
1079 selected = ref_key
1078 selected = ref_key
1080
1079
1081 if group_refs:
1080 if group_refs:
1082 groups.append((group_refs, group_name))
1081 groups.append((group_refs, group_name))
1083
1082
1084 if not selected:
1083 if not selected:
1085 ref = commit_id or branch or bookmark
1084 ref = commit_id or branch or bookmark
1086 if ref:
1085 if ref:
1087 raise CommitDoesNotExistError(
1086 raise CommitDoesNotExistError(
1088 'No commit refs could be found matching: %s' % ref)
1087 'No commit refs could be found matching: %s' % ref)
1089 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1088 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1090 selected = 'branch:%s:%s' % (
1089 selected = 'branch:%s:%s' % (
1091 repo.DEFAULT_BRANCH_NAME,
1090 repo.DEFAULT_BRANCH_NAME,
1092 repo.branches[repo.DEFAULT_BRANCH_NAME]
1091 repo.branches[repo.DEFAULT_BRANCH_NAME]
1093 )
1092 )
1094 elif repo.commit_ids:
1093 elif repo.commit_ids:
1095 rev = repo.commit_ids[0]
1094 rev = repo.commit_ids[0]
1096 selected = 'rev:%s:%s' % (rev, rev)
1095 selected = 'rev:%s:%s' % (rev, rev)
1097 else:
1096 else:
1098 raise EmptyRepositoryError()
1097 raise EmptyRepositoryError()
1099 return groups, selected
1098 return groups, selected
1100
1099
1101 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1100 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1102 pull_request = self.__get_pull_request(pull_request)
1101 pull_request = self.__get_pull_request(pull_request)
1103 return self._get_diff_from_pr_or_version(pull_request, context=context)
1102 return self._get_diff_from_pr_or_version(pull_request, context=context)
1104
1103
1105 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1104 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1106 source_repo = pr_or_version.source_repo
1105 source_repo = pr_or_version.source_repo
1107
1106
1108 # we swap org/other ref since we run a simple diff on one repo
1107 # we swap org/other ref since we run a simple diff on one repo
1109 target_ref_id = pr_or_version.target_ref_parts.commit_id
1108 target_ref_id = pr_or_version.target_ref_parts.commit_id
1110 source_ref_id = pr_or_version.source_ref_parts.commit_id
1109 source_ref_id = pr_or_version.source_ref_parts.commit_id
1111 target_commit = source_repo.get_commit(
1110 target_commit = source_repo.get_commit(
1112 commit_id=safe_str(target_ref_id))
1111 commit_id=safe_str(target_ref_id))
1113 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1112 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1114 vcs_repo = source_repo.scm_instance()
1113 vcs_repo = source_repo.scm_instance()
1115
1114
1116 # TODO: johbo: In the context of an update, we cannot reach
1115 # TODO: johbo: In the context of an update, we cannot reach
1117 # the old commit anymore with our normal mechanisms. It needs
1116 # the old commit anymore with our normal mechanisms. It needs
1118 # some sort of special support in the vcs layer to avoid this
1117 # some sort of special support in the vcs layer to avoid this
1119 # workaround.
1118 # workaround.
1120 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1119 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1121 vcs_repo.alias == 'git'):
1120 vcs_repo.alias == 'git'):
1122 source_commit.raw_id = safe_str(source_ref_id)
1121 source_commit.raw_id = safe_str(source_ref_id)
1123
1122
1124 log.debug('calculating diff between '
1123 log.debug('calculating diff between '
1125 'source_ref:%s and target_ref:%s for repo `%s`',
1124 'source_ref:%s and target_ref:%s for repo `%s`',
1126 target_ref_id, source_ref_id,
1125 target_ref_id, source_ref_id,
1127 safe_unicode(vcs_repo.path))
1126 safe_unicode(vcs_repo.path))
1128
1127
1129 vcs_diff = vcs_repo.get_diff(
1128 vcs_diff = vcs_repo.get_diff(
1130 commit1=target_commit, commit2=source_commit, context=context)
1129 commit1=target_commit, commit2=source_commit, context=context)
1131 return vcs_diff
1130 return vcs_diff
1132
1131
1133 def _is_merge_enabled(self, pull_request):
1132 def _is_merge_enabled(self, pull_request):
1134 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1133 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1135 settings = settings_model.get_general_settings()
1134 settings = settings_model.get_general_settings()
1136 return settings.get('rhodecode_pr_merge_enabled', False)
1135 return settings.get('rhodecode_pr_merge_enabled', False)
1137
1136
1138 def _use_rebase_for_merging(self, pull_request):
1137 def _use_rebase_for_merging(self, pull_request):
1139 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1138 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1140 settings = settings_model.get_general_settings()
1139 settings = settings_model.get_general_settings()
1141 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1140 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1142
1141
1143 def _log_action(self, action, user, pull_request):
1142 def _log_action(self, action, user, pull_request):
1144 action_logger(
1143 action_logger(
1145 user,
1144 user,
1146 '{action}:{pr_id}'.format(
1145 '{action}:{pr_id}'.format(
1147 action=action, pr_id=pull_request.pull_request_id),
1146 action=action, pr_id=pull_request.pull_request_id),
1148 pull_request.target_repo)
1147 pull_request.target_repo)
1149
1148
1150
1149
1151 ChangeTuple = namedtuple('ChangeTuple',
1150 ChangeTuple = namedtuple('ChangeTuple',
1152 ['added', 'common', 'removed'])
1151 ['added', 'common', 'removed'])
1153
1152
1154 FileChangeTuple = namedtuple('FileChangeTuple',
1153 FileChangeTuple = namedtuple('FileChangeTuple',
1155 ['added', 'modified', 'removed'])
1154 ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now