14
17
# You should have received a copy of the GNU General Public License
15
18
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
# Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA
22
from __future__ import print_function
25
return '%d days' % delta.days
31
seg.append('%d days' % delta.days)
32
hrs = delta.seconds // 3600
33
mins = (delta.seconds % 3600) // 60
38
seg.append('%d hours' % hrs)
42
seg.append('1 minute')
44
seg.append('%d minutes' % mins)
46
seg.append('less than a minute')
50
class Container (object):
34
from xml.etree import ElementTree as ET
37
from breezy import urlutils
39
log = logging.getLogger("loggerhead.controllers")
51
# date_day -- just the day
52
# date_time -- full date with time (UTC)
54
# approximatedate -- for use in tables
56
# approximatedate return an elementtree <span> Element
57
# with the full date (UTC) in a tooltip.
61
return value.strftime('%Y-%m-%d')
66
# Note: this assumes that the value is UTC in some fashion.
67
return value.strftime('%Y-%m-%d %H:%M:%S UTC')
72
def _approximatedate(date):
75
delta = datetime.datetime.utcnow() - date
76
future = delta < datetime.timedelta(0, 0, 0)
78
years = delta.days // 365
79
months = delta.days // 30 # This is approximate.
81
hours = delta.seconds // 3600
82
minutes = (delta.seconds - (3600*hours)) / 60
83
seconds = delta.seconds % 60
107
result += '%s %s' % (int(amount), unit)
113
def _wrap_with_date_time_title(date, formatted_date):
114
elem = ET.Element("span")
115
elem.text = formatted_date
116
elem.set("title", date_time(date))
120
def approximatedate(date):
121
#FIXME: Returns an object instead of a string
122
return _wrap_with_date_time_title(date, _approximatedate(date))
125
class Container(object):
52
127
Convert a dict into an object with attributes.
54
130
def __init__(self, _dict=None, **kw):
131
self._properties = {}
55
132
if _dict is not None:
56
for key, value in _dict.iteritems():
133
for key, value in _dict.items():
57
134
setattr(self, key, value)
58
for key, value in kw.iteritems():
135
for key, value in kw.items():
59
136
setattr(self, key, value)
62
def clean_revid(revid):
63
if revid == 'missing':
65
return sha.new(revid).hexdigest()
69
return ''.join([ '&#%d;' % ord(c) for c in text ])
140
for key, value in self.__dict__.items():
141
if key.startswith('_') or (getattr(self.__dict__[key],
142
'__call__', None) is not None):
144
out += '%r => %r, ' % (key, value)
148
def __getattr__(self, attr):
149
"""Used for handling things that aren't already available."""
150
if attr.startswith('_') or attr not in self._properties:
151
raise AttributeError('No attribute: %s' % (attr,))
152
val = self._properties[attr](self, attr)
153
setattr(self, attr, val)
156
def _set_property(self, attr, prop_func):
157
"""Set a function that will be called when an attribute is desired.
159
We will cache the return value, so the function call should be
160
idempotent. We will pass 'self' and the 'attr' name when triggered.
162
if attr.startswith('_'):
163
raise ValueError("Cannot create properties that start with _")
164
self._properties[attr] = prop_func
167
def trunc(text, limit=10):
168
if len(text) <= limit:
170
return text[:limit] + '...'
72
173
STANDARD_PATTERN = re.compile(r'^(.*?)\s*<(.*?)>\s*$')
73
174
EMAIL_PATTERN = re.compile(r'[-\w\d\+_!%\.]+@[-\w\d\+_!%\.]+')
75
177
def hide_email(email):
77
try to obsure any email address in a bazaar committer's name.
179
try to obscure any email address in a bazaar committer's name.
79
181
m = STANDARD_PATTERN.search(email)
91
193
return '%s at %s' % (username, domains[-2])
92
194
return '%s at %s' % (username, domains[0])
100
yield n * factors[index]
102
if index >= len(factors):
107
def scan_range(pos, max):
109
given a position in a maximum range, return a list of negative and positive
110
jump factors for an hgweb-style triple-factor geometric scan.
112
for example, with pos=20 and max=500, the range would be:
113
[ -10, -3, -1, 1, 3, 10, 30, 100, 300 ]
115
i admit this is a very strange way of jumping through revisions. i didn't
119
for n in triple_factors():
196
def hide_emails(emails):
198
try to obscure any email address in a list of bazaar committers' names.
202
result.append(hide_email(email))
205
# only do this if unicode turns out to be a problem
206
#_BADCHARS_RE = re.compile(ur'[\u007f-\uffff]')
208
# Can't be a dict; & needs to be done first.
212
("'", "'"), # ' is defined in XML, but not HTML.
219
"""Transform dangerous (X)HTML characters into entities.
221
Like cgi.escape, except also escaping \" and '. This makes it safe to use
222
in both attribute and element content.
224
If you want to safely fill a format string with escaped values, use
227
for char, repl in html_entity_subs:
228
s = s.replace(char, repl)
232
def html_format(template, *args):
233
"""Safely format an HTML template string, escaping the arguments.
235
The template string must not be user-controlled; it will not be escaped.
237
return template % tuple(html_escape(arg) for arg in args)
240
# FIXME: get rid of this method; use fixed_width() and avoid XML().
244
clean up a string for html display. expand any tabs, encode any html
245
entities, and replace spaces with ' '. this is primarily for use
246
in displaying monospace text.
248
s = html_escape(s.expandtabs())
249
s = s.replace(' ', ' ')
253
NONBREAKING_SPACE = u'\N{NO-BREAK SPACE}'
258
CSS is stupid. In some cases we need to replace an empty value with
259
a non breaking space ( ). There has to be a better way of doing this.
261
return: the same value received if not empty, and a ' ' if it is.
265
elif isinstance(s, int):
269
elif isinstance(s, bytes):
271
s = s.decode('utf-8')
272
except UnicodeDecodeError:
273
s = s.decode('iso-8859-15')
275
elif isinstance(s, str):
283
expand tabs and turn spaces into "non-breaking spaces", so browsers won't
286
if not isinstance(s, str):
287
# this kinda sucks. file contents are just binary data, and no
288
# encoding metadata is stored, so we need to guess. this is probably
289
# okay for most code, but for people using things like KOI-8, this
290
# will display gibberish. we have no way of detecting the correct
293
s = s.decode('utf-8')
294
except UnicodeDecodeError:
295
s = s.decode('iso-8859-15')
297
s = html_escape(s).expandtabs().replace(' ', NONBREAKING_SPACE)
299
return bleach.clean(s).replace('\n', '<br/>')
302
def fake_permissions(kind, executable):
303
# fake up unix-style permissions given only a "kind" and executable bit
304
if kind == 'directory':
312
s = base64.encodestring(s).replace('\n', '')
313
while (len(s) > 0) and (s[-1] == '='):
320
turn a potentially long string into a unique smaller string.
324
uniqs[type(None)] = next = uniqs.get(type(None), 0) + 1
325
x = struct.pack('>I', next)
326
while (len(x) > 1) and (x[0] == '\x00'):
335
P95_MEG = int(0.9 * MEG)
336
P95_GIG = int(0.9 * GIG)
339
def human_size(size, min_divisor=0):
341
if (size == 0) and (min_divisor == 0):
343
if (size < 1024) and (min_divisor == 0):
344
return str(size) + ' bytes'
346
if (size >= P95_GIG) or (min_divisor >= GIG):
348
elif (size >= P95_MEG) or (min_divisor >= MEG):
355
dot = dot * 10 // divisor
362
if (base < 100) and (dot != 0):
363
out += '.%d' % (dot,)
373
def local_path_from_url(url):
374
"""Convert Bazaar URL to local path, ignoring readonly+ prefix"""
375
readonly_prefix = 'readonly+'
376
if url.startswith(readonly_prefix):
377
url = url[len(readonly_prefix):]
378
return urlutils.local_path_from_url(url)
381
def fill_in_navigation(navigation):
383
given a navigation block (used by the template for the page header), fill
384
in useful calculated values.
386
if navigation.revid in navigation.revid_list: # XXX is this always true?
387
navigation.position = navigation.revid_list.index(navigation.revid)
389
navigation.position = 0
390
navigation.count = len(navigation.revid_list)
391
navigation.page_position = navigation.position // navigation.pagesize + 1
392
navigation.page_count = (len(navigation.revid_list) + (navigation.pagesize\
393
- 1)) // navigation.pagesize
395
def get_offset(offset):
396
if (navigation.position + offset < 0) or (
397
navigation.position + offset > navigation.count - 1):
399
return navigation.revid_list[navigation.position + offset]
401
navigation.last_in_page_revid = get_offset(navigation.pagesize - 1)
402
navigation.prev_page_revid = get_offset(-1 * navigation.pagesize)
403
navigation.next_page_revid = get_offset(1 * navigation.pagesize)
404
prev_page_revno = navigation.history.get_revno(
405
navigation.prev_page_revid)
406
next_page_revno = navigation.history.get_revno(
407
navigation.next_page_revid)
408
start_revno = navigation.history.get_revno(navigation.start_revid)
410
params = {'filter_path': navigation.filter_path}
411
if getattr(navigation, 'query', None) is not None:
412
params['q'] = navigation.query
414
if getattr(navigation, 'start_revid', None) is not None:
415
params['start_revid'] = start_revno
417
if navigation.prev_page_revid:
418
navigation.prev_page_url = navigation.branch.context_url(
419
[navigation.scan_url, prev_page_revno], **params)
420
if navigation.next_page_revid:
421
navigation.next_page_url = navigation.branch.context_url(
422
[navigation.scan_url, next_page_revno], **params)
425
def directory_breadcrumbs(path, is_root, view):
427
Generate breadcrumb information from the directory path given
429
The path given should be a path up to any branch that is currently being
433
path -- The path to convert into breadcrumbs
434
is_root -- Whether or not loggerhead is serving a branch at its root
435
view -- The type of view we are showing (files, changes etc)
437
# Is our root directory itself a branch?
445
# Create breadcrumb trail for the path leading up to the branch
447
'dir_name': "(root)",
452
dir_parts = path.strip('/').split('/')
453
for index, dir_name in enumerate(dir_parts):
455
'dir_name': dir_name,
456
'path': '/'.join(dir_parts[:index + 1]),
459
# If we are not in the directory view, the last crumb is a branch,
460
# so we need to specify a view
461
if view != 'directory':
462
breadcrumbs[-1]['suffix'] = '/' + view
466
def branch_breadcrumbs(path, tree, view):
468
Generate breadcrumb information from the branch path given
470
The path given should be a path that exists within a branch
473
path -- The path to convert into breadcrumbs
474
tree -- Tree to get file information from
475
view -- The type of view we are showing (files, changes etc)
477
dir_parts = path.strip('/').split('/')
478
inner_breadcrumbs = []
479
for index, dir_name in enumerate(dir_parts):
480
inner_breadcrumbs.append({
481
'dir_name': dir_name,
482
'path': '/'.join(dir_parts[:index + 1]),
483
'suffix': '/' + view,
485
return inner_breadcrumbs
488
def decorator(unbound):
490
def new_decorator(f):
492
g.__name__ = f.__name__
493
g.__doc__ = f.__doc__
494
g.__dict__.update(f.__dict__)
496
new_decorator.__name__ = unbound.__name__
497
new_decorator.__doc__ = unbound.__doc__
498
new_decorator.__dict__.update(unbound.__dict__)
509
from .loggerhead.lsprof import profile
511
ret, stats = profile(f, *a, **kw)
512
log.debug('Finished profiled %s in %d msec.' % (f.__name__,
513
int((time.time() - z) * 1000)))
517
msec = int(now * 1000) % 1000
518
timestr = time.strftime('%Y%m%d%H%M%S',
519
time.localtime(now)) + ('%03d' % (msec,))
520
filename = f.__name__ + '-' + timestr + '.lsprof'
521
cPickle.dump(stats, open(filename, 'w'), 2)
526
# just thinking out loud here...
528
# so, when browsing around, there are 5 pieces of context, most optional:
530
# current location along the navigation path (while browsing)
531
# - starting revid (start_revid)
532
# the current beginning of navigation (navigation continues back to
533
# the original revision) -- this defines an 'alternate mainline'
534
# when the user navigates into a branch.
536
# if navigating the revisions that touched a file
538
# if navigating the revisions that matched a search query
540
# a previous revision to remember for future comparisons
542
# current revid is given on the url path. the rest are optional components
545
# other transient things can be set:
547
# to compare one revision to another, on /revision only
549
# for re-ordering an existing page by different sort
551
t_context = threading.local()
553
'start_revid', 'filter_path', 'q', 'remember', 'compare_revid', 'sort')
556
def set_context(map):
557
t_context.map = dict((k, v) for (k, v) in map.items() if k in _valid)
560
def get_context(**overrides):
562
Soon to be deprecated.
565
return a context map that may be overridden by specific values passed in,
566
but only contains keys from the list of valid context keys.
568
if 'clear' is set, only the 'remember' context value will be added, and
569
all other context will be omitted.
572
if overrides.get('clear', False):
573
map['remember'] = t_context.map.get('remember', None)
575
map.update(t_context.map)
576
overrides = dict((k, v) for (k, v) in overrides.items() if k in _valid)
577
map.update(overrides)
581
class Reloader(object):
583
This class wraps all paste.reloader logic. All methods are @classmethod.
586
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
589
def _turn_sigterm_into_systemexit(cls):
591
Attempts to turn a SIGTERM exception into a SystemExit exception.
598
def handle_term(signo, frame):
600
signal.signal(signal.SIGTERM, handle_term)
603
def is_installed(cls):
604
return os.environ.get(cls._reloader_environ_key)
608
from paste import reloader
609
reloader.install(int(1))
612
def restart_with_reloader(cls):
613
"""Based on restart_with_monitor from paste.script.serve."""
614
print('Starting subprocess with file monitor')
616
args = [sys.executable] + sys.argv
617
new_environ = os.environ.copy()
618
new_environ[cls._reloader_environ_key] = 'true'
622
cls._turn_sigterm_into_systemexit()
623
proc = subprocess.Popen(args, env=new_environ)
624
exit_code = proc.wait()
626
except KeyboardInterrupt:
627
print('^C caught in monitor process')
631
and getattr(os, 'kill', None) is not None):
634
os.kill(proc.pid, signal.SIGTERM)
635
except (OSError, IOError):
638
# Reloader always exits with code 3; but if we are
639
# a monitor, any exit code will restart
642
print('-'*20, 'Restarting', '-'*20)
645
def convert_file_errors(application):
646
"""WSGI wrapper to convert some file errors to Paste exceptions"""
647
def new_application(environ, start_response):
649
return application(environ, start_response)
650
except (IOError, OSError) as e:
653
from paste import httpexceptions
654
if e.errno == errno.ENOENT:
655
raise httpexceptions.HTTPNotFound()
656
elif e.errno == errno.EACCES:
657
raise httpexceptions.HTTPForbidden()
660
return new_application
663
def convert_to_json_ready(obj):
664
if isinstance(obj, Container):
665
d = obj.__dict__.copy()
668
elif isinstance(obj, bytes):
669
return obj.decode('UTF-8')
670
elif isinstance(obj, datetime.datetime):
671
return tuple(obj.utctimetuple())
672
raise TypeError(repr(obj) + " is not JSON serializable")