##// END OF EJS Templates
urls: improve removal of credentials in repository header....
ergo -
r4399:326ccc76 stable
parent child Browse files
Show More
@@ -1,1104 +1,1070 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 Some simple helper functions
23 Some simple helper functions
24 """
24 """
25
25
26 import collections
26 import collections
27 import datetime
27 import datetime
28 import dateutil.relativedelta
28 import dateutil.relativedelta
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import re
31 import re
32 import sys
32 import sys
33 import time
33 import time
34 import urllib
34 import urllib
35 import urlobject
35 import urlobject
36 import uuid
36 import uuid
37 import getpass
37 import getpass
38 from functools import update_wrapper, partial
38 from functools import update_wrapper, partial
39
39
40 import pygments.lexers
40 import pygments.lexers
41 import sqlalchemy
41 import sqlalchemy
42 import sqlalchemy.engine.url
42 import sqlalchemy.engine.url
43 import sqlalchemy.exc
43 import sqlalchemy.exc
44 import sqlalchemy.sql
44 import sqlalchemy.sql
45 import webob
45 import webob
46 import pyramid.threadlocal
46 import pyramid.threadlocal
47 from pyramid import compat
47 from pyramid import compat
48 from pyramid.settings import asbool
48 from pyramid.settings import asbool
49
49
50 import rhodecode
50 import rhodecode
51 from rhodecode.translation import _, _pluralize
51 from rhodecode.translation import _, _pluralize
52
52
53
53
54 def md5(s):
54 def md5(s):
55 return hashlib.md5(s).hexdigest()
55 return hashlib.md5(s).hexdigest()
56
56
57
57
58 def md5_safe(s):
58 def md5_safe(s):
59 return md5(safe_str(s))
59 return md5(safe_str(s))
60
60
61
61
62 def sha1(s):
62 def sha1(s):
63 return hashlib.sha1(s).hexdigest()
63 return hashlib.sha1(s).hexdigest()
64
64
65
65
66 def sha1_safe(s):
66 def sha1_safe(s):
67 return sha1(safe_str(s))
67 return sha1(safe_str(s))
68
68
69
69
70 def __get_lem(extra_mapping=None):
70 def __get_lem(extra_mapping=None):
71 """
71 """
72 Get language extension map based on what's inside pygments lexers
72 Get language extension map based on what's inside pygments lexers
73 """
73 """
74 d = collections.defaultdict(lambda: [])
74 d = collections.defaultdict(lambda: [])
75
75
76 def __clean(s):
76 def __clean(s):
77 s = s.lstrip('*')
77 s = s.lstrip('*')
78 s = s.lstrip('.')
78 s = s.lstrip('.')
79
79
80 if s.find('[') != -1:
80 if s.find('[') != -1:
81 exts = []
81 exts = []
82 start, stop = s.find('['), s.find(']')
82 start, stop = s.find('['), s.find(']')
83
83
84 for suffix in s[start + 1:stop]:
84 for suffix in s[start + 1:stop]:
85 exts.append(s[:s.find('[')] + suffix)
85 exts.append(s[:s.find('[')] + suffix)
86 return [e.lower() for e in exts]
86 return [e.lower() for e in exts]
87 else:
87 else:
88 return [s.lower()]
88 return [s.lower()]
89
89
90 for lx, t in sorted(pygments.lexers.LEXERS.items()):
90 for lx, t in sorted(pygments.lexers.LEXERS.items()):
91 m = map(__clean, t[-2])
91 m = map(__clean, t[-2])
92 if m:
92 if m:
93 m = reduce(lambda x, y: x + y, m)
93 m = reduce(lambda x, y: x + y, m)
94 for ext in m:
94 for ext in m:
95 desc = lx.replace('Lexer', '')
95 desc = lx.replace('Lexer', '')
96 d[ext].append(desc)
96 d[ext].append(desc)
97
97
98 data = dict(d)
98 data = dict(d)
99
99
100 extra_mapping = extra_mapping or {}
100 extra_mapping = extra_mapping or {}
101 if extra_mapping:
101 if extra_mapping:
102 for k, v in extra_mapping.items():
102 for k, v in extra_mapping.items():
103 if k not in data:
103 if k not in data:
104 # register new mapping2lexer
104 # register new mapping2lexer
105 data[k] = [v]
105 data[k] = [v]
106
106
107 return data
107 return data
108
108
109
109
110 def str2bool(_str):
110 def str2bool(_str):
111 """
111 """
112 returns True/False value from given string, it tries to translate the
112 returns True/False value from given string, it tries to translate the
113 string into boolean
113 string into boolean
114
114
115 :param _str: string value to translate into boolean
115 :param _str: string value to translate into boolean
116 :rtype: boolean
116 :rtype: boolean
117 :returns: boolean from given string
117 :returns: boolean from given string
118 """
118 """
119 if _str is None:
119 if _str is None:
120 return False
120 return False
121 if _str in (True, False):
121 if _str in (True, False):
122 return _str
122 return _str
123 _str = str(_str).strip().lower()
123 _str = str(_str).strip().lower()
124 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
124 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
125
125
126
126
127 def aslist(obj, sep=None, strip=True):
127 def aslist(obj, sep=None, strip=True):
128 """
128 """
129 Returns given string separated by sep as list
129 Returns given string separated by sep as list
130
130
131 :param obj:
131 :param obj:
132 :param sep:
132 :param sep:
133 :param strip:
133 :param strip:
134 """
134 """
135 if isinstance(obj, (basestring,)):
135 if isinstance(obj, (basestring,)):
136 lst = obj.split(sep)
136 lst = obj.split(sep)
137 if strip:
137 if strip:
138 lst = [v.strip() for v in lst]
138 lst = [v.strip() for v in lst]
139 return lst
139 return lst
140 elif isinstance(obj, (list, tuple)):
140 elif isinstance(obj, (list, tuple)):
141 return obj
141 return obj
142 elif obj is None:
142 elif obj is None:
143 return []
143 return []
144 else:
144 else:
145 return [obj]
145 return [obj]
146
146
147
147
148 def convert_line_endings(line, mode):
148 def convert_line_endings(line, mode):
149 """
149 """
150 Converts a given line "line end" accordingly to given mode
150 Converts a given line "line end" accordingly to given mode
151
151
152 Available modes are::
152 Available modes are::
153 0 - Unix
153 0 - Unix
154 1 - Mac
154 1 - Mac
155 2 - DOS
155 2 - DOS
156
156
157 :param line: given line to convert
157 :param line: given line to convert
158 :param mode: mode to convert to
158 :param mode: mode to convert to
159 :rtype: str
159 :rtype: str
160 :return: converted line according to mode
160 :return: converted line according to mode
161 """
161 """
162 if mode == 0:
162 if mode == 0:
163 line = line.replace('\r\n', '\n')
163 line = line.replace('\r\n', '\n')
164 line = line.replace('\r', '\n')
164 line = line.replace('\r', '\n')
165 elif mode == 1:
165 elif mode == 1:
166 line = line.replace('\r\n', '\r')
166 line = line.replace('\r\n', '\r')
167 line = line.replace('\n', '\r')
167 line = line.replace('\n', '\r')
168 elif mode == 2:
168 elif mode == 2:
169 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
169 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
170 return line
170 return line
171
171
172
172
173 def detect_mode(line, default):
173 def detect_mode(line, default):
174 """
174 """
175 Detects line break for given line, if line break couldn't be found
175 Detects line break for given line, if line break couldn't be found
176 given default value is returned
176 given default value is returned
177
177
178 :param line: str line
178 :param line: str line
179 :param default: default
179 :param default: default
180 :rtype: int
180 :rtype: int
181 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
181 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
182 """
182 """
183 if line.endswith('\r\n'):
183 if line.endswith('\r\n'):
184 return 2
184 return 2
185 elif line.endswith('\n'):
185 elif line.endswith('\n'):
186 return 0
186 return 0
187 elif line.endswith('\r'):
187 elif line.endswith('\r'):
188 return 1
188 return 1
189 else:
189 else:
190 return default
190 return default
191
191
192
192
193 def safe_int(val, default=None):
193 def safe_int(val, default=None):
194 """
194 """
195 Returns int() of val if val is not convertable to int use default
195 Returns int() of val if val is not convertable to int use default
196 instead
196 instead
197
197
198 :param val:
198 :param val:
199 :param default:
199 :param default:
200 """
200 """
201
201
202 try:
202 try:
203 val = int(val)
203 val = int(val)
204 except (ValueError, TypeError):
204 except (ValueError, TypeError):
205 val = default
205 val = default
206
206
207 return val
207 return val
208
208
209
209
210 def safe_unicode(str_, from_encoding=None, use_chardet=False):
210 def safe_unicode(str_, from_encoding=None, use_chardet=False):
211 """
211 """
212 safe unicode function. Does few trick to turn str_ into unicode
212 safe unicode function. Does few trick to turn str_ into unicode
213
213
214 In case of UnicodeDecode error, we try to return it with encoding detected
214 In case of UnicodeDecode error, we try to return it with encoding detected
215 by chardet library if it fails fallback to unicode with errors replaced
215 by chardet library if it fails fallback to unicode with errors replaced
216
216
217 :param str_: string to decode
217 :param str_: string to decode
218 :rtype: unicode
218 :rtype: unicode
219 :returns: unicode object
219 :returns: unicode object
220 """
220 """
221 if isinstance(str_, unicode):
221 if isinstance(str_, unicode):
222 return str_
222 return str_
223
223
224 if not from_encoding:
224 if not from_encoding:
225 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
225 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
226 'utf8'), sep=',')
226 'utf8'), sep=',')
227 from_encoding = DEFAULT_ENCODINGS
227 from_encoding = DEFAULT_ENCODINGS
228
228
229 if not isinstance(from_encoding, (list, tuple)):
229 if not isinstance(from_encoding, (list, tuple)):
230 from_encoding = [from_encoding]
230 from_encoding = [from_encoding]
231
231
232 try:
232 try:
233 return unicode(str_)
233 return unicode(str_)
234 except UnicodeDecodeError:
234 except UnicodeDecodeError:
235 pass
235 pass
236
236
237 for enc in from_encoding:
237 for enc in from_encoding:
238 try:
238 try:
239 return unicode(str_, enc)
239 return unicode(str_, enc)
240 except UnicodeDecodeError:
240 except UnicodeDecodeError:
241 pass
241 pass
242
242
243 if use_chardet:
243 if use_chardet:
244 try:
244 try:
245 import chardet
245 import chardet
246 encoding = chardet.detect(str_)['encoding']
246 encoding = chardet.detect(str_)['encoding']
247 if encoding is None:
247 if encoding is None:
248 raise Exception()
248 raise Exception()
249 return str_.decode(encoding)
249 return str_.decode(encoding)
250 except (ImportError, UnicodeDecodeError, Exception):
250 except (ImportError, UnicodeDecodeError, Exception):
251 return unicode(str_, from_encoding[0], 'replace')
251 return unicode(str_, from_encoding[0], 'replace')
252 else:
252 else:
253 return unicode(str_, from_encoding[0], 'replace')
253 return unicode(str_, from_encoding[0], 'replace')
254
254
255 def safe_str(unicode_, to_encoding=None, use_chardet=False):
255 def safe_str(unicode_, to_encoding=None, use_chardet=False):
256 """
256 """
257 safe str function. Does few trick to turn unicode_ into string
257 safe str function. Does few trick to turn unicode_ into string
258
258
259 In case of UnicodeEncodeError, we try to return it with encoding detected
259 In case of UnicodeEncodeError, we try to return it with encoding detected
260 by chardet library if it fails fallback to string with errors replaced
260 by chardet library if it fails fallback to string with errors replaced
261
261
262 :param unicode_: unicode to encode
262 :param unicode_: unicode to encode
263 :rtype: str
263 :rtype: str
264 :returns: str object
264 :returns: str object
265 """
265 """
266
266
267 # if it's not basestr cast to str
267 # if it's not basestr cast to str
268 if not isinstance(unicode_, compat.string_types):
268 if not isinstance(unicode_, compat.string_types):
269 return str(unicode_)
269 return str(unicode_)
270
270
271 if isinstance(unicode_, str):
271 if isinstance(unicode_, str):
272 return unicode_
272 return unicode_
273
273
274 if not to_encoding:
274 if not to_encoding:
275 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
275 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
276 'utf8'), sep=',')
276 'utf8'), sep=',')
277 to_encoding = DEFAULT_ENCODINGS
277 to_encoding = DEFAULT_ENCODINGS
278
278
279 if not isinstance(to_encoding, (list, tuple)):
279 if not isinstance(to_encoding, (list, tuple)):
280 to_encoding = [to_encoding]
280 to_encoding = [to_encoding]
281
281
282 for enc in to_encoding:
282 for enc in to_encoding:
283 try:
283 try:
284 return unicode_.encode(enc)
284 return unicode_.encode(enc)
285 except UnicodeEncodeError:
285 except UnicodeEncodeError:
286 pass
286 pass
287
287
288 if use_chardet:
288 if use_chardet:
289 try:
289 try:
290 import chardet
290 import chardet
291 encoding = chardet.detect(unicode_)['encoding']
291 encoding = chardet.detect(unicode_)['encoding']
292 if encoding is None:
292 if encoding is None:
293 raise UnicodeEncodeError()
293 raise UnicodeEncodeError()
294
294
295 return unicode_.encode(encoding)
295 return unicode_.encode(encoding)
296 except (ImportError, UnicodeEncodeError):
296 except (ImportError, UnicodeEncodeError):
297 return unicode_.encode(to_encoding[0], 'replace')
297 return unicode_.encode(to_encoding[0], 'replace')
298 else:
298 else:
299 return unicode_.encode(to_encoding[0], 'replace')
299 return unicode_.encode(to_encoding[0], 'replace')
300
300
301
301
302 def remove_suffix(s, suffix):
302 def remove_suffix(s, suffix):
303 if s.endswith(suffix):
303 if s.endswith(suffix):
304 s = s[:-1 * len(suffix)]
304 s = s[:-1 * len(suffix)]
305 return s
305 return s
306
306
307
307
308 def remove_prefix(s, prefix):
308 def remove_prefix(s, prefix):
309 if s.startswith(prefix):
309 if s.startswith(prefix):
310 s = s[len(prefix):]
310 s = s[len(prefix):]
311 return s
311 return s
312
312
313
313
314 def find_calling_context(ignore_modules=None):
314 def find_calling_context(ignore_modules=None):
315 """
315 """
316 Look through the calling stack and return the frame which called
316 Look through the calling stack and return the frame which called
317 this function and is part of core module ( ie. rhodecode.* )
317 this function and is part of core module ( ie. rhodecode.* )
318
318
319 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
319 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
320 """
320 """
321
321
322 ignore_modules = ignore_modules or []
322 ignore_modules = ignore_modules or []
323
323
324 f = sys._getframe(2)
324 f = sys._getframe(2)
325 while f.f_back is not None:
325 while f.f_back is not None:
326 name = f.f_globals.get('__name__')
326 name = f.f_globals.get('__name__')
327 if name and name.startswith(__name__.split('.')[0]):
327 if name and name.startswith(__name__.split('.')[0]):
328 if name not in ignore_modules:
328 if name not in ignore_modules:
329 return f
329 return f
330 f = f.f_back
330 f = f.f_back
331 return None
331 return None
332
332
333
333
334 def ping_connection(connection, branch):
334 def ping_connection(connection, branch):
335 if branch:
335 if branch:
336 # "branch" refers to a sub-connection of a connection,
336 # "branch" refers to a sub-connection of a connection,
337 # we don't want to bother pinging on these.
337 # we don't want to bother pinging on these.
338 return
338 return
339
339
340 # turn off "close with result". This flag is only used with
340 # turn off "close with result". This flag is only used with
341 # "connectionless" execution, otherwise will be False in any case
341 # "connectionless" execution, otherwise will be False in any case
342 save_should_close_with_result = connection.should_close_with_result
342 save_should_close_with_result = connection.should_close_with_result
343 connection.should_close_with_result = False
343 connection.should_close_with_result = False
344
344
345 try:
345 try:
346 # run a SELECT 1. use a core select() so that
346 # run a SELECT 1. use a core select() so that
347 # the SELECT of a scalar value without a table is
347 # the SELECT of a scalar value without a table is
348 # appropriately formatted for the backend
348 # appropriately formatted for the backend
349 connection.scalar(sqlalchemy.sql.select([1]))
349 connection.scalar(sqlalchemy.sql.select([1]))
350 except sqlalchemy.exc.DBAPIError as err:
350 except sqlalchemy.exc.DBAPIError as err:
351 # catch SQLAlchemy's DBAPIError, which is a wrapper
351 # catch SQLAlchemy's DBAPIError, which is a wrapper
352 # for the DBAPI's exception. It includes a .connection_invalidated
352 # for the DBAPI's exception. It includes a .connection_invalidated
353 # attribute which specifies if this connection is a "disconnect"
353 # attribute which specifies if this connection is a "disconnect"
354 # condition, which is based on inspection of the original exception
354 # condition, which is based on inspection of the original exception
355 # by the dialect in use.
355 # by the dialect in use.
356 if err.connection_invalidated:
356 if err.connection_invalidated:
357 # run the same SELECT again - the connection will re-validate
357 # run the same SELECT again - the connection will re-validate
358 # itself and establish a new connection. The disconnect detection
358 # itself and establish a new connection. The disconnect detection
359 # here also causes the whole connection pool to be invalidated
359 # here also causes the whole connection pool to be invalidated
360 # so that all stale connections are discarded.
360 # so that all stale connections are discarded.
361 connection.scalar(sqlalchemy.sql.select([1]))
361 connection.scalar(sqlalchemy.sql.select([1]))
362 else:
362 else:
363 raise
363 raise
364 finally:
364 finally:
365 # restore "close with result"
365 # restore "close with result"
366 connection.should_close_with_result = save_should_close_with_result
366 connection.should_close_with_result = save_should_close_with_result
367
367
368
368
369 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
369 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
370 """Custom engine_from_config functions."""
370 """Custom engine_from_config functions."""
371 log = logging.getLogger('sqlalchemy.engine')
371 log = logging.getLogger('sqlalchemy.engine')
372 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
372 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
373 debug = asbool(configuration.pop('sqlalchemy.db1.debug_query', None))
373 debug = asbool(configuration.pop('sqlalchemy.db1.debug_query', None))
374
374
375 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
375 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
376
376
377 def color_sql(sql):
377 def color_sql(sql):
378 color_seq = '\033[1;33m' # This is yellow: code 33
378 color_seq = '\033[1;33m' # This is yellow: code 33
379 normal = '\x1b[0m'
379 normal = '\x1b[0m'
380 return ''.join([color_seq, sql, normal])
380 return ''.join([color_seq, sql, normal])
381
381
382 if use_ping_connection:
382 if use_ping_connection:
383 log.debug('Adding ping_connection on the engine config.')
383 log.debug('Adding ping_connection on the engine config.')
384 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
384 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
385
385
386 if debug:
386 if debug:
387 # attach events only for debug configuration
387 # attach events only for debug configuration
388 def before_cursor_execute(conn, cursor, statement,
388 def before_cursor_execute(conn, cursor, statement,
389 parameters, context, executemany):
389 parameters, context, executemany):
390 setattr(conn, 'query_start_time', time.time())
390 setattr(conn, 'query_start_time', time.time())
391 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
391 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
392 calling_context = find_calling_context(ignore_modules=[
392 calling_context = find_calling_context(ignore_modules=[
393 'rhodecode.lib.caching_query',
393 'rhodecode.lib.caching_query',
394 'rhodecode.model.settings',
394 'rhodecode.model.settings',
395 ])
395 ])
396 if calling_context:
396 if calling_context:
397 log.info(color_sql('call context %s:%s' % (
397 log.info(color_sql('call context %s:%s' % (
398 calling_context.f_code.co_filename,
398 calling_context.f_code.co_filename,
399 calling_context.f_lineno,
399 calling_context.f_lineno,
400 )))
400 )))
401
401
402 def after_cursor_execute(conn, cursor, statement,
402 def after_cursor_execute(conn, cursor, statement,
403 parameters, context, executemany):
403 parameters, context, executemany):
404 delattr(conn, 'query_start_time')
404 delattr(conn, 'query_start_time')
405
405
406 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
406 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
407 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
407 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
408
408
409 return engine
409 return engine
410
410
411
411
412 def get_encryption_key(config):
412 def get_encryption_key(config):
413 secret = config.get('rhodecode.encrypted_values.secret')
413 secret = config.get('rhodecode.encrypted_values.secret')
414 default = config['beaker.session.secret']
414 default = config['beaker.session.secret']
415 return secret or default
415 return secret or default
416
416
417
417
418 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
418 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
419 short_format=False):
419 short_format=False):
420 """
420 """
421 Turns a datetime into an age string.
421 Turns a datetime into an age string.
422 If show_short_version is True, this generates a shorter string with
422 If show_short_version is True, this generates a shorter string with
423 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
423 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
424
424
425 * IMPORTANT*
425 * IMPORTANT*
426 Code of this function is written in special way so it's easier to
426 Code of this function is written in special way so it's easier to
427 backport it to javascript. If you mean to update it, please also update
427 backport it to javascript. If you mean to update it, please also update
428 `jquery.timeago-extension.js` file
428 `jquery.timeago-extension.js` file
429
429
430 :param prevdate: datetime object
430 :param prevdate: datetime object
431 :param now: get current time, if not define we use
431 :param now: get current time, if not define we use
432 `datetime.datetime.now()`
432 `datetime.datetime.now()`
433 :param show_short_version: if it should approximate the date and
433 :param show_short_version: if it should approximate the date and
434 return a shorter string
434 return a shorter string
435 :param show_suffix:
435 :param show_suffix:
436 :param short_format: show short format, eg 2D instead of 2 days
436 :param short_format: show short format, eg 2D instead of 2 days
437 :rtype: unicode
437 :rtype: unicode
438 :returns: unicode words describing age
438 :returns: unicode words describing age
439 """
439 """
440
440
441 def _get_relative_delta(now, prevdate):
441 def _get_relative_delta(now, prevdate):
442 base = dateutil.relativedelta.relativedelta(now, prevdate)
442 base = dateutil.relativedelta.relativedelta(now, prevdate)
443 return {
443 return {
444 'year': base.years,
444 'year': base.years,
445 'month': base.months,
445 'month': base.months,
446 'day': base.days,
446 'day': base.days,
447 'hour': base.hours,
447 'hour': base.hours,
448 'minute': base.minutes,
448 'minute': base.minutes,
449 'second': base.seconds,
449 'second': base.seconds,
450 }
450 }
451
451
452 def _is_leap_year(year):
452 def _is_leap_year(year):
453 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
453 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
454
454
455 def get_month(prevdate):
455 def get_month(prevdate):
456 return prevdate.month
456 return prevdate.month
457
457
458 def get_year(prevdate):
458 def get_year(prevdate):
459 return prevdate.year
459 return prevdate.year
460
460
461 now = now or datetime.datetime.now()
461 now = now or datetime.datetime.now()
462 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
462 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
463 deltas = {}
463 deltas = {}
464 future = False
464 future = False
465
465
466 if prevdate > now:
466 if prevdate > now:
467 now_old = now
467 now_old = now
468 now = prevdate
468 now = prevdate
469 prevdate = now_old
469 prevdate = now_old
470 future = True
470 future = True
471 if future:
471 if future:
472 prevdate = prevdate.replace(microsecond=0)
472 prevdate = prevdate.replace(microsecond=0)
473 # Get date parts deltas
473 # Get date parts deltas
474 for part in order:
474 for part in order:
475 rel_delta = _get_relative_delta(now, prevdate)
475 rel_delta = _get_relative_delta(now, prevdate)
476 deltas[part] = rel_delta[part]
476 deltas[part] = rel_delta[part]
477
477
478 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
478 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
479 # not 1 hour, -59 minutes and -59 seconds)
479 # not 1 hour, -59 minutes and -59 seconds)
480 offsets = [[5, 60], [4, 60], [3, 24]]
480 offsets = [[5, 60], [4, 60], [3, 24]]
481 for element in offsets: # seconds, minutes, hours
481 for element in offsets: # seconds, minutes, hours
482 num = element[0]
482 num = element[0]
483 length = element[1]
483 length = element[1]
484
484
485 part = order[num]
485 part = order[num]
486 carry_part = order[num - 1]
486 carry_part = order[num - 1]
487
487
488 if deltas[part] < 0:
488 if deltas[part] < 0:
489 deltas[part] += length
489 deltas[part] += length
490 deltas[carry_part] -= 1
490 deltas[carry_part] -= 1
491
491
492 # Same thing for days except that the increment depends on the (variable)
492 # Same thing for days except that the increment depends on the (variable)
493 # number of days in the month
493 # number of days in the month
494 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
494 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
495 if deltas['day'] < 0:
495 if deltas['day'] < 0:
496 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
496 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
497 deltas['day'] += 29
497 deltas['day'] += 29
498 else:
498 else:
499 deltas['day'] += month_lengths[get_month(prevdate) - 1]
499 deltas['day'] += month_lengths[get_month(prevdate) - 1]
500
500
501 deltas['month'] -= 1
501 deltas['month'] -= 1
502
502
503 if deltas['month'] < 0:
503 if deltas['month'] < 0:
504 deltas['month'] += 12
504 deltas['month'] += 12
505 deltas['year'] -= 1
505 deltas['year'] -= 1
506
506
507 # Format the result
507 # Format the result
508 if short_format:
508 if short_format:
509 fmt_funcs = {
509 fmt_funcs = {
510 'year': lambda d: u'%dy' % d,
510 'year': lambda d: u'%dy' % d,
511 'month': lambda d: u'%dm' % d,
511 'month': lambda d: u'%dm' % d,
512 'day': lambda d: u'%dd' % d,
512 'day': lambda d: u'%dd' % d,
513 'hour': lambda d: u'%dh' % d,
513 'hour': lambda d: u'%dh' % d,
514 'minute': lambda d: u'%dmin' % d,
514 'minute': lambda d: u'%dmin' % d,
515 'second': lambda d: u'%dsec' % d,
515 'second': lambda d: u'%dsec' % d,
516 }
516 }
517 else:
517 else:
518 fmt_funcs = {
518 fmt_funcs = {
519 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
519 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
520 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
520 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
521 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
521 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
522 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
522 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
523 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
523 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
524 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
524 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
525 }
525 }
526
526
527 i = 0
527 i = 0
528 for part in order:
528 for part in order:
529 value = deltas[part]
529 value = deltas[part]
530 if value != 0:
530 if value != 0:
531
531
532 if i < 5:
532 if i < 5:
533 sub_part = order[i + 1]
533 sub_part = order[i + 1]
534 sub_value = deltas[sub_part]
534 sub_value = deltas[sub_part]
535 else:
535 else:
536 sub_value = 0
536 sub_value = 0
537
537
538 if sub_value == 0 or show_short_version:
538 if sub_value == 0 or show_short_version:
539 _val = fmt_funcs[part](value)
539 _val = fmt_funcs[part](value)
540 if future:
540 if future:
541 if show_suffix:
541 if show_suffix:
542 return _(u'in ${ago}', mapping={'ago': _val})
542 return _(u'in ${ago}', mapping={'ago': _val})
543 else:
543 else:
544 return _(_val)
544 return _(_val)
545
545
546 else:
546 else:
547 if show_suffix:
547 if show_suffix:
548 return _(u'${ago} ago', mapping={'ago': _val})
548 return _(u'${ago} ago', mapping={'ago': _val})
549 else:
549 else:
550 return _(_val)
550 return _(_val)
551
551
552 val = fmt_funcs[part](value)
552 val = fmt_funcs[part](value)
553 val_detail = fmt_funcs[sub_part](sub_value)
553 val_detail = fmt_funcs[sub_part](sub_value)
554 mapping = {'val': val, 'detail': val_detail}
554 mapping = {'val': val, 'detail': val_detail}
555
555
556 if short_format:
556 if short_format:
557 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
557 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
558 if show_suffix:
558 if show_suffix:
559 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
559 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
560 if future:
560 if future:
561 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
561 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
562 else:
562 else:
563 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
563 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
564 if show_suffix:
564 if show_suffix:
565 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
565 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
566 if future:
566 if future:
567 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
567 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
568
568
569 return datetime_tmpl
569 return datetime_tmpl
570 i += 1
570 i += 1
571 return _(u'just now')
571 return _(u'just now')
572
572
573
573
574 def age_from_seconds(seconds):
574 def age_from_seconds(seconds):
575 seconds = safe_int(seconds) or 0
575 seconds = safe_int(seconds) or 0
576 prevdate = time_to_datetime(time.time() + seconds)
576 prevdate = time_to_datetime(time.time() + seconds)
577 return age(prevdate, show_suffix=False, show_short_version=True)
577 return age(prevdate, show_suffix=False, show_short_version=True)
578
578
579
579
580 def cleaned_uri(uri):
580 def cleaned_uri(uri):
581 """
581 """
582 Quotes '[' and ']' from uri if there is only one of them.
582 Quotes '[' and ']' from uri if there is only one of them.
583 according to RFC3986 we cannot use such chars in uri
583 according to RFC3986 we cannot use such chars in uri
584 :param uri:
584 :param uri:
585 :return: uri without this chars
585 :return: uri without this chars
586 """
586 """
587 return urllib.quote(uri, safe='@$:/')
587 return urllib.quote(uri, safe='@$:/')
588
588
589
589
590 def uri_filter(uri):
591 """
592 Removes user:password from given url string
593
594 :param uri:
595 :rtype: unicode
596 :returns: filtered list of strings
597 """
598 if not uri:
599 return ''
600
601 proto = ''
602
603 for pat in ('https://', 'http://'):
604 if uri.startswith(pat):
605 uri = uri[len(pat):]
606 proto = pat
607 break
608
609 # remove passwords and username
610 uri = uri[uri.find('@') + 1:]
611
612 # get the port
613 cred_pos = uri.find(':')
614 if cred_pos == -1:
615 host, port = uri, None
616 else:
617 host, port = uri[:cred_pos], uri[cred_pos + 1:]
618
619 return filter(None, [proto, host, port])
620
621
622 def credentials_filter(uri):
590 def credentials_filter(uri):
623 """
591 """
624 Returns a url with removed credentials
592 Returns a url with removed credentials
625
593
626 :param uri:
594 :param uri:
627 """
595 """
596 import urlobject
597 url_obj = urlobject.URLObject(cleaned_uri(uri))
598 url_obj = url_obj.without_password().without_username()
628
599
629 uri = uri_filter(uri)
600 return url_obj
630 # check if we have port
631 if len(uri) > 2 and uri[2]:
632 uri[2] = ':' + uri[2]
633
634 return ''.join(uri)
635
601
636
602
637 def get_host_info(request):
603 def get_host_info(request):
638 """
604 """
639 Generate host info, to obtain full url e.g https://server.com
605 Generate host info, to obtain full url e.g https://server.com
640 use this
606 use this
641 `{scheme}://{netloc}`
607 `{scheme}://{netloc}`
642 """
608 """
643 if not request:
609 if not request:
644 return {}
610 return {}
645
611
646 qualified_home_url = request.route_url('home')
612 qualified_home_url = request.route_url('home')
647 parsed_url = urlobject.URLObject(qualified_home_url)
613 parsed_url = urlobject.URLObject(qualified_home_url)
648 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
614 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
649
615
650 return {
616 return {
651 'scheme': parsed_url.scheme,
617 'scheme': parsed_url.scheme,
652 'netloc': parsed_url.netloc+decoded_path,
618 'netloc': parsed_url.netloc+decoded_path,
653 'hostname': parsed_url.hostname,
619 'hostname': parsed_url.hostname,
654 }
620 }
655
621
656
622
657 def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
623 def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
658 qualified_home_url = request.route_url('home')
624 qualified_home_url = request.route_url('home')
659 parsed_url = urlobject.URLObject(qualified_home_url)
625 parsed_url = urlobject.URLObject(qualified_home_url)
660 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
626 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
661
627
662 args = {
628 args = {
663 'scheme': parsed_url.scheme,
629 'scheme': parsed_url.scheme,
664 'user': '',
630 'user': '',
665 'sys_user': getpass.getuser(),
631 'sys_user': getpass.getuser(),
666 # path if we use proxy-prefix
632 # path if we use proxy-prefix
667 'netloc': parsed_url.netloc+decoded_path,
633 'netloc': parsed_url.netloc+decoded_path,
668 'hostname': parsed_url.hostname,
634 'hostname': parsed_url.hostname,
669 'prefix': decoded_path,
635 'prefix': decoded_path,
670 'repo': repo_name,
636 'repo': repo_name,
671 'repoid': str(repo_id),
637 'repoid': str(repo_id),
672 'repo_type': repo_type
638 'repo_type': repo_type
673 }
639 }
674 args.update(override)
640 args.update(override)
675 args['user'] = urllib.quote(safe_str(args['user']))
641 args['user'] = urllib.quote(safe_str(args['user']))
676
642
677 for k, v in args.items():
643 for k, v in args.items():
678 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
644 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
679
645
680 # special case for SVN clone url
646 # special case for SVN clone url
681 if repo_type == 'svn':
647 if repo_type == 'svn':
682 uri_tmpl = uri_tmpl.replace('ssh://', 'svn+ssh://')
648 uri_tmpl = uri_tmpl.replace('ssh://', 'svn+ssh://')
683
649
684 # remove leading @ sign if it's present. Case of empty user
650 # remove leading @ sign if it's present. Case of empty user
685 url_obj = urlobject.URLObject(uri_tmpl)
651 url_obj = urlobject.URLObject(uri_tmpl)
686 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
652 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
687
653
688 return safe_unicode(url)
654 return safe_unicode(url)
689
655
690
656
691 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
657 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
692 maybe_unreachable=False):
658 maybe_unreachable=False):
693 """
659 """
694 Safe version of get_commit if this commit doesn't exists for a
660 Safe version of get_commit if this commit doesn't exists for a
695 repository it returns a Dummy one instead
661 repository it returns a Dummy one instead
696
662
697 :param repo: repository instance
663 :param repo: repository instance
698 :param commit_id: commit id as str
664 :param commit_id: commit id as str
699 :param commit_idx: numeric commit index
665 :param commit_idx: numeric commit index
700 :param pre_load: optional list of commit attributes to load
666 :param pre_load: optional list of commit attributes to load
701 :param maybe_unreachable: translate unreachable commits on git repos
667 :param maybe_unreachable: translate unreachable commits on git repos
702 """
668 """
703 # TODO(skreft): remove these circular imports
669 # TODO(skreft): remove these circular imports
704 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
670 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
705 from rhodecode.lib.vcs.exceptions import RepositoryError
671 from rhodecode.lib.vcs.exceptions import RepositoryError
706 if not isinstance(repo, BaseRepository):
672 if not isinstance(repo, BaseRepository):
707 raise Exception('You must pass an Repository '
673 raise Exception('You must pass an Repository '
708 'object as first argument got %s', type(repo))
674 'object as first argument got %s', type(repo))
709
675
710 try:
676 try:
711 commit = repo.get_commit(
677 commit = repo.get_commit(
712 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load,
678 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load,
713 maybe_unreachable=maybe_unreachable)
679 maybe_unreachable=maybe_unreachable)
714 except (RepositoryError, LookupError):
680 except (RepositoryError, LookupError):
715 commit = EmptyCommit()
681 commit = EmptyCommit()
716 return commit
682 return commit
717
683
718
684
719 def datetime_to_time(dt):
685 def datetime_to_time(dt):
720 if dt:
686 if dt:
721 return time.mktime(dt.timetuple())
687 return time.mktime(dt.timetuple())
722
688
723
689
724 def time_to_datetime(tm):
690 def time_to_datetime(tm):
725 if tm:
691 if tm:
726 if isinstance(tm, compat.string_types):
692 if isinstance(tm, compat.string_types):
727 try:
693 try:
728 tm = float(tm)
694 tm = float(tm)
729 except ValueError:
695 except ValueError:
730 return
696 return
731 return datetime.datetime.fromtimestamp(tm)
697 return datetime.datetime.fromtimestamp(tm)
732
698
733
699
734 def time_to_utcdatetime(tm):
700 def time_to_utcdatetime(tm):
735 if tm:
701 if tm:
736 if isinstance(tm, compat.string_types):
702 if isinstance(tm, compat.string_types):
737 try:
703 try:
738 tm = float(tm)
704 tm = float(tm)
739 except ValueError:
705 except ValueError:
740 return
706 return
741 return datetime.datetime.utcfromtimestamp(tm)
707 return datetime.datetime.utcfromtimestamp(tm)
742
708
743
709
744 MENTIONS_REGEX = re.compile(
710 MENTIONS_REGEX = re.compile(
745 # ^@ or @ without any special chars in front
711 # ^@ or @ without any special chars in front
746 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
712 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
747 # main body starts with letter, then can be . - _
713 # main body starts with letter, then can be . - _
748 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
714 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
749 re.VERBOSE | re.MULTILINE)
715 re.VERBOSE | re.MULTILINE)
750
716
751
717
752 def extract_mentioned_users(s):
718 def extract_mentioned_users(s):
753 """
719 """
754 Returns unique usernames from given string s that have @mention
720 Returns unique usernames from given string s that have @mention
755
721
756 :param s: string to get mentions
722 :param s: string to get mentions
757 """
723 """
758 usrs = set()
724 usrs = set()
759 for username in MENTIONS_REGEX.findall(s):
725 for username in MENTIONS_REGEX.findall(s):
760 usrs.add(username)
726 usrs.add(username)
761
727
762 return sorted(list(usrs), key=lambda k: k.lower())
728 return sorted(list(usrs), key=lambda k: k.lower())
763
729
764
730
765 class AttributeDictBase(dict):
731 class AttributeDictBase(dict):
766 def __getstate__(self):
732 def __getstate__(self):
767 odict = self.__dict__ # get attribute dictionary
733 odict = self.__dict__ # get attribute dictionary
768 return odict
734 return odict
769
735
770 def __setstate__(self, dict):
736 def __setstate__(self, dict):
771 self.__dict__ = dict
737 self.__dict__ = dict
772
738
773 __setattr__ = dict.__setitem__
739 __setattr__ = dict.__setitem__
774 __delattr__ = dict.__delitem__
740 __delattr__ = dict.__delitem__
775
741
776
742
777 class StrictAttributeDict(AttributeDictBase):
743 class StrictAttributeDict(AttributeDictBase):
778 """
744 """
779 Strict Version of Attribute dict which raises an Attribute error when
745 Strict Version of Attribute dict which raises an Attribute error when
780 requested attribute is not set
746 requested attribute is not set
781 """
747 """
782 def __getattr__(self, attr):
748 def __getattr__(self, attr):
783 try:
749 try:
784 return self[attr]
750 return self[attr]
785 except KeyError:
751 except KeyError:
786 raise AttributeError('%s object has no attribute %s' % (
752 raise AttributeError('%s object has no attribute %s' % (
787 self.__class__, attr))
753 self.__class__, attr))
788
754
789
755
790 class AttributeDict(AttributeDictBase):
756 class AttributeDict(AttributeDictBase):
791 def __getattr__(self, attr):
757 def __getattr__(self, attr):
792 return self.get(attr, None)
758 return self.get(attr, None)
793
759
794
760
795
761
796 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
762 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
797 def __init__(self, default_factory=None, *args, **kwargs):
763 def __init__(self, default_factory=None, *args, **kwargs):
798 # in python3 you can omit the args to super
764 # in python3 you can omit the args to super
799 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
765 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
800 self.default_factory = default_factory
766 self.default_factory = default_factory
801
767
802
768
803 def fix_PATH(os_=None):
769 def fix_PATH(os_=None):
804 """
770 """
805 Get current active python path, and append it to PATH variable to fix
771 Get current active python path, and append it to PATH variable to fix
806 issues of subprocess calls and different python versions
772 issues of subprocess calls and different python versions
807 """
773 """
808 if os_ is None:
774 if os_ is None:
809 import os
775 import os
810 else:
776 else:
811 os = os_
777 os = os_
812
778
813 cur_path = os.path.split(sys.executable)[0]
779 cur_path = os.path.split(sys.executable)[0]
814 if not os.environ['PATH'].startswith(cur_path):
780 if not os.environ['PATH'].startswith(cur_path):
815 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
781 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
816
782
817
783
818 def obfuscate_url_pw(engine):
784 def obfuscate_url_pw(engine):
819 _url = engine or ''
785 _url = engine or ''
820 try:
786 try:
821 _url = sqlalchemy.engine.url.make_url(engine)
787 _url = sqlalchemy.engine.url.make_url(engine)
822 if _url.password:
788 if _url.password:
823 _url.password = 'XXXXX'
789 _url.password = 'XXXXX'
824 except Exception:
790 except Exception:
825 pass
791 pass
826 return unicode(_url)
792 return unicode(_url)
827
793
828
794
829 def get_server_url(environ):
795 def get_server_url(environ):
830 req = webob.Request(environ)
796 req = webob.Request(environ)
831 return req.host_url + req.script_name
797 return req.host_url + req.script_name
832
798
833
799
834 def unique_id(hexlen=32):
800 def unique_id(hexlen=32):
835 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
801 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
836 return suuid(truncate_to=hexlen, alphabet=alphabet)
802 return suuid(truncate_to=hexlen, alphabet=alphabet)
837
803
838
804
839 def suuid(url=None, truncate_to=22, alphabet=None):
805 def suuid(url=None, truncate_to=22, alphabet=None):
840 """
806 """
841 Generate and return a short URL safe UUID.
807 Generate and return a short URL safe UUID.
842
808
843 If the url parameter is provided, set the namespace to the provided
809 If the url parameter is provided, set the namespace to the provided
844 URL and generate a UUID.
810 URL and generate a UUID.
845
811
846 :param url to get the uuid for
812 :param url to get the uuid for
847 :truncate_to: truncate the basic 22 UUID to shorter version
813 :truncate_to: truncate the basic 22 UUID to shorter version
848
814
849 The IDs won't be universally unique any longer, but the probability of
815 The IDs won't be universally unique any longer, but the probability of
850 a collision will still be very low.
816 a collision will still be very low.
851 """
817 """
852 # Define our alphabet.
818 # Define our alphabet.
853 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
819 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
854
820
855 # If no URL is given, generate a random UUID.
821 # If no URL is given, generate a random UUID.
856 if url is None:
822 if url is None:
857 unique_id = uuid.uuid4().int
823 unique_id = uuid.uuid4().int
858 else:
824 else:
859 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
825 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
860
826
861 alphabet_length = len(_ALPHABET)
827 alphabet_length = len(_ALPHABET)
862 output = []
828 output = []
863 while unique_id > 0:
829 while unique_id > 0:
864 digit = unique_id % alphabet_length
830 digit = unique_id % alphabet_length
865 output.append(_ALPHABET[digit])
831 output.append(_ALPHABET[digit])
866 unique_id = int(unique_id / alphabet_length)
832 unique_id = int(unique_id / alphabet_length)
867 return "".join(output)[:truncate_to]
833 return "".join(output)[:truncate_to]
868
834
869
835
870 def get_current_rhodecode_user(request=None):
836 def get_current_rhodecode_user(request=None):
871 """
837 """
872 Gets rhodecode user from request
838 Gets rhodecode user from request
873 """
839 """
874 pyramid_request = request or pyramid.threadlocal.get_current_request()
840 pyramid_request = request or pyramid.threadlocal.get_current_request()
875
841
876 # web case
842 # web case
877 if pyramid_request and hasattr(pyramid_request, 'user'):
843 if pyramid_request and hasattr(pyramid_request, 'user'):
878 return pyramid_request.user
844 return pyramid_request.user
879
845
880 # api case
846 # api case
881 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
847 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
882 return pyramid_request.rpc_user
848 return pyramid_request.rpc_user
883
849
884 return None
850 return None
885
851
886
852
887 def action_logger_generic(action, namespace=''):
853 def action_logger_generic(action, namespace=''):
888 """
854 """
889 A generic logger for actions useful to the system overview, tries to find
855 A generic logger for actions useful to the system overview, tries to find
890 an acting user for the context of the call otherwise reports unknown user
856 an acting user for the context of the call otherwise reports unknown user
891
857
892 :param action: logging message eg 'comment 5 deleted'
858 :param action: logging message eg 'comment 5 deleted'
893 :param type: string
859 :param type: string
894
860
895 :param namespace: namespace of the logging message eg. 'repo.comments'
861 :param namespace: namespace of the logging message eg. 'repo.comments'
896 :param type: string
862 :param type: string
897
863
898 """
864 """
899
865
900 logger_name = 'rhodecode.actions'
866 logger_name = 'rhodecode.actions'
901
867
902 if namespace:
868 if namespace:
903 logger_name += '.' + namespace
869 logger_name += '.' + namespace
904
870
905 log = logging.getLogger(logger_name)
871 log = logging.getLogger(logger_name)
906
872
907 # get a user if we can
873 # get a user if we can
908 user = get_current_rhodecode_user()
874 user = get_current_rhodecode_user()
909
875
910 logfunc = log.info
876 logfunc = log.info
911
877
912 if not user:
878 if not user:
913 user = '<unknown user>'
879 user = '<unknown user>'
914 logfunc = log.warning
880 logfunc = log.warning
915
881
916 logfunc('Logging action by {}: {}'.format(user, action))
882 logfunc('Logging action by {}: {}'.format(user, action))
917
883
918
884
919 def escape_split(text, sep=',', maxsplit=-1):
885 def escape_split(text, sep=',', maxsplit=-1):
920 r"""
886 r"""
921 Allows for escaping of the separator: e.g. arg='foo\, bar'
887 Allows for escaping of the separator: e.g. arg='foo\, bar'
922
888
923 It should be noted that the way bash et. al. do command line parsing, those
889 It should be noted that the way bash et. al. do command line parsing, those
924 single quotes are required.
890 single quotes are required.
925 """
891 """
926 escaped_sep = r'\%s' % sep
892 escaped_sep = r'\%s' % sep
927
893
928 if escaped_sep not in text:
894 if escaped_sep not in text:
929 return text.split(sep, maxsplit)
895 return text.split(sep, maxsplit)
930
896
931 before, _mid, after = text.partition(escaped_sep)
897 before, _mid, after = text.partition(escaped_sep)
932 startlist = before.split(sep, maxsplit) # a regular split is fine here
898 startlist = before.split(sep, maxsplit) # a regular split is fine here
933 unfinished = startlist[-1]
899 unfinished = startlist[-1]
934 startlist = startlist[:-1]
900 startlist = startlist[:-1]
935
901
936 # recurse because there may be more escaped separators
902 # recurse because there may be more escaped separators
937 endlist = escape_split(after, sep, maxsplit)
903 endlist = escape_split(after, sep, maxsplit)
938
904
939 # finish building the escaped value. we use endlist[0] becaue the first
905 # finish building the escaped value. we use endlist[0] becaue the first
940 # part of the string sent in recursion is the rest of the escaped value.
906 # part of the string sent in recursion is the rest of the escaped value.
941 unfinished += sep + endlist[0]
907 unfinished += sep + endlist[0]
942
908
943 return startlist + [unfinished] + endlist[1:] # put together all the parts
909 return startlist + [unfinished] + endlist[1:] # put together all the parts
944
910
945
911
946 class OptionalAttr(object):
912 class OptionalAttr(object):
947 """
913 """
948 Special Optional Option that defines other attribute. Example::
914 Special Optional Option that defines other attribute. Example::
949
915
950 def test(apiuser, userid=Optional(OAttr('apiuser')):
916 def test(apiuser, userid=Optional(OAttr('apiuser')):
951 user = Optional.extract(userid)
917 user = Optional.extract(userid)
952 # calls
918 # calls
953
919
954 """
920 """
955
921
956 def __init__(self, attr_name):
922 def __init__(self, attr_name):
957 self.attr_name = attr_name
923 self.attr_name = attr_name
958
924
959 def __repr__(self):
925 def __repr__(self):
960 return '<OptionalAttr:%s>' % self.attr_name
926 return '<OptionalAttr:%s>' % self.attr_name
961
927
962 def __call__(self):
928 def __call__(self):
963 return self
929 return self
964
930
965
931
966 # alias
932 # alias
967 OAttr = OptionalAttr
933 OAttr = OptionalAttr
968
934
969
935
970 class Optional(object):
936 class Optional(object):
971 """
937 """
972 Defines an optional parameter::
938 Defines an optional parameter::
973
939
974 param = param.getval() if isinstance(param, Optional) else param
940 param = param.getval() if isinstance(param, Optional) else param
975 param = param() if isinstance(param, Optional) else param
941 param = param() if isinstance(param, Optional) else param
976
942
977 is equivalent of::
943 is equivalent of::
978
944
979 param = Optional.extract(param)
945 param = Optional.extract(param)
980
946
981 """
947 """
982
948
983 def __init__(self, type_):
949 def __init__(self, type_):
984 self.type_ = type_
950 self.type_ = type_
985
951
986 def __repr__(self):
952 def __repr__(self):
987 return '<Optional:%s>' % self.type_.__repr__()
953 return '<Optional:%s>' % self.type_.__repr__()
988
954
989 def __call__(self):
955 def __call__(self):
990 return self.getval()
956 return self.getval()
991
957
992 def getval(self):
958 def getval(self):
993 """
959 """
994 returns value from this Optional instance
960 returns value from this Optional instance
995 """
961 """
996 if isinstance(self.type_, OAttr):
962 if isinstance(self.type_, OAttr):
997 # use params name
963 # use params name
998 return self.type_.attr_name
964 return self.type_.attr_name
999 return self.type_
965 return self.type_
1000
966
1001 @classmethod
967 @classmethod
1002 def extract(cls, val):
968 def extract(cls, val):
1003 """
969 """
1004 Extracts value from Optional() instance
970 Extracts value from Optional() instance
1005
971
1006 :param val:
972 :param val:
1007 :return: original value if it's not Optional instance else
973 :return: original value if it's not Optional instance else
1008 value of instance
974 value of instance
1009 """
975 """
1010 if isinstance(val, cls):
976 if isinstance(val, cls):
1011 return val.getval()
977 return val.getval()
1012 return val
978 return val
1013
979
1014
980
1015 def glob2re(pat):
981 def glob2re(pat):
1016 """
982 """
1017 Translate a shell PATTERN to a regular expression.
983 Translate a shell PATTERN to a regular expression.
1018
984
1019 There is no way to quote meta-characters.
985 There is no way to quote meta-characters.
1020 """
986 """
1021
987
1022 i, n = 0, len(pat)
988 i, n = 0, len(pat)
1023 res = ''
989 res = ''
1024 while i < n:
990 while i < n:
1025 c = pat[i]
991 c = pat[i]
1026 i = i+1
992 i = i+1
1027 if c == '*':
993 if c == '*':
1028 #res = res + '.*'
994 #res = res + '.*'
1029 res = res + '[^/]*'
995 res = res + '[^/]*'
1030 elif c == '?':
996 elif c == '?':
1031 #res = res + '.'
997 #res = res + '.'
1032 res = res + '[^/]'
998 res = res + '[^/]'
1033 elif c == '[':
999 elif c == '[':
1034 j = i
1000 j = i
1035 if j < n and pat[j] == '!':
1001 if j < n and pat[j] == '!':
1036 j = j+1
1002 j = j+1
1037 if j < n and pat[j] == ']':
1003 if j < n and pat[j] == ']':
1038 j = j+1
1004 j = j+1
1039 while j < n and pat[j] != ']':
1005 while j < n and pat[j] != ']':
1040 j = j+1
1006 j = j+1
1041 if j >= n:
1007 if j >= n:
1042 res = res + '\\['
1008 res = res + '\\['
1043 else:
1009 else:
1044 stuff = pat[i:j].replace('\\','\\\\')
1010 stuff = pat[i:j].replace('\\','\\\\')
1045 i = j+1
1011 i = j+1
1046 if stuff[0] == '!':
1012 if stuff[0] == '!':
1047 stuff = '^' + stuff[1:]
1013 stuff = '^' + stuff[1:]
1048 elif stuff[0] == '^':
1014 elif stuff[0] == '^':
1049 stuff = '\\' + stuff
1015 stuff = '\\' + stuff
1050 res = '%s[%s]' % (res, stuff)
1016 res = '%s[%s]' % (res, stuff)
1051 else:
1017 else:
1052 res = res + re.escape(c)
1018 res = res + re.escape(c)
1053 return res + '\Z(?ms)'
1019 return res + '\Z(?ms)'
1054
1020
1055
1021
1056 def parse_byte_string(size_str):
1022 def parse_byte_string(size_str):
1057 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1023 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1058 if not match:
1024 if not match:
1059 raise ValueError('Given size:%s is invalid, please make sure '
1025 raise ValueError('Given size:%s is invalid, please make sure '
1060 'to use format of <num>(MB|KB)' % size_str)
1026 'to use format of <num>(MB|KB)' % size_str)
1061
1027
1062 _parts = match.groups()
1028 _parts = match.groups()
1063 num, type_ = _parts
1029 num, type_ = _parts
1064 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1030 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1065
1031
1066
1032
1067 class CachedProperty(object):
1033 class CachedProperty(object):
1068 """
1034 """
1069 Lazy Attributes. With option to invalidate the cache by running a method
1035 Lazy Attributes. With option to invalidate the cache by running a method
1070
1036
1071 class Foo():
1037 class Foo():
1072
1038
1073 @CachedProperty
1039 @CachedProperty
1074 def heavy_func():
1040 def heavy_func():
1075 return 'super-calculation'
1041 return 'super-calculation'
1076
1042
1077 foo = Foo()
1043 foo = Foo()
1078 foo.heavy_func() # first computions
1044 foo.heavy_func() # first computions
1079 foo.heavy_func() # fetch from cache
1045 foo.heavy_func() # fetch from cache
1080 foo._invalidate_prop_cache('heavy_func')
1046 foo._invalidate_prop_cache('heavy_func')
1081 # at this point calling foo.heavy_func() will be re-computed
1047 # at this point calling foo.heavy_func() will be re-computed
1082 """
1048 """
1083
1049
1084 def __init__(self, func, func_name=None):
1050 def __init__(self, func, func_name=None):
1085
1051
1086 if func_name is None:
1052 if func_name is None:
1087 func_name = func.__name__
1053 func_name = func.__name__
1088 self.data = (func, func_name)
1054 self.data = (func, func_name)
1089 update_wrapper(self, func)
1055 update_wrapper(self, func)
1090
1056
1091 def __get__(self, inst, class_):
1057 def __get__(self, inst, class_):
1092 if inst is None:
1058 if inst is None:
1093 return self
1059 return self
1094
1060
1095 func, func_name = self.data
1061 func, func_name = self.data
1096 value = func(inst)
1062 value = func(inst)
1097 inst.__dict__[func_name] = value
1063 inst.__dict__[func_name] = value
1098 if '_invalidate_prop_cache' not in inst.__dict__:
1064 if '_invalidate_prop_cache' not in inst.__dict__:
1099 inst.__dict__['_invalidate_prop_cache'] = partial(
1065 inst.__dict__['_invalidate_prop_cache'] = partial(
1100 self._invalidate_prop_cache, inst)
1066 self._invalidate_prop_cache, inst)
1101 return value
1067 return value
1102
1068
1103 def _invalidate_prop_cache(self, inst, name):
1069 def _invalidate_prop_cache(self, inst, name):
1104 inst.__dict__.pop(name, None)
1070 inst.__dict__.pop(name, None)
@@ -1,746 +1,739 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 Package for testing various lib/helper functions in rhodecode
23 Package for testing various lib/helper functions in rhodecode
24 """
24 """
25
25
26 import datetime
26 import datetime
27 import string
27 import string
28 import mock
28 import mock
29 import pytest
29 import pytest
30
30
31 from rhodecode.tests import no_newline_id_generator
31 from rhodecode.tests import no_newline_id_generator
32 from rhodecode.tests.utils import run_test_concurrently
32 from rhodecode.tests.utils import run_test_concurrently
33
33
34 from rhodecode.lib import rc_cache
34 from rhodecode.lib import rc_cache
35 from rhodecode.lib.helpers import InitialsGravatar
35 from rhodecode.lib.helpers import InitialsGravatar
36 from rhodecode.lib.utils2 import AttributeDict
36 from rhodecode.lib.utils2 import AttributeDict
37
37
38 from rhodecode.model.db import Repository, CacheKey
38 from rhodecode.model.db import Repository, CacheKey
39
39
40
40
41 def _urls_for_proto(proto):
41 TEST_URLS = [
42 return [
42 ('127.0.0.1', '127.0.0.1'),
43 ('%s://127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
43 ('marcink@127.0.0.1', '127.0.0.1'),
44 '%s://127.0.0.1' % proto),
44 ('marcink:pass@127.0.0.1', '127.0.0.1'),
45 ('%s://marcink@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
45 ('marcink@domain.name:pass@127.0.0.1', '127.0.0.1'),
46 '%s://127.0.0.1' % proto),
46
47 ('%s://marcink:pass@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
47 ('127.0.0.1:8080', '127.0.0.1:8080'),
48 '%s://127.0.0.1' % proto),
48 ('marcink@127.0.0.1:8080', '127.0.0.1:8080'),
49 ('%s://127.0.0.1:8080' % proto, ['%s://' % proto, '127.0.0.1', '8080'],
49 ('marcink:pass@127.0.0.1:8080', '127.0.0.1:8080'),
50 '%s://127.0.0.1:8080' % proto),
50 ('marcink@domain.name:pass@127.0.0.1:8080', '127.0.0.1:8080'),
51 ('%s://domain.org' % proto, ['%s://' % proto, 'domain.org'],
51
52 '%s://domain.org' % proto),
52 ('domain.org', 'domain.org'),
53 ('%s://user:pass@domain.org:8080' % proto,
53 ('user:pass@domain.org:8080', 'domain.org:8080'),
54 ['%s://' % proto, 'domain.org', '8080'],
54 ('user@domain.org:pass@domain.org:8080', 'domain.org:8080'),
55 '%s://domain.org:8080' % proto),
56 ]
55 ]
57
56
58 TEST_URLS = _urls_for_proto('http') + _urls_for_proto('https')
59
57
60
58 @pytest.mark.parametrize("protocol", ['http://', 'https://'])
61 @pytest.mark.parametrize("test_url, expected, expected_creds", TEST_URLS)
59 @pytest.mark.parametrize("test_url, expected", TEST_URLS)
62 def test_uri_filter(test_url, expected, expected_creds):
60 def test_credentials_filter(protocol, test_url, expected):
63 from rhodecode.lib.utils2 import uri_filter
64 assert uri_filter(test_url) == expected
65
66
67 @pytest.mark.parametrize("test_url, expected, expected_creds", TEST_URLS)
68 def test_credentials_filter(test_url, expected, expected_creds):
69 from rhodecode.lib.utils2 import credentials_filter
61 from rhodecode.lib.utils2 import credentials_filter
70 assert credentials_filter(test_url) == expected_creds
62 test_url = protocol + test_url
63 assert credentials_filter(test_url) == protocol + expected
71
64
72
65
73 @pytest.mark.parametrize("str_bool, expected", [
66 @pytest.mark.parametrize("str_bool, expected", [
74 ('t', True),
67 ('t', True),
75 ('true', True),
68 ('true', True),
76 ('y', True),
69 ('y', True),
77 ('yes', True),
70 ('yes', True),
78 ('on', True),
71 ('on', True),
79 ('1', True),
72 ('1', True),
80 ('Y', True),
73 ('Y', True),
81 ('yeS', True),
74 ('yeS', True),
82 ('Y', True),
75 ('Y', True),
83 ('TRUE', True),
76 ('TRUE', True),
84 ('T', True),
77 ('T', True),
85 ('False', False),
78 ('False', False),
86 ('F', False),
79 ('F', False),
87 ('FALSE', False),
80 ('FALSE', False),
88 ('0', False),
81 ('0', False),
89 ('-1', False),
82 ('-1', False),
90 ('', False)
83 ('', False)
91 ])
84 ])
92 def test_str2bool(str_bool, expected):
85 def test_str2bool(str_bool, expected):
93 from rhodecode.lib.utils2 import str2bool
86 from rhodecode.lib.utils2 import str2bool
94 assert str2bool(str_bool) == expected
87 assert str2bool(str_bool) == expected
95
88
96
89
97 @pytest.mark.parametrize("text, expected", reduce(lambda a1,a2:a1+a2, [
90 @pytest.mark.parametrize("text, expected", reduce(lambda a1,a2:a1+a2, [
98 [
91 [
99 (pref+"", []),
92 (pref+"", []),
100 (pref+"Hi there @marcink", ['marcink']),
93 (pref+"Hi there @marcink", ['marcink']),
101 (pref+"Hi there @marcink and @bob", ['bob', 'marcink']),
94 (pref+"Hi there @marcink and @bob", ['bob', 'marcink']),
102 (pref+"Hi there @marcink\n", ['marcink']),
95 (pref+"Hi there @marcink\n", ['marcink']),
103 (pref+"Hi there @marcink and @bob\n", ['bob', 'marcink']),
96 (pref+"Hi there @marcink and @bob\n", ['bob', 'marcink']),
104 (pref+"Hi there marcin@rhodecode.com", []),
97 (pref+"Hi there marcin@rhodecode.com", []),
105 (pref+"Hi there @john.malcovic and @bob\n", ['bob', 'john.malcovic']),
98 (pref+"Hi there @john.malcovic and @bob\n", ['bob', 'john.malcovic']),
106 (pref+"This needs to be reviewed: (@marcink,@john)", ["john", "marcink"]),
99 (pref+"This needs to be reviewed: (@marcink,@john)", ["john", "marcink"]),
107 (pref+"This needs to be reviewed: (@marcink, @john)", ["john", "marcink"]),
100 (pref+"This needs to be reviewed: (@marcink, @john)", ["john", "marcink"]),
108 (pref+"This needs to be reviewed: [@marcink,@john]", ["john", "marcink"]),
101 (pref+"This needs to be reviewed: [@marcink,@john]", ["john", "marcink"]),
109 (pref+"This needs to be reviewed: (@marcink @john)", ["john", "marcink"]),
102 (pref+"This needs to be reviewed: (@marcink @john)", ["john", "marcink"]),
110 (pref+"@john @mary, please review", ["john", "mary"]),
103 (pref+"@john @mary, please review", ["john", "mary"]),
111 (pref+"@john,@mary, please review", ["john", "mary"]),
104 (pref+"@john,@mary, please review", ["john", "mary"]),
112 (pref+"Hej @123, @22john,@mary, please review", ['123', '22john', 'mary']),
105 (pref+"Hej @123, @22john,@mary, please review", ['123', '22john', 'mary']),
113 (pref+"@first hi there @marcink here's my email marcin@email.com "
106 (pref+"@first hi there @marcink here's my email marcin@email.com "
114 "@lukaszb check @one_more22 it pls @ ttwelve @D[] @one@two@three ", ['first', 'lukaszb', 'marcink', 'one', 'one_more22']),
107 "@lukaszb check @one_more22 it pls @ ttwelve @D[] @one@two@three ", ['first', 'lukaszb', 'marcink', 'one', 'one_more22']),
115 (pref+"@MARCIN @maRCiN @2one_more22 @john please see this http://org.pl", ['2one_more22', 'john', 'MARCIN', 'maRCiN']),
108 (pref+"@MARCIN @maRCiN @2one_more22 @john please see this http://org.pl", ['2one_more22', 'john', 'MARCIN', 'maRCiN']),
116 (pref+"@marian.user just do it @marco-polo and next extract @marco_polo", ['marco-polo', 'marco_polo', 'marian.user']),
109 (pref+"@marian.user just do it @marco-polo and next extract @marco_polo", ['marco-polo', 'marco_polo', 'marian.user']),
117 (pref+"user.dot hej ! not-needed maril@domain.org", []),
110 (pref+"user.dot hej ! not-needed maril@domain.org", []),
118 (pref+"\n@marcin", ['marcin']),
111 (pref+"\n@marcin", ['marcin']),
119 ]
112 ]
120 for pref in ['', '\n', 'hi !', '\t', '\n\n']]), ids=no_newline_id_generator)
113 for pref in ['', '\n', 'hi !', '\t', '\n\n']]), ids=no_newline_id_generator)
121 def test_mention_extractor(text, expected):
114 def test_mention_extractor(text, expected):
122 from rhodecode.lib.utils2 import extract_mentioned_users
115 from rhodecode.lib.utils2 import extract_mentioned_users
123 got = extract_mentioned_users(text)
116 got = extract_mentioned_users(text)
124 assert sorted(got, key=lambda x: x.lower()) == got
117 assert sorted(got, key=lambda x: x.lower()) == got
125 assert set(expected) == set(got)
118 assert set(expected) == set(got)
126
119
127 @pytest.mark.parametrize("age_args, expected, kw", [
120 @pytest.mark.parametrize("age_args, expected, kw", [
128 ({}, u'just now', {}),
121 ({}, u'just now', {}),
129 ({'seconds': -1}, u'1 second ago', {}),
122 ({'seconds': -1}, u'1 second ago', {}),
130 ({'seconds': -60 * 2}, u'2 minutes ago', {}),
123 ({'seconds': -60 * 2}, u'2 minutes ago', {}),
131 ({'hours': -1}, u'1 hour ago', {}),
124 ({'hours': -1}, u'1 hour ago', {}),
132 ({'hours': -24}, u'1 day ago', {}),
125 ({'hours': -24}, u'1 day ago', {}),
133 ({'hours': -24 * 5}, u'5 days ago', {}),
126 ({'hours': -24 * 5}, u'5 days ago', {}),
134 ({'months': -1}, u'1 month ago', {}),
127 ({'months': -1}, u'1 month ago', {}),
135 ({'months': -1, 'days': -2}, u'1 month and 2 days ago', {}),
128 ({'months': -1, 'days': -2}, u'1 month and 2 days ago', {}),
136 ({'years': -1, 'months': -1}, u'1 year and 1 month ago', {}),
129 ({'years': -1, 'months': -1}, u'1 year and 1 month ago', {}),
137 ({}, u'just now', {'short_format': True}),
130 ({}, u'just now', {'short_format': True}),
138 ({'seconds': -1}, u'1sec ago', {'short_format': True}),
131 ({'seconds': -1}, u'1sec ago', {'short_format': True}),
139 ({'seconds': -60 * 2}, u'2min ago', {'short_format': True}),
132 ({'seconds': -60 * 2}, u'2min ago', {'short_format': True}),
140 ({'hours': -1}, u'1h ago', {'short_format': True}),
133 ({'hours': -1}, u'1h ago', {'short_format': True}),
141 ({'hours': -24}, u'1d ago', {'short_format': True}),
134 ({'hours': -24}, u'1d ago', {'short_format': True}),
142 ({'hours': -24 * 5}, u'5d ago', {'short_format': True}),
135 ({'hours': -24 * 5}, u'5d ago', {'short_format': True}),
143 ({'months': -1}, u'1m ago', {'short_format': True}),
136 ({'months': -1}, u'1m ago', {'short_format': True}),
144 ({'months': -1, 'days': -2}, u'1m, 2d ago', {'short_format': True}),
137 ({'months': -1, 'days': -2}, u'1m, 2d ago', {'short_format': True}),
145 ({'years': -1, 'months': -1}, u'1y, 1m ago', {'short_format': True}),
138 ({'years': -1, 'months': -1}, u'1y, 1m ago', {'short_format': True}),
146 ])
139 ])
147 def test_age(age_args, expected, kw, baseapp):
140 def test_age(age_args, expected, kw, baseapp):
148 from rhodecode.lib.utils2 import age
141 from rhodecode.lib.utils2 import age
149 from dateutil import relativedelta
142 from dateutil import relativedelta
150 n = datetime.datetime(year=2012, month=5, day=17)
143 n = datetime.datetime(year=2012, month=5, day=17)
151 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
144 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
152
145
153 def translate(elem):
146 def translate(elem):
154 return elem.interpolate()
147 return elem.interpolate()
155
148
156 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
149 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
157
150
158
151
159 @pytest.mark.parametrize("age_args, expected, kw", [
152 @pytest.mark.parametrize("age_args, expected, kw", [
160 ({}, u'just now', {}),
153 ({}, u'just now', {}),
161 ({'seconds': 1}, u'in 1 second', {}),
154 ({'seconds': 1}, u'in 1 second', {}),
162 ({'seconds': 60 * 2}, u'in 2 minutes', {}),
155 ({'seconds': 60 * 2}, u'in 2 minutes', {}),
163 ({'hours': 1}, u'in 1 hour', {}),
156 ({'hours': 1}, u'in 1 hour', {}),
164 ({'hours': 24}, u'in 1 day', {}),
157 ({'hours': 24}, u'in 1 day', {}),
165 ({'hours': 24 * 5}, u'in 5 days', {}),
158 ({'hours': 24 * 5}, u'in 5 days', {}),
166 ({'months': 1}, u'in 1 month', {}),
159 ({'months': 1}, u'in 1 month', {}),
167 ({'months': 1, 'days': 1}, u'in 1 month and 1 day', {}),
160 ({'months': 1, 'days': 1}, u'in 1 month and 1 day', {}),
168 ({'years': 1, 'months': 1}, u'in 1 year and 1 month', {}),
161 ({'years': 1, 'months': 1}, u'in 1 year and 1 month', {}),
169 ({}, u'just now', {'short_format': True}),
162 ({}, u'just now', {'short_format': True}),
170 ({'seconds': 1}, u'in 1sec', {'short_format': True}),
163 ({'seconds': 1}, u'in 1sec', {'short_format': True}),
171 ({'seconds': 60 * 2}, u'in 2min', {'short_format': True}),
164 ({'seconds': 60 * 2}, u'in 2min', {'short_format': True}),
172 ({'hours': 1}, u'in 1h', {'short_format': True}),
165 ({'hours': 1}, u'in 1h', {'short_format': True}),
173 ({'hours': 24}, u'in 1d', {'short_format': True}),
166 ({'hours': 24}, u'in 1d', {'short_format': True}),
174 ({'hours': 24 * 5}, u'in 5d', {'short_format': True}),
167 ({'hours': 24 * 5}, u'in 5d', {'short_format': True}),
175 ({'months': 1}, u'in 1m', {'short_format': True}),
168 ({'months': 1}, u'in 1m', {'short_format': True}),
176 ({'months': 1, 'days': 1}, u'in 1m, 1d', {'short_format': True}),
169 ({'months': 1, 'days': 1}, u'in 1m, 1d', {'short_format': True}),
177 ({'years': 1, 'months': 1}, u'in 1y, 1m', {'short_format': True}),
170 ({'years': 1, 'months': 1}, u'in 1y, 1m', {'short_format': True}),
178 ])
171 ])
179 def test_age_in_future(age_args, expected, kw, baseapp):
172 def test_age_in_future(age_args, expected, kw, baseapp):
180 from rhodecode.lib.utils2 import age
173 from rhodecode.lib.utils2 import age
181 from dateutil import relativedelta
174 from dateutil import relativedelta
182 n = datetime.datetime(year=2012, month=5, day=17)
175 n = datetime.datetime(year=2012, month=5, day=17)
183 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
176 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
184
177
185 def translate(elem):
178 def translate(elem):
186 return elem.interpolate()
179 return elem.interpolate()
187
180
188 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
181 assert translate(age(n + delt(**age_args), now=n, **kw)) == expected
189
182
190
183
191 @pytest.mark.parametrize("sample, expected_tags", [
184 @pytest.mark.parametrize("sample, expected_tags", [
192 # entry
185 # entry
193 ((
186 ((
194 ""
187 ""
195 ),
188 ),
196 [
189 [
197
190
198 ]),
191 ]),
199 # entry
192 # entry
200 ((
193 ((
201 "hello world [stale]"
194 "hello world [stale]"
202 ),
195 ),
203 [
196 [
204 ('state', '[stale]'),
197 ('state', '[stale]'),
205 ]),
198 ]),
206 # entry
199 # entry
207 ((
200 ((
208 "hello world [v2.0.0] [v1.0.0]"
201 "hello world [v2.0.0] [v1.0.0]"
209 ),
202 ),
210 [
203 [
211 ('generic', '[v2.0.0]'),
204 ('generic', '[v2.0.0]'),
212 ('generic', '[v1.0.0]'),
205 ('generic', '[v1.0.0]'),
213 ]),
206 ]),
214 # entry
207 # entry
215 ((
208 ((
216 "he[ll]o wo[rl]d"
209 "he[ll]o wo[rl]d"
217 ),
210 ),
218 [
211 [
219 ('label', '[ll]'),
212 ('label', '[ll]'),
220 ('label', '[rl]'),
213 ('label', '[rl]'),
221 ]),
214 ]),
222 # entry
215 # entry
223 ((
216 ((
224 "hello world [stale]\n[featured]\n[stale] [dead] [dev]"
217 "hello world [stale]\n[featured]\n[stale] [dead] [dev]"
225 ),
218 ),
226 [
219 [
227 ('state', '[stale]'),
220 ('state', '[stale]'),
228 ('state', '[featured]'),
221 ('state', '[featured]'),
229 ('state', '[stale]'),
222 ('state', '[stale]'),
230 ('state', '[dead]'),
223 ('state', '[dead]'),
231 ('state', '[dev]'),
224 ('state', '[dev]'),
232 ]),
225 ]),
233 # entry
226 # entry
234 ((
227 ((
235 "hello world \n\n [stale] \n [url =&gt; [name](http://rc.com)]"
228 "hello world \n\n [stale] \n [url =&gt; [name](http://rc.com)]"
236 ),
229 ),
237 [
230 [
238 ('state', '[stale]'),
231 ('state', '[stale]'),
239 ('url', '[url =&gt; [name](http://rc.com)]'),
232 ('url', '[url =&gt; [name](http://rc.com)]'),
240 ]),
233 ]),
241 # entry
234 # entry
242 ((
235 ((
243 "[url =&gt; [linkNameJS](javascript:alert(document.domain))]\n"
236 "[url =&gt; [linkNameJS](javascript:alert(document.domain))]\n"
244 "[url =&gt; [linkNameHTTP](http://rhodecode.com)]\n"
237 "[url =&gt; [linkNameHTTP](http://rhodecode.com)]\n"
245 "[url =&gt; [linkNameHTTPS](https://rhodecode.com)]\n"
238 "[url =&gt; [linkNameHTTPS](https://rhodecode.com)]\n"
246 "[url =&gt; [linkNamePath](/repo_group)]\n"
239 "[url =&gt; [linkNamePath](/repo_group)]\n"
247 ),
240 ),
248 [
241 [
249 ('generic', '[linkNameJS]'),
242 ('generic', '[linkNameJS]'),
250 ('url', '[url =&gt; [linkNameHTTP](http://rhodecode.com)]'),
243 ('url', '[url =&gt; [linkNameHTTP](http://rhodecode.com)]'),
251 ('url', '[url =&gt; [linkNameHTTPS](https://rhodecode.com)]'),
244 ('url', '[url =&gt; [linkNameHTTPS](https://rhodecode.com)]'),
252 ('url', '[url =&gt; [linkNamePath](/repo_group)]'),
245 ('url', '[url =&gt; [linkNamePath](/repo_group)]'),
253 ]),
246 ]),
254 # entry
247 # entry
255 ((
248 ((
256 "hello pta[tag] gog [[]] [[] sda ero[or]d [me =&gt;>< sa]"
249 "hello pta[tag] gog [[]] [[] sda ero[or]d [me =&gt;>< sa]"
257 "[requires] [stale] [see<>=&gt;] [see =&gt; http://url.com]"
250 "[requires] [stale] [see<>=&gt;] [see =&gt; http://url.com]"
258 "[requires =&gt; url] [lang =&gt; python] [just a tag] "
251 "[requires =&gt; url] [lang =&gt; python] [just a tag] "
259 "<html_tag first='abc' attr=\"my.url?attr=&another=\"></html_tag>"
252 "<html_tag first='abc' attr=\"my.url?attr=&another=\"></html_tag>"
260 "[,d] [ =&gt; ULR ] [obsolete] [desc]]"
253 "[,d] [ =&gt; ULR ] [obsolete] [desc]]"
261 ),
254 ),
262 [
255 [
263 ('label', '[desc]'),
256 ('label', '[desc]'),
264 ('label', '[obsolete]'),
257 ('label', '[obsolete]'),
265 ('label', '[or]'),
258 ('label', '[or]'),
266 ('label', '[requires]'),
259 ('label', '[requires]'),
267 ('label', '[tag]'),
260 ('label', '[tag]'),
268 ('state', '[stale]'),
261 ('state', '[stale]'),
269 ('lang', '[lang =&gt; python]'),
262 ('lang', '[lang =&gt; python]'),
270 ('ref', '[requires =&gt; url]'),
263 ('ref', '[requires =&gt; url]'),
271 ('see', '[see =&gt; http://url.com]'),
264 ('see', '[see =&gt; http://url.com]'),
272
265
273 ]),
266 ]),
274
267
275 ], ids=no_newline_id_generator)
268 ], ids=no_newline_id_generator)
276 def test_metatag_extraction(sample, expected_tags):
269 def test_metatag_extraction(sample, expected_tags):
277 from rhodecode.lib.helpers import extract_metatags
270 from rhodecode.lib.helpers import extract_metatags
278 tags, value = extract_metatags(sample)
271 tags, value = extract_metatags(sample)
279 assert sorted(tags) == sorted(expected_tags)
272 assert sorted(tags) == sorted(expected_tags)
280
273
281
274
282 @pytest.mark.parametrize("tag_data, expected_html", [
275 @pytest.mark.parametrize("tag_data, expected_html", [
283
276
284 (('state', '[stable]'), '<div class="metatag" tag="state stable">stable</div>'),
277 (('state', '[stable]'), '<div class="metatag" tag="state stable">stable</div>'),
285 (('state', '[stale]'), '<div class="metatag" tag="state stale">stale</div>'),
278 (('state', '[stale]'), '<div class="metatag" tag="state stale">stale</div>'),
286 (('state', '[featured]'), '<div class="metatag" tag="state featured">featured</div>'),
279 (('state', '[featured]'), '<div class="metatag" tag="state featured">featured</div>'),
287 (('state', '[dev]'), '<div class="metatag" tag="state dev">dev</div>'),
280 (('state', '[dev]'), '<div class="metatag" tag="state dev">dev</div>'),
288 (('state', '[dead]'), '<div class="metatag" tag="state dead">dead</div>'),
281 (('state', '[dead]'), '<div class="metatag" tag="state dead">dead</div>'),
289
282
290 (('label', '[personal]'), '<div class="metatag" tag="label">personal</div>'),
283 (('label', '[personal]'), '<div class="metatag" tag="label">personal</div>'),
291 (('generic', '[v2.0.0]'), '<div class="metatag" tag="generic">v2.0.0</div>'),
284 (('generic', '[v2.0.0]'), '<div class="metatag" tag="generic">v2.0.0</div>'),
292
285
293 (('lang', '[lang =&gt; JavaScript]'), '<div class="metatag" tag="lang">JavaScript</div>'),
286 (('lang', '[lang =&gt; JavaScript]'), '<div class="metatag" tag="lang">JavaScript</div>'),
294 (('lang', '[lang =&gt; C++]'), '<div class="metatag" tag="lang">C++</div>'),
287 (('lang', '[lang =&gt; C++]'), '<div class="metatag" tag="lang">C++</div>'),
295 (('lang', '[lang =&gt; C#]'), '<div class="metatag" tag="lang">C#</div>'),
288 (('lang', '[lang =&gt; C#]'), '<div class="metatag" tag="lang">C#</div>'),
296 (('lang', '[lang =&gt; Delphi/Object]'), '<div class="metatag" tag="lang">Delphi/Object</div>'),
289 (('lang', '[lang =&gt; Delphi/Object]'), '<div class="metatag" tag="lang">Delphi/Object</div>'),
297 (('lang', '[lang =&gt; Objective-C]'), '<div class="metatag" tag="lang">Objective-C</div>'),
290 (('lang', '[lang =&gt; Objective-C]'), '<div class="metatag" tag="lang">Objective-C</div>'),
298 (('lang', '[lang =&gt; .NET]'), '<div class="metatag" tag="lang">.NET</div>'),
291 (('lang', '[lang =&gt; .NET]'), '<div class="metatag" tag="lang">.NET</div>'),
299
292
300 (('license', '[license =&gt; BSD 3-clause]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/BSD 3-clause">BSD 3-clause</a></div>'),
293 (('license', '[license =&gt; BSD 3-clause]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/BSD 3-clause">BSD 3-clause</a></div>'),
301 (('license', '[license =&gt; GPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/GPLv3">GPLv3</a></div>'),
294 (('license', '[license =&gt; GPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/GPLv3">GPLv3</a></div>'),
302 (('license', '[license =&gt; MIT]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/MIT">MIT</a></div>'),
295 (('license', '[license =&gt; MIT]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/MIT">MIT</a></div>'),
303 (('license', '[license =&gt; AGPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/AGPLv3">AGPLv3</a></div>'),
296 (('license', '[license =&gt; AGPLv3]'), '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/AGPLv3">AGPLv3</a></div>'),
304
297
305 (('ref', '[requires =&gt; RepoName]'), '<div class="metatag" tag="ref requires">requires: <a href="/RepoName">RepoName</a></div>'),
298 (('ref', '[requires =&gt; RepoName]'), '<div class="metatag" tag="ref requires">requires: <a href="/RepoName">RepoName</a></div>'),
306 (('ref', '[recommends =&gt; GroupName]'), '<div class="metatag" tag="ref recommends">recommends: <a href="/GroupName">GroupName</a></div>'),
299 (('ref', '[recommends =&gt; GroupName]'), '<div class="metatag" tag="ref recommends">recommends: <a href="/GroupName">GroupName</a></div>'),
307 (('ref', '[conflicts =&gt; SomeName]'), '<div class="metatag" tag="ref conflicts">conflicts: <a href="/SomeName">SomeName</a></div>'),
300 (('ref', '[conflicts =&gt; SomeName]'), '<div class="metatag" tag="ref conflicts">conflicts: <a href="/SomeName">SomeName</a></div>'),
308 (('ref', '[base =&gt; SomeName]'), '<div class="metatag" tag="ref base">base: <a href="/SomeName">SomeName</a></div>'),
301 (('ref', '[base =&gt; SomeName]'), '<div class="metatag" tag="ref base">base: <a href="/SomeName">SomeName</a></div>'),
309
302
310 (('see', '[see =&gt; http://rhodecode.com]'), '<div class="metatag" tag="see">see: http://rhodecode.com </div>'),
303 (('see', '[see =&gt; http://rhodecode.com]'), '<div class="metatag" tag="see">see: http://rhodecode.com </div>'),
311
304
312 (('url', '[url =&gt; [linkName](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">linkName</a> </div>'),
305 (('url', '[url =&gt; [linkName](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">linkName</a> </div>'),
313 (('url', '[url =&gt; [example link](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">example link</a> </div>'),
306 (('url', '[url =&gt; [example link](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">example link</a> </div>'),
314 (('url', '[url =&gt; [v1.0.0](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">v1.0.0</a> </div>'),
307 (('url', '[url =&gt; [v1.0.0](https://rhodecode.com)]'), '<div class="metatag" tag="url"> <a href="https://rhodecode.com">v1.0.0</a> </div>'),
315
308
316 ])
309 ])
317 def test_metatags_stylize(tag_data, expected_html):
310 def test_metatags_stylize(tag_data, expected_html):
318 from rhodecode.lib.helpers import style_metatag
311 from rhodecode.lib.helpers import style_metatag
319 tag_type,value = tag_data
312 tag_type,value = tag_data
320 assert style_metatag(tag_type, value) == expected_html
313 assert style_metatag(tag_type, value) == expected_html
321
314
322
315
323 @pytest.mark.parametrize("tmpl_url, email, expected", [
316 @pytest.mark.parametrize("tmpl_url, email, expected", [
324 ('http://test.com/{email}', 'test@foo.com', 'http://test.com/test@foo.com'),
317 ('http://test.com/{email}', 'test@foo.com', 'http://test.com/test@foo.com'),
325
318
326 ('http://test.com/{md5email}', 'test@foo.com', 'http://test.com/3cb7232fcc48743000cb86d0d5022bd9'),
319 ('http://test.com/{md5email}', 'test@foo.com', 'http://test.com/3cb7232fcc48743000cb86d0d5022bd9'),
327 ('http://test.com/{md5email}', 'testΔ…Δ‡@foo.com', 'http://test.com/978debb907a3c55cd741872ab293ef30'),
320 ('http://test.com/{md5email}', 'testΔ…Δ‡@foo.com', 'http://test.com/978debb907a3c55cd741872ab293ef30'),
328
321
329 ('http://testX.com/{md5email}?s={size}', 'test@foo.com', 'http://testX.com/3cb7232fcc48743000cb86d0d5022bd9?s=24'),
322 ('http://testX.com/{md5email}?s={size}', 'test@foo.com', 'http://testX.com/3cb7232fcc48743000cb86d0d5022bd9?s=24'),
330 ('http://testX.com/{md5email}?s={size}', 'testΔ…Δ‡@foo.com', 'http://testX.com/978debb907a3c55cd741872ab293ef30?s=24'),
323 ('http://testX.com/{md5email}?s={size}', 'testΔ…Δ‡@foo.com', 'http://testX.com/978debb907a3c55cd741872ab293ef30?s=24'),
331
324
332 ('{scheme}://{netloc}/{md5email}/{size}', 'test@foo.com', 'https://server.com/3cb7232fcc48743000cb86d0d5022bd9/24'),
325 ('{scheme}://{netloc}/{md5email}/{size}', 'test@foo.com', 'https://server.com/3cb7232fcc48743000cb86d0d5022bd9/24'),
333 ('{scheme}://{netloc}/{md5email}/{size}', 'testΔ…Δ‡@foo.com', 'https://server.com/978debb907a3c55cd741872ab293ef30/24'),
326 ('{scheme}://{netloc}/{md5email}/{size}', 'testΔ…Δ‡@foo.com', 'https://server.com/978debb907a3c55cd741872ab293ef30/24'),
334
327
335 ('http://test.com/{email}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com'),
328 ('http://test.com/{email}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com'),
336 ('http://test.com/{email}?size={size}', 'test@foo.com', 'http://test.com/test@foo.com?size=24'),
329 ('http://test.com/{email}?size={size}', 'test@foo.com', 'http://test.com/test@foo.com?size=24'),
337 ('http://test.com/{email}?size={size}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com?size=24'),
330 ('http://test.com/{email}?size={size}', 'testΔ…Δ‡@foo.com', 'http://test.com/testΔ…Δ‡@foo.com?size=24'),
338 ])
331 ])
339 def test_gravatar_url_builder(tmpl_url, email, expected, request_stub):
332 def test_gravatar_url_builder(tmpl_url, email, expected, request_stub):
340 from rhodecode.lib.helpers import gravatar_url
333 from rhodecode.lib.helpers import gravatar_url
341
334
342 def fake_tmpl_context(_url):
335 def fake_tmpl_context(_url):
343 _c = AttributeDict()
336 _c = AttributeDict()
344 _c.visual = AttributeDict()
337 _c.visual = AttributeDict()
345 _c.visual.use_gravatar = True
338 _c.visual.use_gravatar = True
346 _c.visual.gravatar_url = _url
339 _c.visual.gravatar_url = _url
347 return _c
340 return _c
348
341
349 # mock pyramid.threadlocals
342 # mock pyramid.threadlocals
350 def fake_get_current_request():
343 def fake_get_current_request():
351 request_stub.scheme = 'https'
344 request_stub.scheme = 'https'
352 request_stub.host = 'server.com'
345 request_stub.host = 'server.com'
353
346
354 request_stub._call_context = fake_tmpl_context(tmpl_url)
347 request_stub._call_context = fake_tmpl_context(tmpl_url)
355 return request_stub
348 return request_stub
356
349
357 with mock.patch('rhodecode.lib.helpers.get_current_request',
350 with mock.patch('rhodecode.lib.helpers.get_current_request',
358 fake_get_current_request):
351 fake_get_current_request):
359
352
360 grav = gravatar_url(email_address=email, size=24)
353 grav = gravatar_url(email_address=email, size=24)
361 assert grav == expected
354 assert grav == expected
362
355
363
356
364 @pytest.mark.parametrize(
357 @pytest.mark.parametrize(
365 "email, first_name, last_name, expected_initials, expected_color", [
358 "email, first_name, last_name, expected_initials, expected_color", [
366
359
367 ('test@rhodecode.com', '', '', 'TR', '#8a994d'),
360 ('test@rhodecode.com', '', '', 'TR', '#8a994d'),
368 ('marcin.kuzminski@rhodecode.com', '', '', 'MK', '#6559b3'),
361 ('marcin.kuzminski@rhodecode.com', '', '', 'MK', '#6559b3'),
369 # special cases of email
362 # special cases of email
370 ('john.van.dam@rhodecode.com', '', '', 'JD', '#526600'),
363 ('john.van.dam@rhodecode.com', '', '', 'JD', '#526600'),
371 ('Guido.van.Rossum@rhodecode.com', '', '', 'GR', '#990052'),
364 ('Guido.van.Rossum@rhodecode.com', '', '', 'GR', '#990052'),
372 ('Guido.van.Rossum@rhodecode.com', 'Guido', 'Van Rossum', 'GR', '#990052'),
365 ('Guido.van.Rossum@rhodecode.com', 'Guido', 'Van Rossum', 'GR', '#990052'),
373
366
374 ('rhodecode+Guido.van.Rossum@rhodecode.com', '', '', 'RR', '#46598c'),
367 ('rhodecode+Guido.van.Rossum@rhodecode.com', '', '', 'RR', '#46598c'),
375 ('pclouds@rhodecode.com', 'Nguyα»…n ThΓ‘i', 'Tgọc Duy', 'ND', '#665200'),
368 ('pclouds@rhodecode.com', 'Nguyα»…n ThΓ‘i', 'Tgọc Duy', 'ND', '#665200'),
376
369
377 ('john-brown@foo.com', '', '', 'JF', '#73006b'),
370 ('john-brown@foo.com', '', '', 'JF', '#73006b'),
378 ('admin@rhodecode.com', 'Marcin', 'Kuzminski', 'MK', '#104036'),
371 ('admin@rhodecode.com', 'Marcin', 'Kuzminski', 'MK', '#104036'),
379 # partials
372 # partials
380 ('admin@rhodecode.com', 'Marcin', '', 'MR', '#104036'), # fn+email
373 ('admin@rhodecode.com', 'Marcin', '', 'MR', '#104036'), # fn+email
381 ('admin@rhodecode.com', '', 'Kuzminski', 'AK', '#104036'), # em+ln
374 ('admin@rhodecode.com', '', 'Kuzminski', 'AK', '#104036'), # em+ln
382 # non-ascii
375 # non-ascii
383 ('admin@rhodecode.com', 'Marcin', 'Śuzminski', 'MS', '#104036'),
376 ('admin@rhodecode.com', 'Marcin', 'Śuzminski', 'MS', '#104036'),
384 ('marcin.Ε›uzminski@rhodecode.com', '', '', 'MS', '#73000f'),
377 ('marcin.Ε›uzminski@rhodecode.com', '', '', 'MS', '#73000f'),
385
378
386 # special cases, LDAP can provide those...
379 # special cases, LDAP can provide those...
387 ('admin@', 'Marcin', 'Śuzminski', 'MS', '#aa00ff'),
380 ('admin@', 'Marcin', 'Śuzminski', 'MS', '#aa00ff'),
388 ('marcin.Ε›uzminski', '', '', 'MS', '#402020'),
381 ('marcin.Ε›uzminski', '', '', 'MS', '#402020'),
389 ('null', '', '', 'NL', '#8c4646'),
382 ('null', '', '', 'NL', '#8c4646'),
390 ('some.@abc.com', 'some', '', 'SA', '#664e33')
383 ('some.@abc.com', 'some', '', 'SA', '#664e33')
391 ])
384 ])
392 def test_initials_gravatar_pick_of_initials_and_color_algo(
385 def test_initials_gravatar_pick_of_initials_and_color_algo(
393 email, first_name, last_name, expected_initials, expected_color):
386 email, first_name, last_name, expected_initials, expected_color):
394 instance = InitialsGravatar(email, first_name, last_name)
387 instance = InitialsGravatar(email, first_name, last_name)
395 assert instance.get_initials() == expected_initials
388 assert instance.get_initials() == expected_initials
396 assert instance.str2color(email) == expected_color
389 assert instance.str2color(email) == expected_color
397
390
398
391
399 def test_initials_gravatar_mapping_algo():
392 def test_initials_gravatar_mapping_algo():
400 pos = set()
393 pos = set()
401 instance = InitialsGravatar('', '', '')
394 instance = InitialsGravatar('', '', '')
402 iterations = 0
395 iterations = 0
403
396
404 variations = []
397 variations = []
405 for letter1 in string.ascii_letters:
398 for letter1 in string.ascii_letters:
406 for letter2 in string.ascii_letters[::-1][:10]:
399 for letter2 in string.ascii_letters[::-1][:10]:
407 for letter3 in string.ascii_letters[:10]:
400 for letter3 in string.ascii_letters[:10]:
408 variations.append(
401 variations.append(
409 '%s@rhodecode.com' % (letter1+letter2+letter3))
402 '%s@rhodecode.com' % (letter1+letter2+letter3))
410
403
411 max_variations = 4096
404 max_variations = 4096
412 for email in variations[:max_variations]:
405 for email in variations[:max_variations]:
413 iterations += 1
406 iterations += 1
414 pos.add(
407 pos.add(
415 instance.pick_color_bank_index(email,
408 instance.pick_color_bank_index(email,
416 instance.get_color_bank()))
409 instance.get_color_bank()))
417
410
418 # we assume that we have match all 256 possible positions,
411 # we assume that we have match all 256 possible positions,
419 # in reasonable amount of different email addresses
412 # in reasonable amount of different email addresses
420 assert len(pos) == 256
413 assert len(pos) == 256
421 assert iterations == max_variations
414 assert iterations == max_variations
422
415
423
416
424 @pytest.mark.parametrize("tmpl, repo_name, overrides, prefix, expected", [
417 @pytest.mark.parametrize("tmpl, repo_name, overrides, prefix, expected", [
425 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '', 'http://vps1:8000/group/repo1'),
418 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '', 'http://vps1:8000/group/repo1'),
426 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/group/repo1'),
419 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/group/repo1'),
427 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '/rc', 'http://vps1:8000/rc/group/repo1'),
420 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {}, '/rc', 'http://vps1:8000/rc/group/repo1'),
428 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc', 'http://user@vps1:8000/rc/group/repo1'),
421 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc', 'http://user@vps1:8000/rc/group/repo1'),
429 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc', 'http://marcink@vps1:8000/rc/group/repo1'),
422 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc', 'http://marcink@vps1:8000/rc/group/repo1'),
430 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc/', 'http://user@vps1:8000/rc/group/repo1'),
423 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'user'}, '/rc/', 'http://user@vps1:8000/rc/group/repo1'),
431 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc/', 'http://marcink@vps1:8000/rc/group/repo1'),
424 (Repository.DEFAULT_CLONE_URI, 'group/repo1', {'user': 'marcink'}, '/rc/', 'http://marcink@vps1:8000/rc/group/repo1'),
432 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {}, '', 'http://vps1:8000/_23'),
425 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {}, '', 'http://vps1:8000/_23'),
433 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
426 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
434 ('http://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
427 ('http://{user}@{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://marcink@vps1:8000/_23'),
435 ('http://{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://vps1:8000/_23'),
428 ('http://{netloc}/_{repoid}', 'group/repo1', {'user': 'marcink'}, '', 'http://vps1:8000/_23'),
436 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://marcink@proxy1.server.com/group/repo1'),
429 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://marcink@proxy1.server.com/group/repo1'),
437 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {}, '', 'https://proxy1.server.com/group/repo1'),
430 ('https://{user}@proxy1.server.com/{repo}', 'group/repo1', {}, '', 'https://proxy1.server.com/group/repo1'),
438 ('https://proxy1.server.com/{user}/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://proxy1.server.com/marcink/group/repo1'),
431 ('https://proxy1.server.com/{user}/{repo}', 'group/repo1', {'user': 'marcink'}, '', 'https://proxy1.server.com/marcink/group/repo1'),
439 ])
432 ])
440 def test_clone_url_generator(tmpl, repo_name, overrides, prefix, expected):
433 def test_clone_url_generator(tmpl, repo_name, overrides, prefix, expected):
441 from rhodecode.lib.utils2 import get_clone_url
434 from rhodecode.lib.utils2 import get_clone_url
442
435
443 class RequestStub(object):
436 class RequestStub(object):
444 def request_url(self, name):
437 def request_url(self, name):
445 return 'http://vps1:8000' + prefix
438 return 'http://vps1:8000' + prefix
446
439
447 def route_url(self, name):
440 def route_url(self, name):
448 return self.request_url(name)
441 return self.request_url(name)
449
442
450 clone_url = get_clone_url(
443 clone_url = get_clone_url(
451 request=RequestStub(),
444 request=RequestStub(),
452 uri_tmpl=tmpl,
445 uri_tmpl=tmpl,
453 repo_name=repo_name, repo_id=23, repo_type='hg', **overrides)
446 repo_name=repo_name, repo_id=23, repo_type='hg', **overrides)
454 assert clone_url == expected
447 assert clone_url == expected
455
448
456
449
457 def test_clone_url_svn_ssh_generator():
450 def test_clone_url_svn_ssh_generator():
458 from rhodecode.lib.utils2 import get_clone_url
451 from rhodecode.lib.utils2 import get_clone_url
459
452
460 class RequestStub(object):
453 class RequestStub(object):
461 def request_url(self, name):
454 def request_url(self, name):
462 return 'http://vps1:8000'
455 return 'http://vps1:8000'
463
456
464 def route_url(self, name):
457 def route_url(self, name):
465 return self.request_url(name)
458 return self.request_url(name)
466
459
467 clone_url = get_clone_url(
460 clone_url = get_clone_url(
468 request=RequestStub(),
461 request=RequestStub(),
469 uri_tmpl=Repository.DEFAULT_CLONE_URI_SSH,
462 uri_tmpl=Repository.DEFAULT_CLONE_URI_SSH,
470 repo_name='svn-test', repo_id=23, repo_type='svn', **{'sys_user': 'rcdev'})
463 repo_name='svn-test', repo_id=23, repo_type='svn', **{'sys_user': 'rcdev'})
471 assert clone_url == 'svn+ssh://rcdev@vps1/svn-test'
464 assert clone_url == 'svn+ssh://rcdev@vps1/svn-test'
472
465
473
466
474 idx = 0
467 idx = 0
475
468
476
469
477 def _quick_url(text, tmpl="""<a class="tooltip-hovercard revision-link" href="%s" data-hovercard-alt="Commit: %s" data-hovercard-url="/some-url">%s</a>""", url_=None, commits=''):
470 def _quick_url(text, tmpl="""<a class="tooltip-hovercard revision-link" href="%s" data-hovercard-alt="Commit: %s" data-hovercard-url="/some-url">%s</a>""", url_=None, commits=''):
478 """
471 """
479 Changes `some text url[foo]` => `some text <a href="/">foo</a>
472 Changes `some text url[foo]` => `some text <a href="/">foo</a>
480
473
481 :param text:
474 :param text:
482 """
475 """
483 import re
476 import re
484 # quickly change expected url[] into a link
477 # quickly change expected url[] into a link
485 url_pat = re.compile(r'(?:url\[)(.+?)(?:\])')
478 url_pat = re.compile(r'(?:url\[)(.+?)(?:\])')
486 commits = commits or []
479 commits = commits or []
487
480
488 global idx
481 global idx
489 idx = 0
482 idx = 0
490
483
491 def url_func(match_obj):
484 def url_func(match_obj):
492 global idx
485 global idx
493 _url = match_obj.groups()[0]
486 _url = match_obj.groups()[0]
494 if commits:
487 if commits:
495 commit = commits[idx]
488 commit = commits[idx]
496 idx += 1
489 idx += 1
497 return tmpl % (url_ or '/some-url', _url, commit)
490 return tmpl % (url_ or '/some-url', _url, commit)
498 else:
491 else:
499 return tmpl % (url_ or '/some-url', _url)
492 return tmpl % (url_ or '/some-url', _url)
500
493
501 return url_pat.sub(url_func, text)
494 return url_pat.sub(url_func, text)
502
495
503
496
504 @pytest.mark.parametrize("sample, expected, commits", [
497 @pytest.mark.parametrize("sample, expected, commits", [
505 (
498 (
506 "",
499 "",
507 "",
500 "",
508 [""]
501 [""]
509 ),
502 ),
510 (
503 (
511 "git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
504 "git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
512 "git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
505 "git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
513 [""]
506 [""]
514 ),
507 ),
515 (
508 (
516 "from rev 000000000000",
509 "from rev 000000000000",
517 "from rev url[000000000000]",
510 "from rev url[000000000000]",
518 ["000000000000"]
511 ["000000000000"]
519 ),
512 ),
520
513
521 (
514 (
522 "from rev 000000000000123123 also rev 000000000000",
515 "from rev 000000000000123123 also rev 000000000000",
523 "from rev url[000000000000123123] also rev url[000000000000]",
516 "from rev url[000000000000123123] also rev url[000000000000]",
524 ["000000000000123123", "000000000000"]
517 ["000000000000123123", "000000000000"]
525 ),
518 ),
526 (
519 (
527 "this should-000 00",
520 "this should-000 00",
528 "this should-000 00",
521 "this should-000 00",
529 [""]
522 [""]
530 ),
523 ),
531 (
524 (
532 "longtextffffffffff rev 123123123123",
525 "longtextffffffffff rev 123123123123",
533 "longtextffffffffff rev url[123123123123]",
526 "longtextffffffffff rev url[123123123123]",
534 ["123123123123"]
527 ["123123123123"]
535 ),
528 ),
536 (
529 (
537 "rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
530 "rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
538 "rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
531 "rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
539 ["ffffffffffffffffffffffffffffffffffffffffffffffffff"]
532 ["ffffffffffffffffffffffffffffffffffffffffffffffffff"]
540 ),
533 ),
541 (
534 (
542 "ffffffffffff some text traalaa",
535 "ffffffffffff some text traalaa",
543 "url[ffffffffffff] some text traalaa",
536 "url[ffffffffffff] some text traalaa",
544 ["ffffffffffff"]
537 ["ffffffffffff"]
545 ),
538 ),
546 (
539 (
547 """Multi line
540 """Multi line
548 123123123123
541 123123123123
549 some text 000000000000
542 some text 000000000000
550 sometimes !
543 sometimes !
551 """,
544 """,
552 """Multi line
545 """Multi line
553 url[123123123123]
546 url[123123123123]
554 some text url[000000000000]
547 some text url[000000000000]
555 sometimes !
548 sometimes !
556 """,
549 """,
557 ["123123123123", "000000000000"]
550 ["123123123123", "000000000000"]
558 )
551 )
559 ], ids=no_newline_id_generator)
552 ], ids=no_newline_id_generator)
560 def test_urlify_commits(sample, expected, commits):
553 def test_urlify_commits(sample, expected, commits):
561 def fake_url(self, *args, **kwargs):
554 def fake_url(self, *args, **kwargs):
562 return '/some-url'
555 return '/some-url'
563
556
564 expected = _quick_url(expected, commits=commits)
557 expected = _quick_url(expected, commits=commits)
565
558
566 with mock.patch('rhodecode.lib.helpers.route_url', fake_url):
559 with mock.patch('rhodecode.lib.helpers.route_url', fake_url):
567 from rhodecode.lib.helpers import urlify_commits
560 from rhodecode.lib.helpers import urlify_commits
568 assert urlify_commits(sample, 'repo_name') == expected
561 assert urlify_commits(sample, 'repo_name') == expected
569
562
570
563
571 @pytest.mark.parametrize("sample, expected, url_", [
564 @pytest.mark.parametrize("sample, expected, url_", [
572 ("",
565 ("",
573 "",
566 "",
574 ""),
567 ""),
575 ("https://svn.apache.org/repos",
568 ("https://svn.apache.org/repos",
576 "url[https://svn.apache.org/repos]",
569 "url[https://svn.apache.org/repos]",
577 "https://svn.apache.org/repos"),
570 "https://svn.apache.org/repos"),
578 ("http://svn.apache.org/repos",
571 ("http://svn.apache.org/repos",
579 "url[http://svn.apache.org/repos]",
572 "url[http://svn.apache.org/repos]",
580 "http://svn.apache.org/repos"),
573 "http://svn.apache.org/repos"),
581 ("from rev a also rev http://google.com",
574 ("from rev a also rev http://google.com",
582 "from rev a also rev url[http://google.com]",
575 "from rev a also rev url[http://google.com]",
583 "http://google.com"),
576 "http://google.com"),
584 ("""Multi line
577 ("""Multi line
585 https://foo.bar.com
578 https://foo.bar.com
586 some text lalala""",
579 some text lalala""",
587 """Multi line
580 """Multi line
588 url[https://foo.bar.com]
581 url[https://foo.bar.com]
589 some text lalala""",
582 some text lalala""",
590 "https://foo.bar.com")
583 "https://foo.bar.com")
591 ], ids=no_newline_id_generator)
584 ], ids=no_newline_id_generator)
592 def test_urlify_test(sample, expected, url_):
585 def test_urlify_test(sample, expected, url_):
593 from rhodecode.lib.helpers import urlify_text
586 from rhodecode.lib.helpers import urlify_text
594 expected = _quick_url(expected, tmpl="""<a href="%s">%s</a>""", url_=url_)
587 expected = _quick_url(expected, tmpl="""<a href="%s">%s</a>""", url_=url_)
595 assert urlify_text(sample) == expected
588 assert urlify_text(sample) == expected
596
589
597
590
598 @pytest.mark.parametrize("test, expected", [
591 @pytest.mark.parametrize("test, expected", [
599 ("", None),
592 ("", None),
600 ("/_2", '2'),
593 ("/_2", '2'),
601 ("_2", '2'),
594 ("_2", '2'),
602 ("/_2/", '2'),
595 ("/_2/", '2'),
603 ("_2/", '2'),
596 ("_2/", '2'),
604
597
605 ("/_21", '21'),
598 ("/_21", '21'),
606 ("_21", '21'),
599 ("_21", '21'),
607 ("/_21/", '21'),
600 ("/_21/", '21'),
608 ("_21/", '21'),
601 ("_21/", '21'),
609
602
610 ("/_21/foobar", '21'),
603 ("/_21/foobar", '21'),
611 ("_21/121", '21'),
604 ("_21/121", '21'),
612 ("/_21/_12", '21'),
605 ("/_21/_12", '21'),
613 ("_21/rc/foo", '21'),
606 ("_21/rc/foo", '21'),
614
607
615 ])
608 ])
616 def test_get_repo_by_id(test, expected):
609 def test_get_repo_by_id(test, expected):
617 from rhodecode.model.repo import RepoModel
610 from rhodecode.model.repo import RepoModel
618 _test = RepoModel()._extract_id_from_repo_name(test)
611 _test = RepoModel()._extract_id_from_repo_name(test)
619 assert _test == expected
612 assert _test == expected
620
613
621
614
622 def test_invalidation_context(baseapp):
615 def test_invalidation_context(baseapp):
623 repo_id = 9999
616 repo_id = 9999
624
617
625 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
618 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
626 repo_id, CacheKey.CACHE_TYPE_FEED)
619 repo_id, CacheKey.CACHE_TYPE_FEED)
627 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
620 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
628 repo_id=repo_id)
621 repo_id=repo_id)
629 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
622 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
630
623
631 calls = [1, 2]
624 calls = [1, 2]
632
625
633 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
626 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
634 def _dummy_func(cache_key):
627 def _dummy_func(cache_key):
635 val = calls.pop(0)
628 val = calls.pop(0)
636 return 'result:{}'.format(val)
629 return 'result:{}'.format(val)
637
630
638 inv_context_manager = rc_cache.InvalidationContext(
631 inv_context_manager = rc_cache.InvalidationContext(
639 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
632 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
640
633
641 # 1st call, fresh caches
634 # 1st call, fresh caches
642 with inv_context_manager as invalidation_context:
635 with inv_context_manager as invalidation_context:
643 should_invalidate = invalidation_context.should_invalidate()
636 should_invalidate = invalidation_context.should_invalidate()
644 if should_invalidate:
637 if should_invalidate:
645 result = _dummy_func.refresh('some-key')
638 result = _dummy_func.refresh('some-key')
646 else:
639 else:
647 result = _dummy_func('some-key')
640 result = _dummy_func('some-key')
648
641
649 assert isinstance(invalidation_context, rc_cache.FreshRegionCache)
642 assert isinstance(invalidation_context, rc_cache.FreshRegionCache)
650 assert should_invalidate is True
643 assert should_invalidate is True
651
644
652 assert 'result:1' == result
645 assert 'result:1' == result
653 # should be cached so calling it twice will give the same result !
646 # should be cached so calling it twice will give the same result !
654 result = _dummy_func('some-key')
647 result = _dummy_func('some-key')
655 assert 'result:1' == result
648 assert 'result:1' == result
656
649
657 # 2nd call, we create a new context manager, this should be now key aware, and
650 # 2nd call, we create a new context manager, this should be now key aware, and
658 # return an active cache region
651 # return an active cache region
659 with inv_context_manager as invalidation_context:
652 with inv_context_manager as invalidation_context:
660 should_invalidate = invalidation_context.should_invalidate()
653 should_invalidate = invalidation_context.should_invalidate()
661 assert isinstance(invalidation_context, rc_cache.ActiveRegionCache)
654 assert isinstance(invalidation_context, rc_cache.ActiveRegionCache)
662 assert should_invalidate is False
655 assert should_invalidate is False
663
656
664 # Mark invalidation
657 # Mark invalidation
665 CacheKey.set_invalidate(invalidation_namespace)
658 CacheKey.set_invalidate(invalidation_namespace)
666
659
667 # 3nd call, fresh caches
660 # 3nd call, fresh caches
668 with inv_context_manager as invalidation_context:
661 with inv_context_manager as invalidation_context:
669 should_invalidate = invalidation_context.should_invalidate()
662 should_invalidate = invalidation_context.should_invalidate()
670 if should_invalidate:
663 if should_invalidate:
671 result = _dummy_func.refresh('some-key')
664 result = _dummy_func.refresh('some-key')
672 else:
665 else:
673 result = _dummy_func('some-key')
666 result = _dummy_func('some-key')
674
667
675 assert isinstance(invalidation_context, rc_cache.FreshRegionCache)
668 assert isinstance(invalidation_context, rc_cache.FreshRegionCache)
676 assert should_invalidate is True
669 assert should_invalidate is True
677
670
678 assert 'result:2' == result
671 assert 'result:2' == result
679
672
680 # cached again, same result
673 # cached again, same result
681 result = _dummy_func('some-key')
674 result = _dummy_func('some-key')
682 assert 'result:2' == result
675 assert 'result:2' == result
683
676
684
677
685 def test_invalidation_context_exception_in_compute(baseapp):
678 def test_invalidation_context_exception_in_compute(baseapp):
686 repo_id = 888
679 repo_id = 888
687
680
688 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
681 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
689 repo_id, CacheKey.CACHE_TYPE_FEED)
682 repo_id, CacheKey.CACHE_TYPE_FEED)
690 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
683 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
691 repo_id=repo_id)
684 repo_id=repo_id)
692 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
685 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
693
686
694 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
687 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
695 def _dummy_func(cache_key):
688 def _dummy_func(cache_key):
696 raise Exception('Error in cache func')
689 raise Exception('Error in cache func')
697
690
698 with pytest.raises(Exception):
691 with pytest.raises(Exception):
699 inv_context_manager = rc_cache.InvalidationContext(
692 inv_context_manager = rc_cache.InvalidationContext(
700 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
693 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
701
694
702 # 1st call, fresh caches
695 # 1st call, fresh caches
703 with inv_context_manager as invalidation_context:
696 with inv_context_manager as invalidation_context:
704 should_invalidate = invalidation_context.should_invalidate()
697 should_invalidate = invalidation_context.should_invalidate()
705 if should_invalidate:
698 if should_invalidate:
706 _dummy_func.refresh('some-key-2')
699 _dummy_func.refresh('some-key-2')
707 else:
700 else:
708 _dummy_func('some-key-2')
701 _dummy_func('some-key-2')
709
702
710
703
711 @pytest.mark.parametrize('execution_number', range(5))
704 @pytest.mark.parametrize('execution_number', range(5))
712 def test_cache_invalidation_race_condition(execution_number, baseapp):
705 def test_cache_invalidation_race_condition(execution_number, baseapp):
713 import time
706 import time
714
707
715 repo_id = 777
708 repo_id = 777
716
709
717 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
710 cache_namespace_uid = 'cache_repo_instance.{}_{}'.format(
718 repo_id, CacheKey.CACHE_TYPE_FEED)
711 repo_id, CacheKey.CACHE_TYPE_FEED)
719 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
712 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
720 repo_id=repo_id)
713 repo_id=repo_id)
721 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
714 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
722
715
723 @run_test_concurrently(25)
716 @run_test_concurrently(25)
724 def test_create_and_delete_cache_keys():
717 def test_create_and_delete_cache_keys():
725 time.sleep(0.2)
718 time.sleep(0.2)
726
719
727 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
720 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
728 def _dummy_func(cache_key):
721 def _dummy_func(cache_key):
729 val = 'async'
722 val = 'async'
730 return 'result:{}'.format(val)
723 return 'result:{}'.format(val)
731
724
732 inv_context_manager = rc_cache.InvalidationContext(
725 inv_context_manager = rc_cache.InvalidationContext(
733 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
726 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace)
734
727
735 # 1st call, fresh caches
728 # 1st call, fresh caches
736 with inv_context_manager as invalidation_context:
729 with inv_context_manager as invalidation_context:
737 should_invalidate = invalidation_context.should_invalidate()
730 should_invalidate = invalidation_context.should_invalidate()
738 if should_invalidate:
731 if should_invalidate:
739 _dummy_func.refresh('some-key-3')
732 _dummy_func.refresh('some-key-3')
740 else:
733 else:
741 _dummy_func('some-key-3')
734 _dummy_func('some-key-3')
742
735
743 # Mark invalidation
736 # Mark invalidation
744 CacheKey.set_invalidate(invalidation_namespace)
737 CacheKey.set_invalidate(invalidation_namespace)
745
738
746 test_create_and_delete_cache_keys()
739 test_create_and_delete_cache_keys()
General Comments 0
You need to be logged in to leave comments. Login now