~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Michael Hudson
  • Date: 2009-03-31 15:23:54 UTC
  • Revision ID: michael.hudson@canonical.com-20090331152354-49pk82qw3xup0mad
display nick of merged revisions sensibly

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#
 
2
# Copyright (C) 2008  Canonical Ltd.
 
3
#                     (Authored by Martin Albisetti <argentina@gmail.com)
2
4
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
3
5
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
4
6
#
22
24
except ImportError:
23
25
    from elementtree import ElementTree as ET
24
26
 
 
27
from simpletal.simpleTALUtils import HTMLStructureCleaner
 
28
 
25
29
import base64
26
30
import cgi
27
31
import datetime
28
32
import logging
29
33
import re
30
 
import sha
31
34
import struct
32
 
import sys
33
35
import threading
34
36
import time
35
 
import traceback
36
 
 
 
37
import sys
 
38
import os
 
39
import subprocess
37
40
 
38
41
log = logging.getLogger("loggerhead.controllers")
39
42
 
 
43
 
40
44
def fix_year(year):
41
45
    if year < 70:
42
46
        year += 2000
55
59
# displaydate and approximatedate return an elementtree <span> Element
56
60
# with the full date in a tooltip.
57
61
 
 
62
 
58
63
def date_day(value):
59
64
    return value.strftime('%Y-%m-%d')
60
65
 
61
66
 
62
67
def date_time(value):
63
 
    return value.strftime('%Y-%m-%d %T')
 
68
    if value is not None:
 
69
        return value.strftime('%Y-%m-%d %T')
 
70
    else:
 
71
        return 'N/A'
64
72
 
65
73
 
66
74
def _displaydate(date):
125
133
    """
126
134
    Convert a dict into an object with attributes.
127
135
    """
 
136
 
128
137
    def __init__(self, _dict=None, **kw):
129
138
        if _dict is not None:
130
139
            for key, value in _dict.iteritems():
135
144
    def __repr__(self):
136
145
        out = '{ '
137
146
        for key, value in self.__dict__.iteritems():
138
 
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
 
147
            if key.startswith('_') or (getattr(self.__dict__[key],
 
148
                                       '__call__', None) is not None):
139
149
                continue
140
150
            out += '%r => %r, ' % (key, value)
141
151
        out += '}'
142
152
        return out
143
153
 
144
154
 
145
 
def clean_revid(revid):
146
 
    if revid == 'missing':
147
 
        return revid
148
 
    return sha.new(revid).hexdigest()
149
 
 
150
 
 
151
 
def obfuscate(text):
152
 
    return ''.join([ '&#%d;' % ord(c) for c in text ])
153
 
 
154
 
 
155
155
def trunc(text, limit=10):
156
156
    if len(text) <= limit:
157
157
        return text
158
158
    return text[:limit] + '...'
159
159
 
160
160
 
161
 
def to_utf8(s):
162
 
    if isinstance(s, unicode):
163
 
        return s.encode('utf-8')
164
 
    return s
165
 
 
166
 
 
167
161
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
168
162
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
169
163
 
 
164
 
170
165
def hide_email(email):
171
166
    """
172
167
    try to obsure any email address in a bazaar committer's name.
186
181
        return '%s at %s' % (username, domains[-2])
187
182
    return '%s at %s' % (username, domains[0])
188
183
 
189
 
 
190
 
def triple_factors(min_value=1):
191
 
    factors = (1, 3)
192
 
    index = 0
193
 
    n = 1
194
 
    while True:
195
 
        if n >= min_value:
196
 
            yield n * factors[index]
197
 
        index += 1
198
 
        if index >= len(factors):
199
 
            index = 0
200
 
            n *= 10
201
 
 
202
 
 
203
 
def scan_range(pos, max, pagesize=1):
204
 
    """
205
 
    given a position in a maximum range, return a list of negative and positive
206
 
    jump factors for an hgweb-style triple-factor geometric scan.
207
 
 
208
 
    for example, with pos=20 and max=500, the range would be:
209
 
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
210
 
 
211
 
    i admit this is a very strange way of jumping through revisions.  i didn't
212
 
    invent it. :)
213
 
    """
214
 
    out = []
215
 
    for n in triple_factors(pagesize + 1):
216
 
        if n > max:
217
 
            return out
218
 
        if pos + n < max:
219
 
            out.append(n)
220
 
        if pos - n >= 0:
221
 
            out.insert(0, -n)
222
 
 
 
184
def hide_emails(emails):
 
185
    """
 
186
    try to obscure any email address in a list of bazaar committers' names.
 
187
    """
 
188
    result = []
 
189
    for email in emails:
 
190
        result.append(hide_email(email))
 
191
    return result
223
192
 
224
193
# only do this if unicode turns out to be a problem
225
194
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
226
195
 
227
196
# FIXME: get rid of this method; use fixed_width() and avoid XML().
 
197
 
 
198
 
228
199
def html_clean(s):
229
200
    """
230
201
    clean up a string for html display.  expand any tabs, encode any html
236
207
    return s
237
208
 
238
209
 
239
 
 
240
210
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
241
211
 
 
212
 
 
213
def fill_div(s):
 
214
    """
 
215
    CSS is stupid. In some cases we need to replace an empty value with
 
216
    a non breaking space (&nbsp;). There has to be a better way of doing this.
 
217
 
 
218
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
 
219
    """
 
220
 
 
221
 
 
222
    if s is None:
 
223
        return '&nbsp;'
 
224
    elif isinstance(s, int):
 
225
        return s
 
226
    elif not s.strip():
 
227
        return '&nbsp;'
 
228
    else:
 
229
        try:
 
230
            s = s.decode('utf-8')
 
231
        except UnicodeDecodeError:
 
232
            s = s.decode('iso-8859-15')
 
233
        return s
 
234
 
 
235
HSC = HTMLStructureCleaner()
 
236
 
242
237
def fixed_width(s):
243
238
    """
244
239
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
254
249
            s = s.decode('utf-8')
255
250
        except UnicodeDecodeError:
256
251
            s = s.decode('iso-8859-15')
257
 
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
 
252
 
 
253
    s = s.expandtabs().replace(' ', NONBREAKING_SPACE)
 
254
 
 
255
    return HSC.clean(s).replace('\n', '<br/>')
258
256
 
259
257
 
260
258
def fake_permissions(kind, executable):
266
264
    return '-rw-r--r--'
267
265
 
268
266
 
269
 
def if_present(format, value):
270
 
    """
271
 
    format a value using a format string, if the value exists and is not None.
272
 
    """
273
 
    if value is None:
274
 
        return ''
275
 
    return format % value
276
 
 
277
 
 
278
267
def b64(s):
279
268
    s = base64.encodestring(s).replace('\n', '')
280
269
    while (len(s) > 0) and (s[-1] == '='):
302
291
P95_MEG = int(0.9 * MEG)
303
292
P95_GIG = int(0.9 * GIG)
304
293
 
 
294
 
305
295
def human_size(size, min_divisor=0):
306
296
    size = int(size)
307
297
    if (size == 0) and (min_divisor == 0):
326
316
 
327
317
    out = str(base)
328
318
    if (base < 100) and (dot != 0):
329
 
        out += '.%d' % (dot,)
 
319
        out += '.%d' % (dot)
330
320
    if divisor == KILO:
331
321
        out += 'K'
332
322
    elif divisor == MEG:
347
337
        navigation.position = 0
348
338
    navigation.count = len(navigation.revid_list)
349
339
    navigation.page_position = navigation.position // navigation.pagesize + 1
350
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
 
340
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
 
341
 - 1)) // navigation.pagesize
351
342
 
352
343
    def get_offset(offset):
353
 
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
 
344
        if (navigation.position + offset < 0) or (
 
345
           navigation.position + offset > navigation.count - 1):
354
346
            return None
355
347
        return navigation.revid_list[navigation.position + offset]
356
348
 
 
349
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
357
350
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
358
351
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
359
 
    prev_page_revno = navigation.branch.history.get_revno(
 
352
    prev_page_revno = navigation.history.get_revno(
360
353
            navigation.prev_page_revid)
361
 
    next_page_revno = navigation.branch.history.get_revno(
 
354
    next_page_revno = navigation.history.get_revno(
362
355
            navigation.next_page_revid)
363
 
    start_revno = navigation.branch._history.get_revno(navigation.start_revid)
 
356
    start_revno = navigation.history.get_revno(navigation.start_revid)
364
357
 
365
 
    params = { 'filter_file_id': navigation.filter_file_id }
 
358
    params = {'filter_file_id': navigation.filter_file_id}
366
359
    if getattr(navigation, 'query', None) is not None:
367
360
        params['q'] = navigation.query
368
361
 
377
370
            [navigation.scan_url, next_page_revno], **params)
378
371
 
379
372
 
380
 
def log_exception(log):
381
 
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
382
 
        log.debug(line)
 
373
def directory_breadcrumbs(path, is_root, view):
 
374
    """
 
375
    Generate breadcrumb information from the directory path given
 
376
 
 
377
    The path given should be a path up to any branch that is currently being
 
378
    served
 
379
 
 
380
    Arguments:
 
381
    path -- The path to convert into breadcrumbs
 
382
    is_root -- Whether or not loggerhead is serving a branch at its root
 
383
    view -- The type of view we are showing (files, changes etc)
 
384
    """
 
385
    # Is our root directory itself a branch?
 
386
    if is_root:
 
387
        if view == 'directory':
 
388
            directory = 'files'
 
389
        breadcrumbs = [{
 
390
            'dir_name': path,
 
391
            'path': '',
 
392
            'suffix': view,
 
393
        }]
 
394
    else:
 
395
        # Create breadcrumb trail for the path leading up to the branch
 
396
        breadcrumbs = [{
 
397
            'dir_name': "(root)",
 
398
            'path': '',
 
399
            'suffix': '',
 
400
        }]
 
401
        if path != '/':
 
402
            dir_parts = path.strip('/').split('/')
 
403
            for index, dir_name in enumerate(dir_parts):
 
404
                breadcrumbs.append({
 
405
                    'dir_name': dir_name,
 
406
                    'path': '/'.join(dir_parts[:index + 1]),
 
407
                    'suffix': '',
 
408
                })
 
409
            # If we are not in the directory view, the last crumb is a branch,
 
410
            # so we need to specify a view
 
411
            if view != 'directory':
 
412
                breadcrumbs[-1]['suffix'] = '/' + view
 
413
    return breadcrumbs
 
414
 
 
415
 
 
416
def branch_breadcrumbs(path, inv, view):
 
417
    """
 
418
    Generate breadcrumb information from the branch path given
 
419
 
 
420
    The path given should be a path that exists within a branch
 
421
 
 
422
    Arguments:
 
423
    path -- The path to convert into breadcrumbs
 
424
    inv -- Inventory to get file information from
 
425
    view -- The type of view we are showing (files, changes etc)
 
426
    """
 
427
    dir_parts = path.strip('/').split('/')
 
428
    inner_breadcrumbs = []
 
429
    for index, dir_name in enumerate(dir_parts):
 
430
        inner_breadcrumbs.append({
 
431
            'dir_name': dir_name,
 
432
            'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
 
433
            'suffix': '/' + view,
 
434
        })
 
435
    return inner_breadcrumbs
383
436
 
384
437
 
385
438
def decorator(unbound):
 
439
 
386
440
    def new_decorator(f):
387
441
        g = unbound(f)
388
442
        g.__name__ = f.__name__
395
449
    return new_decorator
396
450
 
397
451
 
398
 
# common threading-lock decorator
399
 
def with_lock(lockname, debug_name=None):
400
 
    if debug_name is None:
401
 
        debug_name = lockname
402
 
    @decorator
403
 
    def _decorator(unbound):
404
 
        def locked(self, *args, **kw):
405
 
            getattr(self, lockname).acquire()
406
 
            try:
407
 
                return unbound(self, *args, **kw)
408
 
            finally:
409
 
                getattr(self, lockname).release()
410
 
        return locked
411
 
    return _decorator
412
 
 
413
 
 
414
 
@decorator
415
 
def strip_whitespace(f):
416
 
    def _f(*a, **kw):
417
 
        out = f(*a, **kw)
418
 
        orig_len = len(out)
419
 
        out = re.sub(r'\n\s+', '\n', out)
420
 
        out = re.sub(r'[ \t]+', ' ', out)
421
 
        out = re.sub(r'\s+\n', '\n', out)
422
 
        new_len = len(out)
423
 
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
424
 
                  human_size(orig_len - new_len),
425
 
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
426
 
        return out
427
 
    return _f
428
 
 
429
452
 
430
453
@decorator
431
454
def lsprof(f):
 
455
 
432
456
    def _f(*a, **kw):
433
457
        from loggerhead.lsprof import profile
434
458
        import cPickle
435
459
        z = time.time()
436
460
        ret, stats = profile(f, *a, **kw)
437
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
 
461
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
 
462
            int((time.time() - z) * 1000)))
438
463
        stats.sort()
439
464
        stats.freeze()
440
465
        now = time.time()
441
466
        msec = int(now * 1000) % 1000
442
 
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
 
467
        timestr = time.strftime('%Y%m%d%H%M%S',
 
468
                                time.localtime(now)) + ('%03d' % msec)
443
469
        filename = f.__name__ + '-' + timestr + '.lsprof'
444
470
        cPickle.dump(stats, open(filename, 'w'), 2)
445
471
        return ret
501
527
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
502
528
    map.update(overrides)
503
529
    return map
 
530
 
 
531
 
 
532
class Reloader(object):
 
533
    """
 
534
    This class wraps all paste.reloader logic. All methods are @classmethod.
 
535
    """
 
536
 
 
537
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
 
538
 
 
539
    @classmethod
 
540
    def _turn_sigterm_into_systemexit(self):
 
541
        """
 
542
        Attempts to turn a SIGTERM exception into a SystemExit exception.
 
543
        """
 
544
        try:
 
545
            import signal
 
546
        except ImportError:
 
547
            return
 
548
 
 
549
        def handle_term(signo, frame):
 
550
            raise SystemExit
 
551
        signal.signal(signal.SIGTERM, handle_term)
 
552
 
 
553
    @classmethod
 
554
    def is_installed(self):
 
555
        return os.environ.get(self._reloader_environ_key)
 
556
 
 
557
    @classmethod
 
558
    def install(self):
 
559
        from paste import reloader
 
560
        reloader.install(int(1))
 
561
 
 
562
    @classmethod
 
563
    def restart_with_reloader(self):
 
564
        """Based on restart_with_monitor from paste.script.serve."""
 
565
        print 'Starting subprocess with file monitor'
 
566
        while 1:
 
567
            args = [sys.executable] + sys.argv
 
568
            new_environ = os.environ.copy()
 
569
            new_environ[self._reloader_environ_key] = 'true'
 
570
            proc = None
 
571
            try:
 
572
                try:
 
573
                    self._turn_sigterm_into_systemexit()
 
574
                    proc = subprocess.Popen(args, env=new_environ)
 
575
                    exit_code = proc.wait()
 
576
                    proc = None
 
577
                except KeyboardInterrupt:
 
578
                    print '^C caught in monitor process'
 
579
                    return 1
 
580
            finally:
 
581
                if (proc is not None
 
582
                    and hasattr(os, 'kill')):
 
583
                    import signal
 
584
                    try:
 
585
                        os.kill(proc.pid, signal.SIGTERM)
 
586
                    except (OSError, IOError):
 
587
                        pass
 
588
 
 
589
            # Reloader always exits with code 3; but if we are
 
590
            # a monitor, any exit code will restart
 
591
            if exit_code != 3:
 
592
                return exit_code
 
593
            print '-'*20, 'Restarting', '-'*20