/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
148.3.6 by Michael Hudson
clarify some things
20
from elementtree import ElementTree as ET
21
80 by Robey Pointer
refactor and clean up the javascript code for the expand/collapse buttons,
22
import base64
5 by Robey Pointer
okay, redo the changes-list screen
23
import cgi
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
24
import datetime
20 by Robey Pointer
add a timed event to fill in the revision cache, so that after running for
25
import logging
1 by Robey Pointer
initial checkin
26
import re
27
import sha
84 by Robey Pointer
add a uniq() function for helping trim some of the verbosity on the revision
28
import struct
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
29
import sys
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
30
import threading
113 by Robey Pointer
add lsprof decorator; comment out unnecessary nbsp; conversion.
31
import time
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
32
import traceback
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
33
1 by Robey Pointer
initial checkin
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
148.3.6 by Michael Hudson
clarify some things
37
# Display of times.
38
39
# date_day -- just the day
40
# date_time -- full date with time
41
#
42
# displaydate -- for use in sentences
43
# approximatedate -- for use in tables
44
#
45
# displaydate and approximatedate return an elementtree <span> Element
46
# with the full date in a tooltip.
47
48
def date_day(value):
148.3.1 by Michael Hudson
do what mark said
49
    return value.strftime('%Y-%m-%d')
50
148.3.6 by Michael Hudson
clarify some things
51
52
def date_time(value):
53
    return value.strftime('%Y-%m-%d %T')
54
55
56
def _displaydate(date):
148.3.1 by Michael Hudson
do what mark said
57
    delta = abs(datetime.datetime.now() - date)
58
    if delta > datetime.timedelta(1, 0, 0):
59
        # far in the past or future, display the date
148.3.6 by Michael Hudson
clarify some things
60
        return 'on ' + date_day(date)
61
    return _approximatedate(date)
62
63
64
def _approximatedate(date):
128.2.32 by Robey Pointer
i can't decide on a decent default date format, so i'm punting on it:
65
    delta = datetime.datetime.now() - date
148.3.1 by Michael Hudson
do what mark said
66
    if abs(delta) > datetime.timedelta(1, 0, 0):
67
        # far in the past or future, display the date
148.3.6 by Michael Hudson
clarify some things
68
        return date_day(date)
148.3.1 by Michael Hudson
do what mark said
69
    future = delta < datetime.timedelta(0, 0, 0)
70
    delta = abs(delta)
71
    days = delta.days
72
    hours = delta.seconds / 3600
73
    minutes = (delta.seconds - (3600*hours)) / 60
74
    seconds = delta.seconds % 60
75
    result = ''
76
    if future:
77
        result += 'in '
78
    if days != 0:
79
        amount = days
80
        unit = 'day'
81
    elif hours != 0:
82
        amount = hours
83
        unit = 'hour'
84
    elif minutes != 0:
85
        amount = minutes
86
        unit = 'minute'
128.2.32 by Robey Pointer
i can't decide on a decent default date format, so i'm punting on it:
87
    else:
148.3.1 by Michael Hudson
do what mark said
88
        amount = seconds
89
        unit = 'second'
90
    if amount != 1:
91
        unit += 's'
92
    result += '%s %s' % (amount, unit)
93
    if not future:
94
        result += ' ago'
95
        return result
128.2.32 by Robey Pointer
i can't decide on a decent default date format, so i'm punting on it:
96
148.3.6 by Michael Hudson
clarify some things
97
98
def _wrap_with_date_time_title(date, formatted_date):
148.3.1 by Michael Hudson
do what mark said
99
    elem = ET.Element("span")
148.3.6 by Michael Hudson
clarify some things
100
    elem.text = formatted_date
101
    elem.set("title", date_time(date))
148.3.1 by Michael Hudson
do what mark said
102
    return elem
128.2.32 by Robey Pointer
i can't decide on a decent default date format, so i'm punting on it:
103
104
148.3.6 by Michael Hudson
clarify some things
105
def approximatedate(date):
106
    return _wrap_with_date_time_title(date, _approximatedate(date))
107
108
109
def displaydate(date):
110
    return _wrap_with_date_time_title(date, _displaydate(date))
111
112
1 by Robey Pointer
initial checkin
113
class Container (object):
114
    """
115
    Convert a dict into an object with attributes.
116
    """
117
    def __init__(self, _dict=None, **kw):
118
        if _dict is not None:
119
            for key, value in _dict.iteritems():
120
                setattr(self, key, value)
121
        for key, value in kw.iteritems():
122
            setattr(self, key, value)
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
123
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
124
    def __repr__(self):
125
        out = '{ '
126
        for key, value in self.__dict__.iteritems():
127
            if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
128
                continue
129
            out += '%r => %r, ' % (key, value)
130
        out += '}'
131
        return out
1 by Robey Pointer
initial checkin
132
133
134
def clean_revid(revid):
135
    if revid == 'missing':
136
        return revid
137
    return sha.new(revid).hexdigest()
138
139
140
def obfuscate(text):
141
    return ''.join([ '&#%d;' % ord(c) for c in text ])
142
143
23 by Robey Pointer
lots of little changes:
144
def trunc(text, limit=10):
145
    if len(text) <= limit:
146
        return text
147
    return text[:limit] + '...'
148
149
42 by Robey Pointer
add text substring indexer
150
def to_utf8(s):
151
    if isinstance(s, unicode):
152
        return s.encode('utf-8')
153
    return s
154
155
1 by Robey Pointer
initial checkin
156
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
157
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
158
159
def hide_email(email):
160
    """
161
    try to obsure any email address in a bazaar committer's name.
162
    """
163
    m = STANDARD_PATTERN.search(email)
164
    if m is not None:
165
        name = m.group(1)
166
        email = m.group(2)
167
        return name
168
    m = EMAIL_PATTERN.search(email)
169
    if m is None:
170
        # can't find an email address in here
171
        return email
172
    username, domain = m.group(0).split('@')
173
    domains = domain.split('.')
174
    if len(domains) >= 2:
175
        return '%s at %s' % (username, domains[-2])
176
    return '%s at %s' % (username, domains[0])
177
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
178
22 by Robey Pointer
clean up the navbar, and add some profiling for the bad method.
179
def triple_factors(min_value=1):
1 by Robey Pointer
initial checkin
180
    factors = (1, 3)
181
    index = 0
182
    n = 1
183
    while True:
22 by Robey Pointer
clean up the navbar, and add some profiling for the bad method.
184
        if n >= min_value:
13 by Robey Pointer
clean up revision navigation so that the "revlist" you're browsing is
185
            yield n * factors[index]
1 by Robey Pointer
initial checkin
186
        index += 1
187
        if index >= len(factors):
188
            index = 0
189
            n *= 10
190
191
22 by Robey Pointer
clean up the navbar, and add some profiling for the bad method.
192
def scan_range(pos, max, pagesize=1):
1 by Robey Pointer
initial checkin
193
    """
194
    given a position in a maximum range, return a list of negative and positive
195
    jump factors for an hgweb-style triple-factor geometric scan.
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
196
1 by Robey Pointer
initial checkin
197
    for example, with pos=20 and max=500, the range would be:
198
    [ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
199
1 by Robey Pointer
initial checkin
200
    i admit this is a very strange way of jumping through revisions.  i didn't
201
    invent it. :)
202
    """
203
    out = []
22 by Robey Pointer
clean up the navbar, and add some profiling for the bad method.
204
    for n in triple_factors(pagesize + 1):
1 by Robey Pointer
initial checkin
205
        if n > max:
206
            return out
207
        if pos + n < max:
208
            out.append(n)
209
        if pos - n >= 0:
210
            out.insert(0, -n)
211
5 by Robey Pointer
okay, redo the changes-list screen
212
38 by Robey Pointer
another pile of semi-related changes:
213
# only do this if unicode turns out to be a problem
214
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
215
128.2.8 by Robey Pointer
avoid using XML() when displaying diffs. for diff lines, tabs are still
216
# FIXME: get rid of this method; use fixed_width() and avoid XML().
5 by Robey Pointer
okay, redo the changes-list screen
217
def html_clean(s):
218
    """
219
    clean up a string for html display.  expand any tabs, encode any html
220
    entities, and replace spaces with '&nbsp;'.  this is primarily for use
221
    in displaying monospace text.
222
    """
38 by Robey Pointer
another pile of semi-related changes:
223
    s = cgi.escape(s.expandtabs())
114 by Robey Pointer
revert the change that stopped converting spaces to '&nbsp;'.
224
    s = s.replace(' ', '&nbsp;')
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
225
    return s
226
227
128.2.9 by Robey Pointer
merge mwhudson's branch. amusingly, we came up with nearly identical fixes
228
128.2.11 by Robey Pointer
bug 117799: do some heuristic guessing for file data encoding, when
229
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
230
128.2.8 by Robey Pointer
avoid using XML() when displaying diffs. for diff lines, tabs are still
231
def fixed_width(s):
232
    """
233
    expand tabs and turn spaces into "non-breaking spaces", so browsers won't
234
    chop up the string.
235
    """
128.2.11 by Robey Pointer
bug 117799: do some heuristic guessing for file data encoding, when
236
    if not isinstance(s, unicode):
237
        # this kinda sucks.  file contents are just binary data, and no
238
        # encoding metadata is stored, so we need to guess.  this is probably
239
        # okay for most code, but for people using things like KOI-8, this
240
        # will display gibberish.  we have no way of detecting the correct
241
        # encoding to use.
242
        try:
243
            s = s.decode('utf-8')
244
        except UnicodeDecodeError:
245
            s = s.decode('iso-8859-15')
128.2.9 by Robey Pointer
merge mwhudson's branch. amusingly, we came up with nearly identical fixes
246
    return s.expandtabs().replace(' ', NONBREAKING_SPACE)
128.2.8 by Robey Pointer
avoid using XML() when displaying diffs. for diff lines, tabs are still
247
248
9 by Robey Pointer
starting work on the inventory page, and some starting work on getting a changelog per-path
249
def fake_permissions(kind, executable):
250
    # fake up unix-style permissions given only a "kind" and executable bit
251
    if kind == 'directory':
252
        return 'drwxr-xr-x'
253
    if executable:
254
        return '-rwxr-xr-x'
255
    return '-rw-r--r--'
256
18 by Robey Pointer
add a caching system for revision/change entries, since those should never
257
20 by Robey Pointer
add a timed event to fill in the revision cache, so that after running for
258
def if_present(format, value):
259
    """
260
    format a value using a format string, if the value exists and is not None.
261
    """
262
    if value is None:
263
        return ''
264
    return format % value
265
266
80 by Robey Pointer
refactor and clean up the javascript code for the expand/collapse buttons,
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
84 by Robey Pointer
add a uniq() function for helping trim some of the verbosity on the revision
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
39 by Robey Pointer
add a download link to the inventory page, and allow sorting by size and
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
def human_size(size, min_divisor=0):
295
    size = int(size)
296
    if (size == 0) and (min_divisor == 0):
297
        return '0'
298
    if (size < 512) and (min_divisor == 0):
299
        return str(size)
300
301
    if (size >= P95_GIG) or (min_divisor >= GIG):
302
        divisor = GIG
303
    elif (size >= P95_MEG) or (min_divisor >= MEG):
304
        divisor = MEG
305
    else:
306
        divisor = KILO
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
307
39 by Robey Pointer
add a download link to the inventory page, and allow sorting by size and
308
    dot = size % divisor
309
    base = size - dot
310
    dot = dot * 10 // divisor
311
    base //= divisor
312
    if dot >= 10:
313
        base += 1
314
        dot -= 10
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
315
39 by Robey Pointer
add a download link to the inventory page, and allow sorting by size and
316
    out = str(base)
317
    if (base < 100) and (dot != 0):
318
        out += '.%d' % (dot,)
319
    if divisor == KILO:
320
        out += 'K'
321
    elif divisor == MEG:
322
        out += 'M'
323
    elif divisor == GIG:
324
        out += 'G'
325
    return out
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
326
39 by Robey Pointer
add a download link to the inventory page, and allow sorting by size and
327
128.1.19 by Michael Hudson
simplifications
328
def fill_in_navigation(navigation):
23 by Robey Pointer
lots of little changes:
329
    """
330
    given a navigation block (used by the template for the page header), fill
331
    in useful calculated values.
332
    """
128.1.19 by Michael Hudson
simplifications
333
    if navigation.revid in navigation.revid_list: # XXX is this always true?
334
        navigation.position = navigation.revid_list.index(navigation.revid)
335
    else:
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
336
        navigation.position = 0
43 by Robey Pointer
fix up the other (non-changelog) pages to work with search queries by
337
    navigation.count = len(navigation.revid_list)
23 by Robey Pointer
lots of little changes:
338
    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
339
    navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
340
23 by Robey Pointer
lots of little changes:
341
    def get_offset(offset):
25 by Robey Pointer
fix a couple of bugs:
342
        if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
23 by Robey Pointer
lots of little changes:
343
            return None
43 by Robey Pointer
fix up the other (non-changelog) pages to work with search queries by
344
        return navigation.revid_list[navigation.position + offset]
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
345
23 by Robey Pointer
lots of little changes:
346
    navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
347
    navigation.next_page_revid = get_offset(1 * navigation.pagesize)
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
348
151.1.2 by Michael Hudson
oops
349
    params = { 'filter_file_id': navigation.filter_file_id }
41 by Robey Pointer
initial search ui (revid, date, and very slow text search)
350
    if getattr(navigation, 'query', None) is not None:
351
        params['q'] = navigation.query
352
    else:
353
        params['start_revid'] = navigation.start_revid
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
354
23 by Robey Pointer
lots of little changes:
355
    if navigation.prev_page_revid:
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
356
        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:
357
    if navigation.next_page_revid:
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
358
        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)
359
360
361
def log_exception(log):
362
    for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
363
        log.debug(line)
364
365
366
def decorator(unbound):
367
    def new_decorator(f):
368
        g = unbound(f)
369
        g.__name__ = f.__name__
370
        g.__doc__ = f.__doc__
371
        g.__dict__.update(f.__dict__)
372
        return g
373
    new_decorator.__name__ = unbound.__name__
374
    new_decorator.__doc__ = unbound.__doc__
375
    new_decorator.__dict__.update(unbound.__dict__)
376
    return new_decorator
23 by Robey Pointer
lots of little changes:
377
378
48 by Robey Pointer
the big migration of branch-specific data to a BranchView object: actually
379
# 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
380
def with_lock(lockname, debug_name=None):
381
    if debug_name is None:
382
        debug_name = lockname
48 by Robey Pointer
the big migration of branch-specific data to a BranchView object: actually
383
    @decorator
384
    def _decorator(unbound):
385
        def locked(self, *args, **kw):
386
            getattr(self, lockname).acquire()
387
            try:
388
                return unbound(self, *args, **kw)
389
            finally:
390
                getattr(self, lockname).release()
391
        return locked
392
    return _decorator
24 by Robey Pointer
figured out how to make my own separate config file like BzrInspect, and
393
94 by Robey Pointer
add a decorator to strip the whitespace from the generated html in the big
394
395
@decorator
396
def strip_whitespace(f):
397
    def _f(*a, **kw):
398
        out = f(*a, **kw)
399
        orig_len = len(out)
400
        out = re.sub(r'\n\s+', '\n', out)
401
        out = re.sub(r'[ \t]+', ' ', out)
402
        out = re.sub(r'\s+\n', '\n', out)
403
        new_len = len(out)
404
        log.debug('Saved %sB (%d%%) by stripping whitespace.',
405
                  human_size(orig_len - new_len),
406
                  round(100.0 - float(new_len) * 100.0 / float(orig_len)))
407
        return out
408
    return _f
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
409
410
113 by Robey Pointer
add lsprof decorator; comment out unnecessary nbsp; conversion.
411
@decorator
412
def lsprof(f):
413
    def _f(*a, **kw):
414
        from loggerhead.lsprof import profile
415
        import cPickle
416
        z = time.time()
417
        ret, stats = profile(f, *a, **kw)
418
        log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
419
        stats.sort()
420
        stats.freeze()
421
        now = time.time()
422
        msec = int(now * 1000) % 1000
423
        timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
424
        filename = f.__name__ + '-' + timestr + '.lsprof'
425
        cPickle.dump(stats, open(filename, 'w'), 2)
426
        return ret
427
    return _f
428
429
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
430
# just thinking out loud here...
431
#
432
# so, when browsing around, there are 5 pieces of context, most optional:
433
#     - current revid
434
#         current location along the navigation path (while browsing)
435
#     - starting revid (start_revid)
436
#         the current beginning of navigation (navigation continues back to
151.1.1 by Michael Hudson
separate the names of the arguments used to restict to revisions that touch a
437
#         the original revision) -- this defines an 'alternate mainline'
438
#         when the user navigates into a branch.
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
439
#     - file_id
151.1.1 by Michael Hudson
separate the names of the arguments used to restict to revisions that touch a
440
#         the file being looked at
441
#     - filter_file_id
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
442
#         if navigating the revisions that touched a file
443
#     - q (query)
444
#         if navigating the revisions that matched a search query
445
#     - remember
446
#         a previous revision to remember for future comparisons
447
#
448
# current revid is given on the url path.  the rest are optional components
449
# in the url params.
450
#
451
# other transient things can be set:
452
#     - compare_revid
453
#         to compare one revision to another, on /revision only
454
#     - sort
455
#         for re-ordering an existing page by different sort
456
457
t_context = threading.local()
151.1.1 by Michael Hudson
separate the names of the arguments used to restict to revisions that touch a
458
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
459
          'compare_revid', 'sort')
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
460
461
462
def set_context(map):
463
    t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
464
465
466
def get_context(**overrides):
467
    """
468
    return a context map that may be overriden by specific values passed in,
469
    but only contains keys from the list of valid context keys.
144.1.10 by Michael Hudson
run reindent.py over the loggerhead package
470
97 by Robey Pointer
big checkpoint commit. added some functions to util for tracking browsing
471
    if 'clear' is set, only the 'remember' context value will be added, and
472
    all other context will be omitted.
473
    """
474
    map = dict()
475
    if overrides.get('clear', False):
476
        map['remember'] = t_context.map.get('remember', None)
477
    else:
478
        map.update(t_context.map)
479
    overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
480
    map.update(overrides)
481
    return map