~loggerhead-team/loggerhead/trunk-rich

« back to all changes in this revision

Viewing changes to loggerhead/util.py

  • Committer: Robey Pointer
  • Date: 2006-12-16 01:43:52 UTC
  • Revision ID: robey@lag.net-20061216014352-euf7pthd6ik80p6n
some suggestions from bazaar-dev

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