##// END OF EJS Templates
clone-uri: fixed the problems with key mismatch that caused errors on summary page.
milka -
r4667:7c6563e6 default
parent child Browse files
Show More
@@ -1,1071 +1,1074 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Some simple helper functions
24 24 """
25 25
26 26 import collections
27 27 import datetime
28 28 import dateutil.relativedelta
29 29 import hashlib
30 30 import logging
31 31 import re
32 32 import sys
33 33 import time
34 34 import urllib
35 35 import urlobject
36 36 import uuid
37 37 import getpass
38 38 from functools import update_wrapper, partial
39 39
40 40 import pygments.lexers
41 41 import sqlalchemy
42 42 import sqlalchemy.engine.url
43 43 import sqlalchemy.exc
44 44 import sqlalchemy.sql
45 45 import webob
46 46 import pyramid.threadlocal
47 47 from pyramid import compat
48 48 from pyramid.settings import asbool
49 49
50 50 import rhodecode
51 51 from rhodecode.translation import _, _pluralize
52 52
53 53
54 54 def md5(s):
55 55 return hashlib.md5(s).hexdigest()
56 56
57 57
58 58 def md5_safe(s):
59 59 return md5(safe_str(s))
60 60
61 61
62 62 def sha1(s):
63 63 return hashlib.sha1(s).hexdigest()
64 64
65 65
66 66 def sha1_safe(s):
67 67 return sha1(safe_str(s))
68 68
69 69
70 70 def __get_lem(extra_mapping=None):
71 71 """
72 72 Get language extension map based on what's inside pygments lexers
73 73 """
74 74 d = collections.defaultdict(lambda: [])
75 75
76 76 def __clean(s):
77 77 s = s.lstrip('*')
78 78 s = s.lstrip('.')
79 79
80 80 if s.find('[') != -1:
81 81 exts = []
82 82 start, stop = s.find('['), s.find(']')
83 83
84 84 for suffix in s[start + 1:stop]:
85 85 exts.append(s[:s.find('[')] + suffix)
86 86 return [e.lower() for e in exts]
87 87 else:
88 88 return [s.lower()]
89 89
90 90 for lx, t in sorted(pygments.lexers.LEXERS.items()):
91 91 m = map(__clean, t[-2])
92 92 if m:
93 93 m = reduce(lambda x, y: x + y, m)
94 94 for ext in m:
95 95 desc = lx.replace('Lexer', '')
96 96 d[ext].append(desc)
97 97
98 98 data = dict(d)
99 99
100 100 extra_mapping = extra_mapping or {}
101 101 if extra_mapping:
102 102 for k, v in extra_mapping.items():
103 103 if k not in data:
104 104 # register new mapping2lexer
105 105 data[k] = [v]
106 106
107 107 return data
108 108
109 109
110 110 def str2bool(_str):
111 111 """
112 112 returns True/False value from given string, it tries to translate the
113 113 string into boolean
114 114
115 115 :param _str: string value to translate into boolean
116 116 :rtype: boolean
117 117 :returns: boolean from given string
118 118 """
119 119 if _str is None:
120 120 return False
121 121 if _str in (True, False):
122 122 return _str
123 123 _str = str(_str).strip().lower()
124 124 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
125 125
126 126
127 127 def aslist(obj, sep=None, strip=True):
128 128 """
129 129 Returns given string separated by sep as list
130 130
131 131 :param obj:
132 132 :param sep:
133 133 :param strip:
134 134 """
135 135 if isinstance(obj, (basestring,)):
136 136 lst = obj.split(sep)
137 137 if strip:
138 138 lst = [v.strip() for v in lst]
139 139 return lst
140 140 elif isinstance(obj, (list, tuple)):
141 141 return obj
142 142 elif obj is None:
143 143 return []
144 144 else:
145 145 return [obj]
146 146
147 147
148 148 def convert_line_endings(line, mode):
149 149 """
150 150 Converts a given line "line end" accordingly to given mode
151 151
152 152 Available modes are::
153 153 0 - Unix
154 154 1 - Mac
155 155 2 - DOS
156 156
157 157 :param line: given line to convert
158 158 :param mode: mode to convert to
159 159 :rtype: str
160 160 :return: converted line according to mode
161 161 """
162 162 if mode == 0:
163 163 line = line.replace('\r\n', '\n')
164 164 line = line.replace('\r', '\n')
165 165 elif mode == 1:
166 166 line = line.replace('\r\n', '\r')
167 167 line = line.replace('\n', '\r')
168 168 elif mode == 2:
169 169 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
170 170 return line
171 171
172 172
173 173 def detect_mode(line, default):
174 174 """
175 175 Detects line break for given line, if line break couldn't be found
176 176 given default value is returned
177 177
178 178 :param line: str line
179 179 :param default: default
180 180 :rtype: int
181 181 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
182 182 """
183 183 if line.endswith('\r\n'):
184 184 return 2
185 185 elif line.endswith('\n'):
186 186 return 0
187 187 elif line.endswith('\r'):
188 188 return 1
189 189 else:
190 190 return default
191 191
192 192
193 193 def safe_int(val, default=None):
194 194 """
195 195 Returns int() of val if val is not convertable to int use default
196 196 instead
197 197
198 198 :param val:
199 199 :param default:
200 200 """
201 201
202 202 try:
203 203 val = int(val)
204 204 except (ValueError, TypeError):
205 205 val = default
206 206
207 207 return val
208 208
209 209
210 210 def safe_unicode(str_, from_encoding=None, use_chardet=False):
211 211 """
212 212 safe unicode function. Does few trick to turn str_ into unicode
213 213
214 214 In case of UnicodeDecode error, we try to return it with encoding detected
215 215 by chardet library if it fails fallback to unicode with errors replaced
216 216
217 217 :param str_: string to decode
218 218 :rtype: unicode
219 219 :returns: unicode object
220 220 """
221 221 if isinstance(str_, unicode):
222 222 return str_
223 223
224 224 if not from_encoding:
225 225 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
226 226 'utf8'), sep=',')
227 227 from_encoding = DEFAULT_ENCODINGS
228 228
229 229 if not isinstance(from_encoding, (list, tuple)):
230 230 from_encoding = [from_encoding]
231 231
232 232 try:
233 233 return unicode(str_)
234 234 except UnicodeDecodeError:
235 235 pass
236 236
237 237 for enc in from_encoding:
238 238 try:
239 239 return unicode(str_, enc)
240 240 except UnicodeDecodeError:
241 241 pass
242 242
243 243 if use_chardet:
244 244 try:
245 245 import chardet
246 246 encoding = chardet.detect(str_)['encoding']
247 247 if encoding is None:
248 248 raise Exception()
249 249 return str_.decode(encoding)
250 250 except (ImportError, UnicodeDecodeError, Exception):
251 251 return unicode(str_, from_encoding[0], 'replace')
252 252 else:
253 253 return unicode(str_, from_encoding[0], 'replace')
254 254
255 255 def safe_str(unicode_, to_encoding=None, use_chardet=False):
256 256 """
257 257 safe str function. Does few trick to turn unicode_ into string
258 258
259 259 In case of UnicodeEncodeError, we try to return it with encoding detected
260 260 by chardet library if it fails fallback to string with errors replaced
261 261
262 262 :param unicode_: unicode to encode
263 263 :rtype: str
264 264 :returns: str object
265 265 """
266 266
267 267 # if it's not basestr cast to str
268 268 if not isinstance(unicode_, compat.string_types):
269 269 return str(unicode_)
270 270
271 271 if isinstance(unicode_, str):
272 272 return unicode_
273 273
274 274 if not to_encoding:
275 275 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
276 276 'utf8'), sep=',')
277 277 to_encoding = DEFAULT_ENCODINGS
278 278
279 279 if not isinstance(to_encoding, (list, tuple)):
280 280 to_encoding = [to_encoding]
281 281
282 282 for enc in to_encoding:
283 283 try:
284 284 return unicode_.encode(enc)
285 285 except UnicodeEncodeError:
286 286 pass
287 287
288 288 if use_chardet:
289 289 try:
290 290 import chardet
291 291 encoding = chardet.detect(unicode_)['encoding']
292 292 if encoding is None:
293 293 raise UnicodeEncodeError()
294 294
295 295 return unicode_.encode(encoding)
296 296 except (ImportError, UnicodeEncodeError):
297 297 return unicode_.encode(to_encoding[0], 'replace')
298 298 else:
299 299 return unicode_.encode(to_encoding[0], 'replace')
300 300
301 301
302 302 def remove_suffix(s, suffix):
303 303 if s.endswith(suffix):
304 304 s = s[:-1 * len(suffix)]
305 305 return s
306 306
307 307
308 308 def remove_prefix(s, prefix):
309 309 if s.startswith(prefix):
310 310 s = s[len(prefix):]
311 311 return s
312 312
313 313
314 314 def find_calling_context(ignore_modules=None):
315 315 """
316 316 Look through the calling stack and return the frame which called
317 317 this function and is part of core module ( ie. rhodecode.* )
318 318
319 319 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
320 320 """
321 321
322 322 ignore_modules = ignore_modules or []
323 323
324 324 f = sys._getframe(2)
325 325 while f.f_back is not None:
326 326 name = f.f_globals.get('__name__')
327 327 if name and name.startswith(__name__.split('.')[0]):
328 328 if name not in ignore_modules:
329 329 return f
330 330 f = f.f_back
331 331 return None
332 332
333 333
334 334 def ping_connection(connection, branch):
335 335 if branch:
336 336 # "branch" refers to a sub-connection of a connection,
337 337 # we don't want to bother pinging on these.
338 338 return
339 339
340 340 # turn off "close with result". This flag is only used with
341 341 # "connectionless" execution, otherwise will be False in any case
342 342 save_should_close_with_result = connection.should_close_with_result
343 343 connection.should_close_with_result = False
344 344
345 345 try:
346 346 # run a SELECT 1. use a core select() so that
347 347 # the SELECT of a scalar value without a table is
348 348 # appropriately formatted for the backend
349 349 connection.scalar(sqlalchemy.sql.select([1]))
350 350 except sqlalchemy.exc.DBAPIError as err:
351 351 # catch SQLAlchemy's DBAPIError, which is a wrapper
352 352 # for the DBAPI's exception. It includes a .connection_invalidated
353 353 # attribute which specifies if this connection is a "disconnect"
354 354 # condition, which is based on inspection of the original exception
355 355 # by the dialect in use.
356 356 if err.connection_invalidated:
357 357 # run the same SELECT again - the connection will re-validate
358 358 # itself and establish a new connection. The disconnect detection
359 359 # here also causes the whole connection pool to be invalidated
360 360 # so that all stale connections are discarded.
361 361 connection.scalar(sqlalchemy.sql.select([1]))
362 362 else:
363 363 raise
364 364 finally:
365 365 # restore "close with result"
366 366 connection.should_close_with_result = save_should_close_with_result
367 367
368 368
369 369 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
370 370 """Custom engine_from_config functions."""
371 371 log = logging.getLogger('sqlalchemy.engine')
372 372 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
373 373 debug = asbool(configuration.pop('sqlalchemy.db1.debug_query', None))
374 374
375 375 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
376 376
377 377 def color_sql(sql):
378 378 color_seq = '\033[1;33m' # This is yellow: code 33
379 379 normal = '\x1b[0m'
380 380 return ''.join([color_seq, sql, normal])
381 381
382 382 if use_ping_connection:
383 383 log.debug('Adding ping_connection on the engine config.')
384 384 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
385 385
386 386 if debug:
387 387 # attach events only for debug configuration
388 388 def before_cursor_execute(conn, cursor, statement,
389 389 parameters, context, executemany):
390 390 setattr(conn, 'query_start_time', time.time())
391 391 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
392 392 calling_context = find_calling_context(ignore_modules=[
393 393 'rhodecode.lib.caching_query',
394 394 'rhodecode.model.settings',
395 395 ])
396 396 if calling_context:
397 397 log.info(color_sql('call context %s:%s' % (
398 398 calling_context.f_code.co_filename,
399 399 calling_context.f_lineno,
400 400 )))
401 401
402 402 def after_cursor_execute(conn, cursor, statement,
403 403 parameters, context, executemany):
404 404 delattr(conn, 'query_start_time')
405 405
406 406 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
407 407 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
408 408
409 409 return engine
410 410
411 411
412 412 def get_encryption_key(config):
413 413 secret = config.get('rhodecode.encrypted_values.secret')
414 414 default = config['beaker.session.secret']
415 415 return secret or default
416 416
417 417
418 418 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
419 419 short_format=False):
420 420 """
421 421 Turns a datetime into an age string.
422 422 If show_short_version is True, this generates a shorter string with
423 423 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
424 424
425 425 * IMPORTANT*
426 426 Code of this function is written in special way so it's easier to
427 427 backport it to javascript. If you mean to update it, please also update
428 428 `jquery.timeago-extension.js` file
429 429
430 430 :param prevdate: datetime object
431 431 :param now: get current time, if not define we use
432 432 `datetime.datetime.now()`
433 433 :param show_short_version: if it should approximate the date and
434 434 return a shorter string
435 435 :param show_suffix:
436 436 :param short_format: show short format, eg 2D instead of 2 days
437 437 :rtype: unicode
438 438 :returns: unicode words describing age
439 439 """
440 440
441 441 def _get_relative_delta(now, prevdate):
442 442 base = dateutil.relativedelta.relativedelta(now, prevdate)
443 443 return {
444 444 'year': base.years,
445 445 'month': base.months,
446 446 'day': base.days,
447 447 'hour': base.hours,
448 448 'minute': base.minutes,
449 449 'second': base.seconds,
450 450 }
451 451
452 452 def _is_leap_year(year):
453 453 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
454 454
455 455 def get_month(prevdate):
456 456 return prevdate.month
457 457
458 458 def get_year(prevdate):
459 459 return prevdate.year
460 460
461 461 now = now or datetime.datetime.now()
462 462 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
463 463 deltas = {}
464 464 future = False
465 465
466 466 if prevdate > now:
467 467 now_old = now
468 468 now = prevdate
469 469 prevdate = now_old
470 470 future = True
471 471 if future:
472 472 prevdate = prevdate.replace(microsecond=0)
473 473 # Get date parts deltas
474 474 for part in order:
475 475 rel_delta = _get_relative_delta(now, prevdate)
476 476 deltas[part] = rel_delta[part]
477 477
478 478 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
479 479 # not 1 hour, -59 minutes and -59 seconds)
480 480 offsets = [[5, 60], [4, 60], [3, 24]]
481 481 for element in offsets: # seconds, minutes, hours
482 482 num = element[0]
483 483 length = element[1]
484 484
485 485 part = order[num]
486 486 carry_part = order[num - 1]
487 487
488 488 if deltas[part] < 0:
489 489 deltas[part] += length
490 490 deltas[carry_part] -= 1
491 491
492 492 # Same thing for days except that the increment depends on the (variable)
493 493 # number of days in the month
494 494 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
495 495 if deltas['day'] < 0:
496 496 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
497 497 deltas['day'] += 29
498 498 else:
499 499 deltas['day'] += month_lengths[get_month(prevdate) - 1]
500 500
501 501 deltas['month'] -= 1
502 502
503 503 if deltas['month'] < 0:
504 504 deltas['month'] += 12
505 505 deltas['year'] -= 1
506 506
507 507 # Format the result
508 508 if short_format:
509 509 fmt_funcs = {
510 510 'year': lambda d: u'%dy' % d,
511 511 'month': lambda d: u'%dm' % d,
512 512 'day': lambda d: u'%dd' % d,
513 513 'hour': lambda d: u'%dh' % d,
514 514 'minute': lambda d: u'%dmin' % d,
515 515 'second': lambda d: u'%dsec' % d,
516 516 }
517 517 else:
518 518 fmt_funcs = {
519 519 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
520 520 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
521 521 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
522 522 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
523 523 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
524 524 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
525 525 }
526 526
527 527 i = 0
528 528 for part in order:
529 529 value = deltas[part]
530 530 if value != 0:
531 531
532 532 if i < 5:
533 533 sub_part = order[i + 1]
534 534 sub_value = deltas[sub_part]
535 535 else:
536 536 sub_value = 0
537 537
538 538 if sub_value == 0 or show_short_version:
539 539 _val = fmt_funcs[part](value)
540 540 if future:
541 541 if show_suffix:
542 542 return _(u'in ${ago}', mapping={'ago': _val})
543 543 else:
544 544 return _(_val)
545 545
546 546 else:
547 547 if show_suffix:
548 548 return _(u'${ago} ago', mapping={'ago': _val})
549 549 else:
550 550 return _(_val)
551 551
552 552 val = fmt_funcs[part](value)
553 553 val_detail = fmt_funcs[sub_part](sub_value)
554 554 mapping = {'val': val, 'detail': val_detail}
555 555
556 556 if short_format:
557 557 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
558 558 if show_suffix:
559 559 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
560 560 if future:
561 561 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
562 562 else:
563 563 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
564 564 if show_suffix:
565 565 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
566 566 if future:
567 567 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
568 568
569 569 return datetime_tmpl
570 570 i += 1
571 571 return _(u'just now')
572 572
573 573
574 574 def age_from_seconds(seconds):
575 575 seconds = safe_int(seconds) or 0
576 576 prevdate = time_to_datetime(time.time() + seconds)
577 577 return age(prevdate, show_suffix=False, show_short_version=True)
578 578
579 579
580 580 def cleaned_uri(uri):
581 581 """
582 582 Quotes '[' and ']' from uri if there is only one of them.
583 583 according to RFC3986 we cannot use such chars in uri
584 584 :param uri:
585 585 :return: uri without this chars
586 586 """
587 587 return urllib.quote(uri, safe='@$:/')
588 588
589 589
590 590 def credentials_filter(uri):
591 591 """
592 592 Returns a url with removed credentials
593 593
594 594 :param uri:
595 595 """
596 596 import urlobject
597 if isinstance(uri, rhodecode.lib.encrypt.InvalidDecryptedValue):
598 return 'InvalidDecryptionKey'
599
597 600 url_obj = urlobject.URLObject(cleaned_uri(uri))
598 601 url_obj = url_obj.without_password().without_username()
599 602
600 603 return url_obj
601 604
602 605
603 606 def get_host_info(request):
604 607 """
605 608 Generate host info, to obtain full url e.g https://server.com
606 609 use this
607 610 `{scheme}://{netloc}`
608 611 """
609 612 if not request:
610 613 return {}
611 614
612 615 qualified_home_url = request.route_url('home')
613 616 parsed_url = urlobject.URLObject(qualified_home_url)
614 617 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
615 618
616 619 return {
617 620 'scheme': parsed_url.scheme,
618 621 'netloc': parsed_url.netloc+decoded_path,
619 622 'hostname': parsed_url.hostname,
620 623 }
621 624
622 625
623 626 def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
624 627 qualified_home_url = request.route_url('home')
625 628 parsed_url = urlobject.URLObject(qualified_home_url)
626 629 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
627 630
628 631 args = {
629 632 'scheme': parsed_url.scheme,
630 633 'user': '',
631 634 'sys_user': getpass.getuser(),
632 635 # path if we use proxy-prefix
633 636 'netloc': parsed_url.netloc+decoded_path,
634 637 'hostname': parsed_url.hostname,
635 638 'prefix': decoded_path,
636 639 'repo': repo_name,
637 640 'repoid': str(repo_id),
638 641 'repo_type': repo_type
639 642 }
640 643 args.update(override)
641 644 args['user'] = urllib.quote(safe_str(args['user']))
642 645
643 646 for k, v in args.items():
644 647 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
645 648
646 649 # special case for SVN clone url
647 650 if repo_type == 'svn':
648 651 uri_tmpl = uri_tmpl.replace('ssh://', 'svn+ssh://')
649 652
650 653 # remove leading @ sign if it's present. Case of empty user
651 654 url_obj = urlobject.URLObject(uri_tmpl)
652 655 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
653 656
654 657 return safe_unicode(url)
655 658
656 659
657 660 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
658 661 maybe_unreachable=False, reference_obj=None):
659 662 """
660 663 Safe version of get_commit if this commit doesn't exists for a
661 664 repository it returns a Dummy one instead
662 665
663 666 :param repo: repository instance
664 667 :param commit_id: commit id as str
665 668 :param commit_idx: numeric commit index
666 669 :param pre_load: optional list of commit attributes to load
667 670 :param maybe_unreachable: translate unreachable commits on git repos
668 671 :param reference_obj: explicitly search via a reference obj in git. E.g "branch:123" would mean branch "123"
669 672 """
670 673 # TODO(skreft): remove these circular imports
671 674 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
672 675 from rhodecode.lib.vcs.exceptions import RepositoryError
673 676 if not isinstance(repo, BaseRepository):
674 677 raise Exception('You must pass an Repository '
675 678 'object as first argument got %s', type(repo))
676 679
677 680 try:
678 681 commit = repo.get_commit(
679 682 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load,
680 683 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
681 684 except (RepositoryError, LookupError):
682 685 commit = EmptyCommit()
683 686 return commit
684 687
685 688
686 689 def datetime_to_time(dt):
687 690 if dt:
688 691 return time.mktime(dt.timetuple())
689 692
690 693
691 694 def time_to_datetime(tm):
692 695 if tm:
693 696 if isinstance(tm, compat.string_types):
694 697 try:
695 698 tm = float(tm)
696 699 except ValueError:
697 700 return
698 701 return datetime.datetime.fromtimestamp(tm)
699 702
700 703
701 704 def time_to_utcdatetime(tm):
702 705 if tm:
703 706 if isinstance(tm, compat.string_types):
704 707 try:
705 708 tm = float(tm)
706 709 except ValueError:
707 710 return
708 711 return datetime.datetime.utcfromtimestamp(tm)
709 712
710 713
711 714 MENTIONS_REGEX = re.compile(
712 715 # ^@ or @ without any special chars in front
713 716 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
714 717 # main body starts with letter, then can be . - _
715 718 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
716 719 re.VERBOSE | re.MULTILINE)
717 720
718 721
719 722 def extract_mentioned_users(s):
720 723 """
721 724 Returns unique usernames from given string s that have @mention
722 725
723 726 :param s: string to get mentions
724 727 """
725 728 usrs = set()
726 729 for username in MENTIONS_REGEX.findall(s):
727 730 usrs.add(username)
728 731
729 732 return sorted(list(usrs), key=lambda k: k.lower())
730 733
731 734
732 735 class AttributeDictBase(dict):
733 736 def __getstate__(self):
734 737 odict = self.__dict__ # get attribute dictionary
735 738 return odict
736 739
737 740 def __setstate__(self, dict):
738 741 self.__dict__ = dict
739 742
740 743 __setattr__ = dict.__setitem__
741 744 __delattr__ = dict.__delitem__
742 745
743 746
744 747 class StrictAttributeDict(AttributeDictBase):
745 748 """
746 749 Strict Version of Attribute dict which raises an Attribute error when
747 750 requested attribute is not set
748 751 """
749 752 def __getattr__(self, attr):
750 753 try:
751 754 return self[attr]
752 755 except KeyError:
753 756 raise AttributeError('%s object has no attribute %s' % (
754 757 self.__class__, attr))
755 758
756 759
757 760 class AttributeDict(AttributeDictBase):
758 761 def __getattr__(self, attr):
759 762 return self.get(attr, None)
760 763
761 764
762 765
763 766 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
764 767 def __init__(self, default_factory=None, *args, **kwargs):
765 768 # in python3 you can omit the args to super
766 769 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
767 770 self.default_factory = default_factory
768 771
769 772
770 773 def fix_PATH(os_=None):
771 774 """
772 775 Get current active python path, and append it to PATH variable to fix
773 776 issues of subprocess calls and different python versions
774 777 """
775 778 if os_ is None:
776 779 import os
777 780 else:
778 781 os = os_
779 782
780 783 cur_path = os.path.split(sys.executable)[0]
781 784 if not os.environ['PATH'].startswith(cur_path):
782 785 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
783 786
784 787
785 788 def obfuscate_url_pw(engine):
786 789 _url = engine or ''
787 790 try:
788 791 _url = sqlalchemy.engine.url.make_url(engine)
789 792 if _url.password:
790 793 _url.password = 'XXXXX'
791 794 except Exception:
792 795 pass
793 796 return unicode(_url)
794 797
795 798
796 799 def get_server_url(environ):
797 800 req = webob.Request(environ)
798 801 return req.host_url + req.script_name
799 802
800 803
801 804 def unique_id(hexlen=32):
802 805 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
803 806 return suuid(truncate_to=hexlen, alphabet=alphabet)
804 807
805 808
806 809 def suuid(url=None, truncate_to=22, alphabet=None):
807 810 """
808 811 Generate and return a short URL safe UUID.
809 812
810 813 If the url parameter is provided, set the namespace to the provided
811 814 URL and generate a UUID.
812 815
813 816 :param url to get the uuid for
814 817 :truncate_to: truncate the basic 22 UUID to shorter version
815 818
816 819 The IDs won't be universally unique any longer, but the probability of
817 820 a collision will still be very low.
818 821 """
819 822 # Define our alphabet.
820 823 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
821 824
822 825 # If no URL is given, generate a random UUID.
823 826 if url is None:
824 827 unique_id = uuid.uuid4().int
825 828 else:
826 829 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
827 830
828 831 alphabet_length = len(_ALPHABET)
829 832 output = []
830 833 while unique_id > 0:
831 834 digit = unique_id % alphabet_length
832 835 output.append(_ALPHABET[digit])
833 836 unique_id = int(unique_id / alphabet_length)
834 837 return "".join(output)[:truncate_to]
835 838
836 839
837 840 def get_current_rhodecode_user(request=None):
838 841 """
839 842 Gets rhodecode user from request
840 843 """
841 844 pyramid_request = request or pyramid.threadlocal.get_current_request()
842 845
843 846 # web case
844 847 if pyramid_request and hasattr(pyramid_request, 'user'):
845 848 return pyramid_request.user
846 849
847 850 # api case
848 851 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
849 852 return pyramid_request.rpc_user
850 853
851 854 return None
852 855
853 856
854 857 def action_logger_generic(action, namespace=''):
855 858 """
856 859 A generic logger for actions useful to the system overview, tries to find
857 860 an acting user for the context of the call otherwise reports unknown user
858 861
859 862 :param action: logging message eg 'comment 5 deleted'
860 863 :param type: string
861 864
862 865 :param namespace: namespace of the logging message eg. 'repo.comments'
863 866 :param type: string
864 867
865 868 """
866 869
867 870 logger_name = 'rhodecode.actions'
868 871
869 872 if namespace:
870 873 logger_name += '.' + namespace
871 874
872 875 log = logging.getLogger(logger_name)
873 876
874 877 # get a user if we can
875 878 user = get_current_rhodecode_user()
876 879
877 880 logfunc = log.info
878 881
879 882 if not user:
880 883 user = '<unknown user>'
881 884 logfunc = log.warning
882 885
883 886 logfunc('Logging action by {}: {}'.format(user, action))
884 887
885 888
886 889 def escape_split(text, sep=',', maxsplit=-1):
887 890 r"""
888 891 Allows for escaping of the separator: e.g. arg='foo\, bar'
889 892
890 893 It should be noted that the way bash et. al. do command line parsing, those
891 894 single quotes are required.
892 895 """
893 896 escaped_sep = r'\%s' % sep
894 897
895 898 if escaped_sep not in text:
896 899 return text.split(sep, maxsplit)
897 900
898 901 before, _mid, after = text.partition(escaped_sep)
899 902 startlist = before.split(sep, maxsplit) # a regular split is fine here
900 903 unfinished = startlist[-1]
901 904 startlist = startlist[:-1]
902 905
903 906 # recurse because there may be more escaped separators
904 907 endlist = escape_split(after, sep, maxsplit)
905 908
906 909 # finish building the escaped value. we use endlist[0] becaue the first
907 910 # part of the string sent in recursion is the rest of the escaped value.
908 911 unfinished += sep + endlist[0]
909 912
910 913 return startlist + [unfinished] + endlist[1:] # put together all the parts
911 914
912 915
913 916 class OptionalAttr(object):
914 917 """
915 918 Special Optional Option that defines other attribute. Example::
916 919
917 920 def test(apiuser, userid=Optional(OAttr('apiuser')):
918 921 user = Optional.extract(userid)
919 922 # calls
920 923
921 924 """
922 925
923 926 def __init__(self, attr_name):
924 927 self.attr_name = attr_name
925 928
926 929 def __repr__(self):
927 930 return '<OptionalAttr:%s>' % self.attr_name
928 931
929 932 def __call__(self):
930 933 return self
931 934
932 935
933 936 # alias
934 937 OAttr = OptionalAttr
935 938
936 939
937 940 class Optional(object):
938 941 """
939 942 Defines an optional parameter::
940 943
941 944 param = param.getval() if isinstance(param, Optional) else param
942 945 param = param() if isinstance(param, Optional) else param
943 946
944 947 is equivalent of::
945 948
946 949 param = Optional.extract(param)
947 950
948 951 """
949 952
950 953 def __init__(self, type_):
951 954 self.type_ = type_
952 955
953 956 def __repr__(self):
954 957 return '<Optional:%s>' % self.type_.__repr__()
955 958
956 959 def __call__(self):
957 960 return self.getval()
958 961
959 962 def getval(self):
960 963 """
961 964 returns value from this Optional instance
962 965 """
963 966 if isinstance(self.type_, OAttr):
964 967 # use params name
965 968 return self.type_.attr_name
966 969 return self.type_
967 970
968 971 @classmethod
969 972 def extract(cls, val):
970 973 """
971 974 Extracts value from Optional() instance
972 975
973 976 :param val:
974 977 :return: original value if it's not Optional instance else
975 978 value of instance
976 979 """
977 980 if isinstance(val, cls):
978 981 return val.getval()
979 982 return val
980 983
981 984
982 985 def glob2re(pat):
983 986 """
984 987 Translate a shell PATTERN to a regular expression.
985 988
986 989 There is no way to quote meta-characters.
987 990 """
988 991
989 992 i, n = 0, len(pat)
990 993 res = ''
991 994 while i < n:
992 995 c = pat[i]
993 996 i = i+1
994 997 if c == '*':
995 998 #res = res + '.*'
996 999 res = res + '[^/]*'
997 1000 elif c == '?':
998 1001 #res = res + '.'
999 1002 res = res + '[^/]'
1000 1003 elif c == '[':
1001 1004 j = i
1002 1005 if j < n and pat[j] == '!':
1003 1006 j = j+1
1004 1007 if j < n and pat[j] == ']':
1005 1008 j = j+1
1006 1009 while j < n and pat[j] != ']':
1007 1010 j = j+1
1008 1011 if j >= n:
1009 1012 res = res + '\\['
1010 1013 else:
1011 1014 stuff = pat[i:j].replace('\\','\\\\')
1012 1015 i = j+1
1013 1016 if stuff[0] == '!':
1014 1017 stuff = '^' + stuff[1:]
1015 1018 elif stuff[0] == '^':
1016 1019 stuff = '\\' + stuff
1017 1020 res = '%s[%s]' % (res, stuff)
1018 1021 else:
1019 1022 res = res + re.escape(c)
1020 1023 return res + '\Z(?ms)'
1021 1024
1022 1025
1023 1026 def parse_byte_string(size_str):
1024 1027 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1025 1028 if not match:
1026 1029 raise ValueError('Given size:%s is invalid, please make sure '
1027 1030 'to use format of <num>(MB|KB)' % size_str)
1028 1031
1029 1032 _parts = match.groups()
1030 1033 num, type_ = _parts
1031 1034 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1032 1035
1033 1036
1034 1037 class CachedProperty(object):
1035 1038 """
1036 1039 Lazy Attributes. With option to invalidate the cache by running a method
1037 1040
1038 1041 class Foo():
1039 1042
1040 1043 @CachedProperty
1041 1044 def heavy_func():
1042 1045 return 'super-calculation'
1043 1046
1044 1047 foo = Foo()
1045 1048 foo.heavy_func() # first computions
1046 1049 foo.heavy_func() # fetch from cache
1047 1050 foo._invalidate_prop_cache('heavy_func')
1048 1051 # at this point calling foo.heavy_func() will be re-computed
1049 1052 """
1050 1053
1051 1054 def __init__(self, func, func_name=None):
1052 1055
1053 1056 if func_name is None:
1054 1057 func_name = func.__name__
1055 1058 self.data = (func, func_name)
1056 1059 update_wrapper(self, func)
1057 1060
1058 1061 def __get__(self, inst, class_):
1059 1062 if inst is None:
1060 1063 return self
1061 1064
1062 1065 func, func_name = self.data
1063 1066 value = func(inst)
1064 1067 inst.__dict__[func_name] = value
1065 1068 if '_invalidate_prop_cache' not in inst.__dict__:
1066 1069 inst.__dict__['_invalidate_prop_cache'] = partial(
1067 1070 self._invalidate_prop_cache, inst)
1068 1071 return value
1069 1072
1070 1073 def _invalidate_prop_cache(self, inst, name):
1071 1074 inst.__dict__.pop(name, None)
General Comments 0
You need to be logged in to leave comments. Login now