##// END OF EJS Templates
merged stable into default
super-admin -
r4713:5b71ff87 merge default
parent child Browse files
Show More
@@ -0,0 +1,389 b''
1 import sys
2 import threading
3 import weakref
4 from base64 import b64encode
5 from logging import getLogger
6 from os import urandom
7
8 from redis import StrictRedis
9
10 __version__ = '3.7.0'
11
12 loggers = {
13 k: getLogger("rhodecode" + ".".join((__name__, k)))
14 for k in [
15 "acquire",
16 "refresh.thread.start",
17 "refresh.thread.stop",
18 "refresh.thread.exit",
19 "refresh.start",
20 "refresh.shutdown",
21 "refresh.exit",
22 "release",
23 ]
24 }
25
26 PY3 = sys.version_info[0] == 3
27
28 if PY3:
29 text_type = str
30 binary_type = bytes
31 else:
32 text_type = unicode # noqa
33 binary_type = str
34
35
36 # Check if the id match. If not, return an error code.
37 UNLOCK_SCRIPT = b"""
38 if redis.call("get", KEYS[1]) ~= ARGV[1] then
39 return 1
40 else
41 redis.call("del", KEYS[2])
42 redis.call("lpush", KEYS[2], 1)
43 redis.call("pexpire", KEYS[2], ARGV[2])
44 redis.call("del", KEYS[1])
45 return 0
46 end
47 """
48
49 # Covers both cases when key doesn't exist and doesn't equal to lock's id
50 EXTEND_SCRIPT = b"""
51 if redis.call("get", KEYS[1]) ~= ARGV[1] then
52 return 1
53 elseif redis.call("ttl", KEYS[1]) < 0 then
54 return 2
55 else
56 redis.call("expire", KEYS[1], ARGV[2])
57 return 0
58 end
59 """
60
61 RESET_SCRIPT = b"""
62 redis.call('del', KEYS[2])
63 redis.call('lpush', KEYS[2], 1)
64 redis.call('pexpire', KEYS[2], ARGV[2])
65 return redis.call('del', KEYS[1])
66 """
67
68 RESET_ALL_SCRIPT = b"""
69 local locks = redis.call('keys', 'lock:*')
70 local signal
71 for _, lock in pairs(locks) do
72 signal = 'lock-signal:' .. string.sub(lock, 6)
73 redis.call('del', signal)
74 redis.call('lpush', signal, 1)
75 redis.call('expire', signal, 1)
76 redis.call('del', lock)
77 end
78 return #locks
79 """
80
81
82 class AlreadyAcquired(RuntimeError):
83 pass
84
85
86 class NotAcquired(RuntimeError):
87 pass
88
89
90 class AlreadyStarted(RuntimeError):
91 pass
92
93
94 class TimeoutNotUsable(RuntimeError):
95 pass
96
97
98 class InvalidTimeout(RuntimeError):
99 pass
100
101
102 class TimeoutTooLarge(RuntimeError):
103 pass
104
105
106 class NotExpirable(RuntimeError):
107 pass
108
109
110 class Lock(object):
111 """
112 A Lock context manager implemented via redis SETNX/BLPOP.
113 """
114 unlock_script = None
115 extend_script = None
116 reset_script = None
117 reset_all_script = None
118
119 def __init__(self, redis_client, name, expire=None, id=None, auto_renewal=False, strict=True, signal_expire=1000):
120 """
121 :param redis_client:
122 An instance of :class:`~StrictRedis`.
123 :param name:
124 The name (redis key) the lock should have.
125 :param expire:
126 The lock expiry time in seconds. If left at the default (None)
127 the lock will not expire.
128 :param id:
129 The ID (redis value) the lock should have. A random value is
130 generated when left at the default.
131
132 Note that if you specify this then the lock is marked as "held". Acquires
133 won't be possible.
134 :param auto_renewal:
135 If set to ``True``, Lock will automatically renew the lock so that it
136 doesn't expire for as long as the lock is held (acquire() called
137 or running in a context manager).
138
139 Implementation note: Renewal will happen using a daemon thread with
140 an interval of ``expire*2/3``. If wishing to use a different renewal
141 time, subclass Lock, call ``super().__init__()`` then set
142 ``self._lock_renewal_interval`` to your desired interval.
143 :param strict:
144 If set ``True`` then the ``redis_client`` needs to be an instance of ``redis.StrictRedis``.
145 :param signal_expire:
146 Advanced option to override signal list expiration in milliseconds. Increase it for very slow clients. Default: ``1000``.
147 """
148 if strict and not isinstance(redis_client, StrictRedis):
149 raise ValueError("redis_client must be instance of StrictRedis. "
150 "Use strict=False if you know what you're doing.")
151 if auto_renewal and expire is None:
152 raise ValueError("Expire may not be None when auto_renewal is set")
153
154 self._client = redis_client
155
156 if expire:
157 expire = int(expire)
158 if expire < 0:
159 raise ValueError("A negative expire is not acceptable.")
160 else:
161 expire = None
162 self._expire = expire
163
164 self._signal_expire = signal_expire
165 if id is None:
166 self._id = b64encode(urandom(18)).decode('ascii')
167 elif isinstance(id, binary_type):
168 try:
169 self._id = id.decode('ascii')
170 except UnicodeDecodeError:
171 self._id = b64encode(id).decode('ascii')
172 elif isinstance(id, text_type):
173 self._id = id
174 else:
175 raise TypeError("Incorrect type for `id`. Must be bytes/str not %s." % type(id))
176 self._name = 'lock:' + name
177 self._signal = 'lock-signal:' + name
178 self._lock_renewal_interval = (float(expire) * 2 / 3
179 if auto_renewal
180 else None)
181 self._lock_renewal_thread = None
182
183 self.register_scripts(redis_client)
184
185 @classmethod
186 def register_scripts(cls, redis_client):
187 global reset_all_script
188 if reset_all_script is None:
189 reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
190 cls.unlock_script = redis_client.register_script(UNLOCK_SCRIPT)
191 cls.extend_script = redis_client.register_script(EXTEND_SCRIPT)
192 cls.reset_script = redis_client.register_script(RESET_SCRIPT)
193 cls.reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
194
195 @property
196 def _held(self):
197 return self.id == self.get_owner_id()
198
199 def reset(self):
200 """
201 Forcibly deletes the lock. Use this with care.
202 """
203 self.reset_script(client=self._client, keys=(self._name, self._signal), args=(self.id, self._signal_expire))
204
205 @property
206 def id(self):
207 return self._id
208
209 def get_owner_id(self):
210 owner_id = self._client.get(self._name)
211 if isinstance(owner_id, binary_type):
212 owner_id = owner_id.decode('ascii', 'replace')
213 return owner_id
214
215 def acquire(self, blocking=True, timeout=None):
216 """
217 :param blocking:
218 Boolean value specifying whether lock should be blocking or not.
219 :param timeout:
220 An integer value specifying the maximum number of seconds to block.
221 """
222 logger = loggers["acquire"]
223
224 logger.debug("Getting %r ...", self._name)
225
226 if self._held:
227 raise AlreadyAcquired("Already acquired from this Lock instance.")
228
229 if not blocking and timeout is not None:
230 raise TimeoutNotUsable("Timeout cannot be used if blocking=False")
231
232 if timeout:
233 timeout = int(timeout)
234 if timeout < 0:
235 raise InvalidTimeout("Timeout (%d) cannot be less than or equal to 0" % timeout)
236
237 if self._expire and not self._lock_renewal_interval and timeout > self._expire:
238 raise TimeoutTooLarge("Timeout (%d) cannot be greater than expire (%d)" % (timeout, self._expire))
239
240 busy = True
241 blpop_timeout = timeout or self._expire or 0
242 timed_out = False
243 while busy:
244 busy = not self._client.set(self._name, self._id, nx=True, ex=self._expire)
245 if busy:
246 if timed_out:
247 return False
248 elif blocking:
249 timed_out = not self._client.blpop(self._signal, blpop_timeout) and timeout
250 else:
251 logger.warning("Failed to get %r.", self._name)
252 return False
253
254 logger.info("Got lock for %r.", self._name)
255 if self._lock_renewal_interval is not None:
256 self._start_lock_renewer()
257 return True
258
259 def extend(self, expire=None):
260 """Extends expiration time of the lock.
261
262 :param expire:
263 New expiration time. If ``None`` - `expire` provided during
264 lock initialization will be taken.
265 """
266 if expire:
267 expire = int(expire)
268 if expire < 0:
269 raise ValueError("A negative expire is not acceptable.")
270 elif self._expire is not None:
271 expire = self._expire
272 else:
273 raise TypeError(
274 "To extend a lock 'expire' must be provided as an "
275 "argument to extend() method or at initialization time."
276 )
277
278 error = self.extend_script(client=self._client, keys=(self._name, self._signal), args=(self._id, expire))
279 if error == 1:
280 raise NotAcquired("Lock %s is not acquired or it already expired." % self._name)
281 elif error == 2:
282 raise NotExpirable("Lock %s has no assigned expiration time" % self._name)
283 elif error:
284 raise RuntimeError("Unsupported error code %s from EXTEND script" % error)
285
286 @staticmethod
287 def _lock_renewer(lockref, interval, stop):
288 """
289 Renew the lock key in redis every `interval` seconds for as long
290 as `self._lock_renewal_thread.should_exit` is False.
291 """
292 while not stop.wait(timeout=interval):
293 loggers["refresh.thread.start"].debug("Refreshing lock")
294 lock = lockref()
295 if lock is None:
296 loggers["refresh.thread.stop"].debug(
297 "The lock no longer exists, stopping lock refreshing"
298 )
299 break
300 lock.extend(expire=lock._expire)
301 del lock
302 loggers["refresh.thread.exit"].debug("Exit requested, stopping lock refreshing")
303
304 def _start_lock_renewer(self):
305 """
306 Starts the lock refresher thread.
307 """
308 if self._lock_renewal_thread is not None:
309 raise AlreadyStarted("Lock refresh thread already started")
310
311 loggers["refresh.start"].debug(
312 "Starting thread to refresh lock every %s seconds",
313 self._lock_renewal_interval
314 )
315 self._lock_renewal_stop = threading.Event()
316 self._lock_renewal_thread = threading.Thread(
317 group=None,
318 target=self._lock_renewer,
319 kwargs={'lockref': weakref.ref(self),
320 'interval': self._lock_renewal_interval,
321 'stop': self._lock_renewal_stop}
322 )
323 self._lock_renewal_thread.setDaemon(True)
324 self._lock_renewal_thread.start()
325
326 def _stop_lock_renewer(self):
327 """
328 Stop the lock renewer.
329
330 This signals the renewal thread and waits for its exit.
331 """
332 if self._lock_renewal_thread is None or not self._lock_renewal_thread.is_alive():
333 return
334 loggers["refresh.shutdown"].debug("Signalling the lock refresher to stop")
335 self._lock_renewal_stop.set()
336 self._lock_renewal_thread.join()
337 self._lock_renewal_thread = None
338 loggers["refresh.exit"].debug("Lock refresher has stopped")
339
340 def __enter__(self):
341 acquired = self.acquire(blocking=True)
342 assert acquired, "Lock wasn't acquired, but blocking=True"
343 return self
344
345 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
346 self.release()
347
348 def release(self):
349 """Releases the lock, that was acquired with the same object.
350
351 .. note::
352
353 If you want to release a lock that you acquired in a different place you have two choices:
354
355 * Use ``Lock("name", id=id_from_other_place).release()``
356 * Use ``Lock("name").reset()``
357 """
358 if self._lock_renewal_thread is not None:
359 self._stop_lock_renewer()
360 loggers["release"].debug("Releasing %r.", self._name)
361 error = self.unlock_script(client=self._client, keys=(self._name, self._signal), args=(self._id, self._signal_expire))
362 if error == 1:
363 raise NotAcquired("Lock %s is not acquired or it already expired." % self._name)
364 elif error:
365 raise RuntimeError("Unsupported error code %s from EXTEND script." % error)
366
367 def locked(self):
368 """
369 Return true if the lock is acquired.
370
371 Checks that lock with same name already exists. This method returns true, even if
372 lock have another id.
373 """
374 return self._client.exists(self._name) == 1
375
376
377 reset_all_script = None
378
379
380 def reset_all(redis_client):
381 """
382 Forcibly deletes all locks if its remains (like a crash reason). Use this with care.
383
384 :param redis_client:
385 An instance of :class:`~StrictRedis`.
386 """
387 Lock.register_scripts(redis_client)
388
389 reset_all_script(client=redis_client) # noqa
@@ -119,8 +119,16 b' class BaseAppView(object):'
119 request.matched_route.name, self._rhodecode_db_user)
119 request.matched_route.name, self._rhodecode_db_user)
120
120
121 def _maybe_needs_password_change(self, view_name, user_obj):
121 def _maybe_needs_password_change(self, view_name, user_obj):
122
123 dont_check_views = [
124 'channelstream_connect'
125 ]
126 if view_name in dont_check_views:
127 return
128
122 log.debug('Checking if user %s needs password change on view %s',
129 log.debug('Checking if user %s needs password change on view %s',
123 user_obj, view_name)
130 user_obj, view_name)
131
124 skip_user_views = [
132 skip_user_views = [
125 'logout', 'login',
133 'logout', 'login',
126 'my_account_password', 'my_account_password_update'
134 'my_account_password', 'my_account_password_update'
@@ -326,8 +326,9 b' class RepoPullRequestsView(RepoAppView, '
326 _new_state = {
326 _new_state = {
327 'created': PullRequest.STATE_CREATED,
327 'created': PullRequest.STATE_CREATED,
328 }.get(self.request.GET.get('force_state'))
328 }.get(self.request.GET.get('force_state'))
329 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
329
330
330 if c.is_super_admin and _new_state:
331 if can_force_state and _new_state:
331 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
332 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
332 h.flash(
333 h.flash(
333 _('Pull Request state was force changed to `{}`').format(_new_state),
334 _('Pull Request state was force changed to `{}`').format(_new_state),
@@ -1268,6 +1269,9 b' class RepoPullRequestsView(RepoAppView, '
1268
1269
1269 c = self.load_default_context()
1270 c = self.load_default_context()
1270 redirect_url = None
1271 redirect_url = None
1272 # we do this check as first, because we want to know ASAP in the flow that
1273 # pr is updating currently
1274 is_state_changing = pull_request.is_state_changing()
1271
1275
1272 if pull_request.is_closed():
1276 if pull_request.is_closed():
1273 log.debug('update: forbidden because pull request is closed')
1277 log.debug('update: forbidden because pull request is closed')
@@ -1276,7 +1280,6 b' class RepoPullRequestsView(RepoAppView, '
1276 return {'response': True,
1280 return {'response': True,
1277 'redirect_url': redirect_url}
1281 'redirect_url': redirect_url}
1278
1282
1279 is_state_changing = pull_request.is_state_changing()
1280 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1283 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1281
1284
1282 # only owner or admin can update it
1285 # only owner or admin can update it
@@ -1285,7 +1288,8 b' class RepoPullRequestsView(RepoAppView, '
1285
1288
1286 if allowed_to_update:
1289 if allowed_to_update:
1287 controls = peppercorn.parse(self.request.POST.items())
1290 controls = peppercorn.parse(self.request.POST.items())
1288 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1291 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1292 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1289
1293
1290 if 'review_members' in controls:
1294 if 'review_members' in controls:
1291 self._update_reviewers(
1295 self._update_reviewers(
@@ -1299,7 +1303,7 b' class RepoPullRequestsView(RepoAppView, '
1299 pull_request, controls['observer_members'],
1303 pull_request, controls['observer_members'],
1300 pull_request.reviewer_data,
1304 pull_request.reviewer_data,
1301 PullRequestReviewers.ROLE_OBSERVER)
1305 PullRequestReviewers.ROLE_OBSERVER)
1302 elif str2bool(self.request.POST.get('update_commits', 'false')):
1306 elif do_update_commits:
1303 if is_state_changing:
1307 if is_state_changing:
1304 log.debug('commits update: forbidden because pull request is in state %s',
1308 log.debug('commits update: forbidden because pull request is in state %s',
1305 pull_request.pull_request_state)
1309 pull_request.pull_request_state)
@@ -1352,8 +1356,9 b' class RepoPullRequestsView(RepoAppView, '
1352
1356
1353 def _update_commits(self, c, pull_request):
1357 def _update_commits(self, c, pull_request):
1354 _ = self.request.translate
1358 _ = self.request.translate
1359 log.debug('pull-request: running update commits actions')
1355
1360
1356 @retry(exception=Exception, n_tries=3)
1361 @retry(exception=Exception, n_tries=3, delay=2)
1357 def commits_update():
1362 def commits_update():
1358 return PullRequestModel().update_commits(
1363 return PullRequestModel().update_commits(
1359 pull_request, self._rhodecode_db_user)
1364 pull_request, self._rhodecode_db_user)
@@ -34,6 +34,9 b' log = logging.getLogger(__name__)'
34
34
35
35
36 class SshWrapper(object):
36 class SshWrapper(object):
37 hg_cmd_pat = re.compile(r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$')
38 git_cmd_pat = re.compile(r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
39 svn_cmd_pat = re.compile(r'^svnserve -t')
37
40
38 def __init__(self, command, connection_info, mode,
41 def __init__(self, command, connection_info, mode,
39 user, user_id, key_id, shell, ini_path, env):
42 user, user_id, key_id, shell, ini_path, env):
@@ -90,35 +93,42 b' class SshWrapper(object):'
90 return conn
93 return conn
91
94
92 def maybe_translate_repo_uid(self, repo_name):
95 def maybe_translate_repo_uid(self, repo_name):
96 _org_name = repo_name
97 if _org_name.startswith('_'):
98 # remove format of _ID/subrepo
99 _org_name = _org_name.split('/', 1)[0]
100
93 if repo_name.startswith('_'):
101 if repo_name.startswith('_'):
94 from rhodecode.model.repo import RepoModel
102 from rhodecode.model.repo import RepoModel
103 org_repo_name = repo_name
104 log.debug('translating UID repo %s', org_repo_name)
95 by_id_match = RepoModel().get_repo_by_id(repo_name)
105 by_id_match = RepoModel().get_repo_by_id(repo_name)
96 if by_id_match:
106 if by_id_match:
97 repo_name = by_id_match.repo_name
107 repo_name = by_id_match.repo_name
98 return repo_name
108 log.debug('translation of UID repo %s got `%s`', org_repo_name, repo_name)
109
110 return repo_name, _org_name
99
111
100 def get_repo_details(self, mode):
112 def get_repo_details(self, mode):
101 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
113 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
102 repo_name = None
114 repo_name = None
103
115
104 hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$'
116 hg_match = self.hg_cmd_pat.match(self.command)
105 hg_match = re.match(hg_pattern, self.command)
106 if hg_match is not None:
117 if hg_match is not None:
107 vcs_type = 'hg'
118 vcs_type = 'hg'
108 repo_name = self.maybe_translate_repo_uid(hg_match.group(1).strip('/'))
119 repo_id = hg_match.group(1).strip('/')
120 repo_name, org_name = self.maybe_translate_repo_uid(repo_id)
109 return vcs_type, repo_name, mode
121 return vcs_type, repo_name, mode
110
122
111 git_pattern = r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$'
123 git_match = self.git_cmd_pat.match(self.command)
112 git_match = re.match(git_pattern, self.command)
113 if git_match is not None:
124 if git_match is not None:
125 mode = git_match.group(1)
114 vcs_type = 'git'
126 vcs_type = 'git'
115 repo_name = self.maybe_translate_repo_uid(git_match.group(2).strip('/'))
127 repo_id = git_match.group(2).strip('/')
116 mode = git_match.group(1)
128 repo_name, org_name = self.maybe_translate_repo_uid(repo_id)
117 return vcs_type, repo_name, mode
129 return vcs_type, repo_name, mode
118
130
119 svn_pattern = r'^svnserve -t'
131 svn_match = self.svn_cmd_pat.match(self.command)
120 svn_match = re.match(svn_pattern, self.command)
121
122 if svn_match is not None:
132 if svn_match is not None:
123 vcs_type = 'svn'
133 vcs_type = 'svn'
124 # Repo name should be extracted from the input stream, we're unable to
134 # Repo name should be extracted from the input stream, we're unable to
@@ -261,7 +261,6 b' def store(action, user, action_data=None'
261 ip_address = safe_unicode(ip_addr)
261 ip_address = safe_unicode(ip_addr)
262
262
263 with sa_session.no_autoflush:
263 with sa_session.no_autoflush:
264 update_user_last_activity(sa_session, user_id)
265
264
266 user_log = _store_log(
265 user_log = _store_log(
267 action_name=action_name,
266 action_name=action_name,
@@ -275,11 +274,15 b' def store(action, user, action_data=None'
275 )
274 )
276
275
277 sa_session.add(user_log)
276 sa_session.add(user_log)
277 if commit:
278 sa_session.commit()
279 entry_id = user_log.entry_id or ''
280
281 update_user_last_activity(sa_session, user_id)
278
282
279 if commit:
283 if commit:
280 sa_session.commit()
284 sa_session.commit()
281
285
282 entry_id = user_log.entry_id or ''
283 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
286 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
284 entry_id, action_name, user_id, username, ip_address)
287 entry_id, action_name, user_id, username, ip_address)
285
288
@@ -295,4 +298,6 b' def update_user_last_activity(sa_session'
295 log.debug(
298 log.debug(
296 'updated user `%s` last activity to:%s', user_id, _last_activity)
299 'updated user `%s` last activity to:%s', user_id, _last_activity)
297 except Exception:
300 except Exception:
298 log.exception("Failed last activity update")
301 log.exception("Failed last activity update for user_id: %s", user_id)
302 sa_session.rollback()
303
@@ -102,7 +102,7 b' def send_email(recipients, subject, body'
102 # Location of maildir
102 # Location of maildir
103 # queue_path='',
103 # queue_path='',
104
104
105 default_sender=email_config.get('app_email_from', 'RhodeCode'),
105 default_sender=email_config.get('app_email_from', 'RhodeCode-noreply@rhodecode.com'),
106
106
107 debug=str2bool(email_config.get('smtp_debug')),
107 debug=str2bool(email_config.get('smtp_debug')),
108 # /usr/sbin/sendmail Sendmail executable
108 # /usr/sbin/sendmail Sendmail executable
@@ -183,8 +183,11 b' class FileNamespaceBackend(PickleSeriali'
183 return False
183 return False
184
184
185 with self._dbm_file(True) as dbm:
185 with self._dbm_file(True) as dbm:
186
186 try:
187 return filter(cond, dbm.keys())
187 return filter(cond, dbm.keys())
188 except Exception:
189 log.error('Failed to fetch DBM keys from DB: %s', self.get_store())
190 raise
188
191
189 def get_store(self):
192 def get_store(self):
190 return self.filename
193 return self.filename
@@ -283,11 +286,18 b' class BaseRedisBackend(redis_backend.Red'
283 pipe.execute()
286 pipe.execute()
284
287
285 def get_mutex(self, key):
288 def get_mutex(self, key):
286 u = redis_backend.u
287 if self.distributed_lock:
289 if self.distributed_lock:
288 lock_key = u('_lock_{0}').format(key)
290 import redis_lock
291 lock_key = redis_backend.u('_lock_{0}').format(key)
289 log.debug('Trying to acquire Redis lock for key %s', lock_key)
292 log.debug('Trying to acquire Redis lock for key %s', lock_key)
290 return self.client.lock(lock_key, self.lock_timeout, self.lock_sleep)
293 lock = redis_lock.Lock(
294 redis_client=self.client,
295 name=lock_key,
296 expire=self.lock_timeout,
297 auto_renewal=False,
298 strict=True,
299 )
300 return lock
291 else:
301 else:
292 return None
302 return None
293
303
@@ -1198,14 +1198,20 b' class PullRequestModel(BaseModel):'
1198 pull_request=pull_request,
1198 pull_request=pull_request,
1199 revision=commit_id)
1199 revision=commit_id)
1200
1200
1201 # initial commit
1202 Session().commit()
1203
1204 if pr_has_changes:
1201 # send update email to users
1205 # send update email to users
1202 try:
1206 try:
1203 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1207 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1204 ancestor_commit_id=ancestor_commit_id,
1208 ancestor_commit_id=ancestor_commit_id,
1205 commit_changes=commit_changes,
1209 commit_changes=commit_changes,
1206 file_changes=file_changes)
1210 file_changes=file_changes)
1211 Session().commit()
1207 except Exception:
1212 except Exception:
1208 log.exception('Failed to send email notification to users')
1213 log.exception('Failed to send email notification to users')
1214 Session().rollback()
1209
1215
1210 log.debug(
1216 log.debug(
1211 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1217 'Updated pull request %s, added_ids: %s, common_ids: %s, '
@@ -1221,7 +1227,7 b' class PullRequestModel(BaseModel):'
1221 pull_request.pull_request_id, source_ref_id,
1227 pull_request.pull_request_id, source_ref_id,
1222 pull_request.source_ref_parts.commit_id,
1228 pull_request.source_ref_parts.commit_id,
1223 pull_request_version.pull_request_version_id)
1229 pull_request_version.pull_request_version_id)
1224 Session().commit()
1230
1225 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1231 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1226
1232
1227 return UpdateResponse(
1233 return UpdateResponse(
@@ -22,6 +22,7 b''
22 import colander
22 import colander
23 import deform.widget
23 import deform.widget
24
24
25 from rhodecode.model.validation_schema.utils import username_converter
25 from rhodecode.translation import _
26 from rhodecode.translation import _
26 from rhodecode.model.validation_schema import validators, preparers, types
27 from rhodecode.model.validation_schema import validators, preparers, types
27
28
@@ -120,6 +121,7 b' def deferred_repo_group_owner_validator('
120
121
121 def repo_owner_validator(node, value):
122 def repo_owner_validator(node, value):
122 from rhodecode.model.db import User
123 from rhodecode.model.db import User
124 value = username_converter(value)
123 existing = User.get_by_username(value)
125 existing = User.get_by_username(value)
124 if not existing:
126 if not existing:
125 msg = _(u'Repo group owner with id `{}` does not exists').format(
127 msg = _(u'Repo group owner with id `{}` does not exists').format(
@@ -22,7 +22,7 b' import colander'
22 import deform.widget
22 import deform.widget
23
23
24 from rhodecode.translation import _
24 from rhodecode.translation import _
25 from rhodecode.model.validation_schema.utils import convert_to_optgroup
25 from rhodecode.model.validation_schema.utils import convert_to_optgroup, username_converter
26 from rhodecode.model.validation_schema import validators, preparers, types
26 from rhodecode.model.validation_schema import validators, preparers, types
27
27
28 DEFAULT_LANDING_REF = 'rev:tip'
28 DEFAULT_LANDING_REF = 'rev:tip'
@@ -55,6 +55,7 b' def deferred_repo_owner_validator(node, '
55
55
56 def repo_owner_validator(node, value):
56 def repo_owner_validator(node, value):
57 from rhodecode.model.db import User
57 from rhodecode.model.db import User
58 value = username_converter(value)
58 existing = User.get_by_username(value)
59 existing = User.get_by_username(value)
59 if not existing:
60 if not existing:
60 msg = _(u'Repo owner with id `{}` does not exists').format(value)
61 msg = _(u'Repo owner with id `{}` does not exists').format(value)
@@ -21,6 +21,7 b' import re'
21 import colander
21 import colander
22
22
23 from rhodecode.model.validation_schema import types, validators
23 from rhodecode.model.validation_schema import types, validators
24 from rhodecode.model.validation_schema.utils import username_converter
24 from rhodecode.translation import _
25 from rhodecode.translation import _
25
26
26
27
@@ -43,6 +44,7 b' def deferred_user_group_owner_validator('
43
44
44 def owner_validator(node, value):
45 def owner_validator(node, value):
45 from rhodecode.model.db import User
46 from rhodecode.model.db import User
47 value = username_converter(value)
46 existing = User.get_by_username(value)
48 existing = User.get_by_username(value)
47 if not existing:
49 if not existing:
48 msg = _(u'User group owner with id `{}` does not exists').format(value)
50 msg = _(u'User group owner with id `{}` does not exists').format(value)
@@ -47,3 +47,10 b' def convert_to_optgroup(items):'
47 result.append((value, label))
47 result.append((value, label))
48
48
49 return result
49 return result
50
51
52 def username_converter(value):
53 for noise in ('/', ',', '*', '"', "'", '<', '>', '(', ')', '[', ']', ';'):
54 value = value.replace(noise, '')
55
56 return value
@@ -274,11 +274,12 b''
274 % if c.state_progressing:
274 % if c.state_progressing:
275
275
276 <h2 style="text-align: center">
276 <h2 style="text-align: center">
277 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
277 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span><br/>
278 ${_('Consider refreshing the page to check if the status transition was finished')}.
278
279
279 % if c.is_super_admin:
280 % if c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name):
280 <br/>
281 <br/>
281 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
282 ${_('If you think this is an error try ')}<a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
282 % endif
283 % endif
283 </h2>
284 </h2>
284
285
@@ -20,6 +20,7 b''
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.events import UserPermissionsChange
23 from rhodecode.lib.utils2 import StrictAttributeDict
24 from rhodecode.lib.utils2 import StrictAttributeDict
24 from rhodecode.tests.events.conftest import EventCatcher
25 from rhodecode.tests.events.conftest import EventCatcher
25
26
@@ -97,7 +98,7 b' def test_vcs_repo_push_event_serialize(c'
97 def test_create_delete_repo_fires_events(backend):
98 def test_create_delete_repo_fires_events(backend):
98 with EventCatcher() as event_catcher:
99 with EventCatcher() as event_catcher:
99 repo = backend.create_repo()
100 repo = backend.create_repo()
100 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreateEvent]
101 assert event_catcher.events_types == [RepoPreCreateEvent, RepoCreateEvent, UserPermissionsChange]
101
102
102 with EventCatcher() as event_catcher:
103 with EventCatcher() as event_catcher:
103 RepoModel().delete(repo)
104 RepoModel().delete(repo)
@@ -127,7 +127,7 b' class TestPullRequestModel(object):'
127 Session().commit()
127 Session().commit()
128
128
129 prs = PullRequestModel().get_awaiting_my_review(
129 prs = PullRequestModel().get_awaiting_my_review(
130 pull_request.target_repo, user_id=pull_request.author.user_id)
130 pull_request.target_repo.repo_name, user_id=pull_request.author.user_id)
131 assert isinstance(prs, list)
131 assert isinstance(prs, list)
132 assert len(prs) == 1
132 assert len(prs) == 1
133
133
@@ -138,7 +138,7 b' class TestPullRequestModel(object):'
138 Session().commit()
138 Session().commit()
139
139
140 pr_count = PullRequestModel().count_awaiting_my_review(
140 pr_count = PullRequestModel().count_awaiting_my_review(
141 pull_request.target_repo, user_id=pull_request.author.user_id)
141 pull_request.target_repo.repo_name, user_id=pull_request.author.user_id)
142 assert pr_count == 1
142 assert pr_count == 1
143
143
144 def test_delete_calls_cleanup_merge(self, pull_request):
144 def test_delete_calls_cleanup_merge(self, pull_request):
General Comments 0
You need to be logged in to leave comments. Login now