aboutsummaryrefslogtreecommitdiffstats
path: root/libbe
diff options
context:
space:
mode:
Diffstat (limited to 'libbe')
-rw-r--r--libbe/__init__.py1
-rw-r--r--libbe/bug.py46
-rw-r--r--libbe/bugdir.py22
-rw-r--r--libbe/command/__init__.py4
-rw-r--r--libbe/command/assign.py17
-rw-r--r--libbe/command/base.py45
-rw-r--r--libbe/command/comment.py2
-rw-r--r--libbe/command/commit.py30
-rw-r--r--libbe/command/depend.py4
-rw-r--r--libbe/command/diff.py1
-rw-r--r--libbe/command/due.py3
-rw-r--r--libbe/command/help.py1
-rw-r--r--libbe/command/html.py1091
-rw-r--r--libbe/command/import_xml.py12
-rw-r--r--libbe/command/init.py1
-rw-r--r--libbe/command/list.py2
-rw-r--r--libbe/command/merge.py3
-rw-r--r--libbe/command/new.py28
-rw-r--r--libbe/command/remove.py1
-rw-r--r--libbe/command/serve.py66
-rw-r--r--libbe/command/set.py52
-rw-r--r--libbe/command/severity.py29
-rw-r--r--libbe/command/show.py1
-rw-r--r--libbe/command/status.py10
-rw-r--r--libbe/command/subscribe.py3
-rw-r--r--libbe/command/tag.py3
-rw-r--r--libbe/command/util.py3
-rw-r--r--libbe/comment.py27
-rw-r--r--libbe/diff.py1
-rw-r--r--libbe/error.py3
-rw-r--r--libbe/storage/__init__.py3
-rw-r--r--libbe/storage/base.py3
-rw-r--r--libbe/storage/http.py14
-rw-r--r--libbe/storage/util/config.py1
-rw-r--r--libbe/storage/util/mapfile.py1
-rw-r--r--libbe/storage/util/properties.py3
-rw-r--r--libbe/storage/util/settings_object.py3
-rw-r--r--libbe/storage/util/upgrade.py3
-rw-r--r--libbe/storage/vcs/__init__.py3
-rw-r--r--libbe/storage/vcs/arch.py3
-rw-r--r--libbe/storage/vcs/base.py101
-rw-r--r--libbe/storage/vcs/bzr.py55
-rw-r--r--libbe/storage/vcs/darcs.py5
-rw-r--r--libbe/storage/vcs/git.py5
-rw-r--r--libbe/storage/vcs/hg.py19
-rw-r--r--libbe/storage/vcs/monotone.py5
-rw-r--r--libbe/ui/__init__.py3
-rw-r--r--libbe/ui/command_line.py75
-rw-r--r--libbe/ui/util/__init__.py3
-rw-r--r--libbe/ui/util/editor.py3
-rw-r--r--libbe/ui/util/pager.py3
-rw-r--r--libbe/ui/util/user.py39
-rw-r--r--libbe/util/__init__.py3
-rw-r--r--libbe/util/encoding.py8
-rw-r--r--libbe/util/id.py11
-rw-r--r--libbe/util/plugin.py1
-rw-r--r--libbe/util/subproc.py44
-rw-r--r--libbe/util/tree.py3
-rw-r--r--libbe/util/utility.py5
-rw-r--r--libbe/version.py3
60 files changed, 1148 insertions, 795 deletions
diff --git a/libbe/__init__.py b/libbe/__init__.py
index d5a8f2e..c57fd09 100644
--- a/libbe/__init__.py
+++ b/libbe/__init__.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/bug.py b/libbe/bug.py
index 6d3d836..be6f44d 100644
--- a/libbe/bug.py
+++ b/libbe/bug.py
@@ -1,5 +1,8 @@
-# Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
+# Robert Lehmann <mail@robertlehmann.de>
# Thomas Habets <thomas@habets.pp.se>
+# Valtteri Kokkoniemi <rvk@iki.fi>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
@@ -47,11 +50,6 @@ if libbe.TESTING == True:
import doctest
-class DiskAccessRequired (Exception):
- def __init__(self, goal):
- msg = "Cannot %s without accessing the disk" % goal
- Exception.__init__(self, msg)
-
### Define and describe valid bug categories
# Use a tuple of (category, description) tuples since we don't have
# ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
@@ -199,10 +197,19 @@ class Bug (settings_object.SavedSettingsObject):
def _get_time(self):
if self.time_string == None:
+ self._cached_time_string = None
+ self._cached_time = None
return None
- return utility.str_to_time(self.time_string)
+ if (not hasattr(self, '_cached_time_string')
+ or self.time_string != self._cached_time_string):
+ self._cached_time_string = self.time_string
+ self._cached_time = utility.str_to_time(self.time_string)
+ return self._cached_time
def _set_time(self, value):
- self.time_string = utility.time_to_str(value)
+ if not hasattr(self, '_cached_time') or value != self._cached_time:
+ self.time_string = utility.time_to_str(value)
+ self._cached_time_string = self.time_string
+ self._cached_time = value
time = property(fget=_get_time,
fset=_set_time,
doc="An integer version of .time_string")
@@ -290,6 +297,8 @@ class Bug (settings_object.SavedSettingsObject):
("Reporter", self._setting_attr_string("reporter")),
("Creator", self._setting_attr_string("creator")),
("Created", timestring)]
+ for estr in self.extra_strings:
+ info.append(('Extra string', estr))
longest_key_len = max([len(k) for k,v in info])
infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
@@ -337,7 +346,7 @@ class Bug (settings_object.SavedSettingsObject):
sep = '\n' + istring
return istring + sep.join(lines).rstrip('\n')
- def from_xml(self, xml_string, verbose=True):
+ def from_xml(self, xml_string, preserve_uuids=False, verbose=True):
u"""
Note: If a bug uuid is given, set .alt_id to it's value.
>>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
@@ -359,9 +368,13 @@ class Bug (settings_object.SavedSettingsObject):
>>> bugB.xml(show_comments=True) == xml
True
>>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE
- ['severity', 'status', 'creator', 'created', 'summary']
+ ['severity', 'status', 'creator', 'time', 'summary']
>>> len(list(bugB.comments()))
3
+ >>> bugC = Bug()
+ >>> bugC.from_xml(xml, preserve_uuids=True)
+ >>> bugC.uuid == bugA.uuid
+ True
"""
if type(xml_string) == types.UnicodeType:
xml_string = xml_string.strip().encode('unicode_escape')
@@ -383,7 +396,8 @@ class Bug (settings_object.SavedSettingsObject):
pass
elif child.tag == 'comment':
comm = comment.Comment(bug=self)
- comm.from_xml(child)
+ comm.from_xml(
+ child, preserve_uuids=preserve_uuids, verbose=verbose)
comments.append(comm)
continue
elif child.tag in tags:
@@ -392,9 +406,13 @@ class Bug (settings_object.SavedSettingsObject):
else:
text = xml.sax.saxutils.unescape(child.text)
text = text.decode('unicode_escape').strip()
- if child.tag == 'uuid':
+ if child.tag == 'uuid' and not preserve_uuids:
uuid = text
continue # don't set the bug's uuid tag.
+ elif child.tag == 'created':
+ self.time = utility.str_to_time(text)
+ self.explicit_attrs.append('time')
+ continue
elif child.tag == 'extra-string':
estrs.append(text)
continue # don't set the bug's extra_string yet.
@@ -632,8 +650,8 @@ class Bug (settings_object.SavedSettingsObject):
def load_settings(self, settings_mapfile=None):
if settings_mapfile == None:
- settings_mapfile = \
- self.storage.get(self.id.storage('values'), default='\n')
+ settings_mapfile = self.storage.get(
+ self.id.storage('values'), '\n')
try:
settings = mapfile.parse(settings_mapfile)
except mapfile.InvalidMapfileContents, e:
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index 9741239..c73e097 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -220,14 +220,18 @@ class BugDir (list, settings_object.SavedSettingsObject):
def uuids(self, use_cached_disk_uuids=True):
if use_cached_disk_uuids==False or not hasattr(self, '_uuids_cache'):
- self._uuids_cache = []
- # list bugs that are in storage
- if self.storage != None and self.storage.is_readable():
- child_uuids = libbe.util.id.child_uuids(
- self.storage.children(self.id.storage()))
- for id in child_uuids:
- self._uuids_cache.append(id)
- return list(set([bug.uuid for bug in self] + self._uuids_cache))
+ self._refresh_uuid_cache()
+ self._uuids_cache = self._uuids_cache.union([bug.uuid for bug in self])
+ return self._uuids_cache
+
+ def _refresh_uuid_cache(self):
+ self._uuids_cache = set()
+ # list bugs that are in storage
+ if self.storage != None and self.storage.is_readable():
+ child_uuids = libbe.util.id.child_uuids(
+ self.storage.children(self.id.storage()))
+ for id in child_uuids:
+ self._uuids_cache.add(id)
def _clear_bugs(self):
while len(self) > 0:
@@ -248,7 +252,7 @@ class BugDir (list, settings_object.SavedSettingsObject):
self.append(bg)
self._bug_map_gen()
if hasattr(self, '_uuids_cache') and not bg.uuid in self._uuids_cache:
- self._uuids_cache.append(bg.uuid)
+ self._uuids_cache.add(bg.uuid)
return bg
def remove_bug(self, bug):
diff --git a/libbe/command/__init__.py b/libbe/command/__init__.py
index 1cad096..b520f40 100644
--- a/libbe/command/__init__.py
+++ b/libbe/command/__init__.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
@@ -19,6 +20,7 @@
import base
UserError = base.UserError
+UsageError = base.UsageError
UnknownCommand = base.UnknownCommand
get_command = base.get_command
get_command_class = base.get_command_class
@@ -33,7 +35,7 @@ UnconnectedStorageGetter = base.UnconnectedStorageGetter
StorageCallbacks = base.StorageCallbacks
UserInterface = base.UserInterface
-__all__ = [UserError, UnknownCommand,
+__all__ = [UserError, UsageError, UnknownCommand,
get_command, get_command_class, commands,
Option, Argument, Command,
InputOutput, StdInputOutput, StringInputOutput,
diff --git a/libbe/command/assign.py b/libbe/command/assign.py
index 0b3f407..99a657b 100644
--- a/libbe/command/assign.py
+++ b/libbe/command/assign.py
@@ -1,6 +1,8 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Marien Zwart <marien.zwart@gmail.com>
+# Robert Lehmann <mail@robertlehmann.de>
# Thomas Gerigk <tgerigk@gmx.de>
# W. Trevor King <wking@drexel.edu>
#
@@ -73,11 +75,7 @@ class Assign (libbe.command.Command):
])
def _run(self, **params):
- assigned = params['assigned']
- if assigned == 'none':
- assigned = None
- elif assigned == '-':
- assigned = self._get_user_id()
+ assigned = parse_assigned(self, params['assigned'])
bugdir = self._get_bugdir()
for bug_id in params['bug-id']:
bug,dummy_comment = \
@@ -99,3 +97,12 @@ Special assigned strings:
"-" assign the bug to yourself
"none" un-assigns the bug
"""
+
+def parse_assigned(command, assigned):
+ """Standard processing for the 'assigned' Argument.
+ """
+ if assigned == 'none':
+ assigned = None
+ elif assigned == '-':
+ assigned = command._get_user_id()
+ return assigned
diff --git a/libbe/command/base.py b/libbe/command/base.py
index b5f5a22..11835ee 100644
--- a/libbe/command/base.py
+++ b/libbe/command/base.py
@@ -1,4 +1,6 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# Robert Lehmann <mail@robertlehmann.de>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
@@ -27,13 +29,41 @@ import libbe.ui.util.user
import libbe.util.encoding
import libbe.util.plugin
-class UserError(Exception):
+
+class UserError (Exception):
+ "An error due to improper BE usage."
pass
-class UnknownCommand(UserError):
- def __init__(self, cmd):
- Exception.__init__(self, "Unknown command '%s'" % cmd)
- self.cmd = cmd
+
+class UsageError (UserError):
+ """A serious parsing error due to invalid BE command construction.
+
+ The distinction between `UserError`\s and the more specific
+ `UsageError`\s is that when displaying a `UsageError` to the user,
+ the user is pointed towards the command usage information. Use
+ the more general `UserError` if you feel that usage information
+ would not be particularly enlightening.
+ """
+ def __init__(self, command=None, command_name=None, message=None):
+ super(UsageError, self).__init__(message)
+ self.command = command
+ if command_name is None and command is not None:
+ command_name = command.name
+ self.command_name = command_name
+ self.message = message
+
+
+class UnknownCommand (UsageError):
+ def __init__(self, command_name, message=None):
+ uc_message = "Unknown command '%s'" % command_name
+ if message is None:
+ message = uc_message
+ else:
+ message = '%s\n(%s)' % (uc_message, message)
+ super(UnknownCommand, self).__init__(
+ command_name=command_name,
+ message=message)
+
def get_command(command_name):
"""Retrieves the module for a user command
@@ -43,6 +73,7 @@ def get_command(command_name):
... except UnknownCommand, e:
... print e
Unknown command 'asdf'
+ (No module named asdf)
>>> repr(get_command('list')).startswith("<module 'libbe.command.list' from ")
True
"""
@@ -50,7 +81,7 @@ def get_command(command_name):
cmd = libbe.util.plugin.import_by_name(
'libbe.command.%s' % command_name.replace("-", "_"))
except ImportError, e:
- raise UnknownCommand(command_name)
+ raise UnknownCommand(command_name, message=unicode(e))
return cmd
def get_command_class(module=None, command_name=None):
diff --git a/libbe/command/comment.py b/libbe/command/comment.py
index d182840..7fa6ec7 100644
--- a/libbe/command/comment.py
+++ b/libbe/command/comment.py
@@ -1,5 +1,7 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
+# Robert Lehmann <mail@robertlehmann.de>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/command/commit.py b/libbe/command/commit.py
index a2ed051..8416107 100644
--- a/libbe/command/commit.py
+++ b/libbe/command/commit.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
@@ -59,16 +60,19 @@ class Commit (libbe.command.Command):
])
self.args.extend([
libbe.command.Argument(
- name='comment', metavar='COMMENT', default=None),
+ name='summary', metavar='SUMMARY', default=None,
+ optional=True),
])
def _run(self, **params):
- if params['comment'] == '-': # read summary from stdin
+ if params['summary'] == '-': # read summary from stdin
assert params['body'] != 'EDITOR', \
'Cannot spawn and editor when the summary is using stdin.'
summary = sys.stdin.readline()
else:
- summary = params['comment']
+ summary = params['summary']
+ if summary == None and params['body'] == None:
+ params['body'] = 'EDITOR'
storage = self._get_storage()
if params['body'] == None:
body = None
@@ -79,6 +83,13 @@ class Commit (libbe.command.Command):
self._check_restricted_access(storage, params['body'])
body = libbe.util.encoding.get_file_contents(
params['body'], decode=True)
+ if summary == None: # use the first body line as the summary
+ if body == None:
+ raise libbe.command.UserError(
+ 'cannot commit without a summary')
+ lines = body.splitlines()
+ summary = lines[0]
+ body = '\n'.join(lines[1:]).strip() + '\n'
try:
revision = storage.commit(summary, body=body,
allow_empty=params['allow-empty'])
@@ -89,7 +100,12 @@ class Commit (libbe.command.Command):
def _long_help(self):
return """
-Commit the current repository status. The summary specified on the
-commandline is a string (only one line) that describes the commit
-briefly or "-", in which case the string will be read from stdin.
+Commit the current repository status.
+
+The summary specified on the commandline is a string (only one line)
+that describes the commit briefly or "-", in which case the string
+will be read from stdin. If no summary is given, the first line from
+the body message is used instead. If no summary or body is given, we
+spawn an editor without needing the special "EDITOR" value for the
+"--body" option.
"""
diff --git a/libbe/command/depend.py b/libbe/command/depend.py
index 9ae449a..1aa5053 100644
--- a/libbe/command/depend.py
+++ b/libbe/command/depend.py
@@ -1,4 +1,6 @@
-# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
+# Robert Lehmann <mail@robertlehmann.de>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/command/diff.py b/libbe/command/diff.py
index 08a7efb..a9cdd50 100644
--- a/libbe/command/diff.py
+++ b/libbe/command/diff.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
diff --git a/libbe/command/due.py b/libbe/command/due.py
index e4fd0f1..cf1500d 100644
--- a/libbe/command/due.py
+++ b/libbe/command/due.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
diff --git a/libbe/command/help.py b/libbe/command/help.py
index e4825f0..01eae5c 100644
--- a/libbe/command/help.py
+++ b/libbe/command/help.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Marien Zwart <marien.zwart@gmail.com>
# Thomas Gerigk <tgerigk@gmx.de>
diff --git a/libbe/command/html.py b/libbe/command/html.py
index bb5b554..7420ce8 100644
--- a/libbe/command/html.py
+++ b/libbe/command/html.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# Mathieu Clabaut <mathieu.clabaut@gmail.com>
# W. Trevor King <wking@drexel.edu>
#
@@ -26,6 +27,8 @@ import string
import time
import xml.sax.saxutils
+from jinja2 import Environment, FileSystemLoader, DictLoader, ChoiceLoader
+
import libbe
import libbe.command
import libbe.command.util
@@ -82,12 +85,12 @@ class HTML (libbe.command.Command):
help='Set the bug repository title (%default)',
arg=libbe.command.Argument(
name='title', metavar='STRING',
- default='BugsEverywhere Issue Tracker')),
+ default='Bugs Everywhere Issue Tracker')),
libbe.command.Option(name='index-header',
help='Set the index page headers (%default)',
arg=libbe.command.Argument(
name='index-header', metavar='STRING',
- default='BugsEverywhere Bug List')),
+ default='Bugs Everywhere Bug List')),
libbe.command.Option(name='export-template', short_name='e',
help='Export the default template and exit.'),
libbe.command.Option(name='export-template-dir', short_name='d',
@@ -112,9 +115,9 @@ class HTML (libbe.command.Command):
bugdir = self._get_bugdir()
bugdir.load_all_bugs()
html_gen = HTMLGen(bugdir,
- template=params['template-dir'],
+ template_dir=params['template-dir'],
title=params['title'],
- index_header=params['index-header'],
+ header=params['index-header'],
min_id_length=params['min-id-length'],
verbose=params['verbose'],
stdout=self.stdout)
@@ -132,28 +135,22 @@ directory.
Html = HTML # alias for libbe.command.base.get_command_class()
class HTMLGen (object):
- def __init__(self, bd, template=None,
- title="Site Title", index_header="Index Header",
+ def __init__(self, bd, template_dir=None,
+ title="Site Title", header="Header",
min_id_length=-1,
verbose=False, encoding=None, stdout=None,
):
self.generation_time = time.ctime()
self.bd = bd
- if template == None:
- self.template = "default"
- else:
- self.template = os.path.abspath(os.path.expanduser(template))
self.title = title
- self.index_header = index_header
+ self.header = header
self.verbose = verbose
self.stdout = stdout
if encoding != None:
self.encoding = encoding
else:
self.encoding = libbe.util.encoding.get_filesystem_encoding()
- self._load_default_templates()
- if template != None:
- self._load_user_templates()
+ self._load_templates(template_dir)
self.min_id_length = min_id_length
def run(self, out_dir):
@@ -179,21 +176,24 @@ class HTMLGen (object):
self._create_output_directories(out_dir)
self._write_css_file()
for b in bugs:
- if b.active:
+ if b.severity == 'target':
+ up_link = '../../index_target.html'
+ elif b.active:
up_link = '../../index.html'
else:
- up_link = '../../index_inactive.html'
-
- self._write_bug_file(b, up_link)
+ up_link = '../../index_inactive.html'
+ self._write_bug_file(
+ b, title=self.title, header=self.header,
+ up_link=up_link)
self._write_index_file(
bugs_active, title=self.title,
- index_header=self.index_header, bug_type='active')
+ header=self.header, bug_type='active')
self._write_index_file(
bugs_inactive, title=self.title,
- index_header=self.index_header, bug_type='inactive')
+ header=self.header, bug_type='inactive')
self._write_index_file(
bugs_target, title=self.title,
- index_header=self.index_header, bug_type='target')
+ header=self.header, bug_type='target')
def _truncated_bug_id(self, bug):
return libbe.util.id._truncate(
@@ -217,10 +217,10 @@ class HTMLGen (object):
print >> self.stdout, 'Writing css file'
assert hasattr(self, 'out_dir'), \
'Must run after ._create_output_directories()'
- self._write_file(self.css_file,
- [self.out_dir,'style.css'])
+ template = self.template.get_template('style.css')
+ self._write_file(template.render(), [self.out_dir, 'style.css'])
- def _write_bug_file(self, bug, up_link):
+ def _write_bug_file(self, bug, title, header, up_link):
if self.verbose:
print >> self.stdout, '\tCreating bug file for %s' % bug.id.user()
assert hasattr(self, 'out_dir_bugs'), \
@@ -229,96 +229,77 @@ class HTMLGen (object):
if bug.active == True:
index_type = 'Active'
- else :
+ else:
index_type = 'Inactive'
if bug.severity == 'target':
index_type = 'Target'
bug.load_comments(load_full=True)
- comment_entries = self._generate_bug_comment_entries(bug)
+ bug.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
dirname = self._truncated_bug_id(bug)
fullpath = os.path.join(self.out_dir_bugs, dirname, 'index.html')
- template_info = {'title':self.title,
- 'charset':self.encoding,
- 'up_link':up_link,
- 'shortname':bug.id.user(),
- 'comment_entries':comment_entries,
- 'generation_time':self.generation_time,
- 'index_type':index_type}
- for attr in ['uuid', 'severity', 'status', 'assigned',
- 'reporter', 'creator', 'time_string', 'summary']:
- template_info[attr] = self._escape(getattr(bug, attr))
+ template_info = {
+ 'title': title,
+ 'charset': self.encoding,
+ 'stylesheet': '../../style.css',
+ 'header': header,
+ 'backlinks': self.template.get_template('bug_backlinks.html'),
+ 'up_link': up_link,
+ 'index_type': index_type,
+ 'bug': bug,
+ 'comment_entry': self.template.get_template(
+ 'bug_comment_entry.html'),
+ 'comments': [(depth,comment) for depth,comment
+ in bug.comment_root.thread(flatten=False)],
+ 'comment_dir': self._truncated_comment_id,
+ 'format_body': self._format_comment_body,
+ 'div_close': _DivCloser(),
+ 'generation_time': self.generation_time,
+ }
fulldir = os.path.join(self.out_dir_bugs, dirname)
if not os.path.exists(fulldir):
os.mkdir(fulldir)
- self._write_file(self.bug_file % template_info, [fullpath])
+ template = self.template.get_template('bug.html')
+ self._write_file(template.render(template_info), [fullpath])
- def _generate_bug_comment_entries(self, bug):
- assert hasattr(self, 'out_dir_bugs'), \
- 'Must run after ._create_output_directories()'
+ def _write_index_file(self, bugs, title, header, bug_type='active'):
+ if self.verbose:
+ print >> self.stdout, 'Writing %s index file for %d bugs' % (bug_type, len(bugs))
+ assert hasattr(self, 'out_dir'), 'Must run after ._create_output_directories()'
- stack = []
- comment_entries = []
- bug.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
- for depth,comment in bug.comment_root.thread(flatten=False):
- while len(stack) > depth:
- # pop non-parents off the stack
- stack.pop(-1)
- # close non-parent <div class="comment...
- comment_entries.append('</div>\n')
- assert len(stack) == depth
- stack.append(comment)
- template_info = {
- 'shortname': comment.id.user(),
- 'truncated_id': self._truncated_comment_id(comment)}
- if depth == 0:
- comment_entries.append('<div class="comment root">')
- else:
- comment_entries.append(
- '<div class="comment" id="%s">'
- % template_info['truncated_id'])
- for attr in ['uuid', 'author', 'date', 'body']:
- value = getattr(comment, attr)
- if attr == 'body':
- link_long_ids = False
- save_body = False
- if comment.content_type == 'text/html':
- link_long_ids = True
- elif comment.content_type.startswith('text/'):
- value = '<pre>\n'+self._escape(value)+'\n</pre>'
- link_long_ids = True
- elif comment.content_type.startswith('image/'):
- save_body = True
- value = '<img src="./%s/%s" />' \
- % (self._truncated_bug_id(bug),
- self._truncated_comment_id(comment))
- else:
- save_body = True
- value = '<a href="./%s/%s">Link to %s file</a>.' \
- % (self._truncated_bug_id(bug),
- self._truncated_comment_id(comment),
- comment.content_type)
- if link_long_ids == True:
- value = self._long_to_linked_user(value)
- if save_body == True:
- per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
- if not os.path.exists(per_bug_dir):
- os.mkdir(per_bug_dir)
- comment_path = os.path.join(per_bug_dir, comment.uuid)
- self._write_file(
- '<Files %s>\n ForceType %s\n</Files>' \
- % (comment.uuid, comment.content_type),
- [per_bug_dir, '.htaccess'], mode='a')
- self._write_file(comment.body,
- [per_bug_dir, comment.uuid], mode='wb')
- else:
- value = self._escape(value)
- template_info[attr] = value
- comment_entries.append(self.bug_comment_entry % template_info)
- while len(stack) > 0:
- stack.pop(-1)
- comment_entries.append('</div>\n') # close every remaining <div class='comment...
- return '\n'.join(comment_entries)
+ if bug_type == 'active':
+ filename = 'index.html'
+ elif bug_type == 'inactive':
+ filename = 'index_inactive.html'
+ elif bug_type == 'target':
+ filename = 'index_by_target.html'
+ else:
+ raise ValueError('unrecognized bug_type: "%s"' % bug_type)
+
+ template_info = {
+ 'title': title,
+ 'charset': self.encoding,
+ 'stylesheet': 'style.css',
+ 'header': header,
+ 'active_class': 'tab nsel',
+ 'inactive_class': 'tab nsel',
+ 'target_class': 'tab nsel',
+ 'bugs': bugs,
+ 'bug_entry': self.template.get_template('index_bug_entry.html'),
+ 'bug_dir': self._truncated_bug_id,
+ 'generation_time': self.generation_time,
+ }
+ template_info['%s_class' % bug_type] = 'tab sel'
+ if bug_type == 'target':
+ template = self.template.get_template('target_index.html')
+ template_info['targets'] = [
+ (target, sorted(libbe.command.depend.get_blocked_by(
+ self.bd, target)))
+ for target in bugs]
+ else:
+ template = self.template.get_template('standard_index.html')
+ self._write_file(
+ template.render(template_info)+'\n', [self.out_dir,filename])
def _long_to_linked_user(self, text):
"""
@@ -382,91 +363,46 @@ class HTMLGen (object):
raise Exception('Invalid id type %s for "%s"'
% (p['type'], long_id))
- def _write_index_file(self, bugs, title, index_header, bug_type='active'):
- if self.verbose:
- print >> self.stdout, 'Writing %s index file for %d bugs' % (bug_type, len(bugs))
- assert hasattr(self, 'out_dir'), 'Must run after ._create_output_directories()'
- esc = self._escape
- if (bug_type == 'target'):
- bug_entries = self._generate_index_bug_entries_target(bugs)
+ def _format_comment_body(self, bug, comment):
+ link_long_ids = False
+ save_body = False
+ value = comment.body
+ if comment.content_type == 'text/html':
+ link_long_ids = True
+ elif comment.content_type.startswith('text/'):
+ value = '<pre>\n'+self._escape(value)+'\n</pre>'
+ link_long_ids = True
+ elif comment.content_type.startswith('image/'):
+ save_body = True
+ value = '<img src="./%s/%s" />' % (
+ self._truncated_bug_id(bug),
+ self._truncated_comment_id(comment))
else:
- bug_entries = self._generate_index_bug_entries(bugs)
-
- if bug_type == 'active':
- filename = 'index.html'
- elif bug_type == 'inactive':
- filename = 'index_inactive.html'
- elif bug_type == 'target':
- filename = 'index_by_target.html'
- else:
- raise Exception, 'Unrecognized bug_type: "%s"' % bug_type
- template_info = {'title':title,
- 'index_header':index_header,
- 'charset':self.encoding,
- 'active_class':'tab sel',
- 'inactive_class':'tab nsel',
- 'target_class':'tab nsel',
- 'bug_entries':bug_entries,
- 'generation_time':self.generation_time}
- if bug_type == 'inactive':
- template_info['active_class'] = 'tab nsel'
- template_info['inactive_class'] = 'tab sel'
- if bug_type == 'target':
- template_info['active_class'] = 'tab nsel'
- template_info['target_class'] = 'tab sel'
- self._write_file(self.index_file % template_info,
- [self.out_dir, filename])
-
- def _generate_index_bug_entries_target(self, targets):
-
- target_entries = []
- for target in targets:
- bug_entries = []
- template_info_list = {'target':target.summary, 'bug_entries': '', 'status': target.status}
- blocker = libbe.command.depend.get_blocked_by(self.bd, target)
- for bug in blocker:
- if self.verbose:
- print >> self.stdout, '\tCreating bug entry for %s' % bug.id.user()
- template_info = {'shortname':bug.id.user()}
- for attr in ['uuid', 'severity', 'status', 'assigned',
- 'reporter', 'creator', 'time_string', 'summary']:
- template_info[attr] = self._escape(getattr(bug, attr))
- template_info['dir'] = self._truncated_bug_id(bug)
- bug_entries.append(self.index_bug_entry % template_info)
- template_info_list['bug_entries'] = '\n'.join(bug_entries)
- target_entries.append(self.target_bug_list % template_info_list)
- return '\n'.join(target_entries)
-
- def _generate_index_bug_entries(self, bugs):
- bug_entries = []
- template_info_list = {'bug_entries': ''}
- for bug in bugs:
- if self.verbose:
- print >> self.stdout, '\tCreating bug entry for %s' % bug.id.user()
- template_info = {'shortname':bug.id.user()}
- for attr in ['uuid', 'severity', 'status', 'assigned',
- 'reporter', 'creator', 'time_string', 'summary']:
- template_info[attr] = self._escape(getattr(bug, attr))
- template_info['dir'] = self._truncated_bug_id(bug)
- bug_entries.append(self.index_bug_entry % template_info)
- template_info_list['bug_entries'] = '\n'.join(bug_entries)
- return self.bug_list % template_info_list
+ save_body = True
+ value = '<a href="./%s/%s">Link to %s file</a>.' % (
+ self._truncated_bug_id(bug),
+ self._truncated_comment_id(comment),
+ comment.content_type)
+ if link_long_ids == True:
+ value = self._long_to_linked_user(value)
+ if save_body == True:
+ per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
+ if not os.path.exists(per_bug_dir):
+ os.mkdir(per_bug_dir)
+ comment_path = os.path.join(per_bug_dir, comment.uuid)
+ self._write_file(
+ '<Files %s>\n ForceType %s\n</Files>' \
+ % (comment.uuid, comment.content_type),
+ [per_bug_dir, '.htaccess'], mode='a')
+ self._write_file(comment.body,
+ [per_bug_dir, comment.uuid], mode='wb')
+ return value
def _escape(self, string):
if string == None:
return ''
return xml.sax.saxutils.escape(string)
- def _load_user_templates(self):
- for filename,attr in [('style.css','css_file'),
- ('index_file.tpl','index_file'),
- ('index_bug_entry.tpl','index_bug_entry'),
- ('bug_file.tpl','bug_file'),
- ('bug_comment_entry.tpl','bug_comment_entry')]:
- fullpath = os.path.join(self.template, filename)
- if os.path.exists(fullpath):
- setattr(self, attr, self._read_file([fullpath]))
-
def _make_dir(self, dir_path):
dir_path = os.path.abspath(os.path.expanduser(dir_path))
if not os.path.exists(dir_path):
@@ -489,388 +425,425 @@ class HTMLGen (object):
if self.verbose:
print >> self.stdout, 'Creating output directories'
self.out_dir = self._make_dir(out_dir)
- if self.verbose:
- print >> self.stdout, 'Creating css file'
- self._write_css_file()
- if self.verbose:
- print >> self.stdout, 'Creating index_file.tpl file'
- self._write_file(self.index_file,
- [self.out_dir, 'index_file.tpl'])
- if self.verbose:
- print >> self.stdout, 'Creating index_bug_entry.tpl file'
- self._write_file(self.index_bug_entry,
- [self.out_dir, 'index_bug_entry.tpl'])
- if self.verbose:
- print >> self.stdout, 'Creating bug_file.tpl file'
- self._write_file(self.bug_file,
- [self.out_dir, 'bug_file.tpl'])
- if self.verbose:
- print >> self.stdout, 'Creating bug_comment_entry.tpl file'
- self._write_file(self.bug_comment_entry,
- [self.out_dir, 'bug_comment_entry.tpl'])
-
- def _load_default_templates(self):
- self.css_file = """
- body {
- font-family: "lucida grande", "sans serif";
- font-size: 14px;
- color: #333;
- width: auto;
- margin: auto;
- }
-
- div.main {
- padding: 20px;
- margin: auto;
- padding-top: 0;
- margin-top: 1em;
- background-color: #fcfcfc;
- -moz-border-radius: 10px;
-
- }
-
- div.footer {
- font-size: small;
- padding-left: 20px;
- padding-right: 20px;
- padding-top: 5px;
- padding-bottom: 5px;
- margin: auto;
- background: #305275;
- color: #fffee7;
- -moz-border-radius: 10px;
- }
-
- div.header {
- font-size: xx-large;
- padding-left: 20px;
- padding-right: 20px;
- padding-top: 10px;
- font-weight:bold;
- padding-bottom: 10px;
- background: #305275;
- color: #fffee7;
- -moz-border-radius: 10px;
- }
-
- div.target_name {
- border: 1px solid;
- border-color: #305275;
- background-color: #305275;
- color: #fff;
- width: auto%;
- -moz-border-radius-topleft: 8px;
- -moz-border-radius-topright: 8px;
- padding-left: 5px;
- padding-right: 5px;
- }
-
- table {
- border-style: solid;
- border: 1px #c3d9ff;
- border-spacing: 0px 0px;
- width: auto;
- padding: 0px;
-
- }
-
- tb { border: 1px; }
-
- tr {
- vertical-align: top;
- border: 1px #c3d9ff;
- border-style: dotted;
- width: auto;
- padding: 0px;
- }
-
- th {
- border-width: 1px;
- border-style: solid;
- border-color: #c3d9ff;
- border-collapse: collapse;
- padding-left: 5px;
- padding-right: 5px;
- }
-
-
- td {
- border-width: 1px;
- border-color: #c3d9ff;
- border-collapse: collapse;
- padding-left: 5px;
- padding-right: 5px;
- width: auto%;
- }
-
- img { border-style: none; }
-
- h1 {
- padding: 0.5em;
- background-color: #305275;
- margin-top: 0;
- margin-bottom: 0;
- color: #fff;
- margin-left: -20px;
- margin-right: -20px;
- }
-
- ul {
- list-style-type: none;
- padding: 0;
- }
-
- p { width: auto; }
-
- a, a:visited {
- background: inherit;
- text-decoration: none;
- }
-
- a { color: #003d41; }
- a:visited { color: #553d41; }
- .footer a { color: #508d91; }
-
- /* bug index pages */
-
- td.tab {
- padding-right: 1em;
- padding-left: 1em;
- }
-
- td.sel.tab {
- background-color: #c3d9ff ;
- border: 1px solid #c3d9ff;
- font-weight:bold;
- -moz-border-radius-topleft: 15px;
- -moz-border-radius-topright: 15px;
- }
-
- td.nsel.tab {
- border: 1px solid #c3d9ff;
- font-weight:bold;
- -moz-border-radius-topleft: 5px;
- -moz-border-radius-topright: 5px;
- }
-
- table.bug_list {
- border-width: 1px;
- border-style: solid;
- border-color: #c3d9ff;
- padding: 0px;
- width: 100%;
- border: 1px solid #c3d9ff;
- }
-
- table.target_list {
- border-width: 1px;
- border-style: solid;
- border-collapse: collapse;
- border-color: #c3d9ff;
- padding: 0px;
- width: 100%;
- margin-bottom: 10px;
- }
-
- table.target_list.td {
- border-width: 1px;
- }
-
- tr.wishlist { background-color: #DCFAFF;}
- tr.wishlist:hover { background-color: #C2DCE1; }
-
- tr.minor { background-color: #FFFFA6; }
- tr.minor:hover { background-color: #E6E696; }
-
- tr.serious { background-color: #FF9077;}
- tr.serious:hover { background-color: #E6826B; }
-
- tr.critical { background-color: #FF752A; }
- tr.critical:hover { background-color: #D63905;}
-
- tr.fatal { background-color: #FF3300;}
- tr.fatal:hover { background-color: #D60000;}
-
- td.uuid { width: 5%; border-style: dotted;}
- td.status { width: 5%; border-style: dotted;}
- td.severity { width: 5%; border-style: dotted;}
- td.summary { border-style: dotted;}
- td.date { width: 25%; border-style: dotted;}
-
- /* bug detail pages */
-
- td.bug_detail_label { text-align: right; border: none;}
- td.bug_detail { border: none;}
- td.bug_comment_label { text-align: right; vertical-align: top; }
- td.bug_comment { }
-
- div.comment {
- padding: 20px;
- padding-top: 20px;
- margin: auto;
- margin-top: 0;
+ for filename,text in self.template_dict.iteritems():
+ if self.verbose:
+ print >> self.stdout, 'Creating %s file'
+ self._write_file(text, [self.out_dir, filename])
+
+ def _load_templates(self, template_dir=None):
+ if template_dir is not None:
+ template_dir = os.path.abspath(os.path.expanduser(template_dir))
+
+ self.template_dict = {
+##
+ 'style.css':
+"""body {
+ font-family: "lucida grande", "sans serif";
+ font-size: 14px;
+ color: #333;
+ width: auto;
+ margin: auto;
+}
+
+div.main {
+ padding: 20px;
+ margin: auto;
+ padding-top: 0;
+ margin-top: 1em;
+ background-color: #fcfcfc;
+ -moz-border-radius: 10px;
+
+}
+
+div.footer {
+ font-size: small;
+ padding-left: 20px;
+ padding-right: 20px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ margin: auto;
+ background: #305275;
+ color: #fffee7;
+ -moz-border-radius: 10px;
+}
+
+div.header {
+ font-size: xx-large;
+ padding-left: 20px;
+ padding-right: 20px;
+ padding-top: 10px;
+ font-weight:bold;
+ padding-bottom: 10px;
+ background: #305275;
+ color: #fffee7;
+ -moz-border-radius: 10px;
+}
+
+th.target_name {
+ text-align:left;
+ border: 1px solid;
+ border-color: #305275;
+ background-color: #305275;
+ color: #fff;
+ width: auto%;
+ -moz-border-radius-topleft: 8px;
+ -moz-border-radius-topright: 8px;
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+table {
+ border-style: solid;
+ border: 1px #c3d9ff;
+ border-spacing: 0px 0px;
+ width: auto;
+ padding: 0px;
+
+ }
+
+tb { border: 1px; }
+
+tr {
+ vertical-align: top;
+ border: 1px #c3d9ff;
+ border-style: dotted;
+ width: auto;
+ padding: 0px;
+}
+
+th {
+ border-width: 1px;
+ border-style: solid;
+ border-color: #c3d9ff;
+ border-collapse: collapse;
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+
+td {
+ border-width: 1px;
+ border-color: #c3d9ff;
+ border-collapse: collapse;
+ padding-left: 5px;
+ padding-right: 5px;
+ width: auto%;
+}
+
+img { border-style: none; }
+
+ul {
+ list-style-type: none;
+ padding: 0;
+}
+
+p { width: auto; }
+
+p.backlink {
+ width: auto;
+ font-weight: bold;
+}
+
+a {
+ background: inherit;
+ text-decoration: none;
+}
+
+a { color: #553d41; }
+a:hover { color: #003d41; }
+a:visited { color: #305275; }
+.footer a { color: #508d91; }
+
+/* bug index pages */
+
+td.tab {
+ padding-right: 1em;
+ padding-left: 1em;
+}
+
+td.sel.tab {
+ background-color: #c3d9ff ;
+ border: 1px solid #c3d9ff;
+ font-weight:bold;
+ -moz-border-radius-topleft: 15px;
+ -moz-border-radius-topright: 15px;
+}
+
+td.nsel.tab {
+ border: 1px solid #c3d9ff;
+ font-weight:bold;
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+}
+
+table.bug_list {
+ border-width: 1px;
+ border-style: solid;
+ border-color: #c3d9ff;
+ padding: 0px;
+ width: 100%;
+ border: 1px solid #c3d9ff;
+}
+
+table.target_list {
+ padding: 0px;
+ width: 100%;
+ margin-bottom: 10px;
+}
+
+table.target_list.td {
+ border-width: 1px;
+}
+
+tr.wishlist { background-color: #DCFAFF;}
+tr.wishlist:hover { background-color: #C2DCE1; }
+
+tr.minor { background-color: #FFFFA6; }
+tr.minor:hover { background-color: #E6E696; }
+
+tr.serious { background-color: #FF9077;}
+tr.serious:hover { background-color: #E6826B; }
+
+tr.critical { background-color: #FF752A; }
+tr.critical:hover { background-color: #D63905;}
+
+tr.fatal { background-color: #FF3300;}
+tr.fatal:hover { background-color: #D60000;}
+
+td.uuid { width: 5%; border-style: dotted;}
+td.status { width: 5%; border-style: dotted;}
+td.severity { width: 5%; border-style: dotted;}
+td.summary { border-style: dotted;}
+td.date { width: 25%; border-style: dotted;}
+
+/* bug detail pages */
+
+td.bug_detail_label { text-align: right; border: none;}
+td.bug_detail { border: none;}
+td.bug_comment_label { text-align: right; vertical-align: top; }
+td.bug_comment { }
+
+div.comment {
+ padding: 20px;
+ padding-top: 20px;
+ margin: auto;
+ margin-top: 0;
+}
+
+div.root.comment {
+ padding: 0px;
+ /* padding-top: 0px; */
+ padding-bottom: 20px;
+}
+""",
+##
+ 'base.html':
+"""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ <head>
+ <title>{{ title }}</title>
+ <meta http-equiv="Content-Type" content="text/html; charset={{ charset }}" />
+ <link rel="stylesheet" href="{{ stylesheet }}" type="text/css" />
+ </head>
+ <body>
+ <div class="header">{{ header }}</div>
+ <div class="main">
+ {% block content %}{% endblock %}
+ </div>
+ <div class="footer">
+ <p>Generated by <a href="http://www.bugseverywhere.org/">
+ Bugs Everywhere</a> on {{ generation_time }}</p>
+ <p>
+ <a href="http://validator.w3.org/check?uri=referer">
+ Validate XHTML</a>&nbsp;|&nbsp;
+ <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">
+ Validate CSS</a>
+ </p>
+ </div>
+ </body>
+</html>
+""",
+ 'index.html':
+"""{% extends "base.html" %}
+
+{% block content %}
+<table>
+ <tbody>
+ <tr>
+ <td class="{{ active_class }}"><a href="index.html">Active Bugs</a></td>
+ <td class="{{ inactive_class }}"><a href="index_inactive.html">Inactive Bugs</a></td>
+ <td class="{{ target_class }}"><a href="index_by_target.html">Divided by target</a></td>
+ </tr>
+ </tbody>
+</table>
+{% if bugs %}
+{% block bug_table %}{% endblock %}
+{% else %}
+<p>No bugs.</p>
+{% endif %}
+{% endblock %}
+""",
+##
+ 'standard_index.html':
+"""{% extends "index.html" %}
+
+{% block bug_table %}
+<table class="bug_list">
+ <thead>
+ <tr>
+ <th>UUID</th>
+ <th>Status</th>
+ <th>Severity</th>
+ <th>Summary</th>
+ <th>Date</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for bug in bugs %}
+ {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug)}) }}
+ {% endfor %}
+ </tbody>
+</table>
+{% endblock %}
+""",
+##
+ 'target_index.html':
+"""{% extends "index.html" %}
+
+{% block bug_table %}
+{% for target,bugs in targets %}
+<table class="target_list">
+ <thead>
+ <tr>
+ <th class="target_name" colspan="5">
+ Target: {{ target.summary|e }} ({{ target.status|e }})
+ </th>
+ </tr>
+ <tr>
+ <th>UUID</th>
+ <th>Status</th>
+ <th>Severity</th>
+ <th>Summary</th>
+ <th>Date</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for bug in bugs %}
+ {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug)}) }}
+ {% endfor %}
+ </tbody>
+</table>
+{% endfor %}
+{% endblock %}
+""",
+##
+ 'index_bug_entry.html':
+"""<tr class="{{ bug.severity }}">
+ <td class="uuid"><a href="bugs/{{ dir }}/index.html">{{ bug.id.user()|e }}</a></td>
+ <td class="status"><a href="bugs/{{ dir }}/index.html">{{ bug.status|e }}</a></td>
+ <td class="severity"><a href="bugs/{{ dir }}/index.html">{{ bug.severity|e }}</a></td>
+ <td class="summary"><a href="bugs/{{ dir }}/index.html">{{ bug.summary|e }}</a></td>
+ <td class="date"><a href="bugs/{{ dir }}/index.html">{{ (bug.time_string or '')|e }}</a></td>
+</tr>
+""",
+##
+ 'bug.html':
+"""{% extends "base.html" %}
+
+{% block content %}
+{{ backlinks.render({'up_link': up_link, 'index_type':index_type}) }}
+<h1>Bug: {{ bug.id.user()|e }}</h1>
+
+<table>
+ <tbody>
+ <tr><td class="bug_detail_label">ID :</td>
+ <td class="bug_detail">{{ bug.uuid|e }}</td></tr>
+ <tr><td class="bug_detail_label">Short name :</td>
+ <td class="bug_detail">{{ bug.id.user()|e }}</td></tr>
+ <tr><td class="bug_detail_label">Status :</td>
+ <td class="bug_detail">{{ bug.status|e }}</td></tr>
+ <tr><td class="bug_detail_label">Severity :</td>
+ <td class="bug_detail">{{ bug.severity|e }}</td></tr>
+ <tr><td class="bug_detail_label">Assigned :</td>
+ <td class="bug_detail">{{ (bug.assigned or '')|e }}</td></tr>
+ <tr><td class="bug_detail_label">Reporter :</td>
+ <td class="bug_detail">{{ (bug.reporter or '')|e }}</td></tr>
+ <tr><td class="bug_detail_label">Creator :</td>
+ <td class="bug_detail">{{ (bug.creator or '')|e }}</td></tr>
+ <tr><td class="bug_detail_label">Created :</td>
+ <td class="bug_detail">{{ (bug.time_string or '')|e }}</td></tr>
+ <tr><td class="bug_detail_label">Summary :</td>
+ <td class="bug_detail">{{ bug.summary|e }}</td></tr>
+ </tbody>
+</table>
+
+<hr/>
+
+{% if comments %}
+{% for depth,comment in comments %}
+{% if depth == 0 %}
+<div class="comment root" id="C{{ comment_dir(comment) }}">
+{% else %}
+<div class="comment" id="C{{ comment_dir(comment) }}">
+{% endif %}
+{{ comment_entry.render({
+ 'depth':depth, 'bug': bug, 'comment':comment, 'comment_dir':comment_dir,
+ 'format_body': format_body, 'div_close': div_close}) }}
+{{ div_close(depth) }}
+{% endfor %}
+{% if comments[-1][0] > 0 %}
+{{ div_close(0) }}
+{% endif %}
+{% else %}
+<p>No comments.</p>
+{% endif %}
+{{ backlinks.render({'up_link': up_link, 'index_type': index_type}) }}
+{% endblock %}
+""",
+##
+ 'bug_backlinks.html':
+"""<p class="backlink"><a href="{{ up_link }}">Back to {{ index_type }} Index</a></p>
+<p class="backlink"><a href="../../index_by_target.html">Back to Target Index</a></p>
+""",
+##
+ 'bug_comment_entry.html':
+"""<table>
+ <tbody>
+ <tr>
+ <td class="bug_comment_label">Comment:</td>
+ <td class="bug_comment">
+ --------- Comment ---------<br/>
+ ID: {{ comment.uuid }}<br/>
+ Short name: {{ comment.id.user() }}<br/>
+ From: {{ (comment.author or '')|e }}<br/>
+ Date: {{ (comment.date or '')|e }}<br/>
+ <br/>
+ {{ format_body(bug, comment) }}
+ </td>
+ </tr>
+ </tbody>
+</table>
+""",
}
- div.root.comment {
- padding: 0px;
- /* padding-top: 0px; */
- padding-bottom: 20px;
- }
- """
-
- self.index_file = """
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
- <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
- <head>
- <title>%(title)s</title>
- <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
- <link rel="stylesheet" href="style.css" type="text/css" />
- </head>
- <body>
-
- <div class="header">%(index_header)s</div>
- <div class="main">
- <p></p>
-
- <table>
- <tr>
- <td class="%(active_class)s"><a href="index.html">Active Bugs</a></td>
- <td class="%(inactive_class)s"><a href="index_inactive.html">Inactive Bugs</a></td>
- <td class="%(target_class)s"><a href="index_by_target.html">Divided by target</a></td>
- </tr>
- </table>
+ loader = DictLoader(self.template_dict)
+ if template_dir:
+ file_system_loader = FileSystemLoader(template_dir)
+ loader = ChoiceLoader([file_system_loader, loader])
- %(bug_entries)s
+ self.template = Environment(loader=loader)
- </div>
-
- <div class="footer">
- <p>Generated by <a href="http://www.bugseverywhere.org/">
- BugsEverywhere</a> on %(generation_time)s</p>
- <p>
- <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
- <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
- </p>
- </div>
-
- </body>
- </html>
- """
-
- self.index_bug_entry ="""
- <tr class="%(severity)s">
- <td class="uuid"><a href="bugs/%(dir)s/index.html">%(shortname)s</a></td>
- <td class="status"><a href="bugs/%(dir)s/index.html">%(status)s</a></td>
- <td class="severity"><a href="bugs/%(dir)s/index.html">%(severity)s</a></td>
- <td class="summary"><a href="bugs/%(dir)s/index.html">%(summary)s</a></td>
- <td class="date"><a href="bugs/%(dir)s/index.html">%(time_string)s</a></td>
- </tr>
- """
- self.target_bug_list = """
- <tr>
- <td>
- <div class="target_name">
- Target: %(target)s (%(status)s)
- </div>
- <div>
- <table class="target_list">
-
- %(bug_entries)s
- </table>
- </div>
- </td>
- </tr>
- """
- self.bug_list = """
- <table class="bug_list">
-
- %(bug_entries)s
-
- </table>
-
- """
- self.bug_file = """
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
- <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
- <head>
- <title>%(title)s</title>
- <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
- <link rel="stylesheet" href="../../style.css" type="text/css" />
- </head>
- <body>
-
- <div class="header">BugsEverywhere Bug List</div>
- <div class="main">
- <h5><a href="%(up_link)s">Back to %(index_type)s Index</a></h5>
- <h5><a href="../../index_by_target.html">Back to Target Index</a></h5>
- <h2>Bug: %(shortname)s</h2>
- <table>
- <tbody>
-
- <tr><td class="bug_detail_label">ID :</td>
- <td class="bug_detail">%(uuid)s</td></tr>
- <tr><td class="bug_detail_label">Short name :</td>
- <td class="bug_detail">%(shortname)s</td></tr>
- <tr><td class="bug_detail_label">Status :</td>
- <td class="bug_detail">%(status)s</td></tr>
- <tr><td class="bug_detail_label">Severity :</td>
- <td class="bug_detail">%(severity)s</td></tr>
- <tr><td class="bug_detail_label">Assigned :</td>
- <td class="bug_detail">%(assigned)s</td></tr>
- <tr><td class="bug_detail_label">Reporter :</td>
- <td class="bug_detail">%(reporter)s</td></tr>
- <tr><td class="bug_detail_label">Creator :</td>
- <td class="bug_detail">%(creator)s</td></tr>
- <tr><td class="bug_detail_label">Created :</td>
- <td class="bug_detail">%(time_string)s</td></tr>
- <tr><td class="bug_detail_label">Summary :</td>
- <td class="bug_detail">%(summary)s</td></tr>
- </tbody>
- </table>
-
- <hr/>
-
- %(comment_entries)s
-
- </div>
- <h5><a href="%(up_link)s">Back to %(index_type)s Index</a></h5>
- <h5><a href="../../index_by_target.html">Back to Target Index</a></h5>
- <div class="footer">
- <p>Generated by <a href="http://www.bugseverywhere.org/">
- BugsEverywhere</a> on %(generation_time)s</p>
- <p>
- <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
- <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
- </p>
- </div>
-
- </body>
- </html>
- """
- self.bug_comment_entry ="""
- <table>
- <tr>
- <td class="bug_comment_label">Comment:</td>
- <td class="bug_comment">
- --------- Comment ---------<br/>
- ID: %(uuid)s<br/>
- Short name: %(shortname)s<br/>
- From: %(author)s<br/>
- Date: %(date)s<br/>
- <br/>
- %(body)s
- </td>
- </tr>
- </table>
- """
+class _DivCloser (object):
+ def __init__(self, depth=0):
+ self.depth = depth
- # strip leading whitespace
- for attr in ['css_file', 'index_file', 'index_bug_entry', 'bug_file',
- 'bug_comment_entry']:
- value = getattr(self, attr)
- value = value.replace('\n'+' '*12, '\n')
- setattr(self, attr, value.strip()+'\n')
+ def __call__(self, depth):
+ ret = []
+ while self.depth >= depth:
+ self.depth -= 1
+ ret.append('</div>')
+ self.depth = depth
+ return '\n'.join(ret)
diff --git a/libbe/command/import_xml.py b/libbe/command/import_xml.py
index bd25372..d53df8c 100644
--- a/libbe/command/import_xml.py
+++ b/libbe/command/import_xml.py
@@ -1,4 +1,6 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# Valtteri Kokkoniemi <rvk@iki.fi>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
@@ -76,6 +78,8 @@ class Import_XML (libbe.command.Command):
help="If any comment's <in-reply-to> refers to a non-existent comment, ignore it (instead of raising an exception)."),
libbe.command.Option(name='add-only', short_name='a',
help='If any bug or comment listed in the XML file already exists in the bug repository, do not alter the repository version.'),
+ libbe.command.Option(name='preserve-uuids', short_name='p',
+ help='Preserve UUIDs for trusted input (potential name collisions).'),
libbe.command.Option(name='comment-root', short_name='c',
help='Supply a bug or comment ID as the root of any <comment> elements that are direct children of the <be-xml> element. If any such <comment> elements exist, you are required to set this option.',
arg=libbe.command.Argument(
@@ -131,11 +135,11 @@ class Import_XML (libbe.command.Command):
for child in be_xml.getchildren():
if child.tag == 'bug':
new = libbe.bug.Bug(bugdir=bugdir)
- new.from_xml(child)
+ new.from_xml(child, preserve_uuids=params['preserve-uuids'])
root_bugs.append(new)
elif child.tag == 'comment':
new = libbe.comment.Comment(croot_bug)
- new.from_xml(child)
+ new.from_xml(child, preserve_uuids=params['preserve-uuids'])
root_comments.append(new)
elif child.tag == 'version':
for gchild in child.getchildren():
@@ -184,7 +188,7 @@ class Import_XML (libbe.command.Command):
except KeyError:
old = None
if old == None:
- bd.append(new)
+ bugdir.append(new)
else:
old.load_comments(load_full=True)
old.merge(new, accept_changes=accept_changes,
diff --git a/libbe/command/init.py b/libbe/command/init.py
index 92e07f9..378e544 100644
--- a/libbe/command/init.py
+++ b/libbe/command/init.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
diff --git a/libbe/command/list.py b/libbe/command/list.py
index 9eda277..59254b2 100644
--- a/libbe/command/list.py
+++ b/libbe/command/list.py
@@ -1,6 +1,8 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
+# Robert Lehmann <mail@robertlehmann.de>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/command/merge.py b/libbe/command/merge.py
index 7fd62bb..0f49de9 100644
--- a/libbe/command/merge.py
+++ b/libbe/command/merge.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/command/new.py b/libbe/command/new.py
index 7dfbed8..deba8da 100644
--- a/libbe/command/new.py
+++ b/libbe/command/new.py
@@ -1,4 +1,6 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Andrew Cooper <andrew.cooper@hkcreations.org>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
@@ -21,6 +23,8 @@ import libbe
import libbe.command
import libbe.command.util
+from .assign import parse_assigned as _parse_assigned
+
class New (libbe.command.Command):
"""Create a new bug
@@ -40,7 +44,8 @@ class New (libbe.command.Command):
>>> uuid_gen = libbe.util.id.uuid_gen
>>> libbe.util.id.uuid_gen = lambda: 'X'
>>> ui._user_id = u'Fran\\xe7ois'
- >>> ret = ui.run(cmd, args=['this is a test',])
+ >>> options = {'assigned': 'none'}
+ >>> ret = ui.run(cmd, options=options, args=['this is a test',])
Created bug with ID abc/X
>>> libbe.util.id.uuid_gen = uuid_gen
>>> bd.flush_reload()
@@ -57,6 +62,8 @@ class New (libbe.command.Command):
minor
>>> print bug.status
open
+ >>> print bug.assigned
+ None
>>> ui.cleanup()
>>> bd.cleanup()
"""
@@ -78,6 +85,16 @@ class New (libbe.command.Command):
arg=libbe.command.Argument(
name='assigned', metavar='NAME',
completion_callback=libbe.command.util.complete_assigned)),
+ libbe.command.Option(name='status', short_name='t',
+ help='The bug\'s status level',
+ arg=libbe.command.Argument(
+ name='status', metavar='STATUS',
+ completion_callback=libbe.command.util.complete_status)),
+ libbe.command.Option(name='severity', short_name='s',
+ help='The bug\'s severity',
+ arg=libbe.command.Argument(
+ name='severity', metavar='SEVERITY',
+ completion_callback=libbe.command.util.complete_severity)),
])
self.args.extend([
libbe.command.Argument(name='summary', metavar='SUMMARY')
@@ -89,6 +106,7 @@ class New (libbe.command.Command):
else:
summary = params['summary']
bugdir = self._get_bugdir()
+ bugdir.storage.writeable = False
bug = bugdir.new_bug(summary=summary.strip())
if params['creator'] != None:
bug.creator = params['creator']
@@ -99,7 +117,13 @@ class New (libbe.command.Command):
else:
bug.reporter = bug.creator
if params['assigned'] != None:
- bug.assigned = params['assigned']
+ bug.assigned = _parse_assigned(self, params['assigned'])
+ if params['status'] != None:
+ bug.status = params['status']
+ if params['severity'] != None:
+ bug.severity = params['severity']
+ bugdir.storage.writeable = True
+ bug.save()
print >> self.stdout, 'Created bug with ID %s' % bug.id.user()
return 0
diff --git a/libbe/command/remove.py b/libbe/command/remove.py
index 1d4f265..095f1d3 100644
--- a/libbe/command/remove.py
+++ b/libbe/command/remove.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Marien Zwart <marien.zwart@gmail.com>
# Thomas Gerigk <tgerigk@gmx.de>
diff --git a/libbe/command/serve.py b/libbe/command/serve.py
index ba4b0d8..00591c0 100644
--- a/libbe/command/serve.py
+++ b/libbe/command/serve.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2010-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2010-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
@@ -58,6 +59,7 @@ import libbe
import libbe.command
import libbe.command.util
import libbe.util.encoding
+import libbe.util.subproc
import libbe.version
if libbe.TESTING == True:
@@ -507,9 +509,10 @@ class ServerApp (WSGI_AppObject):
"""
server_version = "BE-server/" + libbe.version.version()
- def __init__(self, storage, *args, **kwargs):
- WSGI_AppObject.__init__(self, *args, **kwargs)
+ def __init__(self, storage, notify=False, **kwargs):
+ WSGI_AppObject.__init__(self, **kwargs)
self.storage = storage
+ self.notify = notify
self.http_user_error = 418
self.urls = [
@@ -570,6 +573,9 @@ class ServerApp (WSGI_AppObject):
directory = self.data_get_boolean(
data, 'directory', default=False, source=source)
self.storage.add(id, parent=parent, directory=directory)
+ if self.notify:
+ self._notify(environ, 'add', id,
+ [('parent', parent), ('directory', directory)])
return self.ok_response(environ, start_response, None)
def exists(self, environ, start_response):
@@ -593,6 +599,8 @@ class ServerApp (WSGI_AppObject):
self.storage.recursive_remove(id)
else:
self.storage.remove(id)
+ if self.notify:
+ self._notify(environ, 'remove', id, [('recursive', recursive)])
return self.ok_response(environ, start_response, None)
def ancestors(self, environ, start_response):
@@ -641,6 +649,8 @@ class ServerApp (WSGI_AppObject):
raise _HandlerError(406, 'Missing query key value')
value = data['value']
self.storage.set(id, value)
+ if self.notify:
+ self._notify(environ, 'set', id, [('value', value)])
return self.ok_response(environ, start_response, None)
def commit(self, environ, start_response):
@@ -661,6 +671,10 @@ class ServerApp (WSGI_AppObject):
revision = self.storage.commit(summary, body, allow_empty)
except libbe.storage.EmptyCommit, e:
raise _HandlerError(self.http_user_error, 'EmptyCommit')
+ if self.notify:
+ self._notify(environ, 'commit', id,
+ [('allow_empty', allow_empty), ('summary', summary),
+ ('body', body)])
return self.ok_response(environ, start_response, revision)
def revision_id(self, environ, start_response):
@@ -700,9 +714,43 @@ class ServerApp (WSGI_AppObject):
raise _Unauthorized() # only non-guests allowed to write
# allow read-only commands for all users
+ def _notify(self, environ, command, id, params):
+ message = self._format_notification(environ, command, id, params)
+ self._submit_notification(message)
+
+ def _format_notification(self, environ, command, id, params):
+ key_length = len('command')
+ for key,value in params:
+ if len(key) > key_length and '\n' not in str(value):
+ key_length = len(key)
+ key_length += 1
+ lines = []
+ multi_line_params = []
+ for key,value in [('address', environ.get('REMOTE_ADDR', '-')),
+ ('command', command), ('id', id)]+params:
+ v = str(value)
+ if '\n' in v:
+ multi_line_params.append((key,v))
+ continue
+ lines.append('%*.*s %s' % (key_length, key_length, key+':', v))
+ lines.append('')
+ for key,value in multi_line_params:
+ lines.extend(['=== START %s ===' % key, v,
+ '=== STOP %s ===' % key, ''])
+ lines.append('')
+ return '\n'.join(lines)
+
+ def _submit_notification(self, message):
+ libbe.util.subproc.invoke(self.notify, stdin=message, shell=True)
+
class Serve (libbe.command.Command):
- """:class:`~libbe.command.base.Command` wrapper around
+ """Serve bug directory storage over HTTP.
+
+ This allows you to run local `be` commands interfacing with remote
+ data, transmitting file reads/writes/etc. over the network.
+
+ :class:`~libbe.command.base.Command` wrapper around
:class:`ServerApp`.
"""
@@ -721,6 +769,10 @@ class Serve (libbe.command.Command):
name='host', metavar='HOST', default='')),
libbe.command.Option(name='read-only', short_name='r',
help='Dissable operations that require writing'),
+ libbe.command.Option(name='notify', short_name='n',
+ help='Send notification emails for changes.',
+ arg=libbe.command.Argument(
+ name='notify', metavar='EMAIL-COMMAND', default=None)),
libbe.command.Option(name='ssl', short_name='s',
help='Use CherryPy to serve HTTPS (HTTP over SSL/TLS)'),
libbe.command.Option(name='auth', short_name='a',
@@ -742,7 +794,8 @@ class Serve (libbe.command.Command):
self._check_restricted_access(storage, params['auth'])
users = Users(params['auth'])
users.load()
- app = ServerApp(storage=storage, logger=self.logger)
+ app = ServerApp(
+ storage=storage, notify=params['notify'], logger=self.logger)
if params['auth'] != None:
app = AdminApp(app, users=users, logger=self.logger)
app = AuthenticationApp(app, realm=storage.repo,
@@ -860,6 +913,7 @@ if libbe.TESTING == True:
self.logger.setLevel(logging.INFO)
self.default_environ = { # required by PEP 333
'REQUEST_METHOD': 'GET', # 'POST', 'HEAD'
+ 'REMOTE_ADDR': '192.168.0.123',
'SCRIPT_NAME':'',
'PATH_INFO': '',
#'QUERY_STRING':'', # may be empty or absent
@@ -920,7 +974,7 @@ if libbe.TESTING == True:
self.app.log_request(
environ=self.default_environ, status='-1 OK', bytes=123)
log = self.logstream.getvalue()
- self.failUnless(log.startswith('- -'), log)
+ self.failUnless(log.startswith('192.168.0.123 -'), log)
class ExceptionAppTestCase (WSGITestCase):
def setUp(self):
diff --git a/libbe/command/set.py b/libbe/command/set.py
index b3eb583..d70d33e 100644
--- a/libbe/command/set.py
+++ b/libbe/command/set.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Marien Zwart <marien.zwart@gmail.com>
# Thomas Gerigk <tgerigk@gmx.de>
@@ -100,7 +101,56 @@ To unset a setting, set it to "none".
Allowed settings are:
-%s""" % ('\n'.join(get_bugdir_settings()),)
+%s
+
+Note that this command does not provide a good interface for some of
+these settings (yet!). You may need to edit the bugdir settings file
+(`.be/<bugdir>/settings`) manually. Examples for each troublesome
+setting are given below.
+
+Add the following lines to override the default severities and use
+your own:
+
+ severities:
+ - - target
+ - The issue is a target or milestone, not a bug.
+ - - wishlist
+ - A feature that could improve usefulness, but not a bug.
+
+You may add as many name/description pairs as you wish to have; they
+are sorted in order from least important at the top, to most important
+at the bottom. The target severity gets special handling by `be
+target`.
+
+Note that the values here _override_ the defaults. That means that if
+you like the defaults, and wish to keep them, you will have to copy
+them here before adding any of your own. See `be severity --help` for
+the current list.
+
+Add the following lines to override the default statuses and use your
+own:
+
+ active_status:
+ - - unconfirmed
+ - A possible bug which lacks independent existance confirmation.
+ - - open
+ - A working bug that has not been assigned to a developer.
+
+ inactive_status:
+ - - closed
+ - The bug is no longer relevant.
+ - - fixed
+ - The bug should no longer occur.
+
+You may add as many name/description pairs as you wish to have; they
+are sorted in order from most important at the top, to least important
+at the bottom.
+
+Note that the values here _override_ the defaults. That means that if
+you like the defaults, and wish to keep them, you will have to copy
+them here before adding any of your own. See `be status --help` for
+the current list.
+""" % ('\n'.join(get_bugdir_settings()),)
def get_bugdir_settings():
settings = []
diff --git a/libbe/command/severity.py b/libbe/command/severity.py
index a84efe8..fa6007a 100644
--- a/libbe/command/severity.py
+++ b/libbe/command/severity.py
@@ -1,7 +1,9 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Marien Zwart <marien.zwart@gmail.com>
# Thomas Gerigk <tgerigk@gmx.de>
+# Tim Guirgies <lt.infiltrator@gmail.com>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
@@ -79,21 +81,26 @@ class Severity (libbe.command.Command):
return 0
def _long_help(self):
- ret = ["""
-Show or change a bug's severity level.
-
-If no severity is specified, the current value is printed. If a severity level
-is specified, it will be assigned to the bug.
-
-Severity levels are:
-"""]
try: # See if there are any per-tree severity configurations
bd = self._get_bugdir()
except NotImplementedError:
pass # No tree, just show the defaults
longest_severity_len = max([len(s) for s in libbe.bug.severity_values])
+ severity_levels = []
for severity in libbe.bug.severity_values :
description = libbe.bug.severity_description[severity]
- ret.append('%*s : %s\n' \
- % (longest_severity_len, severity, description))
- return ''.join(ret)
+ s = '%*s : %s' % (longest_severity_len, severity, description)
+ severity_levels.append(s)
+ ret = """
+Show or change a bug's severity level.
+
+If no severity is specified, the current value is printed. If a severity level
+is specified, it will be assigned to the bug.
+
+Severity levels are:
+ %s
+
+You can overide the list of allowed severities on a per-repository
+basis. See `be set --help` for details.
+""" % ('\n '.join(severity_levels))
+ return ret
diff --git a/libbe/command/show.py b/libbe/command/show.py
index ea86191..4f85c69 100644
--- a/libbe/command/show.py
+++ b/libbe/command/show.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Thomas Gerigk <tgerigk@gmx.de>
# Thomas Habets <thomas@habets.pp.se>
diff --git a/libbe/command/status.py b/libbe/command/status.py
index 2e470e4..2eb0755 100644
--- a/libbe/command/status.py
+++ b/libbe/command/status.py
@@ -1,7 +1,9 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Marien Zwart <marien.zwart@gmail.com>
# Thomas Gerigk <tgerigk@gmx.de>
+# Tim Guirgies <lt.infiltrator@gmail.com>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
@@ -80,6 +82,10 @@ class Status (libbe.command.Command):
return 0
def _long_help(self):
+ try: # See if there are any per-tree status configurations
+ bd = self._get_bugdir()
+ except NotImplementedError:
+ pass # No tree, just show the defaults
longest_status_len = max([len(s) for s in libbe.bug.status_values])
active_statuses = []
for status in libbe.bug.active_status_values :
@@ -106,7 +112,7 @@ Active status levels are:
Inactive status levels are:
%s
-You can overide the list of allowed statuses on a per-repository basis.
-See "be set --help" for more details.
+You can overide the list of allowed statuses on a per-repository
+basis. See `be set --help` for details.
""" % ('\n '.join(active_statuses), '\n '.join(inactive_statuses))
return ret
diff --git a/libbe/command/subscribe.py b/libbe/command/subscribe.py
index f49abb9..50f1e7e 100644
--- a/libbe/command/subscribe.py
+++ b/libbe/command/subscribe.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/command/tag.py b/libbe/command/tag.py
index dea6e00..1da8fd9 100644
--- a/libbe/command/tag.py
+++ b/libbe/command/tag.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/command/util.py b/libbe/command/util.py
index d8e049e..4e5471d 100644
--- a/libbe/command/util.py
+++ b/libbe/command/util.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
diff --git a/libbe/comment.py b/libbe/comment.py
index 6350e2c..3527d50 100644
--- a/libbe/comment.py
+++ b/libbe/comment.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# Thomas Habets <thomas@habets.pp.se>
# W. Trevor King <wking@drexel.edu>
#
@@ -53,13 +54,6 @@ if libbe.TESTING == True:
import doctest
-class InvalidShortname(KeyError):
- def __init__(self, shortname, shortnames):
- msg = "Invalid shortname %s\n%s" % (shortname, shortnames)
- KeyError.__init__(self, msg)
- self.shortname = shortname
- self.shortnames = shortnames
-
class MissingReference(ValueError):
def __init__(self, comment):
msg = "Missing reference to %s" % (comment.in_reply_to)
@@ -67,11 +61,6 @@ class MissingReference(ValueError):
self.reference = comment.in_reply_to
self.comment = comment
-class DiskAccessRequired (Exception):
- def __init__(self, goal):
- msg = "Cannot %s without accessing the disk" % goal
- Exception.__init__(self, msg)
-
INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!"
def load_comments(bug, load_full=False):
@@ -340,7 +329,7 @@ class Comment (Tree, settings_object.SavedSettingsObject):
sep = '\n' + istring
return istring + sep.join(lines).rstrip('\n')
- def from_xml(self, xml_string, verbose=True):
+ def from_xml(self, xml_string, preserve_uuids=False, verbose=True):
u"""
Note: If alt-id is not given, translates any <uuid> fields to
<alt-id> fields.
@@ -360,6 +349,10 @@ class Comment (Tree, settings_object.SavedSettingsObject):
>>> commB.alt_id = None
>>> commB.xml() == xml
True
+ >>> commC = Comment()
+ >>> commC.from_xml(xml, preserve_uuids=True)
+ >>> commC.uuid == commA.uuid
+ True
"""
if type(xml_string) == types.UnicodeType:
xml_string = xml_string.strip().encode('unicode_escape')
@@ -385,7 +378,7 @@ class Comment (Tree, settings_object.SavedSettingsObject):
else:
text = xml.sax.saxutils.unescape(child.text)
text = text.decode('unicode_escape').strip()
- if child.tag == 'uuid':
+ if child.tag == 'uuid' and not preserve_uuids:
uuid = text
continue # don't set the comment's uuid tag.
elif child.tag == 'body':
@@ -607,8 +600,8 @@ class Comment (Tree, settings_object.SavedSettingsObject):
if self.uuid == INVALID_UUID:
return
if settings_mapfile == None:
- settings_mapfile = \
- self.storage.get(self.id.storage("values"), default="\n")
+ settings_mapfile = self.storage.get(
+ self.id.storage('values'), '\n')
try:
settings = mapfile.parse(settings_mapfile)
except mapfile.InvalidMapfileContents, e:
diff --git a/libbe/diff.py b/libbe/diff.py
index 1802be4..4c24073 100644
--- a/libbe/diff.py
+++ b/libbe/diff.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
diff --git a/libbe/error.py b/libbe/error.py
index f385db0..0108177 100644
--- a/libbe/error.py
+++ b/libbe/error.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
diff --git a/libbe/storage/__init__.py b/libbe/storage/__init__.py
index 90e3b0d..46e5bf2 100644
--- a/libbe/storage/__init__.py
+++ b/libbe/storage/__init__.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
diff --git a/libbe/storage/base.py b/libbe/storage/base.py
index ef42c98..a9c1064 100644
--- a/libbe/storage/base.py
+++ b/libbe/storage/base.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
diff --git a/libbe/storage/http.py b/libbe/storage/http.py
index 4cf0f84..c2bb65b 100644
--- a/libbe/storage/http.py
+++ b/libbe/storage/http.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2010-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2010-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
@@ -334,14 +335,14 @@ if TESTING == True:
class GetPostUrlTestCase (unittest.TestCase):
"""Test cases for get_post_url()"""
def test_get(self):
- url = 'http://bugseverywhere.org/be/show/HomePage'
+ url = 'http://bugseverywhere.org/'
page,final_url,info = get_post_url(url=url)
self.failUnless(final_url == url,
'Redirect?\n Expected: "%s"\n Got: "%s"'
% (url, final_url))
def test_get_redirect(self):
- url = 'http://bugseverywhere.org'
- expected = 'http://bugseverywhere.org/be/show/HomePage'
+ url = 'http://physics.drexel.edu/~wking/code/be/redirect'
+ expected = 'http://physics.drexel.edu/~wking/'
page,final_url,info = get_post_url(url=url)
self.failUnless(final_url == expected,
'Redirect?\n Expected: "%s"\n Got: "%s"'
@@ -355,9 +356,10 @@ if TESTING == True:
storage=self._storage_backend)
HTTP.__init__(self, repo='http://localhost:8000/', *args, **kwargs)
self.intitialized = False
- # duplicated from libbe.storage.serve.WSGITestCase
+ # duplicated from libbe.command.serve.WSGITestCase
self.default_environ = {
'REQUEST_METHOD': 'GET', # 'POST', 'HEAD'
+ 'REMOTE_ADDR': '192.168.0.123',
'SCRIPT_NAME':'',
'PATH_INFO': '',
#'QUERY_STRING':'', # may be empty or absent
@@ -376,7 +378,7 @@ if TESTING == True:
}
def getURL(self, app, path='/', method='GET', data=None,
scheme='http', environ={}):
- # duplicated from libbe.storage.serve.WSGITestCase
+ # duplicated from libbe.command.serve.WSGITestCase
env = copy.copy(self.default_environ)
env['PATH_INFO'] = path
env['REQUEST_METHOD'] = method
diff --git a/libbe/storage/util/config.py b/libbe/storage/util/config.py
index 8a3d7c8..714d4e7 100644
--- a/libbe/storage/util/config.py
+++ b/libbe/storage/util/config.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
diff --git a/libbe/storage/util/mapfile.py b/libbe/storage/util/mapfile.py
index 9eca586..abea6c8 100644
--- a/libbe/storage/util/mapfile.py
+++ b/libbe/storage/util/mapfile.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
diff --git a/libbe/storage/util/properties.py b/libbe/storage/util/properties.py
index 152f5cc..9b0549a 100644
--- a/libbe/storage/util/properties.py
+++ b/libbe/storage/util/properties.py
@@ -1,5 +1,6 @@
# Bugs Everywhere - a distributed bugtracker
-# Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py
index 2026906..7c02734 100644
--- a/libbe/storage/util/settings_object.py
+++ b/libbe/storage/util/settings_object.py
@@ -1,5 +1,6 @@
# Bugs Everywhere - a distributed bugtracker
-# Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/storage/util/upgrade.py b/libbe/storage/util/upgrade.py
index 45e3058..3a5aa1c 100644
--- a/libbe/storage/util/upgrade.py
+++ b/libbe/storage/util/upgrade.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/storage/vcs/__init__.py b/libbe/storage/vcs/__init__.py
index 47d92db..1f7166d 100644
--- a/libbe/storage/vcs/__init__.py
+++ b/libbe/storage/vcs/__init__.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
diff --git a/libbe/storage/vcs/arch.py b/libbe/storage/vcs/arch.py
index 6c519c4..5a40f7c 100644
--- a/libbe/storage/vcs/arch.py
+++ b/libbe/storage/vcs/arch.py
@@ -1,5 +1,6 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
# Ben Finney <benf@cybersource.com.au>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# James Rowe <jnrowe@ukfsn.org>
# W. Trevor King <wking@drexel.edu>
@@ -437,7 +438,7 @@ class Arch(base.VCS):
def _vcs_changed(self, revision):
return self._parse_diff(self._diff(revision))
-
+
if libbe.TESTING == True:
base.make_vcs_testcase_subclasses(Arch, sys.modules[__name__])
diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py
index aba6159..22874a5 100644
--- a/libbe/storage/vcs/base.py
+++ b/libbe/storage/vcs/base.py
@@ -156,11 +156,11 @@ class CachedPathID (object):
>>> os.mkdir(os.path.join(dir.path, '.be', 'abc', 'bugs', '123', 'comments'))
>>> os.mkdir(os.path.join(dir.path, '.be', 'abc', 'bugs', '123', 'comments', 'def'))
>>> os.mkdir(os.path.join(dir.path, '.be', 'abc', 'bugs', '456'))
- >>> file(os.path.join(dir.path, '.be', 'abc', 'values'),
+ >>> open(os.path.join(dir.path, '.be', 'abc', 'values'),
... 'w').close()
- >>> file(os.path.join(dir.path, '.be', 'abc', 'bugs', '123', 'values'),
+ >>> open(os.path.join(dir.path, '.be', 'abc', 'bugs', '123', 'values'),
... 'w').close()
- >>> file(os.path.join(dir.path, '.be', 'abc', 'bugs', '123', 'comments', 'def', 'values'),
+ >>> open(os.path.join(dir.path, '.be', 'abc', 'bugs', '123', 'comments', 'def', 'values'),
... 'w').close()
>>> c = CachedPathID()
>>> c.root(dir.path)
@@ -521,6 +521,97 @@ class VCS (libbe.storage.base.VersionedStorage):
self._version = self._vcs_version()
return self._version
+ def version_cmp(self, *args):
+ """Compare the installed VCS version `V_i` with another version
+ `V_o` (given in `*args`). Returns
+
+ === ===============
+ 1 if `V_i > V_o`
+ 0 if `V_i == V_o`
+ -1 if `V_i < V_o`
+ === ===============
+
+ Examples
+ --------
+
+ >>> v = VCS(repo='.')
+ >>> v._version = '2.3.1 (release)'
+ >>> v.version_cmp(2,3,1)
+ 0
+ >>> v.version_cmp(2,3,2)
+ -1
+ >>> v.version_cmp(2,3,'a',5)
+ 1
+ >>> v.version_cmp(2,3,0)
+ 1
+ >>> v.version_cmp(2,3,1,'a',5)
+ 1
+ >>> v.version_cmp(2,3,1,1)
+ -1
+ >>> v.version_cmp(3)
+ -1
+ >>> v._version = '2.0.0pre2'
+ >>> v._parsed_version = None
+ >>> v.version_cmp(3)
+ -1
+ >>> v.version_cmp(2,0,1)
+ -1
+ >>> v.version_cmp(2,0,0,'pre',1)
+ 1
+ >>> v.version_cmp(2,0,0,'pre',2)
+ 0
+ >>> v.version_cmp(2,0,0,'pre',3)
+ -1
+ >>> v.version_cmp(2,0,0,'a',3)
+ 1
+ >>> v.version_cmp(2,0,0,'rc',1)
+ -1
+ """
+ if not hasattr(self, '_parsed_version') \
+ or self._parsed_version == None:
+ num_part = self.version().split(' ')[0]
+ self._parsed_version = []
+ for num in num_part.split('.'):
+ try:
+ self._parsed_version.append(int(num))
+ except ValueError, e:
+ # bzr version number might contain non-numerical tags
+ splitter = re.compile(r'[\D]') # Match non-digits
+ splits = splitter.split(num)
+ # if len(tag) > 1 some splits will be empty; remove
+ splits = filter(lambda s: s != '', splits)
+ tag_starti = len(splits[0])
+ num_starti = num.find(splits[1], tag_starti)
+ tag = num[tag_starti:num_starti]
+ self._parsed_version.append(int(splits[0]))
+ self._parsed_version.append(tag)
+ self._parsed_version.append(int(splits[1]))
+ for current,other in zip(self._parsed_version, args):
+ if type(current) != type (other):
+ # one of them is a pre-release string
+ if type(current) != types.IntType:
+ return -1
+ else:
+ return 1
+ c = cmp(current,other)
+ if c != 0:
+ return c
+ # see if one is longer than the other
+ verlen = len(self._parsed_version)
+ arglen = len(args)
+ if verlen == arglen:
+ return 0
+ elif verlen > arglen:
+ if type(self._parsed_version[arglen]) != types.IntType:
+ return -1 # self is a prerelease
+ else:
+ return 1
+ else:
+ if type(args[verlen]) != types.IntType:
+ return 1 # args is a prerelease
+ else:
+ return -1
+
def installed(self):
if self.version() != None:
return True
@@ -537,7 +628,7 @@ class VCS (libbe.storage.base.VersionedStorage):
self.user_id = self._vcs_get_user_id()
if self.user_id == None:
# guess missing info
- name = libbe.ui.util.user.get_fallback_username()
+ name = libbe.ui.util.user.get_fallback_fullname()
email = libbe.ui.util.user.get_fallback_email()
self.user_id = libbe.ui.util.user.create_user_id(name, email)
return self.user_id
@@ -1003,7 +1094,7 @@ class VCS (libbe.storage.base.VersionedStorage):
libbe.storage.STORAGE_VERSION+'\n')
self._vcs_add(self._u_rel_path(path))
-
+
if libbe.TESTING == True:
class VCSTestCase (unittest.TestCase):
"""
diff --git a/libbe/storage/vcs/bzr.py b/libbe/storage/vcs/bzr.py
index 9464d1d..c01b6c7 100644
--- a/libbe/storage/vcs/bzr.py
+++ b/libbe/storage/vcs/bzr.py
@@ -1,5 +1,6 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
# Ben Finney <benf@cybersource.com.au>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Marien Zwart <marien.zwart@gmail.com>
# W. Trevor King <wking@drexel.edu>
@@ -39,7 +40,6 @@ import re
import shutil
import StringIO
import sys
-import types
import libbe
import base
@@ -67,57 +67,6 @@ class Bzr(base.VCS):
return None
return bzrlib.__version__
- def version_cmp(self, *args):
- """Compare the installed Bazaar version `V_i` with another version
- `V_o` (given in `*args`). Returns
-
- === ===============
- 1 if `V_i > V_o`
- 0 if `V_i == V_o`
- -1 if `V_i < V_o`
- === ===============
-
- Examples
- --------
-
- >>> b = Bzr(repo='.')
- >>> b._version = '2.3.1 (release)'
- >>> b.version_cmp(2,3,1)
- 0
- >>> b.version_cmp(2,3,2)
- -1
- >>> b.version_cmp(2,3,0)
- 1
- >>> b.version_cmp(3)
- -1
- >>> b._version = '2.0.0pre2'
- >>> b._parsed_version = None
- >>> b.version_cmp(3)
- -1
- >>> b.version_cmp(2,0,1)
- Traceback (most recent call last):
- ...
- NotImplementedError: Cannot parse non-integer portion "0pre2" of Bzr version "2.0.0pre2"
- """
- if not hasattr(self, '_parsed_version') \
- or self._parsed_version == None:
- num_part = self.version().split(' ')[0]
- self._parsed_version = []
- for num in num_part.split('.'):
- try:
- self._parsed_version.append(int(num))
- except ValueError, e:
- self._parsed_version.append(num)
- for current,other in zip(self._parsed_version, args):
- if type(current) != types.IntType:
- raise NotImplementedError(
- 'Cannot parse non-integer portion "%s" of Bzr version "%s"'
- % (current, self.version()))
- c = cmp(current,other)
- if c != 0:
- return c
- return 0
-
def _vcs_get_user_id(self):
# excerpted from bzrlib.builtins.cmd_whoami.run()
try:
@@ -378,7 +327,7 @@ class Bzr(base.VCS):
def _vcs_changed(self, revision):
return self._parse_diff(self._diff(revision))
-
+
if libbe.TESTING == True:
base.make_vcs_testcase_subclasses(Bzr, sys.modules[__name__])
diff --git a/libbe/storage/vcs/darcs.py b/libbe/storage/vcs/darcs.py
index 4a19d1d..aef89cd 100644
--- a/libbe/storage/vcs/darcs.py
+++ b/libbe/storage/vcs/darcs.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
@@ -402,7 +403,7 @@ class Darcs(base.VCS):
def _vcs_changed(self, revision):
return self._parse_diff(self._diff(revision))
-
+
if libbe.TESTING == True:
base.make_vcs_testcase_subclasses(Darcs, sys.modules[__name__])
diff --git a/libbe/storage/vcs/git.py b/libbe/storage/vcs/git.py
index 5c17303..f23f0ea 100644
--- a/libbe/storage/vcs/git.py
+++ b/libbe/storage/vcs/git.py
@@ -1,6 +1,7 @@
# Copyright (C) 2008-2011 Ben Finney <benf@cybersource.com.au>
# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
+# Robert Lehmann <mail@robertlehmann.de>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
@@ -74,7 +75,7 @@ class Git(base.VCS):
if name != '' or email != '': # got something!
# guess missing info, if necessary
if name == '':
- name = _user.get_fallback_username()
+ name = _user.get_fallback_fullname()
if email == '':
email = _user.get_fallback_email()
return _user.create_user_id(name, email)
@@ -265,7 +266,7 @@ class Git(base.VCS):
def _vcs_changed(self, revision):
return self._parse_diff(self._diff(revision))
-
+
if libbe.TESTING == True:
base.make_vcs_testcase_subclasses(Git, sys.modules[__name__])
diff --git a/libbe/storage/vcs/hg.py b/libbe/storage/vcs/hg.py
index d610352..b61e796 100644
--- a/libbe/storage/vcs/hg.py
+++ b/libbe/storage/vcs/hg.py
@@ -1,5 +1,6 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
# Ben Finney <benf@cybersource.com.au>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Marien Zwart <marien.zwart@gmail.com>
# W. Trevor King <wking@drexel.edu>
@@ -82,14 +83,18 @@ class Hg(base.VCS):
assert len(kwargs) == 1, kwargs
fullargs = ['--cwd', kwargs['cwd']]
fullargs.extend(args)
- stdout = sys.stdout
- tmp_stdout = StringIO.StringIO()
- sys.stdout = tmp_stdout
cwd = os.getcwd()
- mercurial.dispatch.dispatch(fullargs)
+ output = StringIO.StringIO()
+ if self.version_cmp(1,9):
+ req = mercurial.dispatch.request(fullargs, fout=output)
+ mercurial.dispatch.dispatch(req)
+ else:
+ stdout = sys.stdout
+ sys.stdout = output
+ mercurial.dispatch.dispatch(fullargs)
+ sys.stdout = stdout
os.chdir(cwd)
- sys.stdout = stdout
- return tmp_stdout.getvalue().rstrip('\n')
+ return output.getvalue().rstrip('\n')
def _vcs_get_user_id(self):
output = self._u_invoke_client(
@@ -251,7 +256,7 @@ class Hg(base.VCS):
def _vcs_changed(self, revision):
return self._parse_diff(self._diff(revision))
-
+
if libbe.TESTING == True:
base.make_vcs_testcase_subclasses(Hg, sys.modules[__name__])
diff --git a/libbe/storage/vcs/monotone.py b/libbe/storage/vcs/monotone.py
index e99a6ec..9f4e278 100644
--- a/libbe/storage/vcs/monotone.py
+++ b/libbe/storage/vcs/monotone.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2010-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2010-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
@@ -366,7 +367,7 @@ class Monotone (base.VCS):
def _vcs_changed(self, revision):
return self._parse_diff(self._diff(revision))
-
+
if libbe.TESTING == True:
base.make_vcs_testcase_subclasses(Monotone, sys.modules[__name__])
diff --git a/libbe/ui/__init__.py b/libbe/ui/__init__.py
index 227269c..45068b4 100644
--- a/libbe/ui/__init__.py
+++ b/libbe/ui/__init__.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py
index 5f42147..d5719a6 100644
--- a/libbe/ui/command_line.py
+++ b/libbe/ui/command_line.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
@@ -29,6 +30,8 @@ import libbe.command
import libbe.command.util
import libbe.version
import libbe.ui.util.pager
+import libbe.util.encoding
+
if libbe.TESTING == True:
import doctest
@@ -86,11 +89,11 @@ class CmdOptionParser(optparse.OptionParser):
if '_' in name: # reconstruct original option name
options[name.replace('_', '-')] = options.pop(name)
for name,value in options.items():
+ argument = None
+ option = self._option_by_name[name]
+ if option.arg != None:
+ argument = option.arg
if value == '--complete':
- argument = None
- option = self._option_by_name[name]
- if option.arg != None:
- argument = option.arg
fragment = None
indices = [i for i,arg in enumerate(args)
if arg == '--complete']
@@ -108,29 +111,36 @@ class CmdOptionParser(optparse.OptionParser):
if i+1 < len(args):
fragment = args[i+1]
self.complete(argument, fragment)
+ elif argument is not None:
+ value = self.process_raw_argument(argument=argument, value=value)
+ options[name] = value
for i,arg in enumerate(parsed_args):
+ if i > 0 and self.command.name == 'be':
+ break # let this pass through for the command parser to handle
+ elif i < len(self.command.args):
+ argument = self.command.args[i]
+ elif len(self.command.args) == 0:
+ break # command doesn't take arguments
+ else:
+ argument = self.command.args[-1]
+ if argument.repeatable == False:
+ raise libbe.command.UserError('Too many arguments')
if arg == '--complete':
- if i > 0 and self.command.name == 'be':
- break # let this pass through for the command parser to handle
- elif i < len(self.command.args):
- argument = self.command.args[i]
- elif len(self.command.args) == 0:
- break # command doesn't take arguments
- else:
- argument = self.command.args[-1]
- if argument.repeatable == False:
- raise libbe.command.UserError('Too many arguments')
fragment = None
if i < len(parsed_args) - 1:
fragment = parsed_args[i+1]
self.complete(argument, fragment)
+ else:
+ value = self.process_raw_argument(argument=argument, value=arg)
+ parsed_args[i] = value
if len(parsed_args) > len(self.command.args) \
and self.command.args[-1].repeatable == False:
raise libbe.command.UserError('Too many arguments')
for arg in self.command.args[len(parsed_args):]:
if arg.optional == False:
- raise libbe.command.UserError(
- 'Missing required argument %s' % arg.metavar)
+ raise libbe.command.UsageError(
+ command=self.command,
+ message='Missing required argument %s' % arg.metavar)
return (options, parsed_args)
def callback(self, option, opt, value, parser):
@@ -154,6 +164,16 @@ class CmdOptionParser(optparse.OptionParser):
print >> self.command.stdout, '\n'.join(comps)
raise CallbackExit
+ def process_raw_argument(self, argument, value):
+ if value == argument.default:
+ return value
+ if argument.type == 'string':
+ if not hasattr(self, 'argv_encoding'):
+ self.argv_encoding = libbe.util.encoding.get_argv_encoding()
+ return unicode(value, self.argv_encoding)
+ return value
+
+
class BE (libbe.command.Command):
"""Class for parsing the command line arguments for `be`.
This class does not contain a useful _run() method. Call this
@@ -271,6 +291,13 @@ def dispatch(ui, command, args):
'See http://docs.python.org/library/locale.html for details',
])
return 1
+ except libbe.command.UsageError, e:
+ print >> ui.io.stdout, 'Usage Error:\n', e
+ if e.command:
+ print >> ui.io.stdout, e.command.usage()
+ print >> ui.io.stdout, 'For usage information, try'
+ print >> ui.io.stdout, ' be help %s' % e.command_name
+ return 1
except libbe.command.UserError, e:
print >> ui.io.stdout, 'ERROR:\n', e
return 1
@@ -296,15 +323,19 @@ def main():
options,args = parser.parse_args()
except CallbackExit:
return 0
- except libbe.command.UserError, e:
- if str(e).endswith('COMMAND'):
+ except libbe.command.UsageError, e:
+ if isinstance(e.command, BE):
# no command given, print usage string
- print >> ui.io.stdout, 'ERROR:'
- print >> ui.io.stdout, be.usage(), '\n', e
+ print >> ui.io.stdout, 'Usage Error:\n', e
+ print >> ui.io.stdout, be.usage()
print >> ui.io.stdout, 'For example, try'
print >> ui.io.stdout, ' be help'
else:
- print >> ui.io.stdout, 'ERROR:\n', e
+ print >> ui.io.stdout, 'Usage Error:\n', e
+ if e.command:
+ print >> ui.io.stdout, e.command.usage()
+ print >> ui.io.stdout, 'For usage information, try'
+ print >> ui.io.stdout, ' be help %s' % e.command_name
return 1
command_name = args.pop(0)
diff --git a/libbe/ui/util/__init__.py b/libbe/ui/util/__init__.py
index 227269c..45068b4 100644
--- a/libbe/ui/util/__init__.py
+++ b/libbe/ui/util/__init__.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
diff --git a/libbe/ui/util/editor.py b/libbe/ui/util/editor.py
index dcf73c8..206e9c4 100644
--- a/libbe/ui/util/editor.py
+++ b/libbe/ui/util/editor.py
@@ -1,5 +1,6 @@
# Bugs Everywhere, a distributed bugtracker
-# Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/ui/util/pager.py b/libbe/ui/util/pager.py
index d82dcef..de3b3fc 100644
--- a/libbe/ui/util/pager.py
+++ b/libbe/ui/util/pager.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
diff --git a/libbe/ui/util/user.py b/libbe/ui/util/user.py
index 35665e4..261ecdf 100644
--- a/libbe/ui/util/user.py
+++ b/libbe/ui/util/user.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
@@ -28,32 +29,54 @@ are human-readable tags refering to objects.
try:
from email.utils import formataddr, parseaddr
-except ImportErrror: # adjust to old python < 2.5
+except ImportErrror: # adjust to old python < 2.5
from email.Utils import formataddr, parseaddr
import os
+try:
+ import pwd
+except ImportError: # handle non-Unix systems
+ pwd = None
import re
from socket import gethostname
import libbe
import libbe.storage.util.config
+
def get_fallback_username():
"""Return a username extracted from environmental variables.
"""
name = None
- for env in ["LOGNAME", "USERNAME"]:
+ for env in ['LOGNAME', 'USERNAME']:
+ if os.environ.has_key(env):
+ name = os.environ[env]
+ break
+ if name is None and pwd:
+ pw_ent = pwd.getpwuid(os.getuid())
+ name = pw_ent.pw_name
+ assert name is not None
+ return name
+
+def get_fallback_fullname():
+ """Return a full name extracted from environmental variables.
+ """
+ name = None
+ for env in ['FULLNAME']:
if os.environ.has_key(env):
name = os.environ[env]
break
- assert name != None
+ if pwd and not name:
+ pw_ent = pwd.getpwuid(os.getuid())
+ name = pw_ent.pw_gecos.split(',', 1)[0]
+ if not name:
+ name = get_fallback_username()
return name
def get_fallback_email():
"""Return an email address extracted from environmental variables.
"""
- hostname = gethostname()
- name = get_fallback_username()
- return "%s@%s" % (name, hostname)
+ return os.getenv('EMAIL') or '%s@%s' % (
+ get_fallback_username(), gethostname())
def create_user_id(name, email=None):
"""Create a user ID string from given `name` and `email` strings.
@@ -122,7 +145,7 @@ def get_user_id(storage=None):
user = storage.get_user_id()
if user != None:
return user
- name = get_fallback_username()
+ name = get_fallback_fullname()
email = get_fallback_email()
user = create_user_id(name, email)
return user
diff --git a/libbe/util/__init__.py b/libbe/util/__init__.py
index b9166f0..78f615f 100644
--- a/libbe/util/__init__.py
+++ b/libbe/util/__init__.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
diff --git a/libbe/util/encoding.py b/libbe/util/encoding.py
index 5950bb9..22a2e30 100644
--- a/libbe/util/encoding.py
+++ b/libbe/util/encoding.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
@@ -49,11 +50,14 @@ def get_input_encoding():
return get_encoding()
def get_output_encoding():
- return get_encoding()
+ return sys.__stdout__.encoding or get_encoding()
def get_filesystem_encoding():
return get_encoding()
+def get_argv_encoding():
+ return get_encoding()
+
def known_encoding(encoding):
"""
>>> known_encoding("highly-unlikely-encoding")
diff --git a/libbe/util/id.py b/libbe/util/id.py
index 8d8c75c..c14dd90 100644
--- a/libbe/util/id.py
+++ b/libbe/util/id.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
@@ -224,8 +225,8 @@ def _truncate(uuid, other_uuids, min_length=3):
uuid : str
The UUID to truncate.
other_uuids : list of str
- The other UUIDs which the truncation *might* (but doesn't) refer
- to.
+ The other UUIDs which the truncation *might* refer to. May
+ contain `uuid`.
min_length : int
Avoid rapidly outdated truncations, even if they are unique now.
@@ -255,8 +256,8 @@ def _expand(truncated_id, common, other_ids):
`other_ids`. Not used by ``_expand``, but passed on to the
matching exceptions if they occur.
other_uuids : list of str
- The other UUIDs which the truncation *might* (but doesn't) refer
- to.
+ The other UUIDs which the truncation *might* refer to. May
+ contain `uuid`.
Raises
------
diff --git a/libbe/util/plugin.py b/libbe/util/plugin.py
index b6200fc..034b750 100644
--- a/libbe/util/plugin.py
+++ b/libbe/util/plugin.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Marien Zwart <marien.zwart@gmail.com>
# W. Trevor King <wking@drexel.edu>
diff --git a/libbe/util/subproc.py b/libbe/util/subproc.py
index b10f84a..fde5af5 100644
--- a/libbe/util/subproc.py
+++ b/libbe/util/subproc.py
@@ -1,4 +1,5 @@
-# Copyright (C) 2009-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
#
@@ -21,6 +22,7 @@ Functions for running external commands in subprocesses.
from subprocess import Popen, PIPE
import sys
+import types
import libbe
from encoding import get_encoding
@@ -45,7 +47,8 @@ class CommandError(Exception):
self.stderr = stderr
def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,),
- cwd=None, unicode_output=True, verbose=False, encoding=None):
+ cwd=None, shell=None, unicode_output=True, verbose=False,
+ encoding=None):
"""
expect should be a tuple of allowed exit codes. cwd should be
the directory from which the command will be executed. When
@@ -54,18 +57,29 @@ def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,),
"""
if cwd == None:
cwd = '.'
+ if isinstance(shell, types.StringTypes):
+ list_args = ' '.split(args) # sloppy, but just for logging
+ str_args = args
+ else:
+ list_args = args
+ str_args = ' '.join(args) # sloppy, but just for logging
if verbose == True:
- print >> sys.stderr, '%s$ %s' % (cwd, ' '.join(args))
+ print >> sys.stderr, '%s$ %s' % (cwd, str_args)
try :
if _POSIX:
- q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd)
+ if shell is None:
+ shell = False
+ q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr,
+ shell=shell, cwd=cwd)
else:
assert _MSWINDOWS==True, 'invalid platform'
+ if shell is None:
+ shell = True
# win32 don't have os.execvp() so have to run command in a shell
q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr,
- shell=True, cwd=cwd)
+ shell=shell, cwd=cwd)
except OSError, e:
- raise CommandError(args, status=e.args[0], stderr=e)
+ raise CommandError(list_args, status=e.args[0], stderr=e)
stdout,stderr = q.communicate(input=stdin)
status = q.wait()
if unicode_output == True:
@@ -78,18 +92,18 @@ def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,),
if verbose == True:
print >> sys.stderr, '%d\n%s%s' % (status, stdout, stderr)
if status not in expect:
- raise CommandError(args, status, stdout, stderr)
+ raise CommandError(list_args, status, stdout, stderr)
return status, stdout, stderr
class Pipe (object):
- """
- Simple interface for executing POSIX-style pipes based on the
- subprocess module. The only complication is the adaptation of
- subprocess.Popen._comminucate to listen to the stderrs of all
- processes involved in the pipe, as well as the terminal process'
- stdout. There are two implementations of Pipe._communicate, one
- for MS Windows, and one for POSIX systems. The MS Windows
- implementation is currently untested.
+ """Simple interface for executing POSIX-style pipes.
+
+ Based on the `subprocess` module. The only complication is the
+ adaptation of `subprocess.Popen._communicate` to listen to the
+ stderrs of all processes involved in the pipe, as well as the
+ terminal process' stdout. There are two implementations of
+ `Pipe._communicate`, one for MS Windows, and one for POSIX
+ systems. The MS Windows implementation is currently untested.
>>> p = Pipe([['find', '/etc/'], ['grep', '^/etc/ssh$']])
>>> p.stdout
diff --git a/libbe/util/tree.py b/libbe/util/tree.py
index f676b0b..6e2ffec 100644
--- a/libbe/util/tree.py
+++ b/libbe/util/tree.py
@@ -1,5 +1,6 @@
# Bugs Everywhere, a distributed bugtracker
-# Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.
diff --git a/libbe/util/utility.py b/libbe/util/utility.py
index 3f9a1b0..0bdacb6 100644
--- a/libbe/util/utility.py
+++ b/libbe/util/utility.py
@@ -1,4 +1,5 @@
# Copyright (C) 2005-2011 Aaron Bentley <abentley@panoramicfeedback.com>
+# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
@@ -28,6 +29,10 @@ import shutil
import tempfile
import time
import types
+try: # import core module, Python >= 2.5
+ from xml.etree import ElementTree
+except ImportError: # look for non-core module
+ from elementtree import ElementTree
import libbe
if libbe.TESTING == True:
diff --git a/libbe/version.py b/libbe/version.py
index b695b13..29f4eed 100644
--- a/libbe/version.py
+++ b/libbe/version.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python
-# Copyright (C) 2009-2011 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2009-2011 Chris Ball <cjb@laptop.org>
+# Gianluca Montecchi <gian@grys.it>
# W. Trevor King <wking@drexel.edu>
#
# This file is part of Bugs Everywhere.