##// END OF EJS Templates
svn: fix checkout SVN ssh url....
marcink -
r4133:23b8627d default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

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