/loggerhead/trunk

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/loggerhead/trunk
1 by Robey Pointer
initial checkin
1
#
2
# Copyright (C) 2006  Robey Pointer <robey@lag.net>
23 by Robey Pointer
lots of little changes:
3
# Copyright (C) 2006  Goffredo Baroncelli <kreijack@inwind.it>
1 by Robey Pointer
initial checkin
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 2 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
#
19
80 by Robey Pointer
refactor and clean up the javascript code for the expand/collapse buttons,
20
import base64
5 by Robey Pointer
okay, redo the changes-list screen
21
import cgi
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
22
import datetime
20 by Robey Pointer
add a timed event to fill in the revision cache, so that after running for
23
import logging
1 by Robey Pointer
initial checkin
24
import re
25
import sha
84 by Robey Pointer
add a uniq() function for helping trim some of the verbosity on the revision
26
import struct
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
27
import sys
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
28
import threading
113 by Robey Pointer
add lsprof decorator; comment out unnecessary nbsp; conversion.
29
import time
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
30
import traceback
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
31
32
import turbogears
1 by Robey Pointer
initial checkin
33
34
20 by Robey Pointer
add a timed event to fill in the revision cache, so that after running for
35
log = logging.getLogger("loggerhead.controllers")
36
37
1 by Robey Pointer
initial checkin
38
def timespan(delta):
6 by Robey Pointer
if more than 730 days have passed, let's just start counting years (GOD I'M OLD)
39
    if delta.days > 730:
40
        # good grief!
41
        return '%d years' % (int(delta.days // 365.25),)
1 by Robey Pointer
initial checkin
42
    if delta.days >= 3:
43
        return '%d days' % delta.days
44
    seg = []
45
    if delta.days > 0:
46
        if delta.days == 1:
47
            seg.append('1 day')
48
        else:
49
            seg.append('%d days' % delta.days)
50
    hrs = delta.seconds // 3600
51
    mins = (delta.seconds % 3600) // 60
52
    if hrs > 0:
53
        if hrs == 1:
54
            seg.append('1 hour')
55
        else:
56
            seg.append('%d hours' % hrs)
57
    if delta.days == 0:
58
        if mins > 0:
59
            if mins == 1:
60
                seg.append('1 minute')
61
            else:
62
                seg.append('%d minutes' % mins)
63
        elif hrs == 0:
64
            seg.append('less than a minute')
65
    return ', '.join(seg)
66
67
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
68
def ago(timestamp):
69
    now = datetime.datetime.now()
70
    return timespan(now - timestamp) + ' ago'
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
71
72
73
def fix_year(year):
74
    if year < 70:
75
        year += 2000
76
    if year < 100:
77
        year += 1900
78
    return year
79
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
80
1 by Robey Pointer
initial checkin
81
class Container (object):
82
    """
83
    Convert a dict into an object with attributes.
84
    """
85
    def __init__(self, _dict=None, **kw):
86
        if _dict is not None:
87
            for key, value in _dict.iteritems():
88
                setattr(self, key, value)
89
        for key, value in kw.iteritems():
90
            setattr(self, key, value)
3 by Robey Pointer
possibly i'm going a little crazy here, but dramatically improve diff output by parsing it into a nested structure and letting the template format it
91
    
92
    def __repr__(self):
93
        out = '{ '
94
        for key, value in self.__dict__.iteritems():
95
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
96
                continue
97
            out += '%r => %r, ' % (key, value)
98
        out += '}'
99
        return out
1 by Robey Pointer
initial checkin
100
101
102
def clean_revid(revid):
103
    if revid == 'missing':
104
        return revid
105
    return sha.new(revid).hexdigest()
106
107
108
def obfuscate(text):
109
    return ''.join([ '&#%d;' % ord(c) for c in text ])
110
111
23 by Robey Pointer
lots of little changes:
112
def trunc(text, limit=10):
113
    if len(text) <= limit:
114
        return text
115
    return text[:limit] + '...'
116
117
42 by Robey Pointer
add text substring indexer
118
def to_utf8(s):
119
    if isinstance(s, unicode):
120
        return s.encode('utf-8')
121
    return s
122
123
1 by Robey Pointer
initial checkin
124
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
125
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
126
127
def hide_email(email):
128
    """
129
    try to obsure any email address in a bazaar committer's name.
130
    """
131
    m = STANDARD_PATTERN.search(email)
132
    if m is not None:
133
        name = m.group(1)
134
        email = m.group(2)
135
        return name
136
    m = EMAIL_PATTERN.search(email)
137
    if m is None:
138
        # can't find an email address in here
139
        return email
140
    username, domain = m.group(0).split('@')
141
    domains = domain.split('.')
142
    if len(domains) >= 2:
143
        return '%s at %s' % (username, domains[-2])
144
    return '%s at %s' % (username, domains[0])
145
146
    
22 by Robey Pointer
clean up the navbar, and add some profiling for the bad method.
147
def triple_factors(min_value=1):
1 by Robey Pointer
initial checkin
148
    factors = (1, 3)
149
    index = 0
150
    n = 1
151
    while True:
22 by Robey Pointer
clean up the navbar, and add some profiling for the bad method.
152
        if n >= min_value:
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
153
            yield n * factors[index]
1 by Robey Pointer
initial checkin
154
        index += 1
155
        if index >= len(factors):
156
            index = 0
157
            n *= 10
158
159
22 by Robey Pointer
clean up the navbar, and add some profiling for the bad method.
160
def scan_range(pos, max, pagesize=1):
1 by Robey Pointer
initial checkin
161
    """
162
    given a position in a maximum range, return a list of negative and positive
163
    jump factors for an hgweb-style triple-factor geometric scan.
164
    
165
    for example, with pos=20 and max=500, the range would be:
166
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
167
    
168
    i admit this is a very strange way of jumping through revisions.  i didn't
169
    invent it. :)
170
    """
171
    out = []
22 by Robey Pointer
clean up the navbar, and add some profiling for the bad method.
172
    for n in triple_factors(pagesize + 1):
1 by Robey Pointer
initial checkin
173
        if n > max:
174
            return out
175
        if pos + n < max:
176
            out.append(n)
177
        if pos - n >= 0:
178
            out.insert(0, -n)
179
5 by Robey Pointer
okay, redo the changes-list screen
180
38 by Robey Pointer
another pile of semi-related changes:
181
# only do this if unicode turns out to be a problem
182
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
183
128.2.8 by Robey Pointer
avoid using XML() when displaying diffs. for diff lines, tabs are still
184
# FIXME: get rid of this method; use fixed_width() and avoid XML().
5 by Robey Pointer
okay, redo the changes-list screen
185
def html_clean(s):
186
    """
187
    clean up a string for html display.  expand any tabs, encode any html
188
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
189
    in displaying monospace text.
190
    """
38 by Robey Pointer
another pile of semi-related changes:
191
    s = cgi.escape(s.expandtabs())
114 by Robey Pointer
revert the change that stopped converting spaces to '&nbsp;'.
192
    s = s.replace(' ', '&nbsp;')
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
193
    return s
194
195
128.2.9 by Robey Pointer
merge mwhudson's branch. amusingly, we came up with nearly identical fixes
196
128.2.11 by Robey Pointer
bug 117799: do some heuristic guessing for file data encoding, when
197
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
128.2.9 by Robey Pointer
merge mwhudson's branch. amusingly, we came up with nearly identical fixes
198
128.2.8 by Robey Pointer
avoid using XML() when displaying diffs. for diff lines, tabs are still
199
def fixed_width(s):
200
    """
201
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
202
    chop up the string.
203
    """
128.2.11 by Robey Pointer
bug 117799: do some heuristic guessing for file data encoding, when
204
    if not isinstance(s, unicode):
205
        # this kinda sucks.  file contents are just binary data, and no
206
        # encoding metadata is stored, so we need to guess.  this is probably
207
        # okay for most code, but for people using things like KOI-8, this
208
        # will display gibberish.  we have no way of detecting the correct
209
        # encoding to use.
210
        try:
211
            s = s.decode('utf-8')
212
        except UnicodeDecodeError:
213
            s = s.decode('iso-8859-15')
128.2.9 by Robey Pointer
merge mwhudson's branch. amusingly, we came up with nearly identical fixes
214
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
128.2.8 by Robey Pointer
avoid using XML() when displaying diffs. for diff lines, tabs are still
215
216
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
217
def fake_permissions(kind, executable):
218
    # fake up unix-style permissions given only a "kind" and executable bit
219
    if kind == 'directory':
220
        return 'drwxr-xr-x'
221
    if executable:
222
        return '-rwxr-xr-x'
223
    return '-rw-r--r--'
224
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
225
20 by Robey Pointer
add a timed event to fill in the revision cache, so that after running for
226
def if_present(format, value):
227
    """
228
    format a value using a format string, if the value exists and is not None.
229
    """
230
    if value is None:
231
        return ''
232
    return format % value
233
234
80 by Robey Pointer
refactor and clean up the javascript code for the expand/collapse buttons,
235
def b64(s):
236
    s = base64.encodestring(s).replace('\n', '')
237
    while (len(s) > 0) and (s[-1] == '='):
238
        s = s[:-1]
239
    return s
240
241
84 by Robey Pointer
add a uniq() function for helping trim some of the verbosity on the revision
242
def uniq(uniqs, s):
243
    """
244
    turn a potentially long string into a unique smaller string.
245
    """
246
    if s in uniqs:
247
        return uniqs[s]
248
    uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
249
    x = struct.pack('>I', next)
250
    while (len(x) > 1) and (x[0] == '\x00'):
251
        x = x[1:]
252
    uniqs[s] = b64(x)
253
    return uniqs[s]
254
255
39 by Robey Pointer
add a download link to the inventory page, and allow sorting by size and
256
KILO = 1024
257
MEG = 1024 * KILO
258
GIG = 1024 * MEG
259
P95_MEG = int(0.9 * MEG)
260
P95_GIG = int(0.9 * GIG)
261
262
def human_size(size, min_divisor=0):
263
    size = int(size)
264
    if (size == 0) and (min_divisor == 0):
265
        return '0'
266
    if (size < 512) and (min_divisor == 0):
267
        return str(size)
268
269
    if (size >= P95_GIG) or (min_divisor >= GIG):
270
        divisor = GIG
271
    elif (size >= P95_MEG) or (min_divisor >= MEG):
272
        divisor = MEG
273
    else:
274
        divisor = KILO
275
    
276
    dot = size % divisor
277
    base = size - dot
278
    dot = dot * 10 // divisor
279
    base //= divisor
280
    if dot >= 10:
281
        base += 1
282
        dot -= 10
283
    
284
    out = str(base)
285
    if (base < 100) and (dot != 0):
286
        out += '.%d' % (dot,)
287
    if divisor == KILO:
288
        out += 'K'
289
    elif divisor == MEG:
290
        out += 'M'
291
    elif divisor == GIG:
292
        out += 'G'
293
    return out
294
    
295
128.1.19 by Michael Hudson
simplifications
296
def fill_in_navigation(navigation):
23 by Robey Pointer
lots of little changes:
297
    """
298
    given a navigation block (used by the template for the page header), fill
299
    in useful calculated values.
300
    """
128.1.19 by Michael Hudson
simplifications
301
    if navigation.revid in navigation.revid_list: # XXX is this always true?
302
        navigation.position = navigation.revid_list.index(navigation.revid)
303
    else:
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
304
        navigation.position = 0
43 by Robey Pointer
fix up the other (non-changelog) pages to work with search queries by
305
    navigation.count = len(navigation.revid_list)
23 by Robey Pointer
lots of little changes:
306
    navigation.page_position = navigation.position // navigation.pagesize + 1
43 by Robey Pointer
fix up the other (non-changelog) pages to work with search queries by
307
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
23 by Robey Pointer
lots of little changes:
308
    
309
    def get_offset(offset):
25 by Robey Pointer
fix a couple of bugs:
310
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
23 by Robey Pointer
lots of little changes:
311
            return None
43 by Robey Pointer
fix up the other (non-changelog) pages to work with search queries by
312
        return navigation.revid_list[navigation.position + offset]
23 by Robey Pointer
lots of little changes:
313
    
314
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
315
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
316
    
317
    params = { 'file_id': navigation.file_id }
318
    if getattr(navigation, 'query', None) is not None:
319
        params['q'] = navigation.query
320
    else:
321
        params['start_revid'] = navigation.start_revid
322
        
23 by Robey Pointer
lots of little changes:
323
    if navigation.prev_page_revid:
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
324
        navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
23 by Robey Pointer
lots of little changes:
325
    if navigation.next_page_revid:
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
326
        navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
327
328
329
def log_exception(log):
330
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
331
        log.debug(line)
332
333
334
def decorator(unbound):
335
    def new_decorator(f):
336
        g = unbound(f)
337
        g.__name__ = f.__name__
338
        g.__doc__ = f.__doc__
339
        g.__dict__.update(f.__dict__)
340
        return g
341
    new_decorator.__name__ = unbound.__name__
342
    new_decorator.__doc__ = unbound.__doc__
343
    new_decorator.__dict__.update(unbound.__dict__)
344
    return new_decorator
23 by Robey Pointer
lots of little changes:
345
346
48 by Robey Pointer
the big migration of branch-specific data to a BranchView object: actually
347
# common threading-lock decorator
49 by Robey Pointer
add top-level page listing available branches. also a patch from matty to not require external-url in atom feeds any more
348
def with_lock(lockname, debug_name=None):
349
    if debug_name is None:
350
        debug_name = lockname
48 by Robey Pointer
the big migration of branch-specific data to a BranchView object: actually
351
    @decorator
352
    def _decorator(unbound):
353
        def locked(self, *args, **kw):
354
            getattr(self, lockname).acquire()
355
            try:
356
                return unbound(self, *args, **kw)
357
            finally:
358
                getattr(self, lockname).release()
359
        return locked
360
    return _decorator
24 by Robey Pointer
figured out how to make my own separate config file like BzrInspect, and
361
94 by Robey Pointer
add a decorator to strip the whitespace from the generated html in the big
362
363
@decorator
364
def strip_whitespace(f):
365
    def _f(*a, **kw):
366
        out = f(*a, **kw)
367
        orig_len = len(out)
368
        out = re.sub(r'\n\s+', '\n', out)
369
        out = re.sub(r'[ \t]+', ' ', out)
370
        out = re.sub(r'\s+\n', '\n', out)
371
        new_len = len(out)
372
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
373
                  human_size(orig_len - new_len),
374
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
375
        return out
376
    return _f
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
377
378
113 by Robey Pointer
add lsprof decorator; comment out unnecessary nbsp; conversion.
379
@decorator
380
def lsprof(f):
381
    def _f(*a, **kw):
382
        from loggerhead.lsprof import profile
383
        import cPickle
384
        z = time.time()
385
        ret, stats = profile(f, *a, **kw)
386
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
387
        stats.sort()
388
        stats.freeze()
389
        now = time.time()
390
        msec = int(now * 1000) % 1000
391
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
392
        filename = f.__name__ + '-' + timestr + '.lsprof'
393
        cPickle.dump(stats, open(filename, 'w'), 2)
394
        return ret
395
    return _f
396
397
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
398
# just thinking out loud here...
399
#
400
# so, when browsing around, there are 5 pieces of context, most optional:
401
#     - current revid
402
#         current location along the navigation path (while browsing)
403
#     - starting revid (start_revid)
404
#         the current beginning of navigation (navigation continues back to
405
#         the original revision) -- this may not be along the primary revision
406
#         path since the user may have navigated into a branch
407
#     - file_id
408
#         if navigating the revisions that touched a file
409
#     - q (query)
410
#         if navigating the revisions that matched a search query
411
#     - remember
412
#         a previous revision to remember for future comparisons
413
#
414
# current revid is given on the url path.  the rest are optional components
415
# in the url params.
416
#
417
# other transient things can be set:
418
#     - compare_revid
419
#         to compare one revision to another, on /revision only
420
#     - sort
421
#         for re-ordering an existing page by different sort
422
423
t_context = threading.local()
424
_valid = ('start_revid', 'file_id', 'q', 'remember', 'compare_revid', 'sort')
425
426
427
def set_context(map):
428
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
429
430
431
def get_context(**overrides):
432
    """
433
    return a context map that may be overriden by specific values passed in,
434
    but only contains keys from the list of valid context keys.
435
    
436
    if 'clear' is set, only the 'remember' context value will be added, and
437
    all other context will be omitted.
438
    """
439
    map = dict()
440
    if overrides.get('clear', False):
441
        map['remember'] = t_context.map.get('remember', None)
442
    else:
443
        map.update(t_context.map)
444
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
445
    map.update(overrides)
446
    return map