19
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23
from xml.etree import ElementTree as ET
25
from elementtree import ElementTree as ET
27
from simpletal.simpleTALUtils import HTMLStructureCleaner
41
32
log = logging.getLogger("loggerhead.controllers")
38
return '%d years' % (int(delta.days // 365.25),)
40
return '%d days' % delta.days
46
seg.append('%d days' % delta.days)
47
hrs = delta.seconds // 3600
48
mins = (delta.seconds % 3600) // 60
53
seg.append('%d hours' % hrs)
57
seg.append('1 minute')
59
seg.append('%d minutes' % mins)
61
seg.append('less than a minute')
66
now = datetime.datetime.now()
67
return timespan(now - timestamp) + ' ago'
44
70
def fix_year(year):
53
# date_day -- just the day
54
# date_time -- full date with time
56
# displaydate -- for use in sentences
57
# approximatedate -- for use in tables
59
# displaydate and approximatedate return an elementtree <span> Element
60
# with the full date in a tooltip.
64
return value.strftime('%Y-%m-%d')
69
return value.strftime('%Y-%m-%d %T')
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)
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
87
future = delta < datetime.timedelta(0, 0, 0)
90
hours = delta.seconds / 3600
91
minutes = (delta.seconds - (3600*hours)) / 60
92
seconds = delta.seconds % 60
110
result += '%s %s' % (amount, unit)
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))
123
def approximatedate(date):
124
#FIXME: Returns an object instead of a string
125
return _wrap_with_date_time_title(date, _approximatedate(date))
128
def displaydate(date):
129
return _wrap_with_date_time_title(date, _displaydate(date))
132
78
class Container (object):
134
80
Convert a dict into an object with attributes.
137
82
def __init__(self, _dict=None, **kw):
138
83
if _dict is not None:
139
84
for key, value in _dict.iteritems():
140
85
setattr(self, key, value)
141
86
for key, value in kw.iteritems():
142
87
setattr(self, key, value)
144
89
def __repr__(self):
146
91
for key, value in self.__dict__.iteritems():
147
if key.startswith('_') or (getattr(self.__dict__[key],
148
'__call__', None) is not None):
92
if key.startswith('_') or (getattr(self.__dict__[key], '__call__', None) is not None):
150
94
out += '%r => %r, ' % (key, value)
99
def clean_revid(revid):
100
if revid == 'missing':
102
return sha.new(revid).hexdigest()
106
return ''.join([ '&#%d;' % ord(c) for c in text ])
155
109
def trunc(text, limit=10):
156
110
if len(text) <= limit:
158
112
return text[:limit] + '...'
116
if isinstance(s, unicode):
117
return s.encode('utf-8')
161
121
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
162
122
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
165
124
def hide_email(email):
167
126
try to obsure any email address in a bazaar committer's name.
181
140
return '%s at %s' % (username, domains[-2])
182
141
return '%s at %s' % (username, domains[0])
144
def triple_factors(min_value=1):
150
yield n * factors[index]
152
if index >= len(factors):
157
def scan_range(pos, max, pagesize=1):
159
given a position in a maximum range, return a list of negative and positive
160
jump factors for an hgweb-style triple-factor geometric scan.
162
for example, with pos=20 and max=500, the range would be:
163
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
165
i admit this is a very strange way of jumping through revisions. i didn't
169
for n in triple_factors(pagesize + 1):
185
178
# only do this if unicode turns out to be a problem
186
179
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
188
# FIXME: get rid of this method; use fixed_width() and avoid XML().
191
181
def html_clean(s):
193
183
clean up a string for html display. expand any tabs, encode any html
195
185
in displaying monospace text.
197
187
s = cgi.escape(s.expandtabs())
188
# s = _BADCHARS_RE.sub(lambda x: '&#%d;' % (ord(x.group(0)),), s)
198
189
s = s.replace(' ', ' ')
202
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
207
CSS is stupid. In some cases we need to replace an empty value with
208
a non breaking space ( ). There has to be a better way of doing this.
210
return: the same value recieved if not empty, and a ' ' if it is.
216
elif isinstance(s, int):
222
s = s.decode('utf-8')
223
except UnicodeDecodeError:
224
s = s.decode('iso-8859-15')
227
HSC = HTMLStructureCleaner()
231
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
234
if not isinstance(s, unicode):
235
# this kinda sucks. file contents are just binary data, and no
236
# encoding metadata is stored, so we need to guess. this is probably
237
# okay for most code, but for people using things like KOI-8, this
238
# will display gibberish. we have no way of detecting the correct
241
s = s.decode('utf-8')
242
except UnicodeDecodeError:
243
s = s.decode('iso-8859-15')
245
s = s.expandtabs().replace(' ', NONBREAKING_SPACE)
247
return HSC.clean(s).replace('\n', '<br/>')
250
193
def fake_permissions(kind, executable):
251
194
# fake up unix-style permissions given only a "kind" and executable bit
252
195
if kind == 'directory':
316
246
elif divisor == GIG:
321
def fill_in_navigation(navigation):
251
def fill_in_navigation(history, navigation):
323
253
given a navigation block (used by the template for the page header), fill
324
254
in useful calculated values.
326
if navigation.revid in navigation.revid_list: # XXX is this always true?
327
navigation.position = navigation.revid_list.index(navigation.revid)
256
navigation.position = history.get_revid_sequence(navigation.revid_list, navigation.revid)
257
if navigation.position is None:
329
258
navigation.position = 0
330
259
navigation.count = len(navigation.revid_list)
331
260
navigation.page_position = navigation.position // navigation.pagesize + 1
332
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
333
- 1)) // navigation.pagesize
261
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize - 1)) // navigation.pagesize
335
263
def get_offset(offset):
336
if (navigation.position + offset < 0) or (
337
navigation.position + offset > navigation.count - 1):
264
if (navigation.position + offset < 0) or (navigation.position + offset > navigation.count - 1):
339
266
return navigation.revid_list[navigation.position + offset]
341
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
342
268
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
343
269
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
344
prev_page_revno = navigation.history.get_revno(
345
navigation.prev_page_revid)
346
next_page_revno = navigation.history.get_revno(
347
navigation.next_page_revid)
348
start_revno = navigation.history.get_revno(navigation.start_revid)
350
params = {'filter_file_id': navigation.filter_file_id}
271
params = { 'file_id': navigation.file_id }
351
272
if getattr(navigation, 'query', None) is not None:
352
273
params['q'] = navigation.query
354
if getattr(navigation, 'start_revid', None) is not None:
355
params['start_revid'] = start_revno
275
params['start_revid'] = navigation.start_revid
357
277
if navigation.prev_page_revid:
358
navigation.prev_page_url = navigation.branch.context_url(
359
[navigation.scan_url, prev_page_revno], **params)
278
navigation.prev_page_url = turbogears.url([ navigation.scan_url, navigation.prev_page_revid ], **params)
360
279
if navigation.next_page_revid:
361
navigation.next_page_url = navigation.branch.context_url(
362
[navigation.scan_url, next_page_revno], **params)
365
def directory_breadcrumbs(path, is_root, view):
367
Generate breadcrumb information from the directory path given
369
The path given should be a path up to any branch that is currently being
373
path -- The path to convert into breadcrumbs
374
is_root -- Whether or not loggerhead is serving a branch at its root
375
view -- The type of view we are showing (files, changes etc)
377
# Is our root directory itself a branch?
379
if view == 'directory':
387
# Create breadcrumb trail for the path leading up to the branch
389
'dir_name': "(root)",
394
dir_parts = path.strip('/').split('/')
395
for index, dir_name in enumerate(dir_parts):
397
'dir_name': dir_name,
398
'path': '/'.join(dir_parts[:index + 1]),
401
# If we are not in the directory view, the last crumb is a branch,
402
# so we need to specify a view
403
if view != 'directory':
404
breadcrumbs[-1]['suffix'] = '/' + view
408
def branch_breadcrumbs(path, inv, view):
410
Generate breadcrumb information from the branch path given
412
The path given should be a path that exists within a branch
415
path -- The path to convert into breadcrumbs
416
inv -- Inventory to get file information from
417
view -- The type of view we are showing (files, changes etc)
419
dir_parts = path.strip('/').split('/')
420
inner_breadcrumbs = []
421
for index, dir_name in enumerate(dir_parts):
422
inner_breadcrumbs.append({
423
'dir_name': dir_name,
424
'file_id': inv.path2id('/'.join(dir_parts[:index + 1])),
425
'suffix': '/' + view,
427
return inner_breadcrumbs
280
navigation.next_page_url = turbogears.url([ navigation.scan_url, navigation.next_page_revid ], **params)
283
def log_exception(log):
284
for line in ''.join(traceback.format_exception(*sys.exc_info())).split('\n'):
430
288
def decorator(unbound):
432
289
def new_decorator(f):
434
291
g.__name__ = f.__name__
441
298
return new_decorator
444
# common threading-lock decorator
447
def with_lock(lockname, debug_name=None):
448
if debug_name is None:
449
debug_name = lockname
452
def _decorator(unbound):
454
def locked(self, *args, **kw):
455
getattr(self, lockname).acquire()
457
return unbound(self, *args, **kw)
459
getattr(self, lockname).release()
468
from loggerhead.lsprof import profile
471
ret, stats = profile(f, *a, **kw)
472
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
473
int((time.time() - z) * 1000)))
477
msec = int(now * 1000) % 1000
478
timestr = time.strftime('%Y%m%d%H%M%S',
479
time.localtime(now)) + ('%03d' % msec)
480
filename = f.__name__ + '-' + timestr + '.lsprof'
481
cPickle.dump(stats, open(filename, 'w'), 2)
486
# just thinking out loud here...
488
# so, when browsing around, there are 5 pieces of context, most optional:
490
# current location along the navigation path (while browsing)
491
# - starting revid (start_revid)
492
# the current beginning of navigation (navigation continues back to
493
# the original revision) -- this defines an 'alternate mainline'
494
# when the user navigates into a branch.
496
# the file being looked at
498
# if navigating the revisions that touched a file
500
# if navigating the revisions that matched a search query
502
# a previous revision to remember for future comparisons
504
# current revid is given on the url path. the rest are optional components
507
# other transient things can be set:
509
# to compare one revision to another, on /revision only
511
# for re-ordering an existing page by different sort
513
t_context = threading.local()
514
_valid = ('start_revid', 'file_id', 'filter_file_id', 'q', 'remember',
515
'compare_revid', 'sort')
518
def set_context(map):
519
t_context.map = dict((k, v) for (k, v) in map.iteritems() if k in _valid)
522
def get_context(**overrides):
524
Soon to be deprecated.
527
return a context map that may be overriden by specific values passed in,
528
but only contains keys from the list of valid context keys.
530
if 'clear' is set, only the 'remember' context value will be added, and
531
all other context will be omitted.
534
if overrides.get('clear', False):
535
map['remember'] = t_context.map.get('remember', None)
537
map.update(t_context.map)
538
overrides = dict((k, v) for (k, v) in overrides.iteritems() if k in _valid)
539
map.update(overrides)
543
class Reloader(object):
545
This class wraps all paste.reloader logic. All methods are @classmethod.
548
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
551
def _turn_sigterm_into_systemexit(self):
553
Attempts to turn a SIGTERM exception into a SystemExit exception.
560
def handle_term(signo, frame):
562
signal.signal(signal.SIGTERM, handle_term)
565
def is_installed(self):
566
return os.environ.get(self._reloader_environ_key)
570
from paste import reloader
571
reloader.install(int(1))
574
def restart_with_reloader(self):
575
"""Based on restart_with_monitor from paste.script.serve."""
576
print 'Starting subprocess with file monitor'
578
args = [sys.executable] + sys.argv
579
new_environ = os.environ.copy()
580
new_environ[self._reloader_environ_key] = 'true'
584
self._turn_sigterm_into_systemexit()
585
proc = subprocess.Popen(args, env=new_environ)
586
exit_code = proc.wait()
588
except KeyboardInterrupt:
589
print '^C caught in monitor process'
593
and hasattr(os, 'kill')):
596
os.kill(proc.pid, signal.SIGTERM)
597
except (OSError, IOError):
600
# Reloader always exits with code 3; but if we are
601
# a monitor, any exit code will restart
604
print '-'*20, 'Restarting', '-'*20
301
# global branch history & cache
304
_history_lock = threading.RLock()
308
global _history, _index
309
from loggerhead.history import History
311
config = get_config()
313
_history_lock.acquire()
315
if (_history is None) or _history.out_of_date():
316
log.debug('Reload branch history...')
317
if _history is not None:
318
_history.dont_use_cache()
319
_history = History.from_folder(config.get('folder'))
320
_history.use_cache(config.get('cachepath'))
323
_history_lock.release()
327
from loggerhead.textindex import TextIndex
329
config = get_config()
330
cachepath = config.get('cachepath', None)
331
if cachepath is None:
333
_history_lock.acquire()
336
_index = TextIndex(get_history(), config.get('cachepath'))
339
_history_lock.release()
344
def set_config(config):