35
from xml.etree import ElementTree as ET
37
from elementtree import ElementTree as ET
39
from bzrlib import urlutils
41
from simpletal.simpleTALUtils import HTMLStructureCleaner
43
35
log = logging.getLogger("loggerhead.controllers")
41
return '%d years' % (int(delta.days // 365.25),)
43
return '%d days' % delta.days
49
seg.append('%d days' % delta.days)
50
hrs = delta.seconds // 3600
51
mins = (delta.seconds % 3600) // 60
56
seg.append('%d hours' % hrs)
60
seg.append('1 minute')
62
seg.append('%d minutes' % mins)
64
seg.append('less than a minute')
69
now = datetime.datetime.now()
70
return timespan(now - timestamp) + ' ago'
46
73
def fix_year(year):
55
# date_day -- just the day
56
# date_time -- full date with time
58
# displaydate -- for use in sentences
59
# approximatedate -- for use in tables
61
# displaydate and approximatedate return an elementtree <span> Element
62
# with the full date in a tooltip.
66
return value.strftime('%Y-%m-%d')
71
return value.strftime('%Y-%m-%d %H:%M:%S')
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)
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
89
future = delta < datetime.timedelta(0, 0, 0)
92
hours = delta.seconds / 3600
93
minutes = (delta.seconds - (3600*hours)) / 60
94
seconds = delta.seconds % 60
112
result += '%s %s' % (amount, unit)
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))
125
def approximatedate(date):
126
#FIXME: Returns an object instead of a string
127
return _wrap_with_date_time_title(date, _approximatedate(date))
130
def displaydate(date):
131
return _wrap_with_date_time_title(date, _displaydate(date))
134
class Container(object):
81
class Container (object):
136
83
Convert a dict into an object with attributes.
139
85
def __init__(self, _dict=None, **kw):
140
self._properties = {}
141
86
if _dict is not None:
142
87
for key, value in _dict.iteritems():
143
88
setattr(self, key, value)
144
89
for key, value in kw.iteritems():
145
90
setattr(self, key, value)
147
92
def __repr__(self):
149
94
for key, value in self.__dict__.iteritems():
150
if key.startswith('_') or (getattr(self.__dict__[key],
151
'__call__', None) is not None):
95
if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
153
97
out += '%r => %r, ' % (key, value)
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)
165
def _set_property(self, attr, prop_func):
166
"""Set a function that will be called when an attribute is desired.
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.
171
if attr.startswith('_'):
172
raise ValueError("Cannot create properties that start with _")
173
self._properties[attr] = prop_func
102
def clean_revid(revid):
103
if revid == 'missing':
105
return sha.new(revid).hexdigest()
109
return ''.join([ '&#%d;' % ord(c) for c in text ])
176
112
def trunc(text, limit=10):
202
143
return '%s at %s' % (username, domains[-2])
203
144
return '%s at %s' % (username, domains[0])
205
def hide_emails(emails):
207
try to obscure any email address in a list of bazaar committers' names.
211
result.append(hide_email(email))
147
def triple_factors(min_value=1):
153
yield n * factors[index]
155
if index >= len(factors):
160
def scan_range(pos, max, pagesize=1):
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.
165
for example, with pos=20 and max=500, the range would be:
166
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
168
i admit this is a very strange way of jumping through revisions. i didn't
172
for n in triple_factors(pagesize + 1):
214
181
# only do this if unicode turns out to be a problem
215
182
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
217
184
# FIXME: get rid of this method; use fixed_width() and avoid XML().
220
185
def html_clean(s):
222
187
clean up a string for html display. expand any tabs, encode any html
364
304
navigation.position = 0
365
305
navigation.count = len(navigation.revid_list)
366
306
navigation.page_position = navigation.position // navigation.pagesize + 1
367
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
368
- 1)) // navigation.pagesize
307
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
370
309
def get_offset(offset):
371
if (navigation.position + offset < 0) or (
372
navigation.position + offset > navigation.count - 1):
310
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
374
312
return navigation.revid_list[navigation.position + offset]
376
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
377
314
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
378
315
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)
385
params = {'filter_file_id': navigation.filter_file_id}
317
params = { 'file_id': navigation.file_id }
386
318
if getattr(navigation, 'query', None) is not None:
387
319
params['q'] = navigation.query
389
if getattr(navigation, 'start_revid', None) is not None:
390
params['start_revid'] = start_revno
321
params['start_revid'] = navigation.start_revid
392
323
if navigation.prev_page_revid:
393
navigation.prev_page_url = navigation.branch.context_url(
394
[navigation.scan_url, prev_page_revno], **params)
324
navigation.prev_page_url = navigation.branch.url([ navigation.scan_url, navigation.prev_page_revid ], **get_context(**params))
395
325
if navigation.next_page_revid:
396
navigation.next_page_url = navigation.branch.context_url(
397
[navigation.scan_url, next_page_revno], **params)
400
def directory_breadcrumbs(path, is_root, view):
402
Generate breadcrumb information from the directory path given
404
The path given should be a path up to any branch that is currently being
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)
412
# Is our root directory itself a branch?
420
# Create breadcrumb trail for the path leading up to the branch
422
'dir_name': "(root)",
427
dir_parts = path.strip('/').split('/')
428
for index, dir_name in enumerate(dir_parts):
430
'dir_name': dir_name,
431
'path': '/'.join(dir_parts[:index + 1]),
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
441
def branch_breadcrumbs(path, inv, view):
443
Generate breadcrumb information from the branch path given
445
The path given should be a path that exists within a branch
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)
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,
460
return inner_breadcrumbs
326
navigation.next_page_url = navigation.branch.url([ navigation.scan_url, navigation.next_page_revid ], **get_context(**params))
329
def log_exception(log):
330
for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
463
334
def decorator(unbound):
465
335
def new_decorator(f):
467
337
g.__name__ = f.__name__
474
344
return new_decorator
347
# common threading-lock decorator
348
def with_lock(lockname, debug_name=None):
349
if debug_name is None:
350
debug_name = lockname
352
def _decorator(unbound):
353
def locked(self, *args, **kw):
354
getattr(self, lockname).acquire()
356
return unbound(self, *args, **kw)
358
getattr(self, lockname).release()
364
def strip_whitespace(f):
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)
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)))
481
381
def _f(*a, **kw):
482
382
from loggerhead.lsprof import profile
485
385
ret, stats = profile(f, *a, **kw)
486
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
487
int((time.time() - z) * 1000)))
386
log.debug('Finished profiled %s in %d msec.' % (f.__name__, int((time.time() - z) * 1000)))
490
389
now = time.time()
491
390
msec = int(now * 1000) % 1000
492
timestr = time.strftime('%Y%m%d%H%M%S',
493
time.localtime(now)) + ('%03d' % (msec,))
391
timestr = time.strftime('%Y%m%d%H%M%S', time.localtime(now)) + ('%03d' % msec)
494
392
filename = f.__name__ + '-' + timestr + '.lsprof'
495
393
cPickle.dump(stats, open(filename, 'w'), 2)
552
444
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
553
445
map.update(overrides)
557
class Reloader(object):
559
This class wraps all paste.reloader logic. All methods are @classmethod.
562
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
565
def _turn_sigterm_into_systemexit(cls):
567
Attempts to turn a SIGTERM exception into a SystemExit exception.
574
def handle_term(signo, frame):
576
signal.signal(signal.SIGTERM, handle_term)
579
def is_installed(cls):
580
return os.environ.get(cls._reloader_environ_key)
584
from paste import reloader
585
reloader.install(int(1))
588
def restart_with_reloader(cls):
589
"""Based on restart_with_monitor from paste.script.serve."""
590
print 'Starting subprocess with file monitor'
592
args = [sys.executable] + sys.argv
593
new_environ = os.environ.copy()
594
new_environ[cls._reloader_environ_key] = 'true'
598
cls._turn_sigterm_into_systemexit()
599
proc = subprocess.Popen(args, env=new_environ)
600
exit_code = proc.wait()
602
except KeyboardInterrupt:
603
print '^C caught in monitor process'
607
and getattr(os, 'kill', None) is not None):
610
os.kill(proc.pid, signal.SIGTERM)
611
except (OSError, IOError):
614
# Reloader always exits with code 3; but if we are
615
# a monitor, any exit code will restart
618
print '-'*20, 'Restarting', '-'*20
621
def convert_file_errors(application):
622
"""WSGI wrapper to convert some file errors to Paste exceptions"""
623
def new_application(environ, start_response):
625
return application(environ, start_response)
626
except (IOError, OSError), e:
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()
635
return new_application