~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
 
try:
23
 
    from xml.etree import ElementTree as ET
24
 
except ImportError:
25
 
    from elementtree import ElementTree as ET
26
 
 
27
 
import base64
28
19
import cgi
29
20
import datetime
30
21
import logging
31
22
import re
32
 
import struct
 
23
import sha
33
24
import threading
34
 
import time
35
 
import types
 
25
 
 
26
import turbogears
 
27
 
36
28
 
37
29
log = logging.getLogger("loggerhead.controllers")
38
30
 
39
 
def fix_year(year):
40
 
    if year < 70:
41
 
        year += 2000
42
 
    if year < 100:
43
 
        year += 1900
44
 
    return year
45
 
 
46
 
# Display of times.
47
 
 
48
 
# date_day -- just the day
49
 
# date_time -- full date with time
50
 
#
51
 
# displaydate -- for use in sentences
52
 
# approximatedate -- for use in tables
53
 
#
54
 
# displaydate and approximatedate return an elementtree <span> Element
55
 
# with the full date in a tooltip.
56
 
 
57
 
def date_day(value):
58
 
    return value.strftime('%Y-%m-%d')
59
 
 
60
 
 
61
 
def date_time(value):
62
 
    return value.strftime('%Y-%m-%d %T')
63
 
 
64
 
 
65
 
def _displaydate(date):
66
 
    delta = abs(datetime.datetime.now() - date)
67
 
    if delta > datetime.timedelta(1, 0, 0):
68
 
        # far in the past or future, display the date
69
 
        return 'on ' + date_day(date)
70
 
    return _approximatedate(date)
71
 
 
72
 
 
73
 
def _approximatedate(date):
74
 
    delta = datetime.datetime.now() - date
75
 
    if abs(delta) > datetime.timedelta(1, 0, 0):
76
 
        # far in the past or future, display the date
77
 
        return date_day(date)
78
 
    future = delta < datetime.timedelta(0, 0, 0)
79
 
    delta = abs(delta)
80
 
    days = delta.days
81
 
    hours = delta.seconds / 3600
82
 
    minutes = (delta.seconds - (3600*hours)) / 60
83
 
    seconds = delta.seconds % 60
84
 
    result = ''
85
 
    if future:
86
 
        result += 'in '
87
 
    if days != 0:
88
 
        amount = days
89
 
        unit = 'day'
90
 
    elif hours != 0:
91
 
        amount = hours
92
 
        unit = 'hour'
93
 
    elif minutes != 0:
94
 
        amount = minutes
95
 
        unit = 'minute'
96
 
    else:
97
 
        amount = seconds
98
 
        unit = 'second'
99
 
    if amount != 1:
100
 
        unit += 's'
101
 
    result += '%s %s' % (amount, unit)
102
 
    if not future:
103
 
        result += ' ago'
104
 
        return result
105
 
 
106
 
 
107
 
def _wrap_with_date_time_title(date, formatted_date):
108
 
    elem = ET.Element("span")
109
 
    elem.text = formatted_date
110
 
    elem.set("title", date_time(date))
111
 
    return elem
112
 
 
113
 
 
114
 
def approximatedate(date):
115
 
    #FIXME: Returns an object instead of a string
116
 
    return _wrap_with_date_time_title(date, _approximatedate(date))
117
 
 
118
 
 
119
 
def displaydate(date):
120
 
    return _wrap_with_date_time_title(date, _displaydate(date))
121
 
 
 
31
 
 
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
    
122
66
 
123
67
class Container (object):
124
68
    """
130
74
                setattr(self, key, value)
131
75
        for key, value in kw.iteritems():
132
76
            setattr(self, key, value)
133
 
 
 
77
    
134
78
    def __repr__(self):
135
79
        out = '{ '
136
80
        for key, value in self.__dict__.iteritems():
141
85
        return out
142
86
 
143
87
 
144
 
def trunc(text, limit=10):
145
 
    if len(text) <= limit:
146
 
        return text
147
 
    return text[:limit] + '...'
 
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 ])
148
96
 
149
97
 
150
98
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
169
117
        return '%s at %s' % (username, domains[-2])
170
118
    return '%s at %s' % (username, domains[0])
171
119
 
172
 
 
173
 
# only do this if unicode turns out to be a problem
174
 
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
175
 
 
176
 
# 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)
 
153
 
 
154
 
177
155
def html_clean(s):
178
156
    """
179
157
    clean up a string for html display.  expand any tabs, encode any html
180
158
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
181
159
    in displaying monospace text.
182
160
    """
183
 
    s = cgi.escape(s.expandtabs())
184
 
    s = s.replace(' ', '&nbsp;')
 
161
    s = cgi.escape(s.expandtabs()).replace(' ', '&nbsp;')
185
162
    return s
186
163
 
187
 
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
188
 
 
189
 
def fill_div(s):
190
 
    """
191
 
    CSS is stupid. In some cases we need to replace an empty value with
192
 
    a non breaking space (&nbsp;). There has to be a better way of doing this.
193
 
 
194
 
    return: the same value recieved if not empty, and a '&nbsp;' if it is.
195
 
    """
196
 
    
197
 
 
198
 
    if s is None:
199
 
        return '&nbsp;'
200
 
    elif isinstance(s, int):
201
 
        return s
202
 
    elif not s.strip():
203
 
        return '&nbsp;'
204
 
    else:
205
 
        try:
206
 
            s = s.decode('utf-8')
207
 
        except UnicodeDecodeError:
208
 
            s = s.decode('iso-8859-15')
209
 
        return s
210
 
 
211
 
 
212
 
def fixed_width(s):
213
 
    """
214
 
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
215
 
    chop up the string.
216
 
    """
217
 
    if not isinstance(s, unicode):
218
 
        # this kinda sucks.  file contents are just binary data, and no
219
 
        # encoding metadata is stored, so we need to guess.  this is probably
220
 
        # okay for most code, but for people using things like KOI-8, this
221
 
        # will display gibberish.  we have no way of detecting the correct
222
 
        # encoding to use.
223
 
        try:
224
 
            s = s.decode('utf-8')
225
 
        except UnicodeDecodeError:
226
 
            s = s.decode('iso-8859-15')
227
 
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
228
 
 
229
164
 
230
165
def fake_permissions(kind, executable):
231
166
    # fake up unix-style permissions given only a "kind" and executable bit
236
171
    return '-rw-r--r--'
237
172
 
238
173
 
239
 
def b64(s):
240
 
    s = base64.encodestring(s).replace('\n', '')
241
 
    while (len(s) > 0) and (s[-1] == '='):
242
 
        s = s[:-1]
243
 
    return s
244
 
 
245
 
 
246
 
def uniq(uniqs, s):
247
 
    """
248
 
    turn a potentially long string into a unique smaller string.
249
 
    """
250
 
    if s in uniqs:
251
 
        return uniqs[s]
252
 
    uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
253
 
    x = struct.pack('>I', next)
254
 
    while (len(x) > 1) and (x[0] == '\x00'):
255
 
        x = x[1:]
256
 
    uniqs[s] = b64(x)
257
 
    return uniqs[s]
258
 
 
259
 
 
260
 
KILO = 1024
261
 
MEG = 1024 * KILO
262
 
GIG = 1024 * MEG
263
 
P95_MEG = int(0.9 * MEG)
264
 
P95_GIG = int(0.9 * GIG)
265
 
 
266
 
def human_size(size, min_divisor=0):
267
 
    size = int(size)
268
 
    if (size == 0) and (min_divisor == 0):
269
 
        return '0'
270
 
    if (size < 512) and (min_divisor == 0):
271
 
        return str(size)
272
 
 
273
 
    if (size >= P95_GIG) or (min_divisor >= GIG):
274
 
        divisor = GIG
275
 
    elif (size >= P95_MEG) or (min_divisor >= MEG):
276
 
        divisor = MEG
277
 
    else:
278
 
        divisor = KILO
279
 
 
280
 
    dot = size % divisor
281
 
    base = size - dot
282
 
    dot = dot * 10 // divisor
283
 
    base //= divisor
284
 
    if dot >= 10:
285
 
        base += 1
286
 
        dot -= 10
287
 
 
288
 
    out = str(base)
289
 
    if (base < 100) and (dot != 0):
290
 
        out += '.%d' % (dot,)
291
 
    if divisor == KILO:
292
 
        out += 'K'
293
 
    elif divisor == MEG:
294
 
        out += 'M'
295
 
    elif divisor == GIG:
296
 
        out += 'G'
297
 
    return out
298
 
 
299
 
 
300
 
def fill_in_navigation(navigation):
301
 
    """
302
 
    given a navigation block (used by the template for the page header), fill
303
 
    in useful calculated values.
304
 
    """
305
 
    if navigation.revid in navigation.revid_list: # XXX is this always true?
306
 
        navigation.position = navigation.revid_list.index(navigation.revid)
307
 
    else:
308
 
        navigation.position = 0
309
 
    navigation.count = len(navigation.revid_list)
310
 
    navigation.page_position = navigation.position // navigation.pagesize + 1
311
 
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
312
 
 
313
 
    def get_offset(offset):
314
 
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
315
 
            return None
316
 
        return navigation.revid_list[navigation.position + offset]
317
 
 
318
 
    navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
319
 
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
320
 
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
321
 
    prev_page_revno = navigation.history.get_revno(
322
 
            navigation.prev_page_revid)
323
 
    next_page_revno = navigation.history.get_revno(
324
 
            navigation.next_page_revid)
325
 
    start_revno = navigation.history.get_revno(navigation.start_revid)
326
 
 
327
 
    params = { 'filter_file_id': navigation.filter_file_id }
328
 
    if getattr(navigation, 'query', None) is not None:
329
 
        params['q'] = navigation.query
330
 
 
331
 
    if getattr(navigation, 'start_revid', None) is not None:
332
 
        params['start_revid'] = start_revno
333
 
 
334
 
    if navigation.prev_page_revid:
335
 
        navigation.prev_page_url = navigation.branch.context_url(
336
 
            [navigation.scan_url, prev_page_revno], **params)
337
 
    if navigation.next_page_revid:
338
 
        navigation.next_page_url = navigation.branch.context_url(
339
 
            [navigation.scan_url, next_page_revno], **params)
340
 
 
341
 
 
342
 
def decorator(unbound):
343
 
    def new_decorator(f):
344
 
        g = unbound(f)
345
 
        g.__name__ = f.__name__
346
 
        g.__doc__ = f.__doc__
347
 
        g.__dict__.update(f.__dict__)
348
 
        return g
349
 
    new_decorator.__name__ = unbound.__name__
350
 
    new_decorator.__doc__ = unbound.__doc__
351
 
    new_decorator.__dict__.update(unbound.__dict__)
352
 
    return new_decorator
353
 
 
354
 
 
355
 
# common threading-lock decorator
356
 
def with_lock(lockname, debug_name=None):
357
 
    if debug_name is None:
358
 
        debug_name = lockname
359
 
    @decorator
360
 
    def _decorator(unbound):
361
 
        def locked(self, *args, **kw):
362
 
            getattr(self, lockname).acquire()
363
 
            try:
364
 
                return unbound(self, *args, **kw)
365
 
            finally:
366
 
                getattr(self, lockname).release()
367
 
        return locked
368
 
    return _decorator
369
 
 
370
 
 
371
 
@decorator
372
 
def lsprof(f):
373
 
    def _f(*a, **kw):
374
 
        from loggerhead.lsprof import profile
375
 
        import cPickle
376
 
        z = time.time()
377
 
        ret, stats = profile(f, *a, **kw)
378
 
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
379
 
        stats.sort()
380
 
        stats.freeze()
381
 
        now = time.time()
382
 
        msec = int(now * 1000) % 1000
383
 
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
384
 
        filename = f.__name__ + '-' + timestr + '.lsprof'
385
 
        cPickle.dump(stats, open(filename, 'w'), 2)
386
 
        return ret
387
 
    return _f
388
 
 
389
 
 
390
 
# just thinking out loud here...
391
 
#
392
 
# so, when browsing around, there are 5 pieces of context, most optional:
393
 
#     - current revid
394
 
#         current location along the navigation path (while browsing)
395
 
#     - starting revid (start_revid)
396
 
#         the current beginning of navigation (navigation continues back to
397
 
#         the original revision) -- this defines an 'alternate mainline'
398
 
#         when the user navigates into a branch.
399
 
#     - file_id
400
 
#         the file being looked at
401
 
#     - filter_file_id
402
 
#         if navigating the revisions that touched a file
403
 
#     - q (query)
404
 
#         if navigating the revisions that matched a search query
405
 
#     - remember
406
 
#         a previous revision to remember for future comparisons
407
 
#
408
 
# current revid is given on the url path.  the rest are optional components
409
 
# in the url params.
410
 
#
411
 
# other transient things can be set:
412
 
#     - compare_revid
413
 
#         to compare one revision to another, on /revision only
414
 
#     - sort
415
 
#         for re-ordering an existing page by different sort
416
 
 
417
 
t_context = threading.local()
418
 
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
419
 
          'compare_revid', 'sort')
420
 
 
421
 
 
422
 
def set_context(map):
423
 
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
424
 
 
425
 
 
426
 
def get_context(**overrides):
427
 
    """
428
 
    Soon to be deprecated.
429
 
 
430
 
 
431
 
    return a context map that may be overriden by specific values passed in,
432
 
    but only contains keys from the list of valid context keys.
433
 
 
434
 
    if 'clear' is set, only the 'remember' context value will be added, and
435
 
    all other context will be omitted.
436
 
    """
437
 
    map = dict()
438
 
    if overrides.get('clear', False):
439
 
        map['remember'] = t_context.map.get('remember', None)
440
 
    else:
441
 
        map.update(t_context.map)
442
 
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
443
 
    map.update(overrides)
444
 
    return map
 
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