aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values35
-rw-r--r--.be/bugs/14c65eab-b9f2-4d43-991d-2dac6c239fc4/values28
-rw-r--r--.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body1
-rw-r--r--.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values21
-rw-r--r--.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values2
-rw-r--r--.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values2
-rw-r--r--.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values2
-rw-r--r--.be/bugs/38bd9b8a-3325-4ee5-bb75-600dfb415285/values28
-rw-r--r--.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values2
-rw-r--r--.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values2
-rw-r--r--.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values2
-rw-r--r--.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values2
-rw-r--r--.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values2
-rw-r--r--.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values2
-rw-r--r--.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body1
-rw-r--r--.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values21
-rw-r--r--.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values2
-rw-r--r--.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values2
-rw-r--r--.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values2
-rwxr-xr-xbe29
-rw-r--r--becommands/__init__.py15
-rw-r--r--becommands/assign.py2
-rw-r--r--becommands/close.py2
-rw-r--r--becommands/comment.py9
-rw-r--r--becommands/diff.py2
-rw-r--r--becommands/help.py5
-rw-r--r--becommands/list.py199
-rw-r--r--becommands/new.py9
-rw-r--r--becommands/open.py2
-rw-r--r--becommands/set.py2
-rw-r--r--becommands/set_root.py1
-rw-r--r--becommands/severity.py22
-rw-r--r--becommands/show.py16
-rw-r--r--becommands/status.py (renamed from becommands/inprogress.py)53
-rw-r--r--becommands/template21
-rw-r--r--becommands/upgrade.py3
-rw-r--r--libbe/arch.py17
-rw-r--r--libbe/bug.py389
-rw-r--r--libbe/bugdir.py266
-rw-r--r--libbe/cmdutil.py58
-rw-r--r--libbe/diff.py15
-rw-r--r--libbe/mapfile.py86
-rw-r--r--libbe/names.py18
-rw-r--r--libbe/plugin.py1
-rw-r--r--libbe/template48
-rw-r--r--libbe/tests.py6
-rwxr-xr-xtest_usage.sh47
47 files changed, 799 insertions, 703 deletions
diff --git a/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values b/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values
deleted file mode 100644
index 68c357f..0000000
--- a/.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-creator=abentley
-
-
-
-
-
-
-severity=minor
-
-
-
-
-
-
-status=disabled
-
-
-
-
-
-
-summary=Oh, wait
-
-
-
-
-
-
-time=Fri, 03 Feb 2006 21:35:52 +0000
-
-
-
diff --git a/.be/bugs/14c65eab-b9f2-4d43-991d-2dac6c239fc4/values b/.be/bugs/14c65eab-b9f2-4d43-991d-2dac6c239fc4/values
deleted file mode 100644
index 33cacf2..0000000
--- a/.be/bugs/14c65eab-b9f2-4d43-991d-2dac6c239fc4/values
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-creator=abentley
-
-
-
-
-
-
-severity=minor
-
-
-
-
-
-
-status=closed
-
-
-
-
-
-
-summary=
-
-
-
diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body
new file mode 100644
index 0000000..d09a4be
--- /dev/null
+++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/body
@@ -0,0 +1 @@
+This seems to be taken care of.
diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values
new file mode 100644
index 0000000..6c7fb63
--- /dev/null
+++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Fri, 14 Nov 2008 05:00:43 +0000
+
+
+
+
+
+
+From=wking
+
+
+
diff --git a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values
index 3b96b7b..cf41641 100644
--- a/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values
+++ b/.be/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/values
@@ -15,7 +15,7 @@ severity=minor
-status=open
+status=fixed
diff --git a/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values b/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values
index b528771..02f718a 100644
--- a/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values
+++ b/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/values
@@ -15,7 +15,7 @@ severity=minor
-status=closed
+status=fixed
diff --git a/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values b/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values
index 2971ab4..08c3ae4 100644
--- a/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values
+++ b/.be/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec/values
@@ -15,7 +15,7 @@ severity=minor
-status=closed
+status=fixed
diff --git a/.be/bugs/38bd9b8a-3325-4ee5-bb75-600dfb415285/values b/.be/bugs/38bd9b8a-3325-4ee5-bb75-600dfb415285/values
deleted file mode 100644
index 33cacf2..0000000
--- a/.be/bugs/38bd9b8a-3325-4ee5-bb75-600dfb415285/values
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-creator=abentley
-
-
-
-
-
-
-severity=minor
-
-
-
-
-
-
-status=closed
-
-
-
-
-
-
-summary=
-
-
-
diff --git a/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values b/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values
index 5a7b54e..4d1cded 100644
--- a/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values
+++ b/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/values
@@ -15,7 +15,7 @@ severity=minor
-status=closed
+status=fixed
diff --git a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values
index 480386b..823e2bc 100644
--- a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values
+++ b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values
@@ -15,7 +15,7 @@ severity=minor
-status=disabled
+status=closed
diff --git a/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values b/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values
index 625495f..4622bc6 100644
--- a/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values
+++ b/.be/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428/values
@@ -15,7 +15,7 @@ severity=serious
-status=closed
+status=fixed
diff --git a/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values b/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values
index 93689fb..921528e 100644
--- a/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values
+++ b/.be/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb/values
@@ -15,7 +15,7 @@ severity=minor
-status=closed
+status=fixed
diff --git a/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values b/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values
index ff8a30a..2fa1905 100644
--- a/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values
+++ b/.be/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/values
@@ -22,7 +22,7 @@ severity=minor
-status=closed
+status=fixed
diff --git a/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values b/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values
index e854f0e..7e7f554 100644
--- a/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values
+++ b/.be/bugs/c592a1e8-f2c8-4dfb-8550-955123073947/values
@@ -15,7 +15,7 @@ severity=minor
-status=closed
+status=fixed
diff --git a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body
new file mode 100644
index 0000000..d7a57d9
--- /dev/null
+++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/body
@@ -0,0 +1 @@
+I dunno, bugs everywhere is such a great mental image... ;)
diff --git a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values
new file mode 100644
index 0000000..cb5a094
--- /dev/null
+++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values
@@ -0,0 +1,21 @@
+
+
+
+Content-type=text/plain
+
+
+
+
+
+
+Date=Sat, 15 Nov 2008 23:56:51 +0000
+
+
+
+
+
+
+From=wking
+
+
+
diff --git a/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values b/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values
index 8024d04..39b0fd7 100644
--- a/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values
+++ b/.be/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b/values
@@ -22,7 +22,7 @@ severity=minor
-status=closed
+status=fixed
diff --git a/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values b/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values
index 87a5ca5..ef82d6f 100644
--- a/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values
+++ b/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values
@@ -8,7 +8,7 @@ severity=fatal
-status=closed
+status=fixed
diff --git a/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values b/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values
index 233e336..bcf47f4 100644
--- a/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values
+++ b/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values
@@ -15,7 +15,7 @@ severity=minor
-status=closed
+status=fixed
diff --git a/be b/be
index 11eff74..ea7f65a 100755
--- a/be
+++ b/be
@@ -22,37 +22,16 @@ from libbe.bugdir import tree_root, create_bug_dir
from libbe import names, plugin, cmdutil
import sys
import os
-import becommands.severity
-import becommands.list
-import becommands.show
-import becommands.set_root
-import becommands.new
-import becommands.close
-import becommands.open
-import becommands.inprogress
-__doc__ = """Bugs Everywhere - Distributed bug tracking
-
-Supported becommands
- set-root: assign the root directory for bug tracking
- new: Create a new bug
- list: list bugs
- show: show a particular bug
- close: close a bug
- open: re-open a bug
- severity: %s
-
-Unimplemented becommands
- comment: append a comment to a bug
-""" % becommands.severity.__desc__
-
+import becommands
+__doc__ == cmdutil.help()
if len(sys.argv) == 1 or sys.argv[1] in ('--help', '-h'):
- cmdutil.print_command_list()
+ print cmdutil.help()
else:
try:
try:
- sys.exit(execute(sys.argv[1], sys.argv[2:]))
+ sys.exit(cmdutil.execute(sys.argv[1], sys.argv[2:]))
except KeyError, e:
raise UserError("Unknown command \"%s\"" % e.args[0])
except cmdutil.GetHelp:
diff --git a/becommands/__init__.py b/becommands/__init__.py
index e69de29..6b07378 100644
--- a/becommands/__init__.py
+++ b/becommands/__init__.py
@@ -0,0 +1,15 @@
+"Command plugins for the BugsEverywhere be script."
+
+__all__ = ["set_root", "set", "new", "remove", "list", "show", "close", "open",
+ "assign", "severity", "status", "target", "comment", "diff",
+ "upgrade", "help"]
+
+def import_all():
+ for i in __all__:
+ name = __name__ + "." + i
+ try:
+ __import__(name, globals(), locals(), [])
+ except ImportError:
+ print "Import of %s failed!" % (name,)
+
+import_all()
diff --git a/becommands/assign.py b/becommands/assign.py
index 38ece52..d595c1d 100644
--- a/becommands/assign.py
+++ b/becommands/assign.py
@@ -15,7 +15,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Assign an individual or group to fix a bug"""
-from libbe import bugdir, cmdutil, names
+from libbe import cmdutil, names
__desc__ = __doc__
def execute(args):
diff --git a/becommands/close.py b/becommands/close.py
index 52ab735..8e62b90 100644
--- a/becommands/close.py
+++ b/becommands/close.py
@@ -16,6 +16,8 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Close a bug"""
from libbe import cmdutil
+__desc__ = __doc__
+
def execute(args):
"""
>>> from libbe import tests
diff --git a/becommands/comment.py b/becommands/comment.py
index e3a1d93..5939490 100644
--- a/becommands/comment.py
+++ b/becommands/comment.py
@@ -15,8 +15,11 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Add a comment to a bug"""
-from libbe import bugdir, cmdutil, names, utility
+from libbe import cmdutil, names, utility
+from libbe.bug import new_comment
import os
+__desc__ = __doc__
+
def execute(args):
"""
>>> from libbe import tests, names
@@ -29,7 +32,7 @@ def execute(args):
u'This is a comment about a\\n'
>>> comment.From == names.creator()
True
- >>> comment.date <= int(time.time())
+ >>> comment.time <= int(time.time())
True
>>> comment.in_reply_to is None
True
@@ -62,7 +65,7 @@ def execute(args):
if not body.endswith('\n'):
body+='\n'
- comment = bugdir.new_comment(bug, body)
+ comment = new_comment(bug, body)
if parent_comment is not None:
comment.in_reply_to = parent_comment.uuid
comment.save()
diff --git a/becommands/diff.py b/becommands/diff.py
index 82ebb2c..5a3a7cf 100644
--- a/becommands/diff.py
+++ b/becommands/diff.py
@@ -18,6 +18,8 @@
"""Compare bug reports with older tree"""
from libbe import bugdir, diff, cmdutil
import os
+__desc__ = __doc__
+
def execute(args):
options, args = get_parser().parse_args(args)
if len(args) == 0:
diff --git a/becommands/help.py b/becommands/help.py
index aa4aa64..f6cfe3f 100644
--- a/becommands/help.py
+++ b/becommands/help.py
@@ -15,7 +15,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Print help for given subcommand"""
-from libbe import bugdir, cmdutil, names, utility
+from libbe import cmdutil, names, utility
+__desc__ = __doc__
def execute(args):
"""
@@ -25,7 +26,7 @@ def execute(args):
if len(args) > 1:
raise cmdutil.UserError("Too many arguments.")
if len(args) == 0:
- cmdutil.print_command_list()
+ print cmdutil.help()
else:
try:
print cmdutil.help(args[0])
diff --git a/becommands/list.py b/becommands/list.py
index d745702..59eb8ad 100644
--- a/becommands/list.py
+++ b/becommands/list.py
@@ -15,30 +15,86 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""List bugs"""
-from libbe import bugdir, cmdutil, names
+from libbe import cmdutil, names
+from libbe.bug import cmp_full, severity_values, status_values, \
+ active_status_values, inactive_status_values
import os
+__desc__ = __doc__
+
def execute(args):
options, args = get_parser().parse_args(args)
if len(args) > 0:
raise cmdutil.UsageError
- active = True
- severity = ("minor", "serious", "critical", "fatal")
- if options.wishlist:
- severity = ("wishlist",)
- if options.closed:
- active = False
tree = cmdutil.bug_tree()
- current_id = names.creator()
+ # select status
+ if options.status != None:
+ if options.status == "all":
+ status = status_values
+ else:
+ status = options.status.split(',')
+ else:
+ status = []
+ if options.active == True:
+ status.extend(list(active_status_values))
+ if options.unconfirmed == True:
+ status.append("unconfirmed")
+ if options.open == True:
+ status.append("opened")
+ if options.test == True:
+ status.append("test")
+ if status == []: # set the default value
+ status = active_status_values
+ # select severity
+ if options.severity != None:
+ if options.severity == "all":
+ severity = severity_values
+ else:
+ severity = options.severity.split(',')
+ else:
+ severity = []
+ if options.wishlist == True:
+ severity.extend("wishlist")
+ if options.important == True:
+ serious = severity_values.index("serious")
+ severity.append(list(severity_values[serious:]))
+ if severity == []: # set the default value
+ severity = severity_values
+ # select assigned
+ if options.assigned != None:
+ if options.assigned == "all":
+ assigned = "all"
+ else:
+ assigned = options.assigned.split(',')
+ else:
+ assigned = []
+ if options.mine == True:
+ assigned.extend('-')
+ if assigned == []: # set the default value
+ assigned = "all"
+ for i in range(len(assigned)):
+ if assigned[i] == '-':
+ assigned[i] = names.creator()
+ # select target
+ if options.target != None:
+ if options.target == "all":
+ target = "all"
+ else:
+ target = options.target.split(',')
+ else:
+ target = []
+ if options.cur_target == True:
+ target.append(tree.target)
+ if target == []: # set the default value
+ target = "all"
+
def filter(bug):
- if options.mine and bug.assigned != current_id:
+ if status != "all" and not bug.status in status:
+ return False
+ if severity != "all" and not bug.severity in severity:
return False
- if options.cur_target:
- if tree.target is None or bug.target != tree.target:
- return False
- if active is not None:
- if bug.active != active:
- return False
- if bug.severity not in severity:
+ if assigned != "all" and not bug.assigned in assigned:
+ return False
+ if target != "all" and not bug.target in target:
return False
return True
@@ -47,74 +103,67 @@ def execute(args):
if len(bugs) == 0:
print "No matching bugs found"
- my_target_bugs = []
- other_target_bugs = []
- unassigned_target_bugs = []
- my_bugs = []
- other_bugs = []
- unassigned_bugs = []
- if tree.target is not None:
- for bug in bugs:
- if bug.target != tree.target:
- continue
- if bug.assigned == current_id:
- my_target_bugs.append(bug)
- elif bug.assigned is None:
- unassigned_target_bugs.append(bug)
- else:
- other_target_bugs.append(bug)
-
- for bug in bugs:
- if tree.target is not None and bug.target == tree.target:
- continue
- if bug.assigned == current_id:
- my_bugs.append(bug)
- elif bug.assigned is None:
- unassigned_bugs.append(bug)
- else:
- other_bugs.append(bug)
-
- def list_bugs(cur_bugs, title, no_target=False):
- def cmp_date(bug1, bug2):
- return -cmp(bug1.time, bug2.time)
- cur_bugs.sort(cmp_date)
- cur_bugs.sort(bugdir.cmp_severity)
+ def list_bugs(cur_bugs, title=None, no_target=False):
+ cur_bugs.sort(cmp_full)
if len(cur_bugs) > 0:
- print cmdutil.underlined(title)
+ if title != None:
+ print cmdutil.underlined(title)
for bug in cur_bugs:
- print cmdutil.bug_summary(bug, all_bugs, no_target=no_target,
- shortlist=True)
+ print bug.string(all_bugs, shortlist=True),
- list_bugs(my_target_bugs,
- "Bugs assigned to you for target %s" % tree.target,
- no_target=True)
- list_bugs(unassigned_target_bugs,
- "Unassigned bugs for target %s" % tree.target, no_target=True)
- list_bugs(other_target_bugs,
- "Bugs assigned to others for target %s" % tree.target,
- no_target=True)
- list_bugs(my_bugs, "Bugs assigned to you")
- list_bugs(unassigned_bugs, "Unassigned bugs")
- list_bugs(other_bugs, "Bugs assigned to others")
-
+ list_bugs(bugs, no_target=False)
def get_parser():
parser = cmdutil.CmdOptionParser("be list [options]")
- parser.add_option("-w", "--wishlist", action="store_true", dest="wishlist",
- help="List bugs with 'wishlist' severity")
- parser.add_option("-c", "--closed", action="store_true", dest="closed",
- help="List closed bugs")
- parser.add_option("-m", "--mine", action="store_true", dest="mine",
- help="List only bugs assigned to you")
- parser.add_option("-t", "--cur-target", action="store_true",
- dest="cur_target",
- help="List only bugs for the current target")
+ parser.add_option("-s", "--status", metavar="STATUS", dest="status",
+ help="List options matching STATUS", default=None)
+ parser.add_option("-v", "--severity", metavar="SEVERITY", dest="severity",
+ help="List options matching SEVERITY", default=None)
+ parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned",
+ help="List options matching ASSIGNED", default=None)
+ parser.add_option("-t", "--target", metavar="TARGET", dest="target",
+ help="List options matching TARGET", default=None)
+ # boolean shortucts. All of these are special cases of long forms
+ bools = (("w", "wishlist", "List bugs with 'wishlist' severity"),
+ ("i", "important", "List bugs with >= 'serious' severity"),
+ ("A", "active", "List all active bugs"),
+ ("u", "unconfirmed", "List unconfirmed bugs"),
+ ("o", "open", "List open bugs"),
+ ("T", "test", "List bugs in testing"),
+ ("m", "mine", "List bugs assigned to you"),
+ ("c", "cur-target", "List bugs for the current target"))
+ for s in bools:
+ attr = s[1].replace('-','_')
+ short = "-%c" % s[0]
+ long = "--%s" % s[1]
+ help = s[2]
+ parser.add_option(short, long, action="store_true", dest=attr, help=help)
return parser
longhelp="""
-This command lists bugs. Options are cumulative, so that -mc will list only
-closed bugs assigned to you.
-"""
+This command lists bugs. There are several criteria that you can
+search by:
+ * status
+ * severity
+ * assigned (who the bug is assigned to)
+ * target (bugfix deadline)
+Allowed values for each criterion may be given in a comma seperated
+list. The special string "all" may be used with any of these options
+to match all values of the criterion.
+
+status
+ %s
+severity
+ %s
+assigned
+ free form, with the string '-' being a shortcut for yourself.
+target
+ free form
+
+In addition, there are some shortcut options that set boolean flags.
+The boolean options are ignored if the matching string option is used.
+""" % (','.join(status_values),
+ ','.join(severity_values))
def help():
return get_parser().help_str() + longhelp
diff --git a/becommands/new.py b/becommands/new.py
index 7bd2382..40ab3f5 100644
--- a/becommands/new.py
+++ b/becommands/new.py
@@ -15,7 +15,10 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Create a new bug"""
-from libbe import bugdir, cmdutil, names, utility
+from libbe import cmdutil, names, utility
+from libbe.bug import new_bug
+__desc__ = __doc__
+
def execute(args):
"""
>>> import os, time
@@ -41,11 +44,11 @@ def execute(args):
if len(args) != 1:
raise cmdutil.UserError("Please supply a summary message")
dir = cmdutil.bug_tree()
- bug = bugdir.new_bug(dir)
+ bug = new_bug(dir)
bug.summary = args[0]
bug.save()
bugs = (dir.list())
- print "Created bug with ID %s" % cmdutil.unique_name(bug, bugs)
+ print "Created bug with ID %s" % names.unique_name(bug, bugs)
def get_parser():
parser = cmdutil.CmdOptionParser("be new SUMMARY")
diff --git a/becommands/open.py b/becommands/open.py
index f7c23c1..654a1f5 100644
--- a/becommands/open.py
+++ b/becommands/open.py
@@ -16,6 +16,8 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Re-open a bug"""
from libbe import cmdutil
+__desc__ = __doc__
+
def execute(args):
"""
>>> from libbe import tests
diff --git a/becommands/set.py b/becommands/set.py
index e359df1..8a76133 100644
--- a/becommands/set.py
+++ b/becommands/set.py
@@ -16,6 +16,8 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Change tree settings"""
from libbe import cmdutil
+__desc__ = __doc__
+
def execute(args):
"""
>>> from libbe import tests
diff --git a/becommands/set_root.py b/becommands/set_root.py
index 2ae7e1a..cc21c31 100644
--- a/becommands/set_root.py
+++ b/becommands/set_root.py
@@ -16,6 +16,7 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Assign the root directory for bug tracking"""
from libbe import bugdir, cmdutil, rcs
+__desc__ = __doc__
def execute(args):
"""
diff --git a/becommands/severity.py b/becommands/severity.py
index af99bf7..6845875 100644
--- a/becommands/severity.py
+++ b/becommands/severity.py
@@ -15,8 +15,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Show or change a bug's severity level"""
-from libbe import bugdir
from libbe import cmdutil
+from libbe.bug import severity_values, severity_description
__desc__ = __doc__
def execute(args):
@@ -46,7 +46,7 @@ def execute(args):
elif len(args) == 2:
try:
bug.severity = args[1]
- except bugdir.InvalidValue, e:
+ except ValueError, e:
if e.name != "severity":
raise
raise cmdutil.UserError ("Invalid severity level: %s" % e.value)
@@ -56,19 +56,21 @@ def get_parser():
parser = cmdutil.CmdOptionParser("be severity bug-id [severity]")
return parser
-longhelp="""
-Show or change a bug's severity level.
+longhelp=["""
+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:
-wishlist: A feature that could improve usefulness, but not a bug.
- minor: The standard bug level.
- serious: A bug that requires workarounds.
-critical: A bug that prevents some features from working at all.
- fatal: A bug that makes the package unusable.
-"""
+"""]
+longest_severity_len = max([len(s) for s in severity_values])
+for severity in severity_values :
+ description = severity_description[severity]
+ s = "%*s : %s\n" % (longest_severity_len, severity, description)
+ longhelp.append(s)
+longhelp = ''.join(longhelp)
+
def help():
return get_parser().help_str() + longhelp
diff --git a/becommands/show.py b/becommands/show.py
index 8e83a1f..669a81d 100644
--- a/becommands/show.py
+++ b/becommands/show.py
@@ -15,8 +15,10 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Show a particular bug"""
-from libbe import bugdir, cmdutil, utility
+from libbe import cmdutil, names, utility
+from libbe.bug import thread_comments
import os
+__desc__ = __doc__
def execute(args):
options, args = get_parser().parse_args(args)
@@ -24,20 +26,14 @@ def execute(args):
raise cmdutil.UserError("Please specify a bug id.")
bug_dir = cmdutil.bug_tree()
bug = cmdutil.get_bug(args[0], bug_dir)
- print cmdutil.bug_summary(bug, list(bug_dir.list())).rstrip("\n")
- if bug.time is None:
- time_str = "(Unknown time)"
- else:
- time_str = "%s (%s)" % (utility.handy_time(bug.time),
- utility.time_to_str(bug.time))
- print "Created: %s" % time_str
- unique_name = cmdutil.unique_name(bug, bug_dir.list())
+ print bug.string().rstrip("\n")
+ unique_name = names.unique_name(bug, bug_dir.list())
comments = []
name_map = {}
for c_name, comment in cmdutil.iter_comment_name(bug, unique_name):
name_map[comment.uuid] = c_name
comments.append(comment)
- threaded = bugdir.thread_comments(comments)
+ threaded = thread_comments(comments)
cmdutil.print_threaded_comments(threaded, name_map)
def get_parser():
diff --git a/becommands/inprogress.py b/becommands/status.py
index 05da971..b57db4e 100644
--- a/becommands/inprogress.py
+++ b/becommands/status.py
@@ -14,35 +14,62 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-"""Bug fixing in progress"""
+"""Show or change a bug's status"""
from libbe import cmdutil
+from libbe.bug import status_values, status_description
+__desc__ = __doc__
+
def execute(args):
"""
>>> from libbe import tests
>>> import os
>>> dir = tests.simple_bug_dir()
>>> os.chdir(dir.dir)
- >>> dir.get_bug("a").status
- u'open'
>>> execute(["a"])
- >>> dir.get_bug("a").status
- u'in-progress'
+ open
+ >>> execute(["a", "closed"])
+ >>> execute(["a"])
+ closed
+ >>> execute(["a", "none"])
+ Traceback (most recent call last):
+ UserError: Invalid status: none
>>> tests.clean_up()
"""
options, args = get_parser().parse_args(args)
- if len(args) !=1:
- raise cmdutil.UserError("Please specify a bug id.")
+ assert(len(args) in (0, 1, 2))
+ if len(args) == 0:
+ print help()
+ return
bug = cmdutil.get_bug(args[0])
- bug.status = "in-progress"
- bug.save()
+ if len(args) == 1:
+ print bug.status
+ elif len(args) == 2:
+ try:
+ bug.status = args[1]
+ except ValueError, e:
+ if e.name != "status":
+ raise
+ raise cmdutil.UserError ("Invalid status: %s" % e.value)
+ bug.save()
def get_parser():
- parser = cmdutil.CmdOptionParser("be inprogress BUG-ID")
+ parser = cmdutil.CmdOptionParser("be status bug-id [status]")
return parser
-longhelp="""
-Mark a bug as 'in-progress'.
-"""
+longhelp=["""
+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:
+"""]
+longest_status_len = max([len(s) for s in status_values])
+for status in status_values :
+ description = status_description[status]
+ s = "%*s : %s\n" % (longest_status_len, status, description)
+ longhelp.append(s)
+longhelp = ''.join(longhelp)
def help():
return get_parser().help_str() + longhelp
diff --git a/becommands/template b/becommands/template
deleted file mode 100644
index 3c871e6..0000000
--- a/becommands/template
+++ /dev/null
@@ -1,21 +0,0 @@
-"""Short description"""
-from libbe import bugdir, cmdutil, names
-import os
-def execute(args):
- options, args = get_parser().parse_args(args)
- if len(args) > 0:
- raise cmdutil.UsageError
-
-
-def get_parser():
- parser = cmdutil.CmdOptionParser("be list [options]")
-# parser.add_option("-w", "--wishlist", action="store_true", dest="wishlist",
-# help="List bugs with 'wishlist' severity")
- return parser
-
-longhelp="""
-This is for the longwinded description
-"""
-
-def help():
- return get_parser().help_str() + longhelp
diff --git a/becommands/upgrade.py b/becommands/upgrade.py
index 3dcb4eb..7ed3630 100644
--- a/becommands/upgrade.py
+++ b/becommands/upgrade.py
@@ -18,6 +18,7 @@
import os.path
import errno
from libbe import bugdir, rcs, cmdutil
+__desc__ = __doc__
def execute(args):
options, args = get_parser().parse_args(args)
@@ -25,7 +26,7 @@ def execute(args):
for uuid in root.list_uuids():
old_bug = OldBug(root.bugs_path, uuid)
- new_bug = bugdir.Bug(root.bugs_path, None)
+ new_bug = root.get_bug(uuid)
new_bug.uuid = old_bug.uuid
new_bug.summary = old_bug.summary
new_bug.creator = old_bug.creator
diff --git a/libbe/arch.py b/libbe/arch.py
index 038325a..001f852 100644
--- a/libbe/arch.py
+++ b/libbe/arch.py
@@ -14,28 +14,17 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-from subprocess import Popen, PIPE
import os
import config
import errno
+
+from rcs import invoke
+
client = config.get_val("arch_client")
if client is None:
client = "tla"
config.set_val("arch_client", client)
-def invoke(args):
- try :
- q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- except OSError, e :
- strerror = "%s\nwhile executing %s" % (e.args[1], args)
- raise Exception("Command failed: %s" % strerror)
- output = q.stdout.read()
- error = q.stderr.read()
- status = q.wait()
- if status >= 0:
- return status, output, error
- raise Exception("Command failed: %s" % error)
-
def invoke_client(*args, **kwargs):
cl_args = [client]
diff --git a/libbe/bug.py b/libbe/bug.py
new file mode 100644
index 0000000..f973cf0
--- /dev/null
+++ b/libbe/bug.py
@@ -0,0 +1,389 @@
+# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
+# <abentley@panoramicfeedback.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+import os
+import os.path
+import shutil
+import errno
+import names
+import mapfile
+import time
+import utility
+from rcs import rcs_by_name
+
+
+### 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/
+
+# in order of increasing severity
+severity_level_def = (
+ ("wishlist","A feature that could improve usefullness, but not a bug."),
+ ("minor","The standard bug level."),
+ ("serious","A bug that requires workarounds."),
+ ("critical","A bug that prevents some features from working at all."),
+ ("fatal","A bug that makes the package unusable."))
+
+# in order of increasing resolution
+# roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
+active_status_def = (
+ ("unconfirmed","A possible bug which lacks independent existance confirmation."),
+ ("open","A working bug that has not been assigned to a developer."),
+ ("assigned","A working bug that has been assigned to a developer."),
+ ("test","The code has been adjusted, but the fix is still being tested."))
+inactive_status_def = (
+ ("closed", "The bug is no longer relevant."),
+ ("fixed", "The bug should no longer occur."),
+ ("wontfix","It's not a bug, it's a feature."),
+ ("disabled", "?"))
+
+
+### Convert the description tuples to more useful formats
+
+severity_values = tuple([val for val,description in severity_level_def])
+severity_description = dict(severity_level_def)
+severity_index = {}
+for i in range(len(severity_values)):
+ severity_index[severity_values[i]] = i
+
+active_status_values = tuple(val for val,description in active_status_def)
+inactive_status_values = tuple(val for val,description in inactive_status_def)
+status_values = active_status_values + inactive_status_values
+status_description = dict(active_status_def+inactive_status_def)
+status_index = {}
+for i in range(len(status_values)):
+ status_index[status_values[i]] = i
+
+
+def checked_property(name, valid):
+ """
+ Provide access to an attribute name, testing for valid values.
+ """
+ def getter(self):
+ value = getattr(self, "_"+name)
+ if value not in valid:
+ raise InvalidValue(name, value)
+ return value
+
+ def setter(self, value):
+ if value not in valid:
+ raise InvalidValue(name, value)
+ return setattr(self, "_"+name, value)
+ return property(getter, setter)
+
+
+class Bug(object):
+ severity = checked_property("severity", severity_values)
+ status = checked_property("status", status_values)
+
+ def __init__(self, path, uuid, rcs_name, bugdir):
+ self.path = path
+ self.uuid = uuid
+ if uuid is not None:
+ dict = mapfile.map_load(self.get_path("values"))
+ else:
+ dict = {}
+
+ self.rcs_name = rcs_name
+ self.bugdir = bugdir
+
+ self.summary = dict.get("summary")
+ self.creator = dict.get("creator")
+ self.target = dict.get("target")
+ self.status = dict.get("status", "open")
+ self.severity = dict.get("severity", "minor")
+ self.assigned = dict.get("assigned")
+ self.time = dict.get("time")
+ if self.time is not None:
+ self.time = utility.str_to_time(self.time)
+
+ def __repr__(self):
+ return "Bug(uuid=%r)" % self.uuid
+
+ def string(self, bugs=None, shortlist=False):
+ if bugs == None:
+ bugs = list(self.bugdir.list())
+ short_name = names.unique_name(self, bugs)
+ if shortlist == False:
+ if self.time == None:
+ timestring = ""
+ else:
+ htime = utility.handy_time(self.time)
+ ftime = utility.time_to_str(self.time)
+ timestring = "%s (%s)" % (htime, ftime)
+ info = [("ID", self.uuid),
+ ("Short name", short_name),
+ ("Severity", self.severity),
+ ("Status", self.status),
+ ("Assigned", self.assigned),
+ ("Target", self.target),
+ ("Creator", self.creator),
+ ("Created", timestring)]
+ newinfo = []
+ for k,v in info:
+ if v == None:
+ newinfo.append((k,""))
+ else:
+ newinfo.append((k,v))
+ info = newinfo
+ 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]
+ return "".join(infolines) + "%s\n" % self.summary
+ else:
+ statuschar = self.status[0]
+ severitychar = self.severity[0]
+ chars = "%c%c" % (statuschar, severitychar)
+ return "%s:%s: %s\n" % (short_name, chars, self.summary)
+ def __str__(self):
+ return self.string(shortlist=True)
+ def get_path(self, file=None):
+ if file == None:
+ return os.path.join(self.path, self.uuid)
+ else:
+ return os.path.join(self.path, self.uuid, file)
+
+ def _get_active(self):
+ return self.status in active_status_values
+
+ active = property(_get_active)
+
+ def add_attr(self, map, name):
+ value = getattr(self, name)
+ if value is not None:
+ map[name] = value
+
+ def save(self):
+ map = {}
+ self.add_attr(map, "assigned")
+ self.add_attr(map, "summary")
+ self.add_attr(map, "creator")
+ self.add_attr(map, "target")
+ self.add_attr(map, "status")
+ self.add_attr(map, "severity")
+ if self.time is not None:
+ map["time"] = utility.time_to_str(self.time)
+ path = self.get_path("values")
+ mapfile.map_save(rcs_by_name(self.rcs_name), path, map)
+
+ def remove(self):
+ path = self.get_path()
+ shutil.rmtree(path)
+
+ def _get_rcs(self):
+ return rcs_by_name(self.rcs_name)
+
+ rcs = property(_get_rcs)
+
+ def new_comment(self):
+ if not os.path.exists(self.get_path("comments")):
+ self.rcs.mkdir(self.get_path("comments"))
+ comm = Comment(None, self)
+ comm.uuid = names.uuid()
+ return comm
+
+ def get_comment(self, uuid):
+ return Comment(uuid, self)
+
+ def iter_comment_ids(self):
+ path = self.get_path("comments")
+ if not os.path.isdir(path):
+ return
+ try:
+ for uuid in os.listdir(path):
+ if (uuid.startswith('.')):
+ continue
+ yield uuid
+ except OSError, e:
+ if e.errno != errno.ENOENT:
+ raise
+ return
+
+ def list_comments(self):
+ comments = [Comment(id, self) for id in self.iter_comment_ids()]
+ comments.sort(cmp_time)
+ return comments
+
+def new_bug(dir, uuid=None):
+ bug = dir.new_bug(uuid)
+ bug.creator = names.creator()
+ bug.severity = "minor"
+ bug.status = "open"
+ bug.time = time.time()
+ return bug
+
+def new_comment(bug, body=None):
+ comm = bug.new_comment()
+ comm.From = names.creator()
+ comm.time = time.time()
+ comm.body = body
+ return comm
+
+def add_headers(obj, map, names):
+ map_names = {}
+ for name in names:
+ map_names[name] = pyname_to_header(name)
+ add_attrs(obj, map, names, map_names)
+
+def add_attrs(obj, map, names, map_names=None):
+ if map_names is None:
+ map_names = {}
+ for name in names:
+ map_names[name] = name
+
+ for name in names:
+ value = getattr(obj, name)
+ if value is not None:
+ map[map_names[name]] = value
+
+
+class Comment(object):
+ def __init__(self, uuid, bug):
+ object.__init__(self)
+ self.uuid = uuid
+ self.bug = bug
+ if self.uuid is not None and self.bug is not None:
+ map = mapfile.map_load(self.get_path("values"))
+ self.time = utility.str_to_time(map["Date"])
+ self.From = map["From"]
+ self.in_reply_to = map.get("In-reply-to")
+ self.content_type = map.get("Content-type", "text/plain")
+ self.body = file(self.get_path("body")).read().decode("utf-8")
+ else:
+ self.time = None
+ self.From = None
+ self.in_reply_to = None
+ self.content_type = "text/plain"
+ self.body = None
+
+ def save(self):
+ map_file = {"Date": utility.time_to_str(self.time)}
+ add_headers(self, map_file, ("From", "in_reply_to", "content_type"))
+ if not os.path.exists(self.get_path()):
+ self.bug.rcs.mkdir(self.get_path())
+ mapfile.map_save(self.bug.rcs, self.get_path("values"), map_file)
+ self.bug.rcs.set_file_contents(self.get_path("body"),
+ self.body.encode('utf-8'))
+
+
+ def get_path(self, name=None):
+ my_dir = os.path.join(self.bug.get_path("comments"), self.uuid)
+ if name is None:
+ return my_dir
+ return os.path.join(my_dir, name)
+
+
+def thread_comments(comments):
+ child_map = {}
+ top_comments = []
+ for comment in comments:
+ child_map[comment.uuid] = []
+ for comment in comments:
+ if comment.in_reply_to is None or comment.in_reply_to not in child_map:
+ top_comments.append(comment)
+ continue
+ child_map[comment.in_reply_to].append(comment)
+
+ def recurse_children(comment):
+ child_list = []
+ for child in child_map[comment.uuid]:
+ child_list.append(recurse_children(child))
+ return (comment, child_list)
+ return [recurse_children(c) for c in top_comments]
+
+def pyname_to_header(name):
+ return name.capitalize().replace('_', '-')
+
+
+
+class MockBug:
+ def __init__(self, attr, value):
+ setattr(self, attr, value)
+
+# the general rule for bug sorting is that "more important" bugs are
+# less than "less important" bugs. This way sorting a list of bugs
+# will put the most important bugs first in the list. When relative
+# importance is unclear, the sorting follows some arbitrary convention
+# (i.e. dictionary order).
+
+def cmp_severity(bug_1, bug_2):
+ """
+ Compare the severity levels of two bugs, with more severe bugs comparing
+ as less.
+
+ >>> attr="severity"
+ >>> cmp_severity(MockBug(attr,"wishlist"), MockBug(attr,"wishlist")) == 0
+ True
+ >>> cmp_severity(MockBug(attr,"wishlist"), MockBug(attr,"minor")) > 0
+ True
+ >>> cmp_severity(MockBug(attr,"critical"), MockBug(attr,"wishlist")) < 0
+ True
+ """
+ return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
+
+def cmp_status(bug_1, bug_2):
+ """
+ Compare the status levels of two bugs, with more 'open' bugs
+ comparing as less.
+
+ >>> attr="status"
+ >>> cmp_status(MockBug(attr,"open"), MockBug(attr,"open")) == 0
+ True
+ >>> cmp_status(MockBug(attr,"open"), MockBug(attr,"closed")) < 0
+ True
+ >>> cmp_status(MockBug(attr,"closed"), MockBug(attr,"open")) > 0
+ True
+ """
+ val_2 = status_index[bug_2.status]
+ return cmp(status_index[bug_1.status], status_index[bug_2.status])
+
+def cmp_attr(bug_1, bug_2, attr, invert=False):
+ """
+ Compare a general attribute between two bugs using the conventional
+ comparison rule for that attribute type. If invert == True, sort
+ *against* that convention.
+ >>> attr="severity"
+ >>> cmp_attr(MockBug(attr,1), MockBug(attr,2), attr, invert=False) < 0
+ True
+ >>> cmp_attr(MockBug(attr,1), MockBug(attr,2), attr, invert=True) > 0
+ True
+ >>> cmp_attr(MockBug(attr,1), MockBug(attr,1), attr) == 0
+ True
+ """
+ if invert == True :
+ return -cmp(getattr(bug_1, attr), getattr(bug_2, attr))
+ else :
+ return cmp(getattr(bug_1, attr), getattr(bug_2, attr))
+
+# alphabetical rankings (a < z)
+cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
+cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
+# chronological rankings (newer < older)
+cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
+
+def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
+ cmp_time,cmp_creator)):
+ for comparison in cmp_list :
+ val = comparison(bug_1, bug_2)
+ if val != 0 :
+ return val
+ return 0
+
+class InvalidValue(ValueError):
+ def __init__(self, name, value):
+ msg = "Cannot assign value %s to %s" % (value, name)
+ Exception.__init__(self, msg)
+ self.name = name
+ self.value = value
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index 427ed38..f8f45b8 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -23,6 +23,7 @@ import mapfile
import time
import utility
from rcs import rcs_by_name
+from bug import Bug
class NoBugDir(Exception):
def __init__(self, path):
@@ -108,7 +109,8 @@ def create_bug_dir(path, rcs):
raise
rcs.mkdir(os.path.join(root, "bugs"))
set_version(root, rcs)
- map_save(rcs, os.path.join(root, "settings"), {"rcs_name": rcs.name})
+ mapfile.map_save(rcs,
+ os.path.join(root, "settings"), {"rcs_name": rcs.name})
return BugDir(os.path.join(path, ".be"))
@@ -137,8 +139,8 @@ class BugDir:
self.dir = dir
self.bugs_path = os.path.join(self.dir, "bugs")
try:
- self.settings = map_load(os.path.join(self.dir, "settings"))
- except NoSuchFile:
+ self.settings = mapfile.map_load(os.path.join(self.dir, "settings"))
+ except mapfile.NoSuchFile:
self.settings = {"rcs_name": "None"}
rcs_name = setting_property("rcs_name", ("None", "bzr", "git", "Arch", "hg"))
@@ -147,7 +149,8 @@ class BugDir:
target = setting_property("target")
def save_settings(self):
- map_save(self.rcs, os.path.join(self.dir, "settings"), self.settings)
+ mapfile.map_save(self.rcs,
+ os.path.join(self.dir, "settings"), self.settings)
def get_rcs(self):
if self._rcs is not None and self.rcs_name == self._rcs.name:
@@ -171,7 +174,7 @@ class BugDir:
return bugs
def get_bug(self, uuid):
- return Bug(self.bugs_path, uuid, self.rcs_name)
+ return Bug(self.bugs_path, uuid, self.rcs_name, self)
def list_uuids(self):
for uuid in os.listdir(self.bugs_path):
@@ -184,262 +187,13 @@ class BugDir:
uuid = names.uuid()
path = os.path.join(self.bugs_path, uuid)
self.rcs.mkdir(path)
- bug = Bug(self.bugs_path, None, self.rcs_name)
+ bug = Bug(self.bugs_path, None, self.rcs_name, self)
bug.uuid = uuid
return bug
-class InvalidValue(Exception):
+class InvalidValue(ValueError):
def __init__(self, name, value):
msg = "Cannot assign value %s to %s" % (value, name)
Exception.__init__(self, msg)
self.name = name
self.value = value
-
-
-def checked_property(name, valid):
- def getter(self):
- value = self.__getattribute__("_"+name)
- if value not in valid:
- raise InvalidValue(name, value)
- return value
-
- def setter(self, value):
- if value not in valid:
- raise InvalidValue(name, value)
- return self.__setattr__("_"+name, value)
- return property(getter, setter)
-
-severity_levels = ("wishlist", "minor", "serious", "critical", "fatal")
-active_status = ("open", "in-progress", "waiting", "new", "verified")
-inactive_status = ("closed", "disabled", "fixed", "wontfix", "waiting")
-
-severity_value = {}
-for i in range(len(severity_levels)):
- severity_value[severity_levels[i]] = i
-
-class Bug(object):
- status = checked_property("status", (None,)+active_status+inactive_status)
- severity = checked_property("severity", (None, "wishlist", "minor",
- "serious", "critical", "fatal"))
-
- def __init__(self, path, uuid, rcs_name):
- self.path = path
- self.uuid = uuid
- if uuid is not None:
- dict = map_load(self.get_path("values"))
- else:
- dict = {}
-
- self.rcs_name = rcs_name
-
- self.summary = dict.get("summary")
- self.creator = dict.get("creator")
- self.target = dict.get("target")
- self.status = dict.get("status")
- self.severity = dict.get("severity")
- self.assigned = dict.get("assigned")
- self.time = dict.get("time")
- if self.time is not None:
- self.time = utility.str_to_time(self.time)
-
- def __repr__(self):
- return "Bug(uuid=%r)" % self.uuid
-
- def get_path(self, file):
- return os.path.join(self.path, self.uuid, file)
-
- def _get_active(self):
- return self.status in active_status
-
- active = property(_get_active)
-
- def add_attr(self, map, name):
- value = self.__getattribute__(name)
- if value is not None:
- map[name] = value
-
- def save(self):
- map = {}
- self.add_attr(map, "assigned")
- self.add_attr(map, "summary")
- self.add_attr(map, "creator")
- self.add_attr(map, "target")
- self.add_attr(map, "status")
- self.add_attr(map, "severity")
- if self.time is not None:
- map["time"] = utility.time_to_str(self.time)
- path = self.get_path("values")
- map_save(rcs_by_name(self.rcs_name), path, map)
-
- def _get_rcs(self):
- return rcs_by_name(self.rcs_name)
-
- rcs = property(_get_rcs)
-
- def new_comment(self):
- if not os.path.exists(self.get_path("comments")):
- self.rcs.mkdir(self.get_path("comments"))
- comm = Comment(None, self)
- comm.uuid = names.uuid()
- return comm
-
- def get_comment(self, uuid):
- return Comment(uuid, self)
-
- def iter_comment_ids(self):
- path = self.get_path("comments")
- if not os.path.isdir(path):
- return
- try:
- for uuid in os.listdir(path):
- if (uuid.startswith('.')):
- continue
- yield uuid
- except OSError, e:
- if e.errno != errno.ENOENT:
- raise
- return
-
- def list_comments(self):
- comments = [Comment(id, self) for id in self.iter_comment_ids()]
- comments.sort(cmp_date)
- return comments
-
-def cmp_date(comm1, comm2):
- return cmp(comm1.date, comm2.date)
-
-def new_bug(dir, uuid=None):
- bug = dir.new_bug(uuid)
- bug.creator = names.creator()
- bug.severity = "minor"
- bug.status = "open"
- bug.time = time.time()
- return bug
-
-def new_comment(bug, body=None):
- comm = bug.new_comment()
- comm.From = names.creator()
- comm.date = time.time()
- comm.body = body
- return comm
-
-def add_headers(obj, map, names):
- map_names = {}
- for name in names:
- map_names[name] = pyname_to_header(name)
- add_attrs(obj, map, names, map_names)
-
-def add_attrs(obj, map, names, map_names=None):
- if map_names is None:
- map_names = {}
- for name in names:
- map_names[name] = name
-
- for name in names:
- value = obj.__getattribute__(name)
- if value is not None:
- map[map_names[name]] = value
-
-
-class Comment(object):
- def __init__(self, uuid, bug):
- object.__init__(self)
- self.uuid = uuid
- self.bug = bug
- if self.uuid is not None and self.bug is not None:
- mapfile = map_load(self.get_path("values"))
- self.date = utility.str_to_time(mapfile["Date"])
- self.From = mapfile["From"]
- self.in_reply_to = mapfile.get("In-reply-to")
- self.content_type = mapfile.get("Content-type", "text/plain")
- self.body = file(self.get_path("body")).read().decode("utf-8")
- else:
- self.date = None
- self.From = None
- self.in_reply_to = None
- self.content_type = "text/plain"
- self.body = None
-
- def save(self):
- map_file = {"Date": utility.time_to_str(self.date)}
- add_headers(self, map_file, ("From", "in_reply_to", "content_type"))
- if not os.path.exists(self.get_path(None)):
- self.bug.rcs.mkdir(self.get_path(None))
- map_save(self.bug.rcs, self.get_path("values"), map_file)
- self.bug.rcs.set_file_contents(self.get_path("body"),
- self.body.encode('utf-8'))
-
-
- def get_path(self, name):
- my_dir = os.path.join(self.bug.get_path("comments"), self.uuid)
- if name is None:
- return my_dir
- return os.path.join(my_dir, name)
-
-
-def thread_comments(comments):
- child_map = {}
- top_comments = []
- for comment in comments:
- child_map[comment.uuid] = []
- for comment in comments:
- if comment.in_reply_to is None or comment.in_reply_to not in child_map:
- top_comments.append(comment)
- continue
- child_map[comment.in_reply_to].append(comment)
-
- def recurse_children(comment):
- child_list = []
- for child in child_map[comment.uuid]:
- child_list.append(recurse_children(child))
- return (comment, child_list)
- return [recurse_children(c) for c in top_comments]
-
-
-def pyname_to_header(name):
- return name.capitalize().replace('_', '-')
-
-
-def map_save(rcs, path, map):
- """Save the map as a mapfile to the specified path"""
- add = not os.path.exists(path)
- output = file(path, "wb")
- mapfile.generate(output, map)
- if add:
- rcs.add_id(path)
-
-class NoSuchFile(Exception):
- def __init__(self, pathname):
- Exception.__init__(self, "No such file: %s" % pathname)
-
-
-def map_load(path):
- try:
- return mapfile.parse(file(path, "rb"))
- except IOError, e:
- if e.errno != errno.ENOENT:
- raise e
- raise NoSuchFile(path)
-
-
-class MockBug:
- def __init__(self, severity):
- self.severity = severity
-
-def cmp_severity(bug_1, bug_2):
- """
- Compare the severity levels of two bugs, with more sever bugs comparing
- as less.
-
- >>> cmp_severity(MockBug(None), MockBug(None))
- 0
- >>> cmp_severity(MockBug("wishlist"), MockBug(None)) < 0
- True
- >>> cmp_severity(MockBug(None), MockBug("wishlist")) > 0
- True
- >>> cmp_severity(MockBug("critical"), MockBug("wishlist")) < 0
- True
- """
- val_1 = severity_value.get(bug_1.severity)
- val_2 = severity_value.get(bug_2.severity)
- return -cmp(val_1, val_2)
diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py
index 079601e..ace2d81 100644
--- a/libbe/cmdutil.py
+++ b/libbe/cmdutil.py
@@ -23,17 +23,6 @@ from textwrap import TextWrapper
from StringIO import StringIO
import utility
-def unique_name(bug, bugs):
- chars = 1
- for some_bug in bugs:
- if bug.uuid == some_bug.uuid:
- continue
- while (bug.uuid[:chars] == some_bug.uuid[:chars]):
- chars+=1
- if chars < 3:
- chars = 3
- return bug.uuid[:chars]
-
class UserError(Exception):
def __init__(self, msg):
Exception.__init__(self, msg)
@@ -65,23 +54,6 @@ def get_bug(spec, bug_dir=None):
raise UserError("No bug matches %s" % spec)
return matches[0]
-def bug_summary(bug, bugs, no_target=False, shortlist=False):
- target = bug.target
- if target is None or no_target:
- target = ""
- else:
- target = " Target: %s" % target
- if bug.assigned is None:
- assigned = ""
- else:
- assigned = " Assigned: %s" % bug.assigned
- if shortlist == False:
- return " ID: %s\n Severity: %s\n%s%s\n Creator: %s \n%s\n" % \
- (unique_name(bug, bugs), bug.severity, assigned, target,
- bug.creator, bug.summary)
- else:
- return "%4s: %s\n" % (unique_name(bug, bugs), bug.summary)
-
def iter_commands():
for name, module in plugin.iter_plugins("becommands"):
yield name.replace("_", "-"), module
@@ -104,9 +76,20 @@ def execute(cmd, args):
encoding = locale.getpreferredencoding() or 'ascii'
return get_command(cmd).execute([a.decode(encoding) for a in args])
-def help(cmd):
- return get_command(cmd).help()
-
+def help(cmd=None):
+ if cmd != None:
+ return get_command(cmd).help()
+ else:
+ cmdlist = []
+ for name, module in iter_commands():
+ cmdlist.append((name, module.__desc__))
+ longest_cmd_len = max([len(name) for name,desc in cmdlist])
+ ret = ["Bugs Everywhere - Distributed bug tracking\n",
+ "Supported commands"]
+ for name, desc in cmdlist:
+ numExtraSpaces = longest_cmd_len-len(name)
+ ret.append("be %s%*s %s" % (name, numExtraSpaces, "", desc))
+ return "\n".join(ret)
class GetHelp(Exception):
pass
@@ -125,7 +108,7 @@ def iter_comment_name(bug, unique_name):
(This is a user-friendly id, not the comment uuid)
"""
def key(comment):
- return comment.date
+ return comment.time
for num, comment in enumerate(sorted(bug.list_comments(), key=key)):
yield ("%s:%d" % (unique_name, num+1), comment)
@@ -187,7 +170,7 @@ def print_threaded_comments(comments, name_map, indent=""):
print >> s, "--------- Comment ---------"
print >> s, "Name: %s" % name_map[comment.uuid]
print >> s, "From: %s" % comment.From
- print >> s, "Date: %s\n" % utility.time_to_str(comment.date)
+ print >> s, "Date: %s\n" % utility.time_to_str(comment.time)
print >> s, comment.body.rstrip('\n')
s.seek(0)
@@ -215,15 +198,6 @@ def bug_tree(dir=None):
except bugdir.NoBugDir, e:
raise UserErrorWrap(e)
-def print_command_list():
- cmdlist = []
- print """Bugs Everywhere - Distributed bug tracking
-
-Supported commands"""
- for name, module in iter_commands():
- cmdlist.append((name, module.__doc__))
- for name, desc in cmdlist:
- print "be %s\n %s" % (name, desc)
def _test():
import doctest
diff --git a/libbe/diff.py b/libbe/diff.py
index c1dc429..7a1dbcc 100644
--- a/libbe/diff.py
+++ b/libbe/diff.py
@@ -17,6 +17,7 @@
"""Compare two bug trees"""
from libbe import cmdutil, bugdir
from libbe.utility import time_to_str
+from libbe.bug import cmp_severity
def diff(old_tree, new_tree):
old_bug_map = old_tree.bug_map()
@@ -44,16 +45,16 @@ def diff_report(diff_data, bug_dir):
(removed, modified, added) = diff_data
bugs = list(bug_dir.list())
def modified_cmp(left, right):
- return bugdir.cmp_severity(left[1], right[1])
+ return cmp_severity(left[1], right[1])
- added.sort(bugdir.cmp_severity)
- removed.sort(bugdir.cmp_severity)
+ added.sort(cmp_severity)
+ removed.sort(cmp_severity)
modified.sort(modified_cmp)
if len(added) > 0:
print "New bug reports:"
for bug in added:
- print cmdutil.bug_summary(bug, bugs, no_target=True)
+ print bug.string(shortlist=True)
if len(modified) > 0:
printed = False
@@ -69,7 +70,7 @@ def diff_report(diff_data, bug_dir):
if len(removed) > 0:
print "Removed bug reports:"
for bug in removed:
- print cmdutil.bug_summary(bug, bugs, no_target=True)
+ print bug.string(bugs, shortlist=True)
def change_lines(old, new, attributes):
change_list = []
@@ -101,10 +102,10 @@ def bug_changes(old, new, bugs):
if len(change_strings) == 0:
return None
- return "%s%s\n" % (cmdutil.bug_summary(new, bugs, shortlist=True),
+ return "%s%s\n" % (new.string(bugs, shortlist=True),
"\n".join(change_strings))
def comment_summary(comment, status):
return "%8s comment from %s on %s" % (status, comment.From,
- time_to_str(comment.date))
+ time_to_str(comment.time))
diff --git a/libbe/mapfile.py b/libbe/mapfile.py
index 6a304fd..3f09edd 100644
--- a/libbe/mapfile.py
+++ b/libbe/mapfile.py
@@ -14,7 +14,10 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+import os.path
+import errno
import utility
+
class IllegalKey(Exception):
def __init__(self, key):
Exception.__init__(self, 'Illegal key "%s"' % key)
@@ -99,72 +102,23 @@ def parse(f):
result[name] = value
return result
+def map_save(rcs, path, map):
+ """Save the map as a mapfile to the specified path"""
+ add = not os.path.exists(path)
+ output = file(path, "wb")
+ generate(output, map)
+ if add:
+ rcs.add_id(path)
-def split_diff3(this, other, f):
- """Split a file or string with diff3 conflicts into two files.
+class NoSuchFile(Exception):
+ def __init__(self, pathname):
+ Exception.__init__(self, "No such file: %s" % pathname)
- :param this: The THIS file to write. May be a utility.FileString
- :param other: The OTHER file to write. May be a utility.FileString
- :param f: The file or string to split.
- :return: True if there were conflicts
- >>> split_diff3(utility.FileString(), utility.FileString(),
- ... "a\\nb\\nc\\nd\\n")
- False
- >>> this = utility.FileString()
- >>> other = utility.FileString()
- >>> split_diff3(this, other, "<<<<<<< values1\\nstatus=closed\\n=======\\nstatus=closedd\\n>>>>>>> values2\\n")
- True
- >>> this.str
- 'status=closed\\n'
- >>> other.str
- 'status=closedd\\n'
- """
- f = utility.get_file(f)
- this_active = True
- other_active = True
- conflicts = False
- for line in f:
- if line.startswith("<<<<<<<"):
- conflicts = True
- this_active = True
- other_active = False
- elif line.startswith("======="):
- this_active = False
- other_active = True
- elif line.startswith(">>>>>>>"):
- this_active = True
- other_active = True
- else:
- if this_active:
- this.write(line)
- if other_active:
- other.write(line)
- return conflicts
-
-def split_diff3_str(f):
- """Split a file/string with diff3 conflicts into two strings. If there
- were no conflicts, one string is returned.
-
- >>> result = split_diff3_str("<<<<<<< values1\\nstatus=closed\\n=======\\nstatus=closedd\\n>>>>>>> values2\\n")
- >>> len(result)
- 2
- >>> result[0] != result[1]
- True
- >>> result = split_diff3_str("<<<<<<< values1\\nstatus=closed\\n=======\\nstatus=closed\\n>>>>>>> values2\\n")
- >>> len(result)
- 2
- >>> result[0] == result[1]
- True
- >>> result = split_diff3_str("a\\nb\\nc\\nd\\n")
- >>> len(result)
- 1
- >>> result[0]
- 'a\\nb\\nc\\nd\\n'
- """
- this = utility.FileString()
- other = utility.FileString()
- if split_diff3(this, other, f):
- return (this.str, other.str)
- else:
- return (this.str,)
+def map_load(path):
+ try:
+ return parse(file(path, "rb"))
+ except IOError, e:
+ if e.errno != errno.ENOENT:
+ raise e
+ raise NoSuchFile(path)
diff --git a/libbe/names.py b/libbe/names.py
index d2e077a..c86063d 100644
--- a/libbe/names.py
+++ b/libbe/names.py
@@ -35,3 +35,21 @@ def creator():
return os.environ["LOGNAME"]
else:
return os.environ["USERNAME"]
+
+def unique_name(bug, bugs):
+ """
+ Generate short names from uuids. Picks the minimum number of
+ characters (>=3) from the beginning of the uuid such that the
+ short names are unique.
+
+ Obviously, as the number of bugs in the database grows, these
+ short names will cease to be unique. The complete uuid should be
+ used for long term reference.
+ """
+ chars = 3
+ for some_bug in bugs:
+ if bug.uuid == some_bug.uuid:
+ continue
+ while (bug.uuid[:chars] == some_bug.uuid[:chars]):
+ chars+=1
+ return bug.uuid[:chars]
diff --git a/libbe/plugin.py b/libbe/plugin.py
index 4016ca1..9254986 100644
--- a/libbe/plugin.py
+++ b/libbe/plugin.py
@@ -55,6 +55,7 @@ def get_plugin(prefix, name):
plugin_path = os.path.realpath(os.path.dirname(os.path.dirname(__file__)))
if plugin_path not in sys.path:
sys.path.append(plugin_path)
+
def _test():
import doctest
doctest.testmod()
diff --git a/libbe/template b/libbe/template
deleted file mode 100644
index 467eee4..0000000
--- a/libbe/template
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Compare two bug trees"""
-from bugdir import cmdutil
-
-def diff(old_tree, new_tree):
- old_bug_map = old_tree.bug_map()
- new_bug_map = new_tree.bug_map()
- added = []
- removed = []
- modified = []
- for old_bug in old_bug_map.itervalues():
- new_bug = new_bug_map.get(bug.uuid)
- if new_bug is None :
- removed.append(old_bug)
- else:
- if old_bug != new_bug:
- modified.append((old_bug, new_bug))
- for new_bug in new_bug_map.itervalues():
- if not old_bug_map.haskey(new_bug.id):
- added.append(new_bug)
- return (removed, modified, added)
-
-
-def reference_diff(bugdir, spec=None):
- return diff(bugdir.reference_bugdir(), bugdir)
-
-def diff_report(diff_data, bugdir)
- (removed, modified, added) = diff_data
- def modified_cmp(left, right):
- return cmp_severity(left[1], right[1])
-
- added.sort(bugdir.cmp_severity)
- removed.sort(bugdir.cmp_severity)
- modified.sort(modified_cmp)
-
- print "New bug reports:"
- for bug in added:
- cmdutil.bug_summary(bug, bugdir, no_target=True)
-
- print "modified bug reports:"
- for old_bug, new_bug in modified:
- cmdutil.bug_summary(new_bug, bugdir, no_target=True)
-
- print "Removed bug reports:"
- for bug in removed:
- cmdutil.bug_summary(bug, bugdir, no_target=True)
-
-
-
diff --git a/libbe/tests.py b/libbe/tests.py
index a7d925d..461e6e8 100644
--- a/libbe/tests.py
+++ b/libbe/tests.py
@@ -18,7 +18,7 @@ import tempfile
import shutil
import os
import os.path
-from libbe import bugdir, arch
+from libbe import bugdir, bug, arch
cleanable = []
def clean_up():
global cleanable
@@ -47,8 +47,8 @@ def bug_arch_dir():
def simple_bug_dir():
dir = bug_arch_dir()
- bug_a = bugdir.new_bug(dir, "a")
- bug_b = bugdir.new_bug(dir, "b")
+ bug_a = bug.new_bug(dir, "a")
+ bug_b = bug.new_bug(dir, "b")
bug_b.status = "closed"
bug_a.save()
bug_b.save()
diff --git a/test_usage.sh b/test_usage.sh
new file mode 100755
index 0000000..e214e75
--- /dev/null
+++ b/test_usage.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+#
+# Run through some simple usage cases. This both tests that important
+# features work, and gives an example of suggested usage to get people
+# started.
+#
+# usage: test_usage.sh
+
+set -e # exit imediately on failed command
+set -o pipefail # pipes fail if any stage fails
+set -v # verbose, echo commands to stdout
+
+exec 6>&2 # save stderr to file descriptor 6
+exec 2>&1 # fd 2 now writes to stdout
+
+ID=`bzr whoami`
+echo "I am: $ID"
+
+TESTDIR=`mktemp -d /tmp/BEtest.XXXXXXXXXX`
+cd $TESTDIR
+bzr init
+be set-root .
+OUT=`be new 'having too much fun'`
+echo "$OUT"
+BUG=`echo "$OUT" | sed -n 's/Created bug with ID //p'`
+echo "Working with bug: $BUG"
+be comment $BUG "This is an argument"
+be comment $BUG:1 "No it isn't" # comment on the first comment
+be show $BUG # show details on a given bug
+be close $BUG # set bug status to 'closed'
+be comment $BUG "It's closed, but I can still comment."
+be open $BUG # set bug status to 'open'
+be comment $BUG "Reopend, comment again"
+be status $BUG fixed # set bug status to 'fixed'
+be show $BUG # show bug details & comments
+be list # list all open bugs
+be list --status closed # list all closed bugs
+be assign $BUG # assign the bug to yourself
+be list -m # see bugs assigned to you
+be assign $BUG 'Joe' # assign the bug to Joe
+be list --assigned Joe # list the bugs assigned to Joe
+be assign $BUG none # assign the bug to noone
+be remove $BUG # decide that you don't like that bug after all
+cd /
+rm -rf $TESTDIR
+
+exec 2>&6 6>&- # restore stderr and close fd 6