~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Robey Pointer
  • Date: 2006-12-14 03:00:10 UTC
  • Revision ID: robey@lag.net-20061214030010-amia4mec3ydygjgk
add a timed event to fill in the revision cache, so that after running for
a little while, most page loads should be fast.  fix up some of the mechanism
around the history cache, so that it notices when the branch has been
updated, and reloads (and recomputes) the graph cache.

add branch nicks to the merged-in, merged-from listings.

add next/prev navbar to the bottom of the revision page.

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)
4
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
5
 
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
6
3
#
7
4
# This program is free software; you can redistribute it and/or modify
8
5
# it under the terms of the GNU General Public License as published by
19
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
17
#
21
18
 
22
 
import base64
23
19
import cgi
24
20
import datetime
25
21
import logging
26
22
import re
27
 
import struct
 
23
import sha
28
24
import threading
29
 
import time
30
 
import sys
31
 
import os
32
 
import subprocess
33
 
 
34
 
try:
35
 
    from xml.etree import ElementTree as ET
36
 
except ImportError:
37
 
    from elementtree import ElementTree as ET
38
 
 
39
 
from bzrlib import urlutils
40
 
 
41
 
from simpletal.simpleTALUtils import HTMLStructureCleaner
 
25
 
 
26
import turbogears
 
27
 
42
28
 
43
29
log = logging.getLogger("loggerhead.controllers")
44
30
 
45
31
 
46
 
def fix_year(year):
47
 
    if year < 70:
48
 
        year += 2000
49
 
    if year < 100:
50
 
        year += 1900
51
 
    return year
52
 
 
53
 
# Display of times.
54
 
 
55
 
# date_day -- just the day
56
 
# date_time -- full date with time
57
 
#
58
 
# displaydate -- for use in sentences
59
 
# approximatedate -- for use in tables
60
 
#
61
 
# displaydate and approximatedate return an elementtree <span> Element
62
 
# with the full date in a tooltip.
63
 
 
64
 
 
65
 
def date_day(value):
66
 
    return value.strftime('%Y-%m-%d')
67
 
 
68
 
 
69
 
def date_time(value):
70
 
    if value is not None:
71
 
        return value.strftime('%Y-%m-%d %H:%M:%S')
72
 
    else:
73
 
        return 'N/A'
74
 
 
75
 
 
76
 
def _displaydate(date):
77
 
    delta = abs(datetime.datetime.now() - date)
78
 
    if delta > datetime.timedelta(1, 0, 0):
79
 
        # far in the past or future, display the date
80
 
        return 'on ' + date_day(date)
81
 
    return _approximatedate(date)
82
 
 
83
 
 
84
 
def _approximatedate(date):
85
 
    delta = datetime.datetime.now() - date
86
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
87
 
        # far in the past or future, display the date
88
 
        return date_day(date)
89
 
    future = delta < datetime.timedelta(0, 0, 0)
90
 
    delta = abs(delta)
91
 
    days = delta.days
92
 
    hours = delta.seconds / 3600
93
 
    minutes = (delta.seconds - (3600*hours)) / 60
94
 
    seconds = delta.seconds % 60
95
 
    result = ''
96
 
    if future:
97
 
        result += 'in '
98
 
    if days != 0:
99
 
        amount = days
100
 
        unit = 'day'
101
 
    elif hours != 0:
102
 
        amount = hours
103
 
        unit = 'hour'
104
 
    elif minutes != 0:
105
 
        amount = minutes
106
 
        unit = 'minute'
107
 
    else:
108
 
        amount = seconds
109
 
        unit = 'second'
110
 
    if amount != 1:
111
 
        unit += 's'
112
 
    result += '%s %s' % (amount, unit)
113
 
    if not future:
114
 
        result += ' ago'
115
 
        return result
116
 
 
117
 
 
118
 
def _wrap_with_date_time_title(date, formatted_date):
119
 
    elem = ET.Element("span")
120
 
    elem.text = formatted_date
121
 
    elem.set("title", date_time(date))
122
 
    return elem
123
 
 
124
 
 
125
 
def approximatedate(date):
126
 
    #FIXME: Returns an object instead of a string
127
 
    return _wrap_with_date_time_title(date, _approximatedate(date))
128
 
 
129
 
 
130
 
def displaydate(date):
131
 
    return _wrap_with_date_time_title(date, _displaydate(date))
132
 
 
133
 
 
134
 
class Container(object):
 
32
def timespan(delta):
 
33
    if delta.days > 730:
 
34
        # good grief!
 
35
        return '%d years' % (int(delta.days // 365.25),)
 
36
    if delta.days >= 3:
 
37
        return '%d days' % delta.days
 
38
    seg = []
 
39
    if delta.days > 0:
 
40
        if delta.days == 1:
 
41
            seg.append('1 day')
 
42
        else:
 
43
            seg.append('%d days' % delta.days)
 
44
    hrs = delta.seconds // 3600
 
45
    mins = (delta.seconds % 3600) // 60
 
46
    if hrs > 0:
 
47
        if hrs == 1:
 
48
            seg.append('1 hour')
 
49
        else:
 
50
            seg.append('%d hours' % hrs)
 
51
    if delta.days == 0:
 
52
        if mins > 0:
 
53
            if mins == 1:
 
54
                seg.append('1 minute')
 
55
            else:
 
56
                seg.append('%d minutes' % mins)
 
57
        elif hrs == 0:
 
58
            seg.append('less than a minute')
 
59
    return ', '.join(seg)
 
60
 
 
61
 
 
62
def ago(timestamp):
 
63
    now = datetime.datetime.now()
 
64
    return timespan(now - timestamp) + ' ago'
 
65
    
 
66
 
 
67
class Container (object):
135
68
    """
136
69
    Convert a dict into an object with attributes.
137
70
    """
138
 
 
139
71
    def __init__(self, _dict=None, **kw):
140
 
        self._properties = {}
141
72
        if _dict is not None:
142
73
            for key, value in _dict.iteritems():
143
74
                setattr(self, key, value)
144
75
        for key, value in kw.iteritems():
145
76
            setattr(self, key, value)
146
 
 
 
77
    
147
78
    def __repr__(self):
148
79
        out = '{ '
149
80
        for key, value in self.__dict__.iteritems():
150
 
            if key.startswith('_') or (getattr(self.__dict__[key],
151
 
                                       '__call__', None) is not None):
 
81
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
152
82
                continue
153
83
            out += '%r => %r, ' % (key, value)
154
84
        out += '}'
155
85
        return out
156
86
 
157
 
    def __getattr__(self, attr):
158
 
        """Used for handling things that aren't already available."""
159
 
        if attr.startswith('_') or attr not in self._properties:
160
 
            raise AttributeError('No attribute: %s' % (attr,))
161
 
        val = self._properties[attr](self, attr)
162
 
        setattr(self, attr, val)
163
 
        return val
164
 
 
165
 
    def _set_property(self, attr, prop_func):
166
 
        """Set a function that will be called when an attribute is desired.
167
 
 
168
 
        We will cache the return value, so the function call should be
169
 
        idempotent. We will pass 'self' and the 'attr' name when triggered.
170
 
        """
171
 
        if attr.startswith('_'):
172
 
            raise ValueError("Cannot create properties that start with _")
173
 
        self._properties[attr] = prop_func
174
 
 
175
 
 
176
 
def trunc(text, limit=10):
177
 
    if len(text) <= limit:
178
 
        return text
179
 
    return text[:limit] + '...'
 
87
 
 
88
def clean_revid(revid):
 
89
    if revid == 'missing':
 
90
        return revid
 
91
    return sha.new(revid).hexdigest()
 
92
 
 
93
 
 
94
def obfuscate(text):
 
95
    return ''.join([ '&#%d;' % ord(c) for c in text ])
180
96
 
181
97
 
182
98
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
183
99
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
184
100
 
185
 
 
186
101
def hide_email(email):
187
102
    """
188
103
    try to obsure any email address in a bazaar committer's name.
202
117
        return '%s at %s' % (username, domains[-2])
203
118
    return '%s at %s' % (username, domains[0])
204
119
 
205
 
def hide_emails(emails):
206
 
    """
207
 
    try to obscure any email address in a list of bazaar committers' names.
208
 
    """
209
 
    result = []
210
 
    for email in emails:
211
 
        result.append(hide_email(email))
212
 
    return result
213
 
 
214
 
# only do this if unicode turns out to be a problem
215
 
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
216
 
 
217
 
# FIXME: get rid of this method; use fixed_width() and avoid XML().
 
120
    
 
121
def triple_factors():
 
122
    factors = (1, 3)
 
123
    index = 0
 
124
    n = 1
 
125
    while True:
 
126
        if n >= 10:
 
127
            yield n * factors[index]
 
128
        index += 1
 
129
        if index >= len(factors):
 
130
            index = 0
 
131
            n *= 10
 
132
 
 
133
 
 
134
def scan_range(pos, max):
 
135
    """
 
136
    given a position in a maximum range, return a list of negative and positive
 
137
    jump factors for an hgweb-style triple-factor geometric scan.
 
138
    
 
139
    for example, with pos=20 and max=500, the range would be:
 
140
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
 
141
    
 
142
    i admit this is a very strange way of jumping through revisions.  i didn't
 
143
    invent it. :)
 
144
    """
 
145
    out = []
 
146
    for n in triple_factors():
 
147
        if n > max:
 
148
            return out
 
149
        if pos + n < max:
 
150
            out.append(n)
 
151
        if pos - n >= 0:
 
152
            out.insert(0, -n)
218
153
 
219
154
 
220
155
def html_clean(s):
223
158
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
224
159
    in displaying monospace text.
225
160
    """
226
 
    s = cgi.escape(s.expandtabs())
227
 
    s = s.replace(' ', '&nbsp;')
 
161
    s = cgi.escape(s.expandtabs()).replace(' ', '&nbsp;')
228
162
    return s
229
163
 
230
164
 
231
 
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
232
 
 
233
 
 
234
 
def fill_div(s):
235
 
    """
236
 
    CSS is stupid. In some cases we need to replace an empty value with
237
 
    a non breaking space (&nbsp;). There has to be a better way of doing this.
238
 
 
239
 
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
240
 
    """
241
 
    if s is None:
242
 
        return '&nbsp;'
243
 
    elif isinstance(s, int):
244
 
        return s
245
 
    elif not s.strip():
246
 
        return '&nbsp;'
247
 
    else:
248
 
        try:
249
 
            s = s.decode('utf-8')
250
 
        except UnicodeDecodeError:
251
 
            s = s.decode('iso-8859-15')
252
 
        return s
253
 
 
254
 
HSC = HTMLStructureCleaner()
255
 
 
256
 
def fixed_width(s):
257
 
    """
258
 
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
259
 
    chop up the string.
260
 
    """
261
 
    if not isinstance(s, unicode):
262
 
        # this kinda sucks.  file contents are just binary data, and no
263
 
        # encoding metadata is stored, so we need to guess.  this is probably
264
 
        # okay for most code, but for people using things like KOI-8, this
265
 
        # will display gibberish.  we have no way of detecting the correct
266
 
        # encoding to use.
267
 
        try:
268
 
            s = s.decode('utf-8')
269
 
        except UnicodeDecodeError:
270
 
            s = s.decode('iso-8859-15')
271
 
 
272
 
    s = cgi.escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
273
 
 
274
 
    return HSC.clean(s).replace('\n', '<br/>')
275
 
 
276
 
 
277
165
def fake_permissions(kind, executable):
278
166
    # fake up unix-style permissions given only a "kind" and executable bit
279
167
    if kind == 'directory':
283
171
    return '-rw-r--r--'
284
172
 
285
173
 
286
 
def b64(s):
287
 
    s = base64.encodestring(s).replace('\n', '')
288
 
    while (len(s) > 0) and (s[-1] == '='):
289
 
        s = s[:-1]
290
 
    return s
291
 
 
292
 
 
293
 
def uniq(uniqs, s):
294
 
    """
295
 
    turn a potentially long string into a unique smaller string.
296
 
    """
297
 
    if s in uniqs:
298
 
        return uniqs[s]
299
 
    uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
300
 
    x = struct.pack('>I', next)
301
 
    while (len(x) > 1) and (x[0] == '\x00'):
302
 
        x = x[1:]
303
 
    uniqs[s] = b64(x)
304
 
    return uniqs[s]
305
 
 
306
 
 
307
 
KILO = 1024
308
 
MEG = 1024 * KILO
309
 
GIG = 1024 * MEG
310
 
P95_MEG = int(0.9 * MEG)
311
 
P95_GIG = int(0.9 * GIG)
312
 
 
313
 
 
314
 
def human_size(size, min_divisor=0):
315
 
    size = int(size)
316
 
    if (size == 0) and (min_divisor == 0):
317
 
        return '0'
318
 
    if (size < 512) and (min_divisor == 0):
319
 
        return str(size)
320
 
 
321
 
    if (size >= P95_GIG) or (min_divisor >= GIG):
322
 
        divisor = GIG
323
 
    elif (size >= P95_MEG) or (min_divisor >= MEG):
324
 
        divisor = MEG
325
 
    else:
326
 
        divisor = KILO
327
 
 
328
 
    dot = size % divisor
329
 
    base = size - dot
330
 
    dot = dot * 10 // divisor
331
 
    base //= divisor
332
 
    if dot >= 10:
333
 
        base += 1
334
 
        dot -= 10
335
 
 
336
 
    out = str(base)
337
 
    if (base < 100) and (dot != 0):
338
 
        out += '.%d' % (dot,)
339
 
    if divisor == KILO:
340
 
        out += 'K'
341
 
    elif divisor == MEG:
342
 
        out += 'M'
343
 
    elif divisor == GIG:
344
 
        out += 'G'
345
 
    return out
346
 
 
347
 
 
348
 
def local_path_from_url(url):
349
 
    """Convert Bazaar URL to local path, ignoring readonly+ prefix"""
350
 
    readonly_prefix = 'readonly+'
351
 
    if url.startswith(readonly_prefix):
352
 
        url = url[len(readonly_prefix):]
353
 
    return urlutils.local_path_from_url(url)
354
 
 
355
 
 
356
 
def fill_in_navigation(navigation):
357
 
    """
358
 
    given a navigation block (used by the template for the page header), fill
359
 
    in useful calculated values.
360
 
    """
361
 
    if navigation.revid in navigation.revid_list: # XXX is this always true?
362
 
        navigation.position = navigation.revid_list.index(navigation.revid)
363
 
    else:
364
 
        navigation.position = 0
365
 
    navigation.count = len(navigation.revid_list)
366
 
    navigation.page_position = navigation.position // navigation.pagesize + 1
367
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
368
 
 - 1)) // navigation.pagesize
369
 
 
370
 
    def get_offset(offset):
371
 
        if (navigation.position + offset < 0) or (
372
 
           navigation.position + offset > navigation.count - 1):
373
 
            return None
374
 
        return navigation.revid_list[navigation.position + offset]
375
 
 
376
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
377
 
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
378
 
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
379
 
    prev_page_revno = navigation.history.get_revno(
380
 
            navigation.prev_page_revid)
381
 
    next_page_revno = navigation.history.get_revno(
382
 
            navigation.next_page_revid)
383
 
    start_revno = navigation.history.get_revno(navigation.start_revid)
384
 
 
385
 
    params = {'filter_file_id': navigation.filter_file_id}
386
 
    if getattr(navigation, 'query', None) is not None:
387
 
        params['q'] = navigation.query
388
 
 
389
 
    if getattr(navigation, 'start_revid', None) is not None:
390
 
        params['start_revid'] = start_revno
391
 
 
392
 
    if navigation.prev_page_revid:
393
 
        navigation.prev_page_url = navigation.branch.context_url(
394
 
            [navigation.scan_url, prev_page_revno], **params)
395
 
    if navigation.next_page_revid:
396
 
        navigation.next_page_url = navigation.branch.context_url(
397
 
            [navigation.scan_url, next_page_revno], **params)
398
 
 
399
 
 
400
 
def directory_breadcrumbs(path, is_root, view):
401
 
    """
402
 
    Generate breadcrumb information from the directory path given
403
 
 
404
 
    The path given should be a path up to any branch that is currently being
405
 
    served
406
 
 
407
 
    Arguments:
408
 
    path -- The path to convert into breadcrumbs
409
 
    is_root -- Whether or not loggerhead is serving a branch at its root
410
 
    view -- The type of view we are showing (files, changes etc)
411
 
    """
412
 
    # Is our root directory itself a branch?
413
 
    if is_root:
414
 
        breadcrumbs = [{
415
 
            'dir_name': path,
416
 
            'path': '',
417
 
            'suffix': view,
418
 
        }]
419
 
    else:
420
 
        # Create breadcrumb trail for the path leading up to the branch
421
 
        breadcrumbs = [{
422
 
            'dir_name': "(root)",
423
 
            'path': '',
424
 
            'suffix': '',
425
 
        }]
426
 
        if path != '/':
427
 
            dir_parts = path.strip('/').split('/')
428
 
            for index, dir_name in enumerate(dir_parts):
429
 
                breadcrumbs.append({
430
 
                    'dir_name': dir_name,
431
 
                    'path': '/'.join(dir_parts[:index + 1]),
432
 
                    'suffix': '',
433
 
                })
434
 
            # If we are not in the directory view, the last crumb is a branch,
435
 
            # so we need to specify a view
436
 
            if view != 'directory':
437
 
                breadcrumbs[-1]['suffix'] = '/' + view
438
 
    return breadcrumbs
439
 
 
440
 
 
441
 
def branch_breadcrumbs(path, inv, view):
442
 
    """
443
 
    Generate breadcrumb information from the branch path given
444
 
 
445
 
    The path given should be a path that exists within a branch
446
 
 
447
 
    Arguments:
448
 
    path -- The path to convert into breadcrumbs
449
 
    inv -- Inventory to get file information from
450
 
    view -- The type of view we are showing (files, changes etc)
451
 
    """
452
 
    dir_parts = path.strip('/').split('/')
453
 
    inner_breadcrumbs = []
454
 
    for index, dir_name in enumerate(dir_parts):
455
 
        inner_breadcrumbs.append({
456
 
            'dir_name': dir_name,
457
 
            'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
458
 
            'suffix': '/' + view,
459
 
        })
460
 
    return inner_breadcrumbs
461
 
 
462
 
 
463
 
def decorator(unbound):
464
 
 
465
 
    def new_decorator(f):
466
 
        g = unbound(f)
467
 
        g.__name__ = f.__name__
468
 
        g.__doc__ = f.__doc__
469
 
        g.__dict__.update(f.__dict__)
470
 
        return g
471
 
    new_decorator.__name__ = unbound.__name__
472
 
    new_decorator.__doc__ = unbound.__doc__
473
 
    new_decorator.__dict__.update(unbound.__dict__)
474
 
    return new_decorator
475
 
 
476
 
 
477
 
 
478
 
@decorator
479
 
def lsprof(f):
480
 
 
481
 
    def _f(*a, **kw):
482
 
        from loggerhead.lsprof import profile
483
 
        import cPickle
484
 
        z = time.time()
485
 
        ret, stats = profile(f, *a, **kw)
486
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__,
487
 
            int((time.time() - z) * 1000)))
488
 
        stats.sort()
489
 
        stats.freeze()
490
 
        now = time.time()
491
 
        msec = int(now * 1000) % 1000
492
 
        timestr = time.strftime('%Y%m%d%H%M%S',
493
 
                                time.localtime(now)) + ('%03d' % (msec,))
494
 
        filename = f.__name__ + '-' + timestr + '.lsprof'
495
 
        cPickle.dump(stats, open(filename, 'w'), 2)
496
 
        return ret
497
 
    return _f
498
 
 
499
 
 
500
 
# just thinking out loud here...
501
 
#
502
 
# so, when browsing around, there are 5 pieces of context, most optional:
503
 
#     - current revid
504
 
#         current location along the navigation path (while browsing)
505
 
#     - starting revid (start_revid)
506
 
#         the current beginning of navigation (navigation continues back to
507
 
#         the original revision) -- this defines an 'alternate mainline'
508
 
#         when the user navigates into a branch.
509
 
#     - file_id
510
 
#         the file being looked at
511
 
#     - filter_file_id
512
 
#         if navigating the revisions that touched a file
513
 
#     - q (query)
514
 
#         if navigating the revisions that matched a search query
515
 
#     - remember
516
 
#         a previous revision to remember for future comparisons
517
 
#
518
 
# current revid is given on the url path.  the rest are optional components
519
 
# in the url params.
520
 
#
521
 
# other transient things can be set:
522
 
#     - compare_revid
523
 
#         to compare one revision to another, on /revision only
524
 
#     - sort
525
 
#         for re-ordering an existing page by different sort
526
 
 
527
 
t_context = threading.local()
528
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
529
 
          'compare_revid', 'sort')
530
 
 
531
 
 
532
 
def set_context(map):
533
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
534
 
 
535
 
 
536
 
def get_context(**overrides):
537
 
    """
538
 
    Soon to be deprecated.
539
 
 
540
 
 
541
 
    return a context map that may be overriden by specific values passed in,
542
 
    but only contains keys from the list of valid context keys.
543
 
 
544
 
    if 'clear' is set, only the 'remember' context value will be added, and
545
 
    all other context will be omitted.
546
 
    """
547
 
    map = dict()
548
 
    if overrides.get('clear', False):
549
 
        map['remember'] = t_context.map.get('remember', None)
550
 
    else:
551
 
        map.update(t_context.map)
552
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
553
 
    map.update(overrides)
554
 
    return map
555
 
 
556
 
 
557
 
class Reloader(object):
558
 
    """
559
 
    This class wraps all paste.reloader logic. All methods are @classmethod.
560
 
    """
561
 
 
562
 
    _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
563
 
 
564
 
    @classmethod
565
 
    def _turn_sigterm_into_systemexit(cls):
566
 
        """
567
 
        Attempts to turn a SIGTERM exception into a SystemExit exception.
568
 
        """
569
 
        try:
570
 
            import signal
571
 
        except ImportError:
572
 
            return
573
 
 
574
 
        def handle_term(signo, frame):
575
 
            raise SystemExit
576
 
        signal.signal(signal.SIGTERM, handle_term)
577
 
 
578
 
    @classmethod
579
 
    def is_installed(cls):
580
 
        return os.environ.get(cls._reloader_environ_key)
581
 
 
582
 
    @classmethod
583
 
    def install(cls):
584
 
        from paste import reloader
585
 
        reloader.install(int(1))
586
 
 
587
 
    @classmethod
588
 
    def restart_with_reloader(cls):
589
 
        """Based on restart_with_monitor from paste.script.serve."""
590
 
        print 'Starting subprocess with file monitor'
591
 
        while True:
592
 
            args = [sys.executable] + sys.argv
593
 
            new_environ = os.environ.copy()
594
 
            new_environ[cls._reloader_environ_key] = 'true'
595
 
            proc = None
596
 
            try:
597
 
                try:
598
 
                    cls._turn_sigterm_into_systemexit()
599
 
                    proc = subprocess.Popen(args, env=new_environ)
600
 
                    exit_code = proc.wait()
601
 
                    proc = None
602
 
                except KeyboardInterrupt:
603
 
                    print '^C caught in monitor process'
604
 
                    return 1
605
 
            finally:
606
 
                if (proc is not None
607
 
                    and getattr(os, 'kill', None) is not None):
608
 
                    import signal
609
 
                    try:
610
 
                        os.kill(proc.pid, signal.SIGTERM)
611
 
                    except (OSError, IOError):
612
 
                        pass
613
 
 
614
 
            # Reloader always exits with code 3; but if we are
615
 
            # a monitor, any exit code will restart
616
 
            if exit_code != 3:
617
 
                return exit_code
618
 
            print '-'*20, 'Restarting', '-'*20
619
 
 
620
 
 
621
 
def convert_file_errors(application):
622
 
    """WSGI wrapper to convert some file errors to Paste exceptions"""
623
 
    def new_application(environ, start_response):
624
 
        try:
625
 
            return application(environ, start_response)
626
 
        except (IOError, OSError), e:
627
 
            import errno
628
 
            from paste import httpexceptions
629
 
            if e.errno == errno.ENOENT:
630
 
                raise httpexceptions.HTTPNotFound()
631
 
            elif e.errno == errno.EACCES:
632
 
                raise httpexceptions.HTTPForbidden()
633
 
            else:
634
 
                raise
635
 
    return new_application
 
174
def if_present(format, value):
 
175
    """
 
176
    format a value using a format string, if the value exists and is not None.
 
177
    """
 
178
    if value is None:
 
179
        return ''
 
180
    return format % value
 
181
 
 
182
 
 
183
# global branch history & cache
 
184
 
 
185
_history = None
 
186
_history_lock = threading.Lock()
 
187
 
 
188
def get_history():
 
189
    global _history
 
190
    from loggerhead.history import History
 
191
    
 
192
    _history_lock.acquire()
 
193
    try:
 
194
        if (_history is None) or _history.out_of_date():
 
195
            log.debug('Reload branch history...')
 
196
            if _history is not None:
 
197
                _history.dont_use_cache()
 
198
            _history = History.from_folder(turbogears.config.get('loggerhead.folder'))
 
199
            _history.use_cache(turbogears.config.get('loggerhead.cachepath'))
 
200
        return _history
 
201
    finally:
 
202
        _history_lock.release()
 
203