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 |
|
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_ |
|
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_ |
|
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