From b1dbae97e769346246e8a36424c60ce89ef7a310 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 17 Nov 2009 08:21:56 -0500 Subject: rename "output","error" -> "stdout","stderr" in VCS._u_invoke() --- libbe/vcs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libbe/vcs.py b/libbe/vcs.py index 7484660..c260af1 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -474,13 +474,13 @@ class VCS(object): shell=True, cwd=cwd) except OSError, e : raise CommandError(args, status=e.args[0], stdout="", stderr=e) - output,error = q.communicate(input=stdin) + stdout,stderr = q.communicate(input=stdin) status = q.wait() if self.verboseInvoke == True: - print >> sys.stderr, "%d\n%s%s" % (status, output, error) + print >> sys.stderr, "%d\n%s%s" % (status, stdout, stderr) if status not in expect: - raise CommandError(args, status, output, error) - return status, output, error + raise CommandStderr(args, status, stdout, stderr) + return status, stdout, stderr def _u_invoke_client(self, *args, **kwargs): directory = kwargs.get('directory',None) expect = kwargs.get('expect', (0,)) -- cgit From 32fbab0fb8f5defc3698d288024a10b8d32a0f25 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 17 Nov 2009 08:25:15 -0500 Subject: Added unicode_output option to VCS._u_invoke() --- libbe/vcs.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libbe/vcs.py b/libbe/vcs.py index c260af1..45bc59e 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -456,10 +456,13 @@ class VCS(object): if list_string in string: return True return False - def _u_invoke(self, args, stdin=None, expect=(0,), cwd=None): + def _u_invoke(self, args, stdin=None, expect=(0,), cwd=None, + unicode_output=True): """ expect should be a tuple of allowed exit codes. cwd should be - the directory from which the command will be executed. + the directory from which the command will be executed. When + unicode_output == True, convert stdout and stdin strings to + unicode before returing them. """ if cwd == None: cwd = self.rootdir @@ -476,6 +479,9 @@ class VCS(object): raise CommandError(args, status=e.args[0], stdout="", stderr=e) stdout,stderr = q.communicate(input=stdin) status = q.wait() + if unicode_output == True: + stdout = unicode(stdout, self.encoding) + stderr = unicode(stderr, self.encoding) if self.verboseInvoke == True: print >> sys.stderr, "%d\n%s%s" % (status, stdout, stderr) if status not in expect: -- cgit From eaa6d158608b0b47c337fc1433902532bc646128 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 17 Nov 2009 08:36:22 -0500 Subject: Set binary=True for mapfile file handling The YAML library produces Python string encodings of unicode objects. There's no reason to try and convert them back into Python unicode objects just to save them with binary=False, because the files are only read in to be passed into the YAML parser, which can handle the unicode characters correctly. --- libbe/mapfile.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/libbe/mapfile.py b/libbe/mapfile.py index 4d69601..d0e16fe 100644 --- a/libbe/mapfile.py +++ b/libbe/mapfile.py @@ -75,7 +75,7 @@ def generate(map): assert(len(key) > 0) except AssertionError: raise IllegalKey(unicode(key).encode('unicode_escape')) - if "\n" in map[key]: + if '\n' in map[key]: raise IllegalValue(unicode(map[key]).encode('unicode_escape')) lines = [] @@ -83,7 +83,7 @@ def generate(map): lines.append(yaml.safe_dump({key: map[key]}, default_flow_style=False, allow_unicode=True)) - lines.append("") + lines.append('') return '\n'.join(lines) def parse(contents): @@ -101,16 +101,21 @@ def parse(contents): 'd' >>> dict["e"] 'f' + >>> contents = generate({"q":u"Fran\u00e7ais"}) + >>> dict = parse(contents) + >>> dict["q"] + u'Fran\\xe7ais' """ return yaml.load(contents) or {} def map_save(vcs, path, map, allow_no_vcs=False): """Save the map as a mapfile to the specified path""" contents = generate(map) - vcs.set_file_contents(path, contents, allow_no_vcs) + vcs.set_file_contents(path, contents, allow_no_vcs, binary=True) def map_load(vcs, path, allow_no_vcs=False): - contents = vcs.get_file_contents(path, allow_no_vcs=allow_no_vcs) + contents = vcs.get_file_contents(path, allow_no_vcs=allow_no_vcs, + binary=True) return parse(contents) suite = doctest.DocTestSuite() -- cgit From 2f867266d4b47cc8ca0c7ccb3768d240e5096f11 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 17 Nov 2009 08:43:44 -0500 Subject: Don't attempt to convert unicode objects to strings in *._setting_attr_string() --- libbe/bug.py | 4 +++- libbe/comment.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index fd30ff7..6633ab7 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -267,7 +267,9 @@ class Bug(settings_object.SavedSettingsObject): value = getattr(self, setting) if value == None: return "" - return str(value) + if type(value) not in types.StringTypes: + return str(value) + return value def xml(self, show_comments=False): if self.bugdir == None: diff --git a/libbe/comment.py b/libbe/comment.py index 02bcc93..17daf62 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -308,7 +308,9 @@ class Comment(Tree, settings_object.SavedSettingsObject): value = getattr(self, setting) if value == None: return "" - return str(value) + if type(value) not in types.StringTypes: + return str(value) + return value def xml(self, indent=0, shortname=None): """ -- cgit From c47273486de9ec62dea5838d3e5966652fe683ea Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 17 Nov 2009 08:48:14 -0500 Subject: "CommandStderr" -> "CommandError" in VCS._u_invoke() Corrects a mistake from using string replace to move output,error -> stdout,stderr a few commits ago. --- libbe/vcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libbe/vcs.py b/libbe/vcs.py index 45bc59e..d04db28 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -485,7 +485,7 @@ class VCS(object): if self.verboseInvoke == True: print >> sys.stderr, "%d\n%s%s" % (status, stdout, stderr) if status not in expect: - raise CommandStderr(args, status, stdout, stderr) + raise CommandError(args, status, stdout, stderr) return status, stdout, stderr def _u_invoke_client(self, *args, **kwargs): directory = kwargs.get('directory',None) -- cgit From 2a1bca807d4b94610924343169c813a9f7868147 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 17 Nov 2009 09:04:37 -0500 Subject: Cleanup temp. dir. in libbe.vcs tests. --- libbe/vcs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libbe/vcs.py b/libbe/vcs.py index d04db28..59dcaf8 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -384,7 +384,7 @@ class VCS(object): revision==None specifies the current revision. Return the path to the arbitrary directory at the base of the new repo. """ - # Dirname in Baseir to protect against simlink attacks. + # Dirname in Basedir to protect against simlink attacks. if self._duplicateBasedir == None: self._duplicateBasedir = tempfile.mkdtemp(prefix='BEvcs') self._duplicateDirname = \ @@ -684,6 +684,7 @@ class VCSTestCase(unittest.TestCase): def tearDown(self): self.vcs.cleanup() + self.dir.cleanup() super(VCSTestCase, self).tearDown() def full_path(self, rel_path): -- cgit From f108f5a0fb0984c0daccd8be72ea0ffa309b3fff Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 17 Nov 2009 09:47:44 -0500 Subject: Use unicode_output=False in some Darcs._u_invoke_client() calls. This avoids the following error: ====================================================================== ERROR: Should get file contents as committed to specified revision. ---------------------------------------------------------------------- Traceback (most recent call last): File ".../libbe/vcs.py", line 860, in test_revision_file_contents_as_committed full_path, revision) File ".../libbe/vcs.py", line 339, in get_file_contents contents = self._vcs_get_file_contents(relpath,revision,binary=binary) File ".../libbe/darcs.py", line 122, in _vcs_get_file_contents status,output,error = self._u_invoke(args, stdin=major_patch) File ".../libbe/vcs.py", line 488, in _u_invoke raise CommandError(args, status, stdout, stderr) CommandError: Command failed (2): patch: **** Only garbage was found in the patch input. while executing ['patch', '--reverse', 'a/text'] After adding the unicode_output=False lines, I adjusted the VCS._u_invoke_client() definition to pass all it's kwargs automatically through to VCS._u_invoke(). To make this simpler and more consistent, I renamed the "directory" option to "cwd", and adjusted *._u_invoke() calls appropriately in several VCS backends. --- libbe/arch.py | 12 ++++++------ libbe/bzr.py | 2 +- libbe/darcs.py | 8 +++++--- libbe/git.py | 9 ++++----- libbe/hg.py | 4 ++-- libbe/vcs.py | 5 +---- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/libbe/arch.py b/libbe/arch.py index daa8ac6..98c4edd 100644 --- a/libbe/arch.py +++ b/libbe/arch.py @@ -83,7 +83,7 @@ class Arch(vcs.VCS): self._archive_dir = "/tmp/%s" % trailer self._tmp_archive = True self._u_invoke_client("make-archive", self._archive_name, - self._archive_dir, directory=path) + self._archive_dir, cwd=path) def _invoke_client(self, *args, **kwargs): """ Invoke the client on our archive. @@ -121,7 +121,7 @@ class Arch(vcs.VCS): version = "0.1" self._project_name = "%s--%s--%s" % (category, branch, version) self._invoke_client("archive-setup", self._project_name, - directory=path) + cwd=path) self._tmp_project = True def _remove_project(self): assert self._tmp_project == True @@ -164,10 +164,10 @@ class Arch(vcs.VCS): # http://regexps.srparish.net/tutorial-tla/new-source.html # http://regexps.srparish.net/tutorial-tla/importing-first.html self._invoke_client("init-tree", self._project_name, - directory=path) + cwd=path) self._adjust_naming_conventions(path) self._invoke_client("import", "--summary", "Began versioning", - directory=path) + cwd=path) def _vcs_cleanup(self): if self._tmp_project == True: self._remove_project() @@ -198,7 +198,7 @@ class Arch(vcs.VCS): assert self._archive_name != None def _get_archive_project_name(self, root): # get project names - status,output,error = self._u_invoke_client("tree-version", directory=root) + status,output,error = self._u_invoke_client("tree-version", cwd=root) # e.g output # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52/be--mainline--0.1 archive_name,project_name = output.rstrip('\n').split('/') @@ -266,7 +266,7 @@ class Arch(vcs.VCS): vcs.VCS._vcs_duplicate_repo(self, directory, revision) else: status,output,error = \ - self._u_invoke_client("get", revision,directory) + self._u_invoke_client("get", revision, directory) def _vcs_commit(self, commitfile, allow_empty=False): if allow_empty == False: # arch applies empty commits without complaining, so check first diff --git a/libbe/bzr.py b/libbe/bzr.py index ed9e032..8e91d0c 100644 --- a/libbe/bzr.py +++ b/libbe/bzr.py @@ -49,7 +49,7 @@ class Bzr(vcs.VCS): status,output,error = self._u_invoke_client("root", path) return output.rstrip('\n') def _vcs_init(self, path): - self._u_invoke_client("init", directory=path) + self._u_invoke_client("init", cwd=path) def _vcs_get_user_id(self): status,output,error = self._u_invoke_client("whoami") return output.rstrip('\n') diff --git a/libbe/darcs.py b/libbe/darcs.py index 9115886..6bf0119 100644 --- a/libbe/darcs.py +++ b/libbe/darcs.py @@ -60,7 +60,7 @@ class Darcs(vcs.VCS): return None return os.path.dirname(darcs_dir) def _vcs_init(self, path): - self._u_invoke_client("init", directory=path) + self._u_invoke_client("init", cwd=path) def _vcs_get_user_id(self): # following http://darcs.net/manual/node4.html#SECTION00410030000000000000 # as of June 29th, 2009 @@ -107,10 +107,12 @@ class Darcs(vcs.VCS): # Darcs versions < 2.0.0pre2 lack the "show contents" command status,output,error = self._u_invoke_client( \ - "diff", "--unified", "--from-patch", revision, path) + "diff", "--unified", "--from-patch", revision, path, + unicode_output=False) major_patch = output status,output,error = self._u_invoke_client( \ - "diff", "--unified", "--patch", revision, path) + "diff", "--unified", "--patch", revision, path, + unicode_output=False) target_patch = output # "--output -" to be supported in GNU patch > 2.5.9 diff --git a/libbe/git.py b/libbe/git.py index 628f9b9..781a278 100644 --- a/libbe/git.py +++ b/libbe/git.py @@ -50,12 +50,12 @@ class Git(vcs.VCS): if os.path.isdir(path) != True: path = os.path.dirname(path) status,output,error = self._u_invoke_client("rev-parse", "--git-dir", - directory=path) + cwd=path) gitdir = os.path.join(path, output.rstrip('\n')) dirname = os.path.abspath(os.path.dirname(gitdir)) return dirname def _vcs_init(self, path): - self._u_invoke_client("init", directory=path) + self._u_invoke_client("init", cwd=path) def _vcs_get_user_id(self): status,output,error = \ self._u_invoke_client("config", "user.name", expect=(0,1)) @@ -102,9 +102,8 @@ class Git(vcs.VCS): if revision==None: vcs.VCS._vcs_duplicate_repo(self, directory, revision) else: - #self._u_invoke_client("archive", revision, directory) # makes tarball - self._u_invoke_client("clone", "--no-checkout",".",directory) - self._u_invoke_client("checkout", revision, directory=directory) + self._u_invoke_client("clone", "--no-checkout", ".", directory) + self._u_invoke_client("checkout", revision, cwd=directory) def _vcs_commit(self, commitfile, allow_empty=False): args = ['commit', '--all', '--file', commitfile] if allow_empty == True: diff --git a/libbe/hg.py b/libbe/hg.py index 7cd4c2f..40739b6 100644 --- a/libbe/hg.py +++ b/libbe/hg.py @@ -45,10 +45,10 @@ class Hg(vcs.VCS): return True return False def _vcs_root(self, path): - status,output,error = self._u_invoke_client("root", directory=path) + status,output,error = self._u_invoke_client("root", cwd=path) return output.rstrip('\n') def _vcs_init(self, path): - self._u_invoke_client("init", directory=path) + self._u_invoke_client("init", cwd=path) def _vcs_get_user_id(self): status,output,error = self._u_invoke_client("showconfig","ui.username") return output.rstrip('\n') diff --git a/libbe/vcs.py b/libbe/vcs.py index 59dcaf8..ba23858 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -488,12 +488,9 @@ class VCS(object): raise CommandError(args, status, stdout, stderr) return status, stdout, stderr def _u_invoke_client(self, *args, **kwargs): - directory = kwargs.get('directory',None) - expect = kwargs.get('expect', (0,)) - stdin = kwargs.get('stdin', None) cl_args = [self.client] cl_args.extend(args) - return self._u_invoke(cl_args, stdin=stdin,expect=expect,cwd=directory) + return self._u_invoke(cl_args, **kwargs) def _u_search_parent_directories(self, path, filename): """ Find the file (or directory) named filename in path or in any -- cgit From 8a2c80bbaa02fd29ac192135f1ec095b0a048e20 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 17 Nov 2009 19:55:42 -0500 Subject: Updated NEWS file --- NEWS | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/NEWS b/NEWS index dec3787..1f71af6 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,33 @@ +November 17, 2009 + * new becommands: + commit + depend + html + merge + remove + status + subscribe + tag + * renamed becommands: + set_root => init + * removed becommands: + inprogress + upgrade + * new interfaces: + email: + interactive + catmutt + xml: + be-mbox-to-xml + be-xml-to-mbox + * deprecated interfaces: + gui: + beg + wxbe + web: + Bugs-Everywhere-Web + * lots of bugfixes and cleanups, see `be diff 200` for details. + April 10, 2006 * Updated BeWeb to TurboGear 0.9 -- cgit From 6e171ccd2ab07193b2f16350d00270229ec0c0a1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 19 Nov 2009 00:39:31 -0500 Subject: Updated README and Bugs-Everywhere-Web/README. --- README | 18 +------- interfaces/web/Bugs-Everywhere-Web/README | 60 +++++++++++++++++++++++++++ interfaces/web/Bugs-Everywhere-Web/README.txt | 42 ------------------- 3 files changed, 61 insertions(+), 59 deletions(-) create mode 100644 interfaces/web/Bugs-Everywhere-Web/README delete mode 100644 interfaces/web/Bugs-Everywhere-Web/README.txt diff --git a/README b/README index b43c15c..031ae13 100644 --- a/README +++ b/README @@ -15,25 +15,9 @@ Getting started To get started, you must set the bugtracker root. Typically, you will want to set the bug root to your project root, so that Bugs Everywhere works in any part of your project tree. -$ be init $PROJECT_ROOT +$ be init -r $PROJECT_ROOT To create bugs, use "be new $DESCRIPTION". To comment on bugs, you can can use "be comment $BUG_ID". To close a bug, use "be close $BUG_ID" or "be status $BUG_ID fixed". For more commands, see "be help". You can also look at the usage examples in test_usage.sh. - - -Using BeWeb, the web UI -======================= -BeWeb uses the Turbogears framework: http://www.turbogears.org/ -Please ensure you have Turbogears 0.8a5 or a compatible release installed. -Because it uses BE data, the web UI does not require a database. - -To use BeWeb, first create a configuration file, telling it which projects -to track, and what to call them. An example configuration file -(beweb/beweb/config.py.example) is provided. - -Next, cd to beweb, and run ./beweb-start.py - -BeWeb allows you to create, view and edit bugs, but it is in an early stage of -development, so some features are missing. diff --git a/interfaces/web/Bugs-Everywhere-Web/README b/interfaces/web/Bugs-Everywhere-Web/README new file mode 100644 index 0000000..c152757 --- /dev/null +++ b/interfaces/web/Bugs-Everywhere-Web/README @@ -0,0 +1,60 @@ +Using BeWeb, the web UI +======================= +BeWeb uses the Turbogears framework: http://www.turbogears.org/ +Please ensure you have Turbogears 0.8a5 or a compatible release installed. +Because it uses BE data, the web UI does not require a database. + +To use BeWeb, first create a configuration file, telling it which projects +to track, and what to call them. An example configuration file +(beweb/beweb/config.py.example) is provided. + +Next, cd to this directory, and run ./start-beweb.py + +BeWeb allows you to create, view and edit bugs, but it is in an early stage of +development, so some features are missing. + +Configuration file +------------------ + +Configure by creating an appropriate beweb/config.py from +beweb/config.py.example. The server will edit the repositories that +it manages, so you should probably have it running on a seperate +branch than your working repository. You can then merge/push +as you require to keep the branches in sync. + +See + http://docs.turbogears.org/1.0/Configuration +For standard turbogears configuration information. + +Actions +------- + +Currently, you need to login for any methods with a +@identity.require() decorator. The only group in the current +implementation is 'editbugs'. Basically, anyone can browse around, +but only registered 'editbugs' members can change things. + +Anonymous actions: + * See project tree + * See buglist + * See comments +Editbugs required actions: + * Create new comments + * Reply to comments + * Update comment info + +Users +----- + +All login attempts will fail unless you have added some valid users. See + http://docs.turbogears.org/1.0/GettingStartedWithIdentity +For a good intro. For the impatient, try something like + Bugs-Everywhere-Web$ tg-admin toolbox + browse to 'CatWalk' -> 'User' -> 'Add User+' +or + Bugs-Everywhere-Web$ tg-admin sholl + >>> u = User(user_name=u'jdoe', email_address=u'jdoe@example.com', + display_name=u'Jane Doe', password=u'xxx') + >>> g = Group(group_name=u'editbugs', display_name=u'Edit Bugs') + >>> g.addUser(u) # BE-Web uses SQLObject +Exit the tg-admin shell with Ctrl-Z on MS Windows, Ctrl-D on other systems. diff --git a/interfaces/web/Bugs-Everywhere-Web/README.txt b/interfaces/web/Bugs-Everywhere-Web/README.txt deleted file mode 100644 index 10774df..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/README.txt +++ /dev/null @@ -1,42 +0,0 @@ -Bugs-Everywhere-Web - -This is a TurboGears (http://www.turbogears.org) project. It can be -started by running the start-beweb.py script. - -Configure by creating an appropriate beweb/config.py from -beweb/config.py.example. The server will edit the repositories that -it manages, so you should probably have it running on a seperate -branch than your working repository. You can then merge/push -as you require to keep the branches in sync. - -See - http://docs.turbogears.org/1.0/Configuration -For standard turbogears configuration information. - -Currently, you need to login for any methods with a -@identity.require() decorator. The only group in the current -implementation is 'editbugs'. Basically, anyone can browse around, -but only registered 'editbugs' members can change things. - -Anonymous actions: - * See project tree - * See buglist - * See comments -Editbugs required actions: - * Create new comments - * Reply to comments - * Update comment info - - -All login attempts will fail unless you have added some valid users. See - http://docs.turbogears.org/1.0/GettingStartedWithIdentity -For a good intro. For the impatient, try something like - Bugs-Everywhere-Web$ tg-admin toolbox - browse to 'CatWalk' -> 'User' -> 'Add User+' -or - Bugs-Everywhere-Web$ tg-admin sholl - >>> u = User(user_name=u'jdoe', email_address=u'jdoe@example.com', - display_name=u'Jane Doe', password=u'xxx') - >>> g = Group(group_name=u'editbugs', display_name=u'Edit Bugs') - >>> g.addUser(u) # BE-Web uses SQLObject -Exit the tg-admin shell with Ctrl-Z on MS Windows, Ctrl-D on other systems. -- cgit From 839c2146e6ac9ee3a3bb90faba0e8be2bf4bbeb5 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 19 Nov 2009 16:59:06 -0500 Subject: Translated update_copyright.sh to Python update_copyright.py --- update_copyright.py | 454 ++++++++++++++++++++++++++++++++++++++++++++++++++++ update_copyright.sh | 158 ------------------ 2 files changed, 454 insertions(+), 158 deletions(-) create mode 100755 update_copyright.py delete mode 100755 update_copyright.sh diff --git a/update_copyright.py b/update_copyright.py new file mode 100755 index 0000000..4a52fee --- /dev/null +++ b/update_copyright.py @@ -0,0 +1,454 @@ +#!/usr/bin/python +# +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os.path +import re +import sys +import time + +import os +import sys +import select +from subprocess import Popen, PIPE, mswindows +from threading import Thread + +COPYRIGHT_TEXT="""# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.""" + +COPYRIGHT_TAG='-xyz-COPYRIGHT-zyx-' # unlikely to occur in the wild :p + +ALIASES = [ + ['Ben Finney ', + 'Ben Finney ', + 'John Doe '], + ['Chris Ball ', + 'Chris Ball '], + ['Gianluca Montecchi ', + 'gian ', + 'gianluca '], + ['W. Trevor King ', + 'wking '], + [None, + 'j^ '], + ] +COPYRIGHT_ALIASES = [ + ['Aaron Bentley and Panometrics, Inc.', + 'Aaron Bentley '], + ] +EXCLUDES = [ + ['Aaron Bentley and Panometrics, Inc.', + 'Aaron Bentley ',] + ] + + +IGNORED_PATHS = ['./.be/', './.bzr/', './build/'] +IGNORED_FILES = ['COPYING', 'update_copyright.py', 'catmutt'] + +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. + + >>> p = Pipe([['find', '/etc/'], ['grep', '^/etc/ssh$']]) + >>> p.stdout + '/etc/ssh\\n' + >>> p.status + 1 + >>> p.statuses + [1, 0] + >>> p.stderrs # doctest: +ELLIPSIS + ["find: `...': Permission denied\\n...", ''] + """ + def __init__(self, cmds, stdin=None): + # spawn processes + self._procs = [] + for cmd in cmds: + if len(self._procs) != 0: + stdin = self._procs[-1].stdout + self._procs.append(Popen(cmd, stdin=stdin, stdout=PIPE, stderr=PIPE)) + + self.stdout,self.stderrs = self._communicate(input=None) + + # collect process statuses + self.statuses = [] + self.status = 0 + for proc in self._procs: + self.statuses.append(proc.wait()) + if self.statuses[-1] != 0: + self.status = self.statuses[-1] + + # Code excerpted from subprocess.Popen._communicate() + if mswindows == True: + def _communicate(self, input=None): + assert input == None, "stdin != None not yet supported" + # listen to each process' stderr + threads = [] + std_X_arrays = [] + for proc in self._procs: + stderr_array = [] + thread = Thread(target=proc._readerthread, + args=(proc.stderr, stderr_array)) + thread.setDaemon(True) + thread.start() + threads.append(thread) + std_X_arrays.append(stderr_array) + + # also listen to the last processes stdout + stdout_array = [] + thread = Thread(target=proc._readerthread, + args=(proc.stdout, stdout_array)) + thread.setDaemon(True) + thread.start() + threads.append(thread) + std_X_arrays.append(stdout_array) + + # join threads as they die + for thread in threads: + thread.join() + + # read output from reader threads + std_X_strings = [] + for std_X_array in std_X_arrays: + std_X_strings.append(std_X_array[0]) + + stdout = std_X_strings.pop(-1) + stderrs = std_X_strings + return (stdout, stderrs) + else: # POSIX + def _communicate(self, input=None): + read_set = [] + write_set = [] + read_arrays = [] + stdout = None # Return + stderr = None # Return + + if self._procs[0].stdin: + # Flush stdio buffer. This might block, if the user has + # been writing to .stdin in an uncontrolled fashion. + self._procs[0].stdin.flush() + if input: + write_set.append(self._procs[0].stdin) + else: + self._procs[0].stdin.close() + for proc in self._procs: + read_set.append(proc.stderr) + read_arrays.append([]) + read_set.append(self._procs[-1].stdout) + read_arrays.append([]) + + input_offset = 0 + while read_set or write_set: + try: + rlist, wlist, xlist = select.select(read_set, write_set, []) + except select.error, e: + if e.args[0] == errno.EINTR: + continue + raise + if self._procs[0].stdin in wlist: + # When select has indicated that the file is writable, + # we can write up to PIPE_BUF bytes without risk + # blocking. POSIX defines PIPE_BUF >= 512 + chunk = input[input_offset : input_offset + 512] + bytes_written = os.write(self.stdin.fileno(), chunk) + input_offset += bytes_written + if input_offset >= len(input): + self._procs[0].stdin.close() + write_set.remove(self._procs[0].stdin) + if self._procs[-1].stdout in rlist: + data = os.read(self._procs[-1].stdout.fileno(), 1024) + if data == "": + self._procs[-1].stdout.close() + read_set.remove(self._procs[-1].stdout) + read_arrays[-1].append(data) + for i,proc in enumerate(self._procs): + if proc.stderr in rlist: + data = os.read(proc.stderr.fileno(), 1024) + if data == "": + proc.stderr.close() + read_set.remove(proc.stderr) + read_arrays[i].append(data) + + # All data exchanged. Translate lists into strings. + read_strings = [] + for read_array in read_arrays: + read_strings.append(''.join(read_array)) + + stdout = read_strings.pop(-1) + stderrs = read_strings + return (stdout, stderrs) + +def _strip_email(*args): + """ + >>> _strip_email('J Doe ') + ['J Doe'] + >>> _strip_email('J Doe ', 'JJJ Smith ') + ['J Doe', 'JJJ Smith'] + """ + args = list(args) + for i,arg in enumerate(args): + if arg == None: + continue + index = arg.find('<') + if index > 0: + args[i] = arg[:index].rstrip() + return args + +def _replace_aliases(authors, with_email=True, aliases=None, + excludes=None): + """ + >>> aliases = [['J Doe and C, Inc.', 'J Doe '], + ... ['J Doe ', 'Johnny '], + ... ['JJJ Smith ', 'Jingly '], + ... [None, 'Anonymous ']] + >>> excludes = [['J Doe and C, Inc.', 'J Doe ']] + >>> _replace_aliases(['JJJ Smith ', 'Johnny ', + ... 'Jingly ', 'Anonymous '], + ... with_email=True, aliases=aliases, excludes=excludes) + ['J Doe ', 'JJJ Smith '] + >>> _replace_aliases(['JJJ Smith', 'Johnny', 'Jingly', 'Anonymous'], + ... with_email=False, aliases=aliases, excludes=excludes) + ['J Doe', 'JJJ Smith'] + >>> _replace_aliases(['JJJ Smith ', 'Johnny ', + ... 'Jingly ', 'J Doe '], + ... with_email=True, aliases=aliases, excludes=excludes) + ['J Doe and C, Inc.', 'JJJ Smith '] + """ + if aliases == None: + aliases = ALIASES + if excludes == None: + excludes = EXCLUDES + if with_email == False: + aliases = [_strip_email(*alias) for alias in aliases] + exclude = [_strip_email(*exclude) for exclude in excludes] + for i,author in enumerate(authors): + for alias in aliases: + if author in alias[1:]: + authors[i] = alias[0] + break + for i,author in enumerate(authors): + for exclude in excludes: + if author in exclude[1:] and exclude[0] in authors: + authors[i] = None + authors = sorted(set(authors)) + if None in authors: + authors.remove(None) + return authors + +def authors_list(): + p = Pipe([['bzr', 'log', '-n0'], + ['grep', '^ *committer\|^ *author'], + ['cut', '-d:', '-f2'], + ['sed', 's/ <.*//;s/^ *//'], + ['sort'], + ['uniq']]) + assert p.status == 0, p.statuses + authors = p.stdout.rstrip().split('\n') + return _replace_aliases(authors, with_email=False) + +def update_authors(verbose=True): + print "updating AUTHORS" + f = file('AUTHORS', 'w') + authors_text = 'Bugs Everywhere was written by:\n%s\n' % '\n'.join(authors_list()) + f.write(authors_text) + f.close() + +def ignored_file(filename, ignored_paths=None, ignored_files=None): + """ + >>> ignored_paths = ['./a/', './b/'] + >>> ignored_files = ['x', 'y'] + >>> ignored_file('./a/z', ignored_paths, ignored_files) + True + >>> ignored_file('./ab/z', ignored_paths, ignored_files) + False + >>> ignored_file('./ab/x', ignored_paths, ignored_files) + True + >>> ignored_file('./ab/xy', ignored_paths, ignored_files) + False + >>> ignored_file('./z', ignored_paths, ignored_files) + False + """ + if ignored_paths == None: + ignored_paths = IGNORED_PATHS + if ignored_files == None: + ignored_files = IGNORED_FILES + for path in ignored_paths: + if filename.startswith(path): + return True + if os.path.basename(filename) in ignored_files: + return True + if os.path.abspath(filename) != os.path.realpath(filename): + return True # symink somewhere in path... + return False + +def _copyright_string(orig_year, final_year, authors): + """ + >>> print _copyright_string(orig_year=2005, + ... final_year=2005, + ... authors=['A ', 'B '] + ... ) # doctest: +ELLIPSIS + # Copyright (C) 2005 A + # B + # + # This program... + >>> print _copyright_string(orig_year=2005, + ... final_year=2009, + ... authors=['A ', 'B '] + ... ) # doctest: +ELLIPSIS + # Copyright (C) 2005-2009 A + # B + # + # This program... + """ + if orig_year == final_year: + date_range = '%s' % orig_year + else: + date_range = '%s-%s' % (orig_year, final_year) + lines = ['# Copyright (C) %s %s' % (date_range, authors[0])] + for author in authors[1:]: + lines.append('#' + + ' '*(len(' Copyright (C) ')+len(date_range)+1) + + author) + return '%s\n%s' % ('\n'.join(lines), COPYRIGHT_TEXT) + +def _tag_copyright(contents): + """ + >>> contents = '''Some file + ... bla bla + ... # Copyright (copyright begins) + ... # (copyright continues) + ... # bla bla bla + ... (copyright ends) + ... bla bla bla + ... ''' + >>> print _tag_copyright(contents), + Some file + bla bla + -xyz-COPYRIGHT-zyx- + (copyright ends) + bla bla bla + """ + lines = [] + incopy = False + for line in contents.splitlines(): + if incopy == False and line.startswith('# Copyright'): + incopy = True + lines.append(COPYRIGHT_TAG) + elif incopy == True and not line.startswith('#'): + incopy = False + if incopy == False: + lines.append(line.rstrip('\n')) + return '\n'.join(lines)+'\n' + +def _update_copyright(contents, orig_year, authors): + current_year = time.gmtime()[0] + copyright_string = _copyright_string(orig_year, current_year, authors) + contents = _tag_copyright(contents) + return contents.replace(COPYRIGHT_TAG, copyright_string) + +def update_file(filename, verbose=True): + if verbose == True: + print "updating", filename + contents = file(filename, 'r').read() + + p = Pipe([['bzr', 'log', '-n0', filename], + ['grep', '^ *timestamp: '], + ['tail', '-n1'], + ['sed', 's/^ *//;'], + ['cut', '-b', '16-19']]) + if p.status != 0: + assert p.statuses[0] == 3, p.statuses + return # bzr doesn't version that file + assert p.status == 0, p.statuses + orig_year = int(p.stdout.strip()) + + p = Pipe([['bzr', 'log', '-n0', filename], + ['grep', '^ *author: \|^ *committer: '], + ['cut', '-d:', '-f2'], + ['sed', 's/^ *//;s/ *$//'], + ['sort'], + ['uniq']]) + assert p.status == 0, p.statuses + authors = p.stdout.rstrip().split('\n') + authors = _replace_aliases(authors, with_email=True, + aliases=ALIASES+COPYRIGHT_ALIASES) + + contents = _update_copyright(contents, orig_year, authors) + f = file(filename, 'w') + f.write(contents) + f.close() + +def test(): + import doctest + doctest.testmod() + +if __name__ == '__main__': + import optparse + usage = """%prog [options] [file ...] + +Update copyright information in source code with information from +the bzr repository. Run from the BE repository root. + +Replaces every line starting with '^# Copyright' and continuing with +'^#' with an auto-generated copyright blurb. If you want to add +#-commented material after a copyright blurb, please insert a blank +line between the blurb and your comment (as in this file), so the +next run of update_copyright.py doesn't clobber your comment. + +If no files are given, a list of files to update is generated +automatically. +""" + p = optparse.OptionParser(usage) + p.add_option('--test', dest='test', default=False, + action='store_true', help='Run internal tests and exit') + options,args = p.parse_args() + + if options.test == True: + test() + sys.exit(0) + + update_authors() + + files = args + if len(files) == 0: + p = Pipe([['grep', '-rc', '# Copyright', '.'], + ['grep', '-v', ':0$'], + ['cut', '-d:', '-f1']]) + assert p.status == 0 + files = p.stdout.rstrip().split('\n') + + for filename in files: + if ignored_file(filename) == True: + continue + update_file(filename) diff --git a/update_copyright.sh b/update_copyright.sh deleted file mode 100755 index 84a5913..0000000 --- a/update_copyright.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/bin/bash -# -# Copyright (C) 2009 W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -# Update copyright information in source code with information from -# the bzr repository. Run from the BE repository root. -# -# Replaces everything starting with '^# Copyright' and continuing with -# '^#' with an auto-generated copyright blurb. If you want to add -# #-commented material after a copyright blurb, please insert a blank -# line between the blurb and your comment (as in this file), so the -# next run of update_copyright.sh doesn't clobber your comment. -# -# usage: update_copyright.sh [files ...] -# -# If no files are given, a list of files to update is generated -# automatically. - -set -o pipefail - -if [ $# -gt 0 ]; then - FILES="$*" -else - FILES=`grep -rc "# Copyright" . | grep -v ':0$' | cut -d: -f1` -fi - -COPYRIGHT_TEXT="# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA." -# escape newlines and special characters -SED_RM_TRAIL_END='s/[\]n$//' # strip trailing newline escape -SED_ESC_SPECIAL='s/\([()/]\)/\\\1/g' # escape special characters -ESCAPED_TEXT=`echo "$COPYRIGHT_TEXT" | awk '{printf("%s\\\\n", $0)}' | sed "$SED_RM_TRAIL_END" | sed "$SED_ESC_SPECIAL"` - -# adjust the AUTHORS file -AUTHORS=`bzr log | grep '^ *committer\|^ *author' | cut -d: -f2 | sed 's/ <.*//;s/^ *//' | sort | uniq` -AUTHORS=`echo "$AUTHORS" | grep -v 'j\^\|wking\|John Doe\|gianluca'` # remove non-names -echo "Bugs Everywhere was written by:" > AUTHORS -echo "$AUTHORS" >> AUTHORS - -CURRENT_YEAR=`date +"%Y"` -TMP=`mktemp BE_update_copyright.XXXXXXX` - -for file in $FILES -do - # Ignore some files - if [ "${file:0:5}" == "./.be" ]; then - continue - fi - if [ "${file:0:6}" == "./.bzr" ]; then - continue - fi - if [ "${file:0:7}" == "./build" ]; then - continue - fi - if [ "$file" == "./COPYING" ]; then - continue - fi - if [ "$file" == "./update_copyright.sh" ]; then - continue - fi - if [ "$file" == "./xml/catmutt" ]; then - continue - fi - echo "Processing $file" - - # Get author history from bzr - AUTHORS=`bzr log "$file" | grep "^ *author: \|^ *committer: " | cut -d: -f2 | sed 's/^ *//;s/ *$//' | sort | uniq` - if [ $? -ne 0 ]; then - continue # bzr doesn't version that file - fi - ORIG_YEAR=`bzr log "$file" | grep "^ *timestamp: " | tail -n1 | sed 's/^ *//;' | cut -b 16-19` - - # Tweak the author list to make up for problems in the bzr - # history, change of email address, etc. - - # Consolidate Chris Ball - GREP_OUT=`echo "$AUTHORS" | grep 'Chris Ball '` - if [ -n "$GREP_OUT" ]; then - AUTHORS=`echo "$AUTHORS" | grep -v '^Chris Ball $'` - fi - - # Consolidate Aaron Bentley - AUTHORS=`echo "$AUTHORS" | sed 's//and Panometrics, Inc./'` - GREP_OUT=`echo "$AUTHORS" | grep 'Aaron Bentley and Panometrics, Inc.'` - if [ -n "$GREP_OUT" ]; then - AUTHORS=`echo "$AUTHORS" | grep -v '^Aaron Bentley $'` - fi - - # Consolidate Ben Finney - AUTHORS=`echo "$AUTHORS" | sed 's/John Doe /Ben Finney /'` - GREP_OUt=`echo "$AUTHORS" | grep 'Ben Finney '` - if [ -n "$GREP_OUT" ]; then - AUTHORS=`echo "$AUTHORS" | grep -v '^Ben Finney $'` - fi - - # Consolidate Trevor King - AUTHORS=`echo "$AUTHORS" | grep -v "wking "` - - # Consolidate Gianluca Montecchi - AUTHORS=`echo "$AUTHORS" | grep -v "gianluca"` - - # Sort again... - AUTHORS=`echo "$AUTHORS" | sort | uniq` - - # Generate new Copyright string - if [ "$ORIG_YEAR" == "$CURRENT_YEAR" ]; then - DATE_RANGE="$CURRENT_YEAR" - DATE_SPACE=" " - else - DATE_RANGE="${ORIG_YEAR}-${CURRENT_YEAR}" - DATE_SPACE=" " - fi - NUM_AUTHORS=`echo "$AUTHORS" | wc -l` - FIRST_AUTHOR=`echo "$AUTHORS" | head -n 1` - COPYRIGHT="# Copyright (C) $DATE_RANGE $FIRST_AUTHOR" - if [ $NUM_AUTHORS -gt 1 ]; then - OTHER_AUTHORS=`echo "$AUTHORS" | tail -n +2` - while read AUTHOR; do - COPYRIGHT=`echo "$COPYRIGHT\\n# $DATE_SPACE $AUTHOR"` - done < <(echo "$OTHER_AUTHORS") - fi - COPYRIGHT=`echo "$COPYRIGHT\\n$ESCAPED_TEXT"` - - # Strip old copyright info and replace with tag - awk 'BEGIN{incopy==0}{if(match($0, "^# Copyright")>0){incopy=1; print "-xyz-COPYRIGHT-zyx-"}else{if(incopy==0){print $0}else{if(match($0, "^#")==0){incopy=0; print $0}}}}' "$file" > "$TMP" - - # Replace tag in with new string - sed -i "s/^-xyz-COPYRIGHT-zyx-$/$COPYRIGHT/" "$TMP" - cp "$TMP" "$file" -done - -rm -f "$TMP" -- cgit From 4c6a1e6439293c7e584aef4fda0da1a3968fe7c9 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 19 Nov 2009 17:00:02 -0500 Subject: Ran the new update_copyright.py --- AUTHORS | 2 +- Makefile | 1 + be | 1 + becommands/assign.py | 1 + becommands/close.py | 1 + becommands/comment.py | 2 +- becommands/depend.py | 3 ++- becommands/diff.py | 1 + becommands/help.py | 1 + becommands/init.py | 1 + becommands/list.py | 2 +- becommands/merge.py | 3 ++- becommands/new.py | 1 + becommands/open.py | 1 + becommands/remove.py | 3 ++- becommands/set.py | 2 +- becommands/severity.py | 1 + becommands/show.py | 2 +- becommands/status.py | 3 ++- becommands/tag.py | 3 ++- doc/module.mk | 1 + interfaces/xml/be-xml-to-mbox | 3 +-- libbe/arch.py | 3 ++- libbe/beuuid.py | 3 ++- libbe/bug.py | 2 +- libbe/bugdir.py | 1 + libbe/bzr.py | 3 ++- libbe/cmdutil.py | 1 + libbe/comment.py | 2 +- libbe/config.py | 1 + libbe/darcs.py | 3 ++- libbe/diff.py | 1 + libbe/editor.py | 3 ++- libbe/encoding.py | 3 ++- libbe/git.py | 3 ++- libbe/hg.py | 3 ++- libbe/mapfile.py | 1 + libbe/plugin.py | 1 + libbe/properties.py | 3 ++- libbe/settings_object.py | 3 ++- libbe/tree.py | 3 ++- libbe/utility.py | 1 + libbe/vcs.py | 3 ++- 43 files changed, 60 insertions(+), 26 deletions(-) diff --git a/AUTHORS b/AUTHORS index 6b66315..5445afc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,7 +1,7 @@ Bugs Everywhere was written by: Aaron Bentley -Alexander Belchenko Alex Miller +Alexander Belchenko Ben Finney Chris Ball Gianluca Montecchi diff --git a/Makefile b/Makefile index fe482c3..3599a02 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ # # Copyright (C) 2008-2009 Ben Finney # Chris Ball +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/be b/be index feacfb4..8fe7dca 100755 --- a/be +++ b/be @@ -1,6 +1,7 @@ #!/usr/bin/env python # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. # Chris Ball +# Gianluca Montecchi # Oleg Romanyshyn # W. Trevor King # diff --git a/becommands/assign.py b/becommands/assign.py index 794f028..fbef281 100644 --- a/becommands/assign.py +++ b/becommands/assign.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # Marien Zwart # Thomas Gerigk # W. Trevor King diff --git a/becommands/close.py b/becommands/close.py index 0532ed2..2cdcb59 100644 --- a/becommands/close.py +++ b/becommands/close.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # Marien Zwart # Thomas Gerigk # W. Trevor King diff --git a/becommands/comment.py b/becommands/comment.py index 950a95a..dad32dd 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -1,5 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Chris Ball +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/becommands/depend.py b/becommands/depend.py index f72b8ba..f52527e 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -1,4 +1,5 @@ -# Copyright (C) 2009 W. Trevor King +# Copyright (C) 2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/becommands/diff.py b/becommands/diff.py index b6ac5b0..e71da9b 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/becommands/help.py b/becommands/help.py index a8f346a..c12c56a 100644 --- a/becommands/help.py +++ b/becommands/help.py @@ -1,4 +1,5 @@ # Copyright (C) 2006-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # Thomas Gerigk # W. Trevor King # diff --git a/becommands/init.py b/becommands/init.py index a6098ba..275dd77 100644 --- a/becommands/init.py +++ b/becommands/init.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/becommands/list.py b/becommands/list.py index 12e1e29..14e387b 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -1,5 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Chris Ball +# Gianluca Montecchi # Oleg Romanyshyn # W. Trevor King # diff --git a/becommands/merge.py b/becommands/merge.py index f212b01..bc18479 100644 --- a/becommands/merge.py +++ b/becommands/merge.py @@ -1,4 +1,5 @@ -# Copyright (C) 2008-2009 W. Trevor King +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/becommands/new.py b/becommands/new.py index a8ee2ec..30a7d28 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/becommands/open.py b/becommands/open.py index 0c6bf05..c9e55a2 100644 --- a/becommands/open.py +++ b/becommands/open.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # Marien Zwart # Thomas Gerigk # W. Trevor King diff --git a/becommands/remove.py b/becommands/remove.py index 8d85033..0e61362 100644 --- a/becommands/remove.py +++ b/becommands/remove.py @@ -1,4 +1,5 @@ -# Copyright (C) 2008-2009 W. Trevor King +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/becommands/set.py b/becommands/set.py index f7e68d3..e5cab8d 100644 --- a/becommands/set.py +++ b/becommands/set.py @@ -1,5 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Chris Ball +# Gianluca Montecchi # Marien Zwart # Thomas Gerigk # W. Trevor King diff --git a/becommands/severity.py b/becommands/severity.py index 660586e..e987760 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # Marien Zwart # Thomas Gerigk # W. Trevor King diff --git a/becommands/show.py b/becommands/show.py index 50bd6eb..94e16bf 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -1,5 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Chris Ball +# Gianluca Montecchi # Thomas Gerigk # Thomas Habets # W. Trevor King diff --git a/becommands/status.py b/becommands/status.py index f315003..827c7ce 100644 --- a/becommands/status.py +++ b/becommands/status.py @@ -1,4 +1,5 @@ -# Copyright (C) 2008-2009 W. Trevor King +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/becommands/tag.py b/becommands/tag.py index ecd853f..31b43ba 100644 --- a/becommands/tag.py +++ b/becommands/tag.py @@ -1,4 +1,5 @@ -# Copyright (C) 2009 W. Trevor King +# Copyright (C) 2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/doc/module.mk b/doc/module.mk index 7791f48..de33d6a 100644 --- a/doc/module.mk +++ b/doc/module.mk @@ -4,6 +4,7 @@ # Part of Bugs Everywhere, a distributed bug tracking system. # # Copyright (C) 2008-2009 Chris Ball +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/interfaces/xml/be-xml-to-mbox b/interfaces/xml/be-xml-to-mbox index c630447..7960d56 100755 --- a/interfaces/xml/be-xml-to-mbox +++ b/interfaces/xml/be-xml-to-mbox @@ -1,6 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2009 Chris Ball -# W. Trevor King +# Copyright (C) 2009 W. Trevor King # # 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 diff --git a/libbe/arch.py b/libbe/arch.py index 98c4edd..4687555 100644 --- a/libbe/arch.py +++ b/libbe/arch.py @@ -1,5 +1,6 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Ben Finney +# Ben Finney +# Gianluca Montecchi # James Rowe # W. Trevor King # diff --git a/libbe/beuuid.py b/libbe/beuuid.py index 490ed62..260f3dc 100644 --- a/libbe/beuuid.py +++ b/libbe/beuuid.py @@ -1,4 +1,5 @@ -# Copyright (C) 2008-2009 W. Trevor King +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/libbe/bug.py b/libbe/bug.py index 6633ab7..48f8358 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008-2009 Chris Ball +# Copyright (C) 2008-2009 Gianluca Montecchi # Thomas Habets # W. Trevor King # diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 5324163..5b942d3 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -1,6 +1,7 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. # Alexander Belchenko # Chris Ball +# Gianluca Montecchi # Oleg Romanyshyn # W. Trevor King # diff --git a/libbe/bzr.py b/libbe/bzr.py index 8e91d0c..2cf1cba 100644 --- a/libbe/bzr.py +++ b/libbe/bzr.py @@ -1,5 +1,6 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Ben Finney +# Ben Finney +# Gianluca Montecchi # Marien Zwart # W. Trevor King # diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index 9b64142..f1c8acd 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # Oleg Romanyshyn # W. Trevor King # diff --git a/libbe/comment.py b/libbe/comment.py index 17daf62..5f67878 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -1,5 +1,5 @@ # Bugs Everywhere, a distributed bugtracker -# Copyright (C) 2008-2009 Chris Ball +# Copyright (C) 2008-2009 Gianluca Montecchi # Thomas Habets # W. Trevor King # diff --git a/libbe/config.py b/libbe/config.py index fb5a028..4f32731 100644 --- a/libbe/config.py +++ b/libbe/config.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/libbe/darcs.py b/libbe/darcs.py index 6bf0119..059452c 100644 --- a/libbe/darcs.py +++ b/libbe/darcs.py @@ -1,4 +1,5 @@ -# Copyright (C) 2009 W. Trevor King +# Copyright (C) 2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/libbe/diff.py b/libbe/diff.py index 9253a23..cce3b0f 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/libbe/editor.py b/libbe/editor.py index ec41006..ed497a0 100644 --- a/libbe/editor.py +++ b/libbe/editor.py @@ -1,5 +1,6 @@ # Bugs Everywhere, a distributed bugtracker -# Copyright (C) 2008-2009 W. Trevor King +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/libbe/encoding.py b/libbe/encoding.py index fd513b5..b0a04cc 100644 --- a/libbe/encoding.py +++ b/libbe/encoding.py @@ -1,5 +1,6 @@ # Bugs Everywhere, a distributed bugtracker -# Copyright (C) 2008-2009 W. Trevor King +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/libbe/git.py b/libbe/git.py index 781a278..cb4436a 100644 --- a/libbe/git.py +++ b/libbe/git.py @@ -1,5 +1,6 @@ -# Copyright (C) 2008-2009 Ben Finney +# Copyright (C) 2008-2009 Ben Finney # Chris Ball +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/libbe/hg.py b/libbe/hg.py index 40739b6..d7eb796 100644 --- a/libbe/hg.py +++ b/libbe/hg.py @@ -1,5 +1,6 @@ # Copyright (C) 2007-2009 Aaron Bentley and Panometrics, Inc. -# Ben Finney +# Ben Finney +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/libbe/mapfile.py b/libbe/mapfile.py index d0e16fe..062606b 100644 --- a/libbe/mapfile.py +++ b/libbe/mapfile.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/libbe/plugin.py b/libbe/plugin.py index d593d69..bd9bb65 100644 --- a/libbe/plugin.py +++ b/libbe/plugin.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # Marien Zwart # W. Trevor King # diff --git a/libbe/properties.py b/libbe/properties.py index 09dd20e..54375b4 100644 --- a/libbe/properties.py +++ b/libbe/properties.py @@ -1,5 +1,6 @@ # Bugs Everywhere - a distributed bugtracker -# Copyright (C) 2008-2009 W. Trevor King +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/libbe/settings_object.py b/libbe/settings_object.py index ceea9d5..7efda5b 100644 --- a/libbe/settings_object.py +++ b/libbe/settings_object.py @@ -1,5 +1,6 @@ # Bugs Everywhere - a distributed bugtracker -# Copyright (C) 2008-2009 W. Trevor King +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/libbe/tree.py b/libbe/tree.py index 06d09e5..1bfd803 100644 --- a/libbe/tree.py +++ b/libbe/tree.py @@ -1,5 +1,6 @@ # Bugs Everywhere, a distributed bugtracker -# Copyright (C) 2008-2009 W. Trevor King +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King # # 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 diff --git a/libbe/utility.py b/libbe/utility.py index 1e43516..4126913 100644 --- a/libbe/utility.py +++ b/libbe/utility.py @@ -1,4 +1,5 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify diff --git a/libbe/vcs.py b/libbe/vcs.py index ba23858..1ac5dd9 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -1,7 +1,8 @@ # Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. # Alexander Belchenko -# Ben Finney +# Ben Finney # Chris Ball +# Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify -- cgit From 163b3aff3cc5de23bd5e88ba73dd957034cc206d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 07:28:20 -0500 Subject: Added release.py script automating release process --- release.py | 159 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100755 release.py diff --git a/release.py b/release.py new file mode 100755 index 0000000..d064b11 --- /dev/null +++ b/release.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +# +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os +import os.path +import shutil +import string +from subprocess import Popen +import sys + +from update_copyright import Pipe + +def validate_tag(tag): + """ + >>> validate_tag('1.0.0') + >>> validate_tag('A.B.C-r7') + >>> validate_tag('A.B.C r7') + Traceback (most recent call last): + ... + Exception: Invalid character ' ' in tag 'A.B.C r7' + >>> validate_tag('"') + Traceback (most recent call last): + ... + Exception: Invalid character '"' in tag '"' + >>> validate_tag("'") + Traceback (most recent call last): + ... + Exception: Invalid character ''' in tag ''' + """ + for char in tag: + if char in string.digits: + continue + elif char in string.letters: + continue + elif char in ['.','-']: + continue + raise Exception("Invalid character '%s' in tag '%s'" % (char, tag)) + +def bzr_pending_changes(): + """Use `bzr diff`s exit status to detect change: + 1 - changed + 2 - unrepresentable changes + 3 - error + 0 - no change + """ + p = Pipe([['bzr', 'diff']]) + if p.status == 0: + return False + elif p.status in [1,2]: + return True + raise Exception("Error in bzr diff %d\n%s" % (p.status, p.stderrs[-1])) + +def set_release_version(tag): + print "set libbe.version._VERSION = '%s'" % tag + p = Pipe([['sed', '-i', "s/^# *_VERSION *=.*/_VERSION = '%s'/" % tag, + os.path.join('libbe', 'version.py')]]) + assert p.status == 0, p.statuses + +def bzr_commit(commit_message): + print 'commit current status:', commit_message + p = Pipe([['bzr', 'commit', '-m', commit_message]]) + assert p.status == 0, p.statuses + +def bzr_tag(tag): + print 'tag current revision', tag + p = Pipe([['bzr', 'tag', tag]]) + assert p.status == 0, p.statuses + +def bzr_export(target_dir): + print 'export current revision to', target_dir + p = Pipe([['bzr', 'export', target_dir]]) + assert p.status == 0, p.statuses + +def make_version(): + print 'generate libbe/_version.py' + p = Pipe([['make', os.path.join('libbe', '_version.py')]]) + assert p.status == 0, p.statuses + +def make_changelog(filename, tag): + print 'generate ChangeLog file', filename, 'up to tag', tag + p = Popen(['bzr', 'log', '--gnu-changelog', '-n1', '-r', + '..tag:%s' % tag], stdout=file(filename, 'w')) + status = p.wait() + assert status == 0, status + +def set_vcs_name(filename, vcs_name='None'): + """Exported directory is not a bzr repository, so set vcs_name to + something that will work. + vcs_name: new_vcs_name + """ + print 'set_vcs_name in', filename, 'to', vcs_name + p = Pipe([['sed', '-i', "s/^vcs_name:.*/vcs_name: %s/" % vcs_name, + filename]]) + assert p.status == 0, p.statuses + +def create_tarball(tag): + release_name='be-%s' % tag + export_dir = release_name + bzr_export(export_dir) + make_version() + print 'copy libbe/_version.py to %s/libbe/_version.py' % export_dir + shutil.copy(os.path.join('libbe', '_version.py'), + os.path.join(export_dir, 'libbe', '_version.py')) + make_changelog(os.path.join(export_dir, 'ChangeLog'), tag) + set_vcs_name(os.path.join(export_dir, '.be', 'settings')) + tarball_file = '%s.tar.gz' % release_name + print 'create tarball', tarball_file + p = Pipe([['tar', '-czf', tarball_file, export_dir]]) + assert p.status == 0, p.statuses + print 'remove', export_dir + shutil.rmtree(export_dir) + +def test(): + import doctest + doctest.testmod() + +if __name__ == '__main__': + import optparse + usage = """%prog [options] TAG + +Create a bzr tag and a release tarball from the current revision. +For example + %prog 1.0.0 +""" + p = optparse.OptionParser(usage) + p.add_option('--test', dest='test', default=False, + action='store_true', help='Run internal tests and exit') + options,args = p.parse_args() + + if options.test == True: + test() + sys.exit(0) + + assert len(args) == 1, '%d (!= 1) arguments: %s' % (len(args), args) + tag = args[0] + validate_tag(tag) + + if bzr_pending_changes() == True: + print "Handle pending changes before releasing." + sys.exit(1) + set_release_version(tag) + bzr_commit("Bumped to version %s" % tag) + bzr_tag(tag) + create_tarball(tag) -- cgit From 11930e31b3511354da05550d3f7cf9593828df76 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 07:57:17 -0500 Subject: Have release.py update copyrights as well. --- release.py | 6 ++++-- update_copyright.py | 27 ++++++++++++++------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/release.py b/release.py index d064b11..996e363 100755 --- a/release.py +++ b/release.py @@ -23,7 +23,7 @@ import string from subprocess import Popen import sys -from update_copyright import Pipe +from update_copyright import Pipe, update_authors, update_files def validate_tag(tag): """ @@ -103,7 +103,7 @@ def set_vcs_name(filename, vcs_name='None'): something that will work. vcs_name: new_vcs_name """ - print 'set_vcs_name in', filename, 'to', vcs_name + print 'set vcs_name in', filename, 'to', vcs_name p = Pipe([['sed', '-i', "s/^vcs_name:.*/vcs_name: %s/" % vcs_name, filename]]) assert p.status == 0, p.statuses @@ -154,6 +154,8 @@ For example print "Handle pending changes before releasing." sys.exit(1) set_release_version(tag) + update_authors() + update_files() bzr_commit("Bumped to version %s" % tag) bzr_tag(tag) create_tarball(tag) diff --git a/update_copyright.py b/update_copyright.py index 4a52fee..6cdaa2f 100755 --- a/update_copyright.py +++ b/update_copyright.py @@ -409,6 +409,19 @@ def update_file(filename, verbose=True): f.write(contents) f.close() +def update_files(files=None): + if files == None or len(files) == 0: + p = Pipe([['grep', '-rc', '# Copyright', '.'], + ['grep', '-v', ':0$'], + ['cut', '-d:', '-f1']]) + assert p.status == 0 + files = p.stdout.rstrip().split('\n') + + for filename in files: + if ignored_file(filename) == True: + continue + update_file(filename) + def test(): import doctest doctest.testmod() @@ -439,16 +452,4 @@ automatically. sys.exit(0) update_authors() - - files = args - if len(files) == 0: - p = Pipe([['grep', '-rc', '# Copyright', '.'], - ['grep', '-v', ':0$'], - ['cut', '-d:', '-f1']]) - assert p.status == 0 - files = p.stdout.rstrip().split('\n') - - for filename in files: - if ignored_file(filename) == True: - continue - update_file(filename) + update_files(files=args) -- cgit From 9d90a63140f32a50dda9213859a1e168752fcab9 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 08:33:40 -0500 Subject: Created bug 56506b73-36cc-4e32-a578-258a219edba8 describing problem --- .../comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body | 18 ++++++++++++++++++ .../0a995544-20dc-42a6-8d3f-348ebbc8921e/values | 8 ++++++++ .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values | 17 +++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body create mode 100644 .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values create mode 100644 .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body new file mode 100644 index 0000000..8596c92 --- /dev/null +++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/body @@ -0,0 +1,18 @@ +Since we'll be distributing a non-bzr-repo version, it would be nice +to adapt our 'submit bug' procedure + $ be new "The demuxulizer is broken" + Created bug with ID 48f + $ be comment 48f + + $ bzr commit --message "Reported bug in demuxulizer" + $ bzr send --mail-to "be-devel@bugseverywhere.org" +to one that works with this setup. Without guaranteed versioning, +that would probably be something along the lines of + $ be new "The demuxulizer is broken" + Created bug with ID 48f + $ be comment 48f + + $ be email-bugs [--to be-devel@bugseverywhere.org] 48f +With interfaces/email/interactive listening on the recieving end to +grab new-bug emails and import them into an incoming bug repository. + diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values new file mode 100644 index 0000000..4bd8f81 --- /dev/null +++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e/values @@ -0,0 +1,8 @@ +Author: W. Trevor King + + +Content-type: text/plain + + +Date: Fri, 20 Nov 2009 13:31:25 +0000 + diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values new file mode 100644 index 0000000..2e15ca9 --- /dev/null +++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values @@ -0,0 +1,17 @@ +creator: W. Trevor King + + +reporter: W. Trevor King + + +severity: minor + + +status: open + + +summary: be email-bugs for bug submission from bzr-less users + + +time: Fri, 20 Nov 2009 13:26:59 +0000 + -- cgit From 401411e519f1b4e6206e9020902536a54ca8750b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 09:41:44 -0500 Subject: Removed superfluous 'import time' from becommands/commit.py doctest. --- becommands/commit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/becommands/commit.py b/becommands/commit.py index dc70e7e..b530fdc 100644 --- a/becommands/commit.py +++ b/becommands/commit.py @@ -20,7 +20,7 @@ __desc__ = __doc__ def execute(args, manipulate_encodings=True): """ - >>> import os, time + >>> import os >>> from libbe import bug >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) -- cgit From 0d0dbf9587cb65b08365094d23818da2c8823110 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 10:48:36 -0500 Subject: Broke subprocess handling out into its own submodule libbe.subproc. --- libbe/bzr.py | 2 +- libbe/git.py | 2 +- libbe/subproc.py | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++ libbe/vcs.py | 50 +++--------- release.py | 8 +- update_copyright.py | 141 +--------------------------------- 6 files changed, 233 insertions(+), 186 deletions(-) create mode 100644 libbe/subproc.py diff --git a/libbe/bzr.py b/libbe/bzr.py index 2cf1cba..281493d 100644 --- a/libbe/bzr.py +++ b/libbe/bzr.py @@ -90,7 +90,7 @@ class Bzr(vcs.VCS): if self._u_any_in_string(strings, error) == True: raise vcs.EmptyCommit() else: - raise vcs.CommandError(args, status, stdout="", stderr=error) + raise vcs.CommandError(args, status, stderr=error) revision = None revline = re.compile("Committed revision (.*)[.]") match = revline.search(error) diff --git a/libbe/git.py b/libbe/git.py index cb4436a..55556de 100644 --- a/libbe/git.py +++ b/libbe/git.py @@ -134,7 +134,7 @@ class Git(vcs.VCS): if status == 128: if error.startswith("fatal: ambiguous argument 'HEAD': unknown "): return None - raise vcs.CommandError(args, status, stdout="", stderr=error) + raise vcs.CommandError(args, status, stderr=error) commits = output.splitlines() try: return commits[index] diff --git a/libbe/subproc.py b/libbe/subproc.py new file mode 100644 index 0000000..13afcf8 --- /dev/null +++ b/libbe/subproc.py @@ -0,0 +1,216 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Functions for running external commands in subprocesses. +""" + +from subprocess import Popen, PIPE +import sys +import doctest + +from encoding import get_encoding + +_MSWINDOWS = sys.platform == 'win32' +_POSIX = not _MSWINDOWS + +class CommandError(Exception): + def __init__(self, command, status, stdout=None, stderr=None): + strerror = ['Command failed (%d):\n %s\n' % (status, stderr), + 'while executing\n %s' % command] + Exception.__init__(self, '\n'.join(strerror)) + self.command = command + self.status = status + self.stdout = stdout + self.stderr = stderr + +def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,), + cwd=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 + unicode_output == True, convert stdout and stdin strings to + unicode before returing them. + """ + if cwd == None: + cwd = '.' + if verbose == True: + print >> sys.stderr, '%s$ %s' % (cwd, ' '.join(args)) + try : + if _POSIX: + q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd) + else: + assert _MSWINDOWS==True, 'invalid platform' + # 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) + except OSError, e: + raise CommandError(args, status=e.args[0], stderr=e) + stdout,stderr = q.communicate(input=stdin) + status = q.wait() + if unicode_output == True: + if encoding == None: + encoding = get_encoding() + if stdout != None: + stdout = unicode(stdout, encoding) + if stderr != None: + stderr = unicode(stderr, encoding) + if verbose == True: + print >> sys.stderr, '%d\n%s%s' % (status, stdout, stderr) + if status not in expect: + raise CommandError(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. + + >>> p = Pipe([['find', '/etc/'], ['grep', '^/etc/ssh$']]) + >>> p.stdout + '/etc/ssh\\n' + >>> p.status + 1 + >>> p.statuses + [1, 0] + >>> p.stderrs # doctest: +ELLIPSIS + ["find: `...': Permission denied\\n...", ''] + """ + def __init__(self, cmds, stdin=None): + # spawn processes + self._procs = [] + for cmd in cmds: + if len(self._procs) != 0: + stdin = self._procs[-1].stdout + self._procs.append(Popen(cmd, stdin=stdin, stdout=PIPE, stderr=PIPE)) + + self.stdout,self.stderrs = self._communicate(input=None) + + # collect process statuses + self.statuses = [] + self.status = 0 + for proc in self._procs: + self.statuses.append(proc.wait()) + if self.statuses[-1] != 0: + self.status = self.statuses[-1] + + # Code excerpted from subprocess.Popen._communicate() + if _MSWINDOWS == True: + def _communicate(self, input=None): + assert input == None, 'stdin != None not yet supported' + # listen to each process' stderr + threads = [] + std_X_arrays = [] + for proc in self._procs: + stderr_array = [] + thread = Thread(target=proc._readerthread, + args=(proc.stderr, stderr_array)) + thread.setDaemon(True) + thread.start() + threads.append(thread) + std_X_arrays.append(stderr_array) + + # also listen to the last processes stdout + stdout_array = [] + thread = Thread(target=proc._readerthread, + args=(proc.stdout, stdout_array)) + thread.setDaemon(True) + thread.start() + threads.append(thread) + std_X_arrays.append(stdout_array) + + # join threads as they die + for thread in threads: + thread.join() + + # read output from reader threads + std_X_strings = [] + for std_X_array in std_X_arrays: + std_X_strings.append(std_X_array[0]) + + stdout = std_X_strings.pop(-1) + stderrs = std_X_strings + return (stdout, stderrs) + else: + assert _POSIX==True, 'invalid platform' + def _communicate(self, input=None): + read_set = [] + write_set = [] + read_arrays = [] + stdout = None # Return + stderr = None # Return + + if self._procs[0].stdin: + # Flush stdio buffer. This might block, if the user has + # been writing to .stdin in an uncontrolled fashion. + self._procs[0].stdin.flush() + if input: + write_set.append(self._procs[0].stdin) + else: + self._procs[0].stdin.close() + for proc in self._procs: + read_set.append(proc.stderr) + read_arrays.append([]) + read_set.append(self._procs[-1].stdout) + read_arrays.append([]) + + input_offset = 0 + while read_set or write_set: + try: + rlist, wlist, xlist = select.select(read_set, write_set, []) + except select.error, e: + if e.args[0] == errno.EINTR: + continue + raise + if self._procs[0].stdin in wlist: + # When select has indicated that the file is writable, + # we can write up to PIPE_BUF bytes without risk + # blocking. POSIX defines PIPE_BUF >= 512 + chunk = input[input_offset : input_offset + 512] + bytes_written = os.write(self.stdin.fileno(), chunk) + input_offset += bytes_written + if input_offset >= len(input): + self._procs[0].stdin.close() + write_set.remove(self._procs[0].stdin) + if self._procs[-1].stdout in rlist: + data = os.read(self._procs[-1].stdout.fileno(), 1024) + if data == '': + self._procs[-1].stdout.close() + read_set.remove(self._procs[-1].stdout) + read_arrays[-1].append(data) + for i,proc in enumerate(self._procs): + if proc.stderr in rlist: + data = os.read(proc.stderr.fileno(), 1024) + if data == '': + proc.stderr.close() + read_set.remove(proc.stderr) + read_arrays[i].append(data) + + # All data exchanged. Translate lists into strings. + read_strings = [] + for read_array in read_arrays: + read_strings.append(''.join(read_array)) + + stdout = read_strings.pop(-1) + stderrs = read_strings + return (stdout, stderrs) + +suite = doctest.DocTestSuite() diff --git a/libbe/vcs.py b/libbe/vcs.py index 1ac5dd9..be28846 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -25,7 +25,6 @@ subclassed by other Version Control System backends. The base class implements a "do not version" VCS. """ -from subprocess import Popen, PIPE import codecs import os import os.path @@ -38,6 +37,7 @@ import unittest import doctest from utility import Dir, search_parent_directories +from subproc import CommandError, invoke def _get_matching_vcs(matchfn): @@ -67,15 +67,6 @@ def installed_vcs(): return _get_matching_vcs(lambda vcs: vcs.installed()) -class CommandError(Exception): - def __init__(self, command, status, stdout, stderr): - strerror = ["Command failed (%d):\n %s\n" % (status, stderr), - "while executing\n %s" % command] - Exception.__init__(self, "\n".join(strerror)) - self.command = command - self.status = status - self.stdout = stdout - self.stderr = stderr class SettingIDnotSupported(NotImplementedError): pass @@ -457,37 +448,14 @@ class VCS(object): if list_string in string: return True return False - def _u_invoke(self, args, stdin=None, expect=(0,), cwd=None, - unicode_output=True): - """ - expect should be a tuple of allowed exit codes. cwd should be - the directory from which the command will be executed. When - unicode_output == True, convert stdout and stdin strings to - unicode before returing them. - """ - if cwd == None: - cwd = self.rootdir - if self.verboseInvoke == True: - print >> sys.stderr, "%s$ %s" % (cwd, " ".join(args)) - try : - if sys.platform != "win32": - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd) - else: - # win32 don't have os.execvp() so have to run command in a shell - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, - shell=True, cwd=cwd) - except OSError, e : - raise CommandError(args, status=e.args[0], stdout="", stderr=e) - stdout,stderr = q.communicate(input=stdin) - status = q.wait() - if unicode_output == True: - stdout = unicode(stdout, self.encoding) - stderr = unicode(stderr, self.encoding) - if self.verboseInvoke == True: - print >> sys.stderr, "%d\n%s%s" % (status, stdout, stderr) - if status not in expect: - raise CommandError(args, status, stdout, stderr) - return status, stdout, stderr + def _u_invoke(self, *args, **kwargs): + if 'cwd' not in kwargs: + kwargs['cwd'] = self.rootdir + if 'verbose' not in kwargs: + kwargs['verbose'] = self.verboseInvoke + if 'encoding' not in kwargs: + kwargs['encoding'] = self.encoding + return invoke(*args, **kwargs) def _u_invoke_client(self, *args, **kwargs): cl_args = [self.client] cl_args.extend(args) diff --git a/release.py b/release.py index 996e363..2e75687 100755 --- a/release.py +++ b/release.py @@ -20,10 +20,10 @@ import os import os.path import shutil import string -from subprocess import Popen import sys -from update_copyright import Pipe, update_authors, update_files +from libbe.subproc import Pipe, invoke +from update_copyright import update_authors, update_files def validate_tag(tag): """ @@ -93,8 +93,8 @@ def make_version(): def make_changelog(filename, tag): print 'generate ChangeLog file', filename, 'up to tag', tag - p = Popen(['bzr', 'log', '--gnu-changelog', '-n1', '-r', - '..tag:%s' % tag], stdout=file(filename, 'w')) + p = invoke(['bzr', 'log', '--gnu-changelog', '-n1', '-r', + '..tag:%s' % tag], stdout=file(filename, 'w')) status = p.wait() assert status == 0, status diff --git a/update_copyright.py b/update_copyright.py index 6cdaa2f..5ca5455 100755 --- a/update_copyright.py +++ b/update_copyright.py @@ -24,9 +24,10 @@ import time import os import sys import select -from subprocess import Popen, PIPE, mswindows from threading import Thread +from libbe.subproc import Pipe + COPYRIGHT_TEXT="""# # 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 @@ -71,144 +72,6 @@ EXCLUDES = [ IGNORED_PATHS = ['./.be/', './.bzr/', './build/'] IGNORED_FILES = ['COPYING', 'update_copyright.py', 'catmutt'] -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. - - >>> p = Pipe([['find', '/etc/'], ['grep', '^/etc/ssh$']]) - >>> p.stdout - '/etc/ssh\\n' - >>> p.status - 1 - >>> p.statuses - [1, 0] - >>> p.stderrs # doctest: +ELLIPSIS - ["find: `...': Permission denied\\n...", ''] - """ - def __init__(self, cmds, stdin=None): - # spawn processes - self._procs = [] - for cmd in cmds: - if len(self._procs) != 0: - stdin = self._procs[-1].stdout - self._procs.append(Popen(cmd, stdin=stdin, stdout=PIPE, stderr=PIPE)) - - self.stdout,self.stderrs = self._communicate(input=None) - - # collect process statuses - self.statuses = [] - self.status = 0 - for proc in self._procs: - self.statuses.append(proc.wait()) - if self.statuses[-1] != 0: - self.status = self.statuses[-1] - - # Code excerpted from subprocess.Popen._communicate() - if mswindows == True: - def _communicate(self, input=None): - assert input == None, "stdin != None not yet supported" - # listen to each process' stderr - threads = [] - std_X_arrays = [] - for proc in self._procs: - stderr_array = [] - thread = Thread(target=proc._readerthread, - args=(proc.stderr, stderr_array)) - thread.setDaemon(True) - thread.start() - threads.append(thread) - std_X_arrays.append(stderr_array) - - # also listen to the last processes stdout - stdout_array = [] - thread = Thread(target=proc._readerthread, - args=(proc.stdout, stdout_array)) - thread.setDaemon(True) - thread.start() - threads.append(thread) - std_X_arrays.append(stdout_array) - - # join threads as they die - for thread in threads: - thread.join() - - # read output from reader threads - std_X_strings = [] - for std_X_array in std_X_arrays: - std_X_strings.append(std_X_array[0]) - - stdout = std_X_strings.pop(-1) - stderrs = std_X_strings - return (stdout, stderrs) - else: # POSIX - def _communicate(self, input=None): - read_set = [] - write_set = [] - read_arrays = [] - stdout = None # Return - stderr = None # Return - - if self._procs[0].stdin: - # Flush stdio buffer. This might block, if the user has - # been writing to .stdin in an uncontrolled fashion. - self._procs[0].stdin.flush() - if input: - write_set.append(self._procs[0].stdin) - else: - self._procs[0].stdin.close() - for proc in self._procs: - read_set.append(proc.stderr) - read_arrays.append([]) - read_set.append(self._procs[-1].stdout) - read_arrays.append([]) - - input_offset = 0 - while read_set or write_set: - try: - rlist, wlist, xlist = select.select(read_set, write_set, []) - except select.error, e: - if e.args[0] == errno.EINTR: - continue - raise - if self._procs[0].stdin in wlist: - # When select has indicated that the file is writable, - # we can write up to PIPE_BUF bytes without risk - # blocking. POSIX defines PIPE_BUF >= 512 - chunk = input[input_offset : input_offset + 512] - bytes_written = os.write(self.stdin.fileno(), chunk) - input_offset += bytes_written - if input_offset >= len(input): - self._procs[0].stdin.close() - write_set.remove(self._procs[0].stdin) - if self._procs[-1].stdout in rlist: - data = os.read(self._procs[-1].stdout.fileno(), 1024) - if data == "": - self._procs[-1].stdout.close() - read_set.remove(self._procs[-1].stdout) - read_arrays[-1].append(data) - for i,proc in enumerate(self._procs): - if proc.stderr in rlist: - data = os.read(proc.stderr.fileno(), 1024) - if data == "": - proc.stderr.close() - read_set.remove(proc.stderr) - read_arrays[i].append(data) - - # All data exchanged. Translate lists into strings. - read_strings = [] - for read_array in read_arrays: - read_strings.append(''.join(read_array)) - - stdout = read_strings.pop(-1) - stderrs = read_strings - return (stdout, stderrs) - def _strip_email(*args): """ >>> _strip_email('J Doe ') -- cgit From 19b3a1d77946b4bbec0788d1ac3270c5cddbd54a Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 13:25:08 -0500 Subject: Added becommands/email_bugs.py This send a list of bugs in a single email off to be processed by be-handle-mail. Of course, be-handle-mail doesn't yet handle the [be-bug:xml] format that email_bugs.py creates. On to that now... --- becommands/email_bugs.py | 231 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 becommands/email_bugs.py diff --git a/becommands/email_bugs.py b/becommands/email_bugs.py new file mode 100644 index 0000000..9aeccec --- /dev/null +++ b/becommands/email_bugs.py @@ -0,0 +1,231 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Email specified bugs in a be-handle-mail compatible format.""" + +import copy +from cStringIO import StringIO +from email import Message +from email.mime.text import MIMEText +from email.generator import Generator +import sys +import time + +from libbe import cmdutil, bugdir +from libbe.subproc import invoke +from libbe.utility import time_to_str +from libbe.vcs import detect_vcs, installed_vcs + +__desc__ = __doc__ + +sendmail='/usr/sbin/sendmail -t' + +def execute(args, manipulate_encodings=True): + """ + >>> import os + >>> from libbe import bug + >>> bd = bugdir.SimpleBugDir() + >>> bd.encoding = 'utf-8' + >>> os.chdir(bd.root) + >>> import email.charset as c + >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8') + >>> execute(["-o", "--to", "a@b.com", "--from", "b@c.edu", "a", "b"], + ... manipulate_encodings=False) # doctest: +ELLIPSIS + Content-Type: text/xml; charset="utf-8" + MIME-Version: 1.0 + Content-Transfer-Encoding: quoted-printable + From: b@c.edu + To: a@b.com + Date: ... + Subject: [be-bug:xml] Updates to a, b + + + + + a + a + minor + open + John Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug A + + + b + b + minor + closed + Jane Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug B + + + >>> bd.cleanup() + + Note that the '=3D' bits in + + are the way quoted-printable escapes '='. + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={-1: lambda bug : bug.active==True}) + if len(args) == 0: + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + lines = [u'' % bd.encoding, + u''] + for shortname in args: + bug = cmdutil.bug_from_shortname(bd, shortname) + lines.append(bug.xml(show_comments=True)) + lines.append(u'') + subject = options.subject + if subject == None: + subject = '[be-bug:xml] Updates to %s' % ', '.join(args) + submit_email = TextEmail(to_address=options.to_address, + from_address=options.from_address, + subject=subject, + body=u'\n'.join(lines), + encoding=bd.encoding, + subtype='xml') + if options.output == True: + print submit_email + else: + submit_email.send() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be email-bugs [options] ID [ID ...]") + parser.add_option("-t", "--to", metavar="EMAIL", dest="to_address", + help="Submission email address (%default)", + default="be-devel@bugseverywhere.org") + parser.add_option("-f", "--from", metavar="EMAIL", dest="from_address", + help="Senders email address, overriding auto-generated default", + default=None) + parser.add_option("-s", "--subject", metavar="STRING", dest="subject", + help="Subject line, overriding auto-generated default. If you use this option, remember that be-handle-mail probably want something like '[be-bug:xml] ...'", + default=None) + parser.add_option('-o', '--output', dest='output', action='store_true', + help="Don't mail the generated message, print it to stdout instead. Useful for testing functionality.") + return parser + +longhelp=""" +Email specified bugs in a be-handle-mail compatible format. This is +the prefered method for reporting bugs if you did not install bzr by +branching a bzr repository. + +If you _did_ install bzr by branching a bzr repository, we suggest you +commit any new bug information with + bzr commit --message "Reported bug in demuxulizer" +and then email a bzr merge directive with + bzr send --mail-to "be-devel@bugseverywhere.org" +rather than using this command. +""" + +def help(): + return get_parser().help_str() + longhelp + +class TextEmail (object): + """ + Make it very easy to compose and send single-part text emails. + >>> msg = TextEmail(to_address='Monty ', + ... from_address='Python ', + ... subject='Parrots', + ... header={'x-special-header':'your info here'}, + ... body="Remarkable bird, id'nit, squire?\\nLovely plumage!") + >>> print msg # doctest: +ELLIPSIS + Content-Type: text/plain; charset="utf-8" + MIME-Version: 1.0 + Content-Transfer-Encoding: base64 + From: Python + To: Monty + Date: ... + Subject: Parrots + x-special-header: your info here + + UmVtYXJrYWJsZSBiaXJkLCBpZCduaXQsIHNxdWlyZT8KTG92ZWx5IHBsdW1hZ2Uh + + >>> import email.charset as c + >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8') + >>> print msg # doctest: +ELLIPSIS + Content-Type: text/plain; charset="utf-8" + MIME-Version: 1.0 + Content-Transfer-Encoding: quoted-printable + From: Python + To: Monty + Date: ... + Subject: Parrots + x-special-header: your info here + + Remarkable bird, id'nit, squire? + Lovely plumage! + """ + def __init__(self, to_address, from_address=None, subject=None, + header=None, body=None, encoding='utf-8', subtype='plain'): + self.to_address = to_address + self.from_address = from_address + if self.from_address == None: + self.from_address = self._guess_from_address() + self.subject = subject + self.header = header + if self.header == None: + self.header = {} + self.body = body + self.encoding = encoding + self.subtype = subtype + def _guess_from_address(self): + vcs = detect_vcs('.') + if vcs.name == "None": + vcs = installed_vcs() + return vcs.get_user_id() + def encoded_MIME_body(self): + return MIMEText(self.body.encode(self.encoding), + self.subtype, + self.encoding) + def message(self): + response = self.encoded_MIME_body() + response['From'] = self.from_address + response['To'] = self.to_address + response['Date'] = time_to_str(time.time()) + response['Subject'] = self.subject + for k,v in self.header.items(): + response[k] = v + return response + def flatten(self, to_unicode=False): + """ + This is a simplified version of send_pgp_mime.flatten(). + """ + fp = StringIO() + g = Generator(fp, mangle_from_=False) + g.flatten(self.message()) + text = fp.getvalue() + if to_unicode == True: + encoding = msg.get_content_charset() or "utf-8" + text = unicode(text, encoding=encoding) + return text + def __str__(self): + return self.flatten() + def __unicode__(self): + return self.flatten(to_unicode=True) + def send(self, sendmail=None): + """ + This is a simplified version of send_pgp_mime.mail(). + + Send an email Message instance on its merry way by shelling + out to the user specified sendmail. + """ + if sendmail == None: + sendmail = SENDMAIL + invoke(sendmail, stdin=self.flatten()) -- cgit From b35dfdf10b2f58a0632d0a0542bd8232a39b0391 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 14:29:01 -0500 Subject: Adjusted test.py to use an installed vcs by default. Protects agaist the off chance that the user doesn't have Arch (tla) installed ;). Changed Arch.name from "Arch" to "arch" so that each VCSs .name matches the module name. This allows us to use vcs.VCS_ORDER (a list of module names) to set up the allowed values of BugDir.vcs_name. --- becommands/init.py | 13 ++++++------- libbe/arch.py | 2 +- libbe/bugdir.py | 2 +- libbe/vcs.py | 23 +++++++++++++++++------ test.py | 9 +++++---- 5 files changed, 30 insertions(+), 19 deletions(-) diff --git a/becommands/init.py b/becommands/init.py index 275dd77..75db0ac 100644 --- a/becommands/init.py +++ b/becommands/init.py @@ -37,14 +37,13 @@ def execute(args, manipulate_encodings=True): >>> dir = utility.Dir() >>> os.chdir(dir.path) - >>> vcs = vcs.installed_vcs() - >>> vcs.init('.') - >>> print vcs.name - Arch - >>> execute([], manipulate_encodings=False) - Using Arch for revision control. + >>> _vcs = vcs.installed_vcs() + >>> _vcs.init('.') + >>> _vcs.name in vcs.VCS_ORDER + >>> execute([], manipulate_encodings=False) # doctest: +ELLIPSIS + Using ... for revision control. Directory initialized. - >>> vcs.cleanup() + >>> _vcs.cleanup() >>> try: ... execute(['--root', '.'], manipulate_encodings=False) diff --git a/libbe/arch.py b/libbe/arch.py index 4687555..48129b5 100644 --- a/libbe/arch.py +++ b/libbe/arch.py @@ -45,7 +45,7 @@ def new(): return Arch() class Arch(vcs.VCS): - name = "Arch" + name = "arch" client = client versioned = True _archive_name = None diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 5b942d3..675b744 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -213,7 +213,7 @@ that the Arch VCS backend *enforces* ids with this format.""", settings easy. Don't set this attribute. Set .vcs instead, and .vcs_name will be automatically adjusted.""", default="None", - allowed=["None", "Arch", "bzr", "darcs", "git", "hg"]) + allowed=["None"]+vcs.VCS_ORDER) def vcs_name(): return {} def _get_vcs(self, vcs_name=None): diff --git a/libbe/vcs.py b/libbe/vcs.py index be28846..b1fd114 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -38,16 +38,23 @@ import doctest from utility import Dir, search_parent_directories from subproc import CommandError, invoke +from plugin import get_plugin +# List VCS modules in order of preference. +# Don't list this module, it is implicitly last. +VCS_ORDER = ['arch', 'bzr', 'darcs', 'git', 'hg'] + +def set_preferred_vcs(name): + global VCS_ORDER + assert name in VCS_ORDER, \ + 'unrecognized VCS %s not in\n %s' % (name, VCS_ORDER) + VCS_ORDER.remove(name) + VCS_ORDER.insert(0, name) def _get_matching_vcs(matchfn): """Return the first module for which matchfn(VCS_instance) is true""" - import arch - import bzr - import darcs - import git - import hg - for module in [arch, bzr, darcs, git, hg]: + for submodname in VCS_ORDER: + module = get_plugin('libbe', submodname) vcs = module.new() if matchfn(vcs) == True: return vcs @@ -117,6 +124,10 @@ class VCS(object): self._duplicateDirname = None self.encoding = encoding self.version = self._get_version() + def __str__(self): + return "<%s %s>" % (self.__class__.__name__, id(self)) + def __repr__(self): + return str(self) def _vcs_version(self): """ Return the VCS version string. diff --git a/test.py b/test.py index 1f1ffcf..57091c7 100644 --- a/test.py +++ b/test.py @@ -8,7 +8,7 @@ When called with module name arguments, only run the doctests from those modules. """ -from libbe import plugin +from libbe import plugin, vcs import unittest import doctest import sys @@ -41,9 +41,10 @@ else: for modname,module in plugin.iter_plugins("becommands"): suite.addTest(doctest.DocTestSuite(module)) -#for s in suite._tests: -# print s -#exit(0) +_vcs = vcs.installed_vcs() +vcs.set_preferred_vcs(_vcs.name) +print 'Using %s as the testing VCS' % _vcs.name + result = unittest.TextTestRunner(verbosity=2).run(suite) numErrors = len(result.errors) -- cgit From 6d0f064203698638193e6641c1358676c6f37b60 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 15:09:42 -0500 Subject: Import select in libbe.subproc if _POSIX == True --- libbe/subproc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libbe/subproc.py b/libbe/subproc.py index 13afcf8..e925c62 100644 --- a/libbe/subproc.py +++ b/libbe/subproc.py @@ -27,6 +27,9 @@ from encoding import get_encoding _MSWINDOWS = sys.platform == 'win32' _POSIX = not _MSWINDOWS +if _POSIX == True: + import select + class CommandError(Exception): def __init__(self, command, status, stdout=None, stderr=None): strerror = ['Command failed (%d):\n %s\n' % (status, stderr), -- cgit From ec602165cca450dab09ff79f5baf90e8957535d4 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 15:10:59 -0500 Subject: Fix typo in becommands.init's doctest --- becommands/init.py | 1 + 1 file changed, 1 insertion(+) diff --git a/becommands/init.py b/becommands/init.py index 75db0ac..39fb006 100644 --- a/becommands/init.py +++ b/becommands/init.py @@ -40,6 +40,7 @@ def execute(args, manipulate_encodings=True): >>> _vcs = vcs.installed_vcs() >>> _vcs.init('.') >>> _vcs.name in vcs.VCS_ORDER + True >>> execute([], manipulate_encodings=False) # doctest: +ELLIPSIS Using ... for revision control. Directory initialized. -- cgit From 64cb5e5ec672cd357bc66a8480465e531db25f52 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 16:48:07 -0500 Subject: Fixed bug 529c290e-b1cf-4800-be7e-68f1ecb9565c (howto version BE). Also appended some of the recent discussion about releases to the comment list. --- .../1847f1f8-525a-42c4-ae2b-e9377459d2a6/body | 27 ++++++ .../1847f1f8-525a-42c4-ae2b-e9377459d2a6/values | 8 ++ .../49e0425b-3332-4d0e-b371-300eccd55370/body | 51 +++++++++++ .../49e0425b-3332-4d0e-b371-300eccd55370/values | 11 +++ .../72a519e3-3d6b-4f0f-b412-1310efd255eb/body | 22 +++++ .../72a519e3-3d6b-4f0f-b412-1310efd255eb/values | 14 +++ .../96abea83-9867-4c21-8eb8-9e1b1093cba4/body | 36 ++++++++ .../96abea83-9867-4c21-8eb8-9e1b1093cba4/values | 11 +++ .../a4720227-43cf-49aa-8f9f-f49f46e3e809/body | 102 +++++++++++++++++++++ .../a4720227-43cf-49aa-8f9f-f49f46e3e809/values | 11 +++ .../f92c6180-0ed8-4acc-8ced-22995a0c016b/body | 2 + .../f92c6180-0ed8-4acc-8ced-22995a0c016b/values | 8 ++ .../529c290e-b1cf-4800-be7e-68f1ecb9565c/values | 2 +- 13 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/body create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/body create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/body create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/values create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/body create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/body create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/body create mode 100644 .be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/values diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/body b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/body new file mode 100644 index 0000000..8c890f3 --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/body @@ -0,0 +1,27 @@ +"W. Trevor King" writes: + +> On Tue, Nov 17, 2009 at 01:41:26PM -0300, Nicolas Alvarez wrote: +> > I'm using the latest version available on Debian +> > (0.0.193+bzr.r217-2). I should ask for an updated package... +> +[…] + +> There is also an outstanding Debian bug for updating the Debian package +> http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=544515 +> so there may be a more current package on the way, but I don't know +> about timeframes for that sort of thing. + +It would make it much easier on the Debian package maintainer if the +Bugs Everywhere project would make conventional tarball releases, with +conventional version numbers, with a changelog describing what has +changed between versions. + +Trying to maintain a package of a project that is only made available by +undifferentiated VCS revision numbers is a lot more effort, and so +doesn't happen very often. + +-- + \ “Roll dice!” “Why?” “Shut up! I don't need your fucking | + `\ *input*, I need you to roll dice!” —Luke Crane, demonstrating | +_o__) his refined approach to play testing, 2009 | +Ben Finney diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values new file mode 100644 index 0000000..dbd9a0c --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values @@ -0,0 +1,8 @@ +Alt-id: <87d43gn8ju.fsf_-_@benfinney.id.au> + + +Content-type: text/plain + + +Date: Wed, 18 Nov 2009 13:30:29 +0000 + diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/body b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/body new file mode 100644 index 0000000..4ebb4f2 --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/body @@ -0,0 +1,51 @@ +"W. Trevor King" writes: + +> ** NEWS file + +Speaking as the package maintainer, I would like a ‘ChangeLog’ file +separate from a ‘NEWS’ file. + +The ‘NEWS’ file would continue to be hand-edited, and would be a +high-level view of user-visible changes in the project each version. +Users could reasonably expect to be interested in this file when +installing a new version. It would also make sense to retire old news +From this file once it becomes sufficiently old, to keep it relevant to +users to read. + + +The ‘ChangeLog’ would be an automatically-generated changelog of +low-level changes, not for general human consumption but for letting +recipients have a fighting chance at knowing the historical context of a +particular change without access to the VCS. It would probably be best +done as Trevor says: + +> Depending on our level of masochism, either something starting out +> along the lines of [2] +> bzr log --gnu-changelog -n1 -r 200.. + +That makes it necessary to add the changelog file to the tarball, since +it won't be a file tracked by VCS and therefore won't be exported. Not a +problem:: + + $ release_version="1.0.0" + $ release_name="be-$release_version" + $ tarball_file=../$release_name.tar.gz + $ work_dir=$(mktemp -t -d) + $ export_dir=$work_dir/$release_name + $ changelog_file=$export_dir/ChangeLog + + $ bzr export $export_dir + $ bzr log --gnu-changelog -n1 -r ..tag:"$release_version" > $changelog_file + $ tar -czf $tarball_file $export_dir + $ rm -r $work_dir/ + + $ ls $tarball_file + ../be-1.0.0.tar.gz + $ tar -tzf $tarball_file | grep ChangeLog + be-1.0.0/ChangeLog + +-- + \ “I bought a dog the other day. I named him Stay. It's fun to | + `\ call him. ‘Come here, Stay! Come here, Stay!’ He went insane. | +_o__) Now he just ignores me and keeps typing.” —Steven Wright | +Ben Finney diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values new file mode 100644 index 0000000..770f0b4 --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values @@ -0,0 +1,11 @@ +Alt-id: <873a4cmjw5.fsf@benfinney.id.au> + + +Content-type: text/plain + + +Date: Wed, 18 Nov 2009 22:23:06 +0000 + + +In-reply-to: a4720227-43cf-49aa-8f9f-f49f46e3e809 + diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/body b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/body new file mode 100644 index 0000000..d00eb64 --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/body @@ -0,0 +1,22 @@ +Hi, + + > It would make it much easier on the Debian package maintainer if + > the Bugs Everywhere project would make conventional tarball + > releases, with conventional version numbers, with a changelog + > describing what has changed between versions. + +Fair point. + +How do people feel about pushing for a 1.0 release, with Trevor's tree +plus a finished cfbe merge? Or would we rather wait until afterwards +to try for cfbe? + +- Chris. +-- +Chris Ball +One Laptop Per Child + +_______________________________________________ +Be-devel mailing list +Be-devel@bugseverywhere.org +http://void.printf.net/cgi-bin/mailman/listinfo/be-devel diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/values new file mode 100644 index 0000000..4fb068d --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb/values @@ -0,0 +1,14 @@ +Alt-id: + + +Author: Chris Ball + + +Content-type: text/plain + + +Date: Tue, 17 Nov 2009 22:53:31 +0000 + + +In-reply-to: 1847f1f8-525a-42c4-ae2b-e9377459d2a6 + diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/body b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/body new file mode 100644 index 0000000..a3fc57f --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/body @@ -0,0 +1,36 @@ +I've written up a little release script that bundles all the steps +we've mentioned so far into a single command. Of course, we'll still +have to keep NEWS up to date on our own. + +The output prints a trace of what's going on: + + $ ./release.py 1.0.0 + set libbe.version._VERSION = '1.0.0' + updating AUTHORS + updating ./becommands/assign.py + updating ./becommands/html.py + ... + commit current status: Bumped to version 1.0.0 + tag current revision 1.0.0 + export current revision to be-1.0.0 + generate libbe/_version.py + copy libbe/_version.py to be-1.0.0/libbe/_version.py + generate ChangeLog file be-1.0.0/ChangeLog up to tag 1.0.0 + set vcs_name in be-1.0.0/.be/settings to None + create tarball be-1.0.0.tar.gz + remove be-1.0.0 + +Since we'll be distributing a non-bzr-repo version, it would be nice +to adapt our 'submit bug' procedure (outlined on the main page) to one +that works with this setup. Without guaranteed versioning, that would +probably be something along the lines of + be email-bugs [--to be-devel@bugseverywhere.org] BUG-ID ... +With interfaces/email/interactive listening on the recieving end to +grab new-bug emails and import them into an incoming bug repository. + +-- +This email may be signed or encrypted with GPG (http://www.gnupg.org). +The GPG signature (if present) will be attached as 'signature.asc'. +For more information, see http://en.wikipedia.org/wiki/Pretty_Good_Privacy + +My public key is at http://www.physics.drexel.edu/~wking/pubkey.txt diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values new file mode 100644 index 0000000..83fcd63 --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values @@ -0,0 +1,11 @@ +Alt-id: <20091120132219.GA17577@mjolnir.home.net> + + +Content-type: text/plain + + +Date: Fri, 20 Nov 2009 13:22:19 +0000 + + +In-reply-to: 49e0425b-3332-4d0e-b371-300eccd55370 + diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/body b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/body new file mode 100644 index 0000000..5d29f85 --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/body @@ -0,0 +1,102 @@ +On Tue, Nov 17, 2009 at 05:53:31PM -0500, Chris Ball wrote: +> > It would make it much easier on the Debian package maintainer if +> > the Bugs Everywhere project would make conventional tarball +> > releases, with conventional version numbers, with a changelog +> > describing what has changed between versions. +> How do people feel about pushing for a 1.0 release, with Trevor's tree +> plus a finished cfbe merge? Or would we rather wait until afterwards +> to try for cfbe? + +Sounds good to me. Not that my tree is much ahead of the trunk at the +moment. We've talked over most of these issues a few times, so I'll +just summarize where I think we stand on the steps needed to make a +release. + +** cfbe integration + +Postpone until we work out bzr/hg versioning [1]? + +** Conventional version number + +Set to "1.0.0" using libbe.version._VERSION. + +** NEWS file + +Depending on our level of masochism, either something starting out +along the lines of [2] + bzr log --gnu-changelog -n1 -r 200.. +(commit 200, or + aaron.bentley@utoronto.ca-20060411035623-9b8d222282a26ce1 + was the last time anyone touched the NEWS file), +or a much abbreviated entry [3,4], along the lines of my current NEWS +file (changed just a few minutes ago). + +** Tag bzr commit + + bzr tag 1.0.0 + +** Create tarball + +From Ben[5]: + bzr export /tmp/be-1.0.0.tar.gz + + +References: + +[1] +On Thu, Jul 23, 2009 at 05:38:03PM -0400, Steve Losh wrote: +> On Jul 21, 2009, at 9:59 AM, W. Trevor King wrote: +> > Steve's also versioning it with Mercurial. Will he mind changing to +> > Bazaar? +> +> Yeah, I've tried bazaar but really don't like the interface at all. If +> everyone else really wants me to move it over I guess I can though. + +[2] +On Tue, Jul 14, 2009 at 11:05:38AM -0400, Chris Ball wrote: +> Actually, there's a `bzr log --gnu-changelog` now, and `bzr help +> log-formats` offers some more styles. (None of them seem to match +> my preferred style for release announcements exactly, which would +> be `git shortlog`-style.) + +[3] +On Thu, Jul 16, 2009 at 07:21:10PM +1000, Ben Finney wrote: +> I actually don't think the commit log needs to be part of the release at +> all. It's of interest only to those who want fine-level detail about +> changes to every file, and for that purpose I think read access to the +> VCS is much better. Packaging a static copy of the commit log as plain +> text seems pointless. +> +> Rather, we should treat a user-changes level “NEWS” file (or whatever +> name we choose for it) as part of the documentation, and set the +> expectation among the team that it will be updated for each user-visible +> change being worked on, like any other documentation. + +[4] +On Tue, Jul 14, 2009 at 11:11:31AM -0400, Chris Ball wrote: +> Hi, +> +> > That's not a changelog, that's a commit log of every source-level +> > commit made. Far too much detail for a changelog of +> > *user-visible* changes associated with a release. +> +> I think I agree with both of you. :) It seems like it's both true that +> there's no point in keeping a GNU-style ChangeLog these days, and that +> if we make a release we should write an announce mail that directly +> mentions new user-visible changes as well as attaching the commit log. +> That smaller list of highly user-visible changes could live in NEWS, +> or in the announce mail, or both. + +[5] +On Wed, Jul 15, 2009 at 12:54:05AM +1000, Ben Finney wrote: +> Even better: ‘bzr export /tmp/foo.tar.gz’ will create a source tarball +> of all the files in the branch's VCS inventory. All we need to do is +> start the practice of tagging a release in the VCS, and export the +> tarball at that time. + +-- +This email may be signed or encrypted with GPG (http://www.gnupg.org). +The GPG signature (if present) will be attached as 'signature.asc'. +For more information, see http://en.wikipedia.org/wiki/Pretty_Good_Privacy + +My public key is at http://www.physics.drexel.edu/~wking/pubkey.txt diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values new file mode 100644 index 0000000..99b8978 --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values @@ -0,0 +1,11 @@ +Alt-id: <20091118011403.GB9503@mjolnir.home.net> + + +Content-type: text/plain + + +Date: Wed, 18 Nov 2009 01:14:03 +0000 + + +In-reply-to: 72a519e3-3d6b-4f0f-b412-1310efd255eb + diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/body b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/body new file mode 100644 index 0000000..dee72c7 --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/body @@ -0,0 +1,2 @@ +Verdict: run releases.py periodically, and post the tarballs on the +web. diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/values new file mode 100644 index 0000000..2e85e56 --- /dev/null +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b/values @@ -0,0 +1,8 @@ +Author: W. Trevor King + + +Content-type: text/plain + + +Date: Fri, 20 Nov 2009 21:45:50 +0000 + diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values index b9e8dff..89203d2 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/values @@ -7,7 +7,7 @@ reporter: W. Trevor King severity: wishlist -status: open +status: fixed summary: How should we version BE? -- cgit From ccfd3f9b65eba4535d7975395dc689eea40b0547 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 17:07:57 -0500 Subject: Import os in libbe.subproc if _POSIX == True --- libbe/subproc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libbe/subproc.py b/libbe/subproc.py index e925c62..3e58271 100644 --- a/libbe/subproc.py +++ b/libbe/subproc.py @@ -28,6 +28,7 @@ _MSWINDOWS = sys.platform == 'win32' _POSIX = not _MSWINDOWS if _POSIX == True: + import os import select class CommandError(Exception): -- cgit From 75fedab07f9e566ca1c984051d7deece4d910e2c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 20 Nov 2009 17:09:08 -0500 Subject: Added Bug.from_xml() + some .from_xml() fixups. Moved comment.InvalidXML to utility.InvalidXML, so that bug and comment can share it. Added docstring explaining the __init__ arguments. Added indent and shortname options to Bug.xml() to match Comment.xml(). Added .extra_strings export to Comment.xml(). Converted Bug.xml() from string addition to list joining, which avoids a bunch of memory allocation/deallocation. Assorted " -> ' replacements. Elaborated doctests to check UTF-8, extra_strings, ... Added new comparison cmp_extra_strings for both bug. and comment.DEFAULT_CMP_FULL_CMP_LIST. --- libbe/bug.py | 112 +++++++++++++++++++++++++++++++++++++++++++------------ libbe/comment.py | 97 ++++++++++++++++++++++++----------------------- libbe/utility.py | 15 ++++++++ 3 files changed, 152 insertions(+), 72 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index 48f8358..fecb9b7 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -25,6 +25,10 @@ import os.path import errno 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 xml.sax.saxutils import doctest @@ -271,40 +275,100 @@ class Bug(settings_object.SavedSettingsObject): return str(value) return value - def xml(self, show_comments=False): - if self.bugdir == None: - shortname = self.uuid - else: - shortname = self.bugdir.bug_shortname(self) + def xml(self, indent=0, shortname=None, show_comments=False): + if shortname == None: + if self.bugdir == None: + shortname = self.uuid + else: + shortname = self.bugdir.bug_shortname(self) if self.time == None: timestring = "" else: timestring = utility.time_to_str(self.time) - info = [("uuid", self.uuid), - ("short-name", shortname), - ("severity", self.severity), - ("status", self.status), - ("assigned", self.assigned), - ("target", self.target), - ("reporter", self.reporter), - ("creator", self.creator), - ("created", timestring), - ("summary", self.summary)] - ret = '\n' + info = [('uuid', self.uuid), + ('short-name', shortname), + ('severity', self.severity), + ('status', self.status), + ('assigned', self.assigned), + ('target', self.target), + ('reporter', self.reporter), + ('creator', self.creator), + ('created', timestring), + ('summary', self.summary)] + lines = [''] for (k,v) in info: if v is not None: - ret += ' <%s>%s\n' % (k,xml.sax.saxutils.escape(v),k) + lines.append(' <%s>%s' % (k,xml.sax.saxutils.escape(v),k)) for estr in self.extra_strings: - ret += ' %s\n' % estr + lines.append(' %s\n' % estr) if show_comments == True: - comout = self.comment_root.xml_thread(auto_name_map=True, + comout = self.comment_root.xml_thread(indent=indent+2, + auto_name_map=True, bug_shortname=shortname) if len(comout) > 0: - ret += comout+'\n' - ret += '' - return ret + lines.append(comout) + lines.append('') + istring = ' '*indent + sep = '\n' + istring + return istring + sep.join(lines).rstrip('\n') + + def from_xml(self, xml_string, verbose=True): + """ + 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()") + >>> bugA.date = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> bugA.creator = u'Fran\xe7ois' + >>> bugA.extra_strings += ['TAG: very helpful'] + >>> commA = bugA.comment_root.new_reply(body='comment A') + >>> commB = bugA.comment_root.new_reply(body='comment B') + >>> commC = commA.new_reply(body='comment C') + >>> xml = bugA.xml(shortname="bug-1") + >>> bugB = Bug() + >>> bugB.from_xml(xml, verbose=True) + >>> bugB.xml(shortname="bug-1") == xml + False + >>> bugB.uuid = bugB.alt_id + >>> bugB.xml(shortname="bug-1") == xml + True + """ + if type(xml_string) == types.UnicodeType: + xml_string = xml_string.strip().encode('unicode_escape') + bug = ElementTree.XML(xml_string) + if bug.tag != 'bug': + raise utility.InvalidXML( \ + 'bug', bug, 'root element must be ') + tags=['uuid','short-name','severity','status','assigned','target', + 'reporter', 'creator', 'created', 'summary', 'extra-string', + 'comment'] + uuid = None + estrs = [] + for child in bug.getchildren(): + if child.tag == 'short-name': + pass + elif child.tag in tags: + if child.text == None or len(child.text) == 0: + text = settings_object.EMPTY + else: + text = xml.sax.saxutils.unescape(child.text) + text = text.decode('unicode_escape').strip() + if child.tag == "uuid": + uuid = text + continue # don't set the bug's uuid tag. + if child.tag == 'extra-string': + estrs.append(text) + continue # don't set the bug's extra_string yet. + else: + attr_name = child.tag.replace('-','_') + setattr(self, attr_name, text) + elif verbose == True: + print >> sys.stderr, "Ignoring unknown tag %s in %s" \ + % (child.tag, comment.tag) + if uuid not in [None, self.uuid]: + if not hasattr(self, 'alt_id') or self.alt_id == None: + self.alt_id = uuid + self.extra_strings = estrs def string(self, shortlist=False, show_comments=False): if self.bugdir == None: @@ -525,6 +589,7 @@ cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned") cmp_target = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "target") cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter") cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary") +cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings") # chronological rankings (newer < older) cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True) @@ -547,7 +612,8 @@ def cmp_comments(bug_1, bug_2): DEFAULT_CMP_FULL_CMP_LIST = \ (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator, - cmp_reporter, cmp_target, cmp_comments, cmp_summary, cmp_uuid) + cmp_reporter, cmp_target, cmp_comments, cmp_summary, cmp_uuid, + cmp_extra_strings) class BugCompoundComparator (object): def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): diff --git a/libbe/comment.py b/libbe/comment.py index 5f67878..678d8ba 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -51,14 +51,6 @@ class InvalidShortname(KeyError): self.shortname = shortname self.shortnames = shortnames -class InvalidXML(ValueError): - def __init__(self, element, comment): - msg = "Invalid comment xml: %s\n %s\n" \ - % (comment, ElementTree.tostring(element)) - ValueError.__init__(self, msg) - self.element = element - self.comment = comment - class MissingReference(ValueError): def __init__(self, comment): msg = "Missing reference to %s" % (comment.in_reply_to) @@ -331,27 +323,29 @@ class Comment(Tree, settings_object.SavedSettingsObject): """ if shortname == None: shortname = self.uuid - if self.content_type.startswith("text/"): - body = (self.body or "").rstrip('\n') + if self.content_type.startswith('text/'): + body = (self.body or '').rstrip('\n') else: maintype,subtype = self.content_type.split('/',1) msg = email.mime.base.MIMEBase(maintype, subtype) - msg.set_payload(self.body or "") + msg.set_payload(self.body or '') email.encoders.encode_base64(msg) - body = base64.encodestring(self.body or "") - info = [("uuid", self.uuid), - ("alt-id", self.alt_id), - ("short-name", shortname), - ("in-reply-to", self.in_reply_to), - ("author", self._setting_attr_string("author")), - ("date", self.date), - ("content-type", self.content_type), - ("body", body)] - lines = [""] + body = base64.encodestring(self.body or '') + info = [('uuid', self.uuid), + ('alt-id', self.alt_id), + ('short-name', shortname), + ('in-reply-to', self.in_reply_to), + ('author', self._setting_attr_string('author')), + ('date', self.date), + ('content-type', self.content_type), + ('body', body)] + lines = [''] for (k,v) in info: if v != None: lines.append(' <%s>%s' % (k,xml.sax.saxutils.escape(v),k)) - lines.append("") + for estr in self.extra_strings: + lines.append(' %s\n' % estr) + lines.append('') istring = ' '*indent sep = '\n' + istring return istring + sep.join(lines).rstrip('\n') @@ -363,58 +357,61 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> commA = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> commA.uuid = "0123" >>> commA.date = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> commA.author = u'Fran\xe7ois' + >>> commA.extra_strings += ['TAG: very helpful'] >>> xml = commA.xml(shortname="com-1") >>> commB = Comment() - >>> commB.from_xml(xml) - >>> attrs=['uuid','alt_id','in_reply_to','author','date','content_type','body'] - >>> for attr in attrs: # doctest: +ELLIPSIS - ... if getattr(commB, attr) != getattr(commA, attr): - ... estr = "Mismatch on %s: '%s' should be '%s'" - ... args = (attr, getattr(commB, attr), getattr(commA, attr)) - ... print estr % args - Mismatch on uuid: '...' should be '0123' - Mismatch on alt_id: '0123' should be 'None' - >>> print commB.alt_id - 0123 - >>> commA.author - >>> commB.author + >>> commB.from_xml(xml, verbose=True) + >>> commB.xml(shortname="com-1") == xml + False + >>> commB.uuid = commB.alt_id + >>> commB.alt_id = None + >>> commB.xml(shortname="com-1") == xml + True """ if type(xml_string) == types.UnicodeType: - xml_string = xml_string.strip().encode("unicode_escape") + xml_string = xml_string.strip().encode('unicode_escape') comment = ElementTree.XML(xml_string) - if comment.tag != "comment": - raise InvalidXML(comment, "root element must be ") - tags=['uuid','alt-id','in-reply-to','author','date','content-type','body'] + if comment.tag != 'comment': + raise utility.InvalidXML( \ + 'comment', comment, 'root element must be ') + tags=['uuid','alt-id','in-reply-to','author','date','content-type', + 'body','extra-string'] uuid = None body = None + estrs = [] for child in comment.getchildren(): - if child.tag == "short-name": + if child.tag == 'short-name': pass elif child.tag in tags: if child.text == None or len(child.text) == 0: text = settings_object.EMPTY else: text = xml.sax.saxutils.unescape(child.text) - text = unicode(text).decode("unicode_escape").strip() - if child.tag == "uuid": + text = text.decode('unicode_escape').strip() + if child.tag == 'uuid': uuid = text - continue # don't set the bug's uuid tag. - if child.tag == "body": + continue # don't set the comment's uuid tag. + if child.tag == 'body': body = text - continue # don't set the bug's body yet. + continue # don't set the comment's body yet. + if child.tag == 'extra-string': + estrs.append(text) + continue # don't set the comment's extra_string yet. else: attr_name = child.tag.replace('-','_') setattr(self, attr_name, text) elif verbose == True: - print >> sys.stderr, "Ignoring unknown tag %s in %s" \ + print >> sys.stderr, 'Ignoring unknown tag %s in %s' \ % (child.tag, comment.tag) if self.alt_id == None and uuid not in [None, self.uuid]: self.alt_id = uuid if body != None: - if self.content_type.startswith("text/"): - self.body = body+"\n" # restore trailing newline + if self.content_type.startswith('text/'): + self.body = body+'\n' # restore trailing newline else: self.body = base64.decodestring(body) + self.extra_strings = estrs def string(self, indent=0, shortname=None): """ @@ -726,12 +723,14 @@ cmp_author = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "autho cmp_in_reply_to = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "in_reply_to") cmp_content_type = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "content_type") cmp_body = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "body") +cmp_extra_strings = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "extra_strings") # chronological rankings (newer < older) cmp_time = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "time", invert=True) + DEFAULT_CMP_FULL_CMP_LIST = \ (cmp_time, cmp_author, cmp_content_type, cmp_body, cmp_in_reply_to, - cmp_uuid) + cmp_uuid, cmp_extra_strings) class CommentCompoundComparator (object): def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): diff --git a/libbe/utility.py b/libbe/utility.py index 4126913..7510b16 100644 --- a/libbe/utility.py +++ b/libbe/utility.py @@ -29,6 +29,21 @@ import time import types import doctest +class InvalidXML(ValueError): + """ + Invalid XML while parsing for a *.from_xml() method. + type - string identifying *, e.g. "bug", "comment", ... + element - ElementTree.Element instance which caused the error + error - string describing the error + """ + def __init__(self, type, element, error): + msg = 'Invalid %s xml: %s\n %s\n' \ + % (type, error, ElementTree.tostring(element)) + ValueError.__init__(self, msg) + self.type = type + self.element = element + self.error = error + def search_parent_directories(path, filename): """ Find the file (or directory) named filename in path or in any -- cgit From 3b168403ff5e50d767476c4c0f037d1841bb2bf9 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 10:53:04 -0500 Subject: Broke `be comment --xml` out and extended into `be import-xml`. It should currently do everything that `be comment --xml` did, but it still has a way to go before it lives up to it's longhelp string, mostly figuring out bug/comment merging. The allowed XML format also changed a bit, becoming a bit more structured. cmdutil.bug_from_shortname() renamed to cmdutil.bug_from_id(). New functions cmdutil.parse_id() and cmdutil.bug_comment_from_id(). Additional doctests in libbe.comment.Comment.comment_shortnames() to show example output if bug_shortname==None. Brought be-xml-to-mbox and be-mbox-to-xml up to speed on the current -rooted format. * Added handling to their comment handling. * Moved extra strings from email bodies to X-Extra-String headers (some comment bodies are not text, and we should keep the estr location consistent between bugs and comments.) --- becommands/assign.py | 2 +- becommands/close.py | 2 +- becommands/comment.py | 80 ++----------- becommands/depend.py | 4 +- becommands/email_bugs.py | 2 +- becommands/import_xml.py | 257 ++++++++++++++++++++++++++++++++++++++++++ becommands/merge.py | 4 +- becommands/open.py | 2 +- becommands/remove.py | 2 +- becommands/severity.py | 2 +- becommands/show.py | 21 ++-- becommands/status.py | 2 +- becommands/tag.py | 2 +- becommands/target.py | 2 +- interfaces/xml/be-mbox-to-xml | 8 +- interfaces/xml/be-xml-to-mbox | 42 +++---- libbe/cmdutil.py | 59 +++++++++- libbe/comment.py | 6 + 18 files changed, 373 insertions(+), 126 deletions(-) create mode 100644 becommands/import_xml.py diff --git a/becommands/assign.py b/becommands/assign.py index fbef281..31aa607 100644 --- a/becommands/assign.py +++ b/becommands/assign.py @@ -57,7 +57,7 @@ def execute(args, manipulate_encodings=True): raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) - bug = cmdutil.bug_from_shortname(bd, args[0]) + bug = cmdutil.bug_from_id(bd, args[0]) bug = bd.bug_from_shortname(args[0]) if len(args) == 1: bug.assigned = bd.user_id diff --git a/becommands/close.py b/becommands/close.py index 2cdcb59..d65c4e0 100644 --- a/becommands/close.py +++ b/becommands/close.py @@ -45,7 +45,7 @@ def execute(args, manipulate_encodings=True): raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) - bug = cmdutil.bug_from_shortname(bd, args[0]) + bug = cmdutil.bug_from_id(bd, args[0]) bug.status = "closed" bd.save() diff --git a/becommands/comment.py b/becommands/comment.py index dad32dd..a600e9f 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -19,10 +19,6 @@ from libbe import cmdutil, bugdir, comment, editor import os import sys -try: # import core module, Python >= 2.5 - from xml.etree import ElementTree -except ImportError: # look for non-core module - from elementtree import ElementTree __desc__ = __doc__ def execute(args, manipulate_encodings=True): @@ -32,7 +28,7 @@ def execute(args, manipulate_encodings=True): >>> os.chdir(bd.root) >>> execute(["a", "This is a comment about a"], manipulate_encodings=False) >>> bd._clear_bugs() - >>> bug = cmdutil.bug_from_shortname(bd, "a") + >>> bug = cmdutil.bug_from_id(bd, "a") >>> bug.load_comments(load_full=False) >>> comment = bug.comment_root[0] >>> print comment.body @@ -54,7 +50,7 @@ def execute(args, manipulate_encodings=True): >>> os.environ["EDITOR"] = "echo 'I like cheese' > " >>> execute(["b"], manipulate_encodings=False) >>> bd._clear_bugs() - >>> bug = cmdutil.bug_from_shortname(bd, "b") + >>> bug = cmdutil.bug_from_id(bd, "b") >>> bug.load_comments(load_full=False) >>> comment = bug.comment_root[0] >>> print comment.body @@ -71,25 +67,10 @@ def execute(args, manipulate_encodings=True): raise cmdutil.UsageError("Too many arguments.") shortname = args[0] - if shortname.count(':') > 1: - raise cmdutil.UserError("Invalid id '%s'." % shortname) - elif shortname.count(':') == 1: - # Split shortname generated by Comment.comment_shortnames() - bugname = shortname.split(':')[0] - is_reply = True - else: - bugname = shortname - is_reply = False bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) - bug = cmdutil.bug_from_shortname(bd, bugname) - bug.load_comments(load_full=False) - if is_reply: - parent = bug.comment_root.comment_from_shortname(shortname, - bug_shortname=bugname) - else: - parent = bug.comment_root + bug, parent = cmdutil.bug_comment_from_id(bd, shortname) if len(args) == 1: # try to launch an editor for comment-body entry try: @@ -118,51 +99,11 @@ def execute(args, manipulate_encodings=True): if not body.endswith('\n'): body+='\n' - if options.XML == False: - new = parent.new_reply(body=body, content_type=options.content_type) - if options.author != None: - new.author = options.author - if options.alt_id != None: - new.alt_id = options.alt_id - else: # import XML comment [list] - # read in the comments - str_body = body.encode("unicode_escape").replace(r'\n', '\n') - comment_list = ElementTree.XML(str_body) - if comment_list.tag not in ["bug", "comment-list"]: - raise comment.InvalidXML( - comment_list, "root element must be or ") - new_comments = [] - ids = [] - for c in bug.comment_root.traverse(): - ids.append(c.uuid) - if c.alt_id != None: - ids.append(c.alt_id) - for child in comment_list.getchildren(): - if child.tag == "comment": - new = comment.Comment(bug) - new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape")) - if new.alt_id in ids: - raise cmdutil.UserError( - "Clashing comment alt_id: %s" % new.alt_id) - ids.append(new.uuid) - if new.alt_id != None: - ids.append(new.alt_id) - if new.in_reply_to == None: - new.in_reply_to = parent.uuid - new_comments.append(new) - else: - print >> sys.stderr, "Ignoring unknown tag %s in %s" \ - % (child.tag, comment_list.tag) - try: - comment.list_to_root(new_comments,bug,root=parent, # link new comments - ignore_missing_references=options.ignore_missing_references) - except comment.MissingReference, e: - raise cmdutil.UserError(e) - # Protect against programmer error causing data loss: - kids = [c.uuid for c in parent.traverse()] - for nc in new_comments: - assert nc.uuid in kids, "%s wasn't added to %s" % (nc.uuid, parent.uuid) - nc.save() + new = parent.new_reply(body=body, content_type=options.content_type) + if options.author != None: + new.author = options.author + if options.alt_id != None: + new.alt_id = options.alt_id def get_parser(): parser = cmdutil.CmdOptionParser("be comment ID [COMMENT]") @@ -172,11 +113,6 @@ def get_parser(): help="Set an alternate comment ID", default=None) parser.add_option("-c", "--content-type", metavar="MIME", dest="content_type", help="Set comment content-type (e.g. text/plain)", default=None) - parser.add_option("-x", "--xml", action="store_true", default=False, - dest='XML', help="Use COMMENT to specify an XML comment description rather than the comment body. The root XML element should be either or with one or more children. The syntax for the elements should match that generated by 'be show --xml COMMENT-ID'. Unrecognized tags are ignored. Missing tags are left at the default value. The comment UUIDs are always auto-generated, so if you set a field, but no field, your will be used as the comment's . An exception is raised if conflicts with an existing comment.") - parser.add_option("-i", "--ignore-missing-references", action="store_true", - dest="ignore_missing_references", - help="For XML import, if any comment's refers to a non-existent comment, ignore it (instead of raising an exception).") return parser longhelp=""" diff --git a/becommands/depend.py b/becommands/depend.py index f52527e..044aaff 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -95,7 +95,7 @@ def execute(args, manipulate_encodings=True): for blockee,blocker in fixed]) return 0 - bugA = cmdutil.bug_from_shortname(bd, args[0]) + bugA = cmdutil.bug_from_id(bd, args[0]) if options.tree_depth != None: dtree = DependencyTree(bd, bugA, options.tree_depth) @@ -112,7 +112,7 @@ def execute(args, manipulate_encodings=True): return 0 if len(args) == 2: - bugB = cmdutil.bug_from_shortname(bd, args[1]) + bugB = cmdutil.bug_from_id(bd, args[1]) if options.remove == True: remove_block(bugA, bugB) else: # add the dependency diff --git a/becommands/email_bugs.py b/becommands/email_bugs.py index 9aeccec..c188332 100644 --- a/becommands/email_bugs.py +++ b/becommands/email_bugs.py @@ -89,7 +89,7 @@ def execute(args, manipulate_encodings=True): lines = [u'' % bd.encoding, u''] for shortname in args: - bug = cmdutil.bug_from_shortname(bd, shortname) + bug = cmdutil.bug_from_id(bd, shortname) lines.append(bug.xml(show_comments=True)) lines.append(u'') subject = options.subject diff --git a/becommands/import_xml.py b/becommands/import_xml.py new file mode 100644 index 0000000..d28e97a --- /dev/null +++ b/becommands/import_xml.py @@ -0,0 +1,257 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Import comments and bugs from XML""" +from libbe import cmdutil, bugdir, bug, comment, utility +from becommands.comment import complete +import os +import sys +try: # import core module, Python >= 2.5 + from xml.etree import ElementTree +except ImportError: # look for non-core module + from elementtree import ElementTree +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True): + """ + >>> import time + >>> import StringIO + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> orig_stdin = sys.stdin + >>> sys.stdin = StringIO.StringIO("cThis is a comment about a") + >>> execute(["-c", "a", "-"], manipulate_encodings=False) + >>> sys.stdin = orig_stdin + >>> bd._clear_bugs() + >>> bug = cmdutil.bug_from_id(bd, "a") + >>> bug.load_comments(load_full=False) + >>> comment = bug.comment_root[0] + >>> print comment.body + This is a comment about a + + >>> comment.author == bd.user_id + True + >>> comment.time <= int(time.time()) + True + >>> comment.in_reply_to is None + True + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) < 1: + raise cmdutil.UsageError("Please specify an XML file.") + if len(args) > 1: + raise cmdutil.UsageError("Too many arguments.") + filename = args[0] + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + if options.comment_root != None: + croot_bug,croot_comment = \ + cmdutil.bug_comment_from_id(bd, options.comment_root) + else: + croot_bug,croot_comment = (None, None) + + if filename == '-': + xml = sys.stdin.read() + else: + xml = bd.vcs.get_file_contents(filename, allow_no_vcs=True) + str_xml = xml.encode('unicode_escape').replace(r'\n', '\n') + # unicode read + encode to string so we know the encoding, + # which might not be given (?) in a binary string read? + + # parse the xml + root_bugs = [] + root_comments = [] + version = {} + be_xml = ElementTree.XML(str_xml) + if be_xml.tag != 'be-xml': + raise utility.InvalidXML( + 'import-xml', be_xml, 'root element must be ') + for child in be_xml.getchildren(): + if child.tag == 'bug': + new = bug.Bug(bugdir=bd) + new.from_xml(unicode(ElementTree.tostring(child)).decode('unicode_escape')) + root_bugs.append(new) + elif child.tag == 'comment': + new = comment.Comment(croot_bug) + new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape")) + root_comments.append(new) + elif child.tag == 'version': + for gchild in child.getchildren(): + if child.tag in ['tag', 'nick', 'revision', 'revision-id']: + text = xml.sax.saxutils.unescape(child.text) + text = text.decode('unicode_escape').strip() + version[child.tag] = text + else: + print >> sys.stderr, 'ignoring unknown tag %s in %s' \ + % (gchild.tag, child.tag) + else: + print >> sys.stderr, 'ignoring unknown tag %s in %s' \ + % (child.tag, comment_list.tag) + + # merge the new root_comments + croot_cids = [] + for c in croot_bug.comment_root.traverse(): + croot_cids.append(c.uuid) + if c.alt_id != None: + croot_cids.append(c.alt_id) + for new in root_comments: + if new.alt_id in croot_cids: + raise cmdutil.UserError( + 'clashing comment alt_id: %s' % new.alt_id) + croot_cids.append(new.uuid) + if new.alt_id != None: + croot_cids.append(new.alt_id) + if new.in_reply_to == None: + new.in_reply_to = croot_comment.uuid + try: + # link new comments + comment.list_to_root(root_comments,croot_bug,root=croot_comment, + ignore_missing_references= \ + options.ignore_missing_references) + except comment.MissingReference, e: + raise cmdutil.UserError(e) + + # merge the new croot_bugs + for new in root_bugs: + bd.append(new) + + # protect against programmer error causing data loss: + comms = [c.uuid for c in croot_comment.traverse()] + for new in root_comments: + assert new.uuid in comms, \ + "comment %s wasn't added to %s" % (new.uuid, croot_comment.uuid) + for new in root_bugs: + assert bd.has_bug(new.uuid), \ + "bug %s wasn't added" % (new.uuid) + + # save new information + for new in root_comments: + new.save() + for new in root_bugs: + new.save() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be import-xml XMLFILE") + parser.add_option("-i", "--ignore-missing-references", action="store_true", + dest="ignore_missing_references", default=False, + help="If any comment's refers to a non-existent comment, ignore it (instead of raising an exception).") + parser.add_option("-a", "--add-only", action='store_true', + dest="add_only", default=False, + help="If any bug or comment listed in the XML file already exists in the bug repository, do not alter the repository version.") + parser.add_option("-c", "--comment-root", dest="comment_root", + help="Supply a bug or comment ID as the root of any elements that are direct children of the element. If any such elements exist, you are required to set this option.") + return parser + +longhelp=""" +Import comments and bugs from XMLFILE. If XMLFILE is '-', the file is +read from stdin. + +This command provides a fallback mechanism for passing bugs between +repositories, in case the repositories VCSs are incompatible. If the +VCSs are compatible, it's better to use their builtin merge/push/pull +to share this information, as that will preserve a more detailed +history. + +The XML file should be formatted similarly to + + + 1.0.0 + be + 446 + a@b.com-20091119214553-iqyw2cpqluww3zna + + + ... + ... + ... + + ... + ... + ... + +where the ellipses mark output commpatible with Bug.xml() and +Comment.xml(). Take a look at the output of `be show --xml --version` +for some explicit examples. Unrecognized tags are ignored. Missing +tags are left at the default value. The version tag is not required, +but is strongly recommended. + +The bug and comment UUIDs are always auto-generated, so if you set a + field, but no field, your will be used as the +comment's . An exception is raised if conflicts with +an existing comment. Bugs do not have a permantent alt-id, so they +the s you specify are not saved. The s _are_ used to +match agains prexisting bug and comment uuids, and comment alt-ids, +and fields explicitly given in the XML file will replace old versions +unless the --add-only flag. + +*.extra_strings recieves special treatment, and if --add-only is not +set, the resulting list concatenates both source lists and removes +repeats. + +Here's an example of import activity: + Repository + bug (uuid=B, author=John, status=open) + estr (don't forget your towel) + estr (helps with space travel) + com (uuid=C1, author=Jane, body=Hello) + com (uuid=C2, author=Jess, body=World) + XML + bug (uuid=B, status=fixed) + estr (don't forget your towel) + estr (watch out for flying dolphins) + com (uuid=C1, body=So long) + com (uuid=C3, author=Jed, body=And thanks) + Result + bug (uuid=B, author=John, status=fixed) + estr (don't forget your towel) + estr (helps with space travel) + estr (watch out for flying dolphins) + com (uuid=C1, author=Jane, body=So long) + com (uuid=C2, author=Jess, body=World) + com (uuid=C4, alt-id=C3, author=Jed, body=And thanks) + Result, with --add-only + bug (uuid=B, author=John, status=open) + estr (don't forget your towel) + estr (helps with space travel) + com (uuid=C1, author=Jane, body=Hello) + com (uuid=C2, author=Jess, body=World) + com (uuid=C4, alt-id=C3, author=Jed, body=And thanks) + +Examples: + +Import comments (e.g. emails from an mbox) and append to bug XYZ + $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ - +Or you can append those emails underneath the prexisting comment XYZ-3 + $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ-3 - + +User creates a new bug + user$ be new "The demuxulizer is broken" + Created bug with ID 48f + user$ be comment 48f + + ... +User exports bug as xml and emails it to the developers + user$ be show --xml --version 48f > 48f.xml + user$ cat 48f.xml | mail -s "Demuxulizer bug xml" devs@b.com +Devs recieve email, and save it's contents as demux-bug.xml + dev$ cat demux-bug.xml | be import-xml - +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/becommands/merge.py b/becommands/merge.py index bc18479..766af56 100644 --- a/becommands/merge.py +++ b/becommands/merge.py @@ -137,9 +137,9 @@ def execute(args, manipulate_encodings=True): bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) - bugA = cmdutil.bug_from_shortname(bd, args[0]) + bugA = cmdutil.bug_from_id(bd, args[0]) bugA.load_comments() - bugB = cmdutil.bug_from_shortname(bd, args[1]) + bugB = cmdutil.bug_from_id(bd, args[1]) bugB.load_comments() mergeA = bugA.new_comment("Merged from bug %s" % bugB.uuid) newCommTree = copy.deepcopy(bugB.comment_root) diff --git a/becommands/open.py b/becommands/open.py index c9e55a2..1b4c23e 100644 --- a/becommands/open.py +++ b/becommands/open.py @@ -44,7 +44,7 @@ def execute(args, manipulate_encodings=True): raise cmdutil.UsageError, "Too many arguments." bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) - bug = cmdutil.bug_from_shortname(bd, args[0]) + bug = cmdutil.bug_from_id(bd, args[0]) bug.status = "open" def get_parser(): diff --git a/becommands/remove.py b/becommands/remove.py index 0e61362..d265e5c 100644 --- a/becommands/remove.py +++ b/becommands/remove.py @@ -44,7 +44,7 @@ def execute(args, manipulate_encodings=True): raise cmdutil.UsageError, "Please specify a bug id." bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) - bug = cmdutil.bug_from_shortname(bd, args[0]) + bug = cmdutil.bug_from_id(bd, args[0]) bd.remove_bug(bug) print "Removed bug %s" % bug.uuid diff --git a/becommands/severity.py b/becommands/severity.py index e987760..f42f740 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -43,7 +43,7 @@ def execute(args, manipulate_encodings=True): raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) - bug = cmdutil.bug_from_shortname(bd, args[0]) + bug = cmdutil.bug_from_id(bd, args[0]) if len(args) == 1: print bug.severity elif len(args) == 2: diff --git a/becommands/show.py b/becommands/show.py index 94e16bf..4f8c30d 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -63,24 +63,17 @@ def execute(args, manipulate_encodings=True): if options.XML: print '' % bd.encoding for shortname in args: - if shortname.count(':') > 1: - raise cmdutil.UserError("Invalid id '%s'." % shortname) - elif shortname.count(':') == 1: - # Split shortname generated by Comment.comment_shortnames() - bugname = shortname.split(':')[0] - is_comment = True - else: - bugname = shortname - is_comment = False - if is_comment == True and options.comments == False: - continue - bug = cmdutil.bug_from_shortname(bd, bugname) - if is_comment == False: + bugname,commname = cmdutil.parse_id(shortname) + if commname == None: # bug shortname + bug = cmdutil.bug_from_id(bd, shortname) if options.XML: print bug.xml(show_comments=options.comments) else: print bug.string(show_comments=options.comments) - else: + elif options.comments == False: + continue + else: # comment shortname + bug,comment = cmdutil.bug_comment_from_id(shortname) comment = bug.comment_root.comment_from_shortname( shortname, bug_shortname=bugname) if options.XML: diff --git a/becommands/status.py b/becommands/status.py index 827c7ce..bf66c26 100644 --- a/becommands/status.py +++ b/becommands/status.py @@ -40,7 +40,7 @@ def execute(args, manipulate_encodings=True): raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) - bug = cmdutil.bug_from_shortname(bd, args[0]) + bug = cmdutil.bug_from_id(bd, args[0]) if len(args) == 1: print bug.status else: diff --git a/becommands/tag.py b/becommands/tag.py index 31b43ba..e6debb4 100644 --- a/becommands/tag.py +++ b/becommands/tag.py @@ -95,7 +95,7 @@ def execute(args, manipulate_encodings=True): if len(tags) > 0: print '\n'.join(tags) return - bug = cmdutil.bug_from_shortname(bd, args[0]) + bug = cmdutil.bug_from_id(bd, args[0]) if len(args) == 2: given_tag = args[1] estrs = bug.extra_strings diff --git a/becommands/target.py b/becommands/target.py index 7e41451..672eb06 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -55,7 +55,7 @@ def execute(args, manipulate_encodings=True): if target and isinstance(target,str): print target return - bug = cmdutil.bug_from_shortname(bd, args[0]) + bug = cmdutil.bug_from_id(bd, args[0]) if len(args) == 1: if bug.target is None: print "No target assigned." diff --git a/interfaces/xml/be-mbox-to-xml b/interfaces/xml/be-mbox-to-xml index a740117..3af2978 100755 --- a/interfaces/xml/be-mbox-to-xml +++ b/interfaces/xml/be-mbox-to-xml @@ -15,8 +15,8 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Convert an mbox into xml suitable for imput into be. - $ cat mbox | be-mbox-to-xml | be comment --xml - +Convert an mbox into xml suitable for input into be. + $ be-mbox-to-xml file.mbox | be import-xml -c - mbox is a flat-file format, consisting of a series of messages. Messages begin with a a From_ line, followed by RFC 822 email, followed by a blank line. @@ -140,10 +140,10 @@ def comment_message_to_xml(message, fields=None): def main(mbox_filename): mb = mbox(mbox_filename) print u'' % DEFAULT_ENCODING - print u"" + print u"" for message in mb: print comment_message_to_xml(message) - print u"" + print u"" if __name__ == "__main__": diff --git a/interfaces/xml/be-xml-to-mbox b/interfaces/xml/be-xml-to-mbox index 7960d56..ecc6327 100755 --- a/interfaces/xml/be-xml-to-mbox +++ b/interfaces/xml/be-xml-to-mbox @@ -88,19 +88,18 @@ class Bug (LimitedAttrDict): def print_to_mbox(self): name,addr = email.utils.parseaddr(self["creator"]) print "From %s %s" % (addr, rfc2822_to_asctime(self["created"])) - print "Message-ID: <%s@%s>" % (self["uuid"], DEFAULT_DOMAIN) + print "Message-id: <%s@%s>" % (self["uuid"], DEFAULT_DOMAIN) print "Date: %s" % self["created"] print "From: %s" % self["creator"] print "Content-Type: %s; charset=%s" % ("text/plain", DEFAULT_ENCODING) print "Content-Transfer-Encoding: 8bit" print "Subject: %s: %s" % (self["short-name"], self["summary"]) + if "extra-strings" in self: + for estr in self["extra_strings"]: + print "X-Extra-String: %s" % estr print "" print self["summary"] print "" - if "extra-strings" in self: - print "extra strings:\n ", - print '\n '.join(self["extra_strings"]) - print "" if "comments" in self: for comment in self["comments"]: comment.print_to_mbox(self) @@ -131,7 +130,8 @@ class Comment (LimitedAttrDict): u"author", u"date", u"content-type", - u"body"] + u"body", + u"extra-strings"] def print_to_mbox(self, bug=None): if bug == None: bug = Bug() @@ -142,7 +142,7 @@ class Comment (LimitedAttrDict): elif "alt-id" in self: id = self["alt-id"] else: id = None if id != None: - print "Message-ID: <%s@%s>" % (id, DEFAULT_DOMAIN) + print "Message-id: <%s@%s>" % (id, DEFAULT_DOMAIN) print "Date: %s" % self["date"] print "From: %s" % self["author"] subject = "" @@ -156,6 +156,9 @@ class Comment (LimitedAttrDict): if "in-reply-to" not in self.keys(): self["in-reply-to"] = bug["uuid"] print "In-Reply-To: <%s@%s>" % (self["in-reply-to"], DEFAULT_DOMAIN) + if "extra-strings" in self: + for estr in self["extra_strings"]: + print "X-Extra-String: %s" % estr if self["content-type"].startswith("text/"): print "Content-Transfer-Encoding: 8bit" print "Content-Type: %s; charset=%s" % (self["content-type"], DEFAULT_ENCODING) @@ -168,9 +171,15 @@ class Comment (LimitedAttrDict): assert element.tag == "comment", element.tag for field in element.getchildren(): text = unescape(unicode(field.text).decode("unicode_escape").strip()) - if field.tag == "body": - text+="\n" - self[field.tag] = text + if field.tag == "extra-string": + if "extra-strings" in self: + self["extra-strings"].append(text) + else: + self["extra-strings"] = [text] + else: + if field.tag == "body": + text+="\n" + self[field.tag] = text def print_to_mbox(element): if element.tag == "bug": @@ -181,16 +190,9 @@ def print_to_mbox(element): c = Comment() c.init_from_etree(element) c.print_to_mbox() - elif element.tag in ["bugs", "bug-list"]: - for b_elt in element.getchildren(): - b = Bug() - b.init_from_etree(b_elt) - b.print_to_mbox() - elif element.tag in ["comments", "comment-list"]: - for c_elt in element.getchildren(): - c = Comment() - c.init_from_etree(c_elt) - c.print_to_mbox() + elif element.tag in ["be-xml"]: + for elt in element.getchildren(): + print_to_mbox(elt) if __name__ == "__main__": import sys diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index f1c8acd..96430eb 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -30,10 +30,10 @@ import sys import doctest import bugdir +import comment import plugin import encoding - class UserError(Exception): def __init__(self, msg): Exception.__init__(self, msg) @@ -213,16 +213,69 @@ def underlined(instring): return "%s\n%s" % (instring, "="*len(instring)) -def bug_from_shortname(bdir, shortname): +def parse_id(id): + """ + Return (bug_id, comment_id) tuple. + Basically inverts Comment.comment_shortnames() + >>> parse_id('XYZ') + ('XYZ', None) + >>> parse_id('XYZ:123') + ('XYZ', ':123') + >>> parse_id('') + Traceback (most recent call last): + ... + UserError: invalid id ''. + >>> parse_id('::') + Traceback (most recent call last): + ... + UserError: invalid id '::'. + """ + if len(id) == 0: + raise UserError("invalid id '%s'." % id) + if id.count(':') > 1: + raise UserError("invalid id '%s'." % id) + elif id.count(':') == 1: + # Split shortname generated by Comment.comment_shortnames() + bug_id,comment_id = id.split(':') + comment_id = ':'+comment_id + else: + bug_id = id + comment_id = None + return (bug_id, comment_id) + +def bug_from_id(bdir, id): """ Exception translation for the command-line interface. + id can be either the bug shortname or the full uuid. """ try: - bug = bdir.bug_from_shortname(shortname) + bug = bdir.bug_from_shortname(id) except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: raise UserError(e.message) return bug +def bug_comment_from_id(bdir, id): + """ + Return (bug,comment) tuple matching shortname. id can be either + the bug/comment shortname or the full uuid. If there is no + comment part to the id, the returned comment is the bug's + .comment_root. + """ + bug_id,comment_id = parse_id(id) + try: + bug = bdir.bug_from_shortname(bug_id) + except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: + raise UserError(e.message) + if comment_id == None: + comm = bug.comment_root + else: + #bug.load_comments(load_full=False) + try: + comm = bug.comment_root.comment_from_shortname(comment_id) + except comment.InvalidShortname, e: + raise UserError(e.message) + return (bug, comm) + def _test(): import doctest import sys diff --git a/libbe/comment.py b/libbe/comment.py index 678d8ba..5cc43c4 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -641,6 +641,12 @@ class Comment(Tree, settings_object.SavedSettingsObject): bug-1:2 b bug-1:3 c bug-1:4 d + >>> for id,name in a.comment_shortnames(): + ... print id, name.uuid + :1 a + :2 b + :3 c + :4 d """ if bug_shortname == None: bug_shortname = "" -- cgit From a07c70a8cb30fa7295471490a7b7fdfbd48a99ec Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 12:23:47 -0500 Subject: Upgraded `be show --xml` to new format. --- becommands/import_xml.py | 12 ++-- becommands/show.py | 140 +++++++++++++++++++++++++++++++----------- interfaces/xml/be-xml-to-mbox | 31 +++++----- 3 files changed, 128 insertions(+), 55 deletions(-) diff --git a/becommands/import_xml.py b/becommands/import_xml.py index d28e97a..212c7a7 100644 --- a/becommands/import_xml.py +++ b/becommands/import_xml.py @@ -172,8 +172,8 @@ The XML file should be formatted similarly to 1.0.0 - be - 446 + be + 446 a@b.com-20091119214553-iqyw2cpqluww3zna @@ -186,10 +186,10 @@ The XML file should be formatted similarly to ... where the ellipses mark output commpatible with Bug.xml() and -Comment.xml(). Take a look at the output of `be show --xml --version` -for some explicit examples. Unrecognized tags are ignored. Missing -tags are left at the default value. The version tag is not required, -but is strongly recommended. +Comment.xml(). Take a look at the output of `be show --xml` for some +explicit examples. Unrecognized tags are ignored. Missing tags are +left at the default value. The version tag is not required, but is +strongly recommended. The bug and comment UUIDs are always auto-generated, so if you set a field, but no field, your will be used as the diff --git a/becommands/show.py b/becommands/show.py index 4f8c30d..1211e3d 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -17,9 +17,9 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Show a particular bug""" +"""Show a particular bug, comment, or combination of both.""" import sys -from libbe import cmdutil, bugdir +from libbe import cmdutil, bugdir, comment, version, _version __desc__ = __doc__ def execute(args, manipulate_encodings=True): @@ -41,15 +41,23 @@ def execute(args, manipulate_encodings=True): >>> execute (["--xml", "a"], manipulate_encodings=False) # doctest: +ELLIPSIS - - a - a - minor - open - John Doe <jdoe@example.com> - ... - Bug A - + + + ... + ... + ... + ... + + + a + a + minor + open + John Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug A + + >>> bd.cleanup() """ parser = get_parser() @@ -60,31 +68,53 @@ def execute(args, manipulate_encodings=True): raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) + + if options.only_raw_body == True: + if len(args) != 1: + raise cmdutil.UsageError( + 'only one ID accepted with --only-raw-body') + bug,comment = cmdutil.bug_comment_from_id(bd, args[0]) + if comment == bug.comment_root: + raise cmdutil.UsageError( + "--only-raw-body requires a comment ID, not '%s'" % args[0]) + sys.__stdout__.write(comment.body) + sys.exit(0) + + bugs,root_comments = _sort_ids(args, options.comments) if options.XML: - print '' % bd.encoding - for shortname in args: - bugname,commname = cmdutil.parse_id(shortname) - if commname == None: # bug shortname - bug = cmdutil.bug_from_id(bd, shortname) + print _xml_header(bd.encoding) + else: + spaces_left = len(args) - 1 + for bugname in bugs: + bug = cmdutil.bug_from_id(bd, bugname) + if options.XML: + print bug.xml(indent=2, show_comments=options.comments) + else: + print bug.string(show_comments=options.comments) + if spaces_left > 0: + spaces_left -= 1 + print '' # add a blank line between bugs/comments + for bugname,comments in root_comments.items(): + bug = cmdutil.bug_from_id(bd, bugname) + if options.XML: + print ' ' + print ' %s' % bug.uuid + for commname in comments: + try: + comment = bug.comment_root.comment_from_shortname(commname) + except comment.InvalidShortname, e: + raise UserError(e.message) if options.XML: - print bug.xml(show_comments=options.comments) + print comment.xml(indent=4, shortname=bugname) else: - print bug.string(show_comments=options.comments) - elif options.comments == False: - continue - else: # comment shortname - bug,comment = cmdutil.bug_comment_from_id(shortname) - comment = bug.comment_root.comment_from_shortname( - shortname, bug_shortname=bugname) - if options.XML: - print comment.xml(shortname=shortname) - else: - if len(args) == 1 and options.only_raw_body == True: - sys.__stdout__.write(comment.body) - else: - print comment.string(shortname=shortname) - if shortname != args[-1] and options.XML == False: - print "" # add a blank line between bugs/comments + print comment.string(shortname=shortname) + if spaces_left > 0: + spaces_left -= 1 + print '' # add a blank line between bugs/comments + if options.XML: + print '' + if options.XML: + print _xml_footer() def get_parser(): parser = cmdutil.CmdOptionParser("be show [options] ID [ID ...]") @@ -101,9 +131,49 @@ def get_parser(): longhelp=""" Show all information about the bugs or comments whose IDs are given. -It's probably not a good idea to mix bug and comment IDs in a single -call, but you're free to do so if you like. +Without the --xml flag set, it's probably not a good idea to mix bug +and comment IDs in a single call, but you're free to do so if you +like. With the --xml flag set, there will never be any root comments, +so mix and match away (the bug listings for directly requested +comments will be restricted to the bug uuid and the requested +comment(s)). + +Directly requested comments will be grouped by their parent bug and +placed at the end of the output, so the ordering may not match the +order of the listed IDs. """ def help(): return get_parser().help_str() + longhelp + +def _sort_ids(ids, with_comments=True): + bugs = [] + root_comments = {} + for id in ids: + bugname,commname = cmdutil.parse_id(id) + if commname == None: + bugs.append(bugname) + elif with_comments == True: + if bugname not in root_comments: + root_comments[bugname] = [commname] + else: + root_comments[bugname].append(commname) + for bugname in root_comments.keys(): + assert bugname not in bugs, \ + "specifically requested both '%s%s' and '%s'" \ + % (bugname, root_comments[bugname][0], bugname) + return (bugs, root_comments) + +def _xml_header(encoding): + lines = ['' % encoding, + '', + ' ', + ' %s' % version.version()] + for tag in ['branch-nick', 'revno', 'revision-id']: + value = _version.version_info[tag.replace('-', '_')] + lines.append(' <%s>%s' % (tag, value, tag)) + lines.append(' ') + return '\n'.join(lines) + +def _xml_footer(): + return '' diff --git a/interfaces/xml/be-xml-to-mbox b/interfaces/xml/be-xml-to-mbox index ecc6327..dc4524e 100755 --- a/interfaces/xml/be-xml-to-mbox +++ b/interfaces/xml/be-xml-to-mbox @@ -86,20 +86,23 @@ class Bug (LimitedAttrDict): u"comments", u"extra-strings"] def print_to_mbox(self): - name,addr = email.utils.parseaddr(self["creator"]) - print "From %s %s" % (addr, rfc2822_to_asctime(self["created"])) - print "Message-id: <%s@%s>" % (self["uuid"], DEFAULT_DOMAIN) - print "Date: %s" % self["created"] - print "From: %s" % self["creator"] - print "Content-Type: %s; charset=%s" % ("text/plain", DEFAULT_ENCODING) - print "Content-Transfer-Encoding: 8bit" - print "Subject: %s: %s" % (self["short-name"], self["summary"]) - if "extra-strings" in self: - for estr in self["extra_strings"]: - print "X-Extra-String: %s" % estr - print "" - print self["summary"] - print "" + if "creator" in self: + # otherwise, probably a `be show` uuid-only bug to avoid + # root comments. + name,addr = email.utils.parseaddr(self["creator"]) + print "From %s %s" % (addr, rfc2822_to_asctime(self["created"])) + print "Message-id: <%s@%s>" % (self["uuid"], DEFAULT_DOMAIN) + print "Date: %s" % self["created"] + print "From: %s" % self["creator"] + print "Content-Type: %s; charset=%s" % ("text/plain", DEFAULT_ENCODING) + print "Content-Transfer-Encoding: 8bit" + print "Subject: %s: %s" % (self["short-name"], self["summary"]) + if "extra-strings" in self: + for estr in self["extra_strings"]: + print "X-Extra-String: %s" % estr + print "" + print self["summary"] + print "" if "comments" in self: for comment in self["comments"]: comment.print_to_mbox(self) -- cgit From cb6a9e819d05402ee8b9cde356d509ab22de4780 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 12:52:37 -0500 Subject: `be email-bugs` now uses `be show` internals to produce consistent XML. Broke the bulk of show.py out into new function show.output(), which is used by both show.py and email_bugs.py to reduce duplication of effort and increase consistency of the XML. Also a few relevant help string updates. --- becommands/email_bugs.py | 60 +++++++++++++++++++----------------- becommands/import_xml.py | 5 ++- becommands/show.py | 79 +++++++++++++++++++++++++----------------------- 3 files changed, 78 insertions(+), 66 deletions(-) diff --git a/becommands/email_bugs.py b/becommands/email_bugs.py index c188332..27f0b91 100644 --- a/becommands/email_bugs.py +++ b/becommands/email_bugs.py @@ -27,6 +27,7 @@ from libbe import cmdutil, bugdir from libbe.subproc import invoke from libbe.utility import time_to_str from libbe.vcs import detect_vcs, installed_vcs +import show __desc__ = __doc__ @@ -52,31 +53,41 @@ def execute(args, manipulate_encodings=True): Subject: [be-bug:xml] Updates to a, b - - - a - a - minor - open - John Doe <jdoe@example.com> - Thu, 01 Jan 1970 00:00:00 +0000 - Bug A - - - b - b - minor - closed - Jane Doe <jdoe@example.com> - Thu, 01 Jan 1970 00:00:00 +0000 - Bug B - - + + + ... + ... + ... + ... + + + a + a + minor + open + John Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug A + + + b + b + minor + closed + Jane Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug B + + >>> bd.cleanup() Note that the '=3D' bits in are the way quoted-printable escapes '='. + + The unclosed ... is because revision ids can be long + enough to cause line wraps, and we want to ensure we match even if + the closing is split by the wrapping. """ parser = get_parser() options, args = parser.parse_args(args) @@ -86,19 +97,14 @@ def execute(args, manipulate_encodings=True): raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) - lines = [u'' % bd.encoding, - u''] - for shortname in args: - bug = cmdutil.bug_from_id(bd, shortname) - lines.append(bug.xml(show_comments=True)) - lines.append(u'') + xml = show.output(args, bd, as_xml=True, with_comments=True) subject = options.subject if subject == None: subject = '[be-bug:xml] Updates to %s' % ', '.join(args) submit_email = TextEmail(to_address=options.to_address, from_address=options.from_address, subject=subject, - body=u'\n'.join(lines), + body=xml, encoding=bd.encoding, subtype='xml') if options.output == True: diff --git a/becommands/import_xml.py b/becommands/import_xml.py index 212c7a7..2572075 100644 --- a/becommands/import_xml.py +++ b/becommands/import_xml.py @@ -247,8 +247,11 @@ User creates a new bug ... User exports bug as xml and emails it to the developers - user$ be show --xml --version 48f > 48f.xml + user$ be show --xml 48f > 48f.xml user$ cat 48f.xml | mail -s "Demuxulizer bug xml" devs@b.com +or equivalently (with a slightly fancier be-handle-mail compatible +email): + user$ be email-bugs 48f Devs recieve email, and save it's contents as demux-bug.xml dev$ cat demux-bug.xml | be import-xml - """ diff --git a/becommands/show.py b/becommands/show.py index 1211e3d..11890a8 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -79,42 +79,7 @@ def execute(args, manipulate_encodings=True): "--only-raw-body requires a comment ID, not '%s'" % args[0]) sys.__stdout__.write(comment.body) sys.exit(0) - - bugs,root_comments = _sort_ids(args, options.comments) - if options.XML: - print _xml_header(bd.encoding) - else: - spaces_left = len(args) - 1 - for bugname in bugs: - bug = cmdutil.bug_from_id(bd, bugname) - if options.XML: - print bug.xml(indent=2, show_comments=options.comments) - else: - print bug.string(show_comments=options.comments) - if spaces_left > 0: - spaces_left -= 1 - print '' # add a blank line between bugs/comments - for bugname,comments in root_comments.items(): - bug = cmdutil.bug_from_id(bd, bugname) - if options.XML: - print ' ' - print ' %s' % bug.uuid - for commname in comments: - try: - comment = bug.comment_root.comment_from_shortname(commname) - except comment.InvalidShortname, e: - raise UserError(e.message) - if options.XML: - print comment.xml(indent=4, shortname=bugname) - else: - print comment.string(shortname=shortname) - if spaces_left > 0: - spaces_left -= 1 - print '' # add a blank line between bugs/comments - if options.XML: - print '' - if options.XML: - print _xml_footer() + print output(args, bd, as_xml=options.XML, with_comments=options.comments) def get_parser(): parser = cmdutil.CmdOptionParser("be show [options] ID [ID ...]") @@ -173,7 +138,45 @@ def _xml_header(encoding): value = _version.version_info[tag.replace('-', '_')] lines.append(' <%s>%s' % (tag, value, tag)) lines.append(' ') - return '\n'.join(lines) + return lines def _xml_footer(): - return '' + return [''] + +def output(ids, bd, as_xml=True, with_comments=True): + bugs,root_comments = _sort_ids(ids, with_comments) + lines = [] + if as_xml: + lines.extend(_xml_header(bd.encoding)) + else: + spaces_left = len(ids) - 1 + for bugname in bugs: + bug = cmdutil.bug_from_id(bd, bugname) + if as_xml: + lines.append(bug.xml(indent=2, show_comments=with_comments)) + else: + lines.append(bug.string(show_comments=with_comments)) + if spaces_left > 0: + spaces_left -= 1 + lines.append('') # add a blank line between bugs/comments + for bugname,comments in root_comments.items(): + bug = cmdutil.bug_from_id(bd, bugname) + if as_xml: + lines.extend([' ', ' %s' % bug.uuid]) + for commname in comments: + try: + comment = bug.comment_root.comment_from_shortname(commname) + except comment.InvalidShortname, e: + raise UserError(e.message) + if as_xml: + lines.append(comment.xml(indent=4, shortname=bugname)) + else: + lines.append(comment.string(shortname=shortname)) + if spaces_left > 0: + spaces_left -= 1 + lines.append('') # add a blank line between bugs/comments + if as_xml: + lines.append('') + if as_xml: + lines.extend(_xml_footer()) + return '\n'.join(lines) -- cgit From 3b03b9aa1bd0d2550fab48940242453cff238508 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 13:08:05 -0500 Subject: Added interfaces/email/interactive/examples/email_bugs For testing the new [be-bug:xml] interface we're about to write. --- interfaces/email/interactive/examples/email_bugs | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 interfaces/email/interactive/examples/email_bugs diff --git a/interfaces/email/interactive/examples/email_bugs b/interfaces/email/interactive/examples/email_bugs new file mode 100644 index 0000000..949e1c1 --- /dev/null +++ b/interfaces/email/interactive/examples/email_bugs @@ -0,0 +1,37 @@ +From jdoe@example.com Fri Apr 18 12:00:00 2008 +Content-Type: text/xml; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: quoted-printable +From: jdoe@example.com +To: a@b.com +Date: Fri, 18 Apr 2008 12:00:00 +0000 +Subject: [be-bug:xml] Updates to a, b + + + + + 1.0.0 + be + 446 + wking@drexel.edu-20091119214553-iqyw2cpqluww3zna + + + a + a + minor + open + John Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug A + + + b + b + minor + closed + Jane Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug B + + + -- cgit From 65bf5f8d9ddf51625d6b3b282838a9a4c71868d3 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 13:18:54 -0500 Subject: Fixed bug in be-handle-mail.Message.parse_comment() for emails w/o Message-id. You used to get: Uncaught exception: 'NoneType' object has no attribute 'decode' File "./be-handle-mail", line 857, in main m.run() File "./be-handle-mail", line 591, in run command.run() File "./be-handle-mail", line 244, in run manipulate_encodings=False) File "/tmp/be.email-bugs/interfaces/email/interactive/libbe/cmdutil.py", line 82, in execute ret = cmd.execute([a.decode(enc) for a in args], A `print args' in Message.parse_comment() revealed [..., u'--alt-id', None,...] --- interfaces/email/interactive/be-handle-mail | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index fa80698..8831e3c 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -564,8 +564,10 @@ class Message (object): if mime_type == "text/plain": body = self._strip_footer(body) content_type = mime_type - args = [u"--author", author, u"--alt-id", alt_id, - u"--content-type", content_type, bug_id, u"-"] + args = [u"--author", author] + if alt_id != None: + args.extend([u"--alt-id", alt_id]) + args.extend([u"--content-type", content_type, bug_id, u"-"]) commands = [Command(self, command, args, stdin=body)] return commands def parse_control(self): -- cgit From 607c72ea7f3382f3a51598593b71eabbbdbef664 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 13:49:32 -0500 Subject: Adjusted `be import-xml` to properly handle croot_bug==None. If there aren't any root comments, then the user needn't specify --comment-root, in which case croot_bug will be None and we want to skip all the croot processing. --- becommands/import_xml.py | 57 ++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/becommands/import_xml.py b/becommands/import_xml.py index 2572075..928ca46 100644 --- a/becommands/import_xml.py +++ b/becommands/import_xml.py @@ -105,37 +105,42 @@ def execute(args, manipulate_encodings=True): % (child.tag, comment_list.tag) # merge the new root_comments - croot_cids = [] - for c in croot_bug.comment_root.traverse(): - croot_cids.append(c.uuid) - if c.alt_id != None: - croot_cids.append(c.alt_id) - for new in root_comments: - if new.alt_id in croot_cids: - raise cmdutil.UserError( - 'clashing comment alt_id: %s' % new.alt_id) - croot_cids.append(new.uuid) - if new.alt_id != None: - croot_cids.append(new.alt_id) - if new.in_reply_to == None: - new.in_reply_to = croot_comment.uuid - try: - # link new comments - comment.list_to_root(root_comments,croot_bug,root=croot_comment, - ignore_missing_references= \ - options.ignore_missing_references) - except comment.MissingReference, e: - raise cmdutil.UserError(e) - + if len(root_comments) > 0: + if croot_bug == None: + raise UserError( + '--comment-root option is required for your root comments:\n%s' + % '\n\n'.join([c.string() for c in root_comments])) + croot_cids = [] + for c in croot_bug.comment_root.traverse(): + croot_cids.append(c.uuid) + if c.alt_id != None: + croot_cids.append(c.alt_id) + for new in root_comments: + if new.alt_id in croot_cids: + raise cmdutil.UserError( + 'clashing comment alt_id: %s' % new.alt_id) + croot_cids.append(new.uuid) + if new.alt_id != None: + croot_cids.append(new.alt_id) + if new.in_reply_to == None: + new.in_reply_to = croot_comment.uuid + try: + # link new comments + comment.list_to_root(root_comments,croot_bug,root=croot_comment, + ignore_missing_references= \ + options.ignore_missing_references) + except comment.MissingReference, e: + raise cmdutil.UserError(e) # merge the new croot_bugs for new in root_bugs: bd.append(new) # protect against programmer error causing data loss: - comms = [c.uuid for c in croot_comment.traverse()] - for new in root_comments: - assert new.uuid in comms, \ - "comment %s wasn't added to %s" % (new.uuid, croot_comment.uuid) + if croot_bug != None: + comms = [c.uuid for c in croot_comment.traverse()] + for new in root_comments: + assert new.uuid in comms, \ + "comment %s wasn't added to %s" % (new.uuid, croot_comment.uuid) for new in root_bugs: assert bd.has_bug(new.uuid), \ "bug %s wasn't added" % (new.uuid) -- cgit From f1cb3e5d0f6341c4cdf457f4f029270037ecae16 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 13:53:09 -0500 Subject: Added [be-tag:xml] processing to be-handle-mail. Now it will automatically apply and commit emails from be email-bugs ... --- interfaces/email/interactive/be-handle-mail | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index 8831e3c..bd37f55 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -81,6 +81,7 @@ SUBJECT_TAG_START = None SUBJECT_TAG_NEW = None SUBJECT_TAG_COMMENT = None SUBJECT_TAG_CONTROL = None +SUBJECT_TAG_XML = None BREAK = u"--" NEW_REQUIRED_PSEUDOHEADERS = [u"Version"] @@ -90,7 +91,7 @@ NEW_OPTIONAL_PSEUDOHEADERS = [u"Reporter", u"Assign", u"Depend", u"Severity", CONTROL_COMMENT = u"#" ALLOWED_COMMANDS = [u"assign", u"comment", u"commit", u"depend", u"help", u"list", u"merge", u"new", u"open", u"severity", u"show", - u"status", u"subscribe", u"tag", u"target"] + u"status", u"subscribe", u"tag", u"target", u"import-xml"] AUTOCOMMIT = True @@ -402,8 +403,8 @@ class Message (object): def _subject_tag_type(self): """ Parse subject tag, return (type, value), where type is one of - None, "new", "comment", or "control"; and value is None except - in the case of "comment", in which case it's the bug + None, "new", "comment", "control", or "xml"; and value is None + except in the case of "comment", in which case it's the bug ID/shortname. """ tag,subject = self._split_subject() @@ -413,6 +414,8 @@ class Message (object): type = u"new" elif tag == SUBJECT_TAG_CONTROL: type = u"control" + elif tag == SUBJECT_TAG_XML: + type = u"xml" else: match = SUBJECT_TAG_COMMENT.match(tag) if len(match.groups()) == 1: @@ -506,6 +509,8 @@ class Message (object): commands = self.parse_comment(value) elif tag_type == u"control": commands = self.parse_control() + elif tag_type == u"xml": + commands = self.parse_xml() else: raise Exception, u"Unrecognized tag type '%s'" % tag_type return commands @@ -585,6 +590,16 @@ class Message (object): if len(commands) == 0: raise InvalidEmail(self, u"No commands in control email.") return commands + def parse_xml(self): + command = u"import-xml" + body,mime_type = list(self._get_bodies_and_mime_types())[0] + if mime_type != "text/xml": + raise InvalidEmail(self, + u"Emails to %s must have MIME type 'text/xml', not '%s'." + % (SUBJECT_TAG_XML, mime_type)) + args = [u"-"] + commands = [Command(self, command, args, stdin=body)] + return commands def run(self): self._begin_response() commands = self.parse() @@ -738,13 +753,15 @@ def generate_global_tags(tag_base=u"be-bug"): Generate a series of tags from a base tag string. """ global SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \ - SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL + SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL, \ + SUBJECT_TAG_XML SUBJECT_TAG_BASE = tag_base SUBJECT_TAG_START = u"[%s" % tag_base SUBJECT_TAG_RESPONSE = u"[%s]" % tag_base SUBJECT_TAG_NEW = u"[%s:submit]" % tag_base SUBJECT_TAG_COMMENT = re.compile(u"\[%s:([\-0-9a-z]*)]" % tag_base) SUBJECT_TAG_CONTROL = SUBJECT_TAG_RESPONSE + SUBJECT_TAG_XML = u"[%s:xml]" % tag_base def open_logfile(logpath=None): """ @@ -947,6 +964,10 @@ class GenerateGlobalTagsTestCase (unittest.TestCase): m = SUBJECT_TAG_COMMENT.match("[projectX-bug:xyz-123]") self.failUnlessEqual(len(m.groups()), 1) self.failUnlessEqual(m.group(1), u"xyz-123") + def test_subject_tag_xml(self): + "Should set SUBJECT_TAG_XML global correctly" + generate_global_tags(u"projectX-bug") + self.failUnlessEqual(SUBJECT_TAG_XML, u"[projectX-bug:xml]") if __name__ == "__main__": main(sys.argv) -- cgit From 85fd7e2681005e8b47ffc1e73fcc0cca93025921 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 14:03:00 -0500 Subject: Updated interfaces/email/interactive/README for [be-bug:xml] interface --- interfaces/email/interactive/README | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/interfaces/email/interactive/README b/interfaces/email/interactive/README index 79ef9a9..2070973 100644 --- a/interfaces/email/interactive/README +++ b/interfaces/email/interactive/README @@ -23,13 +23,15 @@ execution. Once be-handle-mail receives the email, the parsing method is selected according to the subject tag that procmail used grab the email in the -first place. There are three parsing styles: +first place. There are four parsing styles: Style Subject creating bugs [be-bug:submit] new bug summary commenting on bugs [be-bug:] commit message control [be-bug] commit message + xml [be-bug:xml] commit message These are analogous to submit@bugs.debian.org, nnn@bugs.debian.org, -and control@bugs.debian.org respectively. +and control@bugs.debian.org respectively. The xml style has no Debian +analog. Creating bugs ============= @@ -106,6 +108,15 @@ shlex.split(). -- Goofy tagline ignored. +XML +=== + +This interface allows users without access to the versioned source of +the program to conveniently submit bugs and comments using be. You +should not attempt to compose emails for this interface by hand. See +the documentation for the `email-bugs' and `import-xml' be commands +for details. + Example emails ============== -- cgit From bb8dd5066f730f9bb0ac0398bf9a167e9736a808 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 14:54:11 -0500 Subject: Fix libbe.vcs.VCS.get_file_contents(allow_no_vcs=True,binary=True). In it's previous form it had ignored the binary option if self._use_vcs() returned False. Also added a few more details to the docstring, explaining the arguments. --- libbe/vcs.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libbe/vcs.py b/libbe/vcs.py index b1fd114..57a0245 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -334,6 +334,10 @@ class VCS(object): """ Get the file as it was in a given revision. Revision==None specifies the current revision. + + allow_no_vcs==True allows direct access to files through + codecs.open() or open() if the vcs decides it can't handle the + given path. """ if not os.path.exists(path): raise NoSuchFile(path) @@ -341,7 +345,10 @@ class VCS(object): relpath = self._u_rel_path(path) contents = self._vcs_get_file_contents(relpath,revision,binary=binary) else: - f = codecs.open(path, "r", self.encoding) + if binary == True: + f = codecs.open(path, "r", self.encoding) + else: + f = open(path, "rb") contents = f.read() f.close() return contents -- cgit From 614d4e40e148520ac511cbe0606bcbdcf24c8a08 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 15:18:02 -0500 Subject: Added restrict_file_access to becommands' execute() args. + associated adjustments in other files. See cmdutil.restrict_file_access.__doc__ for an explanation of the security hole this closes. --- README.dev | 10 +++++++++- becommands/assign.py | 2 +- becommands/close.py | 2 +- becommands/comment.py | 2 +- becommands/commit.py | 4 +++- becommands/depend.py | 2 +- becommands/diff.py | 2 +- becommands/email_bugs.py | 2 +- becommands/help.py | 2 +- becommands/html.py | 2 +- becommands/import_xml.py | 4 +++- becommands/init.py | 2 +- becommands/list.py | 2 +- becommands/merge.py | 2 +- becommands/new.py | 2 +- becommands/open.py | 2 +- becommands/remove.py | 2 +- becommands/set.py | 2 +- becommands/severity.py | 2 +- becommands/show.py | 2 +- becommands/status.py | 2 +- becommands/subscribe.py | 2 +- becommands/tag.py | 2 +- becommands/target.py | 2 +- interfaces/email/interactive/be-handle-mail | 3 ++- libbe/cmdutil.py | 21 +++++++++++++++++++-- 26 files changed, 57 insertions(+), 27 deletions(-) diff --git a/README.dev b/README.dev index ddc3a88..fb4f471 100644 --- a/README.dev +++ b/README.dev @@ -10,11 +10,19 @@ To fit into the current framework, your extension module should provide the following elements: __desc__ A short string describing the purpose of your plugin - execute(args) + execute(args, manipulate_encodings=True, restrict_file_access=False) The entry function for your plugin. args is everything from sys.argv after the name of your plugin (e.g. for the command `be open abc', args=['abc']). + manipulate_encodings should be passed through to any calls to + bugdir.BugDir(). See the BugDir documentation for details. + + If restrict_file_access==True, you should call + cmdutil.restrict_file_access(bugdir, path) + before attempting to read or write a file. See the + restrict_file_access documentation for details. + Note: be supports command-completion. To avoid raising errors you need to deal with possible '--complete' options and arguments. See the 'Command completion' section below for more information. diff --git a/becommands/assign.py b/becommands/assign.py index 31aa607..2c78f69 100644 --- a/becommands/assign.py +++ b/becommands/assign.py @@ -21,7 +21,7 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/close.py b/becommands/close.py index d65c4e0..a14cea8 100644 --- a/becommands/close.py +++ b/becommands/close.py @@ -21,7 +21,7 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> from libbe import bugdir >>> import os diff --git a/becommands/comment.py b/becommands/comment.py index a600e9f..8e899ce 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -21,7 +21,7 @@ import os import sys __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import time >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/commit.py b/becommands/commit.py index b530fdc..39d1e2e 100644 --- a/becommands/commit.py +++ b/becommands/commit.py @@ -18,7 +18,7 @@ from libbe import cmdutil, bugdir, editor, vcs import sys __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> from libbe import bug @@ -49,6 +49,8 @@ def execute(args, manipulate_encodings=True): elif options.body == "EDITOR": body = editor.editor_string("Please enter your commit message above") else: + if restrict_file_access == True: + cmdutil.restrict_file_access(bd, options.body) body = bd.vcs.get_file_contents(options.body, allow_no_vcs=True) try: revision = bd.vcs.commit(summary, body=body, diff --git a/becommands/depend.py b/becommands/depend.py index 044aaff..f50d693 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -35,7 +35,7 @@ class BrokenLink (Exception): self.blocking_bug = blocking_bug -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> from libbe import utility >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/diff.py b/becommands/diff.py index e71da9b..6477934 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -21,7 +21,7 @@ from libbe import cmdutil, bugdir, diff import os __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/email_bugs.py b/becommands/email_bugs.py index 27f0b91..d0366df 100644 --- a/becommands/email_bugs.py +++ b/becommands/email_bugs.py @@ -33,7 +33,7 @@ __desc__ = __doc__ sendmail='/usr/sbin/sendmail -t' -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> from libbe import bug diff --git a/becommands/help.py b/becommands/help.py index c12c56a..99ab8c4 100644 --- a/becommands/help.py +++ b/becommands/help.py @@ -20,7 +20,7 @@ from libbe import cmdutil, utility __desc__ = __doc__ -def execute(args, manipulate_encodings=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ Print help of specified command (the manipulate_encodings argument is ignored). diff --git a/becommands/html.py b/becommands/html.py index b0640da..622a531 100644 --- a/becommands/html.py +++ b/becommands/html.py @@ -21,7 +21,7 @@ import xml.sax.saxutils, htmlentitydefs __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/import_xml.py b/becommands/import_xml.py index 928ca46..a74d329 100644 --- a/becommands/import_xml.py +++ b/becommands/import_xml.py @@ -24,7 +24,7 @@ except ImportError: # look for non-core module from elementtree import ElementTree __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import time >>> import StringIO @@ -69,6 +69,8 @@ def execute(args, manipulate_encodings=True): if filename == '-': xml = sys.stdin.read() else: + if restrict_file_access == True: + cmdutil.restrict_file_access(bd, options.body) xml = bd.vcs.get_file_contents(filename, allow_no_vcs=True) str_xml = xml.encode('unicode_escape').replace(r'\n', '\n') # unicode read + encode to string so we know the encoding, diff --git a/becommands/init.py b/becommands/init.py index 39fb006..7d6d475 100644 --- a/becommands/init.py +++ b/becommands/init.py @@ -20,7 +20,7 @@ import os.path from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> from libbe import utility, vcs >>> import os diff --git a/becommands/list.py b/becommands/list.py index 14e387b..4711789 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -26,7 +26,7 @@ __desc__ = __doc__ AVAILABLE_CMPS = [fn[4:] for fn in dir(bug) if fn[:4] == 'cmp_'] AVAILABLE_CMPS.remove("attr") # a cmp_* template. -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/merge.py b/becommands/merge.py index 766af56..31c781f 100644 --- a/becommands/merge.py +++ b/becommands/merge.py @@ -19,7 +19,7 @@ from libbe import cmdutil, bugdir import os, copy __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> from libbe import utility >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/new.py b/becommands/new.py index 30a7d28..92d61e4 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -20,7 +20,7 @@ from libbe import cmdutil, bugdir import sys __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os, time >>> from libbe import bug diff --git a/becommands/open.py b/becommands/open.py index 1b4c23e..c2c15e2 100644 --- a/becommands/open.py +++ b/becommands/open.py @@ -21,7 +21,7 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/remove.py b/becommands/remove.py index d265e5c..e4f0065 100644 --- a/becommands/remove.py +++ b/becommands/remove.py @@ -18,7 +18,7 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> from libbe import mapfile >>> import os diff --git a/becommands/set.py b/becommands/set.py index e5cab8d..c8c5a2c 100644 --- a/becommands/set.py +++ b/becommands/set.py @@ -32,7 +32,7 @@ def _value_string(bd, setting): val = None return str(val) -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/severity.py b/becommands/severity.py index f42f740..524976b 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -21,7 +21,7 @@ from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/show.py b/becommands/show.py index 11890a8..557c63a 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -22,7 +22,7 @@ import sys from libbe import cmdutil, bugdir, comment, version, _version __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/status.py b/becommands/status.py index bf66c26..fd31c97 100644 --- a/becommands/status.py +++ b/becommands/status.py @@ -18,7 +18,7 @@ from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/subscribe.py b/becommands/subscribe.py index 0a23057..051341b 100644 --- a/becommands/subscribe.py +++ b/becommands/subscribe.py @@ -55,7 +55,7 @@ class InvalidType (ValueError): self.type_root = type_root -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> bd = bugdir.SimpleBugDir() >>> bd.set_sync_with_disk(True) diff --git a/becommands/tag.py b/becommands/tag.py index e6debb4..e22cb70 100644 --- a/becommands/tag.py +++ b/becommands/tag.py @@ -19,7 +19,7 @@ from libbe import cmdutil, bugdir import os, copy __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> from libbe import utility >>> bd = bugdir.SimpleBugDir() diff --git a/becommands/target.py b/becommands/target.py index 672eb06..efb2479 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -22,7 +22,7 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True): +def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> import os >>> bd = bugdir.SimpleBugDir() diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index bd37f55..e0e3490 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -242,7 +242,8 @@ class Command (object): os.chdir(BE_DIR) try: self.ret = libbe.cmdutil.execute(self.command, self.args, - manipulate_encodings=False) + manipulate_encodings=False, + restrict_file_access=True) except libbe.cmdutil.GetHelp: print libbe.cmdutil.help(command) except libbe.cmdutil.GetCompletions: diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index 96430eb..e37750d 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -76,11 +76,12 @@ def get_command(command_name): return cmd -def execute(cmd, args, manipulate_encodings=True): +def execute(cmd, args, manipulate_encodings=True, restrict_file_access=False): enc = encoding.get_encoding() cmd = get_command(cmd) ret = cmd.execute([a.decode(enc) for a in args], - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + restrict_file_access=restrict_file_access) if ret == None: ret = 0 return ret @@ -213,6 +214,22 @@ def underlined(instring): return "%s\n%s" % (instring, "="*len(instring)) +def restrict_file_access(bugdir, path): + """ + Check that the file at path is inside bugdir.root. This is + important if you allow other users to execute becommands with your + username (e.g. if you're running be-handle-mail through your + ~/.procmailrc). If this check wasn't made, a user could e.g. + run + be commit -b ~/.ssh/id_rsa "Hack to expose ssh key" + which would expose your ssh key to anyone who could read the VCS + log. + """ + in_root = bugdir.vcs.path_in_root(path, bugdir.root) + if in_root == False: + raise UserError('file access restricted!\n %s not in %s' + % (path, bugdir.root)) + def parse_id(id): """ Return (bug_id, comment_id) tuple. -- cgit From 3e050db00d2ffa2c011efc4d9b47d8edeac5c43c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 28 Nov 2009 07:41:13 -0500 Subject: Added Bug.merge() and Comment.merge(). Added *.explicit_attrs list creation to Bug and Comment.from_xml(). Added match_alt_id keyword argumennt to .comment_from_uuid(). Removed extra enline following '' tag in Bug and Comment.xml(). --- libbe/bug.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++----- libbe/comment.py | 82 ++++++++++++++++++++++++++++++++++---- 2 files changed, 185 insertions(+), 16 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index fecb9b7..23d5488 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -20,6 +20,7 @@ Define the Bug class for representing bugs. """ +import copy import os import os.path import errno @@ -302,7 +303,7 @@ class Bug(settings_object.SavedSettingsObject): if v is not None: lines.append(' <%s>%s' % (k,xml.sax.saxutils.escape(v),k)) for estr in self.extra_strings: - lines.append(' %s\n' % estr) + lines.append(' %s' % estr) if show_comments == True: comout = self.comment_root.xml_thread(indent=indent+2, auto_name_map=True, @@ -332,6 +333,8 @@ class Bug(settings_object.SavedSettingsObject): >>> bugB.uuid = bugB.alt_id >>> bugB.xml(shortname="bug-1") == xml True + >>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE + ['uuid', 'severity', 'status', 'creator', 'created', 'summary'] """ if type(xml_string) == types.UnicodeType: xml_string = xml_string.strip().encode('unicode_escape') @@ -340,8 +343,9 @@ class Bug(settings_object.SavedSettingsObject): raise utility.InvalidXML( \ 'bug', bug, 'root element must be ') tags=['uuid','short-name','severity','status','assigned','target', - 'reporter', 'creator', 'created', 'summary', 'extra-string', + 'reporter', 'creator','created','summary','extra-string', 'comment'] + self.explicit_attrs = [] uuid = None estrs = [] for child in bug.getchildren(): @@ -353,23 +357,120 @@ 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': uuid = text continue # don't set the bug's uuid tag. - if child.tag == 'extra-string': + elif child.tag == 'extra-string': estrs.append(text) continue # don't set the bug's extra_string yet. - else: - attr_name = child.tag.replace('-','_') + attr_name = child.tag.replace('-','_') + self.explicit_attrs.append(attr_name) setattr(self, attr_name, text) elif verbose == True: print >> sys.stderr, "Ignoring unknown tag %s in %s" \ % (child.tag, comment.tag) - if uuid not in [None, self.uuid]: + if uuid != self.uuid: if not hasattr(self, 'alt_id') or self.alt_id == None: self.alt_id = uuid self.extra_strings = estrs + def merge(self, other, allow_changes=True, allow_new_comments=True): + """ + Merge info from other into this bug. Overrides any attributes + in self that are listed in other.explicit_attrs. + >>> bugA = Bug(uuid='0123', summary='Need to test Bug.merge()') + >>> bugA.date = 'Thu, 01 Jan 1970 00:00:00 +0000' + >>> bugA.creator = 'Frank' + >>> bugA.extra_strings += ['TAG: very helpful'] + >>> bugA.extra_strings += ['TAG: favorite'] + >>> commA = bugA.comment_root.new_reply(body='comment A') + >>> commA.uuid = 'uuid-commA' + >>> bugB = Bug(uuid='3210', summary='More tests for Bug.merge()') + >>> bugB.date = 'Fri, 02 Jan 1970 00:00:00 +0000' + >>> bugB.creator = 'John' + >>> bugB.explicit_attrs = ['creator', 'summary'] + >>> bugB.extra_strings += ['TAG: very helpful'] + >>> bugB.extra_strings += ['TAG: useful'] + >>> commB = bugB.comment_root.new_reply(body='comment B') + >>> commB.uuid = 'uuid-commB' + >>> bugA.merge(bugB, allow_changes=False) + Traceback (most recent call last): + ... + ValueError: Merge would change creator "Frank"->"John" for bug 0123 + >>> bugA.merge(bugB, allow_new_comments=False) + Traceback (most recent call last): + ... + ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123 + >>> bugA.merge(bugB) + >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS + + 0123 + 0123 + minor + open + John + ... + More tests for Bug.merge() + TAG: favorite + TAG: useful + TAG: very helpful + + uuid-commA + 0123:1 + + ... + text/plain + comment A + + + uuid-commB + 0123:2 + + ... + text/plain + comment B + + + """ + for attr in other.explicit_attrs: + old = getattr(self, attr) + new = getattr(other, attr) + if old != new: + if allow_changes == True: + setattr(self, attr, new) + else: + raise ValueError, \ + 'Merge would change %s "%s"->"%s" for bug %s' \ + % (attr, old, new, self.uuid) + if allow_changes == False and len(other.extra_strings) > 0: + raise ValueError, \ + 'Merge would change extra_strings for bug %s' % self.uuid + for estr in other.extra_strings: + if not estr in self.extra_strings: + self.extra_strings.append(estr) + import sys + for o_comm in other.comments(): + s_comm = None + try: + s_comm = self.comment_root.comment_from_uuid(o_comm.uuid) + except KeyError, e: + try: + s_comm = self.comment_root.comment_from_uuid(o_comm.alt_id) + except KeyError, e: + pass + if s_comm == None: + if allow_new_comments == False: + raise ValueError, \ + 'Merge would add comment %s (alt: %s) to bug %s' \ + % (o_comm.uuid, o_comm.alt_id, self.uuid) + o_comm_copy = copy.copy(o_comm) + o_comm_copy.bug = self + print >> sys.stderr, "add comment %s" % o_comm.uuid + self.comment_root.add_reply(o_comm_copy) + else: + print >> sys.stderr, "merge comment %s into %s" % (o_comm.uuid, s_comm.uuid) + s_comm.merge(o_comm, allow_changes=allow_changes) + def string(self, shortlist=False, show_comments=False): if self.bugdir == None: shortname = self.uuid @@ -493,8 +594,8 @@ class Bug(settings_object.SavedSettingsObject): return self.comment_root.comment_from_shortname(shortname, *args, **kwargs) - def comment_from_uuid(self, uuid): - return self.comment_root.comment_from_uuid(uuid) + def comment_from_uuid(self, uuid, *args, **kwargs): + return self.comment_root.comment_from_uuid(uuid, *args, **kwargs) def comment_shortnames(self, shortname=None): """ diff --git a/libbe/comment.py b/libbe/comment.py index 5cc43c4..1adb6f4 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -344,7 +344,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): if v != None: lines.append(' <%s>%s' % (k,xml.sax.saxutils.escape(v),k)) for estr in self.extra_strings: - lines.append(' %s\n' % estr) + lines.append(' %s' % estr) lines.append('') istring = ' '*indent sep = '\n' + istring @@ -362,6 +362,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> xml = commA.xml(shortname="com-1") >>> commB = Comment() >>> commB.from_xml(xml, verbose=True) + >>> commB.explicit_attrs + ['author', 'date', 'content_type', 'body', 'alt_id'] >>> commB.xml(shortname="com-1") == xml False >>> commB.uuid = commB.alt_id @@ -377,6 +379,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): 'comment', comment, 'root element must be ') tags=['uuid','alt-id','in-reply-to','author','date','content-type', 'body','extra-string'] + self.explicit_attrs = [] uuid = None body = None estrs = [] @@ -392,19 +395,21 @@ class Comment(Tree, settings_object.SavedSettingsObject): if child.tag == 'uuid': uuid = text continue # don't set the comment's uuid tag. - if child.tag == 'body': + elif child.tag == 'body': body = text + self.explicit_attrs.append(child.tag) continue # don't set the comment's body yet. - if child.tag == 'extra-string': + elif child.tag == 'extra-string': estrs.append(text) continue # don't set the comment's extra_string yet. - else: - attr_name = child.tag.replace('-','_') + attr_name = child.tag.replace('-','_') + self.explicit_attrs.append(attr_name) setattr(self, attr_name, text) elif verbose == True: print >> sys.stderr, 'Ignoring unknown tag %s in %s' \ % (child.tag, comment.tag) - if self.alt_id == None and uuid not in [None, self.uuid]: + if self.alt_id == None: + self.explicit_attrs.append('alt_id') self.alt_id = uuid if body != None: if self.content_type.startswith('text/'): @@ -413,6 +418,58 @@ class Comment(Tree, settings_object.SavedSettingsObject): self.body = base64.decodestring(body) self.extra_strings = estrs + def merge(self, other, allow_changes=True): + """ + Merge info from other into this comment. Overrides any + attributes in self that are listed in other.explicit_attrs. + >>> commA = Comment(bug=None, body='Some insightful remarks') + >>> commA.uuid = '0123' + >>> commA.date = 'Thu, 01 Jan 1970 00:00:00 +0000' + >>> commA.author = 'Frank' + >>> commA.extra_strings += ['TAG: very helpful'] + >>> commA.extra_strings += ['TAG: favorite'] + >>> commB = Comment(bug=None, body='More insightful remarks') + >>> commB.uuid = '3210' + >>> commB.date = 'Fri, 02 Jan 1970 00:00:00 +0000' + >>> commB.author = 'John' + >>> commB.explicit_attrs = ['author', 'body'] + >>> commB.extra_strings += ['TAG: very helpful'] + >>> commB.extra_strings += ['TAG: useful'] + >>> commA.merge(commB, allow_changes=False) + Traceback (most recent call last): + ... + ValueError: Merge would change author "Frank"->"John" for comment 0123 + >>> commA.merge(commB) + >>> print commA.xml() + + 0123 + 0123 + John + Thu, 01 Jan 1970 00:00:00 +0000 + text/plain + More insightful remarks + TAG: favorite + TAG: useful + TAG: very helpful + + """ + for attr in other.explicit_attrs: + old = getattr(self, attr) + new = getattr(other, attr) + if old != new: + if allow_changes == True: + setattr(self, attr, new) + else: + raise ValueError, \ + 'Merge would change %s "%s"->"%s" for comment %s' \ + % (attr, old, new, self.uuid) + if allow_changes == False and len(other.extra_strings) > 0: + raise ValueError, \ + 'Merge would change extra_strings for comment %s' % self.uuid + for estr in other.extra_strings: + if not estr in self.extra_strings: + self.extra_strings.append(estr) + def string(self, indent=0, shortname=None): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") @@ -674,7 +731,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): raise InvalidShortname(comment_shortname, list(self.comment_shortnames(*args, **kwargs))) - def comment_from_uuid(self, uuid): + def comment_from_uuid(self, uuid, match_alt_id=True): """ Use a comment shortname to look up a comment. >>> a = Comment(bug=None, uuid="a") @@ -684,13 +741,24 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> c.uuid = "c" >>> d = a.new_reply() >>> d.uuid = "d" + >>> d.alt_id = "d-alt" >>> comm = a.comment_from_uuid("d") >>> id(comm) == id(d) True + >>> comm = a.comment_from_uuid("d-alt") + >>> id(comm) == id(d) + True + >>> comm = a.comment_from_uuid(None, match_alt_id=False) + Traceback (most recent call last): + ... + KeyError: None """ for comment in self.traverse(): if comment.uuid == uuid: return comment + if match_alt_id == True and uuid != None \ + and comment.alt_id == uuid: + return comment raise KeyError(uuid) def cmp_attr(comment_1, comment_2, attr, invert=False): -- cgit From c90ed61e7deb594edf3707850f2d3a87601a581b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 28 Nov 2009 20:24:19 -0500 Subject: BugDir.list_uuids() -> BugDir.uuids() --- becommands/comment.py | 2 +- becommands/target.py | 2 +- libbe/bugdir.py | 10 +++++----- libbe/diff.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/becommands/comment.py b/becommands/comment.py index 8e899ce..fbc994f 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -145,7 +145,7 @@ def complete(options, args, parser): bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False) bugs = [] - for uuid in bd.list_uuids(): + for uuid in bd.uuids(): if uuid.startswith(partial): bug = bd.bug_from_uuid(uuid) if bug.active == True: diff --git a/becommands/target.py b/becommands/target.py index efb2479..9a202b1 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -50,7 +50,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) if options.list: - ts = set([bd.bug_from_uuid(bug).target for bug in bd.list_uuids()]) + ts = set([bd.bug_from_uuid(bug).target for bug in bd.uuids()]) for target in sorted(ts): if target and isinstance(target,str): print target diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 675b744..3d07754 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -239,7 +239,7 @@ settings easy. Don't set this attribute. Set .vcs instead, and map = {} for bug in self: map[bug.uuid] = bug - for uuid in self.list_uuids(): + for uuid in self.uuids(): if uuid not in map: map[uuid] = None self._bug_map_value = map # ._bug_map_value used by @local_property @@ -483,7 +483,7 @@ settings easy. Don't set this attribute. Set .vcs instead, and if self.sync_with_disk == False: raise DiskAccessRequired("load all bugs") self._clear_bugs() - for uuid in self.list_uuids(): + for uuid in self.uuids(): self._load_bug(uuid) def save(self): @@ -550,7 +550,7 @@ settings easy. Don't set this attribute. Set .vcs instead, and # methods for managing bugs - def list_uuids(self): + def uuids(self): uuids = [] if self.sync_with_disk == True and os.path.exists(self.get_path()): # list the uuids on disk @@ -651,7 +651,7 @@ class SimpleBugDir (BugDir): """ For testing. Set sync_with_disk==False for a memory-only bugdir. >>> bugdir = SimpleBugDir() - >>> uuids = list(bugdir.list_uuids()) + >>> uuids = list(bugdir.uuids()) >>> uuids.sort() >>> print uuids ['a', 'b'] @@ -741,7 +741,7 @@ class BugDirTestCase(unittest.TestCase): self.bugdir.new_bug(uuid="c", summary="Praying mantis") length = len(self.bugdir) self.failUnless(length == 3, "%d != 3 bugs" % length) - uuids = list(self.bugdir.list_uuids()) + uuids = list(self.bugdir.uuids()) self.failUnless(len(uuids) == 3, "%d != 3 uuids" % len(uuids)) self.failUnless(uuids == ["a","b","c"], str(uuids)) bugA = self.bugdir.bug_from_uuid("a") diff --git a/libbe/diff.py b/libbe/diff.py index cce3b0f..6e830c6 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -207,7 +207,7 @@ class Diff (object): added = [] removed = [] modified = [] - for uuid in self.new_bugdir.list_uuids(): + for uuid in self.new_bugdir.uuids(): new_bug = self.new_bugdir.bug_from_uuid(uuid) try: old_bug = self.old_bugdir.bug_from_uuid(uuid) @@ -220,7 +220,7 @@ class Diff (object): new_bug.load_comments() if old_bug != new_bug: modified.append((old_bug, new_bug)) - for uuid in self.old_bugdir.list_uuids(): + for uuid in self.old_bugdir.uuids(): if not self.new_bugdir.has_bug(uuid): old_bug = self.old_bugdir.bug_from_uuid(uuid) removed.append(old_bug) -- cgit From 7e95956f5088346807a233c63f5bc25436550ef8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 28 Nov 2009 20:27:53 -0500 Subject: test.py now uses unittest.TestSuite 'suite' in becommands if present. Such 'suite' instances have been required for libbe submodules. This will allow becommands to test themselves more thoroughly than they could with only doctests. --- test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test.py b/test.py index 57091c7..81674cf 100644 --- a/test.py +++ b/test.py @@ -27,7 +27,10 @@ if len(sys.argv) > 1: print "Module \"%s\" has no test suite" % submodname mod = plugin.get_plugin("becommands", submodname) if mod is not None: - suite.addTest(doctest.DocTestSuite(mod)) + if hasattr(mod, "suite"): + suite.addTest(mod.suite) + else: + suite.addTest(doctest.DocTestSuite(mod)) match = True if not match: print "No modules match \"%s\"" % submodname -- cgit From 832843d26eed9023f4cf4fc431527c63ca1d533d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 28 Nov 2009 22:07:16 -0500 Subject: Added comment import to Bug.from_xml(). This is a pretty critical feature, dunno how I missed it before. I also added a little check to both Bug and Comment.from_xml() so that xml_string can take an ElementTree Element as well as the usual raw string/unicode. This avoids repeated string <-> Element conversions. Added Bug.add_comment() which handles the addition of a Comment instance, matching .in_reply_to, checking .uuid uniqueness, etc. --- libbe/bug.py | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++------ libbe/comment.py | 5 ++- 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index 23d5488..5f2cf54 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -325,32 +325,44 @@ class Bug(settings_object.SavedSettingsObject): >>> commA = bugA.comment_root.new_reply(body='comment A') >>> commB = bugA.comment_root.new_reply(body='comment B') >>> commC = commA.new_reply(body='comment C') - >>> xml = bugA.xml(shortname="bug-1") + >>> xml = bugA.xml(shortname="bug-1", show_comments=True) >>> bugB = Bug() >>> bugB.from_xml(xml, verbose=True) - >>> bugB.xml(shortname="bug-1") == xml + >>> bugB.xml(shortname="bug-1", show_comments=True) == xml False >>> bugB.uuid = bugB.alt_id - >>> bugB.xml(shortname="bug-1") == xml + >>> for comm in bugB.comments(): + ... comm.uuid = comm.alt_id + ... comm.alt_id = None + >>> bugB.xml(shortname="bug-1", show_comments=True) == xml True >>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE - ['uuid', 'severity', 'status', 'creator', 'created', 'summary'] + ['severity', 'status', 'creator', 'created', 'summary'] + >>> len(list(bugB.comments())) + 3 """ if type(xml_string) == types.UnicodeType: xml_string = xml_string.strip().encode('unicode_escape') - bug = ElementTree.XML(xml_string) + if hasattr(xml_string, 'getchildren'): # already an ElementTree Element + bug = xml_string + else: + bug = ElementTree.XML(xml_string) if bug.tag != 'bug': raise utility.InvalidXML( \ 'bug', bug, 'root element must be ') tags=['uuid','short-name','severity','status','assigned','target', - 'reporter', 'creator','created','summary','extra-string', - 'comment'] + 'reporter', 'creator','created','summary','extra-string'] self.explicit_attrs = [] uuid = None estrs = [] for child in bug.getchildren(): if child.tag == 'short-name': pass + elif child.tag == 'comment': + comm = comment.Comment(bug=self) + comm.from_xml(child) + self.add_comment(comm) + continue elif child.tag in tags: if child.text == None or len(child.text) == 0: text = settings_object.EMPTY @@ -374,6 +386,75 @@ class Bug(settings_object.SavedSettingsObject): self.alt_id = uuid self.extra_strings = estrs + def add_comment(self, new_comment): + """ + Add a comment too the current bug, under the parent specified + by comment.in_reply_to. + Note: If a bug uuid is given, set .alt_id to it's value. + >>> bugA = Bug(uuid='0123', summary='Need to test Bug.add_comment()') + >>> bugA.creator = 'Jack' + >>> commA = bugA.comment_root.new_reply(body='comment A') + >>> commA.uuid = 'commA' + >>> commB = comment.Comment(body='comment B') + >>> commB.uuid = 'commB' + >>> bugA.add_comment(commB) + >>> commC = comment.Comment(body='comment C') + >>> commC.uuid = 'commC' + >>> commC.in_reply_to = commA.uuid + >>> bugA.add_comment(commC) + >>> print bugA.xml(shortname="bug-1", show_comments=True) # doctest: +ELLIPSIS + + 0123 + bug-1 + minor + open + Jack + ... + Need to test Bug.add_comment() + + commA + bug-1:1 + + ... + text/plain + comment A + + + commC + bug-1:2 + commA + + ... + text/plain + comment C + + + commB + bug-1:3 + + ... + text/plain + comment B + + + """ + uuid_map = {} + for c in self.comments(): + uuid_map[c.uuid] = c + if c.alt_id != None: + uuid_map[c.alt_id] = c + assert new_comment.uuid not in uuid_map + if new_comment.alt_id != None: + assert new_comment.alt_id not in uuid_map + if new_comment.in_reply_to == comment.INVALID_UUID: + new_comment.in_reply_to = None + if new_comment.in_reply_to == None: + parent = self.comment_root + else: + parent = uuid_map[new_comment.in_reply_to] + new_comment.bug = self + parent.append(new_comment) + def merge(self, other, allow_changes=True, allow_new_comments=True): """ Merge info from other into this bug. Overrides any attributes @@ -450,14 +531,13 @@ class Bug(settings_object.SavedSettingsObject): self.extra_strings.append(estr) import sys for o_comm in other.comments(): - s_comm = None try: s_comm = self.comment_root.comment_from_uuid(o_comm.uuid) except KeyError, e: try: s_comm = self.comment_root.comment_from_uuid(o_comm.alt_id) except KeyError, e: - pass + s_comm = None if s_comm == None: if allow_new_comments == False: raise ValueError, \ diff --git a/libbe/comment.py b/libbe/comment.py index 1adb6f4..e3dfea0 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -373,7 +373,10 @@ class Comment(Tree, settings_object.SavedSettingsObject): """ if type(xml_string) == types.UnicodeType: xml_string = xml_string.strip().encode('unicode_escape') - comment = ElementTree.XML(xml_string) + if hasattr(xml_string, 'getchildren'): # already an ElementTree Element + comment = xml_string + else: + comment = ElementTree.XML(xml_string) if comment.tag != 'comment': raise utility.InvalidXML( \ 'comment', comment, 'root element must be ') -- cgit From 759c69d8c8a4bbd7ba9c42bb3a813cd0d06a52b7 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 29 Nov 2009 03:19:30 -0500 Subject: Moved comment.list_to_root() to Bug.add_comments() with some cleanups. This makes Bug.add_comment simpler. Also makes Bug.from_xml() more robust, since it no longer depends on the order in which the XML file lists the comments. The previous Bug.from_xml() would have choked on B A A because when B was being added, the referenced A hadn't yet been noticed. --- libbe/bug.py | 59 ++++++++++++++++++++++++++++++++++++++++---------------- libbe/comment.py | 51 +++--------------------------------------------- 2 files changed, 45 insertions(+), 65 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index 5f2cf54..d3dbe2d 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -228,7 +228,7 @@ class Bug(settings_object.SavedSettingsObject): @Property @cached_property(generator=_get_comment_root) @local_property("comment_root") - @doc_property(doc="The trunk of the comment tree") + @doc_property(doc="The trunk of the comment tree. We use a dummy root comment by default, because there can be several comment threads rooted on the same parent bug. To simplify comment interaction, we condense these threads into a single thread with a Comment dummy root.") def comment_root(): return {} def _get_vcs(self): @@ -355,13 +355,14 @@ class Bug(settings_object.SavedSettingsObject): self.explicit_attrs = [] uuid = None estrs = [] + comments = [] for child in bug.getchildren(): if child.tag == 'short-name': pass elif child.tag == 'comment': comm = comment.Comment(bug=self) comm.from_xml(child) - self.add_comment(comm) + comments.append(comm) continue elif child.tag in tags: if child.text == None or len(child.text) == 0: @@ -385,8 +386,9 @@ class Bug(settings_object.SavedSettingsObject): if not hasattr(self, 'alt_id') or self.alt_id == None: self.alt_id = uuid self.extra_strings = estrs + self.add_comments(comments) - def add_comment(self, new_comment): + def add_comment(self, comment, *args, **kwargs): """ Add a comment too the current bug, under the parent specified by comment.in_reply_to. @@ -438,22 +440,47 @@ class Bug(settings_object.SavedSettingsObject): """ + self.add_comments([comment], **kwargs) + + def add_comments(self, comments, default_parent=None, + ignore_missing_references=False): + """ + Convert a raw list of comments to single root comment. If a + comment does not specify a parent with .in_reply_to, the + parent defaults to .comment_root, but you can specify another + default parent via default_parent. + """ uuid_map = {} - for c in self.comments(): + if default_parent == None: + default_parent = self.comment_root + for c in list(self.comments()) + comments: + assert c.uuid != None + assert c.uuid not in uuid_map uuid_map[c.uuid] = c if c.alt_id != None: uuid_map[c.alt_id] = c - assert new_comment.uuid not in uuid_map - if new_comment.alt_id != None: - assert new_comment.alt_id not in uuid_map - if new_comment.in_reply_to == comment.INVALID_UUID: - new_comment.in_reply_to = None - if new_comment.in_reply_to == None: - parent = self.comment_root - else: - parent = uuid_map[new_comment.in_reply_to] - new_comment.bug = self - parent.append(new_comment) + uuid_map[None] = self.comment_root + if default_parent != self.comment_root: + assert default_parent.uuid in uuid_map, default_parent + for c in comments: + if c.in_reply_to == None \ + and default_parent.uuid != comment.INVALID_UUID: + c.in_reply_to = default_parent.uuid + elif c.in_reply_to == comment.INVALID_UUID: + c.in_reply_to = None + try: + parent = uuid_map[c.in_reply_to] + except KeyError: + if ignore_missing_references == True: + print >> sys.stderr, \ + "Ignoring missing reference to %s" % c.in_reply_to + parent = default_parent + if parent.uuid != comment.INVALID_UUID: + c.in_reply_to = parent.uuid + else: + raise comment.MissingReference(c) + c.bug = self + parent.append(c) def merge(self, other, allow_changes=True, allow_new_comments=True): """ @@ -545,10 +572,8 @@ class Bug(settings_object.SavedSettingsObject): % (o_comm.uuid, o_comm.alt_id, self.uuid) o_comm_copy = copy.copy(o_comm) o_comm_copy.bug = self - print >> sys.stderr, "add comment %s" % o_comm.uuid self.comment_root.add_reply(o_comm_copy) else: - print >> sys.stderr, "merge comment %s into %s" % (o_comm.uuid, s_comm.uuid) s_comm.merge(o_comm, allow_changes=allow_changes) def string(self, shortlist=False, show_comments=False): diff --git a/libbe/comment.py b/libbe/comment.py index e3dfea0..45134e0 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -65,53 +65,6 @@ class DiskAccessRequired (Exception): INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!" -def list_to_root(comments, bug, root=None, - ignore_missing_references=False): - """ - Convert a raw list of comments to single root comment. We use a - dummy root comment by default, because there can be several - comment threads rooted on the same parent bug. To simplify - comment interaction, we condense these threads into a single - thread with a Comment dummy root. Can also be used to append - a list of subcomments to a non-dummy root comment, so long as - all the new comments are descendants of the root comment. - - No Comment method should use the dummy comment. - """ - root_comments = [] - uuid_map = {} - for comment in comments: - assert comment.uuid != None - uuid_map[comment.uuid] = comment - for comment in comments: - if comment.alt_id != None and comment.alt_id not in uuid_map: - uuid_map[comment.alt_id] = comment - if root == None: - root = Comment(bug, uuid=INVALID_UUID) - else: - uuid_map[root.uuid] = root - for comm in comments: - if comm.in_reply_to == INVALID_UUID: - comm.in_reply_to = None - rep = comm.in_reply_to - if rep == None or rep == bug.uuid: - root_comments.append(comm) - else: - parentUUID = comm.in_reply_to - try: - parent = uuid_map[parentUUID] - parent.add_reply(comm) - except KeyError, e: - if ignore_missing_references == True: - print >> sys.stderr, \ - "Ignoring missing reference to %s" % parentUUID - comm.in_reply_to = None - root_comments.append(comm) - else: - raise MissingReference(comm) - root.extend(root_comments) - return root - def loadComments(bug, load_full=False): """ Set load_full=True when you want to load the comment completely @@ -132,7 +85,9 @@ def loadComments(bug, load_full=False): comm.load_settings() dummy = comm.body # force the body to load comments.append(comm) - return list_to_root(comments, bug) + bug.comment_root = Comment(bug, uuid=INVALID_UUID) + bug.add_comments(comments) + return bug.comment_root def saveComments(bug): if bug.sync_with_disk == False: -- cgit From ff2475e98fe1726f8b1af1b3dc30746cc4e01071 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 05:05:49 -0500 Subject: Minor BugDir docstring typo correction. --- libbe/bugdir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 3d07754..301ceb6 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -115,7 +115,7 @@ class BugDir (list, settings_object.SavedSettingsObject): all bugs/comments/etc. that have been loaded into memory. If you've been living in memory and want to move to .sync_with_disk==True, but you're not sure if anything has been - changed in memory, a call to save() immediately before the + changed in memory, a call to .save() immediately before the .set_sync_with_disk(True) call is a safe move. Regardless of .sync_with_disk, a call to .save() will write out -- cgit From 19cea054def7997bb13ecc77269b7b612f658d16 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:05:03 -0500 Subject: Changed Bug and Comment.merge() kwargs. The old allow_changes and allow_new_comments didn't have separate handling for extra_strings, like import_xml will need. It also didn't have a way to specify what to do if an illegal change occurs. Sometimes you'll want to raise an exception, but sometimes you'll want to ?silently? ignore the change. --- libbe/bug.py | 58 ++++++++++++++++++++++++++++++++++++++++---------------- libbe/comment.py | 36 ++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index d3dbe2d..897d841 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -482,7 +482,9 @@ class Bug(settings_object.SavedSettingsObject): c.bug = self parent.append(c) - def merge(self, other, allow_changes=True, allow_new_comments=True): + def merge(self, other, accept_changes=True, + accept_extra_strings=True, accept_comments=True, + change_exception=False): """ Merge info from other into this bug. Overrides any attributes in self that are listed in other.explicit_attrs. @@ -501,15 +503,35 @@ class Bug(settings_object.SavedSettingsObject): >>> bugB.extra_strings += ['TAG: useful'] >>> commB = bugB.comment_root.new_reply(body='comment B') >>> commB.uuid = 'uuid-commB' - >>> bugA.merge(bugB, allow_changes=False) + >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False, + ... accept_comments=False, change_exception=False) + >>> print bugA.creator + Frank + >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False, + ... accept_comments=False, change_exception=True) Traceback (most recent call last): ... ValueError: Merge would change creator "Frank"->"John" for bug 0123 - >>> bugA.merge(bugB, allow_new_comments=False) + >>> print bugA.creator + Frank + >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=False, + ... accept_comments=False, change_exception=True) + Traceback (most recent call last): + ... + ValueError: Merge would add extra string "TAG: useful" for bug 0123 + >>> print bugA.creator + John + >>> print bugA.extra_strings + ['TAG: favorite', 'TAG: very helpful'] + >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True, + ... accept_comments=False, change_exception=True) Traceback (most recent call last): ... ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123 - >>> bugA.merge(bugB) + >>> print bugA.extra_strings + ['TAG: favorite', 'TAG: useful', 'TAG: very helpful'] + >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True, + ... accept_comments=True, change_exception=True) >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS 0123 @@ -544,19 +566,20 @@ class Bug(settings_object.SavedSettingsObject): old = getattr(self, attr) new = getattr(other, attr) if old != new: - if allow_changes == True: + if accept_changes == True: setattr(self, attr, new) - else: + elif change_exception == True: raise ValueError, \ 'Merge would change %s "%s"->"%s" for bug %s' \ % (attr, old, new, self.uuid) - if allow_changes == False and len(other.extra_strings) > 0: - raise ValueError, \ - 'Merge would change extra_strings for bug %s' % self.uuid for estr in other.extra_strings: if not estr in self.extra_strings: - self.extra_strings.append(estr) - import sys + if accept_extra_strings == True: + self.extra_strings.append(estr) + elif change_exception == True: + raise ValueError, \ + 'Merge would add extra string "%s" for bug %s' \ + % (estr, self.uuid) for o_comm in other.comments(): try: s_comm = self.comment_root.comment_from_uuid(o_comm.uuid) @@ -566,15 +589,18 @@ class Bug(settings_object.SavedSettingsObject): except KeyError, e: s_comm = None if s_comm == None: - if allow_new_comments == False: + if accept_comments == True: + o_comm_copy = copy.copy(o_comm) + o_comm_copy.bug = self + self.comment_root.add_reply(o_comm_copy) + elif change_exception == True: raise ValueError, \ 'Merge would add comment %s (alt: %s) to bug %s' \ % (o_comm.uuid, o_comm.alt_id, self.uuid) - o_comm_copy = copy.copy(o_comm) - o_comm_copy.bug = self - self.comment_root.add_reply(o_comm_copy) else: - s_comm.merge(o_comm, allow_changes=allow_changes) + s_comm.merge(o_comm, accept_changes=accept_changes, + accept_extra_strings=accept_extra_strings, + change_exception=change_exception) def string(self, shortlist=False, show_comments=False): if self.bugdir == None: diff --git a/libbe/comment.py b/libbe/comment.py index 45134e0..9502adf 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -376,7 +376,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): self.body = base64.decodestring(body) self.extra_strings = estrs - def merge(self, other, allow_changes=True): + def merge(self, other, accept_changes=True, + accept_extra_strings=True, change_exception=False): """ Merge info from other into this comment. Overrides any attributes in self that are listed in other.explicit_attrs. @@ -393,11 +394,26 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> commB.explicit_attrs = ['author', 'body'] >>> commB.extra_strings += ['TAG: very helpful'] >>> commB.extra_strings += ['TAG: useful'] - >>> commA.merge(commB, allow_changes=False) + >>> commA.merge(commB, accept_changes=False, + ... accept_extra_strings=False, change_exception=False) + >>> commA.merge(commB, accept_changes=False, + ... accept_extra_strings=False, change_exception=True) Traceback (most recent call last): ... ValueError: Merge would change author "Frank"->"John" for comment 0123 - >>> commA.merge(commB) + >>> commA.merge(commB, accept_changes=True, + ... accept_extra_strings=False, change_exception=True) + Traceback (most recent call last): + ... + ValueError: Merge would add extra string "TAG: useful" to comment 0123 + >>> print commA.author + John + >>> print commA.extra_strings + ['TAG: favorite', 'TAG: very helpful'] + >>> commA.merge(commB, accept_changes=True, + ... accept_extra_strings=True, change_exception=True) + >>> print commA.extra_strings + ['TAG: favorite', 'TAG: useful', 'TAG: very helpful'] >>> print commA.xml() 0123 @@ -415,18 +431,20 @@ class Comment(Tree, settings_object.SavedSettingsObject): old = getattr(self, attr) new = getattr(other, attr) if old != new: - if allow_changes == True: + if accept_changes == True: setattr(self, attr, new) - else: + elif change_exception == True: raise ValueError, \ 'Merge would change %s "%s"->"%s" for comment %s' \ % (attr, old, new, self.uuid) - if allow_changes == False and len(other.extra_strings) > 0: - raise ValueError, \ - 'Merge would change extra_strings for comment %s' % self.uuid for estr in other.extra_strings: if not estr in self.extra_strings: - self.extra_strings.append(estr) + if accept_extra_strings == True: + self.extra_strings.append(estr) + elif change_exception == True: + raise ValueError, \ + 'Merge would add extra string "%s" to comment %s' \ + % (estr, self.uuid) def string(self, indent=0, shortname=None): """ -- cgit From c1fc4595171fa6eec802eb65a0fde0b53878a077 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:26:49 -0500 Subject: Avoid redundant Comment.alt_ids --- libbe/comment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libbe/comment.py b/libbe/comment.py index 9502adf..c5f1cc9 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -366,7 +366,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): elif verbose == True: print >> sys.stderr, 'Ignoring unknown tag %s in %s' \ % (child.tag, comment.tag) - if self.alt_id == None: + if uuid != self.uuid and self.alt_id == None: self.explicit_attrs.append('alt_id') self.alt_id = uuid if body != None: @@ -437,6 +437,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): raise ValueError, \ 'Merge would change %s "%s"->"%s" for comment %s' \ % (attr, old, new, self.uuid) + if self.alt_id == self.uuid: + self.alt_id = None for estr in other.extra_strings: if not estr in self.extra_strings: if accept_extra_strings == True: -- cgit From c903b668c13413f380f99720c01162b7fe90ec6d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:35:29 -0500 Subject: Fixed import_xml.py to live up to its longhelp description. Also added LonghelpTestCase to prove it. --- becommands/import_xml.py | 215 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 188 insertions(+), 27 deletions(-) diff --git a/becommands/import_xml.py b/becommands/import_xml.py index a74d329..892a09e 100644 --- a/becommands/import_xml.py +++ b/becommands/import_xml.py @@ -16,12 +16,15 @@ """Import comments and bugs from XML""" from libbe import cmdutil, bugdir, bug, comment, utility from becommands.comment import complete +import copy import os import sys try: # import core module, Python >= 2.5 from xml.etree import ElementTree except ImportError: # look for non-core module from elementtree import ElementTree +import doctest +import unittest __desc__ = __doc__ def execute(args, manipulate_encodings=True, restrict_file_access=False): @@ -63,6 +66,22 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if options.comment_root != None: croot_bug,croot_comment = \ cmdutil.bug_comment_from_id(bd, options.comment_root) + croot_bug.load_comments(load_full=True) + croot_bug.set_sync_with_disk(False) + if croot_comment.uuid == comment.INVALID_UUID: + croot_comment = croot_bug.comment_root + else: + croot_comment = croot_bug.comment_from_uuid(croot_comment.uuid) + new_croot_bug = bug.Bug(bugdir=bd, uuid=croot_bug.uuid) + new_croot_bug.explicit_attrs = [] + new_croot_bug.comment_root = copy.deepcopy(croot_bug.comment_root) + if croot_comment.uuid == comment.INVALID_UUID: + new_croot_comment = new_croot_bug.comment_root + else: + new_croot_comment = \ + new_croot_bug.comment_from_uuid(croot_comment.uuid) + for new in new_croot_bug.comments(): + new.explicit_attrs = [] else: croot_bug,croot_comment = (None, None) @@ -87,7 +106,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): for child in be_xml.getchildren(): if child.tag == 'bug': new = bug.Bug(bugdir=bd) - new.from_xml(unicode(ElementTree.tostring(child)).decode('unicode_escape')) + new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape")) root_bugs.append(new) elif child.tag == 'comment': new = comment.Comment(croot_bug) @@ -107,35 +126,47 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): % (child.tag, comment_list.tag) # merge the new root_comments + if options.add_only == True: + accept_changes = False + accept_extra_strings = False + else: + accept_changes = True + accept_extra_strings = True + accept_comments = True if len(root_comments) > 0: if croot_bug == None: raise UserError( '--comment-root option is required for your root comments:\n%s' % '\n\n'.join([c.string() for c in root_comments])) - croot_cids = [] - for c in croot_bug.comment_root.traverse(): - croot_cids.append(c.uuid) - if c.alt_id != None: - croot_cids.append(c.alt_id) - for new in root_comments: - if new.alt_id in croot_cids: - raise cmdutil.UserError( - 'clashing comment alt_id: %s' % new.alt_id) - croot_cids.append(new.uuid) - if new.alt_id != None: - croot_cids.append(new.alt_id) - if new.in_reply_to == None: - new.in_reply_to = croot_comment.uuid try: # link new comments - comment.list_to_root(root_comments,croot_bug,root=croot_comment, - ignore_missing_references= \ - options.ignore_missing_references) + new_croot_bug.add_comments(root_comments, + default_parent=new_croot_comment, + ignore_missing_references= \ + options.ignore_missing_references) except comment.MissingReference, e: raise cmdutil.UserError(e) + croot_bug.merge(new_croot_bug, accept_changes=accept_changes, + accept_extra_strings=accept_extra_strings, + accept_comments=accept_comments) + # merge the new croot_bugs + merged_bugs = [] + old_bugs = [] for new in root_bugs: - bd.append(new) + try: + old = bd.bug_from_uuid(new.alt_id) + except KeyError: + old = None + if old == None: + bd.append(new) + else: + old.load_comments(load_full=True) + old.merge(new, accept_changes=accept_changes, + accept_extra_strings=accept_extra_strings, + accept_comments=accept_comments) + merged_bugs.append(new) + old_bugs.append(old) # protect against programmer error causing data loss: if croot_bug != None: @@ -144,14 +175,18 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): assert new.uuid in comms, \ "comment %s wasn't added to %s" % (new.uuid, croot_comment.uuid) for new in root_bugs: - assert bd.has_bug(new.uuid), \ - "bug %s wasn't added" % (new.uuid) + if not new in merged_bugs: + assert bd.has_bug(new.uuid), \ + "bug %s wasn't added" % (new.uuid) # save new information - for new in root_comments: - new.save() + if croot_bug != None: + croot_bug.save() for new in root_bugs: - new.save() + if not new in merged_bugs: + new.save() + for old in old_bugs: + old.save() def get_parser(): parser = cmdutil.CmdOptionParser("be import-xml XMLFILE") @@ -213,7 +248,7 @@ repeats. Here's an example of import activity: Repository - bug (uuid=B, author=John, status=open) + bug (uuid=B, creator=John, status=open) estr (don't forget your towel) estr (helps with space travel) com (uuid=C1, author=Jane, body=Hello) @@ -225,7 +260,7 @@ Here's an example of import activity: com (uuid=C1, body=So long) com (uuid=C3, author=Jed, body=And thanks) Result - bug (uuid=B, author=John, status=fixed) + bug (uuid=B, creator=John, status=fixed) estr (don't forget your towel) estr (helps with space travel) estr (watch out for flying dolphins) @@ -233,7 +268,7 @@ Here's an example of import activity: com (uuid=C2, author=Jess, body=World) com (uuid=C4, alt-id=C3, author=Jed, body=And thanks) Result, with --add-only - bug (uuid=B, author=John, status=open) + bug (uuid=B, creator=John, status=open) estr (don't forget your towel) estr (helps with space travel) com (uuid=C1, author=Jane, body=Hello) @@ -265,3 +300,129 @@ Devs recieve email, and save it's contents as demux-bug.xml def help(): return get_parser().help_str() + longhelp + + +class LonghelpTestCase (unittest.TestCase): + """ + Test import scenarios given in longhelp. + """ + def setUp(self): + self.bugdir = bugdir.SimpleBugDir() + self.original_working_dir = os.getcwd() + os.chdir(self.bugdir.root) + bugA = self.bugdir.bug_from_uuid('a') + self.bugdir.remove_bug(bugA) + self.bugdir.set_sync_with_disk(False) + bugB = self.bugdir.bug_from_uuid('b') + bugB.creator = 'John' + bugB.status = 'open' + bugB.extra_strings += ["don't forget your towel"] + bugB.extra_strings += ['helps with space travel'] + comm1 = bugB.comment_root.new_reply(body='Hello\n') + comm1.uuid = 'c1' + comm1.author = 'Jane' + comm2 = bugB.comment_root.new_reply(body='World\n') + comm2.uuid = 'c2' + comm2.author = 'Jess' + bugB.save() + self.bugdir.set_sync_with_disk(True) + self.xml = """ + + + b + fixed + a test bug + don't forget your towel + watch out for flying dolphins + + c1 + So long + + + c3 + Jed + And thanks + + + + """ + def tearDown(self): + os.chdir(self.original_working_dir) + self.bugdir.cleanup() + def _execute(self, *args): + import StringIO + orig_stdin = sys.stdin + sys.stdin = StringIO.StringIO(self.xml) + execute(list(args)+["-"], manipulate_encodings=False, + restrict_file_access=True) + sys.stdin = orig_stdin + self.bugdir._clear_bugs() + def testCleanBugdir(self): + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + def testNotAddOnly(self): + self._execute() + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + bugB = self.bugdir.bug_from_uuid('b') + self.failUnless(bugB.uuid == 'b', bugB.uuid) + self.failUnless(bugB.creator == 'John', bugB.creator) + self.failUnless(bugB.status == 'fixed', bugB.status) + estrs = ["don't forget your towel", + 'helps with space travel', + 'watch out for flying dolphins'] + self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) + comments = list(bugB.comments()) + self.failUnless(len(comments) == 3, + ['%s (%s, %s)' % (c.uuid, c.alt_id, c.body) for c in comments]) + c1 = bugB.comment_from_uuid('c1') + comments.remove(c1) + self.failUnless(c1.uuid == 'c1', c1.uuid) + self.failUnless(c1.alt_id == None, c1.alt_id) + self.failUnless(c1.author == 'Jane', c1.author) + self.failUnless(c1.body == 'So long\n', c1.body) + c2 = bugB.comment_from_uuid('c2') + comments.remove(c2) + self.failUnless(c2.uuid == 'c2', c2.uuid) + self.failUnless(c2.alt_id == None, c2.alt_id) + self.failUnless(c2.author == 'Jess', c2.author) + self.failUnless(c2.body == 'World\n', c2.body) + c4 = comments[0] + self.failUnless(len(c4.uuid) == 36, c4.uuid) + self.failUnless(c4.alt_id == 'c3', c4.alt_id) + self.failUnless(c4.author == 'Jed', c4.author) + self.failUnless(c4.body == 'And thanks\n', c4.body) + def testAddOnly(self): + self._execute('--add-only') + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + bugB = self.bugdir.bug_from_uuid('b') + self.failUnless(bugB.uuid == 'b', bugB.uuid) + self.failUnless(bugB.creator == 'John', bugB.creator) + self.failUnless(bugB.status == 'open', bugB.status) + estrs = ["don't forget your towel", + 'helps with space travel'] + self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) + comments = list(bugB.comments()) + self.failUnless(len(comments) == 3, + ['%s (%s)' % (c.uuid, c.alt_id) for c in comments]) + c1 = bugB.comment_from_uuid('c1') + comments.remove(c1) + self.failUnless(c1.uuid == 'c1', c1.uuid) + self.failUnless(c1.alt_id == None, c1.alt_id) + self.failUnless(c1.author == 'Jane', c1.author) + self.failUnless(c1.body == 'Hello\n', c1.body) + c2 = bugB.comment_from_uuid('c2') + comments.remove(c2) + self.failUnless(c2.uuid == 'c2', c2.uuid) + self.failUnless(c2.alt_id == None, c2.alt_id) + self.failUnless(c2.author == 'Jess', c2.author) + self.failUnless(c2.body == 'World\n', c2.body) + c4 = comments[0] + self.failUnless(len(c4.uuid) == 36, c4.uuid) + self.failUnless(c4.alt_id == 'c3', c4.alt_id) + self.failUnless(c4.author == 'Jed', c4.author) + self.failUnless(c4.body == 'And thanks\n', c4.body) + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) -- cgit From ef748431936015a9d914255db0dd20f2f8e1c426 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:36:38 -0500 Subject: Added some thoughts on import_xml merge algorithms --- .../4068c833-0c06-475e-8b7e-6701bc416dee/body | 28 ++++++++++++++++++++++ .../4068c833-0c06-475e-8b7e-6701bc416dee/values | 11 +++++++++ 2 files changed, 39 insertions(+) create mode 100644 .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body create mode 100644 .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body new file mode 100644 index 0000000..d3d9d0c --- /dev/null +++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/body @@ -0,0 +1,28 @@ +> With interfaces/email/interactive listening on the recieving end to +> grab new-bug emails and import them into an incoming bug repository. + +The email-bugs -> be-handle-mail import is based on `be import-xml`. +The current import-xml implementation allows good control over what +gets overwritten during a merge by overriding only those fields +defined in the incoming XML. + +For clients without the versioned bugdir (e.g. they installed via a +release tarball or their distro's packaging system), `be email-bugs` +will not know what fields have been changed/added/etc., so it sets +_all_ the fields in the outgoing XML. Importing that XML file will +override any changes that may have been made to the listed +bugs/comments between the release and your current source version, so +you may have to do some manual tweaking of the post-merge bugdir. + +One possible workaround would be to change the merge algorithm in +import-xml to take advantage of version information given in the XML +file. import-xml could checkout the shared root version of any +modified bugs, and compute the changes made by the remote user and +those made in the local tree. It could then merge these changes more +intelligently, by prompting the user, keeping the local changes, +keeping the remote changes, etc. + +While the more automated approach might be better, it's also more +complicated, so for now we'll stick with the simple "override all +fields defined in the XML" approach. + diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values new file mode 100644 index 0000000..e77ec55 --- /dev/null +++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee/values @@ -0,0 +1,11 @@ +Author: W. Trevor King + + +Content-type: text/plain + + +Date: Sun, 29 Nov 2009 01:19:05 +0000 + + +In-reply-to: 0a995544-20dc-42a6-8d3f-348ebbc8921e + -- cgit From 8773e0e95758539addbfbcd11e5f37a2648c6e58 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:26:09 -0500 Subject: Generalized doctest string in subproc.py --- libbe/subproc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libbe/subproc.py b/libbe/subproc.py index 3e58271..fe88206 100644 --- a/libbe/subproc.py +++ b/libbe/subproc.py @@ -96,7 +96,7 @@ class Pipe (object): >>> p.statuses [1, 0] >>> p.stderrs # doctest: +ELLIPSIS - ["find: `...': Permission denied\\n...", ''] + [...find: ...: Permission denied..., ''] """ def __init__(self, cmds, stdin=None): # spawn processes -- cgit From 2156616c3dab8207a933295bfbc9d125dac4bc34 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:28:01 -0500 Subject: be-handle-mail uses more conservative --add-only for be-bugs:xml --- interfaces/email/interactive/be-handle-mail | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index e0e3490..3b321cf 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -598,7 +598,7 @@ class Message (object): raise InvalidEmail(self, u"Emails to %s must have MIME type 'text/xml', not '%s'." % (SUBJECT_TAG_XML, mime_type)) - args = [u"-"] + args = [u"--add-only", u"-"] commands = [Command(self, command, args, stdin=body)] return commands def run(self): -- cgit From 13784e6067b652e4fe08e488fdc4baabc37f24ef Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:29:04 -0500 Subject: Mark bug 565 as fixed: be email-bugs for bug submission from bzr-less users --- .be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values index 2e15ca9..2d546cb 100644 --- a/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values +++ b/.be/bugs/56506b73-36cc-4e32-a578-258a219edba8/values @@ -7,7 +7,7 @@ reporter: W. Trevor King severity: minor -status: open +status: fixed summary: be email-bugs for bug submission from bzr-less users -- cgit From dc5ac53feb42841c48fe0a17a699aa96f5bb39c8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 3 Dec 2009 20:16:56 -0500 Subject: Remove _test() functions from plugin.py and cmdutil.py. Testing should be handled through test.py, not by calling per-module _test() functions. --- libbe/cmdutil.py | 8 -------- libbe/plugin.py | 7 ------- 2 files changed, 15 deletions(-) diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index e37750d..78645ab 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -293,12 +293,4 @@ def bug_comment_from_id(bdir, id): raise UserError(e.message) return (bug, comm) -def _test(): - import doctest - import sys - doctest.testmod() - -if __name__ == "__main__": - _test() - suite = doctest.DocTestSuite() diff --git a/libbe/plugin.py b/libbe/plugin.py index bd9bb65..e6b06fb 100644 --- a/libbe/plugin.py +++ b/libbe/plugin.py @@ -69,10 +69,3 @@ if plugin_path not in sys.path: sys.path.append(plugin_path) suite = doctest.DocTestSuite() - -def _test(): - import doctest - doctest.testmod() - -if __name__ == "__main__": - _test() -- cgit From 193bb7dc4fdb02ec1e79dcfacecdb4c352549a15 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 3 Dec 2009 20:38:58 -0500 Subject: Added copyright blurbs to __init__.py files. --- becommands/__init__.py | 16 ++++++++++++++++ libbe/__init__.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/becommands/__init__.py b/becommands/__init__.py index e69de29..794013c 100644 --- a/becommands/__init__.py +++ b/becommands/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. diff --git a/libbe/__init__.py b/libbe/__init__.py index e69de29..794013c 100644 --- a/libbe/__init__.py +++ b/libbe/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -- cgit From 2ba535acb1f03fb7d1bdb57e4173d55661d300da Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 3 Dec 2009 21:19:54 -0500 Subject: Added libbe.TESTING (defaults to False). This flag allows us to skip unittest and testsuite declaration if we woln't need them. It speeds up simple be calls a suprising amount. With Testing=True (the old behavior): wking@thor:be.wtk$ time ./be > /dev/null real 0m0.393s user 0m0.340s sys 0m0.048s With TESTING=False (the new behavior): be.wtk$ time ./be > /dev/null real 0m0.216s user 0m0.152s sys 0m0.064s This adjustment was inspired by Jakub Wilk's Debian bug: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=559295 --- becommands/import_xml.py | 252 ++++++++++----------- libbe/__init__.py | 5 + libbe/arch.py | 14 +- libbe/beuuid.py | 15 +- libbe/bug.py | 7 +- libbe/bugdir.py | 299 +++++++++++++------------ libbe/bzr.py | 11 +- libbe/cmdutil.py | 8 +- libbe/comment.py | 7 +- libbe/config.py | 8 +- libbe/darcs.py | 13 +- libbe/diff.py | 7 +- libbe/editor.py | 8 +- libbe/encoding.py | 8 +- libbe/git.py | 11 +- libbe/hg.py | 14 +- libbe/mapfile.py | 8 +- libbe/plugin.py | 8 +- libbe/properties.py | 511 +++++++++++++++++++++--------------------- libbe/settings_object.py | 392 ++++++++++++++++---------------- libbe/subproc.py | 7 +- libbe/tree.py | 7 +- libbe/upgrade.py | 7 +- libbe/utility.py | 8 +- libbe/vcs.py | 569 ++++++++++++++++++++++++----------------------- test.py | 2 + 26 files changed, 1161 insertions(+), 1045 deletions(-) diff --git a/becommands/import_xml.py b/becommands/import_xml.py index 892a09e..d1ea026 100644 --- a/becommands/import_xml.py +++ b/becommands/import_xml.py @@ -14,6 +14,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Import comments and bugs from XML""" +import libbe from libbe import cmdutil, bugdir, bug, comment, utility from becommands.comment import complete import copy @@ -23,8 +24,9 @@ try: # import core module, Python >= 2.5 from xml.etree import ElementTree except ImportError: # look for non-core module from elementtree import ElementTree -import doctest -import unittest +if libbe.TESTING == True: + import doctest + import unittest __desc__ = __doc__ def execute(args, manipulate_encodings=True, restrict_file_access=False): @@ -302,127 +304,129 @@ def help(): return get_parser().help_str() + longhelp -class LonghelpTestCase (unittest.TestCase): - """ - Test import scenarios given in longhelp. - """ - def setUp(self): - self.bugdir = bugdir.SimpleBugDir() - self.original_working_dir = os.getcwd() - os.chdir(self.bugdir.root) - bugA = self.bugdir.bug_from_uuid('a') - self.bugdir.remove_bug(bugA) - self.bugdir.set_sync_with_disk(False) - bugB = self.bugdir.bug_from_uuid('b') - bugB.creator = 'John' - bugB.status = 'open' - bugB.extra_strings += ["don't forget your towel"] - bugB.extra_strings += ['helps with space travel'] - comm1 = bugB.comment_root.new_reply(body='Hello\n') - comm1.uuid = 'c1' - comm1.author = 'Jane' - comm2 = bugB.comment_root.new_reply(body='World\n') - comm2.uuid = 'c2' - comm2.author = 'Jess' - bugB.save() - self.bugdir.set_sync_with_disk(True) - self.xml = """ - - - b - fixed - a test bug - don't forget your towel - watch out for flying dolphins - - c1 - So long - - - c3 - Jed - And thanks - - - +if libbe.TESTING == True: + class LonghelpTestCase (unittest.TestCase): + """ + Test import scenarios given in longhelp. """ - def tearDown(self): - os.chdir(self.original_working_dir) - self.bugdir.cleanup() - def _execute(self, *args): - import StringIO - orig_stdin = sys.stdin - sys.stdin = StringIO.StringIO(self.xml) - execute(list(args)+["-"], manipulate_encodings=False, - restrict_file_access=True) - sys.stdin = orig_stdin - self.bugdir._clear_bugs() - def testCleanBugdir(self): - uuids = list(self.bugdir.uuids()) - self.failUnless(uuids == ['b'], uuids) - def testNotAddOnly(self): - self._execute() - uuids = list(self.bugdir.uuids()) - self.failUnless(uuids == ['b'], uuids) - bugB = self.bugdir.bug_from_uuid('b') - self.failUnless(bugB.uuid == 'b', bugB.uuid) - self.failUnless(bugB.creator == 'John', bugB.creator) - self.failUnless(bugB.status == 'fixed', bugB.status) - estrs = ["don't forget your towel", - 'helps with space travel', - 'watch out for flying dolphins'] - self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) - comments = list(bugB.comments()) - self.failUnless(len(comments) == 3, - ['%s (%s, %s)' % (c.uuid, c.alt_id, c.body) for c in comments]) - c1 = bugB.comment_from_uuid('c1') - comments.remove(c1) - self.failUnless(c1.uuid == 'c1', c1.uuid) - self.failUnless(c1.alt_id == None, c1.alt_id) - self.failUnless(c1.author == 'Jane', c1.author) - self.failUnless(c1.body == 'So long\n', c1.body) - c2 = bugB.comment_from_uuid('c2') - comments.remove(c2) - self.failUnless(c2.uuid == 'c2', c2.uuid) - self.failUnless(c2.alt_id == None, c2.alt_id) - self.failUnless(c2.author == 'Jess', c2.author) - self.failUnless(c2.body == 'World\n', c2.body) - c4 = comments[0] - self.failUnless(len(c4.uuid) == 36, c4.uuid) - self.failUnless(c4.alt_id == 'c3', c4.alt_id) - self.failUnless(c4.author == 'Jed', c4.author) - self.failUnless(c4.body == 'And thanks\n', c4.body) - def testAddOnly(self): - self._execute('--add-only') - uuids = list(self.bugdir.uuids()) - self.failUnless(uuids == ['b'], uuids) - bugB = self.bugdir.bug_from_uuid('b') - self.failUnless(bugB.uuid == 'b', bugB.uuid) - self.failUnless(bugB.creator == 'John', bugB.creator) - self.failUnless(bugB.status == 'open', bugB.status) - estrs = ["don't forget your towel", - 'helps with space travel'] - self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) - comments = list(bugB.comments()) - self.failUnless(len(comments) == 3, - ['%s (%s)' % (c.uuid, c.alt_id) for c in comments]) - c1 = bugB.comment_from_uuid('c1') - comments.remove(c1) - self.failUnless(c1.uuid == 'c1', c1.uuid) - self.failUnless(c1.alt_id == None, c1.alt_id) - self.failUnless(c1.author == 'Jane', c1.author) - self.failUnless(c1.body == 'Hello\n', c1.body) - c2 = bugB.comment_from_uuid('c2') - comments.remove(c2) - self.failUnless(c2.uuid == 'c2', c2.uuid) - self.failUnless(c2.alt_id == None, c2.alt_id) - self.failUnless(c2.author == 'Jess', c2.author) - self.failUnless(c2.body == 'World\n', c2.body) - c4 = comments[0] - self.failUnless(len(c4.uuid) == 36, c4.uuid) - self.failUnless(c4.alt_id == 'c3', c4.alt_id) - self.failUnless(c4.author == 'Jed', c4.author) - self.failUnless(c4.body == 'And thanks\n', c4.body) + def setUp(self): + self.bugdir = bugdir.SimpleBugDir() + self.original_working_dir = os.getcwd() + os.chdir(self.bugdir.root) + bugA = self.bugdir.bug_from_uuid('a') + self.bugdir.remove_bug(bugA) + self.bugdir.set_sync_with_disk(False) + bugB = self.bugdir.bug_from_uuid('b') + bugB.creator = 'John' + bugB.status = 'open' + bugB.extra_strings += ["don't forget your towel"] + bugB.extra_strings += ['helps with space travel'] + comm1 = bugB.comment_root.new_reply(body='Hello\n') + comm1.uuid = 'c1' + comm1.author = 'Jane' + comm2 = bugB.comment_root.new_reply(body='World\n') + comm2.uuid = 'c2' + comm2.author = 'Jess' + bugB.save() + self.bugdir.set_sync_with_disk(True) + self.xml = """ + + + b + fixed + a test bug + don't forget your towel + watch out for flying dolphins + + c1 + So long + + + c3 + Jed + And thanks + + + + """ + def tearDown(self): + os.chdir(self.original_working_dir) + self.bugdir.cleanup() + def _execute(self, *args): + import StringIO + orig_stdin = sys.stdin + sys.stdin = StringIO.StringIO(self.xml) + execute(list(args)+["-"], manipulate_encodings=False, + restrict_file_access=True) + sys.stdin = orig_stdin + self.bugdir._clear_bugs() + def testCleanBugdir(self): + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + def testNotAddOnly(self): + self._execute() + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + bugB = self.bugdir.bug_from_uuid('b') + self.failUnless(bugB.uuid == 'b', bugB.uuid) + self.failUnless(bugB.creator == 'John', bugB.creator) + self.failUnless(bugB.status == 'fixed', bugB.status) + estrs = ["don't forget your towel", + 'helps with space travel', + 'watch out for flying dolphins'] + self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) + comments = list(bugB.comments()) + self.failUnless(len(comments) == 3, + ['%s (%s, %s)' % (c.uuid, c.alt_id, c.body) + for c in comments]) + c1 = bugB.comment_from_uuid('c1') + comments.remove(c1) + self.failUnless(c1.uuid == 'c1', c1.uuid) + self.failUnless(c1.alt_id == None, c1.alt_id) + self.failUnless(c1.author == 'Jane', c1.author) + self.failUnless(c1.body == 'So long\n', c1.body) + c2 = bugB.comment_from_uuid('c2') + comments.remove(c2) + self.failUnless(c2.uuid == 'c2', c2.uuid) + self.failUnless(c2.alt_id == None, c2.alt_id) + self.failUnless(c2.author == 'Jess', c2.author) + self.failUnless(c2.body == 'World\n', c2.body) + c4 = comments[0] + self.failUnless(len(c4.uuid) == 36, c4.uuid) + self.failUnless(c4.alt_id == 'c3', c4.alt_id) + self.failUnless(c4.author == 'Jed', c4.author) + self.failUnless(c4.body == 'And thanks\n', c4.body) + def testAddOnly(self): + self._execute('--add-only') + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + bugB = self.bugdir.bug_from_uuid('b') + self.failUnless(bugB.uuid == 'b', bugB.uuid) + self.failUnless(bugB.creator == 'John', bugB.creator) + self.failUnless(bugB.status == 'open', bugB.status) + estrs = ["don't forget your towel", + 'helps with space travel'] + self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) + comments = list(bugB.comments()) + self.failUnless(len(comments) == 3, + ['%s (%s)' % (c.uuid, c.alt_id) for c in comments]) + c1 = bugB.comment_from_uuid('c1') + comments.remove(c1) + self.failUnless(c1.uuid == 'c1', c1.uuid) + self.failUnless(c1.alt_id == None, c1.alt_id) + self.failUnless(c1.author == 'Jane', c1.author) + self.failUnless(c1.body == 'Hello\n', c1.body) + c2 = bugB.comment_from_uuid('c2') + comments.remove(c2) + self.failUnless(c2.uuid == 'c2', c2.uuid) + self.failUnless(c2.alt_id == None, c2.alt_id) + self.failUnless(c2.author == 'Jess', c2.author) + self.failUnless(c2.body == 'World\n', c2.body) + c4 = comments[0] + self.failUnless(len(c4.uuid) == 36, c4.uuid) + self.failUnless(c4.alt_id == 'c3', c4.alt_id) + self.failUnless(c4.author == 'Jed', c4.author) + self.failUnless(c4.body == 'And thanks\n', c4.body) -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/__init__.py b/libbe/__init__.py index 794013c..a646d76 100644 --- a/libbe/__init__.py +++ b/libbe/__init__.py @@ -14,3 +14,8 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +# To reduce module load time, test suite generation is turned of by +# default. If you _do_ want to generate the test suites, set +# TESTING=True before loading any libbe or becommands submodules. +TESTING = False diff --git a/libbe/arch.py b/libbe/arch.py index 48129b5..45a3284 100644 --- a/libbe/arch.py +++ b/libbe/arch.py @@ -28,13 +28,14 @@ import re import shutil import sys import time -import unittest -import doctest +import libbe from beuuid import uuid_gen import config import vcs - +if libbe.TESTING == True: + import unittest + import doctest DEFAULT_CLIENT = "tla" @@ -307,7 +308,8 @@ class CantAddFile(Exception): -vcs.make_vcs_testcase_subclasses(Arch, sys.modules[__name__]) +if libbe.TESTING == True: + vcs.make_vcs_testcase_subclasses(Arch, sys.modules[__name__]) -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/beuuid.py b/libbe/beuuid.py index 260f3dc..a3a3b6c 100644 --- a/libbe/beuuid.py +++ b/libbe/beuuid.py @@ -20,7 +20,9 @@ Backwards compatibility support for Python 2.4. Once people give up on 2.4 ;), the uuid call should be merged into bugdir.py """ -import unittest +import libbe +if libbe.TESTING == True: + import unittest try: @@ -56,9 +58,10 @@ except ImportError: raise Exception, strerror return output.rstrip('\n') -class UUIDtestCase(unittest.TestCase): - def testUUID_gen(self): - id = uuid_gen() - self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) +if libbe.TESTING == True: + class UUIDtestCase(unittest.TestCase): + def testUUID_gen(self): + id = uuid_gen() + self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) -suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) + suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) diff --git a/libbe/bug.py b/libbe/bug.py index 897d841..1a190c3 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -31,8 +31,8 @@ try: # import core module, Python >= 2.5 except ImportError: # look for non-core module from elementtree import ElementTree import xml.sax.saxutils -import doctest +import libbe from beuuid import uuid_gen from properties import Property, doc_property, local_property, \ defaulting_property, checked_property, cached_property, \ @@ -41,6 +41,8 @@ import settings_object import mapfile import comment import utility +if libbe.TESTING == True: + import doctest class DiskAccessRequired (Exception): @@ -877,4 +879,5 @@ def cmp_last_modified(bug_1, bug_2): return -cmp(val_1, val_2) -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 301ceb6..d78e761 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -29,9 +29,8 @@ import os import os.path import sys import time -import unittest -import doctest +import libbe import bug import encoding from properties import Property, doc_property, local_property, \ @@ -43,6 +42,9 @@ import vcs import settings_object import upgrade import utility +if libbe.TESTING == True: + import unittest + import doctest class NoBugDir(Exception): @@ -689,145 +691,156 @@ class SimpleBugDir (BugDir): self._dir_ref.cleanup() BugDir.cleanup(self) -class BugDirTestCase(unittest.TestCase): - def setUp(self): - self.dir = utility.Dir() - self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, - allow_vcs_init=True) - self.vcs = self.bugdir.vcs - def tearDown(self): - self.bugdir.cleanup() - self.dir.cleanup() - def fullPath(self, path): - return os.path.join(self.dir.path, path) - def assertPathExists(self, path): - fullpath = self.fullPath(path) - self.failUnless(os.path.exists(fullpath)==True, - "path %s does not exist" % fullpath) - self.assertRaises(AlreadyInitialized, BugDir, - self.dir.path, assertNewBugDir=True) - def versionTest(self): - if self.vcs.versioned == False: - return - original = self.bugdir.vcs.commit("Began versioning") - bugA = self.bugdir.bug_from_uuid("a") - bugA.status = "fixed" - self.bugdir.save() - new = self.vcs.commit("Fixed bug a") - dupdir = self.bugdir.duplicate_bugdir(original) - self.failUnless(dupdir.root != self.bugdir.root, - "%s, %s" % (dupdir.root, self.bugdir.root)) - bugAorig = dupdir.bug_from_uuid("a") - self.failUnless(bugA != bugAorig, - "\n%s\n%s" % (bugA.string(), bugAorig.string())) - bugAorig.status = "fixed" - self.failUnless(bug.cmp_status(bugA, bugAorig)==0, - "%s, %s" % (bugA.status, bugAorig.status)) - self.failUnless(bug.cmp_severity(bugA, bugAorig)==0, - "%s, %s" % (bugA.severity, bugAorig.severity)) - self.failUnless(bug.cmp_assigned(bugA, bugAorig)==0, - "%s, %s" % (bugA.assigned, bugAorig.assigned)) - self.failUnless(bug.cmp_time(bugA, bugAorig)==0, - "%s, %s" % (bugA.time, bugAorig.time)) - self.failUnless(bug.cmp_creator(bugA, bugAorig)==0, - "%s, %s" % (bugA.creator, bugAorig.creator)) - self.failUnless(bugA == bugAorig, - "\n%s\n%s" % (bugA.string(), bugAorig.string())) - self.bugdir.remove_duplicate_bugdir() - self.failUnless(os.path.exists(dupdir.root)==False, str(dupdir.root)) - def testRun(self): - self.bugdir.new_bug(uuid="a", summary="Ant") - self.bugdir.new_bug(uuid="b", summary="Cockroach") - self.bugdir.new_bug(uuid="c", summary="Praying mantis") - length = len(self.bugdir) - self.failUnless(length == 3, "%d != 3 bugs" % length) - uuids = list(self.bugdir.uuids()) - self.failUnless(len(uuids) == 3, "%d != 3 uuids" % len(uuids)) - self.failUnless(uuids == ["a","b","c"], str(uuids)) - bugA = self.bugdir.bug_from_uuid("a") - bugAprime = self.bugdir.bug_from_shortname("a") - self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime)) - self.bugdir.save() - self.versionTest() - def testComments(self, sync_with_disk=False): - if sync_with_disk == True: - self.bugdir.set_sync_with_disk(True) - self.bugdir.new_bug(uuid="a", summary="Ant") - bug = self.bugdir.bug_from_uuid("a") - comm = bug.comment_root - rep = comm.new_reply("Ants are small.") - rep.new_reply("And they have six legs.") - if sync_with_disk == False: +if libbe.TESTING == True: + class BugDirTestCase(unittest.TestCase): + def setUp(self): + self.dir = utility.Dir() + self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, + allow_vcs_init=True) + self.vcs = self.bugdir.vcs + def tearDown(self): + self.bugdir.cleanup() + self.dir.cleanup() + def fullPath(self, path): + return os.path.join(self.dir.path, path) + def assertPathExists(self, path): + fullpath = self.fullPath(path) + self.failUnless(os.path.exists(fullpath)==True, + "path %s does not exist" % fullpath) + self.assertRaises(AlreadyInitialized, BugDir, + self.dir.path, assertNewBugDir=True) + def versionTest(self): + if self.vcs.versioned == False: + return + original = self.bugdir.vcs.commit("Began versioning") + bugA = self.bugdir.bug_from_uuid("a") + bugA.status = "fixed" self.bugdir.save() - self.bugdir.set_sync_with_disk(True) - self.bugdir._clear_bugs() - bug = self.bugdir.bug_from_uuid("a") - bug.load_comments() - if sync_with_disk == False: - self.bugdir.set_sync_with_disk(False) - self.failUnless(len(bug.comment_root)==1, len(bug.comment_root)) - for index,comment in enumerate(bug.comments()): - if index == 0: - repLoaded = comment - self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid) - self.failUnless(comment.sync_with_disk == sync_with_disk, - comment.sync_with_disk) - self.failUnless(comment.content_type == "text/plain", - comment.content_type) - self.failUnless(repLoaded.settings["Content-type"]=="text/plain", - repLoaded.settings) - self.failUnless(repLoaded.body == "Ants are small.", - repLoaded.body) - elif index == 1: - self.failUnless(comment.in_reply_to == repLoaded.uuid, - repLoaded.uuid) - self.failUnless(comment.body == "And they have six legs.", - comment.body) - else: - self.failIf(True, "Invalid comment: %d\n%s" % (index, comment)) - def testSyncedComments(self): - self.testComments(sync_with_disk=True) - -class SimpleBugDirTestCase (unittest.TestCase): - def setUp(self): - # create a pre-existing bugdir in a temporary directory - self.dir = utility.Dir() - self.original_working_dir = os.getcwd() - os.chdir(self.dir.path) - self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, - allow_vcs_init=True) - self.bugdir.new_bug("preexisting", summary="Hopefully not imported") - self.bugdir.save() - def tearDown(self): - os.chdir(self.original_working_dir) - self.bugdir.cleanup() - self.dir.cleanup() - def testOnDiskCleanLoad(self): - """SimpleBugDir(sync_with_disk==True) should not import preexisting bugs.""" - bugdir = SimpleBugDir(sync_with_disk=True) - self.failUnless(bugdir.sync_with_disk==True, bugdir.sync_with_disk) - uuids = sorted([bug.uuid for bug in bugdir]) - self.failUnless(uuids == ['a', 'b'], uuids) - bugdir._clear_bugs() - uuids = sorted([bug.uuid for bug in bugdir]) - self.failUnless(uuids == [], uuids) - bugdir.load_all_bugs() - uuids = sorted([bug.uuid for bug in bugdir]) - self.failUnless(uuids == ['a', 'b'], uuids) - bugdir.cleanup() - def testInMemoryCleanLoad(self): - """SimpleBugDir(sync_with_disk==False) should not import preexisting bugs.""" - bugdir = SimpleBugDir(sync_with_disk=False) - self.failUnless(bugdir.sync_with_disk==False, bugdir.sync_with_disk) - uuids = sorted([bug.uuid for bug in bugdir]) - self.failUnless(uuids == ['a', 'b'], uuids) - self.failUnlessRaises(DiskAccessRequired, bugdir.load_all_bugs) - uuids = sorted([bug.uuid for bug in bugdir]) - self.failUnless(uuids == ['a', 'b'], uuids) - bugdir._clear_bugs() - uuids = sorted([bug.uuid for bug in bugdir]) - self.failUnless(uuids == [], uuids) - bugdir.cleanup() - -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + new = self.vcs.commit("Fixed bug a") + dupdir = self.bugdir.duplicate_bugdir(original) + self.failUnless(dupdir.root != self.bugdir.root, + "%s, %s" % (dupdir.root, self.bugdir.root)) + bugAorig = dupdir.bug_from_uuid("a") + self.failUnless(bugA != bugAorig, + "\n%s\n%s" % (bugA.string(), bugAorig.string())) + bugAorig.status = "fixed" + self.failUnless(bug.cmp_status(bugA, bugAorig)==0, + "%s, %s" % (bugA.status, bugAorig.status)) + self.failUnless(bug.cmp_severity(bugA, bugAorig)==0, + "%s, %s" % (bugA.severity, bugAorig.severity)) + self.failUnless(bug.cmp_assigned(bugA, bugAorig)==0, + "%s, %s" % (bugA.assigned, bugAorig.assigned)) + self.failUnless(bug.cmp_time(bugA, bugAorig)==0, + "%s, %s" % (bugA.time, bugAorig.time)) + self.failUnless(bug.cmp_creator(bugA, bugAorig)==0, + "%s, %s" % (bugA.creator, bugAorig.creator)) + self.failUnless(bugA == bugAorig, + "\n%s\n%s" % (bugA.string(), bugAorig.string())) + self.bugdir.remove_duplicate_bugdir() + self.failUnless(os.path.exists(dupdir.root)==False, + str(dupdir.root)) + def testRun(self): + self.bugdir.new_bug(uuid="a", summary="Ant") + self.bugdir.new_bug(uuid="b", summary="Cockroach") + self.bugdir.new_bug(uuid="c", summary="Praying mantis") + length = len(self.bugdir) + self.failUnless(length == 3, "%d != 3 bugs" % length) + uuids = list(self.bugdir.uuids()) + self.failUnless(len(uuids) == 3, "%d != 3 uuids" % len(uuids)) + self.failUnless(uuids == ["a","b","c"], str(uuids)) + bugA = self.bugdir.bug_from_uuid("a") + bugAprime = self.bugdir.bug_from_shortname("a") + self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime)) + self.bugdir.save() + self.versionTest() + def testComments(self, sync_with_disk=False): + if sync_with_disk == True: + self.bugdir.set_sync_with_disk(True) + self.bugdir.new_bug(uuid="a", summary="Ant") + bug = self.bugdir.bug_from_uuid("a") + comm = bug.comment_root + rep = comm.new_reply("Ants are small.") + rep.new_reply("And they have six legs.") + if sync_with_disk == False: + self.bugdir.save() + self.bugdir.set_sync_with_disk(True) + self.bugdir._clear_bugs() + bug = self.bugdir.bug_from_uuid("a") + bug.load_comments() + if sync_with_disk == False: + self.bugdir.set_sync_with_disk(False) + self.failUnless(len(bug.comment_root)==1, len(bug.comment_root)) + for index,comment in enumerate(bug.comments()): + if index == 0: + repLoaded = comment + self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid) + self.failUnless(comment.sync_with_disk == sync_with_disk, + comment.sync_with_disk) + self.failUnless(comment.content_type == "text/plain", + comment.content_type) + self.failUnless(repLoaded.settings["Content-type"] == \ + "text/plain", + repLoaded.settings) + self.failUnless(repLoaded.body == "Ants are small.", + repLoaded.body) + elif index == 1: + self.failUnless(comment.in_reply_to == repLoaded.uuid, + repLoaded.uuid) + self.failUnless(comment.body == "And they have six legs.", + comment.body) + else: + self.failIf(True, + "Invalid comment: %d\n%s" % (index, comment)) + def testSyncedComments(self): + self.testComments(sync_with_disk=True) + + class SimpleBugDirTestCase (unittest.TestCase): + def setUp(self): + # create a pre-existing bugdir in a temporary directory + self.dir = utility.Dir() + self.original_working_dir = os.getcwd() + os.chdir(self.dir.path) + self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, + allow_vcs_init=True) + self.bugdir.new_bug("preexisting",summary="Hopefully not imported") + self.bugdir.save() + def tearDown(self): + os.chdir(self.original_working_dir) + self.bugdir.cleanup() + self.dir.cleanup() + def testOnDiskCleanLoad(self): + """ + SimpleBugDir(sync_with_disk==True) should not import + preexisting bugs. + """ + bugdir = SimpleBugDir(sync_with_disk=True) + self.failUnless(bugdir.sync_with_disk==True, bugdir.sync_with_disk) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir._clear_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == [], uuids) + bugdir.load_all_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir.cleanup() + def testInMemoryCleanLoad(self): + """ + SimpleBugDir(sync_with_disk==False) should not import + preexisting bugs. + """ + bugdir = SimpleBugDir(sync_with_disk=False) + self.failUnless(bugdir.sync_with_disk==False, + bugdir.sync_with_disk) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + self.failUnlessRaises(DiskAccessRequired, bugdir.load_all_bugs) + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == ['a', 'b'], uuids) + bugdir._clear_bugs() + uuids = sorted([bug.uuid for bug in bugdir]) + self.failUnless(uuids == [], uuids) + bugdir.cleanup() + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/bzr.py b/libbe/bzr.py index 281493d..62a9b11 100644 --- a/libbe/bzr.py +++ b/libbe/bzr.py @@ -26,9 +26,11 @@ import os import re import sys import unittest -import doctest +import libbe import vcs +if libbe.TESTING == True: + import doctest def new(): @@ -108,7 +110,8 @@ class Bzr(vcs.VCS): return str(current_revision+index+1) -vcs.make_vcs_testcase_subclasses(Bzr, sys.modules[__name__]) +if libbe.TESTING == True: + vcs.make_vcs_testcase_subclasses(Bzr, sys.modules[__name__]) -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index 78645ab..dcd4ca9 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -27,12 +27,15 @@ import os from textwrap import TextWrapper from StringIO import StringIO import sys -import doctest +import libbe import bugdir import comment import plugin import encoding +if libbe.TESTING == True: + import doctest + class UserError(Exception): def __init__(self, msg): @@ -293,4 +296,5 @@ def bug_comment_from_id(bdir, id): raise UserError(e.message) return (bug, comm) -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/comment.py b/libbe/comment.py index c5f1cc9..32536d4 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -32,8 +32,8 @@ try: # import core module, Python >= 2.5 except ImportError: # look for non-core module from elementtree import ElementTree import xml.sax.saxutils -import doctest +import libbe from beuuid import uuid_gen from properties import Property, doc_property, local_property, \ defaulting_property, checked_property, cached_property, \ @@ -42,6 +42,8 @@ import settings_object import mapfile from tree import Tree import utility +if libbe.TESTING == True: + import doctest class InvalidShortname(KeyError): @@ -796,4 +798,5 @@ class CommentCompoundComparator (object): cmp_full = CommentCompoundComparator() -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/config.py b/libbe/config.py index 4f32731..ccd236b 100644 --- a/libbe/config.py +++ b/libbe/config.py @@ -25,7 +25,10 @@ import codecs import locale import os.path import sys -import doctest + +import libbe +if libbe.TESTING == True: + import doctest default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() @@ -87,4 +90,5 @@ def get_val(name, section="DEFAULT", default=None, encoding=None): else: return default -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/darcs.py b/libbe/darcs.py index 059452c..d94eaef 100644 --- a/libbe/darcs.py +++ b/libbe/darcs.py @@ -28,10 +28,12 @@ try: # import core module, Python >= 2.5 except ImportError: # look for non-core module from elementtree import ElementTree from xml.sax.saxutils import unescape -import doctest -import unittest +import libbe import vcs +if libbe.TESTING == True: + import doctest + import unittest def new(): @@ -183,7 +185,8 @@ class Darcs(vcs.VCS): except IndexError: return None -vcs.make_vcs_testcase_subclasses(Darcs, sys.modules[__name__]) +if libbe.TESTING == True: + vcs.make_vcs_testcase_subclasses(Darcs, sys.modules[__name__]) -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/diff.py b/libbe/diff.py index 6e830c6..4c22597 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -19,10 +19,12 @@ """Compare two bug trees.""" import difflib -import doctest +import libbe from libbe import bugdir, bug, settings_object, tree from libbe.utility import time_to_str +if libbe.TESTING == True: + import doctest class DiffTree (tree.Tree): @@ -417,4 +419,5 @@ class Diff (object): return difflib.unified_diff(old_body, new_body) -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/editor.py b/libbe/editor.py index ed497a0..859cedc 100644 --- a/libbe/editor.py +++ b/libbe/editor.py @@ -26,7 +26,10 @@ import locale import os import sys import tempfile -import doctest + +import libbe +if libbe.TESTING == True: + import doctest default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() @@ -106,4 +109,5 @@ def trimmed_string(instring): out.append(line) return ''.join(out) -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/encoding.py b/libbe/encoding.py index b0a04cc..d09117f 100644 --- a/libbe/encoding.py +++ b/libbe/encoding.py @@ -23,7 +23,10 @@ Support input/output/filesystem encodings (e.g. UTF-8). import codecs import locale import sys -import doctest + +import libbe +if libbe.TESTING == True: + import doctest ENCODING = None # override get_encoding() output by setting this @@ -59,4 +62,5 @@ def set_IO_stream_encodings(encoding): sys.stdout = codecs.getwriter(encoding)(sys.__stdout__) sys.stderr = codecs.getwriter(encoding)(sys.__stderr__) -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/git.py b/libbe/git.py index 55556de..7f6e53a 100644 --- a/libbe/git.py +++ b/libbe/git.py @@ -25,9 +25,11 @@ import os import re import sys import unittest -import doctest +import libbe import vcs +if libbe.TESTING == True: + import doctest def new(): @@ -142,7 +144,8 @@ class Git(vcs.VCS): return None -vcs.make_vcs_testcase_subclasses(Git, sys.modules[__name__]) +if libbe.TESTING == True: + vcs.make_vcs_testcase_subclasses(Git, sys.modules[__name__]) -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/hg.py b/libbe/hg.py index d7eb796..ed27717 100644 --- a/libbe/hg.py +++ b/libbe/hg.py @@ -24,11 +24,14 @@ Mercurial (hg) backend. import os import re import sys -import unittest -import doctest +import libbe import vcs +if libbe.TESTING == True: + import unittest + import doctest + def new(): return Hg() @@ -98,7 +101,8 @@ class Hg(vcs.VCS): return None -vcs.make_vcs_testcase_subclasses(Hg, sys.modules[__name__]) +if libbe.TESTING == True: + vcs.make_vcs_testcase_subclasses(Hg, sys.modules[__name__]) -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/mapfile.py b/libbe/mapfile.py index 062606b..8e1e279 100644 --- a/libbe/mapfile.py +++ b/libbe/mapfile.py @@ -25,7 +25,10 @@ independent/conflicting changes. import errno import os.path import yaml -import doctest + +import libbe +if libbe.TESTING == True: + import doctest class IllegalKey(Exception): @@ -119,4 +122,5 @@ def map_load(vcs, path, allow_no_vcs=False): binary=True) return parse(contents) -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/plugin.py b/libbe/plugin.py index e6b06fb..03f68fc 100644 --- a/libbe/plugin.py +++ b/libbe/plugin.py @@ -25,7 +25,10 @@ submodules (i.e. "plugins"). import os import os.path import sys -import doctest + +import libbe +if libbe.TESTING == True: + import doctest def my_import(mod_name): module = __import__(mod_name) @@ -68,4 +71,5 @@ plugin_path = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) if plugin_path not in sys.path: sys.path.append(plugin_path) -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/properties.py b/libbe/properties.py index 54375b4..f756ff0 100644 --- a/libbe/properties.py +++ b/libbe/properties.py @@ -30,7 +30,10 @@ for more information on decorators. import copy import types -import unittest + +import libbe +if libbe.TESTING == True: + import unittest class ValueCheckError (ValueError): @@ -383,257 +386,257 @@ def change_hook_property(hook, mutable=False, default=None): return funcs return decorator - -class DecoratorTests(unittest.TestCase): - def testLocalDoc(self): - class Test(object): - @Property - @doc_property("A fancy property") - def x(): - return {} - self.failUnless(Test.x.__doc__ == "A fancy property", - Test.x.__doc__) - def testLocalProperty(self): - class Test(object): - @Property - @local_property(name="LOCAL") - def x(): - return {} - t = Test() - self.failUnless(t.x == None, str(t.x)) - t.x = 'z' # the first set initializes ._LOCAL_value - self.failUnless(t.x == 'z', str(t.x)) - self.failUnless("_LOCAL_value" in dir(t), dir(t)) - self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value) - def testSettingsProperty(self): - class Test(object): - @Property - @settings_property(name="attr") - def x(): - return {} - def __init__(self): - self.settings = {} - t = Test() - self.failUnless(t.x == None, str(t.x)) - t.x = 'z' # the first set initializes ._LOCAL_value - self.failUnless(t.x == 'z', str(t.x)) - self.failUnless("attr" in t.settings, t.settings) - self.failUnless(t.settings["attr"] == 'z', t.settings["attr"]) - def testDefaultingLocalProperty(self): - class Test(object): - @Property - @defaulting_property(default='y', null='x') - @local_property(name="DEFAULT", null=5) - def x(): return {} - t = Test() - self.failUnless(t.x == 5, str(t.x)) - t.x = 'x' - self.failUnless(t.x == 'y', str(t.x)) - t.x = 'y' - self.failUnless(t.x == 'y', str(t.x)) - t.x = 'z' - self.failUnless(t.x == 'z', str(t.x)) - t.x = 5 - self.failUnless(t.x == 5, str(t.x)) - def testCheckedLocalProperty(self): - class Test(object): - @Property - @checked_property(allowed=['x', 'y', 'z']) - @local_property(name="CHECKED") - def x(): return {} - def __init__(self): - self._CHECKED_value = 'x' - t = Test() - self.failUnless(t.x == 'x', str(t.x)) - try: +if libbe.TESTING == True: + class DecoratorTests(unittest.TestCase): + def testLocalDoc(self): + class Test(object): + @Property + @doc_property("A fancy property") + def x(): + return {} + self.failUnless(Test.x.__doc__ == "A fancy property", + Test.x.__doc__) + def testLocalProperty(self): + class Test(object): + @Property + @local_property(name="LOCAL") + def x(): + return {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("_LOCAL_value" in dir(t), dir(t)) + self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value) + def testSettingsProperty(self): + class Test(object): + @Property + @settings_property(name="attr") + def x(): + return {} + def __init__(self): + self.settings = {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("attr" in t.settings, t.settings) + self.failUnless(t.settings["attr"] == 'z', t.settings["attr"]) + def testDefaultingLocalProperty(self): + class Test(object): + @Property + @defaulting_property(default='y', null='x') + @local_property(name="DEFAULT", null=5) + def x(): return {} + t = Test() + self.failUnless(t.x == 5, str(t.x)) + t.x = 'x' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'y' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'z' + self.failUnless(t.x == 'z', str(t.x)) + t.x = 5 + self.failUnless(t.x == 5, str(t.x)) + def testCheckedLocalProperty(self): + class Test(object): + @Property + @checked_property(allowed=['x', 'y', 'z']) + @local_property(name="CHECKED") + def x(): return {} + def __init__(self): + self._CHECKED_value = 'x' + t = Test() + self.failUnless(t.x == 'x', str(t.x)) + try: + t.x = None + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + def testTwoCheckedLocalProperties(self): + class Test(object): + @Property + @checked_property(allowed=['x', 'y', 'z']) + @local_property(name="X") + def x(): return {} + + @Property + @checked_property(allowed=['a', 'b', 'c']) + @local_property(name="A") + def a(): return {} + def __init__(self): + self._A_value = 'a' + self._X_value = 'x' + t = Test() + try: + t.x = 'a' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + t.x = 'x' + t.x = 'y' + t.x = 'z' + try: + t.a = 'x' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + t.a = 'a' + t.a = 'b' + t.a = 'c' + def testFnCheckedLocalProperty(self): + class Test(object): + @Property + @fn_checked_property(lambda v : v in ['x', 'y', 'z']) + @local_property(name="CHECKED") + def x(): return {} + def __init__(self): + self._CHECKED_value = 'x' + t = Test() + self.failUnless(t.x == 'x', str(t.x)) + try: + t.x = None + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + def testCachedLocalProperty(self): + class Gen(object): + def __init__(self): + self.i = 0 + def __call__(self, owner): + self.i += 1 + return self.i + class Test(object): + @Property + @cached_property(generator=Gen(), initVal=None) + @local_property(name="CACHED") + def x(): return {} + t = Test() + self.failIf("_CACHED_cache" in dir(t), + getattr(t, "_CACHED_cache", None)) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + t.x = 8 + self.failUnless(t.x == 8, t.x) + self.failUnless(t.x == 8, t.x) + t._CACHED_cache = False # Caching is off, but the stored value + val = t.x # is 8, not the initVal (None), so we + self.failUnless(val == 8, val) # get 8. + t._CACHED_value = None # Now we've set the stored value to None + val = t.x # so future calls to fget (like this) + self.failUnless(val == 2, val) # will call the generator every time... + val = t.x + self.failUnless(val == 3, val) + val = t.x + self.failUnless(val == 4, val) + t._CACHED_cache = True # We turn caching back on, and get + self.failUnless(t.x == 1, str(t.x)) # the original cached value. + del t._CACHED_cached_value # Removing that value forces a + self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call + self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which + self.failUnless(t.x == 5, str(t.x)) # we get the new cached value. + def testPrimedLocalProperty(self): + class Test(object): + def prime(self): + self.settings["PRIMED"] = "initialized" + @Property + @primed_property(primer=prime, initVal=None) + @settings_property(name="PRIMED") + def x(): return {} + def __init__(self): + self.settings={} + t = Test() + self.failIf("_PRIMED_prime" in dir(t), + getattr(t, "_PRIMED_prime", None)) + self.failUnless(t.x == "initialized", t.x) + t.x = 1 + self.failUnless(t.x == 1, t.x) t.x = None - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - def testTwoCheckedLocalProperties(self): - class Test(object): - @Property - @checked_property(allowed=['x', 'y', 'z']) - @local_property(name="X") - def x(): return {} - - @Property - @checked_property(allowed=['a', 'b', 'c']) - @local_property(name="A") - def a(): return {} - def __init__(self): - self._A_value = 'a' - self._X_value = 'x' - t = Test() - try: - t.x = 'a' - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - t.x = 'x' - t.x = 'y' - t.x = 'z' - try: - t.a = 'x' - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - t.a = 'a' - t.a = 'b' - t.a = 'c' - def testFnCheckedLocalProperty(self): - class Test(object): - @Property - @fn_checked_property(lambda v : v in ['x', 'y', 'z']) - @local_property(name="CHECKED") - def x(): return {} - def __init__(self): - self._CHECKED_value = 'x' - t = Test() - self.failUnless(t.x == 'x', str(t.x)) - try: - t.x = None - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - def testCachedLocalProperty(self): - class Gen(object): - def __init__(self): - self.i = 0 - def __call__(self, owner): - self.i += 1 - return self.i - class Test(object): - @Property - @cached_property(generator=Gen(), initVal=None) - @local_property(name="CACHED") - def x(): return {} - t = Test() - self.failIf("_CACHED_cache" in dir(t), getattr(t, "_CACHED_cache", None)) - self.failUnless(t.x == 1, t.x) - self.failUnless(t.x == 1, t.x) - self.failUnless(t.x == 1, t.x) - t.x = 8 - self.failUnless(t.x == 8, t.x) - self.failUnless(t.x == 8, t.x) - t._CACHED_cache = False # Caching is off, but the stored value - val = t.x # is 8, not the initVal (None), so we - self.failUnless(val == 8, val) # get 8. - t._CACHED_value = None # Now we've set the stored value to None - val = t.x # so future calls to fget (like this) - self.failUnless(val == 2, val) # will call the generator every time... - val = t.x - self.failUnless(val == 3, val) - val = t.x - self.failUnless(val == 4, val) - t._CACHED_cache = True # We turn caching back on, and get - self.failUnless(t.x == 1, str(t.x)) # the original cached value. - del t._CACHED_cached_value # Removing that value forces a - self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call - self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which - self.failUnless(t.x == 5, str(t.x)) # we get the new cached value. - def testPrimedLocalProperty(self): - class Test(object): - def prime(self): - self.settings["PRIMED"] = "initialized" - @Property - @primed_property(primer=prime, initVal=None) - @settings_property(name="PRIMED") - def x(): return {} - def __init__(self): - self.settings={} - t = Test() - self.failIf("_PRIMED_prime" in dir(t), getattr(t, "_PRIMED_prime", None)) - self.failUnless(t.x == "initialized", t.x) - t.x = 1 - self.failUnless(t.x == 1, t.x) - t.x = None - self.failUnless(t.x == "initialized", t.x) - t._PRIMED_prime = True - t.x = 3 - self.failUnless(t.x == "initialized", t.x) - t._PRIMED_prime = False - t.x = 3 - self.failUnless(t.x == 3, t.x) - def testChangeHookLocalProperty(self): - class Test(object): - def _hook(self, old, new): - self.old = old - self.new = new - - @Property - @change_hook_property(_hook) - @local_property(name="HOOKED") - def x(): return {} - t = Test() - t.x = 1 - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == 1, t.new) - t.x = 1 - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == 1, t.new) - t.x = 2 - self.failUnless(t.old == 1, t.old) - self.failUnless(t.new == 2, t.new) - def testChangeHookMutableProperty(self): - class Test(object): - def _hook(self, old, new): - self.old = old - self.new = new - self.hook_calls += 1 - - @Property - @change_hook_property(_hook, mutable=True) - @local_property(name="HOOKED") - def x(): return {} - t = Test() - t.hook_calls = 0 - t.x = [] - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == [], t.new) - self.failUnless(t.hook_calls == 1, t.hook_calls) - a = t.x - a.append(5) - t.x = a - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5], t.new) - self.failUnless(t.hook_calls == 2, t.hook_calls) - t.x = [] - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [], t.new) - self.failUnless(t.hook_calls == 3, t.hook_calls) - # now append without reassigning. this doesn't trigger the - # change, since we don't ever set t.x, only get it and mess - # with it. It does, however, update our t.new, since t.new = - # t.x and is not a static copy. - t.x.append(5) - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [5], t.new) - self.failUnless(t.hook_calls == 3, t.hook_calls) - # however, the next t.x get _will_ notice the change... - a = t.x - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5], t.new) - self.failUnless(t.hook_calls == 4, t.hook_calls) - t.x.append(6) # this append(6) is not noticed yet - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5,6], t.new) - self.failUnless(t.hook_calls == 4, t.hook_calls) - # this append(7) is not noticed, but the t.x get causes the - # append(6) to be noticed - t.x.append(7) - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [5,6,7], t.new) - self.failUnless(t.hook_calls == 5, t.hook_calls) - a = t.x # now the append(7) is noticed - self.failUnless(t.old == [5,6], t.old) - self.failUnless(t.new == [5,6,7], t.new) - self.failUnless(t.hook_calls == 6, t.hook_calls) - - -suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) - + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = True + t.x = 3 + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = False + t.x = 3 + self.failUnless(t.x == 3, t.x) + def testChangeHookLocalProperty(self): + class Test(object): + def _hook(self, old, new): + self.old = old + self.new = new + + @Property + @change_hook_property(_hook) + @local_property(name="HOOKED") + def x(): return {} + t = Test() + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 2 + self.failUnless(t.old == 1, t.old) + self.failUnless(t.new == 2, t.new) + def testChangeHookMutableProperty(self): + class Test(object): + def _hook(self, old, new): + self.old = old + self.new = new + self.hook_calls += 1 + + @Property + @change_hook_property(_hook, mutable=True) + @local_property(name="HOOKED") + def x(): return {} + t = Test() + t.hook_calls = 0 + t.x = [] + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == [], t.new) + self.failUnless(t.hook_calls == 1, t.hook_calls) + a = t.x + a.append(5) + t.x = a + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 2, t.hook_calls) + t.x = [] + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [], t.new) + self.failUnless(t.hook_calls == 3, t.hook_calls) + # now append without reassigning. this doesn't trigger the + # change, since we don't ever set t.x, only get it and mess + # with it. It does, however, update our t.new, since t.new = + # t.x and is not a static copy. + t.x.append(5) + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 3, t.hook_calls) + # however, the next t.x get _will_ notice the change... + a = t.x + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 4, t.hook_calls) + t.x.append(6) # this append(6) is not noticed yet + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5,6], t.new) + self.failUnless(t.hook_calls == 4, t.hook_calls) + # this append(7) is not noticed, but the t.x get causes the + # append(6) to be noticed + t.x.append(7) + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [5,6,7], t.new) + self.failUnless(t.hook_calls == 5, t.hook_calls) + a = t.x # now the append(7) is noticed + self.failUnless(t.old == [5,6], t.old) + self.failUnless(t.new == [5,6,7], t.new) + self.failUnless(t.hook_calls == 6, t.hook_calls) + + suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) diff --git a/libbe/settings_object.py b/libbe/settings_object.py index 7efda5b..6a00ba9 100644 --- a/libbe/settings_object.py +++ b/libbe/settings_object.py @@ -23,13 +23,14 @@ property storage useful for BE objects with saved properties unittests at the end of the module. """ -import doctest -import unittest - +import libbe from properties import Property, doc_property, local_property, \ defaulting_property, checked_property, fn_checked_property, \ cached_property, primed_property, change_hook_property, \ settings_property +if libbe.TESTING == True: + import doctest + import unittest class _Token (object): @@ -228,186 +229,205 @@ class SavedSettingsObject(object): self.clear_cached_setting(setting) -class SavedSettingsObjectTests(unittest.TestCase): - def testSimpleProperty(self): - """Testing a minimal versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - settings_properties=settings_properties, - required_saved_properties=required_saved_properties) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - # access missing setting - self.failUnless(t._settings_loaded == False, t._settings_loaded) - self.failUnless(len(t.settings) == 0, len(t.settings)) - self.failUnless(t.content_type == None, t.content_type) - # accessing t.content_type triggers the priming, which runs - # t._setup_saved_settings, which fills out t.settings with - # EMPTY data. t._settings_loaded is still false though, since - # the default priming does not do any of the `official' loading - # that occurs in t.load_settings. - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - # load settings creates an EMPTY value in the settings array - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t.content_type == None, t.content_type) - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - # now we set a value - t.content_type = 5 - self.failUnless(t.settings["Content-type"] == 5, - t.settings["Content-type"]) - self.failUnless(t.content_type == 5, t.content_type) - self.failUnless(t.settings["Content-type"] == 5, - t.settings["Content-type"]) - # now we set another value - t.content_type = "text/plain" - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t.settings["Content-type"] == "text/plain", - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, - t._get_saved_settings()) - # now we clear to the post-primed value - t.content_type = EMPTY - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t.content_type == None, t.content_type) - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - def testDefaultingProperty(self): - """Testing a defaulting versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties=required_saved_properties) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings() == {}, t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t.content_type == "text/html", - t.content_type) - self.failUnless(t.settings["Content-type"] == "text/html", - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, - t._get_saved_settings()) - def testRequiredDefaultingProperty(self): - """Testing a required defaulting versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties=required_saved_properties, - require_save=True) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, - t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, - t._get_saved_settings()) - def testClassVersionedPropertyDefinition(self): - """Testing a class-specific _versioned property decorator""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - def _versioned_property(settings_properties=settings_properties, - required_saved_properties=required_saved_properties, - **kwargs): - if "settings_properties" not in kwargs: - kwargs["settings_properties"] = settings_properties - if "required_saved_properties" not in kwargs: - kwargs["required_saved_properties"]=required_saved_properties - return versioned_property(**kwargs) - @_versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - require_save=True) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, - t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, - t._get_saved_settings()) - def testMutableChangeHookedProperty(self): - """Testing a mutable change-hooked property""" - SAVES = [] - def prop_log_save_settings(self, old, new, saves=SAVES): - saves.append("'%s' -> '%s'" % (str(old), str(new))) - prop_save_settings(self, old, new) - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="List-type", - doc="A test property", - mutable=True, - change_hook=prop_log_save_settings, - settings_properties=settings_properties, - required_saved_properties=required_saved_properties) - def list_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(SAVES == [], SAVES) - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.list_type == None, t.list_type) - self.failUnless(SAVES == [], SAVES) - self.failUnless(t.settings["List-type"]==EMPTY,t.settings["List-type"]) - t.list_type = [] - self.failUnless(t.settings["List-type"] == [], t.settings["List-type"]) - self.failUnless(SAVES == [ - "'' -> '[]'" - ], SAVES) - t.list_type.append(5) - self.failUnless(SAVES == [ - "'' -> '[]'", - ], SAVES) - self.failUnless(t.settings["List-type"] == [5],t.settings["List-type"]) - self.failUnless(SAVES == [ # the append(5) has not yet been saved - "'' -> '[]'", - ], SAVES) - self.failUnless(t.list_type == [5], t.list_type) # <-get triggers saved - - self.failUnless(SAVES == [ # now the append(5) has been saved. - "'' -> '[]'", - "'[]' -> '[5]'" - ], SAVES) - -unitsuite=unittest.TestLoader().loadTestsFromTestCase(SavedSettingsObjectTests) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) +if libbe.TESTING == True: + class SavedSettingsObjectTests(unittest.TestCase): + def testSimpleProperty(self): + """Testing a minimal versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + # access missing setting + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(len(t.settings) == 0, len(t.settings)) + self.failUnless(t.content_type == None, t.content_type) + # accessing t.content_type triggers the priming, which runs + # t._setup_saved_settings, which fills out t.settings with + # EMPTY data. t._settings_loaded is still false though, since + # the default priming does not do any of the `official' loading + # that occurs in t.load_settings. + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + # load settings creates an EMPTY value in the settings array + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + # now we set a value + t.content_type = 5 + self.failUnless(t.settings["Content-type"] == 5, + t.settings["Content-type"]) + self.failUnless(t.content_type == 5, t.content_type) + self.failUnless(t.settings["Content-type"] == 5, + t.settings["Content-type"]) + # now we set another value + t.content_type = "text/plain" + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == "text/plain", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + # now we clear to the post-primed value + t.content_type = EMPTY + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + def testDefaultingProperty(self): + """Testing a defaulting versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == {}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t.content_type == "text/html", + t.content_type) + self.failUnless(t.settings["Content-type"] == "text/html", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testRequiredDefaultingProperty(self): + """Testing a required defaulting versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties, + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testClassVersionedPropertyDefinition(self): + """Testing a class-specific _versioned property decorator""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + def _versioned_property(settings_properties= \ + settings_properties, + required_saved_properties= \ + required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"] = \ + required_saved_properties + return versioned_property(**kwargs) + @_versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testMutableChangeHookedProperty(self): + """Testing a mutable change-hooked property""" + SAVES = [] + def prop_log_save_settings(self, old, new, saves=SAVES): + saves.append("'%s' -> '%s'" % (str(old), str(new))) + prop_save_settings(self, old, new) + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="List-type", + doc="A test property", + mutable=True, + change_hook=prop_log_save_settings, + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def list_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._settings_loaded == False, t._settings_loaded) + t.load_settings() + self.failUnless(SAVES == [], SAVES) + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.list_type == None, t.list_type) + self.failUnless(SAVES == [], SAVES) + self.failUnless(t.settings["List-type"]==EMPTY, + t.settings["List-type"]) + t.list_type = [] + self.failUnless(t.settings["List-type"] == [], + t.settings["List-type"]) + self.failUnless(SAVES == [ + "'' -> '[]'" + ], SAVES) + t.list_type.append(5) + self.failUnless(SAVES == [ + "'' -> '[]'", + ], SAVES) + self.failUnless(t.settings["List-type"] == [5], + t.settings["List-type"]) + self.failUnless(SAVES == [ # the append(5) has not yet been saved + "'' -> '[]'", + ], SAVES) + self.failUnless(t.list_type == [5], t.list_type)#get triggers saved + + self.failUnless(SAVES == [ # now the append(5) has been saved. + "'' -> '[]'", + "'[]' -> '[5]'" + ], SAVES) + + unitsuite = unittest.TestLoader().loadTestsFromTestCase( \ + SavedSettingsObjectTests) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/subproc.py b/libbe/subproc.py index fe88206..8806e26 100644 --- a/libbe/subproc.py +++ b/libbe/subproc.py @@ -20,9 +20,11 @@ Functions for running external commands in subprocesses. from subprocess import Popen, PIPE import sys -import doctest +import libbe from encoding import get_encoding +if libbe.TESTING == True: + import doctest _MSWINDOWS = sys.platform == 'win32' _POSIX = not _MSWINDOWS @@ -217,4 +219,5 @@ class Pipe (object): stderrs = read_strings return (stdout, stderrs) -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/tree.py b/libbe/tree.py index 1bfd803..8077da7 100644 --- a/libbe/tree.py +++ b/libbe/tree.py @@ -20,7 +20,9 @@ Define a traversable tree structure. """ -import doctest +import libbe +if libbe.TESTING == True: + import doctest class Tree(list): """ @@ -181,4 +183,5 @@ class Tree(list): return True return False -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/upgrade.py b/libbe/upgrade.py index 4123c72..785249d 100644 --- a/libbe/upgrade.py +++ b/libbe/upgrade.py @@ -20,11 +20,13 @@ Handle conversion between the various on-disk images. import os, os.path import sys -import doctest +import libbe import encoding import mapfile import vcs +if libbe.TESTING == True: + import doctest # a list of all past versions BUGDIR_DISK_VERSIONS = ["Bugs Everywhere Tree 1 0", @@ -184,4 +186,5 @@ def upgrade(path, current_version, break i += 1 -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/utility.py b/libbe/utility.py index 7510b16..f954422 100644 --- a/libbe/utility.py +++ b/libbe/utility.py @@ -27,7 +27,10 @@ import shutil import tempfile import time import types -import doctest + +import libbe +if libbe.TESTING == True: + import doctest class InvalidXML(ValueError): """ @@ -144,4 +147,5 @@ def iterable_full_of_strings(value, alternative=None): return False return True -suite = doctest.DocTestSuite() +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/vcs.py b/libbe/vcs.py index 57a0245..1298a2c 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -33,13 +33,17 @@ from socket import gethostname import shutil import sys import tempfile -import unittest -import doctest +import libbe from utility import Dir, search_parent_directories from subproc import CommandError, invoke from plugin import get_plugin +if libbe.TESTING == True: + import unittest + import doctest + + # List VCS modules in order of preference. # Don't list this module, it is implicitly last. VCS_ORDER = ['arch', 'bzr', 'darcs', 'git', 'hg'] @@ -627,306 +631,307 @@ class VCS(object): return (summary, body) -def setup_vcs_test_fixtures(testcase): - """Set up test fixtures for VCS test case.""" - testcase.vcs = testcase.Class() - testcase.dir = Dir() - testcase.dirname = testcase.dir.path - - vcs_not_supporting_uninitialized_user_id = [] - vcs_not_supporting_set_user_id = ["None", "hg"] - testcase.vcs_supports_uninitialized_user_id = ( - testcase.vcs.name not in vcs_not_supporting_uninitialized_user_id) - testcase.vcs_supports_set_user_id = ( - testcase.vcs.name not in vcs_not_supporting_set_user_id) +if libbe.TESTING == True: + def setup_vcs_test_fixtures(testcase): + """Set up test fixtures for VCS test case.""" + testcase.vcs = testcase.Class() + testcase.dir = Dir() + testcase.dirname = testcase.dir.path - if not testcase.vcs.installed(): - testcase.fail( - "%(name)s VCS not found" % vars(testcase.Class)) + vcs_not_supporting_uninitialized_user_id = [] + vcs_not_supporting_set_user_id = ["None", "hg"] + testcase.vcs_supports_uninitialized_user_id = ( + testcase.vcs.name not in vcs_not_supporting_uninitialized_user_id) + testcase.vcs_supports_set_user_id = ( + testcase.vcs.name not in vcs_not_supporting_set_user_id) - if testcase.Class.name != "None": - testcase.failIf( - testcase.vcs.detect(testcase.dirname), - "Detected %(name)s VCS before initialising" - % vars(testcase.Class)) + if not testcase.vcs.installed(): + testcase.fail( + "%(name)s VCS not found" % vars(testcase.Class)) - testcase.vcs.init(testcase.dirname) + if testcase.Class.name != "None": + testcase.failIf( + testcase.vcs.detect(testcase.dirname), + "Detected %(name)s VCS before initialising" + % vars(testcase.Class)) + testcase.vcs.init(testcase.dirname) -class VCSTestCase(unittest.TestCase): - """Test cases for base VCS class.""" + class VCSTestCase(unittest.TestCase): + """Test cases for base VCS class.""" - Class = VCS + Class = VCS - def __init__(self, *args, **kwargs): - super(VCSTestCase, self).__init__(*args, **kwargs) - self.dirname = None + def __init__(self, *args, **kwargs): + super(VCSTestCase, self).__init__(*args, **kwargs) + self.dirname = None - def setUp(self): - super(VCSTestCase, self).setUp() - setup_vcs_test_fixtures(self) + def setUp(self): + super(VCSTestCase, self).setUp() + setup_vcs_test_fixtures(self) - def tearDown(self): - self.vcs.cleanup() - self.dir.cleanup() - super(VCSTestCase, self).tearDown() + def tearDown(self): + self.vcs.cleanup() + self.dir.cleanup() + super(VCSTestCase, self).tearDown() - def full_path(self, rel_path): - return os.path.join(self.dirname, rel_path) + def full_path(self, rel_path): + return os.path.join(self.dirname, rel_path) -class VCS_init_TestCase(VCSTestCase): - """Test cases for VCS.init method.""" + class VCS_init_TestCase(VCSTestCase): + """Test cases for VCS.init method.""" - def test_detect_should_succeed_after_init(self): - """Should detect VCS in directory after initialization.""" - self.failUnless( - self.vcs.detect(self.dirname), - "Did not detect %(name)s VCS after initialising" - % vars(self.Class)) - - def test_vcs_rootdir_in_specified_root_path(self): - """VCS root directory should be in specified root path.""" - rp = os.path.realpath(self.vcs.rootdir) - dp = os.path.realpath(self.dirname) - vcs_name = self.Class.name - self.failUnless( - dp == rp or rp == None, - "%(vcs_name)s VCS root in wrong dir (%(dp)s %(rp)s)" % vars()) + def test_detect_should_succeed_after_init(self): + """Should detect VCS in directory after initialization.""" + self.failUnless( + self.vcs.detect(self.dirname), + "Did not detect %(name)s VCS after initialising" + % vars(self.Class)) + + def test_vcs_rootdir_in_specified_root_path(self): + """VCS root directory should be in specified root path.""" + rp = os.path.realpath(self.vcs.rootdir) + dp = os.path.realpath(self.dirname) + vcs_name = self.Class.name + self.failUnless( + dp == rp or rp == None, + "%(vcs_name)s VCS root in wrong dir (%(dp)s %(rp)s)" % vars()) -class VCS_get_user_id_TestCase(VCSTestCase): - """Test cases for VCS.get_user_id method.""" + class VCS_get_user_id_TestCase(VCSTestCase): + """Test cases for VCS.get_user_id method.""" - def test_gets_existing_user_id(self): - """Should get the existing user ID.""" - if not self.vcs_supports_uninitialized_user_id: - return + def test_gets_existing_user_id(self): + """Should get the existing user ID.""" + if not self.vcs_supports_uninitialized_user_id: + return - user_id = self.vcs.get_user_id() - self.failUnless( - user_id is not None, - "unable to get a user id") + user_id = self.vcs.get_user_id() + self.failUnless( + user_id is not None, + "unable to get a user id") -class VCS_set_user_id_TestCase(VCSTestCase): - """Test cases for VCS.set_user_id method.""" + class VCS_set_user_id_TestCase(VCSTestCase): + """Test cases for VCS.set_user_id method.""" - def setUp(self): - super(VCS_set_user_id_TestCase, self).setUp() + def setUp(self): + super(VCS_set_user_id_TestCase, self).setUp() - if self.vcs_supports_uninitialized_user_id: - self.prev_user_id = self.vcs.get_user_id() - else: - self.prev_user_id = "Uninitialized identity " - - if self.vcs_supports_set_user_id: - self.test_new_user_id = "John Doe " - self.vcs.set_user_id(self.test_new_user_id) - - def tearDown(self): - if self.vcs_supports_set_user_id: - self.vcs.set_user_id(self.prev_user_id) - super(VCS_set_user_id_TestCase, self).tearDown() - - def test_raises_error_in_unsupported_vcs(self): - """Should raise an error in a VCS that doesn't support it.""" - if self.vcs_supports_set_user_id: - return - self.assertRaises( - SettingIDnotSupported, - self.vcs.set_user_id, "foo") - - def test_updates_user_id_in_supporting_vcs(self): - """Should update the user ID in an VCS that supports it.""" - if not self.vcs_supports_set_user_id: - return - user_id = self.vcs.get_user_id() - self.failUnlessEqual( - self.test_new_user_id, user_id, - "user id not set correctly (expected %s, got %s)" - % (self.test_new_user_id, user_id)) - - -def setup_vcs_revision_test_fixtures(testcase): - """Set up revision test fixtures for VCS test case.""" - testcase.test_dirs = ['a', 'a/b', 'c'] - for path in testcase.test_dirs: - testcase.vcs.mkdir(testcase.full_path(path)) - - testcase.test_files = ['a/text', 'a/b/text'] - - testcase.test_contents = { - 'rev_1': "Lorem ipsum", - 'uncommitted': "dolor sit amet", - } - - -class VCS_mkdir_TestCase(VCSTestCase): - """Test cases for VCS.mkdir method.""" - - def setUp(self): - super(VCS_mkdir_TestCase, self).setUp() - setup_vcs_revision_test_fixtures(self) - - def tearDown(self): - for path in reversed(sorted(self.test_dirs)): - self.vcs.recursive_remove(self.full_path(path)) - super(VCS_mkdir_TestCase, self).tearDown() - - def test_mkdir_creates_directory(self): - """Should create specified directory in filesystem.""" - for path in self.test_dirs: - full_path = self.full_path(path) - self.failUnless( - os.path.exists(full_path), - "path %(full_path)s does not exist" % vars()) - - -class VCS_commit_TestCase(VCSTestCase): - """Test cases for VCS.commit method.""" - - def setUp(self): - super(VCS_commit_TestCase, self).setUp() - setup_vcs_revision_test_fixtures(self) - - def tearDown(self): - for path in reversed(sorted(self.test_dirs)): - self.vcs.recursive_remove(self.full_path(path)) - super(VCS_commit_TestCase, self).tearDown() - - def test_file_contents_as_specified(self): - """Should set file contents as specified.""" - test_contents = self.test_contents['rev_1'] - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents(full_path, test_contents) - current_contents = self.vcs.get_file_contents(full_path) - self.failUnlessEqual(test_contents, current_contents) - - def test_file_contents_as_committed(self): - """Should have file contents as specified after commit.""" - test_contents = self.test_contents['rev_1'] - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents(full_path, test_contents) - revision = self.vcs.commit("Initial file contents.") - current_contents = self.vcs.get_file_contents(full_path) - self.failUnlessEqual(test_contents, current_contents) - - def test_file_contents_as_set_when_uncommitted(self): - """Should set file contents as specified after commit.""" - if not self.vcs.versioned: - return - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents( - full_path, self.test_contents['rev_1']) - revision = self.vcs.commit("Initial file contents.") - self.vcs.set_file_contents( - full_path, self.test_contents['uncommitted']) - current_contents = self.vcs.get_file_contents(full_path) - self.failUnlessEqual( - self.test_contents['uncommitted'], current_contents) - - def test_revision_file_contents_as_committed(self): - """Should get file contents as committed to specified revision.""" - if not self.vcs.versioned: - return - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents( - full_path, self.test_contents['rev_1']) - revision = self.vcs.commit("Initial file contents.") - self.vcs.set_file_contents( - full_path, self.test_contents['uncommitted']) - committed_contents = self.vcs.get_file_contents( - full_path, revision) - self.failUnlessEqual( - self.test_contents['rev_1'], committed_contents) - - def test_revision_id_as_committed(self): - """Check for compatibility between .commit() and .revision_id()""" - if not self.vcs.versioned: - self.failUnlessEqual(self.vcs.revision_id(5), None) - return - committed_revisions = [] - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents( - full_path, self.test_contents['rev_1']) - revision = self.vcs.commit("Initial %s contents." % path) - committed_revisions.append(revision) - self.vcs.set_file_contents( - full_path, self.test_contents['uncommitted']) - revision = self.vcs.commit("Altered %s contents." % path) - committed_revisions.append(revision) - for i,revision in enumerate(committed_revisions): - self.failUnlessEqual(self.vcs.revision_id(i), revision) - i += -len(committed_revisions) # check negative indices - self.failUnlessEqual(self.vcs.revision_id(i), revision) - i = len(committed_revisions) - self.failUnlessEqual(self.vcs.revision_id(i), None) - self.failUnlessEqual(self.vcs.revision_id(-i-1), None) - - def test_revision_id_as_committed(self): - """Check revision id before first commit""" - if not self.vcs.versioned: - self.failUnlessEqual(self.vcs.revision_id(5), None) - return - committed_revisions = [] - for path in self.test_files: - self.failUnlessEqual(self.vcs.revision_id(0), None) - - -class VCS_duplicate_repo_TestCase(VCSTestCase): - """Test cases for VCS.duplicate_repo method.""" - - def setUp(self): - super(VCS_duplicate_repo_TestCase, self).setUp() - setup_vcs_revision_test_fixtures(self) - - def tearDown(self): - self.vcs.remove_duplicate_repo() - for path in reversed(sorted(self.test_dirs)): - self.vcs.recursive_remove(self.full_path(path)) - super(VCS_duplicate_repo_TestCase, self).tearDown() - - def test_revision_file_contents_as_committed(self): - """Should match file contents as committed to specified revision.""" - if not self.vcs.versioned: - return - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents( - full_path, self.test_contents['rev_1']) - revision = self.vcs.commit("Commit current status") - self.vcs.set_file_contents( - full_path, self.test_contents['uncommitted']) - dup_repo_path = self.vcs.duplicate_repo(revision) - dup_file_path = os.path.join(dup_repo_path, path) - dup_file_contents = file(dup_file_path, 'rb').read() + if self.vcs_supports_uninitialized_user_id: + self.prev_user_id = self.vcs.get_user_id() + else: + self.prev_user_id = "Uninitialized identity " + + if self.vcs_supports_set_user_id: + self.test_new_user_id = "John Doe " + self.vcs.set_user_id(self.test_new_user_id) + + def tearDown(self): + if self.vcs_supports_set_user_id: + self.vcs.set_user_id(self.prev_user_id) + super(VCS_set_user_id_TestCase, self).tearDown() + + def test_raises_error_in_unsupported_vcs(self): + """Should raise an error in a VCS that doesn't support it.""" + if self.vcs_supports_set_user_id: + return + self.assertRaises( + SettingIDnotSupported, + self.vcs.set_user_id, "foo") + + def test_updates_user_id_in_supporting_vcs(self): + """Should update the user ID in an VCS that supports it.""" + if not self.vcs_supports_set_user_id: + return + user_id = self.vcs.get_user_id() self.failUnlessEqual( - self.test_contents['rev_1'], dup_file_contents) + self.test_new_user_id, user_id, + "user id not set correctly (expected %s, got %s)" + % (self.test_new_user_id, user_id)) + + + def setup_vcs_revision_test_fixtures(testcase): + """Set up revision test fixtures for VCS test case.""" + testcase.test_dirs = ['a', 'a/b', 'c'] + for path in testcase.test_dirs: + testcase.vcs.mkdir(testcase.full_path(path)) + + testcase.test_files = ['a/text', 'a/b/text'] + + testcase.test_contents = { + 'rev_1': "Lorem ipsum", + 'uncommitted': "dolor sit amet", + } + + + class VCS_mkdir_TestCase(VCSTestCase): + """Test cases for VCS.mkdir method.""" + + def setUp(self): + super(VCS_mkdir_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_mkdir_TestCase, self).tearDown() + + def test_mkdir_creates_directory(self): + """Should create specified directory in filesystem.""" + for path in self.test_dirs: + full_path = self.full_path(path) + self.failUnless( + os.path.exists(full_path), + "path %(full_path)s does not exist" % vars()) + + + class VCS_commit_TestCase(VCSTestCase): + """Test cases for VCS.commit method.""" + + def setUp(self): + super(VCS_commit_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_commit_TestCase, self).tearDown() + + def test_file_contents_as_specified(self): + """Should set file contents as specified.""" + test_contents = self.test_contents['rev_1'] + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents(full_path, test_contents) + current_contents = self.vcs.get_file_contents(full_path) + self.failUnlessEqual(test_contents, current_contents) + + def test_file_contents_as_committed(self): + """Should have file contents as specified after commit.""" + test_contents = self.test_contents['rev_1'] + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents(full_path, test_contents) + revision = self.vcs.commit("Initial file contents.") + current_contents = self.vcs.get_file_contents(full_path) + self.failUnlessEqual(test_contents, current_contents) + + def test_file_contents_as_set_when_uncommitted(self): + """Should set file contents as specified after commit.""" + if not self.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial file contents.") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + current_contents = self.vcs.get_file_contents(full_path) + self.failUnlessEqual( + self.test_contents['uncommitted'], current_contents) + + def test_revision_file_contents_as_committed(self): + """Should get file contents as committed to specified revision.""" + if not self.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial file contents.") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + committed_contents = self.vcs.get_file_contents( + full_path, revision) + self.failUnlessEqual( + self.test_contents['rev_1'], committed_contents) + + def test_revision_id_as_committed(self): + """Check for compatibility between .commit() and .revision_id()""" + if not self.vcs.versioned: + self.failUnlessEqual(self.vcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial %s contents." % path) + committed_revisions.append(revision) + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + revision = self.vcs.commit("Altered %s contents." % path) + committed_revisions.append(revision) + for i,revision in enumerate(committed_revisions): + self.failUnlessEqual(self.vcs.revision_id(i), revision) + i += -len(committed_revisions) # check negative indices + self.failUnlessEqual(self.vcs.revision_id(i), revision) + i = len(committed_revisions) + self.failUnlessEqual(self.vcs.revision_id(i), None) + self.failUnlessEqual(self.vcs.revision_id(-i-1), None) + + def test_revision_id_as_committed(self): + """Check revision id before first commit""" + if not self.vcs.versioned: + self.failUnlessEqual(self.vcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + self.failUnlessEqual(self.vcs.revision_id(0), None) + + + class VCS_duplicate_repo_TestCase(VCSTestCase): + """Test cases for VCS.duplicate_repo method.""" + + def setUp(self): + super(VCS_duplicate_repo_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): self.vcs.remove_duplicate_repo() - - -def make_vcs_testcase_subclasses(vcs_class, namespace): - """Make VCSTestCase subclasses for vcs_class in the namespace.""" - vcs_testcase_classes = [ - c for c in ( - ob for ob in globals().values() if isinstance(ob, type)) - if issubclass(c, VCSTestCase)] - - for base_class in vcs_testcase_classes: - testcase_class_name = vcs_class.__name__ + base_class.__name__ - testcase_class_bases = (base_class,) - testcase_class_dict = dict(base_class.__dict__) - testcase_class_dict['Class'] = vcs_class - testcase_class = type( - testcase_class_name, testcase_class_bases, testcase_class_dict) - setattr(namespace, testcase_class_name, testcase_class) - - -unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) -suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_duplicate_repo_TestCase, self).tearDown() + + def test_revision_file_contents_as_committed(self): + """Should match file contents as committed to specified revision. + """ + if not self.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Commit current status") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + dup_repo_path = self.vcs.duplicate_repo(revision) + dup_file_path = os.path.join(dup_repo_path, path) + dup_file_contents = file(dup_file_path, 'rb').read() + self.failUnlessEqual( + self.test_contents['rev_1'], dup_file_contents) + self.vcs.remove_duplicate_repo() + + + def make_vcs_testcase_subclasses(vcs_class, namespace): + """Make VCSTestCase subclasses for vcs_class in the namespace.""" + vcs_testcase_classes = [ + c for c in ( + ob for ob in globals().values() if isinstance(ob, type)) + if issubclass(c, VCSTestCase)] + + for base_class in vcs_testcase_classes: + testcase_class_name = vcs_class.__name__ + base_class.__name__ + testcase_class_bases = (base_class,) + testcase_class_dict = dict(base_class.__dict__) + testcase_class_dict['Class'] = vcs_class + testcase_class = type( + testcase_class_name, testcase_class_bases, testcase_class_dict) + setattr(namespace, testcase_class_name, testcase_class) + + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/test.py b/test.py index 81674cf..4f20808 100644 --- a/test.py +++ b/test.py @@ -8,6 +8,8 @@ When called with module name arguments, only run the doctests from those modules. """ +import libbe +libbe.TESTING = True from libbe import plugin, vcs import unittest import doctest -- cgit From 864d694c818374ceb4c8e3b34612b0481de17f8d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 3 Dec 2009 21:37:29 -0500 Subject: Don't get VCS version in VCS.__init__(). Often, this just causes a slow subprocess.Popen() call to get information we woln't even look at. Old benchmark: $ time be list > /dev/null real 0m2.369s user 0m1.980s sys 0m0.388s New benchmark: $ time be list > /dev/null real 0m1.472s user 0m1.304s sys 0m0.164s --- libbe/vcs.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libbe/vcs.py b/libbe/vcs.py index 1298a2c..44643a4 100644 --- a/libbe/vcs.py +++ b/libbe/vcs.py @@ -127,7 +127,6 @@ class VCS(object): self._duplicateBasedir = None self._duplicateDirname = None self.encoding = encoding - self.version = self._get_version() def __str__(self): return "<%s %s>" % (self.__class__.__name__, id(self)) def __repr__(self): @@ -235,6 +234,11 @@ class VCS(object): specified revision does not exist. """ return None + def version(self): + """Cache version string for efficiency.""" + if not hasattr(self, '_version'): + self._version = self._get_version() + return self._version def _get_version(self): try: ret = self._vcs_version() @@ -247,7 +251,7 @@ class VCS(object): except CommandError: return None def installed(self): - if self.version != None: + if self.version() != None: return True return False def detect(self, path="."): -- cgit From 8bcbda7e3c022b6d63d86123b3aacabbe6c21ec1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 3 Dec 2009 22:05:23 -0500 Subject: Don't necessarily initialize a new VCS in BugDir.load_settings(). We may already have the right type, in which case, don't mess with it. This speeds up bugdir loading a bit more: $ time be list > /dev/null real 0m1.245s user 0m1.116s sys 0m0.124s --- libbe/bugdir.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libbe/bugdir.py b/libbe/bugdir.py index d78e761..7005181 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -413,8 +413,9 @@ settings easy. Don't set this attribute. Set .vcs instead, and self._setup_encoding(self.encoding) self._setup_severities(self.severities) self._setup_status(self.active_status, self.inactive_status) - self.vcs = vcs.vcs_by_name(self.vcs_name) - self._setup_user_id(self.user_id) + if self.vcs_name != self.vcs.name: + self.vcs = vcs.vcs_by_name(self.vcs_name) + self._setup_user_id(self.user_id) def save_settings(self): settings = self._get_saved_settings() -- cgit From 4cb0182da105d18f067c1a10482c89c966b02827 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 4 Dec 2009 22:44:12 -0500 Subject: Use __cmp__ instead of __eq__ for Tree comparison. This ensures that __ne__ will also work, and makes it easier to subclass Tree. For example, in the previous implementation you could have >>> commA == commB False >>> cmp(commA, commB) 0 if the comments had different ids, but equivalent content. At the user-interface level, this removes some false "modified comments" from `be diff`. --- libbe/tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libbe/tree.py b/libbe/tree.py index 8077da7..d3f6bcd 100644 --- a/libbe/tree.py +++ b/libbe/tree.py @@ -84,8 +84,8 @@ class Tree(list): >>> a.has_descendant(a, match_self=True) True """ - def __eq__(self, other): - return id(self) == id(other) + def __cmp__(self, other): + return cmp(id(self), id(other)) def branch_len(self): """ -- cgit From 2d0bc3a0c2497be662a1742459622d2c37cce415 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 4 Dec 2009 23:30:32 -0500 Subject: Fixed removed comment listing in libbe/diff.py. Previous implementation guaranteed to raise KeyErrors, because new bug doesn't contain the uuid. --- libbe/diff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libbe/diff.py b/libbe/diff.py index 4c22597..c25f7a7 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -263,8 +263,8 @@ class Diff (object): modified.append((old_comment, new_comment)) for uuid in old_comment_ids: if uuid not in new_comment_ids: - new_comment = new.comment_from_uuid(uuid) - removed.append(new_comment) + old_comment = old.comment_from_uuid(uuid) + removed.append(old_comment) self.__changed_comments[new.uuid] = (added, modified, removed) return self.__changed_comments[new.uuid] def _attribute_changes(self, old, new, attributes): -- cgit From f294b8793f241033d57431026aea12bb39a20250 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 4 Dec 2009 23:33:25 -0500 Subject: Added --dir option to `be diff'. Now you can compare two repositories: be -d branchA diff -d branchB or branchA$ be diff -d ../branchB which is helpful for VCSs like bzr that lack cross-branch revid visibility. Git users can still use branchA$ be diff REVID where REVID is a commit from any branch in the repo. This new functionality acts like a BE counterpart to `bzr missing DIR'. --- becommands/diff.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/becommands/diff.py b/becommands/diff.py index 6477934..8e6c0f8 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -65,9 +65,19 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if bd.vcs.versioned == False: print "This directory is not revision-controlled." else: - if revision == None: # get the most recent revision - revision = bd.vcs.revision_id(-1) - old_bd = bd.duplicate_bugdir(revision) + if options.dir == None: + if revision == None: # get the most recent revision + revision = bd.vcs.revision_id(-1) + old_bd = bd.duplicate_bugdir(revision) + else: + cwd = os.getcwd() + os.chdir(options.dir) + old_bd_current = bugdir.BugDir(from_disk=True, manipulate_encodings=False) + if revision == None: # use the current working state + old_bd = old_bd_current + else: + old_bd = old_bd_current.duplicate_bugdir(revision) + os.chdir(cwd) d = diff.Diff(old_bd, bd) tree = d.report_tree() @@ -87,6 +97,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if rep != None: print rep bd.remove_duplicate_bugdir() + if options.dir != None and revision != None: + old_bd_current.remove_duplicate_bugdir() def get_parser(): parser = cmdutil.CmdOptionParser("be diff [options] REVISION") @@ -102,6 +114,8 @@ def get_parser(): help = s[2] parser.add_option(short, long, action="store_true", default=False, dest=attr, help=help) + parser.add_option("-d", "--dir", dest="dir", metavar="DIR", + help="Compare with repository in DIR instead of the current directory.") return parser longhelp=""" -- cgit From efb79f10cec777dfa7fdced96947f7f75119ba69 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 4 Dec 2009 23:39:15 -0500 Subject: Updated NEWS --- NEWS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS b/NEWS index 1f71af6..19a4fe1 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,6 @@ +December 4, 2009 + * added --dir option to `be diff' + November 17, 2009 * new becommands: commit -- cgit From a04fc28b3b840541dde0366b81fc6758823cfca7 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 4 Dec 2009 23:53:27 -0500 Subject: Updated NEWS --- NEWS | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/NEWS b/NEWS index 1f71af6..ab962d4 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,13 @@ +December 4, 2009: + * new commands: + email-bugs + * broke `be comment --xml` out and extended into `be import-xml` + * new XML format + * interfaces/email/interactive: + * added support for [be-bug:xml] interface + * improved security with restrict_file_access + * assorted cleanups, bugfixes, and optimizations + November 17, 2009 * new becommands: commit -- cgit From 282d5cf934eec5c0ae02a01c345c38d0ad7c9fa7 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 00:21:35 -0500 Subject: Add __eq__ and __ne__ methods to Tree. This fixes a bug introduced by revision-id: wking@drexel.edu-20091205034412-8apqxq8zqim48tf7 committer: W. Trevor King timestamp: Fri 2009-12-04 22:44:12 -0500 message: Use __cmp__ instead of __eq__ for Tree comparison. When I made that commit, I'd forgotten that Tree inherits an __eq__ method from list, so it won't fall back to the __cmp__ method to determine equality. The new __eq__ and __ne__ methods use __cmp__ internally, so further subclasses (e.g. Comment) only need to override __cmp__. Of course, list also defines __ge__, __gt__, __le__, __lt__, ... which I don't bother with, so stay away from TreeA > TreeB and the like. --- libbe/tree.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libbe/tree.py b/libbe/tree.py index d3f6bcd..1daac44 100644 --- a/libbe/tree.py +++ b/libbe/tree.py @@ -87,6 +87,12 @@ class Tree(list): def __cmp__(self, other): return cmp(id(self), id(other)) + def __eq__(self, other): + return self.__cmp__(other) == 0 + + def __ne__(self, other): + return self.__cmp__(other) != 0 + def branch_len(self): """ Exhaustive search every time == SLOW. -- cgit From 69300422c6825af80574545b11d7565aa8f38ee3 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 00:57:43 -0500 Subject: Fix cmdutil.help() calls in be to use args not sys.argv. sys.argv won't work if there are any options in the be call, e.g. be -d DIR diff --- be | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/be b/be index 8fe7dca..c5c372f 100755 --- a/be +++ b/be @@ -64,7 +64,7 @@ try: raise cmdutil.UsageError, "must supply a command" sys.exit(cmdutil.execute(args[0], args[1:])) except cmdutil.GetHelp: - print cmdutil.help(sys.argv[1]) + print cmdutil.help(args[0]) sys.exit(0) except cmdutil.GetCompletions, e: print '\n'.join(e.completions) @@ -78,7 +78,7 @@ except cmdutil.UsageError, e: print cmdutil.help(parser=parser) else: print "\nArgs:", args - print cmdutil.help(sys.argv[1]) + print cmdutil.help(args[0]) sys.exit(1) except cmdutil.UserError, e: print "ERROR:" -- cgit From 1bbf068f28a6c05da563bc1224a4456f635227a4 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 01:00:35 -0500 Subject: `be diff` raises UsageError if required revision control not possible. It had previously printed an message and exitted without error. --- becommands/diff.py | 71 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/becommands/diff.py b/becommands/diff.py index 8e6c0f8..f581ace 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -63,42 +63,45 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) if bd.vcs.versioned == False: - print "This directory is not revision-controlled." + raise cmdutil.UsageError("This directory is not revision-controlled.") + if options.dir == None: + if revision == None: # get the most recent revision + revision = bd.vcs.revision_id(-1) + old_bd = bd.duplicate_bugdir(revision) else: - if options.dir == None: - if revision == None: # get the most recent revision - revision = bd.vcs.revision_id(-1) - old_bd = bd.duplicate_bugdir(revision) + cwd = os.getcwd() + os.chdir(options.dir) + old_bd_current = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + if revision == None: # use the current working state + old_bd = old_bd_current else: - cwd = os.getcwd() - os.chdir(options.dir) - old_bd_current = bugdir.BugDir(from_disk=True, manipulate_encodings=False) - if revision == None: # use the current working state - old_bd = old_bd_current - else: - old_bd = old_bd_current.duplicate_bugdir(revision) - os.chdir(cwd) - d = diff.Diff(old_bd, bd) - tree = d.report_tree() + if old_bd_current.vcs.versioned == False: + raise cmdutil.UsageError("%s is not revision-controlled." + % options.dir) + old_bd = old_bd_current.duplicate_bugdir(revision) + os.chdir(cwd) + d = diff.Diff(old_bd, bd) + tree = d.report_tree() - uuids = [] - if options.all == True: - options.new = options.modified = options.removed = True - if options.new == True: - uuids.extend([c.name for c in tree.child_by_path("/bugs/new")]) - if options.modified == True: - uuids.extend([c.name for c in tree.child_by_path("/bugs/mod")]) - if options.removed == True: - uuids.extend([c.name for c in tree.child_by_path("/bugs/rem")]) - if (options.new or options.modified or options.removed) == True: - print "\n".join(uuids) - else : - rep = tree.report_string() - if rep != None: - print rep - bd.remove_duplicate_bugdir() - if options.dir != None and revision != None: - old_bd_current.remove_duplicate_bugdir() + uuids = [] + if options.all == True: + options.new = options.modified = options.removed = True + if options.new == True: + uuids.extend([c.name for c in tree.child_by_path("/bugs/new")]) + if options.modified == True: + uuids.extend([c.name for c in tree.child_by_path("/bugs/mod")]) + if options.removed == True: + uuids.extend([c.name for c in tree.child_by_path("/bugs/rem")]) + if (options.new or options.modified or options.removed) == True: + print "\n".join(uuids) + else : + rep = tree.report_string() + if rep != None: + print rep + bd.remove_duplicate_bugdir() + if options.dir != None and revision != None: + old_bd_current.remove_duplicate_bugdir() def get_parser(): parser = cmdutil.CmdOptionParser("be diff [options] REVISION") @@ -128,7 +131,7 @@ For Arch your specifier must be a fully-qualified revision name. Besides the standard summary output, you can use the options to output UUIDS for the different categories. This output can be used as the -input to 'be show' to get and understanding of the current status. +input to 'be show' to get an understanding of the current status. """ def help(): -- cgit From 49b8c7e7da7fa3a4f3c6092cf0bfdb1c1de863e8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 01:10:01 -0500 Subject: Set boolean options default to False in becommands/list.py Otherwise they default to None. It doesn't matter at the moment, since all the comparisons seem to be if options.XYZ == True: but this protects against confusion in the future if someone tries if options.XYZ == False: --- becommands/list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/becommands/list.py b/becommands/list.py index 4711789..2749228 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -196,7 +196,7 @@ def get_parser(): long = "--%s" % s[1] help = s[2] parser.add_option(short, long, action="store_true", - dest=attr, help=help) + dest=attr, help=help, default=False) return parser -- cgit From 281e98e998b4a1ec550c6702aee0eead003905be Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 01:30:30 -0500 Subject: Replaced `be diff` options --new, --removed, --modified, and --all with --uuids. We'll be adding a --subscribe option which will select the bugs/changes we're interested in, which deprecates the selection portion of the old options. The new --uuids just selects the "bug uuid" output over the default "change summary" output. --- becommands/diff.py | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/becommands/diff.py b/becommands/diff.py index f581ace..aebbfdb 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -84,16 +84,11 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): d = diff.Diff(old_bd, bd) tree = d.report_tree() - uuids = [] - if options.all == True: - options.new = options.modified = options.removed = True - if options.new == True: - uuids.extend([c.name for c in tree.child_by_path("/bugs/new")]) - if options.modified == True: - uuids.extend([c.name for c in tree.child_by_path("/bugs/mod")]) - if options.removed == True: - uuids.extend([c.name for c in tree.child_by_path("/bugs/rem")]) - if (options.new or options.modified or options.removed) == True: + if options.uuids == True: + uuids = [] + bugs = tree.child_by_path("/bugs") + for bug_type in bugs: + uuids.extend([bug.name for bug in bug_type]) print "\n".join(uuids) else : rep = tree.report_string() @@ -105,20 +100,10 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): def get_parser(): parser = cmdutil.CmdOptionParser("be diff [options] REVISION") - # boolean options - bools = (("n", "new", "Print UUIDS for new bugs"), - ("m", "modified", "Print UUIDS for modified bugs"), - ("r", "removed", "Print UUIDS for removed bugs"), - ("a", "all", "Print UUIDS for all changed bugs")) - 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", - default=False, dest=attr, help=help) parser.add_option("-d", "--dir", dest="dir", metavar="DIR", help="Compare with repository in DIR instead of the current directory.") + parser.add_option("-u", "--uuids", action="store_true", dest="uuids", + help="Only print the bug UUIDS.", default=False) return parser longhelp=""" -- cgit From 129c100046231ed15d2f16eaa90b5c01e41a442c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 01:58:41 -0500 Subject: Moved subscription types from becommands/subscribe.py to libbe/diff.py. --- becommands/subscribe.py | 105 ++++++++++------------------ interfaces/email/interactive/be-handle-mail | 8 +-- libbe/diff.py | 46 ++++++++++++ 3 files changed, 86 insertions(+), 73 deletions(-) diff --git a/becommands/subscribe.py b/becommands/subscribe.py index 051341b..4220c37 100644 --- a/becommands/subscribe.py +++ b/becommands/subscribe.py @@ -14,47 +14,12 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """(Un)subscribe to change notification""" -from libbe import cmdutil, bugdir, tree +from libbe import cmdutil, bugdir, tree, diff import os, copy __desc__ = __doc__ TAG="SUBSCRIBE:" -class SubscriptionType (tree.Tree): - """ - Trees of subscription types to allow users to select exactly what - notifications they want to subscribe to. - """ - def __init__(self, type_name, *args, **kwargs): - tree.Tree.__init__(self, *args, **kwargs) - self.type = type_name - def __str__(self): - return self.type - def __repr__(self): - return "" % str(self) - def string_tree(self, indent=0): - lines = [] - for depth,node in self.thread(): - lines.append("%s%s" % (" "*(indent+2*depth), node)) - return "\n".join(lines) - -BUGDIR_TYPE_NEW = SubscriptionType("new") -BUGDIR_TYPE_ALL = SubscriptionType("all", [BUGDIR_TYPE_NEW]) - -# same name as BUGDIR_TYPE_ALL for consistency -BUG_TYPE_ALL = SubscriptionType(str(BUGDIR_TYPE_ALL)) - -INVALID_TYPE = SubscriptionType("INVALID") - -class InvalidType (ValueError): - def __init__(self, type_name, type_root): - msg = "Invalid type %s for tree:\n%s" \ - % (type_name, type_root.string_tree(4)) - ValueError.__init__(self, msg) - self.type_name = type_name - self.type_root = type_root - - def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> bd = bugdir.SimpleBugDir() @@ -127,19 +92,19 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): types = options.types.split(",") if len(args) == 0 or args[0] == "DIR": # directory-wide subscriptions - type_root = BUGDIR_TYPE_ALL + type_root = diff.BUGDIR_TYPE_ALL entity = bd entity_name = "bug directory" else: # bug-specific subscriptions - type_root = BUG_TYPE_ALL + type_root = diff.BUG_TYPE_ALL bug = bd.bug_from_shortname(args[0]) entity = bug entity_name = bug.uuid if options.list_all == True: entity_name = "anything in the bug directory" - types = [type_from_name(name, type_root, default=INVALID_TYPE, - default_ok=options.unsubscribe) + types = [diff.type_from_name(name, type_root, default=diff.INVALID_TYPE, + default_ok=options.unsubscribe) for name in types] estrs = entity.extra_strings if options.list == True or options.list_all == True: @@ -210,8 +175,8 @@ if you're just hacking away on your private repository, you'll known what's changed ;). This command just (un)sets the appropriate subscriptions, and leaves it up to each interface to perform the notification. -""" % (BUG_TYPE_ALL.string_tree(6), BUGDIR_TYPE_ALL.string_tree(6), - BUGDIR_TYPE_ALL) +""" % (diff.BUG_TYPE_ALL.string_tree(6), diff.BUGDIR_TYPE_ALL.string_tree(6), + diff.BUGDIR_TYPE_ALL) def help(): return get_parser().help_str() + longhelp @@ -227,7 +192,7 @@ def _parse_string(string, type_root): assert string.startswith(TAG), string string = string[len(TAG):] subscriber,types,servers = string.split("\t") - types = [type_from_name(name, type_root) for name in types.split(",")] + types = [diff.type_from_name(name, type_root) for name in types.split(",")] return (subscriber,types,servers.split(",")) def _get_subscriber(extra_strings, subscriber, type_root): @@ -240,16 +205,6 @@ def _get_subscriber(extra_strings, subscriber, type_root): # functions exposed to other modules -def type_from_name(name, type_root, default=None, default_ok=False): - if name == str(type_root): - return type_root - for t in type_root.traverse(): - if name == str(t): - return t - if default_ok: - return default - raise InvalidType(name, type_root) - def subscribe(extra_strings, subscriber, types, servers, type_root): args = _get_subscriber(extra_strings, subscriber, type_root) if args == None: # no match @@ -311,17 +266,22 @@ def get_subscribers(extra_strings, type, server, type_root, >>> def sgs(*args, **kwargs): ... return sorted(get_subscribers(*args, **kwargs)) >>> es = [] - >>> es = subscribe(es, "John Doe ", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL) - >>> es = subscribe(es, "Jane Doe ", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL) - >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL) + >>> es = subscribe(es, "John Doe ", [diff.BUGDIR_TYPE_ALL], + ... ["a.com"], diff.BUGDIR_TYPE_ALL) + >>> es = subscribe(es, "Jane Doe ", [diff.BUGDIR_TYPE_NEW], + ... ["*"], diff.BUGDIR_TYPE_ALL) + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL) ['John Doe '] - >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL, match_descendant_types=True) + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL, + ... match_descendant_types=True) ['Jane Doe ', 'John Doe '] - >>> sgs(es, BUGDIR_TYPE_ALL, "b.net", BUGDIR_TYPE_ALL, match_descendant_types=True) + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "b.net", diff.BUGDIR_TYPE_ALL, + ... match_descendant_types=True) ['Jane Doe '] - >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL) + >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL) ['Jane Doe '] - >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL, match_ancestor_types=True) + >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL, + ... match_ancestor_types=True) ['Jane Doe ', 'John Doe '] """ for string in extra_strings: @@ -360,9 +320,12 @@ def get_bugdir_subscribers(bugdir, server): >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) >>> a = bd.bug_from_shortname("a") - >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe ", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL) - >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe ", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL) - >>> a.extra_strings = subscribe(a.extra_strings, "John Doe ", [BUG_TYPE_ALL], ["a.com"], BUG_TYPE_ALL) + >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe ", + ... [diff.BUGDIR_TYPE_ALL], ["a.com"], diff.BUGDIR_TYPE_ALL) + >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe ", + ... [diff.BUGDIR_TYPE_NEW], ["*"], diff.BUGDIR_TYPE_ALL) + >>> a.extra_strings = subscribe(a.extra_strings, "John Doe ", + ... [diff.BUG_TYPE_ALL], ["a.com"], diff.BUG_TYPE_ALL) >>> subscribers = get_bugdir_subscribers(bd, "a.com") >>> subscribers["Jane Doe "]["DIR"] [] @@ -375,14 +338,18 @@ def get_bugdir_subscribers(bugdir, server): >>> bd.cleanup() """ subscribers = {} - for sub in get_subscribers(bugdir.extra_strings, BUGDIR_TYPE_ALL, server, - BUGDIR_TYPE_ALL, match_descendant_types=True): - i,s,ts,srvs = _get_subscriber(bugdir.extra_strings,sub,BUGDIR_TYPE_ALL) + for sub in get_subscribers(bugdir.extra_strings, diff.BUGDIR_TYPE_ALL, + server, diff.BUGDIR_TYPE_ALL, + match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bugdir.extra_strings, sub, + diff.BUGDIR_TYPE_ALL) subscribers[sub] = {"DIR":ts} for bug in bugdir: - for sub in get_subscribers(bug.extra_strings, BUG_TYPE_ALL, server, - BUG_TYPE_ALL, match_descendant_types=True): - i,s,ts,srvs = _get_subscriber(bug.extra_strings,sub,BUG_TYPE_ALL) + for sub in get_subscribers(bug.extra_strings, diff.BUG_TYPE_ALL, + server, diff.BUG_TYPE_ALL, + match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bug.extra_strings, sub, + diff.BUG_TYPE_ALL) if sub in subscribers: subscribers[sub][bug.uuid] = ts else: diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index 3b321cf..952c16d 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -685,21 +685,21 @@ class Message (object): ordered_subscriptions.extend(subscriptions.items()) for id,types in ordered_subscriptions: if id == "DIR": - if subscribe.BUGDIR_TYPE_ALL in types: + if libbe.diff.BUGDIR_TYPE_ALL in types: parts.append(diff_tree.report_or_none()) break # we've attached everything, so stop checking. - if subscribe.BUGDIR_TYPE_NEW in types: + if libbe.diff.BUGDIR_TYPE_NEW in types: new = diff_tree.child_by_path("/bugs/new") parts.append(new.report_or_none()) continue # move on to next id # if we get this far, id refers to a bug. - assert types == [subscribe.BUG_TYPE_ALL], types + assert types == [libbe.diff.BUG_TYPE_ALL], types if id not in bug_index: continue # no changes here, move on to next id type,bug_root = bug_index[id] if type == "added" \ and "DIR" in subscriptions \ - and subscriptions["DIR"] == subscribe.BUGDIR_TYPE_NEW: + and subscriptions["DIR"] == libbe.diff.BUGDIR_TYPE_NEW: # this info already attached at the DIR level continue # move on to next id parts.append(bug_root.report_or_none()) diff --git a/libbe/diff.py b/libbe/diff.py index c25f7a7..66d9f4b 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -27,6 +27,52 @@ if libbe.TESTING == True: import doctest +class SubscriptionType (tree.Tree): + """ + Trees of subscription types to allow users to select exactly what + notifications they want to subscribe to. + """ + def __init__(self, type_name, *args, **kwargs): + tree.Tree.__init__(self, *args, **kwargs) + self.type = type_name + def __str__(self): + return self.type + def __repr__(self): + return "" % str(self) + def string_tree(self, indent=0): + lines = [] + for depth,node in self.thread(): + lines.append("%s%s" % (" "*(indent+2*depth), node)) + return "\n".join(lines) + +BUGDIR_TYPE_NEW = SubscriptionType("new") +BUGDIR_TYPE_ALL = SubscriptionType("all", [BUGDIR_TYPE_NEW]) + +# same name as BUGDIR_TYPE_ALL for consistency +BUG_TYPE_ALL = SubscriptionType(str(BUGDIR_TYPE_ALL)) + +INVALID_TYPE = SubscriptionType("INVALID") + +class InvalidType (ValueError): + def __init__(self, type_name, type_root): + msg = "Invalid type %s for tree:\n%s" \ + % (type_name, type_root.string_tree(4)) + ValueError.__init__(self, msg) + self.type_name = type_name + self.type_root = type_root + +def type_from_name(name, type_root, default=None, default_ok=False): + if name == str(type_root): + return type_root + for t in type_root.traverse(): + if name == str(t): + return t + if default_ok: + return default + raise InvalidType(name, type_root) + + + class DiffTree (tree.Tree): """ A tree holding difference data for easy report generation. -- cgit From 1f59c7ef9019879d0b5e407492e4a6e04c5a29cc Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 03:22:25 -0500 Subject: Added subscriptions option to diff.Diff.report_tree(). Also added diff.BUGDIR_ID to avoid lots of magic 'DIR' definitions, and added diff.Subscription class to make the old (id, type) tuples a bit more elegant. --- becommands/subscribe.py | 5 ++- libbe/diff.py | 97 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 22 deletions(-) diff --git a/becommands/subscribe.py b/becommands/subscribe.py index 4220c37..3f4998e 100644 --- a/becommands/subscribe.py +++ b/becommands/subscribe.py @@ -163,7 +163,7 @@ you of changes, although there is no way to guarantee this behavior. Available TYPES: For bugs: %s - For DIR : + For %s: %s For unsubscription, any listed SERVERS and TYPES are removed from your @@ -175,7 +175,8 @@ if you're just hacking away on your private repository, you'll known what's changed ;). This command just (un)sets the appropriate subscriptions, and leaves it up to each interface to perform the notification. -""" % (diff.BUG_TYPE_ALL.string_tree(6), diff.BUGDIR_TYPE_ALL.string_tree(6), +""" % (diff.BUG_TYPE_ALL.string_tree(6), diff.BUGDIR_ID, + diff.BUGDIR_TYPE_ALL.string_tree(6), diff.BUGDIR_TYPE_ALL) def help(): diff --git a/libbe/diff.py b/libbe/diff.py index 66d9f4b..b5384a8 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -19,6 +19,7 @@ """Compare two bug trees.""" import difflib +import types import libbe from libbe import bugdir, bug, settings_object, tree @@ -45,6 +46,7 @@ class SubscriptionType (tree.Tree): lines.append("%s%s" % (" "*(indent+2*depth), node)) return "\n".join(lines) +BUGDIR_ID = "DIR" BUGDIR_TYPE_NEW = SubscriptionType("new") BUGDIR_TYPE_ALL = SubscriptionType("all", [BUGDIR_TYPE_NEW]) @@ -71,7 +73,32 @@ def type_from_name(name, type_root, default=None, default_ok=False): return default raise InvalidType(name, type_root) - +class Subscription (object): + """ + >>> subscriptions = [Subscription('XYZ', 'all', type_root=BUG_TYPE_ALL), + ... Subscription('DIR', 'new', type_root=BUGDIR_TYPE_ALL), + ... Subscription('ABC', BUG_TYPE_ALL),] + >>> print sorted(subscriptions) + [, , ] + """ + def __init__(self, id, subscription_type, **kwargs): + if type(subscription_type) in types.StringTypes: + subscription_type = type_from_name(subscription_type, **kwargs) + self.id = id + self.type = subscription_type + def __cmp__(self, other): + for attr in 'id', 'type': + value = cmp(getattr(self, attr), getattr(other, attr)) + if value != 0: + if self.id == BUGDIR_ID: + return -1 + elif other.id == BUGDIR_ID: + return 1 + return value + def __str__(self): + return str(self.type) + def __repr__(self): + return "" % (self.id, self.type) class DiffTree (tree.Tree): """ @@ -233,6 +260,19 @@ class Diff (object): New comments: from John Doe on Thu, 01 Jan 1970 00:00:00 +0000 I'm closing this bug... + + You can also limit the report generation by providing a list of + subscriptions. + + >>> subscriptions = [Subscription('DIR', BUGDIR_TYPE_NEW), + ... Subscription('b', BUG_TYPE_ALL)] + >>> r = d.report_tree(subscriptions) + >>> print r.report_string() + New bugs: + c:om: Bug C + Removed bugs: + b:cm: Bug B + >>> bd.cleanup() """ def __init__(self, old_bugdir, new_bugdir): @@ -241,7 +281,7 @@ class Diff (object): # data assembly methods - def _changed_bugs(self): + def _changed_bugs(self, subscriptions): """ Search for differences in all bugs between .old_bugdir and .new_bugdir. Returns @@ -250,33 +290,48 @@ class Diff (object): removed bugs respectively. modified_bugs is a list of (old_bug,new_bug) pairs. """ - if hasattr(self, "__changed_bugs"): - return self.__changed_bugs + bugdir_types = [s.type for s in subscriptions if s.id == BUGDIR_ID] + if BUGDIR_TYPE_ALL in bugdir_types: + new_uuids = list(self.new_bugdir.uuids()) + old_uuids = list(self.old_bugdir.uuids()) + elif BUGDIR_TYPE_NEW in bugdir_types: + new_uuids = list(self.new_bugdir.uuids()) + old_uuids = [] + subscribed_bugs = [s.id for s in subscriptions + if BUG_TYPE_ALL.has_descendant( \ + s.type, match_self=True)] + new_uuids.extend([s for s in subscribed_bugs + if self.new_bugdir.has_bug(s)]) + new_uuids = sorted(set(new_uuids)) + old_uuids.extend([s for s in subscribed_bugs + if self.old_bugdir.has_bug(s)]) + old_uuids = sorted(set(old_uuids)) added = [] removed = [] modified = [] - for uuid in self.new_bugdir.uuids(): + for uuid in new_uuids: new_bug = self.new_bugdir.bug_from_uuid(uuid) try: old_bug = self.old_bugdir.bug_from_uuid(uuid) except KeyError: added.append(new_bug) - else: + continue + if BUGDIR_TYPE_ALL in bugdir_types \ + or uuid in subscribed_bugs: if old_bug.sync_with_disk == True: old_bug.load_comments() if new_bug.sync_with_disk == True: new_bug.load_comments() if old_bug != new_bug: modified.append((old_bug, new_bug)) - for uuid in self.old_bugdir.uuids(): + for uuid in old_uuids: if not self.new_bugdir.has_bug(uuid): old_bug = self.old_bugdir.bug_from_uuid(uuid) removed.append(old_bug) added.sort() removed.sort() modified.sort(self._bug_modified_cmp) - self.__changed_bugs = (added, modified, removed) - return self.__changed_bugs + return (added, modified, removed) def _bug_modified_cmp(self, left, right): return cmp(left[1], right[1]) def _changed_comments(self, old, new): @@ -348,25 +403,28 @@ class Diff (object): # report generation methods - def report_tree(self, diff_tree=DiffTree): + def report_tree(self, subscriptions=None, diff_tree=DiffTree): """ Pretty bare to make it easy to adjust to specific cases. You can pass in a DiffTree subclass via diff_tree to override the default report assembly process. """ - if hasattr(self, "__report_tree"): - return self.__report_tree + if subscriptions == None: + subscriptions = [Subscription(BUGDIR_ID, BUGDIR_TYPE_ALL)] bugdir_settings = sorted(self.new_bugdir.settings_properties) bugdir_settings.remove("vcs_name") # tweaked by bugdir.duplicate_bugdir root = diff_tree("bugdir") - bugdir_attribute_changes = self._bugdir_attribute_changes() - if len(bugdir_attribute_changes) > 0: - bugdir = diff_tree("settings", bugdir_attribute_changes, - self.bugdir_attribute_change_string) - root.append(bugdir) + bugdir_subscriptions = [s.type for s in subscriptions + if s.id == BUGDIR_ID] + if BUGDIR_TYPE_ALL in bugdir_subscriptions: + bugdir_attribute_changes = self._bugdir_attribute_changes() + if len(bugdir_attribute_changes) > 0: + bugdir = diff_tree("settings", bugdir_attribute_changes, + self.bugdir_attribute_change_string) + root.append(bugdir) bug_root = diff_tree("bugs") root.append(bug_root) - add,mod,rem = self._changed_bugs() + add,mod,rem = self._changed_bugs(subscriptions) bnew = diff_tree("new", "New bugs:", requires_children=True) bug_root.append(bnew) for bug in add: @@ -416,8 +474,7 @@ class Diff (object): self.comment_body_change_string) c.append(cbody) cr.extend([cnew, crem, cmod]) - self.__report_tree = root - return self.__report_tree + return root # change data -> string methods. # Feel free to play with these in subclasses. -- cgit From f7f0d9c959aee752298fdfe7a73939cf4c09fda5 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 03:32:06 -0500 Subject: Adjusted diff.Subscription.__init__() to guess type_root if required. --- libbe/diff.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/libbe/diff.py b/libbe/diff.py index b5384a8..73db13d 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -75,13 +75,18 @@ def type_from_name(name, type_root, default=None, default_ok=False): class Subscription (object): """ - >>> subscriptions = [Subscription('XYZ', 'all', type_root=BUG_TYPE_ALL), - ... Subscription('DIR', 'new', type_root=BUGDIR_TYPE_ALL), + >>> subscriptions = [Subscription('XYZ', 'all'), + ... Subscription('DIR', 'new'), ... Subscription('ABC', BUG_TYPE_ALL),] >>> print sorted(subscriptions) [, , ] """ def __init__(self, id, subscription_type, **kwargs): + if 'type_root' not in kwargs: + if id == BUGDIR_ID: + kwargs['type_root'] = BUGDIR_TYPE_ALL + else: + kwargs['type_root'] = BUG_TYPE_ALL if type(subscription_type) in types.StringTypes: subscription_type = type_from_name(subscription_type, **kwargs) self.id = id -- cgit From 06f4869e74ed800156fb4c46116741a17fc04ef1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 03:53:26 -0500 Subject: Added BUGDIR_TYPE_MOD and BUGDIR_TYPE_REM to libbe.diff. Now you can subscribe to only hear about modified bugs or only about removed bugs. Kindof odd for a general subscription, but possibly useful as an argument to the upcoming `be diff --subscribe`, e.g. be diff --subscribe DIR:mod which would replace the old be diff --modified --- libbe/diff.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/libbe/diff.py b/libbe/diff.py index 73db13d..3122fe8 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -48,7 +48,10 @@ class SubscriptionType (tree.Tree): BUGDIR_ID = "DIR" BUGDIR_TYPE_NEW = SubscriptionType("new") -BUGDIR_TYPE_ALL = SubscriptionType("all", [BUGDIR_TYPE_NEW]) +BUGDIR_TYPE_MOD = SubscriptionType("mod") +BUGDIR_TYPE_REM = SubscriptionType("rem") +BUGDIR_TYPE_ALL = SubscriptionType("all", + [BUGDIR_TYPE_NEW, BUGDIR_TYPE_MOD, BUGDIR_TYPE_REM]) # same name as BUGDIR_TYPE_ALL for consistency BUG_TYPE_ALL = SubscriptionType(str(BUGDIR_TYPE_ALL)) @@ -296,12 +299,16 @@ class Diff (object): (old_bug,new_bug) pairs. """ bugdir_types = [s.type for s in subscriptions if s.id == BUGDIR_ID] - if BUGDIR_TYPE_ALL in bugdir_types: - new_uuids = list(self.new_bugdir.uuids()) - old_uuids = list(self.old_bugdir.uuids()) - elif BUGDIR_TYPE_NEW in bugdir_types: - new_uuids = list(self.new_bugdir.uuids()) - old_uuids = [] + new_uuids = [] + old_uuids = [] + for bd_type in [BUGDIR_TYPE_ALL, BUGDIR_TYPE_NEW, BUGDIR_TYPE_MOD]: + if bd_type in bugdir_types: + new_uuids = list(self.new_bugdir.uuids()) + break + for bd_type in [BUGDIR_TYPE_ALL, BUGDIR_TYPE_REM]: + if bd_type in bugdir_types: + old_uuids = list(self.old_bugdir.uuids()) + break subscribed_bugs = [s.id for s in subscriptions if BUG_TYPE_ALL.has_descendant( \ s.type, match_self=True)] @@ -319,9 +326,13 @@ class Diff (object): try: old_bug = self.old_bugdir.bug_from_uuid(uuid) except KeyError: - added.append(new_bug) + if BUGDIR_TYPE_ALL in bugdir_types \ + or BUGDIR_TYPE_NEW in bugdir_types \ + or uuid in subscribed_bugs: + added.append(new_bug) continue if BUGDIR_TYPE_ALL in bugdir_types \ + or BUGDIR_TYPE_MOD in bugdir_types \ or uuid in subscribed_bugs: if old_bug.sync_with_disk == True: old_bug.load_comments() -- cgit From e95de5d97dc05ce5dbb9a553d5e42e437ceccbbf Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 03:55:55 -0500 Subject: Added --subscribe option to `be diff` --- becommands/diff.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/becommands/diff.py b/becommands/diff.py index aebbfdb..5a94462 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -40,15 +40,18 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): Changed bug settings: status: open -> closed >>> if bd.vcs.versioned == True: - ... execute(["--modified", original], manipulate_encodings=False) + ... execute(["--subscribe", "DIR:mod", "--uuids", original], + ... manipulate_encodings=False) ... else: ... print "a" a >>> if bd.vcs.versioned == False: ... execute([original], manipulate_encodings=False) ... else: - ... print "This directory is not revision-controlled." - This directory is not revision-controlled. + ... raise cmdutil.UsageError('This directory is not revision-controlled.') + Traceback (most recent call last): + ... + UsageError: This directory is not revision-controlled. >>> bd.cleanup() """ parser = get_parser() @@ -59,11 +62,22 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) == 1: revision = args[0] if len(args) > 1: - raise cmdutil.UsageError("Too many arguments.") + raise cmdutil.UsageError('Too many arguments.') + if options.subscribe == None: + subscriptions = [diff.Subscription(diff.BUGDIR_ID, + diff.BUGDIR_TYPE_ALL)] + else: + subscriptions = [] + for subscription in options.subscribe.split(','): + fields = subscription.split(':') + if len(fields) != 2: + raise cmdutil.UsageError('Invalid subscription "%s", should be ID:TYPE') + id,type = fields + subscriptions.append(diff.Subscription(id, type)) bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) if bd.vcs.versioned == False: - raise cmdutil.UsageError("This directory is not revision-controlled.") + raise cmdutil.UsageError('This directory is not revision-controlled.') if options.dir == None: if revision == None: # get the most recent revision revision = bd.vcs.revision_id(-1) @@ -77,19 +91,19 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): old_bd = old_bd_current else: if old_bd_current.vcs.versioned == False: - raise cmdutil.UsageError("%s is not revision-controlled." + raise cmdutil.UsageError('%s is not revision-controlled.' % options.dir) old_bd = old_bd_current.duplicate_bugdir(revision) os.chdir(cwd) d = diff.Diff(old_bd, bd) - tree = d.report_tree() + tree = d.report_tree(subscriptions) if options.uuids == True: uuids = [] - bugs = tree.child_by_path("/bugs") + bugs = tree.child_by_path('/bugs') for bug_type in bugs: uuids.extend([bug.name for bug in bug_type]) - print "\n".join(uuids) + print '\n'.join(uuids) else : rep = tree.report_string() if rep != None: @@ -102,6 +116,8 @@ def get_parser(): parser = cmdutil.CmdOptionParser("be diff [options] REVISION") parser.add_option("-d", "--dir", dest="dir", metavar="DIR", help="Compare with repository in DIR instead of the current directory.") + parser.add_option("-s", "--subscribe", dest="subscribe", metavar="SUBSCRIPTION", + help="Only print changes matching SUBSCRIPTION, subscription is a comma-separ\ated list of ID:TYPE tuples. See `be subscribe --help` for descriptions of ID and TYPE.") parser.add_option("-u", "--uuids", action="store_true", dest="uuids", help="Only print the bug UUIDS.", default=False) return parser -- cgit From fbb8504a6c0438e90b046e44a60608159f4e3f63 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 04:11:39 -0500 Subject: Created diff.subscriptions_from_string() --- becommands/diff.py | 16 +++++----------- libbe/diff.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/becommands/diff.py b/becommands/diff.py index 5a94462..e2ff052 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -63,17 +63,11 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): revision = args[0] if len(args) > 1: raise cmdutil.UsageError('Too many arguments.') - if options.subscribe == None: - subscriptions = [diff.Subscription(diff.BUGDIR_ID, - diff.BUGDIR_TYPE_ALL)] - else: - subscriptions = [] - for subscription in options.subscribe.split(','): - fields = subscription.split(':') - if len(fields) != 2: - raise cmdutil.UsageError('Invalid subscription "%s", should be ID:TYPE') - id,type = fields - subscriptions.append(diff.Subscription(id, type)) + try: + subscriptions = diff.subscriptions_from_string( + options.subscribe) + except ValueError, e: + raise cmdutil.UsageError(e.msg) bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) if bd.vcs.versioned == False: diff --git a/libbe/diff.py b/libbe/diff.py index 3122fe8..e947021 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -108,6 +108,29 @@ class Subscription (object): def __repr__(self): return "" % (self.id, self.type) +def subscriptions_from_string(string=None, subscription_sep=',', id_sep=':'): + """ + >>> subscriptions_from_string(None) + [] + >>> subscriptions_from_string('DIR:new,DIR:rem,ABC:all,XYZ:all') + [, , , ] + >>> subscriptions_from_string('DIR::new') + Traceback (most recent call last): + ... + ValueError: Invalid subscription "DIR::new", should be ID:TYPE + """ + if string == None: + return [Subscription(BUGDIR_ID, BUGDIR_TYPE_ALL)] + subscriptions = [] + for subscription in string.split(','): + fields = subscription.split(':') + if len(fields) != 2: + raise ValueError('Invalid subscription "%s", should be ID:TYPE' + % subscription) + id,type = fields + subscriptions.append(Subscription(id, type)) + return subscriptions + class DiffTree (tree.Tree): """ A tree holding difference data for easy report generation. -- cgit From 3cf0394832176a18f658ef3a89521bcccd57cb9e Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 04:21:04 -0500 Subject: More 'DIR'->diff.BUGDIR_ID updates --- becommands/diff.py | 4 ++-- becommands/subscribe.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/becommands/diff.py b/becommands/diff.py index e2ff052..2cff537 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -40,7 +40,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): Changed bug settings: status: open -> closed >>> if bd.vcs.versioned == True: - ... execute(["--subscribe", "DIR:mod", "--uuids", original], + ... execute(["--subscribe", "%(bugdir_id)s:mod", "--uuids", original], ... manipulate_encodings=False) ... else: ... print "a" @@ -53,7 +53,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): ... UsageError: This directory is not revision-controlled. >>> bd.cleanup() - """ + """ % {'bugdir_id':diff.BUGDIR_ID} parser = get_parser() options, args = parser.parse_args(args) cmdutil.default_complete(options, args, parser) diff --git a/becommands/subscribe.py b/becommands/subscribe.py index 3f4998e..19aac53 100644 --- a/becommands/subscribe.py +++ b/becommands/subscribe.py @@ -91,7 +91,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): servers = options.servers.split(",") types = options.types.split(",") - if len(args) == 0 or args[0] == "DIR": # directory-wide subscriptions + if len(args) == 0 or args[0] == diff.BUGDIR_ID: # directory-wide subscriptions type_root = diff.BUGDIR_TYPE_ALL entity = bd entity_name = "bug directory" @@ -314,7 +314,7 @@ def get_bugdir_subscribers(bugdir, server): Returns a dict of dicts: subscribers[user][id] = types where id is either a bug.uuid (in the case of a bug subscription) - or "DIR" (in the case of a bugdir subscription). + or "%(bugdir_id)s" (in the case of a bugdir subscription). Only checks bugs that are currently in memory, so you might want to call bugdir.load_all_bugs() first. @@ -328,16 +328,16 @@ def get_bugdir_subscribers(bugdir, server): >>> a.extra_strings = subscribe(a.extra_strings, "John Doe ", ... [diff.BUG_TYPE_ALL], ["a.com"], diff.BUG_TYPE_ALL) >>> subscribers = get_bugdir_subscribers(bd, "a.com") - >>> subscribers["Jane Doe "]["DIR"] + >>> subscribers["Jane Doe "]["%(bugdir_id)s"] [] - >>> subscribers["John Doe "]["DIR"] + >>> subscribers["John Doe "]["%(bugdir_id)s"] [] >>> subscribers["John Doe "]["a"] [] >>> get_bugdir_subscribers(bd, "b.net") - {'Jane Doe ': {'DIR': []}} + {'Jane Doe ': {'%(bugdir_id)s': []}} >>> bd.cleanup() - """ + """ % {'bugdir_id':diff.BUGDIR_ID} subscribers = {} for sub in get_subscribers(bugdir.extra_strings, diff.BUGDIR_TYPE_ALL, server, diff.BUGDIR_TYPE_ALL, -- cgit From 167a8d2ae883fecf4e9d375e333e406dc723ef3b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 05:38:48 -0500 Subject: Added libbe.diff.Diff.full_report() for speed with several subscription lists. Now report_tree() returns an appropriately .masked version of the cached full report, which is much faster than recomputing a new diff tree from scratch. Also fixed bug in libbe.diff.DiffTree.report() where .requires_children=True was exposing nodes with children, when it should (and now does) only expose nodes with _unmasked_ children. --- libbe/diff.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/libbe/diff.py b/libbe/diff.py index e947021..fb2b249 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -215,7 +215,8 @@ class DiffTree (tree.Tree): if self.masked == True: return None data_part = self.data_part(depth) - if self.requires_children == True and len(self) == 0: + if self.requires_children == True \ + and len([c for c in self if c.masked == False]) == 0: pass else: self.join(root, parent, data_part) @@ -304,6 +305,20 @@ class Diff (object): Removed bugs: b:cm: Bug B + While sending subscriptions to report_tree() makes the report + generation more efficient (because you may not need to compare + _all_ the bugs, etc.), sometimes you will have several sets of + subscriptions. In that case, it's better to run full_report() + first, and then use report_tree() to avoid redundant comparisons. + + >>> d.full_report() + >>> print d.report_tree([subscriptions[0]]).report_string() + New bugs: + c:om: Bug C + >>> print d.report_tree([subscriptions[1]]).report_string() + Removed bugs: + b:cm: Bug B + >>> bd.cleanup() """ def __init__(self, old_bugdir, new_bugdir): @@ -442,12 +457,62 @@ class Diff (object): # report generation methods - def report_tree(self, subscriptions=None, diff_tree=DiffTree): + def full_report(self, diff_tree=DiffTree): + """ + Generate a full report for efficiency if you'll be using + .report_tree() with several sets of subscriptions. + """ + self._cached_full_report = self.report_tree(diff_tree=diff_tree, + allow_cached=False) + self._cached_full_report_diff_tree = diff_tree + def _sub_report(self, subscriptions): + """ + Return ._cached_full_report masked for subscriptions. + """ + root = self._cached_full_report + bugdir_types = [s.type for s in subscriptions if s.id == BUGDIR_ID] + subscribed_bugs = [s.id for s in subscriptions + if BUG_TYPE_ALL.has_descendant( \ + s.type, match_self=True)] + selected_by_bug = [node.name + for node in root.child_by_path('bugdir/bugs')] + if BUGDIR_TYPE_ALL in bugdir_types: + for node in root.traverse(): + node.masked = False + selected_by_bug = [] + else: + node = root.child_by_path('bugdir/settings') + node.masked = True + for name,type in (('new', BUGDIR_TYPE_NEW), + ('mod', BUGDIR_TYPE_MOD), + ('rem', BUGDIR_TYPE_REM)): + if type in bugdir_types: + bugs = root.child_by_path('bugdir/bugs/%s' % name) + for bug_node in bugs: + for node in bug_node.traverse(): + node.masked = False + selected_by_bug.remove(name) + for name in selected_by_bug: + bugs = root.child_by_path('bugdir/bugs/%s' % name) + for bug_node in bugs: + if bug_node.name in subscribed_bugs: + for node in bug_node.traverse(): + node.masked = False + else: + for node in bug_node.traverse(): + node.masked = True + return root + def report_tree(self, subscriptions=None, diff_tree=DiffTree, + allow_cached=True): """ Pretty bare to make it easy to adjust to specific cases. You can pass in a DiffTree subclass via diff_tree to override the default report assembly process. """ + if allow_cached == True \ + and hasattr(self, '_cached_full_report') \ + and diff_tree == self._cached_full_report_diff_tree: + return self._sub_report(subscriptions) if subscriptions == None: subscriptions = [Subscription(BUGDIR_ID, BUGDIR_TYPE_ALL)] bugdir_settings = sorted(self.new_bugdir.settings_properties) -- cgit From cc58188259e36193c3174fbb55e37c790382a7ea Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 07:41:50 -0500 Subject: Use new libbe.diff.Diff.report_tree(subscriptions) in be-handle-mail. This makes Message.subscriber_emails() much cleaner. Also fix libbe.diff.Diff._sub_report() to handle missing 'bugdir/settings'. Added libbe.diff.SubscriptionType.__cmp__ so that SubscriptionType('all') == SubscriptionType('all') This is important when comparing the types returned by becommands.subscribe.get_bugdir_subscribers() with the libbe.diff.*_TYPE_* types. --- interfaces/email/interactive/be-handle-mail | 67 ++++++++--------------------- libbe/diff.py | 13 ++++-- 2 files changed, 26 insertions(+), 54 deletions(-) diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index 952c16d..10f6884 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -306,6 +306,8 @@ class DiffTree (libbe.diff.DiffTree): """ def report_or_none(self): report = self.report() + if report == None: + return None payload = report.get_payload() if payload == None or len(payload) == 0: return None @@ -315,7 +317,7 @@ class DiffTree (libbe.diff.DiffTree): if report == None: return "No changes" else: - return send_pgp_mime.flatten(self.report(), to_unicode=True) + return send_pgp_mime.flatten(report, to_unicode=True) def make_root(self): return MIMEMultipart() def join(self, root, parent, data_part): @@ -658,64 +660,29 @@ class Message (object): bd.load_all_bugs() subscribers = subscribe.get_bugdir_subscribers(bd, THIS_SERVER) - if len(subscribers) == 0: - return [] + return [] + for subscriber,subscriptions in subscribers.items(): + subscribers[subscriber] = [] + for id,types in subscriptions.items(): + for type in types: + subscribers[subscriber].append( + libbe.diff.Subscription(id,type)) before_bd, after_bd = self._get_before_and_after_bugdirs(bd, previous_revision) diff = Diff(before_bd, after_bd) - diff_tree = diff.report_tree(diff_tree=DiffTree) - bug_index = {} - for child in diff_tree.child_by_path("/bugs/new"): - bug_index[child.name] = ("added", child) - for child in diff_tree.child_by_path("/bugs/mod"): - bug_index[child.name] = ("modified", child) - for child in diff_tree.child_by_path("/bugs/rem"): - bug_index[child.name] = ("removed", child) + diff.full_report(diff_tree=DiffTree) header = self._subscriber_header(bd, previous_revision) emails = [] for subscriber,subscriptions in subscribers.items(): header.replace_header("to", subscriber) - parts = [] - if "DIR" in subscriptions: # make sure we check the DIR level first - ordered_subscriptions = [("DIR", subscriptions.pop("DIR"))] - else: - ordered_subscriptions = [] - ordered_subscriptions.extend(subscriptions.items()) - for id,types in ordered_subscriptions: - if id == "DIR": - if libbe.diff.BUGDIR_TYPE_ALL in types: - parts.append(diff_tree.report_or_none()) - break # we've attached everything, so stop checking. - if libbe.diff.BUGDIR_TYPE_NEW in types: - new = diff_tree.child_by_path("/bugs/new") - parts.append(new.report_or_none()) - continue # move on to next id - # if we get this far, id refers to a bug. - assert types == [libbe.diff.BUG_TYPE_ALL], types - if id not in bug_index: - continue # no changes here, move on to next id - type,bug_root = bug_index[id] - if type == "added" \ - and "DIR" in subscriptions \ - and subscriptions["DIR"] == libbe.diff.BUGDIR_TYPE_NEW: - # this info already attached at the DIR level - continue # move on to next id - parts.append(bug_root.report_or_none()) - parts = [p for p in parts if p != None] - if len(parts) == 0: - continue # no email to this subscriber - elif len(parts) == 1: - root = parts[0] - else: # join subscription parts into a single body - root = MIMEMultipart() - root[u"Content-Description"] = u"Multiple subscription trees." - for part in parts: - root.attach(part) - emails.append(send_pgp_mime.attach_root(header, root)) - if LOGFILE != None: - LOGFILE.write(u"Preparing to notify %s of changes\n" % subscriber) + report = diff.report_tree(subscriptions, diff_tree=DiffTree) + root = report.report_or_none() + if root != None: + emails.append(send_pgp_mime.attach_root(header, root)) + if LOGFILE != None: + LOGFILE.write(u"Preparing to notify %s of changes\n" % subscriber) return emails def _get_before_and_after_bugdirs(self, bd, previous_revision=None): if previous_revision == None: diff --git a/libbe/diff.py b/libbe/diff.py index fb2b249..46b8bda 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -38,6 +38,8 @@ class SubscriptionType (tree.Tree): self.type = type_name def __str__(self): return self.type + def __cmp__(self, other): + return cmp(self.type, other.type) def __repr__(self): return "" % str(self) def string_tree(self, indent=0): @@ -222,8 +224,8 @@ class DiffTree (tree.Tree): self.join(root, parent, data_part) if data_part != None: depth += 1 - for child in self: - child.report(root, self, depth) + for child in self: + root = child.report(root, self, depth) return root def make_root(self): return [] @@ -481,8 +483,11 @@ class Diff (object): node.masked = False selected_by_bug = [] else: - node = root.child_by_path('bugdir/settings') - node.masked = True + try: + node = root.child_by_path('bugdir/settings') + node.masked = True + except KeyError: + pass for name,type in (('new', BUGDIR_TYPE_NEW), ('mod', BUGDIR_TYPE_MOD), ('rem', BUGDIR_TYPE_REM)): -- cgit From e260fa7ed1e501404c75cdbe3d7461f29cd6c3e1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 08:08:09 -0500 Subject: Adjust libbe.diff.DiffTree to fix failed doctest. ====================================================================== FAIL: Doctest: libbe.diff.DiffTree ---------------------------------------------------------------------- Traceback (most recent call last): File "/usr/lib/python2.5/doctest.py", line 2128, in runTest raise self.failureException(self.format_failure(new.getvalue())) AssertionError: Failed doctest test for libbe.diff.DiffTree File "/home/wking/src/fun/be/be.diff-subscribe/libbe/diff.py", line 136, in DiffTree ---------------------------------------------------------------------- File "/home/wking/src/fun/be/be.diff-subscribe/libbe/diff.py", line 172, in libbe.diff.DiffTree Failed example: print bugdir.report_string() Exception raised: Traceback (most recent call last): File "/usr/lib/python2.5/doctest.py", line 1228, in __run compileflags, 1) in test.globs File "", line 1, in print bugdir.report_string() File "/home/wking/src/fun/be/be.diff-subscribe/libbe/diff.py", line 213, in report_string return "\n".join(self.report()) TypeError --- libbe/diff.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libbe/diff.py b/libbe/diff.py index 46b8bda..b3cd6bc 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -210,12 +210,15 @@ class DiffTree (tree.Tree): raise KeyError, "%s doesn't match '%s'" % (names, self.name) raise KeyError, "%s points to child not in %s" % (names, [c.name for c in self]) def report_string(self): - return "\n".join(self.report()) + report = self.report() + if report == None: + return '' + return '\n'.join(report) def report(self, root=None, parent=None, depth=0): if root == None: root = self.make_root() if self.masked == True: - return None + return root data_part = self.data_part(depth) if self.requires_children == True \ and len([c for c in self if c.masked == False]) == 0: -- cgit From d3122f5c72cc0a0c345bf0bd545f9e3217ca934f Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 08:15:52 -0500 Subject: Updated NEWS --- NEWS | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NEWS b/NEWS index 669bed2..21a0140 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,11 @@ +December 5, 2009 + * changes to `be diff` + * exits with an error if required revision control is not possible. + Previously it printed a message, but exitted with status 0. + * removed options --new, --removed, --modified, --all + * added options --uuids, --subscribe + * assorted cleanups and bugfixes + December 4, 2009 * new commands: email-bugs -- cgit From e8f1c5709af7fb0d618dcf142d51dc95ef7d6c2c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 08:31:04 -0500 Subject: Cleanup Dir in becommands/init.py doctest. --- becommands/init.py | 1 + 1 file changed, 1 insertion(+) diff --git a/becommands/init.py b/becommands/init.py index 7d6d475..6085286 100644 --- a/becommands/init.py +++ b/becommands/init.py @@ -55,6 +55,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): Traceback (most recent call last): UserError: No such directory: /highly-unlikely-to-exist >>> os.chdir('/') + >>> dir.cleanup() """ parser = get_parser() options, args = parser.parse_args(args) -- cgit From a1f3221ee5bd85a72b6e31a527b4c4e6f6933d6e Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 16:50:34 -0500 Subject: Fix libbe.diff.Diff._changed_bugs() to handle subscriptions by bug shortname. --- libbe/diff.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libbe/diff.py b/libbe/diff.py index b3cd6bc..c0132ff 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -352,9 +352,14 @@ class Diff (object): if bd_type in bugdir_types: old_uuids = list(self.old_bugdir.uuids()) break - subscribed_bugs = [s.id for s in subscriptions - if BUG_TYPE_ALL.has_descendant( \ - s.type, match_self=True)] + subscribed_bugs = [] + for s in subscriptions: + if s.id != BUGDIR_ID: + try: + bug = self.new_bugdir.bug_from_shortname(s.id) + except bugdir.NoBugMatches: + bug = self.old_bugdir.bug_from_shortname(s.id) + subscribed_bugs.append(bug.uuid) new_uuids.extend([s for s in subscribed_bugs if self.new_bugdir.has_bug(s)]) new_uuids = sorted(set(new_uuids)) -- cgit From 9603b95fc0c0b949d2cec10e27215684629f2af1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 17:17:21 -0500 Subject: Added missing author entries to some comments + cleanups. --- .../comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values | 2 +- .../comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values | 2 +- .../comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values | 2 +- .../comments/13012b22-2d02-444c-87c0-8cf0f17137ae/values | 2 +- .../comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/values | 2 +- .../comments/46937fd4-b0bc-4eed-8033-d699445441ea/values | 2 +- .../comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/values | 2 +- .../comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/values | 2 +- .../comments/e5db7c9b-de48-4302-905b-9570bb6e7ade/values | 2 +- .../comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/values | 2 +- .../comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/values | 2 +- .../comments/55263144-9775-4b18-ab83-29d66ed91a53/values | 2 +- .../comments/68927fef-6ce1-4a1f-a414-28695d913a50/values | 2 +- .../comments/83202b83-eea8-452f-8239-d468940bddba/values | 2 +- .../comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values | 2 +- .../comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values | 2 +- .../comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values | 3 +++ .../comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/values | 2 +- .../comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/values | 2 +- .../comments/49e0425b-3332-4d0e-b371-300eccd55370/values | 3 +++ .../comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values | 3 +++ .../comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values | 3 +++ .../comments/a845096e-3cdf-41ed-a0e3-283439665b92/values | 2 +- .../comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/values | 2 +- .../comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/values | 2 +- .../comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/values | 2 +- .../comments/f925e56f-26f9-4620-82fb-a0f160f27921/values | 2 +- .../comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values | 2 +- .../comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values | 2 +- .../comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values | 2 +- .../comments/e249e2aa-2029-4a96-bc84-962366e07fd6/values | 2 +- .../comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/values | 2 +- .../comments/8097468f-87a9-4d84-ac20-1772393bb54d/values | 2 +- .../comments/09f950d4-9366-4e7b-98b3-9057999f8f38/values | 2 +- .../comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/values | 2 +- .../comments/7b904395-86e9-4eb1-8534-69cec63801d4/values | 2 +- .../comments/a0e846ed-1549-4ec3-b94d-391e54610f61/values | 2 +- .../comments/f1cde826-0506-4b4a-92ab-8499e953fa49/values | 2 +- 38 files changed, 46 insertions(+), 34 deletions(-) diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values index 777a3f8..9cfd081 100644 --- a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68/values @@ -1,4 +1,4 @@ -Author: wking +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values index 461a5ab..9e40714 100644 --- a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad/values @@ -1,4 +1,4 @@ -Author: wking +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values index e550f5c..ce0ab73 100644 --- a/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values +++ b/.be/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0/values @@ -1,4 +1,4 @@ -Author: wking +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/values b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/values index 26f7b94..a73aeeb 100644 --- a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/values +++ b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae/values @@ -1,7 +1,7 @@ Alt-id: <20090711125030.GA18185@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/values b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/values index 6e10b7e..bb2305f 100644 --- a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/values +++ b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da/values @@ -1,7 +1,7 @@ Alt-id: <20090713104715.GA13723@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/values b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/values index 968c96a..60c80a1 100644 --- a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/values +++ b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea/values @@ -1,7 +1,7 @@ Alt-id: <20090713115734.GA13788@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/values b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/values index d22c21f..b5ebf31 100644 --- a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/values +++ b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9/values @@ -1,7 +1,7 @@ Alt-id: <20090712235502.GA10782@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/values b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/values index 9b9a279..dbdb347 100644 --- a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/values +++ b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40/values @@ -1,7 +1,7 @@ Alt-id: <20090711152507.GA18461@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain 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 index 31bcacb..7beb827 100644 --- 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 @@ -1,4 +1,4 @@ -Author: wking +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/values b/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/values index fee86e6..c93321b 100644 --- a/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/values +++ b/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7/values @@ -1,7 +1,7 @@ Alt-id: <20090801102742.GA29000@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/values b/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/values index e2dd8b8..0f4ff0a 100644 --- a/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/values +++ b/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f/values @@ -1,7 +1,7 @@ Alt-id: <20090718222701.GA304@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/values b/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/values index 4c9ee4e..8b10a06 100644 --- a/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/values +++ b/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53/values @@ -1,7 +1,7 @@ Alt-id: <20090706104839.GA19537@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/values b/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/values index 69c1846..a01e2cd 100644 --- a/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/values +++ b/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50/values @@ -1,7 +1,7 @@ Alt-id: <20090705143108.GB10709@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/values b/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/values index b918b25..07da71c 100644 --- a/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/values +++ b/.be/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba/values @@ -1,7 +1,7 @@ Alt-id: <20090707013454.GA3721@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values b/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values index ab313b9..2a52700 100644 --- a/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values +++ b/.be/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48/values @@ -1,4 +1,4 @@ -Author: wking +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values b/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values index e434e1e..5e1f3de 100644 --- a/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values +++ b/.be/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a/values @@ -1,4 +1,4 @@ -Author: wking +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values index dbd9a0c..3b45fbf 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6/values @@ -1,6 +1,9 @@ Alt-id: <87d43gn8ju.fsf_-_@benfinney.id.au> +Author: Ben Finney + + Content-type: text/plain diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/values index ce34e73..9e84a24 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624/values @@ -1,7 +1,7 @@ Alt-id: <20090714171725.GB10445@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/values index df4c701..e0c0955 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a/values @@ -1,7 +1,7 @@ Alt-id: <20090714191145.GB10606@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values index 770f0b4..b45a747 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370/values @@ -1,6 +1,9 @@ Alt-id: <873a4cmjw5.fsf@benfinney.id.au> +Author: Ben Finney + + Content-type: text/plain diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values index 83fcd63..b6d25cb 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4/values @@ -1,6 +1,9 @@ Alt-id: <20091120132219.GA17577@mjolnir.home.net> +Author: W. Trevor King + + Content-type: text/plain diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values index 99b8978..7f205d6 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809/values @@ -1,6 +1,9 @@ Alt-id: <20091118011403.GB9503@mjolnir.home.net> +Author: W. Trevor King + + Content-type: text/plain diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/values index ee9cc4b..b757933 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92/values @@ -1,7 +1,7 @@ Alt-id: <20090718105008.GA31639@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/values index fca4962..b5c41c9 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68/values @@ -1,7 +1,7 @@ Alt-id: <20090714182034.GA10606@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/values index 2df38ed..00309a2 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c/values @@ -1,7 +1,7 @@ Alt-id: <20090714110543.GB4855@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/values index 42e7df8..a3f74c4 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b/values @@ -1,7 +1,7 @@ Alt-id: <20090714133732.GB6160@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/values b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/values index 4e46802..63a2cae 100644 --- a/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/values +++ b/.be/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921/values @@ -1,7 +1,7 @@ Alt-id: <20090716103855.GA8579@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values index ae76653..4255708 100644 --- a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values +++ b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4/values @@ -1,4 +1,4 @@ -Author: wking +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values index 8103512..f38cb7f 100644 --- a/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values +++ b/.be/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2/values @@ -1,4 +1,4 @@ -Author: wking +Author: W. Trevor King Content-type: text/plain 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 index 405abc1..06d6017 100644 --- 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 @@ -1,4 +1,4 @@ -Author: wking +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/values b/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/values index d361c19..3847736 100644 --- a/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/values +++ b/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6/values @@ -1,7 +1,7 @@ Alt-id: <20090721135907.GB4469@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/values b/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/values index 49c9314..721f9fd 100644 --- a/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/values +++ b/.be/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a/values @@ -1,7 +1,7 @@ Alt-id: <20090625154734.GA19441@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values index 8496f0a..bb26755 100644 --- a/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values +++ b/.be/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d/values @@ -1,4 +1,4 @@ -Author: wking +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/values b/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/values index 79dd755..78bc87b 100644 --- a/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/values +++ b/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38/values @@ -1,7 +1,7 @@ Alt-id: <20090718131220.GA31832@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/values b/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/values index a2751e8..b640d0b 100644 --- a/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/values +++ b/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d/values @@ -1,7 +1,7 @@ Alt-id: <20090719130649.GA4164@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/values b/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/values index 67fc80f..b70c6e3 100644 --- a/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/values +++ b/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4/values @@ -1,7 +1,7 @@ Alt-id: <20090718220551.GB32230@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/values b/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/values index d8ffc73..5f323c6 100644 --- a/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/values +++ b/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61/values @@ -1,7 +1,7 @@ Alt-id: <20090719130153.GA4036@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain diff --git a/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/values b/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/values index 5a6047a..727c4ee 100644 --- a/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/values +++ b/.be/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49/values @@ -1,7 +1,7 @@ Alt-id: <20090716133930.GC12213@mjolnir.home.net> -Author: '"W. Trevor King" ' +Author: W. Trevor King Content-type: text/plain -- cgit From 882492c80f47b6b5330b2510e9b8ef4164666303 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 17:34:09 -0500 Subject: Adjusted be-mbox-to-xml to not drop author info from multipart messages --- interfaces/xml/be-mbox-to-xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/interfaces/xml/be-mbox-to-xml b/interfaces/xml/be-mbox-to-xml index 3af2978..eda6d6e 100755 --- a/interfaces/xml/be-mbox-to-xml +++ b/interfaces/xml/be-mbox-to-xml @@ -40,7 +40,10 @@ def normalize_email_address(address): """ Standardize whitespace, etc. """ - return email.utils.formataddr(email.utils.parseaddr(address)) + addr = email.utils.formataddr(email.utils.parseaddr(address)) + if len(addr) == 0: + return None + return addr def normalize_RFC_2822_date(date): """ -- cgit From af8bd49a6215029c08676a3d4a59cfcab1d80976 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 17:40:08 -0500 Subject: Commented on 12c: Bug aggregation. Multi-repo meta-BE? --- .../624a4542-92e9-442e-b71c-a14da4fe55cf/body | 83 ++++++++++++++++++++++ .../624a4542-92e9-442e-b71c-a14da4fe55cf/values | 8 +++ 2 files changed, 91 insertions(+) create mode 100644 .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/body create mode 100644 .be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/values diff --git a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/body b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/body new file mode 100644 index 0000000..e7b48e0 --- /dev/null +++ b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/body @@ -0,0 +1,83 @@ +I read + http://weblog.masukomi.org/2008/1/3/distributed-bug-tracking +yesterday, and the section on bug visibility got me thinking about +bug 12c (Multi-repo meta-BE?) some more. + +We already have interfaces like this email/html mashup: + +On Sun, Sep 13, 2009 at 07:04:05AM -0400, W. Trevor King wrote: +> Since the non-bzr interfaces to BE are coming along nicely, I've put +> up a non-bzr interface to my be-rr branch. +> http://www.physics.drexel.edu/~wking/code/be +> It uses nightly builds of Gianluca's static html from my devel branch +> to provide read-only browsing, and accepts changes from the general +> public through my email interface into a public branch. I handle the +> synchronization of these two branches manually. + +These interfaces provide a means for remote users to access a BE +repository without bzr or the command line. As far as users are +concerned, this exposed repository looks pretty much like a +centralized bugtracking system (e.g. bugzilla, ...). + +However, with BE we have more bug information living off in other +branches that haven't yet been merged with the exposed repo. The +problem is two-fold: + 1) how to keep up to date within a distributed community. + 2) how do users find branches/patches that fix bug XYZ. + +For (2), I think the best solution at the moment are along the lines +of my little scripts (discussed in the bug 12c comments). With the +addition of the `be diff --dir DIR` option, it's now even easier to +find more information on bug 565 (or whatever UUID): + be/be.wtk$ for repo in ../*; do \ + if [ $repo == "be.wtk" ]; then continue; fi; \ + diff=$(be diff --dir $repo --subscribe 565:all); \ + if [ -n "$diff" ]; then \ + echo "Changed from $repo:"; echo "$diff"; \ + fi; \ + done + Changed from ../be.html: + New bugs: + 565:fm: be email-bugs for bug submission from bzr-less users + Changed from ../be.trunk: + New bugs: + 565:fm: be email-bugs for bug submission from bzr-less users + Changed from ../cherryflavoredbugseverywhere: + New bugs: + 565:fm: be email-bugs for bug submission from bzr-less users +where the --dir and --subscribe options to `be diff` are new. If +people don't like the command line, this would be easy to bundle into +a web-frontend (CFBE?) if you wanted, with a cron job pulling updates +into the tracked branches. + +I was starting into a solution for (1) when I did this: + +On Mon, Jul 27, 2009 at 08:42:19AM -0400, W. Trevor King wrote: +> My email interface now supports subscription: +> be subscribe DIR # see any changes to the bug directory. +> be subscribe BUG-ID # see changes to a particular bug. +> See +> be subscribe --help +> for more details. + +The idea was that a dev/user would subscribe to whatever issues they +wanted to track, and they would get email notifications whenever some +action affected any of those issues. These subscriptions would +percolate through the distributed branches as a result of the usual +mergers. For example, my subscription to all changes has made it into +the trunk branch (see .be/settings). + +This subscription mechanism was setup to work through interactive +public interfaces (my email interface, eventually CFBE, ...), but +it doesn't work for changes made via the command-line interface, +so I browsed around a bit and ran across some interesting workflows +in the bzr documentation + doc/developers/HACKING.txt, "Communicating and Coordinating" +which points out the following plugins + * email (http://doc.bazaar-vcs.org/plugins/en/email-plugin.html) + * dbus (http://doc.bazaar-vcs.org/plugins/en/dbus-plugin.html) +which send automatic notification messages after commits, etc. If +people want this sort of functionality, it would be easy enough to rig +a hook for `be commit' that sent a diff email to subscribers, which +could include be-devel. + diff --git a/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/values b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/values new file mode 100644 index 0000000..adb1ae5 --- /dev/null +++ b/.be/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf/values @@ -0,0 +1,8 @@ +Author: W. Trevor King + + +Content-type: text/plain + + +Date: Sat, 05 Dec 2009 22:39:07 +0000 + -- cgit From f8e29454d91e1ec818fb14fad3d77a159ebbe22a Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 18:16:05 -0500 Subject: Fixed shortname -> bugname in becommands/show.py. Fixes wking@thor:be.target-as-bug$ be show 22b:7 Traceback (most recent call last): File "/home/wking/bin/be", line 65, in sys.exit(cmdutil.execute(args[0], args[1:])) File "/home/wking/src/fun/be/be.target-as-bug/libbe/cmdutil.py", line 87, in execute restrict_file_access=restrict_file_access) File "/home/wking/src/fun/be/be.target-as-bug/becommands/show.py", line 82, in execute print output(args, bd, as_xml=options.XML, with_comments=options.comments) File "/home/wking/src/fun/be/be.target-as-bug/becommands/show.py", line 174, in output lines.append(comment.string(shortname=shortname)) NameError: global name 'shortname' is not defined --- becommands/show.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/becommands/show.py b/becommands/show.py index 557c63a..cc43f9a 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -171,7 +171,7 @@ def output(ids, bd, as_xml=True, with_comments=True): if as_xml: lines.append(comment.xml(indent=4, shortname=bugname)) else: - lines.append(comment.string(shortname=shortname)) + lines.append(comment.string(shortname=bugname)) if spaces_left > 0: spaces_left -= 1 lines.append('') # add a blank line between bugs/comments -- cgit From 01db171aafdd01f184f707ba23c1484692a4a45b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 18:25:03 -0500 Subject: This addresses the following portion of 22b:7: * Targeting normal bugs With "be depend". I think we should remove the "target" field from bugs, and move target dependencies over into the "be depend" framework. * be target list Would become "be list --severity target". A target "severity" would keep target bugs distinct from other bug/issue types. --- libbe/bug.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index 1a190c3..ed1bbd2 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -56,6 +56,7 @@ class DiskAccessRequired (Exception): # in order of increasing severity. (name, description) pairs severity_def = ( + ("target", "The issue is a target or milestone, not a bug."), ("wishlist","A feature that could improve usefulness, but not a bug."), ("minor","The standard bug level."), ("serious","A bug that requires workarounds."), @@ -173,10 +174,6 @@ class Bug(settings_object.SavedSettingsObject): def active(self): return self.status in active_status_values - @_versioned_property(name="target", - doc="The deadline for fixing this bug") - def target(): return {} - @_versioned_property(name="creator", doc="The user who entered the bug into the system") def creator(): return {} -- cgit From 24dfc11bd82210f93a0a2c5dbcb803fc4edd05f8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 19:36:56 -0500 Subject: Added "Bugs Everywhere Directory v1.3" which transitions to bug-type targets. See bug 22b6f620-d2f7-42a5-a02e-145733a4e366 for the motivation. This upgrade will replace all "target" settings. The new BugDir target setting will be the uuid of the appropriate target. The Bug target setting is removed, replaced by an extra_strings BLOCKS tag blocking the appropriate target. New target bugs are created on the fly as required. --- libbe/upgrade.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/libbe/upgrade.py b/libbe/upgrade.py index 785249d..dc9d54f 100644 --- a/libbe/upgrade.py +++ b/libbe/upgrade.py @@ -22,6 +22,7 @@ import os, os.path import sys import libbe +import bug import encoding import mapfile import vcs @@ -31,7 +32,8 @@ if libbe.TESTING == True: # a list of all past versions BUGDIR_DISK_VERSIONS = ["Bugs Everywhere Tree 1 0", "Bugs Everywhere Directory v1.1", - "Bugs Everywhere Directory v1.2"] + "Bugs Everywhere Directory v1.2", + "Bugs Everywhere Directory v1.3"] # the current version BUGDIR_DISK_VERSION = BUGDIR_DISK_VERSIONS[-1] @@ -142,9 +144,63 @@ class Upgrade_1_1_to_1_2 (Upgrader): settings["vcs_name"] = settings.pop("rcs_name") mapfile.map_save(self.vcs, path, settings) +class Upgrade_1_2_to_1_3 (Upgrader): + initial_version = "Bugs Everywhere Directory v1.2" + final_version = "Bugs Everywhere Directory v1.3" + def __init__(self, *args, **kwargs): + Upgrader.__init__(self, *args, **kwargs) + self._targets = {} # key: target text,value: new target bug + path = self.get_path('settings') + settings = mapfile.map_load(self.vcs, path) + if 'vcs_name' in settings: + old_vcs = self.vcs + self.vcs = vcs.vcs_by_name(settings['vcs_name']) + self.vcs.root(self.root) + self.vcs.encoding = old_vcs.encoding + + def _target_bug(self, target_text): + if target_text not in self._targets: + _bug = bug.Bug(bugdir=self, summary=target_text) + # note: we're not a bugdir, but all Bug.save() needs is + # .root, .vcs, and .get_path(), which we have. + _bug.severity = 'target' + self._targets[target_text] = _bug + return self._targets[target_text] + + def _upgrade_bugdir_mapfile(self): + path = self.get_path('settings') + settings = mapfile.map_load(self.vcs, path) + if 'target' in settings: + settings['target'] = self._target_bug(settings['target']).uuid + mapfile.map_save(self.vcs, path, settings) + + def _upgrade_bug_mapfile(self, bug_uuid): + import becommands.depend + path = self.get_path('bugs', bug_uuid, 'values') + settings = mapfile.map_load(self.vcs, path) + if 'target' in settings: + target_bug = self._target_bug(settings['target']) + _bug = bug.Bug(bugdir=self, uuid=bug_uuid, from_disk=True) + # note: we're not a bugdir, but all Bug.load_settings() + # needs is .root, .vcs, and .get_path(), which we have. + becommands.depend.add_block(target_bug, _bug) + _bug.settings.pop('target') + _bug.save() + + def _upgrade(self): + """ + Bug value field "target" -> target bugs. + Bugdir value field "target" -> pointer to current target bug. + """ + for bug_uuid in os.listdir(self.get_path('bugs')): + self._upgrade_bug_mapfile(bug_uuid) + self._upgrade_bugdir_mapfile() + for _bug in self._targets.values(): + _bug.save() upgraders = [Upgrade_1_0_to_1_1, - Upgrade_1_1_to_1_2] + Upgrade_1_1_to_1_2, + Upgrade_1_2_to_1_3] upgrade_classes = {} for upgrader in upgraders: upgrade_classes[(upgrader.initial_version,upgrader.final_version)]=upgrader -- cgit From ea7d2deb318197fc9444f9c2fc7e01813e6a8e8d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 19:38:50 -0500 Subject: Upgraded to Bugs Everywhere Directory v1.3 --- .be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values | 18 ++++++++++++++++++ .be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values | 18 ++++++++++++++++++ .be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values | 18 ++++++++++++++++++ .be/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values | 7 ++++--- .be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values | 7 ++++--- .be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values | 7 ++++--- .be/version | 2 +- 7 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 .be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values create mode 100644 .be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values create mode 100644 .be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values diff --git a/.be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values b/.be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values new file mode 100644 index 0000000..f665b83 --- /dev/null +++ b/.be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values @@ -0,0 +1,18 @@ +creator: W. Trevor King + + +extra_strings: +- BLOCKED-BY:f51dc5a7-37b7-4ce1-859a-b7cb58be6494 + + +severity: target + + +status: open + + +summary: '0.1' + + +time: Sun, 06 Dec 2009 00:37:15 +0000 + diff --git a/.be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values b/.be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values new file mode 100644 index 0000000..f419c52 --- /dev/null +++ b/.be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values @@ -0,0 +1,18 @@ +creator: W. Trevor King + + +extra_strings: +- BLOCKED-BY:ee681951-f254-43d3-a53a-1b36ae415d5c + + +severity: target + + +status: open + + +summary: patch-52 + + +time: Sun, 06 Dec 2009 00:37:16 +0000 + diff --git a/.be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values b/.be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values new file mode 100644 index 0000000..8165308 --- /dev/null +++ b/.be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values @@ -0,0 +1,18 @@ +creator: W. Trevor King + + +extra_strings: +- BLOCKED-BY:f5c06914-dc64-4658-8ec7-32a026a53f55 + + +severity: target + + +status: open + + +summary: '0.2' + + +time: Sun, 06 Dec 2009 00:37:15 +0000 + diff --git a/.be/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values b/.be/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values index 01b1768..2c8543e 100644 --- a/.be/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values +++ b/.be/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c/values @@ -1,6 +1,10 @@ creator: abentley +extra_strings: +- BLOCKS:4fc71206-4285-417f-8a3c-ed6fb31bbbda + + severity: minor @@ -9,6 +13,3 @@ status: closed summary: Support rcs configuration - -target: patch-52 - diff --git a/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values b/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values index 4e5613f..3e4747c 100644 --- a/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values +++ b/.be/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494/values @@ -1,3 +1,7 @@ +extra_strings: +- BLOCKS:47c8fd5f-1f5a-4048-bef7-bb4c9a37c411 + + severity: fatal @@ -6,6 +10,3 @@ status: fixed summary: Can't create bugs - -target: '0.1' - diff --git a/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values b/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values index cf35f07..3c7b8d1 100644 --- a/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values +++ b/.be/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55/values @@ -1,6 +1,10 @@ creator: abentley +extra_strings: +- BLOCKS:bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c + + severity: minor @@ -9,6 +13,3 @@ status: fixed summary: Implement bug tree diff - -target: '0.2' - diff --git a/.be/version b/.be/version index 7bd05c2..29baa0e 100644 --- a/.be/version +++ b/.be/version @@ -1 +1 @@ -Bugs Everywhere Directory v1.2 +Bugs Everywhere Directory v1.3 -- cgit From 6156b88b6d5b8eab7f8a2ad3df7c9e1d27951bd1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 19:43:54 -0500 Subject: Updated the new target bugs' status to match blockers --- .be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values | 2 +- .be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values | 2 +- .be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values b/.be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values index f665b83..b2e5b9e 100644 --- a/.be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values +++ b/.be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values @@ -8,7 +8,7 @@ extra_strings: severity: target -status: open +status: fixed summary: '0.1' diff --git a/.be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values b/.be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values index f419c52..a9e45b7 100644 --- a/.be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values +++ b/.be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values @@ -8,7 +8,7 @@ extra_strings: severity: target -status: open +status: closed summary: patch-52 diff --git a/.be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values b/.be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values index 8165308..8eb51c4 100644 --- a/.be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values +++ b/.be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values @@ -8,7 +8,7 @@ extra_strings: severity: target -status: open +status: fixed summary: '0.2' -- cgit From 117425d1243a553b4566a4ff855a9d5db6b61348 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 19:46:11 -0500 Subject: Remove some more Bug.target references from libbe/bug.py --- libbe/bug.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index ed1bbd2..06c2cc5 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -292,7 +292,6 @@ class Bug(settings_object.SavedSettingsObject): ('severity', self.severity), ('status', self.status), ('assigned', self.assigned), - ('target', self.target), ('reporter', self.reporter), ('creator', self.creator), ('created', timestring), @@ -349,7 +348,7 @@ class Bug(settings_object.SavedSettingsObject): if bug.tag != 'bug': raise utility.InvalidXML( \ 'bug', bug, 'root element must be ') - tags=['uuid','short-name','severity','status','assigned','target', + tags=['uuid','short-name','severity','status','assigned', 'reporter', 'creator','created','summary','extra-string'] self.explicit_attrs = [] uuid = None @@ -617,7 +616,6 @@ class Bug(settings_object.SavedSettingsObject): ("Severity", self.severity), ("Status", self.status), ("Assigned", self._setting_attr_string("assigned")), - ("Target", self._setting_attr_string("target")), ("Reporter", self._setting_attr_string("reporter")), ("Creator", self._setting_attr_string("creator")), ("Created", timestring)] @@ -817,7 +815,6 @@ def cmp_attr(bug_1, bug_2, attr, invert=False): cmp_uuid = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "uuid") 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") -cmp_target = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "target") cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter") cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary") cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings") @@ -843,8 +840,7 @@ def cmp_comments(bug_1, bug_2): DEFAULT_CMP_FULL_CMP_LIST = \ (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator, - cmp_reporter, cmp_target, cmp_comments, cmp_summary, cmp_uuid, - cmp_extra_strings) + cmp_reporter, cmp_comments, cmp_summary, cmp_uuid, cmp_extra_strings) class BugCompoundComparator (object): def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): -- cgit From f4a87cfb4fd66ad14ab6d077fe2defcb76dd2972 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 20:03:36 -0500 Subject: Added retroactive dependencies between the legacy targets. bug uuid created resolved target ee681951-f254-43d3-a53a-1b36ae415d5c revno: 45 revno: 51 patch-52 f51dc5a7-37b7-4ce1-859a-b7cb58be6494 revno: 41 revno: 41 0.1 f5c06914-dc64-4658-8ec7-32a026a53f55 revno: 45 revno: 93 0.2 reasonable target blockage: 0.1 ------------| 0.2 `-| patch-52 --| Here are the old targets $ be list --severity target --status all 4fc:ct: patch-52 47c:ft: 0.1 bd0:ft: 0.2 And here is the implemented dependency tree $ be depend -t -1 bd0 bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c blocked by: 47c:ft: 0.1 f51:ff: Can't create bugs 4fc:ct: patch-52 47c:ft: 0.1 f51:ff: Can't create bugs ee6:cm: Support rcs configuration f5c:fm: Implement bug tree diff --- .be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values | 2 ++ .be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values | 2 ++ .be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values b/.be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values index b2e5b9e..f008963 100644 --- a/.be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values +++ b/.be/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411/values @@ -3,6 +3,8 @@ creator: W. Trevor King extra_strings: - BLOCKED-BY:f51dc5a7-37b7-4ce1-859a-b7cb58be6494 +- BLOCKS:4fc71206-4285-417f-8a3c-ed6fb31bbbda +- BLOCKS:bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c severity: target diff --git a/.be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values b/.be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values index a9e45b7..4eebcc4 100644 --- a/.be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values +++ b/.be/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda/values @@ -2,7 +2,9 @@ creator: W. Trevor King extra_strings: +- BLOCKED-BY:47c8fd5f-1f5a-4048-bef7-bb4c9a37c411 - BLOCKED-BY:ee681951-f254-43d3-a53a-1b36ae415d5c +- BLOCKS:bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c severity: target diff --git a/.be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values b/.be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values index 8eb51c4..e42beab 100644 --- a/.be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values +++ b/.be/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c/values @@ -2,6 +2,8 @@ creator: W. Trevor King extra_strings: +- BLOCKED-BY:47c8fd5f-1f5a-4048-bef7-bb4c9a37c411 +- BLOCKED-BY:4fc71206-4285-417f-8a3c-ed6fb31bbbda - BLOCKED-BY:f5c06914-dc64-4658-8ec7-32a026a53f55 -- cgit From 1e32ab1eb4b4ed4d19de8514db1fd8c1b4fafc4e Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 20:52:29 -0500 Subject: Added --limit-status and --limit-severity to `be depend'. Currently only effective in tree mode, but that's where it matters most. --- becommands/depend.py | 91 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/becommands/depend.py b/becommands/depend.py index f50d693..6cf42eb 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -15,7 +15,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Add/remove bug dependencies""" -from libbe import cmdutil, bugdir, tree +from libbe import cmdutil, bugdir, bug, tree import os, copy __desc__ = __doc__ @@ -69,8 +69,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): parser = get_parser() options, args = parser.parse_args(args) cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True, - 1: lambda bug : bug.active==True}) + bugid_args={0: lambda _bug : _bug.active==True, + 1: lambda _bug : _bug.active==True}) if options.repair == True: if len(args) > 0: @@ -82,7 +82,6 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError("Too many arguments.") elif len(args) == 2 and options.tree_depth != None: raise cmdutil.UsageError("Only one bug id used in tree mode.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) @@ -95,10 +94,16 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): for blockee,blocker in fixed]) return 0 + allowed_status_values = _allowed_values(options.limit_status, + bug.status_values) + allowed_severity_values = _allowed_values(options.limit_severity, + bug.severity_values) bugA = cmdutil.bug_from_id(bd, args[0]) if options.tree_depth != None: - dtree = DependencyTree(bd, bugA, options.tree_depth) + dtree = DependencyTree(bd, bugA, options.tree_depth, + allowed_status_values, + allowed_severity_values) if len(dtree.blocked_by_tree()) > 0: print "%s blocked by:" % bugA.uuid for depth,node in dtree.blocked_by_tree().thread(): @@ -122,18 +127,18 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(blocked_by) > 0: print "%s blocked by:" % bugA.uuid if options.show_status == True: - print '\n'.join(["%s\t%s" % (bug.uuid, bug.status) - for bug in blocked_by]) + print '\n'.join(["%s\t%s" % (_bug.uuid, _bug.status) + for _bug in blocked_by]) else: - print '\n'.join([bug.uuid for bug in blocked_by]) + print '\n'.join([_bug.uuid for _bug in blocked_by]) blocks = get_blocks(bd, bugA) if len(blocks) > 0: print "%s blocks:" % bugA.uuid if options.show_status == True: - print '\n'.join(["%s\t%s" % (bug.uuid, bug.status) - for bug in blocks]) + print '\n'.join(["%s\t%s" % (_bug.uuid, _bug.status) + for _bug in blocks]) else: - print '\n'.join([bug.uuid for bug in blocks]) + print '\n'.join([_bug.uuid for _bug in blocks]) def get_parser(): parser = cmdutil.CmdOptionParser("be depend BUG-ID [BUG-ID]\nor: be depend --repair") @@ -143,6 +148,11 @@ def get_parser(): parser.add_option("-s", "--show-status", action="store_true", dest="show_status", default=False, help="Show status of blocking bugs") + parser.add_option("--limit-status", dest="limit_status", metavar="STATUS", + help="Only show bugs matching the STATUS specifier") + parser.add_option("--limit-severity", dest="limit_severity", + metavar="SEVERITY", + help="Only show bugs matching the SEVERITY specifier") parser.add_option("-t", "--tree-depth", metavar="DEPTH", default=None, type="int", dest="tree_depth", help="Print dependency tree rooted at BUG-ID with DEPTH levels of both blockers and blockees. Set DEPTH <= 0 to disable the depth limit.") @@ -158,6 +168,15 @@ If bug B is not specified, just print a list of bugs blocking (A). To search for bugs blocked by a particular bug, try $ be list --extra-strings BLOCKED-BY: +The --limit-* options allow you to either blacklist or whitelist +values, for example + $ be list --limit-status open,assigned +will only follow and print dependencies with open or assigned status. +You select blacklist mode by starting the list with a minus sign, for +example + $ be list --limit-severity -target +which will only follow and print dependencies with non-target severity. + In repair mode, add the missing direction to any one-way links. The "|--" symbol in the repair-mode output is inspired by the @@ -170,6 +189,44 @@ def help(): # internal helper functions +def _allowed_values(limit_string, possible_values, name="unkown"): + """ + >>> _allowed_values(None, ['abc', 'def', 'hij']) + ['abc', 'def', 'hij'] + >>> _allowed_values('-abc,hij', ['abc', 'def', 'hij']) + ['def'] + >>> _allowed_values('abc,hij', ['abc', 'def', 'hij']) + ['abc', 'hij'] + >>> _allowed_values('-xyz,hij', ['abc', 'def', 'hij'], name="value") + Traceback (most recent call last): + ... + UserError: Invalid value xyz + ['abc', 'def', 'hij'] + >>> _allowed_values('xyz,hij', ['abc', 'def', 'hij'], name="value") + Traceback (most recent call last): + ... + UserError: Invalid value xyz + ['abc', 'def', 'hij'] + """ + possible_values = list(possible_values) # don't alter the original + if limit_string == None: + pass + elif limit_string.startswith('-'): + blacklisted_values = set(limit_string[1:].split(',')) + for value in blacklisted_values: + if value not in possible_values: + raise cmdutil.UserError('Invalid %s %s\n %s' + % (name, value, possible_values)) + possible_values.remove(value) + else: + whitelisted_values = limit_string.split(',') + for value in whitelisted_values: + if value not in possible_values: + raise cmdutil.UserError('Invalid %s %s\n %s' + % (name, value, possible_values)) + possible_values = whitelisted_values + return possible_values + def _generate_blocks_string(blocked_bug): return "%s%s" % (BLOCKS_TAG, blocked_bug.uuid) @@ -310,10 +367,14 @@ class DependencyTree (object): """ Note: should probably be DependencyDiGraph. """ - def __init__(self, bugdir, root_bug, depth_limit=0): + def __init__(self, bugdir, root_bug, depth_limit=0, + allowed_status_values=None, + allowed_severity_values=None): self.bugdir = bugdir self.root_bug = root_bug self.depth_limit = depth_limit + self.allowed_status_values = allowed_status_values + self.allowed_severity_values = allowed_severity_values def _build_tree(self, child_fn): root = tree.Tree() root.bug = self.root_bug @@ -324,6 +385,12 @@ class DependencyTree (object): if self.depth_limit > 0 and node.depth == self.depth_limit: continue for bug in child_fn(self.bugdir, node.bug): + if self.allowed_status_values != None \ + and not bug.status in self.allowed_status_values: + continue + if self.allowed_severity_values != None \ + and not bug.severity in self.allowed_severity_values: + continue child = tree.Tree() child.bug = bug child.depth = node.depth+1 -- cgit From 1f852b56004d50fd7412788a0ae208907acc1120 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 20:57:31 -0500 Subject: Remove Bug.target references from several becommands (all but target.py). --- becommands/html.py | 4 ++-- becommands/merge.py | 2 -- becommands/new.py | 4 ++-- becommands/show.py | 1 - 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/becommands/html.py b/becommands/html.py index 622a531..a031188 100644 --- a/becommands/html.py +++ b/becommands/html.py @@ -177,7 +177,7 @@ class HTMLGen (object): 'shortname':self.bd.bug_shortname(bug), 'comment_entries':comment_entries, 'generation_time':self.generation_time} - for attr in ['uuid', 'severity', 'status', 'assigned', 'target', + for attr in ['uuid', 'severity', 'status', 'assigned', 'reporter', 'creator', 'time_string', 'summary']: template_info[attr] = self._escape(getattr(bug, attr)) self._write_file(self.bug_file % template_info, [fullpath]) @@ -272,7 +272,7 @@ class HTMLGen (object): if self.verbose: print "\tCreating bug entry for %s" % self.bd.bug_shortname(bug) template_info = {'shortname':self.bd.bug_shortname(bug)} - for attr in ['uuid', 'severity', 'status', 'assigned', 'target', + for attr in ['uuid', 'severity', 'status', 'assigned', 'reporter', 'creator', 'time_string', 'summary']: template_info[attr] = self._escape(getattr(bug, attr)) bug_entries.append(self.index_bug_entry % template_info) diff --git a/becommands/merge.py b/becommands/merge.py index 31c781f..8cf7e2f 100644 --- a/becommands/merge.py +++ b/becommands/merge.py @@ -51,7 +51,6 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): Severity : minor Status : open Assigned : - Target : Reporter : Creator : John Doe Created : ... @@ -96,7 +95,6 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): Severity : minor Status : closed Assigned : - Target : Reporter : Creator : Jane Doe Created : ... diff --git a/becommands/new.py b/becommands/new.py index 92d61e4..00e8a47 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -37,8 +37,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): True >>> print bug.severity minor - >>> bug.target == None - True + >>> print bug.status + open >>> bd.cleanup() """ parser = get_parser() diff --git a/becommands/show.py b/becommands/show.py index cc43f9a..ab1708f 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -33,7 +33,6 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): Severity : minor Status : open Assigned : - Target : Reporter : Creator : John Doe Created : ... -- cgit From 3f7ec7f8c3dc9b52cd6d6b14866aad1faf502676 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 21:01:49 -0500 Subject: Removed Bug.target reference from interfaces/xml/be-xml-to-mbox. --- interfaces/xml/be-xml-to-mbox | 1 - 1 file changed, 1 deletion(-) diff --git a/interfaces/xml/be-xml-to-mbox b/interfaces/xml/be-xml-to-mbox index dc4524e..ef7b714 100755 --- a/interfaces/xml/be-xml-to-mbox +++ b/interfaces/xml/be-xml-to-mbox @@ -78,7 +78,6 @@ class Bug (LimitedAttrDict): u"severity", u"status", u"assigned", - u"target", u"reporter", u"creator", u"created", -- cgit From 4d6bc107406ce936639cd1bd314d6ac3b4e3bff3 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 21:04:41 -0500 Subject: Removed Bugs-Everywhere-Web/server.log from version control --- interfaces/web/Bugs-Everywhere-Web/server.log | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 interfaces/web/Bugs-Everywhere-Web/server.log diff --git a/interfaces/web/Bugs-Everywhere-Web/server.log b/interfaces/web/Bugs-Everywhere-Web/server.log deleted file mode 100644 index fe02ade..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/server.log +++ /dev/null @@ -1,26 +0,0 @@ -2005/12/01 15:44:05 CONFIG INFO Server parameters: -2005/12/01 15:44:05 CONFIG INFO server.environment: production -2005/12/01 15:44:05 CONFIG INFO server.logToScreen: False -2005/12/01 15:44:05 CONFIG INFO server.logFile: server.log -2005/12/01 15:44:05 CONFIG INFO server.protocolVersion: HTTP/1.0 -2005/12/01 15:44:05 CONFIG INFO server.socketHost: -2005/12/01 15:44:05 CONFIG INFO server.socketPort: 8080 -2005/12/01 15:44:05 CONFIG INFO server.socketFile: -2005/12/01 15:44:05 CONFIG INFO server.reverseDNS: False -2005/12/01 15:44:05 CONFIG INFO server.socketQueueSize: 5 -2005/12/01 15:44:05 CONFIG INFO server.threadPool: 0 -2005/12/01 15:44:05 HTTP INFO Serving HTTP on http://localhost:8080/ -2005/12/01 15:44:17 HTTP INFO 127.0.0.1 - GET / HTTP/1.1 -2005/12/01 15:44:37 HTTP INFO 192.168.2.12 - GET / HTTP/1.1 -2005/12/01 15:44:42 HTTP INFO 192.168.2.12 - GET /be HTTP/1.1 -2005/12/01 15:44:43 HTTP INFO 192.168.2.12 - GET /be/301724b1-3853-4aff-8f23-44373df7cf1c HTTP/1.1 -2005/12/01 15:44:48 HTTP INFO 192.168.2.12 - GET /be/ HTTP/1.1 -2005/12/01 15:44:50 HTTP INFO 192.168.2.12 - GET / HTTP/1.1 -2005/12/01 15:44:53 HTTP INFO 192.168.2.12 - GET /devel/ HTTP/1.1 -2005/12/01 15:44:58 HTTP INFO 192.168.2.12 - GET / HTTP/1.1 -2005/12/01 15:52:57 HTTP INFO 127.0.0.1 - GET /devel HTTP/1.1 -2005/12/01 15:52:59 HTTP INFO 127.0.0.1 - GET /devel HTTP/1.1 -2005/12/01 15:53:25 HTTP INFO 127.0.0.1 - GET /devel HTTP/1.1 -2005/12/01 15:53:29 HTTP INFO hit: shutting down server -2005/12/01 15:53:29 HTTP INFO HTTP Server shut down -2005/12/01 15:53:29 HTTP INFO CherryPy shut down -- cgit From 0295c3f04ac457081559064ddf965dc507d04553 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 21:35:56 -0500 Subject: Moved becommands.depend._allowed_values() to cmdutil.select_values() I like this code, and I want to use it for other places, e.g. `be list`. Also renamed depend options --limit-severity and --limit-status to --severity and --status --- becommands/depend.py | 59 ++++++++++------------------------------------------ libbe/cmdutil.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 48 deletions(-) diff --git a/becommands/depend.py b/becommands/depend.py index 6cf42eb..d19845d 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -94,10 +94,11 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): for blockee,blocker in fixed]) return 0 - allowed_status_values = _allowed_values(options.limit_status, - bug.status_values) - allowed_severity_values = _allowed_values(options.limit_severity, - bug.severity_values) + allowed_status_values = \ + cmdutil.select_values(options.limit_status, bug.status_values) + allowed_severity_values = \ + cmdutil.select_values(options.limit_severity, bug.severity_values) + bugA = cmdutil.bug_from_id(bd, args[0]) if options.tree_depth != None: @@ -148,9 +149,9 @@ def get_parser(): parser.add_option("-s", "--show-status", action="store_true", dest="show_status", default=False, help="Show status of blocking bugs") - parser.add_option("--limit-status", dest="limit_status", metavar="STATUS", + parser.add_option("--status", dest="limit_status", metavar="STATUS", help="Only show bugs matching the STATUS specifier") - parser.add_option("--limit-severity", dest="limit_severity", + parser.add_option("--severity", dest="limit_severity", metavar="SEVERITY", help="Only show bugs matching the SEVERITY specifier") parser.add_option("-t", "--tree-depth", metavar="DEPTH", default=None, @@ -168,13 +169,13 @@ If bug B is not specified, just print a list of bugs blocking (A). To search for bugs blocked by a particular bug, try $ be list --extra-strings BLOCKED-BY: -The --limit-* options allow you to either blacklist or whitelist -values, for example - $ be list --limit-status open,assigned +The --status and --severity options allow you to either blacklist or +whitelist values, for example + $ be list --status open,assigned will only follow and print dependencies with open or assigned status. You select blacklist mode by starting the list with a minus sign, for example - $ be list --limit-severity -target + $ be list --severity -target which will only follow and print dependencies with non-target severity. In repair mode, add the missing direction to any one-way links. @@ -189,44 +190,6 @@ def help(): # internal helper functions -def _allowed_values(limit_string, possible_values, name="unkown"): - """ - >>> _allowed_values(None, ['abc', 'def', 'hij']) - ['abc', 'def', 'hij'] - >>> _allowed_values('-abc,hij', ['abc', 'def', 'hij']) - ['def'] - >>> _allowed_values('abc,hij', ['abc', 'def', 'hij']) - ['abc', 'hij'] - >>> _allowed_values('-xyz,hij', ['abc', 'def', 'hij'], name="value") - Traceback (most recent call last): - ... - UserError: Invalid value xyz - ['abc', 'def', 'hij'] - >>> _allowed_values('xyz,hij', ['abc', 'def', 'hij'], name="value") - Traceback (most recent call last): - ... - UserError: Invalid value xyz - ['abc', 'def', 'hij'] - """ - possible_values = list(possible_values) # don't alter the original - if limit_string == None: - pass - elif limit_string.startswith('-'): - blacklisted_values = set(limit_string[1:].split(',')) - for value in blacklisted_values: - if value not in possible_values: - raise cmdutil.UserError('Invalid %s %s\n %s' - % (name, value, possible_values)) - possible_values.remove(value) - else: - whitelisted_values = limit_string.split(',') - for value in whitelisted_values: - if value not in possible_values: - raise cmdutil.UserError('Invalid %s %s\n %s' - % (name, value, possible_values)) - possible_values = whitelisted_values - return possible_values - def _generate_blocks_string(blocked_bug): return "%s%s" % (BLOCKS_TAG, blocked_bug.uuid) diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index dcd4ca9..b892bde 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -217,6 +217,59 @@ def underlined(instring): return "%s\n%s" % (instring, "="*len(instring)) +def select_values(string, possible_values, name="unkown"): + """ + This function allows the user to select values from a list of + possible values. The default is to select all the values: + + >>> select_values(None, ['abc', 'def', 'hij']) + ['abc', 'def', 'hij'] + + The user selects values with a comma-separated limit_string. + Prepending a minus sign to such a list denotes blacklist mode: + + >>> select_values('-abc,hij', ['abc', 'def', 'hij']) + ['def'] + + Without the leading -, the selection is in whitelist mode: + + >>> select_values('abc,hij', ['abc', 'def', 'hij']) + ['abc', 'hij'] + + In either case, appropriate errors are raised if on of the + user-values is not in the list of possible values. The name + parameter lets you make the error message more clear: + + >>> select_values('-xyz,hij', ['abc', 'def', 'hij'], name="foobar") + Traceback (most recent call last): + ... + UserError: Invalid foobar xyz + ['abc', 'def', 'hij'] + >>> select_values('xyz,hij', ['abc', 'def', 'hij'], name="foobar") + Traceback (most recent call last): + ... + UserError: Invalid foobar xyz + ['abc', 'def', 'hij'] + """ + possible_values = list(possible_values) # don't alter the original + if string == None: + pass + elif string.startswith('-'): + blacklisted_values = set(string[1:].split(',')) + for value in blacklisted_values: + if value not in possible_values: + raise UserError('Invalid %s %s\n %s' + % (name, value, possible_values)) + possible_values.remove(value) + else: + whitelisted_values = string.split(',') + for value in whitelisted_values: + if value not in possible_values: + raise UserError('Invalid %s %s\n %s' + % (name, value, possible_values)) + possible_values = whitelisted_values + return possible_values + def restrict_file_access(bugdir, path): """ Check that the file at path is inside bugdir.root. This is -- cgit From fadbadc2bf763351ef572c5a584964f42b349f96 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 22:00:04 -0500 Subject: Removed target stuff from becommands/list.py and tweaked options. Now --status, --severity, and --assigned all use cmdutil.select_values() for nice whitelist/blacklist selection. --- becommands/depend.py | 9 ++++----- becommands/list.py | 54 +++++++++++++++++++++------------------------------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/becommands/depend.py b/becommands/depend.py index d19845d..1a2b905 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -95,9 +95,9 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): return 0 allowed_status_values = \ - cmdutil.select_values(options.limit_status, bug.status_values) + cmdutil.select_values(options.status, bug.status_values) allowed_severity_values = \ - cmdutil.select_values(options.limit_severity, bug.severity_values) + cmdutil.select_values(options.severity, bug.severity_values) bugA = cmdutil.bug_from_id(bd, args[0]) @@ -149,10 +149,9 @@ def get_parser(): parser.add_option("-s", "--show-status", action="store_true", dest="show_status", default=False, help="Show status of blocking bugs") - parser.add_option("--status", dest="limit_status", metavar="STATUS", + parser.add_option("--status", dest="status", metavar="STATUS", help="Only show bugs matching the STATUS specifier") - parser.add_option("--severity", dest="limit_severity", - metavar="SEVERITY", + parser.add_option("--severity", dest="severity", metavar="SEVERITY", help="Only show bugs matching the SEVERITY specifier") parser.add_option("-t", "--tree-depth", metavar="DEPTH", default=None, type="int", dest="tree_depth", diff --git a/becommands/list.py b/becommands/list.py index 2749228..fa5647b 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -33,8 +33,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): >>> os.chdir(bd.root) >>> execute([], manipulate_encodings=False) a:om: Bug A - >>> execute(["--status", "all"], manipulate_encodings=False) - a:om: Bug A + >>> execute(["--status", "closed"], manipulate_encodings=False) b:cm: Bug B >>> bd.cleanup() """ @@ -60,7 +59,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if options.status == "all": status = bug.status_values else: - status = options.status.split(',') + status = cmdutil.select_values(options.status, bug.status_values) else: status = [] if options.active == True: @@ -78,7 +77,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if options.severity == "all": severity = bug.severity_values else: - severity = options.severity.split(',') + severity = cmdutil.select_values(options.severity, + bug.severity_values) else: severity = [] if options.wishlist == True: @@ -93,7 +93,14 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if options.assigned == "all": assigned = "all" else: - assigned = options.assigned.split(',') + possible_assignees = [] + for _bug in bd: + if _bug.assigned != None \ + and not _bug.assigned in possible_assignees: + possible_assignees.append(_bug.assigned) + assigned = cmdutil.select_values(options.assigned, + possible_assignees) + print 'assigned', assigned else: assigned = [] if options.mine == True: @@ -103,18 +110,6 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): for i in range(len(assigned)): if assigned[i] == '-': assigned[i] = bd.user_id - # 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(bd.target) - if target == []: # set the default value - target = "all" if options.extra_strings != None: extra_string_regexps = [re.compile(x) for x in options.extra_strings.split(',')] @@ -125,8 +120,6 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): return False if assigned != "all" and not bug.assigned in assigned: return False - if target != "all" and not bug.target in target: - return False if options.extra_strings != None: if len(bug.extra_strings) == 0 and len(extra_string_regexps) > 0: return False @@ -167,29 +160,26 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): def get_parser(): parser = cmdutil.CmdOptionParser("be list [options]") - parser.add_option("-s", "--status", metavar="STATUS", dest="status", - help="List bugs matching STATUS", default=None) - parser.add_option("-v", "--severity", metavar="SEVERITY", dest="severity", - help="List bugs matching SEVERITY", default=None) + parser.add_option("--status", dest="status", metavar="STATUS", + help="Only show bugs matching the STATUS specifier") + parser.add_option("--severity", dest="severity", metavar="SEVERITY", + help="Only show bugs matching the SEVERITY specifier") parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", help="List bugs matching ASSIGNED", default=None) - parser.add_option("-t", "--target", metavar="TARGET", dest="target", - help="List bugs matching TARGET", default=None) parser.add_option("-e", "--extra-strings", metavar="STRINGS", dest="extra_strings", help="List bugs matching _all_ extra strings in comma-seperated list STRINGS. e.g. --extra-strings TAG:working,TAG:xml", default=None) parser.add_option("-S", "--sort", metavar="SORT-BY", dest="sort_by", help="Adjust bug-sort criteria with comma-separated list SORT-BY. e.g. \"--sort creator,time\". Available criteria: %s" % ','.join(AVAILABLE_CMPS), default=None) # boolean options. All but uuids and xml are special cases of long forms bools = (("u", "uuids", "Only print the bug UUIDS"), + ("x", "xml", "Dump as XML"), ("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"), - ("x", "xml", "Dump as XML")) + ("m", "mine", "List bugs assigned to you")) for s in bools: attr = s[1].replace('-','_') short = "-%c" % s[0] @@ -216,10 +206,12 @@ There are several criteria that you can filter 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. +to match all values of the criterion. As with the --status and +--severity options for `be depend`, starting the list with a minus +sign makes your selections a blacklist instead of the default +whitelist. status %s @@ -227,8 +219,6 @@ 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. -- cgit From b1f88a431427319cc35d44b69472d8c3dbb2ffa4 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 23:51:43 -0500 Subject: Docstring clarification in becommands.depend.get_blocked_by() --- becommands/depend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/becommands/depend.py b/becommands/depend.py index 1a2b905..6336793 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -258,7 +258,7 @@ def get_blocks(bugdir, bug): def get_blocked_by(bugdir, bug): """ - Return a list of bugs blocking the given bug blocks. + Return a list of bugs blocking the given bug. """ blocked_by = [] for uuid in _get_blocked_by(bug): -- cgit From 2a7cf58a422bcd55478cea92c5f859df86c911a4 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 23:52:07 -0500 Subject: `be target` gains --resolve and loses --list. `be target` now works with bug-style targets. --- becommands/target.py | 127 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 98 insertions(+), 29 deletions(-) diff --git a/becommands/target.py b/becommands/target.py index 9a202b1..fbc4b37 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -18,13 +18,14 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Show or change a bug's target for fixing""" +"""Assorted bug target manipulations and queries""" from libbe import cmdutil, bugdir +from becommands import depend __desc__ = __doc__ def execute(args, manipulate_encodings=True, restrict_file_access=False): """ - >>> import os + >>> import os, StringIO, sys >>> bd = bugdir.SimpleBugDir() >>> os.chdir(bd.root) >>> execute(["a"], manipulate_encodings=False) @@ -32,8 +33,19 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): >>> execute(["a", "tomorrow"], manipulate_encodings=False) >>> execute(["a"], manipulate_encodings=False) tomorrow - >>> execute(["--list"], manipulate_encodings=False) + + >>> orig_stdout = sys.stdout + >>> tmp_stdout = StringIO.StringIO() + >>> sys.stdout = tmp_stdout + >>> execute(["--resolve", "tomorrow"], manipulate_encodings=False) + >>> sys.stdout = orig_stdout + >>> output = tmp_stdout.getvalue().strip() + >>> target = bd.bug_from_uuid(output) + >>> print target.summary tomorrow + >>> print target.severity + target + >>> execute(["a", "none"], manipulate_encodings=False) >>> execute(["a"], manipulate_encodings=False) No target assigned. @@ -44,52 +56,109 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): cmdutil.default_complete(options, args, parser, bugid_args={0: lambda bug : bug.active==True}) - if len(args) not in (1, 2): - if not (options.list == True and len(args) == 0): - raise cmdutil.UsageError + if (options.resolve == False and len(args) not in (1, 2)) \ + or (options.resolve == True and len(args) not in (0, 1)): + raise cmdutil.UsageError('Incorrect number of arguments.') bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) - if options.list: - ts = set([bd.bug_from_uuid(bug).target for bug in bd.uuids()]) - for target in sorted(ts): - if target and isinstance(target,str): - print target + if options.resolve == True: + if len(args) == 0: + summary = None + else: + summary = args[0] + bug = bug_from_target_summary(bd, summary) + if bug == None: + print 'No target assigned.' + else: + print bug.uuid return bug = cmdutil.bug_from_id(bd, args[0]) if len(args) == 1: - if bug.target is None: + target = bug_target(bd, bug) + if target is None: print "No target assigned." else: - print bug.target + print target.summary else: - assert len(args) == 2 if args[1] == "none": - bug.target = None + target = remove_target(bd, bug) else: - bug.target = args[1] + target = add_target(bd, bug, args[1]) def get_parser(): - parser = cmdutil.CmdOptionParser("be target BUG-ID [TARGET]\nor: be target --list") - parser.add_option("-l", "--list", action="store_true", dest="list", - help="List all available targets and exit") + parser = cmdutil.CmdOptionParser("be target BUG-ID [TARGET]\nor: be target --resolve [TARGET]") + parser.add_option("-r", "--resolve", action="store_true", dest="resolve", + help="Print the UUID for the target bug whose summary matches TARGET. If TARGET is not given, print the UUID of the current bugdir target. If that is not set, don't print anything.", + default=False) return parser longhelp=""" -Show or change a bug's target for fixing. +Assorted bug target manipulations and queries. + +If no target is specified, the bug's current target is printed. If +TARGET is specified, it will be assigned to the bug, creating a new +target bug if necessary. -If no target is specified, the current value is printed. If a target -is specified, it will be assigned to the bug. +Targets are free-form; any text may be specified. They will generally +be milestone names or release numbers. The value "none" can be used +to unset the target. -Targets are freeform; any text may be specified. They will generally be -milestone names or release numbers. +In the alternative `be target --resolve TARGET` form, print the UUID +of the target-bug with summary TARGET. If target is not given, return +use the bugdir's current target (see `be set`). -The value "none" can be used to unset the target. +If you want to list all bugs blocking the current target, try + $ be depend --status -closed,fixed,wontfix --severity -target \ + $(be target --resolve) -In the alternative `be target --list` form print a list of all -currently specified targets. Note that bug status -(i.e. opened/closed) is ignored. If you want to list all bugs -matching a current target, see `be list --target TARGET'. +If you want to set the current bugdir target by summary (rather than +by UUID), try + $ be set target $(be target --resolve SUMMARY) """ def help(): return get_parser().help_str() + longhelp + +def bug_from_target_summary(bugdir, summary=None): + if summary == None: + if bugdir.target == None: + return None + else: + return bugdir.bug_from_uuid(bugdir.target) + matched = [] + for uuid in bugdir.uuids(): + bug = bugdir.bug_from_uuid(uuid) + if bug.severity == 'target' and bug.summary == summary: + matched.append(bug) + if len(matched) == 0: + return None + if len(matched) > 1: + raise Exception('Several targets with same summary: %s' + % '\n '.join([bug.uuid for bug in matched])) + return matched[0] + +def bug_target(bugdir, bug): + matched = [] + for blocked in depend.get_blocks(bugdir, bug): + if blocked.severity == 'target': + matched.append(blocked) + if len(matched) == 0: + return None + if len(matched) > 1: + raise Exception('This bug (%s) blocks several targets: %s' + % (bug.uuid, + '\n '.join([b.uuid for b in matched]))) + return matched[0] + +def remove_target(bugdir, bug): + target = bug_target(bugdir, bug) + depend.remove_block(target, bug) + return target + +def add_target(bugdir, bug, summary): + target = bug_from_target_summary(bugdir, summary) + if target == None: + target = bugdir.new_bug(summary=summary) + target.severity = 'target' + depend.add_block(target, bug) + return target -- cgit From 0a54c4bd9929b385074e7488aadd8f848cdefcd4 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 6 Dec 2009 00:21:07 -0500 Subject: Added becommands/due.py to manage bug due dates. This fulfills the following part of 22b:7 * "due_by" We could add "due-by" to Bug.extra_strings as well, so that anyone could set due dates for any issue they wanted. Currently there's not much going on, but perhaps other people will have ideas for useful extensions. Maybe be due --sort BUG-ID [BUG-ID ...] or be --due-in-days 7 --- becommands/due.py | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 becommands/due.py diff --git a/becommands/due.py b/becommands/due.py new file mode 100644 index 0000000..23f98f6 --- /dev/null +++ b/becommands/due.py @@ -0,0 +1,92 @@ +# Copyright (C) +"""Set bug due dates""" +from libbe import cmdutil, bugdir, utility +__desc__ = __doc__ + +DUE_TAG="DUE:" + +def execute(args, manipulate_encodings=True, restrict_file_access=False): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> bd.save() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + No due date assigned. + >>> execute(["a", "Thu, 01 Jan 1970 00:00:00 +0000"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + Thu, 01 Jan 1970 00:00:00 +0000 + >>> execute(["a", "none"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + >>> execute(["a"], manipulate_encodings=False) + No due date assigned. + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if len(args) not in (1, 2): + raise cmdutil.UsageError('Incorrect number of arguments.') + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) + bug = cmdutil.bug_from_id(bd, args[0]) + if len(args) == 1: + due_time = get_due(bug) + if due_time is None: + print "No due date assigned." + else: + print utility.time_to_str(due_time) + else: + if args[1] == "none": + remove_due(bug) + else: + due_time = utility.str_to_time(args[1]) + set_due(bug, due_time) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be due BUG-ID [DATE]") + return parser + +longhelp=""" +If no DATE is specified, the bug's current due date is printed. If +DATE is specified, it will be assigned to the bug. +""" + +def help(): + return get_parser().help_str() + longhelp + +# internal helper functions + +def _generate_due_string(time): + return "%s%s" % (DUE_TAG, utility.time_to_str(time)) + +def _parse_due_string(string): + assert string.startswith(DUE_TAG) + return utility.str_to_time(string[len(DUE_TAG):]) + +# functions exposed to other modules + +def get_due(bug): + matched = [] + for line in bug.extra_strings: + if line.startswith(DUE_TAG): + matched.append(_parse_due_string(line)) + if len(matched) == 0: + return None + if len(matched) > 1: + raise Exception('Several due dates for %s?:\n %s' + % (bug.uuid, '\n '.join(matched))) + return matched[0] + +def remove_due(bug): + estrs = bug.extra_strings + for due_str in [s for s in estrs if s.startswith(DUE_TAG)]: + estrs.remove(due_str) + bug.extra_strings = estrs # reassign to notice change + +def set_due(bug, time): + remove_due(bug) + estrs = bug.extra_strings + estrs.append(_generate_due_string(time)) + bug.extra_strings = estrs # reassign to notice change -- cgit From f285716a405775a2e62e9ede4abc67c960d2a62c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 6 Dec 2009 00:27:24 -0500 Subject: Updated becommands/due.py copyright. --- becommands/due.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/becommands/due.py b/becommands/due.py index 23f98f6..6f11ad4 100644 --- a/becommands/due.py +++ b/becommands/due.py @@ -1,4 +1,18 @@ -# Copyright (C) +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Set bug due dates""" from libbe import cmdutil, bugdir, utility __desc__ = __doc__ -- cgit From 206d6254a3e02adf34d2e1a49a817f9ef779ebf1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 6 Dec 2009 00:28:22 -0500 Subject: Updated NEWS. --- NEWS | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/NEWS b/NEWS index 21a0140..0cf9aae 100644 --- a/NEWS +++ b/NEWS @@ -1,9 +1,35 @@ December 5, 2009 + * targets are now a special type of bug (severity 'target'), so you + can do all the things you do with normal bugs to them as well + (e.g. comment on them, link them into dependency trees, etc.) + * new command `be due` to get/set bug due dates. * changes to `be diff` * exits with an error if required revision control is not possible. Previously it printed a message, but exitted with status 0. * removed options --new, --removed, --modified, --all * added options --uuids, --subscribe + Replace: + '--new' with '--uuids --subscribe DIR:new' + '--removed' with '--uuids --subscribe DIR:rem' + '--modified' with '--uuids --subscribe DIR:mod' + '--all' with '--uuids' + * changes to `be depend` + * added options --status, --severity + * changes to `be list` + * added blacklist capability to --status, --severity, --assigned + * removed options --target, --cur-target + Replace: + 'be list --target TARGET' with + 'be depend --status -closed,fixed,wontfix --severity -target \ + $(be target --resolve TARGET)' + 'be list --cur-target' with + 'be depend --status -closed,fixed,wontfix --severity -target \ + $(be target --resolve)' + * changes to `be target` + * added option --resolve + * removed option --list + Replace: + 'be target --list' with 'be list --status all --severity target' * assorted cleanups and bugfixes December 4, 2009 -- cgit From ff1ca79e6781447dbad6279d6c4cdad44fad5cdd Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 6 Dec 2009 00:43:59 -0500 Subject: Marked as fixed 22b: Sorting targets chronologically --- .../comments/64424f05-b42b-4835-8afd-8495ae61345d/body | 8 ++++++++ .../comments/64424f05-b42b-4835-8afd-8495ae61345d/values | 11 +++++++++++ .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/body create mode 100644 .be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/values diff --git a/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/body b/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/body new file mode 100644 index 0000000..08595d1 --- /dev/null +++ b/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/body @@ -0,0 +1,8 @@ +Implemented. + +You can now list targets by dependency (not by date, but better for +most cases) with + be depend -t-1 --severity target ID +where ID is the uuid of any target bug, or with + be depend -t-1 --severity target $(be target --resolve TARGET) +where TARGET is the summary of any target bug. diff --git a/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/values b/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/values new file mode 100644 index 0000000..3925aa2 --- /dev/null +++ b/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d/values @@ -0,0 +1,11 @@ +Author: W. Trevor King + + +Content-type: text/plain + + +Date: Sun, 06 Dec 2009 05:42:52 +0000 + + +In-reply-to: 4012c6cc-1300-4f6b-af0e-9176eedf8de7 + diff --git a/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values b/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values index 7440b56..64928e8 100644 --- a/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values +++ b/.be/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/values @@ -14,7 +14,7 @@ reporter: Gianluca Montecchi severity: wishlist -status: assigned +status: fixed summary: Sorting targets chronologically -- cgit From bca7a2a9311a5a23d98229d9918b13d66302537e Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 6 Dec 2009 03:11:39 -0500 Subject: becommands.target.bug_target(TARGET-BUG) now returns TARGET-BUG --- becommands/target.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/becommands/target.py b/becommands/target.py index fbc4b37..9ccbacc 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -138,6 +138,8 @@ def bug_from_target_summary(bugdir, summary=None): return matched[0] def bug_target(bugdir, bug): + if bug.severity == 'target': + return bug matched = [] for blocked in depend.get_blocks(bugdir, bug): if blocked.severity == 'target': -- cgit From fc131e3acbf657f42959910c4f4483a09c871016 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 6 Dec 2009 04:24:07 -0500 Subject: Set BugDir(root=X) instead of os.chdir(X) in --- becommands/diff.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/becommands/diff.py b/becommands/diff.py index 2cff537..e844c10 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -77,9 +77,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): revision = bd.vcs.revision_id(-1) old_bd = bd.duplicate_bugdir(revision) else: - cwd = os.getcwd() - os.chdir(options.dir) - old_bd_current = bugdir.BugDir(from_disk=True, + old_bd_current = bugdir.BugDir(root=os.path.abspath(options.dir), + from_disk=True, manipulate_encodings=False) if revision == None: # use the current working state old_bd = old_bd_current @@ -88,7 +87,6 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError('%s is not revision-controlled.' % options.dir) old_bd = old_bd_current.duplicate_bugdir(revision) - os.chdir(cwd) d = diff.Diff(old_bd, bd) tree = d.report_tree(subscriptions) -- cgit From ba31b657c49649ee0b00663a32e907bb482270ac Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 6 Dec 2009 17:20:39 -0500 Subject: be --dir DIR COMMAND now roots the bugdir in DIR without changing directories. Previously, for the directory structure A |-- X `-- Y You could do something like A$ be --dir X diff --dir ../Y Now it's A$ be --dir X diff --dir Y The --root option to `be init` has been removed as redundant. Replace calls like be init --root DIR with be --dir DIR init --- .../79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body | 13 +++++++++++ .../79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values | 11 +++++++++ NEWS | 5 +++++ README.dev | 5 ++++- be | 8 +++---- becommands/assign.py | 6 +++-- becommands/close.py | 6 +++-- becommands/comment.py | 6 +++-- becommands/commit.py | 6 +++-- becommands/depend.py | 6 +++-- becommands/diff.py | 6 +++-- becommands/due.py | 6 +++-- becommands/email_bugs.py | 6 +++-- becommands/help.py | 3 ++- becommands/html.py | 6 +++-- becommands/import_xml.py | 6 +++-- becommands/init.py | 26 ++++++++++++---------- becommands/list.py | 6 +++-- becommands/merge.py | 6 +++-- becommands/new.py | 6 +++-- becommands/open.py | 6 +++-- becommands/remove.py | 6 +++-- becommands/set.py | 6 +++-- becommands/severity.py | 6 +++-- becommands/show.py | 6 +++-- becommands/status.py | 6 +++-- becommands/subscribe.py | 6 +++-- becommands/tag.py | 6 +++-- becommands/target.py | 6 +++-- libbe/cmdutil.py | 7 ++++-- 30 files changed, 145 insertions(+), 65 deletions(-) create mode 100644 .be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body create mode 100644 .be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values diff --git a/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body b/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body new file mode 100644 index 0000000..7dbeebb --- /dev/null +++ b/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body @@ -0,0 +1,13 @@ +> * Determining what to commit. +> +> You'd have to have RCS keep a log of all versioned files it +> touched, and extend .commit() to accept the keyword list "files" +> and commit only those files. This is doable, but maybe not worth +> the trouble. + +On the other hand, just attemting to commit evverything after each +command would make it nice and easy to commit bug fixes: + be --auto-commit status XYZ fixed +which would commit whatever changes you had outstanding with an +appropriate commit message. + diff --git a/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values b/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values new file mode 100644 index 0000000..b3dba3f --- /dev/null +++ b/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values @@ -0,0 +1,11 @@ +Author: W. Trevor King + + +Content-type: text/plain + + +Date: Sun, 06 Dec 2009 21:45:15 +0000 + + +In-reply-to: 4c50ca0b-a08f-4723-b00d-4bf342cf86b6 + diff --git a/NEWS b/NEWS index 0cf9aae..84256e1 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,8 @@ +December 6, 2009 + * be --dir DIR COMMAND now roots the bugdir in DIR _without_ changing + directories. + * `be init --root DIR` should now be `be --dir DIR init`. + December 5, 2009 * targets are now a special type of bug (severity 'target'), so you can do all the things you do with normal bugs to them as well diff --git a/README.dev b/README.dev index fb4f471..dbb97b0 100644 --- a/README.dev +++ b/README.dev @@ -10,7 +10,8 @@ To fit into the current framework, your extension module should provide the following elements: __desc__ A short string describing the purpose of your plugin - execute(args, manipulate_encodings=True, restrict_file_access=False) + execute(args, manipulate_encodings=True, restrict_file_access=False, + dir=".") The entry function for your plugin. args is everything from sys.argv after the name of your plugin (e.g. for the command `be open abc', args=['abc']). @@ -23,6 +24,8 @@ provide the following elements: before attempting to read or write a file. See the restrict_file_access documentation for details. + dir is a directory inside the repository of interest. + Note: be supports command-completion. To avoid raising errors you need to deal with possible '--complete' options and arguments. See the 'Command completion' section below for more information. diff --git a/be b/be index c5c372f..a4a3ea4 100755 --- a/be +++ b/be @@ -34,8 +34,8 @@ parser.add_option("--version", action="store_true", dest="version", help="Print version string and exit.") parser.add_option("--verbose-version", action="store_true", dest="verbose_version", help="Print verbose version information and exit.") -parser.add_option("-d", "--dir", dest="dir", metavar="DIR", - help="Run this command from DIR instead of the current directory.") +parser.add_option("-d", "--dir", dest="dir", metavar="DIR", default=".", + help="Run this command on the repository in DIR instead of the current directory.") try: options,args = parser.parse_args() @@ -56,13 +56,11 @@ except cmdutil.GetCompletions, e: if options.version == True or options.verbose_version == True: print version.version(verbose=options.verbose_version) sys.exit(0) -if options.dir != None: - os.chdir(options.dir) try: if len(args) == 0: raise cmdutil.UsageError, "must supply a command" - sys.exit(cmdutil.execute(args[0], args[1:])) + sys.exit(cmdutil.execute(args[0], args=args[1:], dir=dir)) except cmdutil.GetHelp: print cmdutil.help(args[0]) sys.exit(0) diff --git a/becommands/assign.py b/becommands/assign.py index 2c78f69..9c971ae 100644 --- a/becommands/assign.py +++ b/becommands/assign.py @@ -21,7 +21,8 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -56,7 +57,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): help() raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) bug = bd.bug_from_shortname(args[0]) if len(args) == 1: diff --git a/becommands/close.py b/becommands/close.py index a14cea8..026c605 100644 --- a/becommands/close.py +++ b/becommands/close.py @@ -21,7 +21,8 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import bugdir >>> import os @@ -44,7 +45,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) > 1: raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) bug.status = "closed" bd.save() diff --git a/becommands/comment.py b/becommands/comment.py index fbc994f..9919d1d 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -21,7 +21,8 @@ import os import sys __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import time >>> bd = bugdir.SimpleBugDir() @@ -69,7 +70,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): shortname = args[0] bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug, parent = cmdutil.bug_comment_from_id(bd, shortname) if len(args) == 1: # try to launch an editor for comment-body entry diff --git a/becommands/commit.py b/becommands/commit.py index 39d1e2e..cade355 100644 --- a/becommands/commit.py +++ b/becommands/commit.py @@ -18,7 +18,8 @@ from libbe import cmdutil, bugdir, editor, vcs import sys __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> from libbe import bug @@ -37,7 +38,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) != 1: raise cmdutil.UsageError("Please supply a commit message") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if args[0] == '-': # read summary from stdin assert options.body != "EDITOR", \ "Cannot spawn and editor when the summary is using stdin." diff --git a/becommands/depend.py b/becommands/depend.py index 6336793..c2cb2a4 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -35,7 +35,8 @@ class BrokenLink (Exception): self.blocking_bug = blocking_bug -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import utility >>> bd = bugdir.SimpleBugDir() @@ -84,7 +85,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError("Only one bug id used in tree mode.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if options.repair == True: good,fixed,broken = check_dependencies(bd, repair_broken_links=True) assert len(broken) == 0, broken diff --git a/becommands/diff.py b/becommands/diff.py index e844c10..c5c34f9 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -21,7 +21,8 @@ from libbe import cmdutil, bugdir, diff import os __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -69,7 +70,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): except ValueError, e: raise cmdutil.UsageError(e.msg) bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if bd.vcs.versioned == False: raise cmdutil.UsageError('This directory is not revision-controlled.') if options.dir == None: diff --git a/becommands/due.py b/becommands/due.py index 6f11ad4..0b8d1e9 100644 --- a/becommands/due.py +++ b/becommands/due.py @@ -19,7 +19,8 @@ __desc__ = __doc__ DUE_TAG="DUE:" -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -43,7 +44,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) not in (1, 2): raise cmdutil.UsageError('Incorrect number of arguments.') bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) if len(args) == 1: due_time = get_due(bug) diff --git a/becommands/email_bugs.py b/becommands/email_bugs.py index d0366df..f6641e3 100644 --- a/becommands/email_bugs.py +++ b/becommands/email_bugs.py @@ -33,7 +33,8 @@ __desc__ = __doc__ sendmail='/usr/sbin/sendmail -t' -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> from libbe import bug @@ -96,7 +97,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) == 0: raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) xml = show.output(args, bd, as_xml=True, with_comments=True) subject = options.subject if subject == None: diff --git a/becommands/help.py b/becommands/help.py index 99ab8c4..9e6d1aa 100644 --- a/becommands/help.py +++ b/becommands/help.py @@ -20,7 +20,8 @@ from libbe import cmdutil, utility __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ Print help of specified command (the manipulate_encodings argument is ignored). diff --git a/becommands/html.py b/becommands/html.py index a031188..d9e0d73 100644 --- a/becommands/html.py +++ b/becommands/html.py @@ -21,7 +21,8 @@ import xml.sax.saxutils, htmlentitydefs __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -50,7 +51,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError, 'Too many arguments.' bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bd.load_all_bugs() html_gen = HTMLGen(bd, template=options.template, verbose=options.verbose, diff --git a/becommands/import_xml.py b/becommands/import_xml.py index d1ea026..b985193 100644 --- a/becommands/import_xml.py +++ b/becommands/import_xml.py @@ -29,7 +29,8 @@ if libbe.TESTING == True: import unittest __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import time >>> import StringIO @@ -64,7 +65,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): filename = args[0] bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if options.comment_root != None: croot_bug,croot_comment = \ cmdutil.bug_comment_from_id(bd, options.comment_root) diff --git a/becommands/init.py b/becommands/init.py index 6085286..ab9255b 100644 --- a/becommands/init.py +++ b/becommands/init.py @@ -20,7 +20,8 @@ import os.path from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import utility, vcs >>> import os @@ -30,7 +31,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): ... except bugdir.NoBugDir, e: ... True True - >>> execute(['--root', dir.path], manipulate_encodings=False) + >>> execute([], manipulate_encodings=False, dir=dir.path) No revision control detected. Directory initialized. >>> dir.cleanup() @@ -47,11 +48,12 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): >>> _vcs.cleanup() >>> try: - ... execute(['--root', '.'], manipulate_encodings=False) + ... execute([], manipulate_encodings=False, dir=".") ... except cmdutil.UserError, e: ... str(e).startswith("Directory already initialized: ") True - >>> execute(['--root', '/highly-unlikely-to-exist'], manipulate_encodings=False) + >>> execute([], manipulate_encodings=False, + ... dir='/highly-unlikely-to-exist') Traceback (most recent call last): UserError: No such directory: /highly-unlikely-to-exist >>> os.chdir('/') @@ -63,14 +65,15 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) > 0: raise cmdutil.UsageError try: - bd = bugdir.BugDir(options.root_dir, from_disk=False, + bd = bugdir.BugDir(from_disk=False, sink_to_existing_root=False, assert_new_BugDir=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) except bugdir.NoRootEntry: - raise cmdutil.UserError("No such directory: %s" % options.root_dir) + raise cmdutil.UserError("No such directory: %s" % dir) except bugdir.AlreadyInitialized: - raise cmdutil.UserError("Directory already initialized: %s" % options.root_dir) + raise cmdutil.UserError("Directory already initialized: %s" % dir) bd.save() if bd.vcs.name is not "None": print "Using %s for revision control." % bd.vcs.name @@ -80,9 +83,6 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): def get_parser(): parser = cmdutil.CmdOptionParser("be init") - parser.add_option("-r", "--root", metavar="DIR", dest="root_dir", - help="Set root dir to something other than the current directory.", - default=".") return parser longhelp=""" @@ -90,7 +90,9 @@ This command initializes Bugs Everywhere support for the specified directory and all its subdirectories. It will auto-detect any supported revision control system. You can use "be set vcs_name" to change the vcs being used. -The directory defaults to your current working directory. +The directory defaults to your current working directory, but you can +change that by passing the --dir option to be + $ be --dir path/to/new/bug/root init It is usually a good idea to put the Bugs Everywhere root at the source code root, but you can put it anywhere. If you root Bugs Everywhere in a diff --git a/becommands/list.py b/becommands/list.py index fa5647b..1c3e78d 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -26,7 +26,8 @@ __desc__ = __doc__ AVAILABLE_CMPS = [fn[4:] for fn in dir(bug) if fn[:4] == 'cmp_'] AVAILABLE_CMPS.remove("attr") # a cmp_* template. -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -52,7 +53,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): cmp_list.append(eval('bug.cmp_%s' % cmp)) bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bd.load_all_bugs() # select status if options.status != None: diff --git a/becommands/merge.py b/becommands/merge.py index 8cf7e2f..ac09b40 100644 --- a/becommands/merge.py +++ b/becommands/merge.py @@ -19,7 +19,8 @@ from libbe import cmdutil, bugdir import os, copy __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import utility >>> bd = bugdir.SimpleBugDir() @@ -134,7 +135,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bugA = cmdutil.bug_from_id(bd, args[0]) bugA.load_comments() bugB = cmdutil.bug_from_id(bd, args[1]) diff --git a/becommands/new.py b/becommands/new.py index 00e8a47..30f8834 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -20,7 +20,8 @@ from libbe import cmdutil, bugdir import sys __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os, time >>> from libbe import bug @@ -47,7 +48,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) != 1: raise cmdutil.UsageError("Please supply a summary message") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if args[0] == '-': # read summary from stdin summary = sys.stdin.readline() else: diff --git a/becommands/open.py b/becommands/open.py index c2c15e2..a6fe48d 100644 --- a/becommands/open.py +++ b/becommands/open.py @@ -21,7 +21,8 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -43,7 +44,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) > 1: raise cmdutil.UsageError, "Too many arguments." bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) bug.status = "open" diff --git a/becommands/remove.py b/becommands/remove.py index e4f0065..bac06c0 100644 --- a/becommands/remove.py +++ b/becommands/remove.py @@ -18,7 +18,8 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import mapfile >>> import os @@ -43,7 +44,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) != 1: raise cmdutil.UsageError, "Please specify a bug id." bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) bd.remove_bug(bug) print "Removed bug %s" % bug.uuid diff --git a/becommands/set.py b/becommands/set.py index c8c5a2c..4d54a59 100644 --- a/becommands/set.py +++ b/becommands/set.py @@ -32,7 +32,8 @@ def _value_string(bd, setting): val = None return str(val) -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -53,7 +54,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) > 2: raise cmdutil.UsageError, "Too many arguments" bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if len(args) == 0: keys = bd.settings_properties keys.sort() diff --git a/becommands/severity.py b/becommands/severity.py index 524976b..804dc4e 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -21,7 +21,8 @@ from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -42,7 +43,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) not in (1,2): raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) if len(args) == 1: print bug.severity diff --git a/becommands/show.py b/becommands/show.py index ab1708f..7757aaa 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -22,7 +22,8 @@ import sys from libbe import cmdutil, bugdir, comment, version, _version __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -66,7 +67,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) == 0: raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if options.only_raw_body == True: if len(args) != 1: diff --git a/becommands/status.py b/becommands/status.py index fd31c97..58b6f63 100644 --- a/becommands/status.py +++ b/becommands/status.py @@ -18,7 +18,8 @@ from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -39,7 +40,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) not in (1,2): raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) if len(args) == 1: print bug.status diff --git a/becommands/subscribe.py b/becommands/subscribe.py index 19aac53..69554f7 100644 --- a/becommands/subscribe.py +++ b/becommands/subscribe.py @@ -20,7 +20,8 @@ __desc__ = __doc__ TAG="SUBSCRIBE:" -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> bd = bugdir.SimpleBugDir() >>> bd.set_sync_with_disk(True) @@ -73,7 +74,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) subscriber = options.subscriber if subscriber == None: diff --git a/becommands/tag.py b/becommands/tag.py index e22cb70..f3819bd 100644 --- a/becommands/tag.py +++ b/becommands/tag.py @@ -19,7 +19,8 @@ from libbe import cmdutil, bugdir import os, copy __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import utility >>> bd = bugdir.SimpleBugDir() @@ -81,7 +82,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if options.list: bd.load_all_bugs() tags = [] diff --git a/becommands/target.py b/becommands/target.py index 9ccbacc..5dd5d38 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -23,7 +23,8 @@ from libbe import cmdutil, bugdir from becommands import depend __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os, StringIO, sys >>> bd = bugdir.SimpleBugDir() @@ -60,7 +61,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): or (options.resolve == True and len(args) not in (0, 1)): raise cmdutil.UsageError('Incorrect number of arguments.') bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if options.resolve == True: if len(args) == 0: summary = None diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index b892bde..c567984 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -79,12 +79,15 @@ def get_command(command_name): return cmd -def execute(cmd, args, manipulate_encodings=True, restrict_file_access=False): +def execute(cmd, args, + manipulate_encodings=True, restrict_file_access=False, + dir="."): enc = encoding.get_encoding() cmd = get_command(cmd) ret = cmd.execute([a.decode(enc) for a in args], manipulate_encodings=manipulate_encodings, - restrict_file_access=restrict_file_access) + restrict_file_access=restrict_file_access, + dir=dir) if ret == None: ret = 0 return ret -- cgit From adfd866bf1fafc3832e03506a09c9b5750fe7447 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 7 Dec 2009 07:00:34 -0500 Subject: Added libbe.pager --- libbe/pager.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 libbe/pager.py diff --git a/libbe/pager.py b/libbe/pager.py new file mode 100644 index 0000000..8190b2a --- /dev/null +++ b/libbe/pager.py @@ -0,0 +1,51 @@ +# Copyright + +""" +Automatic pager for terminal output (a la Git). +""" + +import sys, os, select + +# see http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby +def run_pager(paginate=None): + """ + paginate should be one of 'never', 'auto', or 'always'. + + usage: just call this function and continue using sys.stdout like + you normally would. + """ + if paginate == 'never' \ + or sys.platform == 'win32' \ + or not hasattr(sys.stdout, 'isatty') \ + or sys.stdout.isatty() == False: + return + + if paginate == 'auto': + if 'LESS' not in os.environ: + os.environ['LESS'] = '' # += doesn't work on undefined var + # don't page if the input is short enough + os.environ['LESS'] += ' -FRX' + if 'PAGER' in os.environ: + pager = os.environ['PAGER'] + else: + pager = 'less' + + read_fd, write_fd = os.pipe() + if os.fork() == 0: + # child process + os.close(read_fd) + os.close(0) + os.dup2(write_fd, 1) + os.close(write_fd) + if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() == True: + os.dup2(1, 2) + return + + # parent process, become pager + os.close(write_fd) + os.dup2(read_fd, 0) + os.close(read_fd) + + # Wait until we have input before we start the pager + select.select([0], [], []) + os.execlp(pager, pager) -- cgit From 4e3ad6eacd83ebeac3156bc4944b8943247ffb2a Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 7 Dec 2009 07:02:15 -0500 Subject: Update libbe.pager copyright --- libbe/pager.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/libbe/pager.py b/libbe/pager.py index 8190b2a..3fcae84 100644 --- a/libbe/pager.py +++ b/libbe/pager.py @@ -1,4 +1,18 @@ -# Copyright +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ Automatic pager for terminal output (a la Git). -- cgit From fdf9925ffaada614544d1b2d3ccecb42f1549acb Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 7 Dec 2009 07:18:48 -0500 Subject: be --dir DIR COMMAND now roots the bugdir in DIR without changing directories. Previously, for the directory structure A |-- X `-- Y You could do something like A$ be --dir X diff --dir ../Y Now it's A$ be --dir X diff --dir Y The --root option to `be init` has been removed as redundant. Replace calls like be init --root DIR with be --dir DIR init --- .../79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body | 13 +++++++++++ .../79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values | 11 +++++++++ NEWS | 5 +++++ README.dev | 5 ++++- be | 8 +++---- becommands/assign.py | 6 +++-- becommands/close.py | 6 +++-- becommands/comment.py | 6 +++-- becommands/commit.py | 6 +++-- becommands/depend.py | 6 +++-- becommands/diff.py | 6 +++-- becommands/due.py | 6 +++-- becommands/email_bugs.py | 6 +++-- becommands/help.py | 3 ++- becommands/html.py | 6 +++-- becommands/import_xml.py | 6 +++-- becommands/init.py | 26 ++++++++++++---------- becommands/list.py | 6 +++-- becommands/merge.py | 6 +++-- becommands/new.py | 6 +++-- becommands/open.py | 6 +++-- becommands/remove.py | 6 +++-- becommands/set.py | 6 +++-- becommands/severity.py | 6 +++-- becommands/show.py | 6 +++-- becommands/status.py | 6 +++-- becommands/subscribe.py | 6 +++-- becommands/tag.py | 6 +++-- becommands/target.py | 6 +++-- libbe/cmdutil.py | 7 ++++-- 30 files changed, 145 insertions(+), 65 deletions(-) create mode 100644 .be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body create mode 100644 .be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values diff --git a/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body b/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body new file mode 100644 index 0000000..7dbeebb --- /dev/null +++ b/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/body @@ -0,0 +1,13 @@ +> * Determining what to commit. +> +> You'd have to have RCS keep a log of all versioned files it +> touched, and extend .commit() to accept the keyword list "files" +> and commit only those files. This is doable, but maybe not worth +> the trouble. + +On the other hand, just attemting to commit evverything after each +command would make it nice and easy to commit bug fixes: + be --auto-commit status XYZ fixed +which would commit whatever changes you had outstanding with an +appropriate commit message. + diff --git a/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values b/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values new file mode 100644 index 0000000..b3dba3f --- /dev/null +++ b/.be/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe/values @@ -0,0 +1,11 @@ +Author: W. Trevor King + + +Content-type: text/plain + + +Date: Sun, 06 Dec 2009 21:45:15 +0000 + + +In-reply-to: 4c50ca0b-a08f-4723-b00d-4bf342cf86b6 + diff --git a/NEWS b/NEWS index 0cf9aae..84256e1 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,8 @@ +December 6, 2009 + * be --dir DIR COMMAND now roots the bugdir in DIR _without_ changing + directories. + * `be init --root DIR` should now be `be --dir DIR init`. + December 5, 2009 * targets are now a special type of bug (severity 'target'), so you can do all the things you do with normal bugs to them as well diff --git a/README.dev b/README.dev index fb4f471..dbb97b0 100644 --- a/README.dev +++ b/README.dev @@ -10,7 +10,8 @@ To fit into the current framework, your extension module should provide the following elements: __desc__ A short string describing the purpose of your plugin - execute(args, manipulate_encodings=True, restrict_file_access=False) + execute(args, manipulate_encodings=True, restrict_file_access=False, + dir=".") The entry function for your plugin. args is everything from sys.argv after the name of your plugin (e.g. for the command `be open abc', args=['abc']). @@ -23,6 +24,8 @@ provide the following elements: before attempting to read or write a file. See the restrict_file_access documentation for details. + dir is a directory inside the repository of interest. + Note: be supports command-completion. To avoid raising errors you need to deal with possible '--complete' options and arguments. See the 'Command completion' section below for more information. diff --git a/be b/be index c5c372f..8a594cf 100755 --- a/be +++ b/be @@ -34,8 +34,8 @@ parser.add_option("--version", action="store_true", dest="version", help="Print version string and exit.") parser.add_option("--verbose-version", action="store_true", dest="verbose_version", help="Print verbose version information and exit.") -parser.add_option("-d", "--dir", dest="dir", metavar="DIR", - help="Run this command from DIR instead of the current directory.") +parser.add_option("-d", "--dir", dest="dir", metavar="DIR", default=".", + help="Run this command on the repository in DIR instead of the current directory.") try: options,args = parser.parse_args() @@ -56,13 +56,11 @@ except cmdutil.GetCompletions, e: if options.version == True or options.verbose_version == True: print version.version(verbose=options.verbose_version) sys.exit(0) -if options.dir != None: - os.chdir(options.dir) try: if len(args) == 0: raise cmdutil.UsageError, "must supply a command" - sys.exit(cmdutil.execute(args[0], args[1:])) + sys.exit(cmdutil.execute(args[0], args=args[1:], dir=options.dir)) except cmdutil.GetHelp: print cmdutil.help(args[0]) sys.exit(0) diff --git a/becommands/assign.py b/becommands/assign.py index 2c78f69..9c971ae 100644 --- a/becommands/assign.py +++ b/becommands/assign.py @@ -21,7 +21,8 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -56,7 +57,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): help() raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) bug = bd.bug_from_shortname(args[0]) if len(args) == 1: diff --git a/becommands/close.py b/becommands/close.py index a14cea8..026c605 100644 --- a/becommands/close.py +++ b/becommands/close.py @@ -21,7 +21,8 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import bugdir >>> import os @@ -44,7 +45,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) > 1: raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) bug.status = "closed" bd.save() diff --git a/becommands/comment.py b/becommands/comment.py index fbc994f..9919d1d 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -21,7 +21,8 @@ import os import sys __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import time >>> bd = bugdir.SimpleBugDir() @@ -69,7 +70,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): shortname = args[0] bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug, parent = cmdutil.bug_comment_from_id(bd, shortname) if len(args) == 1: # try to launch an editor for comment-body entry diff --git a/becommands/commit.py b/becommands/commit.py index 39d1e2e..cade355 100644 --- a/becommands/commit.py +++ b/becommands/commit.py @@ -18,7 +18,8 @@ from libbe import cmdutil, bugdir, editor, vcs import sys __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> from libbe import bug @@ -37,7 +38,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) != 1: raise cmdutil.UsageError("Please supply a commit message") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if args[0] == '-': # read summary from stdin assert options.body != "EDITOR", \ "Cannot spawn and editor when the summary is using stdin." diff --git a/becommands/depend.py b/becommands/depend.py index 6336793..c2cb2a4 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -35,7 +35,8 @@ class BrokenLink (Exception): self.blocking_bug = blocking_bug -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import utility >>> bd = bugdir.SimpleBugDir() @@ -84,7 +85,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError("Only one bug id used in tree mode.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if options.repair == True: good,fixed,broken = check_dependencies(bd, repair_broken_links=True) assert len(broken) == 0, broken diff --git a/becommands/diff.py b/becommands/diff.py index e844c10..c5c34f9 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -21,7 +21,8 @@ from libbe import cmdutil, bugdir, diff import os __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -69,7 +70,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): except ValueError, e: raise cmdutil.UsageError(e.msg) bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if bd.vcs.versioned == False: raise cmdutil.UsageError('This directory is not revision-controlled.') if options.dir == None: diff --git a/becommands/due.py b/becommands/due.py index 6f11ad4..0b8d1e9 100644 --- a/becommands/due.py +++ b/becommands/due.py @@ -19,7 +19,8 @@ __desc__ = __doc__ DUE_TAG="DUE:" -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -43,7 +44,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) not in (1, 2): raise cmdutil.UsageError('Incorrect number of arguments.') bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) if len(args) == 1: due_time = get_due(bug) diff --git a/becommands/email_bugs.py b/becommands/email_bugs.py index d0366df..f6641e3 100644 --- a/becommands/email_bugs.py +++ b/becommands/email_bugs.py @@ -33,7 +33,8 @@ __desc__ = __doc__ sendmail='/usr/sbin/sendmail -t' -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> from libbe import bug @@ -96,7 +97,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) == 0: raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) xml = show.output(args, bd, as_xml=True, with_comments=True) subject = options.subject if subject == None: diff --git a/becommands/help.py b/becommands/help.py index 99ab8c4..9e6d1aa 100644 --- a/becommands/help.py +++ b/becommands/help.py @@ -20,7 +20,8 @@ from libbe import cmdutil, utility __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ Print help of specified command (the manipulate_encodings argument is ignored). diff --git a/becommands/html.py b/becommands/html.py index a031188..d9e0d73 100644 --- a/becommands/html.py +++ b/becommands/html.py @@ -21,7 +21,8 @@ import xml.sax.saxutils, htmlentitydefs __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -50,7 +51,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError, 'Too many arguments.' bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bd.load_all_bugs() html_gen = HTMLGen(bd, template=options.template, verbose=options.verbose, diff --git a/becommands/import_xml.py b/becommands/import_xml.py index d1ea026..b985193 100644 --- a/becommands/import_xml.py +++ b/becommands/import_xml.py @@ -29,7 +29,8 @@ if libbe.TESTING == True: import unittest __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import time >>> import StringIO @@ -64,7 +65,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): filename = args[0] bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if options.comment_root != None: croot_bug,croot_comment = \ cmdutil.bug_comment_from_id(bd, options.comment_root) diff --git a/becommands/init.py b/becommands/init.py index 6085286..ab9255b 100644 --- a/becommands/init.py +++ b/becommands/init.py @@ -20,7 +20,8 @@ import os.path from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import utility, vcs >>> import os @@ -30,7 +31,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): ... except bugdir.NoBugDir, e: ... True True - >>> execute(['--root', dir.path], manipulate_encodings=False) + >>> execute([], manipulate_encodings=False, dir=dir.path) No revision control detected. Directory initialized. >>> dir.cleanup() @@ -47,11 +48,12 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): >>> _vcs.cleanup() >>> try: - ... execute(['--root', '.'], manipulate_encodings=False) + ... execute([], manipulate_encodings=False, dir=".") ... except cmdutil.UserError, e: ... str(e).startswith("Directory already initialized: ") True - >>> execute(['--root', '/highly-unlikely-to-exist'], manipulate_encodings=False) + >>> execute([], manipulate_encodings=False, + ... dir='/highly-unlikely-to-exist') Traceback (most recent call last): UserError: No such directory: /highly-unlikely-to-exist >>> os.chdir('/') @@ -63,14 +65,15 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) > 0: raise cmdutil.UsageError try: - bd = bugdir.BugDir(options.root_dir, from_disk=False, + bd = bugdir.BugDir(from_disk=False, sink_to_existing_root=False, assert_new_BugDir=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) except bugdir.NoRootEntry: - raise cmdutil.UserError("No such directory: %s" % options.root_dir) + raise cmdutil.UserError("No such directory: %s" % dir) except bugdir.AlreadyInitialized: - raise cmdutil.UserError("Directory already initialized: %s" % options.root_dir) + raise cmdutil.UserError("Directory already initialized: %s" % dir) bd.save() if bd.vcs.name is not "None": print "Using %s for revision control." % bd.vcs.name @@ -80,9 +83,6 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): def get_parser(): parser = cmdutil.CmdOptionParser("be init") - parser.add_option("-r", "--root", metavar="DIR", dest="root_dir", - help="Set root dir to something other than the current directory.", - default=".") return parser longhelp=""" @@ -90,7 +90,9 @@ This command initializes Bugs Everywhere support for the specified directory and all its subdirectories. It will auto-detect any supported revision control system. You can use "be set vcs_name" to change the vcs being used. -The directory defaults to your current working directory. +The directory defaults to your current working directory, but you can +change that by passing the --dir option to be + $ be --dir path/to/new/bug/root init It is usually a good idea to put the Bugs Everywhere root at the source code root, but you can put it anywhere. If you root Bugs Everywhere in a diff --git a/becommands/list.py b/becommands/list.py index fa5647b..1c3e78d 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -26,7 +26,8 @@ __desc__ = __doc__ AVAILABLE_CMPS = [fn[4:] for fn in dir(bug) if fn[:4] == 'cmp_'] AVAILABLE_CMPS.remove("attr") # a cmp_* template. -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -52,7 +53,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): cmp_list.append(eval('bug.cmp_%s' % cmp)) bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bd.load_all_bugs() # select status if options.status != None: diff --git a/becommands/merge.py b/becommands/merge.py index 8cf7e2f..ac09b40 100644 --- a/becommands/merge.py +++ b/becommands/merge.py @@ -19,7 +19,8 @@ from libbe import cmdutil, bugdir import os, copy __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import utility >>> bd = bugdir.SimpleBugDir() @@ -134,7 +135,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bugA = cmdutil.bug_from_id(bd, args[0]) bugA.load_comments() bugB = cmdutil.bug_from_id(bd, args[1]) diff --git a/becommands/new.py b/becommands/new.py index 00e8a47..30f8834 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -20,7 +20,8 @@ from libbe import cmdutil, bugdir import sys __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os, time >>> from libbe import bug @@ -47,7 +48,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) != 1: raise cmdutil.UsageError("Please supply a summary message") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if args[0] == '-': # read summary from stdin summary = sys.stdin.readline() else: diff --git a/becommands/open.py b/becommands/open.py index c2c15e2..a6fe48d 100644 --- a/becommands/open.py +++ b/becommands/open.py @@ -21,7 +21,8 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -43,7 +44,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) > 1: raise cmdutil.UsageError, "Too many arguments." bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) bug.status = "open" diff --git a/becommands/remove.py b/becommands/remove.py index e4f0065..bac06c0 100644 --- a/becommands/remove.py +++ b/becommands/remove.py @@ -18,7 +18,8 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import mapfile >>> import os @@ -43,7 +44,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) != 1: raise cmdutil.UsageError, "Please specify a bug id." bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) bd.remove_bug(bug) print "Removed bug %s" % bug.uuid diff --git a/becommands/set.py b/becommands/set.py index c8c5a2c..4d54a59 100644 --- a/becommands/set.py +++ b/becommands/set.py @@ -32,7 +32,8 @@ def _value_string(bd, setting): val = None return str(val) -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -53,7 +54,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) > 2: raise cmdutil.UsageError, "Too many arguments" bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if len(args) == 0: keys = bd.settings_properties keys.sort() diff --git a/becommands/severity.py b/becommands/severity.py index 524976b..804dc4e 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -21,7 +21,8 @@ from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -42,7 +43,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) not in (1,2): raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) if len(args) == 1: print bug.severity diff --git a/becommands/show.py b/becommands/show.py index ab1708f..7757aaa 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -22,7 +22,8 @@ import sys from libbe import cmdutil, bugdir, comment, version, _version __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -66,7 +67,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) == 0: raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if options.only_raw_body == True: if len(args) != 1: diff --git a/becommands/status.py b/becommands/status.py index fd31c97..58b6f63 100644 --- a/becommands/status.py +++ b/becommands/status.py @@ -18,7 +18,8 @@ from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os >>> bd = bugdir.SimpleBugDir() @@ -39,7 +40,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) not in (1,2): raise cmdutil.UsageError bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) bug = cmdutil.bug_from_id(bd, args[0]) if len(args) == 1: print bug.status diff --git a/becommands/subscribe.py b/becommands/subscribe.py index 19aac53..69554f7 100644 --- a/becommands/subscribe.py +++ b/becommands/subscribe.py @@ -20,7 +20,8 @@ __desc__ = __doc__ TAG="SUBSCRIBE:" -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> bd = bugdir.SimpleBugDir() >>> bd.set_sync_with_disk(True) @@ -73,7 +74,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) subscriber = options.subscriber if subscriber == None: diff --git a/becommands/tag.py b/becommands/tag.py index e22cb70..f3819bd 100644 --- a/becommands/tag.py +++ b/becommands/tag.py @@ -19,7 +19,8 @@ from libbe import cmdutil, bugdir import os, copy __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> from libbe import utility >>> bd = bugdir.SimpleBugDir() @@ -81,7 +82,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): raise cmdutil.UsageError("Too many arguments.") bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if options.list: bd.load_all_bugs() tags = [] diff --git a/becommands/target.py b/becommands/target.py index 9ccbacc..5dd5d38 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -23,7 +23,8 @@ from libbe import cmdutil, bugdir from becommands import depend __desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False): +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): """ >>> import os, StringIO, sys >>> bd = bugdir.SimpleBugDir() @@ -60,7 +61,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): or (options.resolve == True and len(args) not in (0, 1)): raise cmdutil.UsageError('Incorrect number of arguments.') bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings) + manipulate_encodings=manipulate_encodings, + root=dir) if options.resolve == True: if len(args) == 0: summary = None diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index b892bde..c567984 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -79,12 +79,15 @@ def get_command(command_name): return cmd -def execute(cmd, args, manipulate_encodings=True, restrict_file_access=False): +def execute(cmd, args, + manipulate_encodings=True, restrict_file_access=False, + dir="."): enc = encoding.get_encoding() cmd = get_command(cmd) ret = cmd.execute([a.decode(enc) for a in args], manipulate_encodings=manipulate_encodings, - restrict_file_access=restrict_file_access) + restrict_file_access=restrict_file_access, + dir=dir) if ret == None: ret = 0 return ret -- cgit From 0784330491a640f4e6016342b0bc2958d36b0b40 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 7 Dec 2009 07:20:23 -0500 Subject: Use 'auto' for run_pager default rather than None --- libbe/pager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libbe/pager.py b/libbe/pager.py index 3fcae84..1ddc3fa 100644 --- a/libbe/pager.py +++ b/libbe/pager.py @@ -21,7 +21,7 @@ Automatic pager for terminal output (a la Git). import sys, os, select # see http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby -def run_pager(paginate=None): +def run_pager(paginate='auto'): """ paginate should be one of 'never', 'auto', or 'always'. -- cgit From fada10afd00989bef0468373ae435234224390c1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 7 Dec 2009 07:25:18 -0500 Subject: Added --paginate and --no-pager to be --- be | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/be b/be index a4a3ea4..f026c05 100755 --- a/be +++ b/be @@ -22,7 +22,7 @@ import os import sys -from libbe import cmdutil, version +from libbe import cmdutil, version, pager __doc__ = cmdutil.help() @@ -36,6 +36,13 @@ parser.add_option("--verbose-version", action="store_true", dest="verbose_versio help="Print verbose version information and exit.") parser.add_option("-d", "--dir", dest="dir", metavar="DIR", default=".", help="Run this command on the repository in DIR instead of the current directory.") +parser.add_option("-p", "--paginate", dest="paginate", default=False, + action='store_true', + help="Pipe all output into less (or if set, $PAGER).") +parser.add_option("--no-pager", dest="no_pager", default=False, + action='store_true', + help="Do not pipe git output into a pager.") + try: options,args = parser.parse_args() @@ -57,10 +64,17 @@ if options.version == True or options.verbose_version == True: print version.version(verbose=options.verbose_version) sys.exit(0) +paginate = 'auto' +if options.paginate == True: + paginate = 'always' +if options.no_pager== True: + paginate = 'never' +pager.run_pager(paginate) + try: if len(args) == 0: raise cmdutil.UsageError, "must supply a command" - sys.exit(cmdutil.execute(args[0], args=args[1:], dir=dir)) + sys.exit(cmdutil.execute(args[0], args=args[1:], dir=options.dir)) except cmdutil.GetHelp: print cmdutil.help(args[0]) sys.exit(0) -- cgit From a2562bc912e33fb3748be9d01771c9ae0ed6010f Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 7 Dec 2009 07:34:14 -0500 Subject: Updated NEWS --- NEWS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 84256e1..53804e6 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,5 @@ -December 6, 2009 +December 7, 2009 + * added --paginate and --no-pager to be. * be --dir DIR COMMAND now roots the bugdir in DIR _without_ changing directories. * `be init --root DIR` should now be `be --dir DIR init`. -- cgit From a06030436d3940dddfba37b344f90651366d67e1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 7 Dec 2009 19:51:21 -0500 Subject: Don't run pager for commands that may need a tty (e.g. for editor) --- be | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/be b/be index f026c05..18692a4 100755 --- a/be +++ b/be @@ -64,12 +64,13 @@ if options.version == True or options.verbose_version == True: print version.version(verbose=options.verbose_version) sys.exit(0) -paginate = 'auto' -if options.paginate == True: - paginate = 'always' -if options.no_pager== True: - paginate = 'never' -pager.run_pager(paginate) +if len(args) > 0 and args[0] not in ['comment', 'commit']: + paginate = 'auto' + if options.paginate == True: + paginate = 'always' + if options.no_pager== True: + paginate = 'never' + pager.run_pager(paginate) try: if len(args) == 0: -- cgit From c3bcafe12034d35f5c46f76a7dab97ab08b84dfd Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 7 Dec 2009 20:07:36 -0500 Subject: Reorganization bug created --- .../bb406a33-92b6-46dd-950c-c7cfb5440e7b/body | 30 ++++++++++++++++++++++ .../bb406a33-92b6-46dd-950c-c7cfb5440e7b/values | 8 ++++++ .../1100c966-9671-4bc6-8b68-6d408a910da1/values | 17 ++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 .be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/body create mode 100644 .be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/values create mode 100644 .be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/values diff --git a/.be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/body b/.be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/body new file mode 100644 index 0000000..abb898c --- /dev/null +++ b/.be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/body @@ -0,0 +1,30 @@ +Rather than all the hackery that goes on with email-bugs, the email +interface, etc., it would be nice for distribution if be provided a +uniform issue/bug tracking library and a number of interfaces and +backends. + +Current backends: + filesystem (with assorted VCSs) +Current UIs: + command line (be) + email (be-handle-mail) + web (CFBE) + +Future backend architecture: + be --repo REPO ... +where --repo REPO replaces and extends the current --dir DIR. Example +REPOs could be + path/to/repo (the current DIR) + http://some-server.com:port/path/to/repo (http interface) + mysql://user@server:port/?db=db-name;pwd=password + ... +Each repo would have to support a few get/set commands at the bugdir, +bug, and comment level. + +The UIs would all load BugDir(REPO), and thus be backend agnostic. +This way a GUI app that let you work on your own machine could also be +used to work on a public repository. Setting up a public repository +would just consist of exposing one of the wire-capable REPO formats +(e.g. http via a future `be serve MY-URL`) with public write +permissions. + diff --git a/.be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/values b/.be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/values new file mode 100644 index 0000000..d2e65d3 --- /dev/null +++ b/.be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b/values @@ -0,0 +1,8 @@ +Author: W. Trevor King + + +Content-type: text/plain + + +Date: Tue, 08 Dec 2009 01:06:12 +0000 + diff --git a/.be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/values b/.be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/values new file mode 100644 index 0000000..851aedc --- /dev/null +++ b/.be/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/values @@ -0,0 +1,17 @@ +creator: W. Trevor King + + +reporter: W. Trevor King + + +severity: minor + + +status: open + + +summary: Reoranize BE for more flexible backend / frontend + + +time: Tue, 08 Dec 2009 00:48:27 +0000 + -- cgit From 49a7771336ce09f6d42c7699ef32aecea0e83182 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 7 Dec 2009 20:07:55 -0500 Subject: Initial directory restructuring to clarify dependencies --- becommands/__init__.py | 16 - becommands/assign.py | 90 ---- becommands/close.py | 63 --- becommands/comment.py | 164 ------- becommands/commit.py | 82 ---- becommands/depend.py | 371 --------------- becommands/diff.py | 133 ------ becommands/due.py | 108 ----- becommands/email_bugs.py | 239 ---------- becommands/help.py | 70 --- becommands/html.py | 609 ------------------------ becommands/import_xml.py | 434 ------------------ becommands/init.py | 104 ----- becommands/list.py | 240 ---------- becommands/merge.py | 166 ------- becommands/new.py | 83 ---- becommands/open.py | 61 --- becommands/remove.py | 65 --- becommands/set.py | 132 ------ becommands/severity.py | 106 ----- becommands/show.py | 183 -------- becommands/status.py | 118 ----- becommands/subscribe.py | 360 --------------- becommands/tag.py | 137 ------ becommands/target.py | 168 ------- libbe/arch.py | 315 ------------- libbe/beuuid.py | 67 --- libbe/bzr.py | 117 ----- libbe/cmdutil.py | 356 -------------- libbe/command/__init__.py | 16 + libbe/command/assign.py | 90 ++++ libbe/command/close.py | 63 +++ libbe/command/comment.py | 164 +++++++ libbe/command/commit.py | 82 ++++ libbe/command/depend.py | 371 +++++++++++++++ libbe/command/diff.py | 133 ++++++ libbe/command/due.py | 108 +++++ libbe/command/email_bugs.py | 239 ++++++++++ libbe/command/help.py | 70 +++ libbe/command/html.py | 609 ++++++++++++++++++++++++ libbe/command/import_xml.py | 434 ++++++++++++++++++ libbe/command/init.py | 104 +++++ libbe/command/list.py | 240 ++++++++++ libbe/command/merge.py | 166 +++++++ libbe/command/new.py | 83 ++++ libbe/command/open.py | 61 +++ libbe/command/remove.py | 65 +++ libbe/command/set.py | 132 ++++++ libbe/command/severity.py | 106 +++++ libbe/command/show.py | 183 ++++++++ libbe/command/status.py | 118 +++++ libbe/command/subscribe.py | 360 +++++++++++++++ libbe/command/tag.py | 137 ++++++ libbe/command/target.py | 168 +++++++ libbe/config.py | 94 ---- libbe/darcs.py | 192 -------- libbe/editor.py | 113 ----- libbe/encoding.py | 66 --- libbe/git.py | 151 ------ libbe/hg.py | 108 ----- libbe/mapfile.py | 126 ----- libbe/pager.py | 65 --- libbe/properties.py | 642 -------------------------- libbe/settings_object.py | 433 ------------------ libbe/storage/properties.py | 642 ++++++++++++++++++++++++++ libbe/storage/settings_object.py | 433 ++++++++++++++++++ libbe/storage/vcs/arch.py | 315 +++++++++++++ libbe/storage/vcs/base.py | 941 ++++++++++++++++++++++++++++++++++++++ libbe/storage/vcs/bzr.py | 117 +++++ libbe/storage/vcs/darcs.py | 192 ++++++++ libbe/storage/vcs/git.py | 151 ++++++ libbe/storage/vcs/hg.py | 108 +++++ libbe/storage/vcs/util/config.py | 94 ++++ libbe/storage/vcs/util/mapfile.py | 126 +++++ libbe/storage/vcs/util/upgrade.py | 246 ++++++++++ libbe/subproc.py | 223 --------- libbe/tree.py | 193 -------- libbe/ui/util/cmdutil.py | 356 ++++++++++++++ libbe/ui/util/editor.py | 113 +++++ libbe/ui/util/pager.py | 65 +++ libbe/upgrade.py | 246 ---------- libbe/util/beuuid.py | 67 +++ libbe/util/encoding.py | 66 +++ libbe/util/subproc.py | 223 +++++++++ libbe/util/tree.py | 193 ++++++++ libbe/util/utility.py | 151 ++++++ libbe/utility.py | 151 ------ libbe/vcs.py | 941 -------------------------------------- 88 files changed, 8901 insertions(+), 8901 deletions(-) delete mode 100644 becommands/__init__.py delete mode 100644 becommands/assign.py delete mode 100644 becommands/close.py delete mode 100644 becommands/comment.py delete mode 100644 becommands/commit.py delete mode 100644 becommands/depend.py delete mode 100644 becommands/diff.py delete mode 100644 becommands/due.py delete mode 100644 becommands/email_bugs.py delete mode 100644 becommands/help.py delete mode 100644 becommands/html.py delete mode 100644 becommands/import_xml.py delete mode 100644 becommands/init.py delete mode 100644 becommands/list.py delete mode 100644 becommands/merge.py delete mode 100644 becommands/new.py delete mode 100644 becommands/open.py delete mode 100644 becommands/remove.py delete mode 100644 becommands/set.py delete mode 100644 becommands/severity.py delete mode 100644 becommands/show.py delete mode 100644 becommands/status.py delete mode 100644 becommands/subscribe.py delete mode 100644 becommands/tag.py delete mode 100644 becommands/target.py delete mode 100644 libbe/arch.py delete mode 100644 libbe/beuuid.py delete mode 100644 libbe/bzr.py delete mode 100644 libbe/cmdutil.py create mode 100644 libbe/command/__init__.py create mode 100644 libbe/command/assign.py create mode 100644 libbe/command/close.py create mode 100644 libbe/command/comment.py create mode 100644 libbe/command/commit.py create mode 100644 libbe/command/depend.py create mode 100644 libbe/command/diff.py create mode 100644 libbe/command/due.py create mode 100644 libbe/command/email_bugs.py create mode 100644 libbe/command/help.py create mode 100644 libbe/command/html.py create mode 100644 libbe/command/import_xml.py create mode 100644 libbe/command/init.py create mode 100644 libbe/command/list.py create mode 100644 libbe/command/merge.py create mode 100644 libbe/command/new.py create mode 100644 libbe/command/open.py create mode 100644 libbe/command/remove.py create mode 100644 libbe/command/set.py create mode 100644 libbe/command/severity.py create mode 100644 libbe/command/show.py create mode 100644 libbe/command/status.py create mode 100644 libbe/command/subscribe.py create mode 100644 libbe/command/tag.py create mode 100644 libbe/command/target.py delete mode 100644 libbe/config.py delete mode 100644 libbe/darcs.py delete mode 100644 libbe/editor.py delete mode 100644 libbe/encoding.py delete mode 100644 libbe/git.py delete mode 100644 libbe/hg.py delete mode 100644 libbe/mapfile.py delete mode 100644 libbe/pager.py delete mode 100644 libbe/properties.py delete mode 100644 libbe/settings_object.py create mode 100644 libbe/storage/properties.py create mode 100644 libbe/storage/settings_object.py create mode 100644 libbe/storage/vcs/arch.py create mode 100644 libbe/storage/vcs/base.py create mode 100644 libbe/storage/vcs/bzr.py create mode 100644 libbe/storage/vcs/darcs.py create mode 100644 libbe/storage/vcs/git.py create mode 100644 libbe/storage/vcs/hg.py create mode 100644 libbe/storage/vcs/util/config.py create mode 100644 libbe/storage/vcs/util/mapfile.py create mode 100644 libbe/storage/vcs/util/upgrade.py delete mode 100644 libbe/subproc.py delete mode 100644 libbe/tree.py create mode 100644 libbe/ui/util/cmdutil.py create mode 100644 libbe/ui/util/editor.py create mode 100644 libbe/ui/util/pager.py delete mode 100644 libbe/upgrade.py create mode 100644 libbe/util/beuuid.py create mode 100644 libbe/util/encoding.py create mode 100644 libbe/util/subproc.py create mode 100644 libbe/util/tree.py create mode 100644 libbe/util/utility.py delete mode 100644 libbe/utility.py delete mode 100644 libbe/vcs.py diff --git a/becommands/__init__.py b/becommands/__init__.py deleted file mode 100644 index 794013c..0000000 --- a/becommands/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. diff --git a/becommands/assign.py b/becommands/assign.py deleted file mode 100644 index 9c971ae..0000000 --- a/becommands/assign.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# Marien Zwart -# Thomas Gerigk -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Assign an individual or group to fix a bug""" -from libbe import cmdutil, bugdir -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> bd.bug_from_shortname("a").assigned is None - True - - >>> execute(["a"], manipulate_encodings=False) - >>> bd._clear_bugs() - >>> bd.bug_from_shortname("a").assigned == bd.user_id - True - - >>> execute(["a", "someone"], manipulate_encodings=False) - >>> bd._clear_bugs() - >>> print bd.bug_from_shortname("a").assigned - someone - - >>> execute(["a","none"], manipulate_encodings=False) - >>> bd._clear_bugs() - >>> bd.bug_from_shortname("a").assigned is None - True - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) - assert(len(args) in (0, 1, 2)) - if len(args) == 0: - raise cmdutil.UsageError("Please specify a bug id.") - if len(args) > 2: - help() - raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bug = cmdutil.bug_from_id(bd, args[0]) - bug = bd.bug_from_shortname(args[0]) - if len(args) == 1: - bug.assigned = bd.user_id - elif len(args) == 2: - if args[1] == "none": - bug.assigned = None - else: - bug.assigned = args[1] - bd.save() - -def get_parser(): - parser = cmdutil.CmdOptionParser("be assign BUG-ID [ASSIGNEE]") - return parser - -longhelp = """ -Assign a person to fix a bug. - -By default, the bug is self-assigned. If an assignee is specified, the bug -will be assigned to that person. - -Assignees should be the person's Bugs Everywhere identity, the string that -appears in Creator fields. - -To un-assign a bug, specify "none" for the assignee. -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/close.py b/becommands/close.py deleted file mode 100644 index 026c605..0000000 --- a/becommands/close.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# Marien Zwart -# Thomas Gerigk -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Close a bug""" -from libbe import cmdutil, bugdir -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> from libbe import bugdir - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> print bd.bug_from_shortname("a").status - open - >>> execute(["a"], manipulate_encodings=False) - >>> bd._clear_bugs() - >>> print bd.bug_from_shortname("a").status - closed - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) - if len(args) == 0: - raise cmdutil.UsageError("Please specify a bug id.") - if len(args) > 1: - raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bug = cmdutil.bug_from_id(bd, args[0]) - bug.status = "closed" - bd.save() - -def get_parser(): - parser = cmdutil.CmdOptionParser("be close BUG-ID") - return parser - -longhelp=""" -Close the bug identified by BUG-ID. -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/comment.py b/becommands/comment.py deleted file mode 100644 index 9919d1d..0000000 --- a/becommands/comment.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Add a comment to a bug""" -from libbe import cmdutil, bugdir, comment, editor -import os -import sys -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import time - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute(["a", "This is a comment about a"], manipulate_encodings=False) - >>> bd._clear_bugs() - >>> bug = cmdutil.bug_from_id(bd, "a") - >>> bug.load_comments(load_full=False) - >>> comment = bug.comment_root[0] - >>> print comment.body - This is a comment about a - - >>> comment.author == bd.user_id - True - >>> comment.time <= int(time.time()) - True - >>> comment.in_reply_to is None - True - - >>> if 'EDITOR' in os.environ: - ... del os.environ["EDITOR"] - >>> execute(["b"], manipulate_encodings=False) - Traceback (most recent call last): - UserError: No comment supplied, and EDITOR not specified. - - >>> os.environ["EDITOR"] = "echo 'I like cheese' > " - >>> execute(["b"], manipulate_encodings=False) - >>> bd._clear_bugs() - >>> bug = cmdutil.bug_from_id(bd, "b") - >>> bug.load_comments(load_full=False) - >>> comment = bug.comment_root[0] - >>> print comment.body - I like cheese - - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - if len(args) == 0: - raise cmdutil.UsageError("Please specify a bug or comment id.") - if len(args) > 2: - raise cmdutil.UsageError("Too many arguments.") - - shortname = args[0] - - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bug, parent = cmdutil.bug_comment_from_id(bd, shortname) - - if len(args) == 1: # try to launch an editor for comment-body entry - try: - if parent == bug.comment_root: - parent_body = bug.summary+"\n" - else: - parent_body = parent.body - estr = "Please enter your comment above\n\n> %s\n" \ - % ("\n> ".join(parent_body.splitlines())) - body = editor.editor_string(estr) - except editor.CantFindEditor, e: - raise cmdutil.UserError, "No comment supplied, and EDITOR not specified." - if body is None: - raise cmdutil.UserError("No comment entered.") - elif args[1] == '-': # read body from stdin - binary = not (options.content_type == None - or options.content_type.startswith("text/")) - if not binary: - body = sys.stdin.read() - if not body.endswith('\n'): - body+='\n' - else: # read-in without decoding - body = sys.__stdin__.read() - else: # body = arg[1] - body = args[1] - if not body.endswith('\n'): - body+='\n' - - new = parent.new_reply(body=body, content_type=options.content_type) - if options.author != None: - new.author = options.author - if options.alt_id != None: - new.alt_id = options.alt_id - -def get_parser(): - parser = cmdutil.CmdOptionParser("be comment ID [COMMENT]") - parser.add_option("-a", "--author", metavar="AUTHOR", dest="author", - help="Set the comment author", default=None) - parser.add_option("--alt-id", metavar="ID", dest="alt_id", - help="Set an alternate comment ID", default=None) - parser.add_option("-c", "--content-type", metavar="MIME", dest="content_type", - help="Set comment content-type (e.g. text/plain)", default=None) - return parser - -longhelp=""" -To add a comment to a bug, use the bug ID as the argument. To reply -to another comment, specify the comment name (as shown in "be show" -output). COMMENT, if specified, should be either the text of your -comment or "-", in which case the text will be read from stdin. If -you do not specify a COMMENT, $EDITOR is used to launch an editor. If -COMMENT is unspecified and EDITOR is not set, no comment will be -created. -""" - -def help(): - return get_parser().help_str() + longhelp - -def complete(options, args, parser): - for option,value in cmdutil.option_value_pairs(options, parser): - if value == "--complete": - # no argument-options at the moment, so this is future-proofing - raise cmdutil.GetCompletions() - for pos,value in enumerate(args): - if value == "--complete": - if pos == 0: # fist positional argument is a bug or comment id - if len(args) >= 2: - partial = args[1].split(':')[0] # take only bugid portion - else: - partial = "" - ids = [] - try: - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=False) - bugs = [] - for uuid in bd.uuids(): - if uuid.startswith(partial): - bug = bd.bug_from_uuid(uuid) - if bug.active == True: - bugs.append(bug) - for bug in bugs: - shortname = bd.bug_shortname(bug) - ids.append(shortname) - bug.load_comments(load_full=False) - for id,comment in bug.comment_shortnames(shortname): - ids.append(id) - except bugdir.NoBugDir: - pass - raise cmdutil.GetCompletions(ids) - raise cmdutil.GetCompletions() diff --git a/becommands/commit.py b/becommands/commit.py deleted file mode 100644 index cade355..0000000 --- a/becommands/commit.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (C) 2009 W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Commit the currently pending changes to the repository""" -from libbe import cmdutil, bugdir, editor, vcs -import sys -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> from libbe import bug - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> full_path = "testfile" - >>> test_contents = "A test file" - >>> bd.vcs.set_file_contents(full_path, test_contents) - >>> execute(["Added %s." % (full_path)], manipulate_encodings=False) # doctest: +ELLIPSIS - Committed ... - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser) - if len(args) != 1: - raise cmdutil.UsageError("Please supply a commit message") - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if args[0] == '-': # read summary from stdin - assert options.body != "EDITOR", \ - "Cannot spawn and editor when the summary is using stdin." - summary = sys.stdin.readline() - else: - summary = args[0] - if options.body == None: - body = None - elif options.body == "EDITOR": - body = editor.editor_string("Please enter your commit message above") - else: - if restrict_file_access == True: - cmdutil.restrict_file_access(bd, options.body) - body = bd.vcs.get_file_contents(options.body, allow_no_vcs=True) - try: - revision = bd.vcs.commit(summary, body=body, - allow_empty=options.allow_empty) - except vcs.EmptyCommit, e: - print e - return 1 - else: - print "Committed %s" % revision - -def get_parser(): - parser = cmdutil.CmdOptionParser("be commit COMMENT") - parser.add_option("-b", "--body", metavar="FILE", dest="body", - help='Provide a detailed body for the commit message. In the special case that FILE == "EDITOR", spawn an editor to enter the body text (in which case you cannot use stdin for the summary)', default=None) - parser.add_option("-a", "--allow-empty", dest="allow_empty", - help="Allow empty commits", - default=False, action="store_true") - return parser - -longhelp=""" -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. -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/depend.py b/becommands/depend.py deleted file mode 100644 index c2cb2a4..0000000 --- a/becommands/depend.py +++ /dev/null @@ -1,371 +0,0 @@ -# Copyright (C) 2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Add/remove bug dependencies""" -from libbe import cmdutil, bugdir, bug, tree -import os, copy -__desc__ = __doc__ - -BLOCKS_TAG="BLOCKS:" -BLOCKED_BY_TAG="BLOCKED-BY:" - -class BrokenLink (Exception): - def __init__(self, blocked_bug, blocking_bug, blocks=True): - if blocks == True: - msg = "Missing link: %s blocks %s" \ - % (blocking_bug.uuid, blocked_bug.uuid) - else: - msg = "Missing link: %s blocked by %s" \ - % (blocked_bug.uuid, blocking_bug.uuid) - Exception.__init__(self, msg) - self.blocked_bug = blocked_bug - self.blocking_bug = blocking_bug - - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> from libbe import utility - >>> bd = bugdir.SimpleBugDir() - >>> bd.save() - >>> os.chdir(bd.root) - >>> execute(["a", "b"], manipulate_encodings=False) - a blocked by: - b - >>> execute(["a"], manipulate_encodings=False) - a blocked by: - b - >>> execute(["--show-status", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - a blocked by: - b closed - >>> execute(["b", "a"], manipulate_encodings=False) - b blocked by: - a - b blocks: - a - >>> execute(["--show-status", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - a blocked by: - b closed - a blocks: - b closed - >>> execute(["-r", "b", "a"], manipulate_encodings=False) - b blocks: - a - >>> execute(["-r", "a", "b"], manipulate_encodings=False) - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda _bug : _bug.active==True, - 1: lambda _bug : _bug.active==True}) - - if options.repair == True: - if len(args) > 0: - raise cmdutil.UsageError("No arguments with --repair calls.") - elif len(args) < 1: - raise cmdutil.UsageError("Please a bug id.") - elif len(args) > 2: - help() - raise cmdutil.UsageError("Too many arguments.") - elif len(args) == 2 and options.tree_depth != None: - raise cmdutil.UsageError("Only one bug id used in tree mode.") - - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if options.repair == True: - good,fixed,broken = check_dependencies(bd, repair_broken_links=True) - assert len(broken) == 0, broken - if len(fixed) > 0: - print "Fixed the following links:" - print "\n".join(["%s |-- %s" % (blockee.uuid, blocker.uuid) - for blockee,blocker in fixed]) - return 0 - - allowed_status_values = \ - cmdutil.select_values(options.status, bug.status_values) - allowed_severity_values = \ - cmdutil.select_values(options.severity, bug.severity_values) - - bugA = cmdutil.bug_from_id(bd, args[0]) - - if options.tree_depth != None: - dtree = DependencyTree(bd, bugA, options.tree_depth, - allowed_status_values, - allowed_severity_values) - if len(dtree.blocked_by_tree()) > 0: - print "%s blocked by:" % bugA.uuid - for depth,node in dtree.blocked_by_tree().thread(): - if depth == 0: continue - print "%s%s" % (" "*(depth), node.bug.string(shortlist=True)) - if len(dtree.blocks_tree()) > 0: - print "%s blocks:" % bugA.uuid - for depth,node in dtree.blocks_tree().thread(): - if depth == 0: continue - print "%s%s" % (" "*(depth), node.bug.string(shortlist=True)) - return 0 - - if len(args) == 2: - bugB = cmdutil.bug_from_id(bd, args[1]) - if options.remove == True: - remove_block(bugA, bugB) - else: # add the dependency - add_block(bugA, bugB) - - blocked_by = get_blocked_by(bd, bugA) - if len(blocked_by) > 0: - print "%s blocked by:" % bugA.uuid - if options.show_status == True: - print '\n'.join(["%s\t%s" % (_bug.uuid, _bug.status) - for _bug in blocked_by]) - else: - print '\n'.join([_bug.uuid for _bug in blocked_by]) - blocks = get_blocks(bd, bugA) - if len(blocks) > 0: - print "%s blocks:" % bugA.uuid - if options.show_status == True: - print '\n'.join(["%s\t%s" % (_bug.uuid, _bug.status) - for _bug in blocks]) - else: - print '\n'.join([_bug.uuid for _bug in blocks]) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be depend BUG-ID [BUG-ID]\nor: be depend --repair") - parser.add_option("-r", "--remove", action="store_true", - dest="remove", default=False, - help="Remove dependency (instead of adding it)") - parser.add_option("-s", "--show-status", action="store_true", - dest="show_status", default=False, - help="Show status of blocking bugs") - parser.add_option("--status", dest="status", metavar="STATUS", - help="Only show bugs matching the STATUS specifier") - parser.add_option("--severity", dest="severity", metavar="SEVERITY", - help="Only show bugs matching the SEVERITY specifier") - parser.add_option("-t", "--tree-depth", metavar="DEPTH", default=None, - type="int", dest="tree_depth", - help="Print dependency tree rooted at BUG-ID with DEPTH levels of both blockers and blockees. Set DEPTH <= 0 to disable the depth limit.") - parser.add_option("--repair", action="store_true", - dest="repair", default=False, - help="Check for and repair one-way links") - return parser - -longhelp=""" -Set a dependency with the second bug (B) blocking the first bug (A). -If bug B is not specified, just print a list of bugs blocking (A). - -To search for bugs blocked by a particular bug, try - $ be list --extra-strings BLOCKED-BY: - -The --status and --severity options allow you to either blacklist or -whitelist values, for example - $ be list --status open,assigned -will only follow and print dependencies with open or assigned status. -You select blacklist mode by starting the list with a minus sign, for -example - $ be list --severity -target -which will only follow and print dependencies with non-target severity. - -In repair mode, add the missing direction to any one-way links. - -The "|--" symbol in the repair-mode output is inspired by the -"negative feedback" arrow common in biochemistry. See, for example - http://www.nature.com/nature/journal/v456/n7223/images/nature07513-f5.0.jpg -""" - -def help(): - return get_parser().help_str() + longhelp - -# internal helper functions - -def _generate_blocks_string(blocked_bug): - return "%s%s" % (BLOCKS_TAG, blocked_bug.uuid) - -def _generate_blocked_by_string(blocking_bug): - return "%s%s" % (BLOCKED_BY_TAG, blocking_bug.uuid) - -def _parse_blocks_string(string): - assert string.startswith(BLOCKS_TAG) - return string[len(BLOCKS_TAG):] - -def _parse_blocked_by_string(string): - assert string.startswith(BLOCKED_BY_TAG) - return string[len(BLOCKED_BY_TAG):] - -def _add_remove_extra_string(bug, string, add): - estrs = bug.extra_strings - if add == True: - estrs.append(string) - else: # remove the string - estrs.remove(string) - bug.extra_strings = estrs # reassign to notice change - -def _get_blocks(bug): - uuids = [] - for line in bug.extra_strings: - if line.startswith(BLOCKS_TAG): - uuids.append(_parse_blocks_string(line)) - return uuids - -def _get_blocked_by(bug): - uuids = [] - for line in bug.extra_strings: - if line.startswith(BLOCKED_BY_TAG): - uuids.append(_parse_blocked_by_string(line)) - return uuids - -def _repair_one_way_link(blocked_bug, blocking_bug, blocks=None): - if blocks == True: # add blocks link - blocks_string = _generate_blocks_string(blocked_bug) - _add_remove_extra_string(blocking_bug, blocks_string, add=True) - else: # add blocked by link - blocked_by_string = _generate_blocked_by_string(blocking_bug) - _add_remove_extra_string(blocked_bug, blocked_by_string, add=True) - -# functions exposed to other modules - -def add_block(blocked_bug, blocking_bug): - blocked_by_string = _generate_blocked_by_string(blocking_bug) - _add_remove_extra_string(blocked_bug, blocked_by_string, add=True) - blocks_string = _generate_blocks_string(blocked_bug) - _add_remove_extra_string(blocking_bug, blocks_string, add=True) - -def remove_block(blocked_bug, blocking_bug): - blocked_by_string = _generate_blocked_by_string(blocking_bug) - _add_remove_extra_string(blocked_bug, blocked_by_string, add=False) - blocks_string = _generate_blocks_string(blocked_bug) - _add_remove_extra_string(blocking_bug, blocks_string, add=False) - -def get_blocks(bugdir, bug): - """ - Return a list of bugs that the given bug blocks. - """ - blocks = [] - for uuid in _get_blocks(bug): - blocks.append(bugdir.bug_from_uuid(uuid)) - return blocks - -def get_blocked_by(bugdir, bug): - """ - Return a list of bugs blocking the given bug. - """ - blocked_by = [] - for uuid in _get_blocked_by(bug): - blocked_by.append(bugdir.bug_from_uuid(uuid)) - return blocked_by - -def check_dependencies(bugdir, repair_broken_links=False): - """ - Check that links are bi-directional for all bugs in bugdir. - - >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) - >>> a = bd.bug_from_uuid("a") - >>> b = bd.bug_from_uuid("b") - >>> blocked_by_string = _generate_blocked_by_string(b) - >>> _add_remove_extra_string(a, blocked_by_string, add=True) - >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=False) - >>> good - [] - >>> repaired - [] - >>> broken - [(Bug(uuid='a'), Bug(uuid='b'))] - >>> _get_blocks(b) - [] - >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=True) - >>> _get_blocks(b) - ['a'] - >>> good - [] - >>> repaired - [(Bug(uuid='a'), Bug(uuid='b'))] - >>> broken - [] - """ - if bugdir.sync_with_disk == True: - bugdir.load_all_bugs() - good_links = [] - fixed_links = [] - broken_links = [] - for bug in bugdir: - for blocker in get_blocked_by(bugdir, bug): - blocks = get_blocks(bugdir, blocker) - if (bug, blocks) in good_links+fixed_links+broken_links: - continue # already checked that link - if bug not in blocks: - if repair_broken_links == True: - _repair_one_way_link(bug, blocker, blocks=True) - fixed_links.append((bug, blocker)) - else: - broken_links.append((bug, blocker)) - else: - good_links.append((bug, blocker)) - for blockee in get_blocks(bugdir, bug): - blocked_by = get_blocked_by(bugdir, blockee) - if (blockee, bug) in good_links+fixed_links+broken_links: - continue # already checked that link - if bug not in blocked_by: - if repair_broken_links == True: - _repair_one_way_link(blockee, bug, blocks=False) - fixed_links.append((blockee, bug)) - else: - broken_links.append((blockee, bug)) - else: - good_links.append((blockee, bug)) - return (good_links, fixed_links, broken_links) - -class DependencyTree (object): - """ - Note: should probably be DependencyDiGraph. - """ - def __init__(self, bugdir, root_bug, depth_limit=0, - allowed_status_values=None, - allowed_severity_values=None): - self.bugdir = bugdir - self.root_bug = root_bug - self.depth_limit = depth_limit - self.allowed_status_values = allowed_status_values - self.allowed_severity_values = allowed_severity_values - def _build_tree(self, child_fn): - root = tree.Tree() - root.bug = self.root_bug - root.depth = 0 - stack = [root] - while len(stack) > 0: - node = stack.pop() - if self.depth_limit > 0 and node.depth == self.depth_limit: - continue - for bug in child_fn(self.bugdir, node.bug): - if self.allowed_status_values != None \ - and not bug.status in self.allowed_status_values: - continue - if self.allowed_severity_values != None \ - and not bug.severity in self.allowed_severity_values: - continue - child = tree.Tree() - child.bug = bug - child.depth = node.depth+1 - node.append(child) - stack.append(child) - return root - def blocks_tree(self): - if not hasattr(self, "_blocks_tree"): - self._blocks_tree = self._build_tree(get_blocks) - return self._blocks_tree - def blocked_by_tree(self): - if not hasattr(self, "_blocked_by_tree"): - self._blocked_by_tree = self._build_tree(get_blocked_by) - return self._blocked_by_tree diff --git a/becommands/diff.py b/becommands/diff.py deleted file mode 100644 index c5c34f9..0000000 --- a/becommands/diff.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -"""Compare bug reports with older tree""" -from libbe import cmdutil, bugdir, diff -import os -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> bd.set_sync_with_disk(True) - >>> original = bd.vcs.commit("Original status") - >>> bug = bd.bug_from_uuid("a") - >>> bug.status = "closed" - >>> changed = bd.vcs.commit("Closed bug a") - >>> os.chdir(bd.root) - >>> if bd.vcs.versioned == True: - ... execute([original], manipulate_encodings=False) - ... else: - ... print "Modified bugs:\\n a:cm: Bug A\\n Changed bug settings:\\n status: open -> closed" - Modified bugs: - a:cm: Bug A - Changed bug settings: - status: open -> closed - >>> if bd.vcs.versioned == True: - ... execute(["--subscribe", "%(bugdir_id)s:mod", "--uuids", original], - ... manipulate_encodings=False) - ... else: - ... print "a" - a - >>> if bd.vcs.versioned == False: - ... execute([original], manipulate_encodings=False) - ... else: - ... raise cmdutil.UsageError('This directory is not revision-controlled.') - Traceback (most recent call last): - ... - UsageError: This directory is not revision-controlled. - >>> bd.cleanup() - """ % {'bugdir_id':diff.BUGDIR_ID} - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser) - if len(args) == 0: - revision = None - if len(args) == 1: - revision = args[0] - if len(args) > 1: - raise cmdutil.UsageError('Too many arguments.') - try: - subscriptions = diff.subscriptions_from_string( - options.subscribe) - except ValueError, e: - raise cmdutil.UsageError(e.msg) - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if bd.vcs.versioned == False: - raise cmdutil.UsageError('This directory is not revision-controlled.') - if options.dir == None: - if revision == None: # get the most recent revision - revision = bd.vcs.revision_id(-1) - old_bd = bd.duplicate_bugdir(revision) - else: - old_bd_current = bugdir.BugDir(root=os.path.abspath(options.dir), - from_disk=True, - manipulate_encodings=False) - if revision == None: # use the current working state - old_bd = old_bd_current - else: - if old_bd_current.vcs.versioned == False: - raise cmdutil.UsageError('%s is not revision-controlled.' - % options.dir) - old_bd = old_bd_current.duplicate_bugdir(revision) - d = diff.Diff(old_bd, bd) - tree = d.report_tree(subscriptions) - - if options.uuids == True: - uuids = [] - bugs = tree.child_by_path('/bugs') - for bug_type in bugs: - uuids.extend([bug.name for bug in bug_type]) - print '\n'.join(uuids) - else : - rep = tree.report_string() - if rep != None: - print rep - bd.remove_duplicate_bugdir() - if options.dir != None and revision != None: - old_bd_current.remove_duplicate_bugdir() - -def get_parser(): - parser = cmdutil.CmdOptionParser("be diff [options] REVISION") - parser.add_option("-d", "--dir", dest="dir", metavar="DIR", - help="Compare with repository in DIR instead of the current directory.") - parser.add_option("-s", "--subscribe", dest="subscribe", metavar="SUBSCRIPTION", - help="Only print changes matching SUBSCRIPTION, subscription is a comma-separ\ated list of ID:TYPE tuples. See `be subscribe --help` for descriptions of ID and TYPE.") - parser.add_option("-u", "--uuids", action="store_true", dest="uuids", - help="Only print the bug UUIDS.", default=False) - return parser - -longhelp=""" -Uses the VCS to compare the current tree with a previous tree, and -prints a pretty report. If REVISION is given, it is a specifier for -the particular previous tree to use. Specifiers are specific to their -VCS. - -For Arch your specifier must be a fully-qualified revision name. - -Besides the standard summary output, you can use the options to output -UUIDS for the different categories. This output can be used as the -input to 'be show' to get an understanding of the current status. -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/due.py b/becommands/due.py deleted file mode 100644 index 0b8d1e9..0000000 --- a/becommands/due.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (C) 2009 W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Set bug due dates""" -from libbe import cmdutil, bugdir, utility -__desc__ = __doc__ - -DUE_TAG="DUE:" - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> bd.save() - >>> os.chdir(bd.root) - >>> execute(["a"], manipulate_encodings=False) - No due date assigned. - >>> execute(["a", "Thu, 01 Jan 1970 00:00:00 +0000"], manipulate_encodings=False) - >>> execute(["a"], manipulate_encodings=False) - Thu, 01 Jan 1970 00:00:00 +0000 - >>> execute(["a", "none"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - >>> execute(["a"], manipulate_encodings=False) - No due date assigned. - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) - - if len(args) not in (1, 2): - raise cmdutil.UsageError('Incorrect number of arguments.') - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bug = cmdutil.bug_from_id(bd, args[0]) - if len(args) == 1: - due_time = get_due(bug) - if due_time is None: - print "No due date assigned." - else: - print utility.time_to_str(due_time) - else: - if args[1] == "none": - remove_due(bug) - else: - due_time = utility.str_to_time(args[1]) - set_due(bug, due_time) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be due BUG-ID [DATE]") - return parser - -longhelp=""" -If no DATE is specified, the bug's current due date is printed. If -DATE is specified, it will be assigned to the bug. -""" - -def help(): - return get_parser().help_str() + longhelp - -# internal helper functions - -def _generate_due_string(time): - return "%s%s" % (DUE_TAG, utility.time_to_str(time)) - -def _parse_due_string(string): - assert string.startswith(DUE_TAG) - return utility.str_to_time(string[len(DUE_TAG):]) - -# functions exposed to other modules - -def get_due(bug): - matched = [] - for line in bug.extra_strings: - if line.startswith(DUE_TAG): - matched.append(_parse_due_string(line)) - if len(matched) == 0: - return None - if len(matched) > 1: - raise Exception('Several due dates for %s?:\n %s' - % (bug.uuid, '\n '.join(matched))) - return matched[0] - -def remove_due(bug): - estrs = bug.extra_strings - for due_str in [s for s in estrs if s.startswith(DUE_TAG)]: - estrs.remove(due_str) - bug.extra_strings = estrs # reassign to notice change - -def set_due(bug, time): - remove_due(bug) - estrs = bug.extra_strings - estrs.append(_generate_due_string(time)) - bug.extra_strings = estrs # reassign to notice change diff --git a/becommands/email_bugs.py b/becommands/email_bugs.py deleted file mode 100644 index f6641e3..0000000 --- a/becommands/email_bugs.py +++ /dev/null @@ -1,239 +0,0 @@ -# Copyright (C) 2009 W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Email specified bugs in a be-handle-mail compatible format.""" - -import copy -from cStringIO import StringIO -from email import Message -from email.mime.text import MIMEText -from email.generator import Generator -import sys -import time - -from libbe import cmdutil, bugdir -from libbe.subproc import invoke -from libbe.utility import time_to_str -from libbe.vcs import detect_vcs, installed_vcs -import show - -__desc__ = __doc__ - -sendmail='/usr/sbin/sendmail -t' - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> from libbe import bug - >>> bd = bugdir.SimpleBugDir() - >>> bd.encoding = 'utf-8' - >>> os.chdir(bd.root) - >>> import email.charset as c - >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8') - >>> execute(["-o", "--to", "a@b.com", "--from", "b@c.edu", "a", "b"], - ... manipulate_encodings=False) # doctest: +ELLIPSIS - Content-Type: text/xml; charset="utf-8" - MIME-Version: 1.0 - Content-Transfer-Encoding: quoted-printable - From: b@c.edu - To: a@b.com - Date: ... - Subject: [be-bug:xml] Updates to a, b - - - - - ... - ... - ... - ... - - - a - a - minor - open - John Doe <jdoe@example.com> - Thu, 01 Jan 1970 00:00:00 +0000 - Bug A - - - b - b - minor - closed - Jane Doe <jdoe@example.com> - Thu, 01 Jan 1970 00:00:00 +0000 - Bug B - - - >>> bd.cleanup() - - Note that the '=3D' bits in - - are the way quoted-printable escapes '='. - - The unclosed ... is because revision ids can be long - enough to cause line wraps, and we want to ensure we match even if - the closing is split by the wrapping. - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={-1: lambda bug : bug.active==True}) - if len(args) == 0: - raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - xml = show.output(args, bd, as_xml=True, with_comments=True) - subject = options.subject - if subject == None: - subject = '[be-bug:xml] Updates to %s' % ', '.join(args) - submit_email = TextEmail(to_address=options.to_address, - from_address=options.from_address, - subject=subject, - body=xml, - encoding=bd.encoding, - subtype='xml') - if options.output == True: - print submit_email - else: - submit_email.send() - -def get_parser(): - parser = cmdutil.CmdOptionParser("be email-bugs [options] ID [ID ...]") - parser.add_option("-t", "--to", metavar="EMAIL", dest="to_address", - help="Submission email address (%default)", - default="be-devel@bugseverywhere.org") - parser.add_option("-f", "--from", metavar="EMAIL", dest="from_address", - help="Senders email address, overriding auto-generated default", - default=None) - parser.add_option("-s", "--subject", metavar="STRING", dest="subject", - help="Subject line, overriding auto-generated default. If you use this option, remember that be-handle-mail probably want something like '[be-bug:xml] ...'", - default=None) - parser.add_option('-o', '--output', dest='output', action='store_true', - help="Don't mail the generated message, print it to stdout instead. Useful for testing functionality.") - return parser - -longhelp=""" -Email specified bugs in a be-handle-mail compatible format. This is -the prefered method for reporting bugs if you did not install bzr by -branching a bzr repository. - -If you _did_ install bzr by branching a bzr repository, we suggest you -commit any new bug information with - bzr commit --message "Reported bug in demuxulizer" -and then email a bzr merge directive with - bzr send --mail-to "be-devel@bugseverywhere.org" -rather than using this command. -""" - -def help(): - return get_parser().help_str() + longhelp - -class TextEmail (object): - """ - Make it very easy to compose and send single-part text emails. - >>> msg = TextEmail(to_address='Monty ', - ... from_address='Python ', - ... subject='Parrots', - ... header={'x-special-header':'your info here'}, - ... body="Remarkable bird, id'nit, squire?\\nLovely plumage!") - >>> print msg # doctest: +ELLIPSIS - Content-Type: text/plain; charset="utf-8" - MIME-Version: 1.0 - Content-Transfer-Encoding: base64 - From: Python - To: Monty - Date: ... - Subject: Parrots - x-special-header: your info here - - UmVtYXJrYWJsZSBiaXJkLCBpZCduaXQsIHNxdWlyZT8KTG92ZWx5IHBsdW1hZ2Uh - - >>> import email.charset as c - >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8') - >>> print msg # doctest: +ELLIPSIS - Content-Type: text/plain; charset="utf-8" - MIME-Version: 1.0 - Content-Transfer-Encoding: quoted-printable - From: Python - To: Monty - Date: ... - Subject: Parrots - x-special-header: your info here - - Remarkable bird, id'nit, squire? - Lovely plumage! - """ - def __init__(self, to_address, from_address=None, subject=None, - header=None, body=None, encoding='utf-8', subtype='plain'): - self.to_address = to_address - self.from_address = from_address - if self.from_address == None: - self.from_address = self._guess_from_address() - self.subject = subject - self.header = header - if self.header == None: - self.header = {} - self.body = body - self.encoding = encoding - self.subtype = subtype - def _guess_from_address(self): - vcs = detect_vcs('.') - if vcs.name == "None": - vcs = installed_vcs() - return vcs.get_user_id() - def encoded_MIME_body(self): - return MIMEText(self.body.encode(self.encoding), - self.subtype, - self.encoding) - def message(self): - response = self.encoded_MIME_body() - response['From'] = self.from_address - response['To'] = self.to_address - response['Date'] = time_to_str(time.time()) - response['Subject'] = self.subject - for k,v in self.header.items(): - response[k] = v - return response - def flatten(self, to_unicode=False): - """ - This is a simplified version of send_pgp_mime.flatten(). - """ - fp = StringIO() - g = Generator(fp, mangle_from_=False) - g.flatten(self.message()) - text = fp.getvalue() - if to_unicode == True: - encoding = msg.get_content_charset() or "utf-8" - text = unicode(text, encoding=encoding) - return text - def __str__(self): - return self.flatten() - def __unicode__(self): - return self.flatten(to_unicode=True) - def send(self, sendmail=None): - """ - This is a simplified version of send_pgp_mime.mail(). - - Send an email Message instance on its merry way by shelling - out to the user specified sendmail. - """ - if sendmail == None: - sendmail = SENDMAIL - invoke(sendmail, stdin=self.flatten()) diff --git a/becommands/help.py b/becommands/help.py deleted file mode 100644 index 9e6d1aa..0000000 --- a/becommands/help.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (C) 2006-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# Thomas Gerigk -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Print help for given subcommand""" -from libbe import cmdutil, utility -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - Print help of specified command (the manipulate_encodings argument - is ignored). - - >>> execute(["help"]) - Usage: be help [COMMAND] - - Options: - -h, --help Print a help message - --complete Print a list of available completions - - Print help for specified command or list of all commands. - - """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - if len(args) > 1: - raise cmdutil.UsageError("Too many arguments.") - if len(args) == 0: - print cmdutil.help() - else: - try: - print cmdutil.help(args[0]) - except AttributeError: - print "No help available" - -def get_parser(): - parser = cmdutil.CmdOptionParser("be help [COMMAND]") - return parser - -longhelp=""" -Print help for specified command or list of all commands. -""" - -def help(): - return get_parser().help_str() + longhelp - -def complete(options, args, parser): - for option, value in cmdutil.option_value_pairs(options, parser): - if value == "--complete": - # no argument-options at the moment, so this is future-proofing - raise cmdutil.GetCompletions() - if "--complete" in args: - cmds = [command for command,module in cmdutil.iter_commands()] - raise cmdutil.GetCompletions(cmds) diff --git a/becommands/html.py b/becommands/html.py deleted file mode 100644 index d9e0d73..0000000 --- a/becommands/html.py +++ /dev/null @@ -1,609 +0,0 @@ -# Copyright (C) 2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Generate a static HTML dump of the current repository status""" -from libbe import cmdutil, bugdir, bug -import codecs, os, os.path, re, string, time -import xml.sax.saxutils, htmlentitydefs - -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute([], manipulate_encodings=False) - >>> os.path.exists("./html_export") - True - >>> os.path.exists("./html_export/index.html") - True - >>> os.path.exists("./html_export/index_inactive.html") - True - >>> os.path.exists("./html_export/bugs") - True - >>> os.path.exists("./html_export/bugs/a.html") - True - >>> os.path.exists("./html_export/bugs/b.html") - True - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - cmdutil.default_complete(options, args, parser) - - if len(args) > 0: - raise cmdutil.UsageError, 'Too many arguments.' - - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bd.load_all_bugs() - - html_gen = HTMLGen(bd, template=options.template, verbose=options.verbose, - title=options.title, index_header=options.index_header) - if options.exp_template == True: - html_gen.write_default_template(options.exp_template_dir) - return - html_gen.run(options.out_dir) - -def get_parser(): - parser = cmdutil.CmdOptionParser('be html [options]') - parser.add_option('-o', '--output', metavar='DIR', dest='out_dir', - help='Set the output path (%default)', default='./html_export') - parser.add_option('-t', '--template-dir', metavar='DIR', dest='template', - help='Use a different template, defaults to internal templates', - default=None) - parser.add_option('--title', metavar='STRING', dest='title', - help='Set the bug repository title (%default)', - default='BugsEverywhere Issue Tracker') - parser.add_option('--index-header', metavar='STRING', dest='index_header', - help='Set the index page headers (%default)', - default='BugsEverywhere Bug List') - parser.add_option('-v', '--verbose', action='store_true', - metavar='verbose', dest='verbose', - help='Verbose output, default is %default', default=False) - parser.add_option('-e', '--export-template', action='store_true', - dest='exp_template', - help='Export the default template and exit.', default=False) - parser.add_option('-d', '--export-template-dir', metavar='DIR', - dest='exp_template_dir', default='./default-templates/', - help='Set the directory for the template export (%default)') - return parser - -longhelp=""" -Generate a set of html pages representing the current state of the bug -directory. -""" - -def help(): - return get_parser().help_str() + longhelp - -def complete(options, args, parser): - for option, value in cmdutil.option_value_pairs(options, parser): - if "--complete" in args: - raise cmdutil.GetCompletions() # no positional arguments for list - -class HTMLGen (object): - def __init__(self, bd, template=None, verbose=False, encoding=None, - title="Site Title", index_header="Index Header", - ): - self.generation_time = time.ctime() - self.bd = bd - self.verbose = verbose - self.title = title - self.index_header = index_header - if encoding != None: - self.encoding = encoding - else: - self.encoding = self.bd.encoding - if template == None: - self.template = "default" - else: - self.template = os.path.abspath(os.path.expanduser(template)) - self._load_default_templates() - - if template != None: - self._load_user_templates() - - def run(self, out_dir): - if self.verbose == True: - print "Creating the html output in %s using templates in %s" \ - % (out_dir, self.template) - - bugs_active = [] - bugs_inactive = [] - bugs = [b for b in self.bd] - bugs.sort() - bugs_active = [b for b in bugs if b.active == True] - bugs_inactive = [b for b in bugs if b.active != True] - - self._create_output_directories(out_dir) - self._write_css_file() - for b in bugs: - if b.active: - up_link = "../index.html" - else: - up_link = "../index_inactive.html" - self._write_bug_file(b, up_link) - self._write_index_file( - bugs_active, title=self.title, - index_header=self.index_header, bug_type="active") - self._write_index_file( - bugs_inactive, title=self.title, - index_header=self.index_header, bug_type="inactive") - - def _create_output_directories(self, out_dir): - if self.verbose: - print "Creating output directories" - self.out_dir = self._make_dir(out_dir) - self.out_dir_bugs = self._make_dir( - os.path.join(self.out_dir, "bugs")) - - def _write_css_file(self): - if self.verbose: - print "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"]) - - def _write_bug_file(self, bug, up_link): - if self.verbose: - print "\tCreating bug file for %s" % self.bd.bug_shortname(bug) - assert hasattr(self, "out_dir_bugs"), \ - "Must run after ._create_output_directories()" - - bug.load_comments(load_full=True) - comment_entries = self._generate_bug_comment_entries(bug) - filename = "%s.html" % bug.uuid - fullpath = os.path.join(self.out_dir_bugs, filename) - template_info = {'title':self.title, - 'charset':self.encoding, - 'up_link':up_link, - 'shortname':self.bd.bug_shortname(bug), - 'comment_entries':comment_entries, - 'generation_time':self.generation_time} - for attr in ['uuid', 'severity', 'status', 'assigned', - 'reporter', 'creator', 'time_string', 'summary']: - template_info[attr] = self._escape(getattr(bug, attr)) - self._write_file(self.bug_file % template_info, [fullpath]) - - def _generate_bug_comment_entries(self, bug): - assert hasattr(self, "out_dir_bugs"), \ - "Must run after ._create_output_directories()" - - stack = [] - comment_entries = [] - 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
\n") - assert len(stack) == depth - stack.append(comment) - if depth == 0: - comment_entries.append('
') - else: - comment_entries.append('
') - template_info = {} - for attr in ['uuid', 'author', 'date', 'body']: - value = getattr(comment, attr) - if attr == 'body': - save_body = False - if comment.content_type == 'text/html': - pass # no need to escape html... - elif comment.content_type.startswith('text/'): - value = '
\n'+self._escape(value)+'\n
' - elif comment.content_type.startswith('image/'): - save_body = True - value = '' \ - % (bug.uuid, comment.uuid) - else: - save_body = True - value = 'Link to %s file.' \ - % (bug.uuid, comment.uuid, comment.content_type) - 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( - '\n ForceType %s\n' \ - % (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("
\n") # close every remaining
- - - %(title)s - - - - - -
-

%(index_header)s

-

- - - - - - - -
Active BugsInactive Bugs
- - - - %(bug_entries)s - - -
-
- - - - - - """ - - self.index_bug_entry =""" - - %(shortname)s - %(status)s - %(severity)s - %(summary)s - %(time_string)s - - """ - - self.bug_file = """ - - - - %(title)s - - - - - -
-

BugsEverywhere Bug List

-
Back to Index
-

Bug: %(shortname)s

- - - - - - - - - - - - - - - - - - - - - - -
ID :%(uuid)s
Short name :%(shortname)s
Status :%(status)s
Severity :%(severity)s
Assigned :%(assigned)s
Reporter :%(reporter)s
Creator :%(creator)s
Created :%(time_string)s
Summary :%(summary)s
- -
- - %(comment_entries)s - -
-
Back to Index
- - - - - - """ - - self.bug_comment_entry =""" - - - - - -
Comment: - --------- Comment ---------
- Name: %(uuid)s
- From: %(author)s
- Date: %(date)s
-
- %(body)s -
- """ - - # 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') diff --git a/becommands/import_xml.py b/becommands/import_xml.py deleted file mode 100644 index b985193..0000000 --- a/becommands/import_xml.py +++ /dev/null @@ -1,434 +0,0 @@ -# Copyright (C) 2009 W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Import comments and bugs from XML""" -import libbe -from libbe import cmdutil, bugdir, bug, comment, utility -from becommands.comment import complete -import copy -import os -import sys -try: # import core module, Python >= 2.5 - from xml.etree import ElementTree -except ImportError: # look for non-core module - from elementtree import ElementTree -if libbe.TESTING == True: - import doctest - import unittest -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import time - >>> import StringIO - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> orig_stdin = sys.stdin - >>> sys.stdin = StringIO.StringIO("cThis is a comment about a") - >>> execute(["-c", "a", "-"], manipulate_encodings=False) - >>> sys.stdin = orig_stdin - >>> bd._clear_bugs() - >>> bug = cmdutil.bug_from_id(bd, "a") - >>> bug.load_comments(load_full=False) - >>> comment = bug.comment_root[0] - >>> print comment.body - This is a comment about a - - >>> comment.author == bd.user_id - True - >>> comment.time <= int(time.time()) - True - >>> comment.in_reply_to is None - True - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - if len(args) < 1: - raise cmdutil.UsageError("Please specify an XML file.") - if len(args) > 1: - raise cmdutil.UsageError("Too many arguments.") - filename = args[0] - - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if options.comment_root != None: - croot_bug,croot_comment = \ - cmdutil.bug_comment_from_id(bd, options.comment_root) - croot_bug.load_comments(load_full=True) - croot_bug.set_sync_with_disk(False) - if croot_comment.uuid == comment.INVALID_UUID: - croot_comment = croot_bug.comment_root - else: - croot_comment = croot_bug.comment_from_uuid(croot_comment.uuid) - new_croot_bug = bug.Bug(bugdir=bd, uuid=croot_bug.uuid) - new_croot_bug.explicit_attrs = [] - new_croot_bug.comment_root = copy.deepcopy(croot_bug.comment_root) - if croot_comment.uuid == comment.INVALID_UUID: - new_croot_comment = new_croot_bug.comment_root - else: - new_croot_comment = \ - new_croot_bug.comment_from_uuid(croot_comment.uuid) - for new in new_croot_bug.comments(): - new.explicit_attrs = [] - else: - croot_bug,croot_comment = (None, None) - - if filename == '-': - xml = sys.stdin.read() - else: - if restrict_file_access == True: - cmdutil.restrict_file_access(bd, options.body) - xml = bd.vcs.get_file_contents(filename, allow_no_vcs=True) - str_xml = xml.encode('unicode_escape').replace(r'\n', '\n') - # unicode read + encode to string so we know the encoding, - # which might not be given (?) in a binary string read? - - # parse the xml - root_bugs = [] - root_comments = [] - version = {} - be_xml = ElementTree.XML(str_xml) - if be_xml.tag != 'be-xml': - raise utility.InvalidXML( - 'import-xml', be_xml, 'root element must be ') - for child in be_xml.getchildren(): - if child.tag == 'bug': - new = bug.Bug(bugdir=bd) - new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape")) - root_bugs.append(new) - elif child.tag == 'comment': - new = comment.Comment(croot_bug) - new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape")) - root_comments.append(new) - elif child.tag == 'version': - for gchild in child.getchildren(): - if child.tag in ['tag', 'nick', 'revision', 'revision-id']: - text = xml.sax.saxutils.unescape(child.text) - text = text.decode('unicode_escape').strip() - version[child.tag] = text - else: - print >> sys.stderr, 'ignoring unknown tag %s in %s' \ - % (gchild.tag, child.tag) - else: - print >> sys.stderr, 'ignoring unknown tag %s in %s' \ - % (child.tag, comment_list.tag) - - # merge the new root_comments - if options.add_only == True: - accept_changes = False - accept_extra_strings = False - else: - accept_changes = True - accept_extra_strings = True - accept_comments = True - if len(root_comments) > 0: - if croot_bug == None: - raise UserError( - '--comment-root option is required for your root comments:\n%s' - % '\n\n'.join([c.string() for c in root_comments])) - try: - # link new comments - new_croot_bug.add_comments(root_comments, - default_parent=new_croot_comment, - ignore_missing_references= \ - options.ignore_missing_references) - except comment.MissingReference, e: - raise cmdutil.UserError(e) - croot_bug.merge(new_croot_bug, accept_changes=accept_changes, - accept_extra_strings=accept_extra_strings, - accept_comments=accept_comments) - - # merge the new croot_bugs - merged_bugs = [] - old_bugs = [] - for new in root_bugs: - try: - old = bd.bug_from_uuid(new.alt_id) - except KeyError: - old = None - if old == None: - bd.append(new) - else: - old.load_comments(load_full=True) - old.merge(new, accept_changes=accept_changes, - accept_extra_strings=accept_extra_strings, - accept_comments=accept_comments) - merged_bugs.append(new) - old_bugs.append(old) - - # protect against programmer error causing data loss: - if croot_bug != None: - comms = [c.uuid for c in croot_comment.traverse()] - for new in root_comments: - assert new.uuid in comms, \ - "comment %s wasn't added to %s" % (new.uuid, croot_comment.uuid) - for new in root_bugs: - if not new in merged_bugs: - assert bd.has_bug(new.uuid), \ - "bug %s wasn't added" % (new.uuid) - - # save new information - if croot_bug != None: - croot_bug.save() - for new in root_bugs: - if not new in merged_bugs: - new.save() - for old in old_bugs: - old.save() - -def get_parser(): - parser = cmdutil.CmdOptionParser("be import-xml XMLFILE") - parser.add_option("-i", "--ignore-missing-references", action="store_true", - dest="ignore_missing_references", default=False, - help="If any comment's refers to a non-existent comment, ignore it (instead of raising an exception).") - parser.add_option("-a", "--add-only", action='store_true', - dest="add_only", default=False, - help="If any bug or comment listed in the XML file already exists in the bug repository, do not alter the repository version.") - parser.add_option("-c", "--comment-root", dest="comment_root", - help="Supply a bug or comment ID as the root of any elements that are direct children of the element. If any such elements exist, you are required to set this option.") - return parser - -longhelp=""" -Import comments and bugs from XMLFILE. If XMLFILE is '-', the file is -read from stdin. - -This command provides a fallback mechanism for passing bugs between -repositories, in case the repositories VCSs are incompatible. If the -VCSs are compatible, it's better to use their builtin merge/push/pull -to share this information, as that will preserve a more detailed -history. - -The XML file should be formatted similarly to - - - 1.0.0 - be - 446 - a@b.com-20091119214553-iqyw2cpqluww3zna - - - ... - ... - ... - - ... - ... - ... - -where the ellipses mark output commpatible with Bug.xml() and -Comment.xml(). Take a look at the output of `be show --xml` for some -explicit examples. Unrecognized tags are ignored. Missing tags are -left at the default value. The version tag is not required, but is -strongly recommended. - -The bug and comment UUIDs are always auto-generated, so if you set a - field, but no field, your will be used as the -comment's . An exception is raised if conflicts with -an existing comment. Bugs do not have a permantent alt-id, so they -the s you specify are not saved. The s _are_ used to -match agains prexisting bug and comment uuids, and comment alt-ids, -and fields explicitly given in the XML file will replace old versions -unless the --add-only flag. - -*.extra_strings recieves special treatment, and if --add-only is not -set, the resulting list concatenates both source lists and removes -repeats. - -Here's an example of import activity: - Repository - bug (uuid=B, creator=John, status=open) - estr (don't forget your towel) - estr (helps with space travel) - com (uuid=C1, author=Jane, body=Hello) - com (uuid=C2, author=Jess, body=World) - XML - bug (uuid=B, status=fixed) - estr (don't forget your towel) - estr (watch out for flying dolphins) - com (uuid=C1, body=So long) - com (uuid=C3, author=Jed, body=And thanks) - Result - bug (uuid=B, creator=John, status=fixed) - estr (don't forget your towel) - estr (helps with space travel) - estr (watch out for flying dolphins) - com (uuid=C1, author=Jane, body=So long) - com (uuid=C2, author=Jess, body=World) - com (uuid=C4, alt-id=C3, author=Jed, body=And thanks) - Result, with --add-only - bug (uuid=B, creator=John, status=open) - estr (don't forget your towel) - estr (helps with space travel) - com (uuid=C1, author=Jane, body=Hello) - com (uuid=C2, author=Jess, body=World) - com (uuid=C4, alt-id=C3, author=Jed, body=And thanks) - -Examples: - -Import comments (e.g. emails from an mbox) and append to bug XYZ - $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ - -Or you can append those emails underneath the prexisting comment XYZ-3 - $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ-3 - - -User creates a new bug - user$ be new "The demuxulizer is broken" - Created bug with ID 48f - user$ be comment 48f - - ... -User exports bug as xml and emails it to the developers - user$ be show --xml 48f > 48f.xml - user$ cat 48f.xml | mail -s "Demuxulizer bug xml" devs@b.com -or equivalently (with a slightly fancier be-handle-mail compatible -email): - user$ be email-bugs 48f -Devs recieve email, and save it's contents as demux-bug.xml - dev$ cat demux-bug.xml | be import-xml - -""" - -def help(): - return get_parser().help_str() + longhelp - - -if libbe.TESTING == True: - class LonghelpTestCase (unittest.TestCase): - """ - Test import scenarios given in longhelp. - """ - def setUp(self): - self.bugdir = bugdir.SimpleBugDir() - self.original_working_dir = os.getcwd() - os.chdir(self.bugdir.root) - bugA = self.bugdir.bug_from_uuid('a') - self.bugdir.remove_bug(bugA) - self.bugdir.set_sync_with_disk(False) - bugB = self.bugdir.bug_from_uuid('b') - bugB.creator = 'John' - bugB.status = 'open' - bugB.extra_strings += ["don't forget your towel"] - bugB.extra_strings += ['helps with space travel'] - comm1 = bugB.comment_root.new_reply(body='Hello\n') - comm1.uuid = 'c1' - comm1.author = 'Jane' - comm2 = bugB.comment_root.new_reply(body='World\n') - comm2.uuid = 'c2' - comm2.author = 'Jess' - bugB.save() - self.bugdir.set_sync_with_disk(True) - self.xml = """ - - - b - fixed - a test bug - don't forget your towel - watch out for flying dolphins - - c1 - So long - - - c3 - Jed - And thanks - - - - """ - def tearDown(self): - os.chdir(self.original_working_dir) - self.bugdir.cleanup() - def _execute(self, *args): - import StringIO - orig_stdin = sys.stdin - sys.stdin = StringIO.StringIO(self.xml) - execute(list(args)+["-"], manipulate_encodings=False, - restrict_file_access=True) - sys.stdin = orig_stdin - self.bugdir._clear_bugs() - def testCleanBugdir(self): - uuids = list(self.bugdir.uuids()) - self.failUnless(uuids == ['b'], uuids) - def testNotAddOnly(self): - self._execute() - uuids = list(self.bugdir.uuids()) - self.failUnless(uuids == ['b'], uuids) - bugB = self.bugdir.bug_from_uuid('b') - self.failUnless(bugB.uuid == 'b', bugB.uuid) - self.failUnless(bugB.creator == 'John', bugB.creator) - self.failUnless(bugB.status == 'fixed', bugB.status) - estrs = ["don't forget your towel", - 'helps with space travel', - 'watch out for flying dolphins'] - self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) - comments = list(bugB.comments()) - self.failUnless(len(comments) == 3, - ['%s (%s, %s)' % (c.uuid, c.alt_id, c.body) - for c in comments]) - c1 = bugB.comment_from_uuid('c1') - comments.remove(c1) - self.failUnless(c1.uuid == 'c1', c1.uuid) - self.failUnless(c1.alt_id == None, c1.alt_id) - self.failUnless(c1.author == 'Jane', c1.author) - self.failUnless(c1.body == 'So long\n', c1.body) - c2 = bugB.comment_from_uuid('c2') - comments.remove(c2) - self.failUnless(c2.uuid == 'c2', c2.uuid) - self.failUnless(c2.alt_id == None, c2.alt_id) - self.failUnless(c2.author == 'Jess', c2.author) - self.failUnless(c2.body == 'World\n', c2.body) - c4 = comments[0] - self.failUnless(len(c4.uuid) == 36, c4.uuid) - self.failUnless(c4.alt_id == 'c3', c4.alt_id) - self.failUnless(c4.author == 'Jed', c4.author) - self.failUnless(c4.body == 'And thanks\n', c4.body) - def testAddOnly(self): - self._execute('--add-only') - uuids = list(self.bugdir.uuids()) - self.failUnless(uuids == ['b'], uuids) - bugB = self.bugdir.bug_from_uuid('b') - self.failUnless(bugB.uuid == 'b', bugB.uuid) - self.failUnless(bugB.creator == 'John', bugB.creator) - self.failUnless(bugB.status == 'open', bugB.status) - estrs = ["don't forget your towel", - 'helps with space travel'] - self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) - comments = list(bugB.comments()) - self.failUnless(len(comments) == 3, - ['%s (%s)' % (c.uuid, c.alt_id) for c in comments]) - c1 = bugB.comment_from_uuid('c1') - comments.remove(c1) - self.failUnless(c1.uuid == 'c1', c1.uuid) - self.failUnless(c1.alt_id == None, c1.alt_id) - self.failUnless(c1.author == 'Jane', c1.author) - self.failUnless(c1.body == 'Hello\n', c1.body) - c2 = bugB.comment_from_uuid('c2') - comments.remove(c2) - self.failUnless(c2.uuid == 'c2', c2.uuid) - self.failUnless(c2.alt_id == None, c2.alt_id) - self.failUnless(c2.author == 'Jess', c2.author) - self.failUnless(c2.body == 'World\n', c2.body) - c4 = comments[0] - self.failUnless(len(c4.uuid) == 36, c4.uuid) - self.failUnless(c4.alt_id == 'c3', c4.alt_id) - self.failUnless(c4.author == 'Jed', c4.author) - self.failUnless(c4.body == 'And thanks\n', c4.body) - - unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) - suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/becommands/init.py b/becommands/init.py deleted file mode 100644 index ab9255b..0000000 --- a/becommands/init.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Assign the root directory for bug tracking""" -import os.path -from libbe import cmdutil, bugdir -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> from libbe import utility, vcs - >>> import os - >>> dir = utility.Dir() - >>> try: - ... bugdir.BugDir(dir.path) - ... except bugdir.NoBugDir, e: - ... True - True - >>> execute([], manipulate_encodings=False, dir=dir.path) - No revision control detected. - Directory initialized. - >>> dir.cleanup() - - >>> dir = utility.Dir() - >>> os.chdir(dir.path) - >>> _vcs = vcs.installed_vcs() - >>> _vcs.init('.') - >>> _vcs.name in vcs.VCS_ORDER - True - >>> execute([], manipulate_encodings=False) # doctest: +ELLIPSIS - Using ... for revision control. - Directory initialized. - >>> _vcs.cleanup() - - >>> try: - ... execute([], manipulate_encodings=False, dir=".") - ... except cmdutil.UserError, e: - ... str(e).startswith("Directory already initialized: ") - True - >>> execute([], manipulate_encodings=False, - ... dir='/highly-unlikely-to-exist') - Traceback (most recent call last): - UserError: No such directory: /highly-unlikely-to-exist - >>> os.chdir('/') - >>> dir.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser) - if len(args) > 0: - raise cmdutil.UsageError - try: - bd = bugdir.BugDir(from_disk=False, - sink_to_existing_root=False, - assert_new_BugDir=True, - manipulate_encodings=manipulate_encodings, - root=dir) - except bugdir.NoRootEntry: - raise cmdutil.UserError("No such directory: %s" % dir) - except bugdir.AlreadyInitialized: - raise cmdutil.UserError("Directory already initialized: %s" % dir) - bd.save() - if bd.vcs.name is not "None": - print "Using %s for revision control." % bd.vcs.name - else: - print "No revision control detected." - print "Directory initialized." - -def get_parser(): - parser = cmdutil.CmdOptionParser("be init") - return parser - -longhelp=""" -This command initializes Bugs Everywhere support for the specified directory -and all its subdirectories. It will auto-detect any supported revision control -system. You can use "be set vcs_name" to change the vcs being used. - -The directory defaults to your current working directory, but you can -change that by passing the --dir option to be - $ be --dir path/to/new/bug/root init - -It is usually a good idea to put the Bugs Everywhere root at the source code -root, but you can put it anywhere. If you root Bugs Everywhere in a -subdirectory, then only bugs created in that subdirectory (and its children) -will appear there. -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/list.py b/becommands/list.py deleted file mode 100644 index 1c3e78d..0000000 --- a/becommands/list.py +++ /dev/null @@ -1,240 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# Oleg Romanyshyn -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""List bugs""" -from libbe import cmdutil, bugdir, bug -import os -import re -__desc__ = __doc__ - -# get a list of * for cmp_*() comparing two bugs. -AVAILABLE_CMPS = [fn[4:] for fn in dir(bug) if fn[:4] == 'cmp_'] -AVAILABLE_CMPS.remove("attr") # a cmp_* template. - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute([], manipulate_encodings=False) - a:om: Bug A - >>> execute(["--status", "closed"], manipulate_encodings=False) - b:cm: Bug B - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - if len(args) > 0: - raise cmdutil.UsageError("Too many arguments.") - cmp_list = [] - if options.sort_by != None: - for cmp in options.sort_by.split(','): - if cmp not in AVAILABLE_CMPS: - raise cmdutil.UserError( - "Invalid sort on '%s'.\nValid sorts:\n %s" - % (cmp, '\n '.join(AVAILABLE_CMPS))) - cmp_list.append(eval('bug.cmp_%s' % cmp)) - - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bd.load_all_bugs() - # select status - if options.status != None: - if options.status == "all": - status = bug.status_values - else: - status = cmdutil.select_values(options.status, bug.status_values) - else: - status = [] - if options.active == True: - status.extend(list(bug.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 = bug.active_status_values - # select severity - if options.severity != None: - if options.severity == "all": - severity = bug.severity_values - else: - severity = cmdutil.select_values(options.severity, - bug.severity_values) - else: - severity = [] - if options.wishlist == True: - severity.extend("wishlist") - if options.important == True: - serious = bug.severity_values.index("serious") - severity.append(list(bug.severity_values[serious:])) - if severity == []: # set the default value - severity = bug.severity_values - # select assigned - if options.assigned != None: - if options.assigned == "all": - assigned = "all" - else: - possible_assignees = [] - for _bug in bd: - if _bug.assigned != None \ - and not _bug.assigned in possible_assignees: - possible_assignees.append(_bug.assigned) - assigned = cmdutil.select_values(options.assigned, - possible_assignees) - print 'assigned', assigned - 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] = bd.user_id - if options.extra_strings != None: - extra_string_regexps = [re.compile(x) for x in options.extra_strings.split(',')] - - def filter(bug): - if status != "all" and not bug.status in status: - return False - if severity != "all" and not bug.severity in severity: - return False - if assigned != "all" and not bug.assigned in assigned: - return False - if options.extra_strings != None: - if len(bug.extra_strings) == 0 and len(extra_string_regexps) > 0: - return False - for string in bug.extra_strings: - for regexp in extra_string_regexps: - if not regexp.match(string): - return False - return True - - bugs = [b for b in bd if filter(b) ] - if len(bugs) == 0 and options.xml == False: - print "No matching bugs found" - - def list_bugs(cur_bugs, title=None, just_uuids=False, xml=False): - if xml == True: - print '' % bd.encoding - print "" - if len(cur_bugs) > 0: - if title != None and xml == False: - print cmdutil.underlined(title) - for bg in cur_bugs: - if xml == True: - print bg.xml(show_comments=True) - elif just_uuids: - print bg.uuid - else: - print bg.string(shortlist=True) - if xml == True: - print "" - - # sort bugs - cmp_list.extend(bug.DEFAULT_CMP_FULL_CMP_LIST) - cmp_fn = bug.BugCompoundComparator(cmp_list=cmp_list) - bugs.sort(cmp_fn) - - # print list of bugs - list_bugs(bugs, just_uuids=options.uuids, xml=options.xml) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be list [options]") - parser.add_option("--status", dest="status", metavar="STATUS", - help="Only show bugs matching the STATUS specifier") - parser.add_option("--severity", dest="severity", metavar="SEVERITY", - help="Only show bugs matching the SEVERITY specifier") - parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", - help="List bugs matching ASSIGNED", default=None) - parser.add_option("-e", "--extra-strings", metavar="STRINGS", dest="extra_strings", - help="List bugs matching _all_ extra strings in comma-seperated list STRINGS. e.g. --extra-strings TAG:working,TAG:xml", default=None) - parser.add_option("-S", "--sort", metavar="SORT-BY", dest="sort_by", - help="Adjust bug-sort criteria with comma-separated list SORT-BY. e.g. \"--sort creator,time\". Available criteria: %s" % ','.join(AVAILABLE_CMPS), default=None) - # boolean options. All but uuids and xml are special cases of long forms - bools = (("u", "uuids", "Only print the bug UUIDS"), - ("x", "xml", "Dump as XML"), - ("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")) - 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, default=False) - return parser - - -def help(): - longhelp=""" -This command lists bugs. Normally it prints a short string like - 576:om: Allow attachments -Where - 576 the bug id - o the bug status is 'open' (first letter) - m the bug severity is 'minor' (first letter) - Allo... the bug summary string - -You can optionally (-u) print only the bug ids. - -There are several criteria that you can filter by: - * status - * severity - * assigned (who the bug is assigned to) -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. As with the --status and ---severity options for `be depend`, starting the list with a minus -sign makes your selections a blacklist instead of the default -whitelist. - -status - %s -severity - %s -assigned - free form, with the string '-' being a shortcut for yourself. - -In addition, there are some shortcut options that set boolean flags. -The boolean options are ignored if the matching string option is used. -""" % (','.join(bug.status_values), - ','.join(bug.severity_values)) - return get_parser().help_str() + longhelp - -def complete(options, args, parser): - for option, value in cmdutil.option_value_pairs(options, parser): - if value == "--complete": - if option == "status": - raise cmdutil.GetCompletions(bug.status_values) - elif option == "severity": - raise cmdutil.GetCompletions(bug.severity_values) - raise cmdutil.GetCompletions() - if "--complete" in args: - raise cmdutil.GetCompletions() # no positional arguments for list diff --git a/becommands/merge.py b/becommands/merge.py deleted file mode 100644 index ac09b40..0000000 --- a/becommands/merge.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Merge duplicate bugs""" -from libbe import cmdutil, bugdir -import os, copy -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> from libbe import utility - >>> bd = bugdir.SimpleBugDir() - >>> bd.set_sync_with_disk(True) - >>> a = bd.bug_from_shortname("a") - >>> a.comment_root.time = 0 - >>> dummy = a.new_comment("Testing") - >>> dummy.time = 1 - >>> dummy = dummy.new_reply("Testing...") - >>> dummy.time = 2 - >>> b = bd.bug_from_shortname("b") - >>> b.status = "open" - >>> b.comment_root.time = 0 - >>> dummy = b.new_comment("1 2") - >>> dummy.time = 1 - >>> dummy = dummy.new_reply("1 2 3 4") - >>> dummy.time = 2 - >>> os.chdir(bd.root) - >>> execute(["a", "b"], manipulate_encodings=False) - Merging bugs a and b - >>> bd._clear_bugs() - >>> a = bd.bug_from_shortname("a") - >>> a.load_comments() - >>> mergeA = a.comment_from_shortname(":3") - >>> mergeA.time = 3 - >>> print a.string(show_comments=True) # doctest: +ELLIPSIS - ID : a - Short name : a - Severity : minor - Status : open - Assigned : - Reporter : - Creator : John Doe - Created : ... - Bug A - --------- Comment --------- - Name: a:1 - From: ... - Date: ... - - Testing - --------- Comment --------- - Name: a:2 - From: ... - Date: ... - - Testing... - --------- Comment --------- - Name: a:3 - From: ... - Date: ... - - Merged from bug b - --------- Comment --------- - Name: a:4 - From: ... - Date: ... - - 1 2 - --------- Comment --------- - Name: a:5 - From: ... - Date: ... - - 1 2 3 4 - >>> b = bd.bug_from_shortname("b") - >>> b.load_comments() - >>> mergeB = b.comment_from_shortname(":3") - >>> mergeB.time = 3 - >>> print b.string(show_comments=True) # doctest: +ELLIPSIS - ID : b - Short name : b - Severity : minor - Status : closed - Assigned : - Reporter : - Creator : Jane Doe - Created : ... - Bug B - --------- Comment --------- - Name: b:1 - From: ... - Date: ... - - 1 2 - --------- Comment --------- - Name: b:2 - From: ... - Date: ... - - 1 2 3 4 - --------- Comment --------- - Name: b:3 - From: ... - Date: ... - - Merged into bug a - >>> print b.status - closed - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True, - 1: lambda bug : bug.active==True}) - - if len(args) < 2: - raise cmdutil.UsageError("Please specify two bug ids.") - if len(args) > 2: - help() - raise cmdutil.UsageError("Too many arguments.") - - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bugA = cmdutil.bug_from_id(bd, args[0]) - bugA.load_comments() - bugB = cmdutil.bug_from_id(bd, args[1]) - bugB.load_comments() - mergeA = bugA.new_comment("Merged from bug %s" % bugB.uuid) - newCommTree = copy.deepcopy(bugB.comment_root) - for comment in newCommTree.traverse(): # all descendant comments - comment.bug = bugA - comment.save() # force onto disk under bugA - for comment in newCommTree: # just the child comments - mergeA.add_reply(comment, allow_time_inversion=True) - bugB.new_comment("Merged into bug %s" % bugA.uuid) - bugB.status = "closed" - print "Merging bugs %s and %s" % (bugA.uuid, bugB.uuid) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be merge BUG-ID BUG-ID") - return parser - -longhelp=""" -The second bug (B) is merged into the first (A). This adds merge -comments to both bugs, closes B, and appends B's comment tree to A's -merge comment. -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/new.py b/becommands/new.py deleted file mode 100644 index 30f8834..0000000 --- a/becommands/new.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Create a new bug""" -from libbe import cmdutil, bugdir -import sys -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os, time - >>> from libbe import bug - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> bug.uuid_gen = lambda: "X" - >>> execute (["this is a test",], manipulate_encodings=False) - Created bug with ID X - >>> bd._clear_bugs() - >>> bug = bd.bug_from_uuid("X") - >>> print bug.summary - this is a test - >>> bug.time <= int(time.time()) - True - >>> print bug.severity - minor - >>> print bug.status - open - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser) - if len(args) != 1: - raise cmdutil.UsageError("Please supply a summary message") - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if args[0] == '-': # read summary from stdin - summary = sys.stdin.readline() - else: - summary = args[0] - bug = bd.new_bug(summary=summary.strip()) - if options.reporter != None: - bug.reporter = options.reporter - else: - bug.reporter = bug.creator - if options.assigned != None: - bug.assigned = options.assigned - elif bd.default_assignee != None: - bug.assigned = bd.default_assignee - print "Created bug with ID %s" % bd.bug_shortname(bug) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be new SUMMARY") - parser.add_option("-r", "--reporter", metavar="REPORTER", dest="reporter", - help="The user who reported the bug", default=None) - parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", - help="The developer in charge of the bug", default=None) - return parser - -longhelp=""" -Create a new bug, with a new ID. The summary specified on the -commandline is a string (only one line) that describes the bug briefly -or "-", in which case the string will be read from stdin. -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/open.py b/becommands/open.py deleted file mode 100644 index a6fe48d..0000000 --- a/becommands/open.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# Marien Zwart -# Thomas Gerigk -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Re-open a bug""" -from libbe import cmdutil, bugdir -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> print bd.bug_from_shortname("b").status - closed - >>> execute(["b"], manipulate_encodings=False) - >>> bd._clear_bugs() - >>> print bd.bug_from_shortname("b").status - open - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==False}) - if len(args) == 0: - raise cmdutil.UsageError, "Please specify a bug id." - if len(args) > 1: - raise cmdutil.UsageError, "Too many arguments." - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bug = cmdutil.bug_from_id(bd, args[0]) - bug.status = "open" - -def get_parser(): - parser = cmdutil.CmdOptionParser("be open BUG-ID") - return parser - -longhelp=""" -Mark a bug as 'open'. -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/remove.py b/becommands/remove.py deleted file mode 100644 index bac06c0..0000000 --- a/becommands/remove.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Remove (delete) a bug and its comments""" -from libbe import cmdutil, bugdir -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> from libbe import mapfile - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> print bd.bug_from_shortname("b").status - closed - >>> execute (["b"], manipulate_encodings=False) - Removed bug b - >>> bd._clear_bugs() - >>> try: - ... bd.bug_from_shortname("b") - ... except bugdir.NoBugMatches: - ... print "Bug not found" - Bug not found - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) - if len(args) != 1: - raise cmdutil.UsageError, "Please specify a bug id." - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bug = cmdutil.bug_from_id(bd, args[0]) - bd.remove_bug(bug) - print "Removed bug %s" % bug.uuid - -def get_parser(): - parser = cmdutil.CmdOptionParser("be remove BUG-ID") - return parser - -longhelp=""" -Remove (delete) an existing bug. Use with caution: if you're not using a -revision control system, there may be no way to recover the lost information. -You should use this command, for example, to get rid of blank or otherwise -mangled bugs. -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/set.py b/becommands/set.py deleted file mode 100644 index 4d54a59..0000000 --- a/becommands/set.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# Marien Zwart -# Thomas Gerigk -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Change tree settings""" -import textwrap -from libbe import cmdutil, bugdir, vcs, settings_object -__desc__ = __doc__ - -def _value_string(bd, setting): - val = bd.settings.get(setting, settings_object.EMPTY) - if val == settings_object.EMPTY: - default = getattr(bd, bd._setting_name_to_attr_name(setting)) - if default not in [None, settings_object.EMPTY]: - val = "None (%s)" % default - else: - val = None - return str(val) - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute(["target"], manipulate_encodings=False) - None - >>> execute(["target", "tomorrow"], manipulate_encodings=False) - >>> execute(["target"], manipulate_encodings=False) - tomorrow - >>> execute(["target", "none"], manipulate_encodings=False) - >>> execute(["target"], manipulate_encodings=False) - None - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - if len(args) > 2: - raise cmdutil.UsageError, "Too many arguments" - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if len(args) == 0: - keys = bd.settings_properties - keys.sort() - for key in keys: - print "%16s: %s" % (key, _value_string(bd, key)) - elif len(args) == 1: - print _value_string(bd, args[0]) - else: - if args[1] == "none": - setattr(bd, args[0], settings_object.EMPTY) - else: - if args[0] not in bd.settings_properties: - msg = "Invalid setting %s\n" % args[0] - msg += 'Allowed settings:\n ' - msg += '\n '.join(bd.settings_properties) - raise cmdutil.UserError(msg) - old_setting = bd.settings.get(args[0]) - setattr(bd, args[0], args[1]) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be set [NAME] [VALUE]") - return parser - -def get_bugdir_settings(): - settings = [] - for s in bugdir.BugDir.settings_properties: - settings.append(s) - settings.sort() - documented_settings = [] - for s in settings: - set = getattr(bugdir.BugDir, s) - dstr = set.__doc__.strip() - # per-setting comment adjustments - if s == "vcs_name": - lines = dstr.split('\n') - while lines[0].startswith("This property defaults to") == False: - lines.pop(0) - assert len(lines) != None, \ - "Unexpected vcs_name docstring:\n '%s'" % dstr - lines.insert( - 0, "The name of the revision control system to use.\n") - dstr = '\n'.join(lines) - doc = textwrap.wrap(dstr, width=70, initial_indent=' ', - subsequent_indent=' ') - documented_settings.append("%s\n%s" % (s, '\n'.join(doc))) - return documented_settings - -longhelp=""" -Show or change per-tree settings. - -If name and value are supplied, the name is set to a new value. -If no value is specified, the current value is printed. -If no arguments are provided, all names and values are listed. - -To unset a setting, set it to "none". - -Allowed settings are: - -%s""" % ('\n'.join(get_bugdir_settings()),) - -def help(): - return get_parser().help_str() + longhelp - -def complete(options, args, parser): - for option, value in cmdutil.option_value_pairs(options, parser): - if value == "--complete": - # no argument-options at the moment, so this is future-proofing - raise cmdutil.GetCompletions() - for pos,value in enumerate(args): - if value == "--complete": - if pos == 0: # first positional argument is a setting name - props = bugdir.BugDir.settings_properties - raise cmdutil.GetCompletions(props) - raise cmdutil.GetCompletions() # no positional arguments for list diff --git a/becommands/severity.py b/becommands/severity.py deleted file mode 100644 index 804dc4e..0000000 --- a/becommands/severity.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# Marien Zwart -# Thomas Gerigk -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Show or change a bug's severity level""" -from libbe import cmdutil, bugdir, bug -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute(["a"], manipulate_encodings=False) - minor - >>> execute(["a", "wishlist"], manipulate_encodings=False) - >>> execute(["a"], manipulate_encodings=False) - wishlist - >>> execute(["a", "none"], manipulate_encodings=False) - Traceback (most recent call last): - UserError: Invalid severity level: none - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - if len(args) not in (1,2): - raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bug = cmdutil.bug_from_id(bd, args[0]) - if len(args) == 1: - print bug.severity - elif len(args) == 2: - try: - bug.severity = args[1] - except ValueError, e: - if e.name != "severity": - raise e - raise cmdutil.UserError ("Invalid severity level: %s" % e.value) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be severity BUG-ID [SEVERITY]") - return parser - -def help(): - 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: -"""] - try: # See if there are any per-tree severity configurations - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False) - except bugdir.NoBugDir, e: - pass # No tree, just show the defaults - longest_severity_len = max([len(s) for s in bug.severity_values]) - for severity in bug.severity_values : - description = bug.severity_description[severity] - s = "%*s : %s\n" % (longest_severity_len, severity, description) - longhelp.append(s) - longhelp = ''.join(longhelp) - return get_parser().help_str() + longhelp - -def complete(options, args, parser): - for option,value in cmdutil.option_value_pairs(options, parser): - if value == "--complete": - # no argument-options at the moment, so this is future-proofing - raise cmdutil.GetCompletions() - for pos,value in enumerate(args): - if value == "--complete": - try: # See if there are any per-tree severity configurations - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=False) - except bugdir.NoBugDir: - bd = None - if pos == 0: # fist positional argument is a bug id - ids = [] - if bd != None: - bd.load_all_bugs() - filter = lambda bg : bg.active==True - bugs = [bg for bg in bd if filter(bg)==True] - ids = [bd.bug_shortname(bg) for bg in bugs] - raise cmdutil.GetCompletions(ids) - elif pos == 1: # second positional argument is a severity - raise cmdutil.GetCompletions(bug.severity_values) - raise cmdutil.GetCompletions() diff --git a/becommands/show.py b/becommands/show.py deleted file mode 100644 index 7757aaa..0000000 --- a/becommands/show.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# Thomas Gerigk -# Thomas Habets -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Show a particular bug, comment, or combination of both.""" -import sys -from libbe import cmdutil, bugdir, comment, version, _version -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute (["a",], manipulate_encodings=False) # doctest: +ELLIPSIS - ID : a - Short name : a - Severity : minor - Status : open - Assigned : - Reporter : - Creator : John Doe - Created : ... - Bug A - - >>> execute (["--xml", "a"], manipulate_encodings=False) # doctest: +ELLIPSIS - - - - ... - ... - ... - ... - - - a - a - minor - open - John Doe <jdoe@example.com> - Thu, 01 Jan 1970 00:00:00 +0000 - Bug A - - - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={-1: lambda bug : bug.active==True}) - if len(args) == 0: - raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - - if options.only_raw_body == True: - if len(args) != 1: - raise cmdutil.UsageError( - 'only one ID accepted with --only-raw-body') - bug,comment = cmdutil.bug_comment_from_id(bd, args[0]) - if comment == bug.comment_root: - raise cmdutil.UsageError( - "--only-raw-body requires a comment ID, not '%s'" % args[0]) - sys.__stdout__.write(comment.body) - sys.exit(0) - print output(args, bd, as_xml=options.XML, with_comments=options.comments) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be show [options] ID [ID ...]") - parser.add_option("-x", "--xml", action="store_true", default=False, - dest='XML', help="Dump as XML") - parser.add_option("--only-raw-body", action="store_true", - dest='only_raw_body', - help="When printing only a single comment, just print it's body. This allows extraction of non-text content types.") - parser.add_option("-c", "--no-comments", dest="comments", - action="store_false", default=True, - help="Disable comment output. This is useful if you just want more details on a bug's current status.") - return parser - -longhelp=""" -Show all information about the bugs or comments whose IDs are given. - -Without the --xml flag set, it's probably not a good idea to mix bug -and comment IDs in a single call, but you're free to do so if you -like. With the --xml flag set, there will never be any root comments, -so mix and match away (the bug listings for directly requested -comments will be restricted to the bug uuid and the requested -comment(s)). - -Directly requested comments will be grouped by their parent bug and -placed at the end of the output, so the ordering may not match the -order of the listed IDs. -""" - -def help(): - return get_parser().help_str() + longhelp - -def _sort_ids(ids, with_comments=True): - bugs = [] - root_comments = {} - for id in ids: - bugname,commname = cmdutil.parse_id(id) - if commname == None: - bugs.append(bugname) - elif with_comments == True: - if bugname not in root_comments: - root_comments[bugname] = [commname] - else: - root_comments[bugname].append(commname) - for bugname in root_comments.keys(): - assert bugname not in bugs, \ - "specifically requested both '%s%s' and '%s'" \ - % (bugname, root_comments[bugname][0], bugname) - return (bugs, root_comments) - -def _xml_header(encoding): - lines = ['' % encoding, - '', - ' ', - ' %s' % version.version()] - for tag in ['branch-nick', 'revno', 'revision-id']: - value = _version.version_info[tag.replace('-', '_')] - lines.append(' <%s>%s' % (tag, value, tag)) - lines.append(' ') - return lines - -def _xml_footer(): - return [''] - -def output(ids, bd, as_xml=True, with_comments=True): - bugs,root_comments = _sort_ids(ids, with_comments) - lines = [] - if as_xml: - lines.extend(_xml_header(bd.encoding)) - else: - spaces_left = len(ids) - 1 - for bugname in bugs: - bug = cmdutil.bug_from_id(bd, bugname) - if as_xml: - lines.append(bug.xml(indent=2, show_comments=with_comments)) - else: - lines.append(bug.string(show_comments=with_comments)) - if spaces_left > 0: - spaces_left -= 1 - lines.append('') # add a blank line between bugs/comments - for bugname,comments in root_comments.items(): - bug = cmdutil.bug_from_id(bd, bugname) - if as_xml: - lines.extend([' ', ' %s' % bug.uuid]) - for commname in comments: - try: - comment = bug.comment_root.comment_from_shortname(commname) - except comment.InvalidShortname, e: - raise UserError(e.message) - if as_xml: - lines.append(comment.xml(indent=4, shortname=bugname)) - else: - lines.append(comment.string(shortname=bugname)) - if spaces_left > 0: - spaces_left -= 1 - lines.append('') # add a blank line between bugs/comments - if as_xml: - lines.append('') - if as_xml: - lines.extend(_xml_footer()) - return '\n'.join(lines) diff --git a/becommands/status.py b/becommands/status.py deleted file mode 100644 index 58b6f63..0000000 --- a/becommands/status.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Show or change a bug's status""" -from libbe import cmdutil, bugdir, bug -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute(["a"], manipulate_encodings=False) - open - >>> execute(["a", "closed"], manipulate_encodings=False) - >>> execute(["a"], manipulate_encodings=False) - closed - >>> execute(["a", "none"], manipulate_encodings=False) - Traceback (most recent call last): - UserError: Invalid status: none - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - if len(args) not in (1,2): - raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bug = cmdutil.bug_from_id(bd, args[0]) - if len(args) == 1: - print bug.status - else: - try: - bug.status = args[1] - except ValueError, e: - if e.name != "status": - raise - raise cmdutil.UserError ("Invalid status: %s" % e.value) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be status BUG-ID [STATUS]") - return parser - - -def help(): - try: # See if there are any per-tree status configurations - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=False) - except bugdir.NoBugDir, e: - pass # No tree, just show the defaults - longest_status_len = max([len(s) for s in bug.status_values]) - active_statuses = [] - for status in bug.active_status_values : - description = bug.status_description[status] - s = "%*s : %s" % (longest_status_len, status, description) - active_statuses.append(s) - inactive_statuses = [] - for status in bug.inactive_status_values : - description = bug.status_description[status] - s = "%*s : %s" % (longest_status_len, status, description) - inactive_statuses.append(s) - longhelp=""" -Show or change a bug's status. - -If no status is specified, the current value is printed. If a status -is specified, it will be assigned to the bug. - -There are two classes of statuses, active and inactive, which are only -important for commands like "be list" that show only active bugs by -default. - -Active status levels are: - %s -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. -""" % ('\n '.join(active_statuses), '\n '.join(inactive_statuses)) - return get_parser().help_str() + longhelp - -def complete(options, args, parser): - for option,value in cmdutil.option_value_pairs(options, parser): - if value == "--complete": - # no argument-options at the moment, so this is future-proofing - raise cmdutil.GetCompletions() - for pos,value in enumerate(args): - if value == "--complete": - try: # See if there are any per-tree status configurations - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=False) - except bugdir.NoBugDir: - bd = None - if pos == 0: # fist positional argument is a bug id - ids = [] - if bd != None: - bd.load_all_bugs() - ids = [bd.bug_shortname(bg) for bg in bd] - raise cmdutil.GetCompletions(ids) - elif pos == 1: # second positional argument is a status - raise cmdutil.GetCompletions(bug.status_values) - raise cmdutil.GetCompletions() diff --git a/becommands/subscribe.py b/becommands/subscribe.py deleted file mode 100644 index 69554f7..0000000 --- a/becommands/subscribe.py +++ /dev/null @@ -1,360 +0,0 @@ -# Copyright (C) 2009 W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""(Un)subscribe to change notification""" -from libbe import cmdutil, bugdir, tree, diff -import os, copy -__desc__ = __doc__ - -TAG="SUBSCRIBE:" - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> bd = bugdir.SimpleBugDir() - >>> bd.set_sync_with_disk(True) - >>> os.chdir(bd.root) - >>> a = bd.bug_from_shortname("a") - >>> print a.extra_strings - [] - >>> execute(["-s","John Doe ", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: - John Doe all * - >>> bd._clear_bugs() # resync our copy of bug - >>> a = bd.bug_from_shortname("a") - >>> print a.extra_strings - ['SUBSCRIBE:John Doe \\tall\\t*'] - >>> execute(["-s","Jane Doe ", "-S", "a.com,b.net", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: - Jane Doe all a.com,b.net - John Doe all * - >>> execute(["-s","Jane Doe ", "-S", "a.edu", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: - Jane Doe all a.com,a.edu,b.net - John Doe all * - >>> execute(["-u", "-s","Jane Doe ", "-S", "a.com", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: - Jane Doe all a.edu,b.net - John Doe all * - >>> execute(["-s","Jane Doe ", "-S", "*", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: - Jane Doe all * - John Doe all * - >>> execute(["-u", "-s","Jane Doe ", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: - John Doe all * - >>> execute(["-u", "-s","John Doe ", "a"], manipulate_encodings=False) - >>> execute(["-s","Jane Doe ", "-t", "new", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for bug directory: - Jane Doe new * - >>> execute(["-s","Jane Doe ", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for bug directory: - Jane Doe all * - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) - - if len(args) > 1: - help() - raise cmdutil.UsageError("Too many arguments.") - - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - - subscriber = options.subscriber - if subscriber == None: - subscriber = bd.user_id - if options.unsubscribe == True: - if options.servers == None: - options.servers = "INVALID" - if options.types == None: - options.types = "INVALID" - else: - if options.servers == None: - options.servers = "*" - if options.types == None: - options.types = "all" - servers = options.servers.split(",") - types = options.types.split(",") - - if len(args) == 0 or args[0] == diff.BUGDIR_ID: # directory-wide subscriptions - type_root = diff.BUGDIR_TYPE_ALL - entity = bd - entity_name = "bug directory" - else: # bug-specific subscriptions - type_root = diff.BUG_TYPE_ALL - bug = bd.bug_from_shortname(args[0]) - entity = bug - entity_name = bug.uuid - if options.list_all == True: - entity_name = "anything in the bug directory" - - types = [diff.type_from_name(name, type_root, default=diff.INVALID_TYPE, - default_ok=options.unsubscribe) - for name in types] - estrs = entity.extra_strings - if options.list == True or options.list_all == True: - pass - else: # alter subscriptions - if options.unsubscribe == True: - estrs = unsubscribe(estrs, subscriber, types, servers, type_root) - else: # add the tag - estrs = subscribe(estrs, subscriber, types, servers, type_root) - entity.extra_strings = estrs # reassign to notice change - - if options.list_all == True: - bd.load_all_bugs() - subscriptions = get_bugdir_subscribers(bd, servers[0]) - else: - subscriptions = [] - for estr in entity.extra_strings: - if estr.startswith(TAG): - subscriptions.append(estr[len(TAG):]) - - if len(subscriptions) > 0: - print "Subscriptions for %s:" % entity_name - print '\n'.join(subscriptions) - - -def get_parser(): - parser = cmdutil.CmdOptionParser("be subscribe ID") - parser.add_option("-u", "--unsubscribe", action="store_true", - dest="unsubscribe", default=False, - help="Unsubscribe instead of subscribing.") - parser.add_option("-a", "--list-all", action="store_true", - dest="list_all", default=False, - help="List all subscribers (no ID argument, read only action).") - parser.add_option("-l", "--list", action="store_true", - dest="list", default=False, - help="List subscribers (read only action).") - parser.add_option("-s", "--subscriber", dest="subscriber", - metavar="SUBSCRIBER", - help="Email address of the subscriber (defaults to bugdir.user_id).") - parser.add_option("-S", "--servers", dest="servers", metavar="SERVERS", - help="Servers from which you want notification.") - parser.add_option("-t", "--type", dest="types", metavar="TYPES", - help="Types of changes you wish to be notified about.") - return parser - -longhelp=""" -ID can be either a bug id, or blank/"DIR", in which case it refers to the -whole bug directory. - -SERVERS specifies the servers from which you would like to receive -notification. Multiple severs may be specified in a comma-separated -list, or you can use "*" to match all servers (the default). If you -have not selected a server, it should politely refrain from notifying -you of changes, although there is no way to guarantee this behavior. - -Available TYPES: - For bugs: -%s - For %s: -%s - -For unsubscription, any listed SERVERS and TYPES are removed from your -subscription. Either the catch-all server "*" or type "%s" will -remove SUBSCRIBER entirely from the specified ID. - -This command is intended for use primarily by public interfaces, since -if you're just hacking away on your private repository, you'll known -what's changed ;). This command just (un)sets the appropriate -subscriptions, and leaves it up to each interface to perform the -notification. -""" % (diff.BUG_TYPE_ALL.string_tree(6), diff.BUGDIR_ID, - diff.BUGDIR_TYPE_ALL.string_tree(6), - diff.BUGDIR_TYPE_ALL) - -def help(): - return get_parser().help_str() + longhelp - -# internal helper functions - -def _generate_string(subscriber, types, servers): - types = sorted([str(t) for t in types]) - servers = sorted(servers) - return "%s%s\t%s\t%s" % (TAG,subscriber,",".join(types),",".join(servers)) - -def _parse_string(string, type_root): - assert string.startswith(TAG), string - string = string[len(TAG):] - subscriber,types,servers = string.split("\t") - types = [diff.type_from_name(name, type_root) for name in types.split(",")] - return (subscriber,types,servers.split(",")) - -def _get_subscriber(extra_strings, subscriber, type_root): - for i,string in enumerate(extra_strings): - if string.startswith(TAG): - s,ts,srvs = _parse_string(string, type_root) - if s == subscriber: - return i,s,ts,srvs # match! - return None # no match - -# functions exposed to other modules - -def subscribe(extra_strings, subscriber, types, servers, type_root): - args = _get_subscriber(extra_strings, subscriber, type_root) - if args == None: # no match - extra_strings.append(_generate_string(subscriber, types, servers)) - return extra_strings - # Alter matched string - i,s,ts,srvs = args - for t in types: - if t not in ts: - ts.append(t) - # remove descendant types - all_ts = copy.copy(ts) - for t in all_ts: - for tt in all_ts: - if tt in ts and t.has_descendant(tt): - ts.remove(tt) - if "*" in servers+srvs: - srvs = ["*"] - else: - srvs = list(set(servers+srvs)) - extra_strings[i] = _generate_string(subscriber, ts, srvs) - return extra_strings - -def unsubscribe(extra_strings, subscriber, types, servers, type_root): - args = _get_subscriber(extra_strings, subscriber, type_root) - if args == None: # no match - return extra_strings # pass - # Remove matched string - i,s,ts,srvs = args - all_ts = copy.copy(ts) - for t in types: - for tt in all_ts: - if tt in ts and t.has_descendant(tt): - ts.remove(tt) - if "*" in servers+srvs: - srvs = [] - else: - for srv in servers: - if srv in srvs: - srvs.remove(srv) - if len(ts) == 0 or len(srvs) == 0: - extra_strings.pop(i) - else: - extra_strings[i] = _generate_string(subscriber, ts, srvs) - return extra_strings - -def get_subscribers(extra_strings, type, server, type_root, - match_ancestor_types=False, - match_descendant_types=False): - """ - Set match_ancestor_types=True if you want to find eveyone who - cares about your particular type. - - Set match_descendant_types=True if you want to find subscribers - who may only care about some subset of your type. This is useful - for generating lists of all the subscribers in a given set of - extra_strings. - - >>> def sgs(*args, **kwargs): - ... return sorted(get_subscribers(*args, **kwargs)) - >>> es = [] - >>> es = subscribe(es, "John Doe ", [diff.BUGDIR_TYPE_ALL], - ... ["a.com"], diff.BUGDIR_TYPE_ALL) - >>> es = subscribe(es, "Jane Doe ", [diff.BUGDIR_TYPE_NEW], - ... ["*"], diff.BUGDIR_TYPE_ALL) - >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL) - ['John Doe '] - >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL, - ... match_descendant_types=True) - ['Jane Doe ', 'John Doe '] - >>> sgs(es, diff.BUGDIR_TYPE_ALL, "b.net", diff.BUGDIR_TYPE_ALL, - ... match_descendant_types=True) - ['Jane Doe '] - >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL) - ['Jane Doe '] - >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL, - ... match_ancestor_types=True) - ['Jane Doe ', 'John Doe '] - """ - for string in extra_strings: - if not string.startswith(TAG): - continue - subscriber,types,servers = _parse_string(string, type_root) - type_match = False - if type in types: - type_match = True - if type_match == False and match_ancestor_types == True: - for t in types: - if t.has_descendant(type): - type_match = True - break - if type_match == False and match_descendant_types == True: - for t in types: - if type.has_descendant(t): - type_match = True - break - server_match = False - if server in servers or servers == ["*"] or server == "*": - server_match = True - if type_match == True and server_match == True: - yield subscriber - -def get_bugdir_subscribers(bugdir, server): - """ - I have a bugdir. Who cares about it, and what do they care about? - Returns a dict of dicts: - subscribers[user][id] = types - where id is either a bug.uuid (in the case of a bug subscription) - or "%(bugdir_id)s" (in the case of a bugdir subscription). - - Only checks bugs that are currently in memory, so you might want - to call bugdir.load_all_bugs() first. - - >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) - >>> a = bd.bug_from_shortname("a") - >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe ", - ... [diff.BUGDIR_TYPE_ALL], ["a.com"], diff.BUGDIR_TYPE_ALL) - >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe ", - ... [diff.BUGDIR_TYPE_NEW], ["*"], diff.BUGDIR_TYPE_ALL) - >>> a.extra_strings = subscribe(a.extra_strings, "John Doe ", - ... [diff.BUG_TYPE_ALL], ["a.com"], diff.BUG_TYPE_ALL) - >>> subscribers = get_bugdir_subscribers(bd, "a.com") - >>> subscribers["Jane Doe "]["%(bugdir_id)s"] - [] - >>> subscribers["John Doe "]["%(bugdir_id)s"] - [] - >>> subscribers["John Doe "]["a"] - [] - >>> get_bugdir_subscribers(bd, "b.net") - {'Jane Doe ': {'%(bugdir_id)s': []}} - >>> bd.cleanup() - """ % {'bugdir_id':diff.BUGDIR_ID} - subscribers = {} - for sub in get_subscribers(bugdir.extra_strings, diff.BUGDIR_TYPE_ALL, - server, diff.BUGDIR_TYPE_ALL, - match_descendant_types=True): - i,s,ts,srvs = _get_subscriber(bugdir.extra_strings, sub, - diff.BUGDIR_TYPE_ALL) - subscribers[sub] = {"DIR":ts} - for bug in bugdir: - for sub in get_subscribers(bug.extra_strings, diff.BUG_TYPE_ALL, - server, diff.BUG_TYPE_ALL, - match_descendant_types=True): - i,s,ts,srvs = _get_subscriber(bug.extra_strings, sub, - diff.BUG_TYPE_ALL) - if sub in subscribers: - subscribers[sub][bug.uuid] = ts - else: - subscribers[sub] = {bug.uuid:ts} - return subscribers diff --git a/becommands/tag.py b/becommands/tag.py deleted file mode 100644 index f3819bd..0000000 --- a/becommands/tag.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (C) 2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Tag a bug, or search bugs for tags""" -from libbe import cmdutil, bugdir -import os, copy -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> from libbe import utility - >>> bd = bugdir.SimpleBugDir() - >>> bd.set_sync_with_disk(True) - >>> os.chdir(bd.root) - >>> a = bd.bug_from_shortname("a") - >>> print a.extra_strings - [] - >>> execute(["a", "GUI"], manipulate_encodings=False) - Tags for a: - GUI - >>> bd._clear_bugs() # resync our copy of bug - >>> a = bd.bug_from_shortname("a") - >>> print a.extra_strings - ['TAG:GUI'] - >>> execute(["a", "later"], manipulate_encodings=False) - Tags for a: - GUI - later - >>> execute(["a"], manipulate_encodings=False) - Tags for a: - GUI - later - >>> execute(["--list"], manipulate_encodings=False) - GUI - later - >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) - Tags for a: - Alphabetically first - GUI - later - >>> bd._clear_bugs() # resync our copy of bug - >>> a = bd.bug_from_shortname("a") - >>> print a.extra_strings - ['TAG:Alphabetically first', 'TAG:GUI', 'TAG:later'] - >>> a.extra_strings = [] - >>> print a.extra_strings - [] - >>> execute(["a"], manipulate_encodings=False) - >>> bd._clear_bugs() # resync our copy of bug - >>> a = bd.bug_from_shortname("a") - >>> print a.extra_strings - [] - >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) - Tags for a: - Alphabetically first - >>> execute(["--remove", "a", "Alphabetically first"], manipulate_encodings=False) - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) - - if len(args) == 0 and options.list == False: - raise cmdutil.UsageError("Please specify a bug id.") - elif len(args) > 2 or (len(args) > 0 and options.list == True): - help() - raise cmdutil.UsageError("Too many arguments.") - - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if options.list: - bd.load_all_bugs() - tags = [] - for bug in bd: - for estr in bug.extra_strings: - if estr.startswith("TAG:"): - tag = estr[4:] - if tag not in tags: - tags.append(tag) - tags.sort() - if len(tags) > 0: - print '\n'.join(tags) - return - bug = cmdutil.bug_from_id(bd, args[0]) - if len(args) == 2: - given_tag = args[1] - estrs = bug.extra_strings - tag_string = "TAG:%s" % given_tag - if options.remove == True: - estrs.remove(tag_string) - else: # add the tag - estrs.append(tag_string) - bug.extra_strings = estrs # reassign to notice change - - tags = [] - for estr in bug.extra_strings: - if estr.startswith("TAG:"): - tags.append(estr[4:]) - - if len(tags) > 0: - print "Tags for %s:" % bug.uuid - print '\n'.join(tags) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be tag BUG-ID [TAG]\nor: be tag --list") - parser.add_option("-r", "--remove", action="store_true", dest="remove", - help="Remove TAG (instead of adding it)") - parser.add_option("-l", "--list", action="store_true", dest="list", - help="List all available tags and exit") - return parser - -longhelp=""" -If TAG is given, add TAG to BUG-ID. If it is not specified, just -print the tags for BUG-ID. - -To search for bugs with a particular tag, try - $ be list --extra-strings TAG: -""" - -def help(): - return get_parser().help_str() + longhelp diff --git a/becommands/target.py b/becommands/target.py deleted file mode 100644 index 5dd5d38..0000000 --- a/becommands/target.py +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Chris Ball -# Gianluca Montecchi -# Marien Zwart -# Thomas Gerigk -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Assorted bug target manipulations and queries""" -from libbe import cmdutil, bugdir -from becommands import depend -__desc__ = __doc__ - -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os, StringIO, sys - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute(["a"], manipulate_encodings=False) - No target assigned. - >>> execute(["a", "tomorrow"], manipulate_encodings=False) - >>> execute(["a"], manipulate_encodings=False) - tomorrow - - >>> orig_stdout = sys.stdout - >>> tmp_stdout = StringIO.StringIO() - >>> sys.stdout = tmp_stdout - >>> execute(["--resolve", "tomorrow"], manipulate_encodings=False) - >>> sys.stdout = orig_stdout - >>> output = tmp_stdout.getvalue().strip() - >>> target = bd.bug_from_uuid(output) - >>> print target.summary - tomorrow - >>> print target.severity - target - - >>> execute(["a", "none"], manipulate_encodings=False) - >>> execute(["a"], manipulate_encodings=False) - No target assigned. - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) - - if (options.resolve == False and len(args) not in (1, 2)) \ - or (options.resolve == True and len(args) not in (0, 1)): - raise cmdutil.UsageError('Incorrect number of arguments.') - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if options.resolve == True: - if len(args) == 0: - summary = None - else: - summary = args[0] - bug = bug_from_target_summary(bd, summary) - if bug == None: - print 'No target assigned.' - else: - print bug.uuid - return - bug = cmdutil.bug_from_id(bd, args[0]) - if len(args) == 1: - target = bug_target(bd, bug) - if target is None: - print "No target assigned." - else: - print target.summary - else: - if args[1] == "none": - target = remove_target(bd, bug) - else: - target = add_target(bd, bug, args[1]) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be target BUG-ID [TARGET]\nor: be target --resolve [TARGET]") - parser.add_option("-r", "--resolve", action="store_true", dest="resolve", - help="Print the UUID for the target bug whose summary matches TARGET. If TARGET is not given, print the UUID of the current bugdir target. If that is not set, don't print anything.", - default=False) - return parser - -longhelp=""" -Assorted bug target manipulations and queries. - -If no target is specified, the bug's current target is printed. If -TARGET is specified, it will be assigned to the bug, creating a new -target bug if necessary. - -Targets are free-form; any text may be specified. They will generally -be milestone names or release numbers. The value "none" can be used -to unset the target. - -In the alternative `be target --resolve TARGET` form, print the UUID -of the target-bug with summary TARGET. If target is not given, return -use the bugdir's current target (see `be set`). - -If you want to list all bugs blocking the current target, try - $ be depend --status -closed,fixed,wontfix --severity -target \ - $(be target --resolve) - -If you want to set the current bugdir target by summary (rather than -by UUID), try - $ be set target $(be target --resolve SUMMARY) -""" - -def help(): - return get_parser().help_str() + longhelp - -def bug_from_target_summary(bugdir, summary=None): - if summary == None: - if bugdir.target == None: - return None - else: - return bugdir.bug_from_uuid(bugdir.target) - matched = [] - for uuid in bugdir.uuids(): - bug = bugdir.bug_from_uuid(uuid) - if bug.severity == 'target' and bug.summary == summary: - matched.append(bug) - if len(matched) == 0: - return None - if len(matched) > 1: - raise Exception('Several targets with same summary: %s' - % '\n '.join([bug.uuid for bug in matched])) - return matched[0] - -def bug_target(bugdir, bug): - if bug.severity == 'target': - return bug - matched = [] - for blocked in depend.get_blocks(bugdir, bug): - if blocked.severity == 'target': - matched.append(blocked) - if len(matched) == 0: - return None - if len(matched) > 1: - raise Exception('This bug (%s) blocks several targets: %s' - % (bug.uuid, - '\n '.join([b.uuid for b in matched]))) - return matched[0] - -def remove_target(bugdir, bug): - target = bug_target(bugdir, bug) - depend.remove_block(target, bug) - return target - -def add_target(bugdir, bug, summary): - target = bug_from_target_summary(bugdir, summary) - if target == None: - target = bugdir.new_bug(summary=summary) - target.severity = 'target' - depend.add_block(target, bug) - return target diff --git a/libbe/arch.py b/libbe/arch.py deleted file mode 100644 index 45a3284..0000000 --- a/libbe/arch.py +++ /dev/null @@ -1,315 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Ben Finney -# Gianluca Montecchi -# James Rowe -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -GNU Arch (tla) backend. -""" - -import codecs -import os -import re -import shutil -import sys -import time - -import libbe -from beuuid import uuid_gen -import config -import vcs -if libbe.TESTING == True: - import unittest - import doctest - - -DEFAULT_CLIENT = "tla" - -client = config.get_val("arch_client", default=DEFAULT_CLIENT) - -def new(): - return Arch() - -class Arch(vcs.VCS): - name = "arch" - client = client - versioned = True - _archive_name = None - _archive_dir = None - _tmp_archive = False - _project_name = None - _tmp_project = False - _arch_paramdir = os.path.expanduser("~/.arch-params") - def _vcs_version(self): - status,output,error = self._u_invoke_client("--version") - return output - def _vcs_detect(self, path): - """Detect whether a directory is revision-controlled using Arch""" - if self._u_search_parent_directories(path, "{arch}") != None : - config.set_val("arch_client", client) - return True - return False - def _vcs_init(self, path): - self._create_archive(path) - self._create_project(path) - self._add_project_code(path) - def _create_archive(self, path): - """ - Create a temporary Arch archive in the directory PATH. This - archive will be removed by - cleanup->_vcs_cleanup->_remove_archive - """ - # http://regexps.srparish.net/tutorial-tla/new-archive.html#Creating_a_New_Archive - assert self._archive_name == None - id = self.get_user_id() - name, email = self._u_parse_id(id) - if email == None: - email = "%s@example.com" % name - trailer = "%s-%s" % ("bugs-everywhere-auto", uuid_gen()[0:8]) - self._archive_name = "%s--%s" % (email, trailer) - self._archive_dir = "/tmp/%s" % trailer - self._tmp_archive = True - self._u_invoke_client("make-archive", self._archive_name, - self._archive_dir, cwd=path) - def _invoke_client(self, *args, **kwargs): - """ - Invoke the client on our archive. - """ - assert self._archive_name != None - command = args[0] - if len(args) > 1: - tailargs = args[1:] - else: - tailargs = [] - arglist = [command, "-A", self._archive_name] - arglist.extend(tailargs) - args = tuple(arglist) - return self._u_invoke_client(*args, **kwargs) - def _remove_archive(self): - assert self._tmp_archive == True - assert self._archive_dir != None - assert self._archive_name != None - os.remove(os.path.join(self._arch_paramdir, - "=locations", self._archive_name)) - shutil.rmtree(self._archive_dir) - self._tmp_archive = False - self._archive_dir = False - self._archive_name = False - def _create_project(self, path): - """ - Create a temporary Arch project in the directory PATH. This - project will be removed by - cleanup->_vcs_cleanup->_remove_project - """ - # http://mwolson.org/projects/GettingStartedWithArch.html - # http://regexps.srparish.net/tutorial-tla/new-project.html#Starting_a_New_Project - category = "bugs-everywhere" - branch = "mainline" - version = "0.1" - self._project_name = "%s--%s--%s" % (category, branch, version) - self._invoke_client("archive-setup", self._project_name, - cwd=path) - self._tmp_project = True - def _remove_project(self): - assert self._tmp_project == True - assert self._project_name != None - assert self._archive_dir != None - shutil.rmtree(os.path.join(self._archive_dir, self._project_name)) - self._tmp_project = False - self._project_name = False - def _archive_project_name(self): - assert self._archive_name != None - assert self._project_name != None - return "%s/%s" % (self._archive_name, self._project_name) - def _adjust_naming_conventions(self, path): - """ - By default, Arch restricts source code filenames to - ^[_=a-zA-Z0-9].*$ - See - http://regexps.srparish.net/tutorial-tla/naming-conventions.html - Since our bug directory '.be' doesn't satisfy these conventions, - we need to adjust them. - - The conventions are specified in - project-root/{arch}/=tagging-method - """ - tagpath = os.path.join(path, "{arch}", "=tagging-method") - lines_out = [] - f = codecs.open(tagpath, "r", self.encoding) - for line in f: - if line.startswith("source "): - lines_out.append("source ^[._=a-zA-X0-9].*$\n") - else: - lines_out.append(line) - f.close() - f = codecs.open(tagpath, "w", self.encoding) - f.write("".join(lines_out)) - f.close() - - def _add_project_code(self, path): - # http://mwolson.org/projects/GettingStartedWithArch.html - # http://regexps.srparish.net/tutorial-tla/new-source.html - # http://regexps.srparish.net/tutorial-tla/importing-first.html - self._invoke_client("init-tree", self._project_name, - cwd=path) - self._adjust_naming_conventions(path) - self._invoke_client("import", "--summary", "Began versioning", - cwd=path) - def _vcs_cleanup(self): - if self._tmp_project == True: - self._remove_project() - if self._tmp_archive == True: - self._remove_archive() - - def _vcs_root(self, path): - if not os.path.isdir(path): - dirname = os.path.dirname(path) - else: - dirname = path - status,output,error = self._u_invoke_client("tree-root", dirname) - root = output.rstrip('\n') - - self._get_archive_project_name(root) - - return root - def _get_archive_name(self, root): - status,output,error = self._u_invoke_client("archives") - lines = output.split('\n') - # e.g. output: - # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52 - # /tmp/BEtestXXXXXX/rootdir - # (+ repeats) - for archive,location in zip(lines[::2], lines[1::2]): - if os.path.realpath(location) == os.path.realpath(root): - self._archive_name = archive - assert self._archive_name != None - def _get_archive_project_name(self, root): - # get project names - status,output,error = self._u_invoke_client("tree-version", cwd=root) - # e.g output - # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52/be--mainline--0.1 - archive_name,project_name = output.rstrip('\n').split('/') - self._archive_name = archive_name - self._project_name = project_name - def _vcs_get_user_id(self): - try: - status,output,error = self._u_invoke_client('my-id') - return output.rstrip('\n') - except Exception, e: - if 'no arch user id set' in e.args[0]: - return None - else: - raise - def _vcs_set_user_id(self, value): - self._u_invoke_client('my-id', value) - def _vcs_add(self, path): - self._u_invoke_client("add-id", path) - realpath = os.path.realpath(self._u_abspath(path)) - pathAdded = realpath in self._list_added(self.rootdir) - if self.paranoid and not pathAdded: - self._force_source(path) - def _list_added(self, root): - assert os.path.exists(root) - assert os.access(root, os.X_OK) - root = os.path.realpath(root) - status,output,error = self._u_invoke_client("inventory", "--source", - "--both", "--all", root) - inv_str = output.rstrip('\n') - return [os.path.join(root, p) for p in inv_str.split('\n')] - def _add_dir_rule(self, rule, dirname, root): - inv_path = os.path.join(dirname, '.arch-inventory') - f = codecs.open(inv_path, "a", self.encoding) - f.write(rule) - f.close() - if os.path.realpath(inv_path) not in self._list_added(root): - paranoid = self.paranoid - self.paranoid = False - self.add(inv_path) - self.paranoid = paranoid - def _force_source(self, path): - rule = "source %s\n" % self._u_rel_path(path) - self._add_dir_rule(rule, os.path.dirname(path), self.rootdir) - if os.path.realpath(path) not in self._list_added(self.rootdir): - raise CantAddFile(path) - def _vcs_remove(self, path): - if not '.arch-ids' in path: - self._u_invoke_client("delete-id", path) - def _vcs_update(self, path): - pass - def _vcs_get_file_contents(self, path, revision=None, binary=False): - if revision == None: - return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) - else: - status,output,error = \ - self._invoke_client("file-find", path, revision) - relpath = output.rstrip('\n') - abspath = os.path.join(self.rootdir, relpath) - f = codecs.open(abspath, "r", self.encoding) - contents = f.read() - f.close() - return contents - def _vcs_duplicate_repo(self, directory, revision=None): - if revision == None: - vcs.VCS._vcs_duplicate_repo(self, directory, revision) - else: - status,output,error = \ - self._u_invoke_client("get", revision, directory) - def _vcs_commit(self, commitfile, allow_empty=False): - if allow_empty == False: - # arch applies empty commits without complaining, so check first - status,output,error = self._u_invoke_client("changes",expect=(0,1)) - if status == 0: - raise vcs.EmptyCommit() - summary,body = self._u_parse_commitfile(commitfile) - args = ["commit", "--summary", summary] - if body != None: - args.extend(["--log-message",body]) - status,output,error = self._u_invoke_client(*args) - revision = None - revline = re.compile("[*] committed (.*)") - match = revline.search(output) - assert match != None, output+error - assert len(match.groups()) == 1 - revpath = match.groups()[0] - assert not " " in revpath, revpath - assert revpath.startswith(self._archive_project_name()+'--') - revision = revpath[len(self._archive_project_name()+'--'):] - return revpath - def _vcs_revision_id(self, index): - status,output,error = self._u_invoke_client("logs") - logs = output.splitlines() - first_log = logs.pop(0) - assert first_log == "base-0", first_log - try: - log = logs[index] - except IndexError: - return None - return "%s--%s" % (self._archive_project_name(), log) - -class CantAddFile(Exception): - def __init__(self, file): - self.file = file - Exception.__init__(self, "Can't automatically add file %s" % file) - - - -if libbe.TESTING == True: - vcs.make_vcs_testcase_subclasses(Arch, sys.modules[__name__]) - - unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) - suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/beuuid.py b/libbe/beuuid.py deleted file mode 100644 index a3a3b6c..0000000 --- a/libbe/beuuid.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Backwards compatibility support for Python 2.4. Once people give up -on 2.4 ;), the uuid call should be merged into bugdir.py -""" - -import libbe -if libbe.TESTING == True: - import unittest - - -try: - from uuid import uuid4 # Python >= 2.5 - def uuid_gen(): - id = uuid4() - idstr = id.urn - start = "urn:uuid:" - assert idstr.startswith(start) - return idstr[len(start):] -except ImportError: - import os - import sys - from subprocess import Popen, PIPE - - def uuid_gen(): - # Shell-out to system uuidgen - args = ['uuidgen', 'r'] - try: - if sys.platform != "win32": - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) - else: - # win32 don't have os.execvp() so have to run command in a shell - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, - shell=True, cwd=cwd) - except OSError, e : - strerror = "%s\nwhile executing %s" % (e.args[1], args) - raise OSError, strerror - output, error = q.communicate() - status = q.wait() - if status != 0: - strerror = "%s\nwhile executing %s" % (status, args) - raise Exception, strerror - return output.rstrip('\n') - -if libbe.TESTING == True: - class UUIDtestCase(unittest.TestCase): - def testUUID_gen(self): - id = uuid_gen() - self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) - - suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) diff --git a/libbe/bzr.py b/libbe/bzr.py deleted file mode 100644 index 62a9b11..0000000 --- a/libbe/bzr.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Ben Finney -# Gianluca Montecchi -# Marien Zwart -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Bazaar (bzr) backend. -""" - -import os -import re -import sys -import unittest - -import libbe -import vcs -if libbe.TESTING == True: - import doctest - - -def new(): - return Bzr() - -class Bzr(vcs.VCS): - name = "bzr" - client = "bzr" - versioned = True - def _vcs_version(self): - status,output,error = self._u_invoke_client("--version") - return output - def _vcs_detect(self, path): - if self._u_search_parent_directories(path, ".bzr") != None : - return True - return False - def _vcs_root(self, path): - """Find the root of the deepest repository containing path.""" - status,output,error = self._u_invoke_client("root", path) - return output.rstrip('\n') - def _vcs_init(self, path): - self._u_invoke_client("init", cwd=path) - def _vcs_get_user_id(self): - status,output,error = self._u_invoke_client("whoami") - return output.rstrip('\n') - def _vcs_set_user_id(self, value): - self._u_invoke_client("whoami", value) - def _vcs_add(self, path): - self._u_invoke_client("add", path) - def _vcs_remove(self, path): - # --force to also remove unversioned files. - self._u_invoke_client("remove", "--force", path) - def _vcs_update(self, path): - pass - def _vcs_get_file_contents(self, path, revision=None, binary=False): - if revision == None: - return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) - else: - status,output,error = \ - self._u_invoke_client("cat","-r",revision,path) - return output - def _vcs_duplicate_repo(self, directory, revision=None): - if revision == None: - vcs.VCS._vcs_duplicate_repo(self, directory, revision) - else: - self._u_invoke_client("branch", "--revision", revision, - ".", directory) - def _vcs_commit(self, commitfile, allow_empty=False): - args = ["commit", "--file", commitfile] - if allow_empty == True: - args.append("--unchanged") - status,output,error = self._u_invoke_client(*args) - else: - kwargs = {"expect":(0,3)} - status,output,error = self._u_invoke_client(*args, **kwargs) - if status != 0: - strings = ["ERROR: no changes to commit.", # bzr 1.3.1 - "ERROR: No changes to commit."] # bzr 1.15.1 - if self._u_any_in_string(strings, error) == True: - raise vcs.EmptyCommit() - else: - raise vcs.CommandError(args, status, stderr=error) - revision = None - revline = re.compile("Committed revision (.*)[.]") - match = revline.search(error) - assert match != None, output+error - assert len(match.groups()) == 1 - revision = match.groups()[0] - return revision - def _vcs_revision_id(self, index): - status,output,error = self._u_invoke_client("revno") - current_revision = int(output) - if index >= current_revision or index < -current_revision: - return None - if index >= 0: - return str(index+1) # bzr commit 0 is the empty tree. - return str(current_revision+index+1) - - -if libbe.TESTING == True: - vcs.make_vcs_testcase_subclasses(Bzr, sys.modules[__name__]) - - unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) - suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py deleted file mode 100644 index c567984..0000000 --- a/libbe/cmdutil.py +++ /dev/null @@ -1,356 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# Oleg Romanyshyn -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Define assorted utilities to make command-line handling easier. -""" - -import glob -import optparse -import os -from textwrap import TextWrapper -from StringIO import StringIO -import sys - -import libbe -import bugdir -import comment -import plugin -import encoding -if libbe.TESTING == True: - import doctest - - -class UserError(Exception): - def __init__(self, msg): - Exception.__init__(self, msg) - -class UnknownCommand(UserError): - def __init__(self, cmd): - Exception.__init__(self, "Unknown command '%s'" % cmd) - self.cmd = cmd - -class UsageError(Exception): - pass - -class GetHelp(Exception): - pass - -class GetCompletions(Exception): - def __init__(self, completions=[]): - msg = "Get allowed completions" - Exception.__init__(self, msg) - self.completions = completions - -def iter_commands(): - for name, module in plugin.iter_plugins("becommands"): - yield name.replace("_", "-"), module - -def get_command(command_name): - """Retrieves the module for a user command - - >>> try: - ... get_command("asdf") - ... except UnknownCommand, e: - ... print e - Unknown command 'asdf' - >>> repr(get_command("list")).startswith(" 0: - max_pos_arg = max(bugid_args.keys()) - else: - max_pos_arg = -1 - for pos,value in enumerate(args): - if value == "--complete": - filter = None - if pos in bugid_args: - filter = bugid_args[pos] - if pos > max_pos_arg and -1 in bugid_args: - filter = bugid_args[-1] - if filter != None: - bugshortnames = [] - try: - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=False) - bd.load_all_bugs() - bugs = [bug for bug in bd if filter(bug) == True] - bugshortnames = [bd.bug_shortname(bug) for bug in bugs] - except bugdir.NoBugDir: - pass - raise GetCompletions(bugshortnames) - raise GetCompletions() - -def complete_path(path): - """List possible path completions for path.""" - comps = glob.glob(path+"*") + glob.glob(path+"/*") - if len(comps) == 1 and os.path.isdir(comps[0]): - comps.extend(glob.glob(comps[0]+"/*")) - return comps - -def underlined(instring): - """Produces a version of a string that is underlined with '=' - - >>> underlined("Underlined String") - 'Underlined String\\n=================' - """ - - return "%s\n%s" % (instring, "="*len(instring)) - -def select_values(string, possible_values, name="unkown"): - """ - This function allows the user to select values from a list of - possible values. The default is to select all the values: - - >>> select_values(None, ['abc', 'def', 'hij']) - ['abc', 'def', 'hij'] - - The user selects values with a comma-separated limit_string. - Prepending a minus sign to such a list denotes blacklist mode: - - >>> select_values('-abc,hij', ['abc', 'def', 'hij']) - ['def'] - - Without the leading -, the selection is in whitelist mode: - - >>> select_values('abc,hij', ['abc', 'def', 'hij']) - ['abc', 'hij'] - - In either case, appropriate errors are raised if on of the - user-values is not in the list of possible values. The name - parameter lets you make the error message more clear: - - >>> select_values('-xyz,hij', ['abc', 'def', 'hij'], name="foobar") - Traceback (most recent call last): - ... - UserError: Invalid foobar xyz - ['abc', 'def', 'hij'] - >>> select_values('xyz,hij', ['abc', 'def', 'hij'], name="foobar") - Traceback (most recent call last): - ... - UserError: Invalid foobar xyz - ['abc', 'def', 'hij'] - """ - possible_values = list(possible_values) # don't alter the original - if string == None: - pass - elif string.startswith('-'): - blacklisted_values = set(string[1:].split(',')) - for value in blacklisted_values: - if value not in possible_values: - raise UserError('Invalid %s %s\n %s' - % (name, value, possible_values)) - possible_values.remove(value) - else: - whitelisted_values = string.split(',') - for value in whitelisted_values: - if value not in possible_values: - raise UserError('Invalid %s %s\n %s' - % (name, value, possible_values)) - possible_values = whitelisted_values - return possible_values - -def restrict_file_access(bugdir, path): - """ - Check that the file at path is inside bugdir.root. This is - important if you allow other users to execute becommands with your - username (e.g. if you're running be-handle-mail through your - ~/.procmailrc). If this check wasn't made, a user could e.g. - run - be commit -b ~/.ssh/id_rsa "Hack to expose ssh key" - which would expose your ssh key to anyone who could read the VCS - log. - """ - in_root = bugdir.vcs.path_in_root(path, bugdir.root) - if in_root == False: - raise UserError('file access restricted!\n %s not in %s' - % (path, bugdir.root)) - -def parse_id(id): - """ - Return (bug_id, comment_id) tuple. - Basically inverts Comment.comment_shortnames() - >>> parse_id('XYZ') - ('XYZ', None) - >>> parse_id('XYZ:123') - ('XYZ', ':123') - >>> parse_id('') - Traceback (most recent call last): - ... - UserError: invalid id ''. - >>> parse_id('::') - Traceback (most recent call last): - ... - UserError: invalid id '::'. - """ - if len(id) == 0: - raise UserError("invalid id '%s'." % id) - if id.count(':') > 1: - raise UserError("invalid id '%s'." % id) - elif id.count(':') == 1: - # Split shortname generated by Comment.comment_shortnames() - bug_id,comment_id = id.split(':') - comment_id = ':'+comment_id - else: - bug_id = id - comment_id = None - return (bug_id, comment_id) - -def bug_from_id(bdir, id): - """ - Exception translation for the command-line interface. - id can be either the bug shortname or the full uuid. - """ - try: - bug = bdir.bug_from_shortname(id) - except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: - raise UserError(e.message) - return bug - -def bug_comment_from_id(bdir, id): - """ - Return (bug,comment) tuple matching shortname. id can be either - the bug/comment shortname or the full uuid. If there is no - comment part to the id, the returned comment is the bug's - .comment_root. - """ - bug_id,comment_id = parse_id(id) - try: - bug = bdir.bug_from_shortname(bug_id) - except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: - raise UserError(e.message) - if comment_id == None: - comm = bug.comment_root - else: - #bug.load_comments(load_full=False) - try: - comm = bug.comment_root.comment_from_shortname(comment_id) - except comment.InvalidShortname, e: - raise UserError(e.message) - return (bug, comm) - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() diff --git a/libbe/command/__init__.py b/libbe/command/__init__.py new file mode 100644 index 0000000..794013c --- /dev/null +++ b/libbe/command/__init__.py @@ -0,0 +1,16 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. diff --git a/libbe/command/assign.py b/libbe/command/assign.py new file mode 100644 index 0000000..9c971ae --- /dev/null +++ b/libbe/command/assign.py @@ -0,0 +1,90 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# Marien Zwart +# Thomas Gerigk +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Assign an individual or group to fix a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> bd.bug_from_shortname("a").assigned is None + True + + >>> execute(["a"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bd.bug_from_shortname("a").assigned == bd.user_id + True + + >>> execute(["a", "someone"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("a").assigned + someone + + >>> execute(["a","none"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bd.bug_from_shortname("a").assigned is None + True + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + assert(len(args) in (0, 1, 2)) + if len(args) == 0: + raise cmdutil.UsageError("Please specify a bug id.") + if len(args) > 2: + help() + raise cmdutil.UsageError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bug = cmdutil.bug_from_id(bd, args[0]) + bug = bd.bug_from_shortname(args[0]) + if len(args) == 1: + bug.assigned = bd.user_id + elif len(args) == 2: + if args[1] == "none": + bug.assigned = None + else: + bug.assigned = args[1] + bd.save() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be assign BUG-ID [ASSIGNEE]") + return parser + +longhelp = """ +Assign a person to fix a bug. + +By default, the bug is self-assigned. If an assignee is specified, the bug +will be assigned to that person. + +Assignees should be the person's Bugs Everywhere identity, the string that +appears in Creator fields. + +To un-assign a bug, specify "none" for the assignee. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/libbe/command/close.py b/libbe/command/close.py new file mode 100644 index 0000000..026c605 --- /dev/null +++ b/libbe/command/close.py @@ -0,0 +1,63 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# Marien Zwart +# Thomas Gerigk +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Close a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> from libbe import bugdir + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("a").status + open + >>> execute(["a"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("a").status + closed + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + if len(args) == 0: + raise cmdutil.UsageError("Please specify a bug id.") + if len(args) > 1: + raise cmdutil.UsageError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bug = cmdutil.bug_from_id(bd, args[0]) + bug.status = "closed" + bd.save() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be close BUG-ID") + return parser + +longhelp=""" +Close the bug identified by BUG-ID. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/libbe/command/comment.py b/libbe/command/comment.py new file mode 100644 index 0000000..9919d1d --- /dev/null +++ b/libbe/command/comment.py @@ -0,0 +1,164 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Add a comment to a bug""" +from libbe import cmdutil, bugdir, comment, editor +import os +import sys +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import time + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a", "This is a comment about a"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bug = cmdutil.bug_from_id(bd, "a") + >>> bug.load_comments(load_full=False) + >>> comment = bug.comment_root[0] + >>> print comment.body + This is a comment about a + + >>> comment.author == bd.user_id + True + >>> comment.time <= int(time.time()) + True + >>> comment.in_reply_to is None + True + + >>> if 'EDITOR' in os.environ: + ... del os.environ["EDITOR"] + >>> execute(["b"], manipulate_encodings=False) + Traceback (most recent call last): + UserError: No comment supplied, and EDITOR not specified. + + >>> os.environ["EDITOR"] = "echo 'I like cheese' > " + >>> execute(["b"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> bug = cmdutil.bug_from_id(bd, "b") + >>> bug.load_comments(load_full=False) + >>> comment = bug.comment_root[0] + >>> print comment.body + I like cheese + + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) == 0: + raise cmdutil.UsageError("Please specify a bug or comment id.") + if len(args) > 2: + raise cmdutil.UsageError("Too many arguments.") + + shortname = args[0] + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bug, parent = cmdutil.bug_comment_from_id(bd, shortname) + + if len(args) == 1: # try to launch an editor for comment-body entry + try: + if parent == bug.comment_root: + parent_body = bug.summary+"\n" + else: + parent_body = parent.body + estr = "Please enter your comment above\n\n> %s\n" \ + % ("\n> ".join(parent_body.splitlines())) + body = editor.editor_string(estr) + except editor.CantFindEditor, e: + raise cmdutil.UserError, "No comment supplied, and EDITOR not specified." + if body is None: + raise cmdutil.UserError("No comment entered.") + elif args[1] == '-': # read body from stdin + binary = not (options.content_type == None + or options.content_type.startswith("text/")) + if not binary: + body = sys.stdin.read() + if not body.endswith('\n'): + body+='\n' + else: # read-in without decoding + body = sys.__stdin__.read() + else: # body = arg[1] + body = args[1] + if not body.endswith('\n'): + body+='\n' + + new = parent.new_reply(body=body, content_type=options.content_type) + if options.author != None: + new.author = options.author + if options.alt_id != None: + new.alt_id = options.alt_id + +def get_parser(): + parser = cmdutil.CmdOptionParser("be comment ID [COMMENT]") + parser.add_option("-a", "--author", metavar="AUTHOR", dest="author", + help="Set the comment author", default=None) + parser.add_option("--alt-id", metavar="ID", dest="alt_id", + help="Set an alternate comment ID", default=None) + parser.add_option("-c", "--content-type", metavar="MIME", dest="content_type", + help="Set comment content-type (e.g. text/plain)", default=None) + return parser + +longhelp=""" +To add a comment to a bug, use the bug ID as the argument. To reply +to another comment, specify the comment name (as shown in "be show" +output). COMMENT, if specified, should be either the text of your +comment or "-", in which case the text will be read from stdin. If +you do not specify a COMMENT, $EDITOR is used to launch an editor. If +COMMENT is unspecified and EDITOR is not set, no comment will be +created. +""" + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + if pos == 0: # fist positional argument is a bug or comment id + if len(args) >= 2: + partial = args[1].split(':')[0] # take only bugid portion + else: + partial = "" + ids = [] + try: + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + bugs = [] + for uuid in bd.uuids(): + if uuid.startswith(partial): + bug = bd.bug_from_uuid(uuid) + if bug.active == True: + bugs.append(bug) + for bug in bugs: + shortname = bd.bug_shortname(bug) + ids.append(shortname) + bug.load_comments(load_full=False) + for id,comment in bug.comment_shortnames(shortname): + ids.append(id) + except bugdir.NoBugDir: + pass + raise cmdutil.GetCompletions(ids) + raise cmdutil.GetCompletions() diff --git a/libbe/command/commit.py b/libbe/command/commit.py new file mode 100644 index 0000000..cade355 --- /dev/null +++ b/libbe/command/commit.py @@ -0,0 +1,82 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Commit the currently pending changes to the repository""" +from libbe import cmdutil, bugdir, editor, vcs +import sys +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> from libbe import bug + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> full_path = "testfile" + >>> test_contents = "A test file" + >>> bd.vcs.set_file_contents(full_path, test_contents) + >>> execute(["Added %s." % (full_path)], manipulate_encodings=False) # doctest: +ELLIPSIS + Committed ... + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) + if len(args) != 1: + raise cmdutil.UsageError("Please supply a commit message") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + if args[0] == '-': # read summary from stdin + assert options.body != "EDITOR", \ + "Cannot spawn and editor when the summary is using stdin." + summary = sys.stdin.readline() + else: + summary = args[0] + if options.body == None: + body = None + elif options.body == "EDITOR": + body = editor.editor_string("Please enter your commit message above") + else: + if restrict_file_access == True: + cmdutil.restrict_file_access(bd, options.body) + body = bd.vcs.get_file_contents(options.body, allow_no_vcs=True) + try: + revision = bd.vcs.commit(summary, body=body, + allow_empty=options.allow_empty) + except vcs.EmptyCommit, e: + print e + return 1 + else: + print "Committed %s" % revision + +def get_parser(): + parser = cmdutil.CmdOptionParser("be commit COMMENT") + parser.add_option("-b", "--body", metavar="FILE", dest="body", + help='Provide a detailed body for the commit message. In the special case that FILE == "EDITOR", spawn an editor to enter the body text (in which case you cannot use stdin for the summary)', default=None) + parser.add_option("-a", "--allow-empty", dest="allow_empty", + help="Allow empty commits", + default=False, action="store_true") + return parser + +longhelp=""" +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. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/libbe/command/depend.py b/libbe/command/depend.py new file mode 100644 index 0000000..c2cb2a4 --- /dev/null +++ b/libbe/command/depend.py @@ -0,0 +1,371 @@ +# Copyright (C) 2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Add/remove bug dependencies""" +from libbe import cmdutil, bugdir, bug, tree +import os, copy +__desc__ = __doc__ + +BLOCKS_TAG="BLOCKS:" +BLOCKED_BY_TAG="BLOCKED-BY:" + +class BrokenLink (Exception): + def __init__(self, blocked_bug, blocking_bug, blocks=True): + if blocks == True: + msg = "Missing link: %s blocks %s" \ + % (blocking_bug.uuid, blocked_bug.uuid) + else: + msg = "Missing link: %s blocked by %s" \ + % (blocked_bug.uuid, blocking_bug.uuid) + Exception.__init__(self, msg) + self.blocked_bug = blocked_bug + self.blocking_bug = blocking_bug + + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> from libbe import utility + >>> bd = bugdir.SimpleBugDir() + >>> bd.save() + >>> os.chdir(bd.root) + >>> execute(["a", "b"], manipulate_encodings=False) + a blocked by: + b + >>> execute(["a"], manipulate_encodings=False) + a blocked by: + b + >>> execute(["--show-status", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + a blocked by: + b closed + >>> execute(["b", "a"], manipulate_encodings=False) + b blocked by: + a + b blocks: + a + >>> execute(["--show-status", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + a blocked by: + b closed + a blocks: + b closed + >>> execute(["-r", "b", "a"], manipulate_encodings=False) + b blocks: + a + >>> execute(["-r", "a", "b"], manipulate_encodings=False) + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda _bug : _bug.active==True, + 1: lambda _bug : _bug.active==True}) + + if options.repair == True: + if len(args) > 0: + raise cmdutil.UsageError("No arguments with --repair calls.") + elif len(args) < 1: + raise cmdutil.UsageError("Please a bug id.") + elif len(args) > 2: + help() + raise cmdutil.UsageError("Too many arguments.") + elif len(args) == 2 and options.tree_depth != None: + raise cmdutil.UsageError("Only one bug id used in tree mode.") + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + if options.repair == True: + good,fixed,broken = check_dependencies(bd, repair_broken_links=True) + assert len(broken) == 0, broken + if len(fixed) > 0: + print "Fixed the following links:" + print "\n".join(["%s |-- %s" % (blockee.uuid, blocker.uuid) + for blockee,blocker in fixed]) + return 0 + + allowed_status_values = \ + cmdutil.select_values(options.status, bug.status_values) + allowed_severity_values = \ + cmdutil.select_values(options.severity, bug.severity_values) + + bugA = cmdutil.bug_from_id(bd, args[0]) + + if options.tree_depth != None: + dtree = DependencyTree(bd, bugA, options.tree_depth, + allowed_status_values, + allowed_severity_values) + if len(dtree.blocked_by_tree()) > 0: + print "%s blocked by:" % bugA.uuid + for depth,node in dtree.blocked_by_tree().thread(): + if depth == 0: continue + print "%s%s" % (" "*(depth), node.bug.string(shortlist=True)) + if len(dtree.blocks_tree()) > 0: + print "%s blocks:" % bugA.uuid + for depth,node in dtree.blocks_tree().thread(): + if depth == 0: continue + print "%s%s" % (" "*(depth), node.bug.string(shortlist=True)) + return 0 + + if len(args) == 2: + bugB = cmdutil.bug_from_id(bd, args[1]) + if options.remove == True: + remove_block(bugA, bugB) + else: # add the dependency + add_block(bugA, bugB) + + blocked_by = get_blocked_by(bd, bugA) + if len(blocked_by) > 0: + print "%s blocked by:" % bugA.uuid + if options.show_status == True: + print '\n'.join(["%s\t%s" % (_bug.uuid, _bug.status) + for _bug in blocked_by]) + else: + print '\n'.join([_bug.uuid for _bug in blocked_by]) + blocks = get_blocks(bd, bugA) + if len(blocks) > 0: + print "%s blocks:" % bugA.uuid + if options.show_status == True: + print '\n'.join(["%s\t%s" % (_bug.uuid, _bug.status) + for _bug in blocks]) + else: + print '\n'.join([_bug.uuid for _bug in blocks]) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be depend BUG-ID [BUG-ID]\nor: be depend --repair") + parser.add_option("-r", "--remove", action="store_true", + dest="remove", default=False, + help="Remove dependency (instead of adding it)") + parser.add_option("-s", "--show-status", action="store_true", + dest="show_status", default=False, + help="Show status of blocking bugs") + parser.add_option("--status", dest="status", metavar="STATUS", + help="Only show bugs matching the STATUS specifier") + parser.add_option("--severity", dest="severity", metavar="SEVERITY", + help="Only show bugs matching the SEVERITY specifier") + parser.add_option("-t", "--tree-depth", metavar="DEPTH", default=None, + type="int", dest="tree_depth", + help="Print dependency tree rooted at BUG-ID with DEPTH levels of both blockers and blockees. Set DEPTH <= 0 to disable the depth limit.") + parser.add_option("--repair", action="store_true", + dest="repair", default=False, + help="Check for and repair one-way links") + return parser + +longhelp=""" +Set a dependency with the second bug (B) blocking the first bug (A). +If bug B is not specified, just print a list of bugs blocking (A). + +To search for bugs blocked by a particular bug, try + $ be list --extra-strings BLOCKED-BY: + +The --status and --severity options allow you to either blacklist or +whitelist values, for example + $ be list --status open,assigned +will only follow and print dependencies with open or assigned status. +You select blacklist mode by starting the list with a minus sign, for +example + $ be list --severity -target +which will only follow and print dependencies with non-target severity. + +In repair mode, add the missing direction to any one-way links. + +The "|--" symbol in the repair-mode output is inspired by the +"negative feedback" arrow common in biochemistry. See, for example + http://www.nature.com/nature/journal/v456/n7223/images/nature07513-f5.0.jpg +""" + +def help(): + return get_parser().help_str() + longhelp + +# internal helper functions + +def _generate_blocks_string(blocked_bug): + return "%s%s" % (BLOCKS_TAG, blocked_bug.uuid) + +def _generate_blocked_by_string(blocking_bug): + return "%s%s" % (BLOCKED_BY_TAG, blocking_bug.uuid) + +def _parse_blocks_string(string): + assert string.startswith(BLOCKS_TAG) + return string[len(BLOCKS_TAG):] + +def _parse_blocked_by_string(string): + assert string.startswith(BLOCKED_BY_TAG) + return string[len(BLOCKED_BY_TAG):] + +def _add_remove_extra_string(bug, string, add): + estrs = bug.extra_strings + if add == True: + estrs.append(string) + else: # remove the string + estrs.remove(string) + bug.extra_strings = estrs # reassign to notice change + +def _get_blocks(bug): + uuids = [] + for line in bug.extra_strings: + if line.startswith(BLOCKS_TAG): + uuids.append(_parse_blocks_string(line)) + return uuids + +def _get_blocked_by(bug): + uuids = [] + for line in bug.extra_strings: + if line.startswith(BLOCKED_BY_TAG): + uuids.append(_parse_blocked_by_string(line)) + return uuids + +def _repair_one_way_link(blocked_bug, blocking_bug, blocks=None): + if blocks == True: # add blocks link + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=True) + else: # add blocked by link + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=True) + +# functions exposed to other modules + +def add_block(blocked_bug, blocking_bug): + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=True) + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=True) + +def remove_block(blocked_bug, blocking_bug): + blocked_by_string = _generate_blocked_by_string(blocking_bug) + _add_remove_extra_string(blocked_bug, blocked_by_string, add=False) + blocks_string = _generate_blocks_string(blocked_bug) + _add_remove_extra_string(blocking_bug, blocks_string, add=False) + +def get_blocks(bugdir, bug): + """ + Return a list of bugs that the given bug blocks. + """ + blocks = [] + for uuid in _get_blocks(bug): + blocks.append(bugdir.bug_from_uuid(uuid)) + return blocks + +def get_blocked_by(bugdir, bug): + """ + Return a list of bugs blocking the given bug. + """ + blocked_by = [] + for uuid in _get_blocked_by(bug): + blocked_by.append(bugdir.bug_from_uuid(uuid)) + return blocked_by + +def check_dependencies(bugdir, repair_broken_links=False): + """ + Check that links are bi-directional for all bugs in bugdir. + + >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) + >>> a = bd.bug_from_uuid("a") + >>> b = bd.bug_from_uuid("b") + >>> blocked_by_string = _generate_blocked_by_string(b) + >>> _add_remove_extra_string(a, blocked_by_string, add=True) + >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=False) + >>> good + [] + >>> repaired + [] + >>> broken + [(Bug(uuid='a'), Bug(uuid='b'))] + >>> _get_blocks(b) + [] + >>> good,repaired,broken = check_dependencies(bd, repair_broken_links=True) + >>> _get_blocks(b) + ['a'] + >>> good + [] + >>> repaired + [(Bug(uuid='a'), Bug(uuid='b'))] + >>> broken + [] + """ + if bugdir.sync_with_disk == True: + bugdir.load_all_bugs() + good_links = [] + fixed_links = [] + broken_links = [] + for bug in bugdir: + for blocker in get_blocked_by(bugdir, bug): + blocks = get_blocks(bugdir, blocker) + if (bug, blocks) in good_links+fixed_links+broken_links: + continue # already checked that link + if bug not in blocks: + if repair_broken_links == True: + _repair_one_way_link(bug, blocker, blocks=True) + fixed_links.append((bug, blocker)) + else: + broken_links.append((bug, blocker)) + else: + good_links.append((bug, blocker)) + for blockee in get_blocks(bugdir, bug): + blocked_by = get_blocked_by(bugdir, blockee) + if (blockee, bug) in good_links+fixed_links+broken_links: + continue # already checked that link + if bug not in blocked_by: + if repair_broken_links == True: + _repair_one_way_link(blockee, bug, blocks=False) + fixed_links.append((blockee, bug)) + else: + broken_links.append((blockee, bug)) + else: + good_links.append((blockee, bug)) + return (good_links, fixed_links, broken_links) + +class DependencyTree (object): + """ + Note: should probably be DependencyDiGraph. + """ + def __init__(self, bugdir, root_bug, depth_limit=0, + allowed_status_values=None, + allowed_severity_values=None): + self.bugdir = bugdir + self.root_bug = root_bug + self.depth_limit = depth_limit + self.allowed_status_values = allowed_status_values + self.allowed_severity_values = allowed_severity_values + def _build_tree(self, child_fn): + root = tree.Tree() + root.bug = self.root_bug + root.depth = 0 + stack = [root] + while len(stack) > 0: + node = stack.pop() + if self.depth_limit > 0 and node.depth == self.depth_limit: + continue + for bug in child_fn(self.bugdir, node.bug): + if self.allowed_status_values != None \ + and not bug.status in self.allowed_status_values: + continue + if self.allowed_severity_values != None \ + and not bug.severity in self.allowed_severity_values: + continue + child = tree.Tree() + child.bug = bug + child.depth = node.depth+1 + node.append(child) + stack.append(child) + return root + def blocks_tree(self): + if not hasattr(self, "_blocks_tree"): + self._blocks_tree = self._build_tree(get_blocks) + return self._blocks_tree + def blocked_by_tree(self): + if not hasattr(self, "_blocked_by_tree"): + self._blocked_by_tree = self._build_tree(get_blocked_by) + return self._blocked_by_tree diff --git a/libbe/command/diff.py b/libbe/command/diff.py new file mode 100644 index 0000000..c5c34f9 --- /dev/null +++ b/libbe/command/diff.py @@ -0,0 +1,133 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +"""Compare bug reports with older tree""" +from libbe import cmdutil, bugdir, diff +import os +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> original = bd.vcs.commit("Original status") + >>> bug = bd.bug_from_uuid("a") + >>> bug.status = "closed" + >>> changed = bd.vcs.commit("Closed bug a") + >>> os.chdir(bd.root) + >>> if bd.vcs.versioned == True: + ... execute([original], manipulate_encodings=False) + ... else: + ... print "Modified bugs:\\n a:cm: Bug A\\n Changed bug settings:\\n status: open -> closed" + Modified bugs: + a:cm: Bug A + Changed bug settings: + status: open -> closed + >>> if bd.vcs.versioned == True: + ... execute(["--subscribe", "%(bugdir_id)s:mod", "--uuids", original], + ... manipulate_encodings=False) + ... else: + ... print "a" + a + >>> if bd.vcs.versioned == False: + ... execute([original], manipulate_encodings=False) + ... else: + ... raise cmdutil.UsageError('This directory is not revision-controlled.') + Traceback (most recent call last): + ... + UsageError: This directory is not revision-controlled. + >>> bd.cleanup() + """ % {'bugdir_id':diff.BUGDIR_ID} + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) + if len(args) == 0: + revision = None + if len(args) == 1: + revision = args[0] + if len(args) > 1: + raise cmdutil.UsageError('Too many arguments.') + try: + subscriptions = diff.subscriptions_from_string( + options.subscribe) + except ValueError, e: + raise cmdutil.UsageError(e.msg) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + if bd.vcs.versioned == False: + raise cmdutil.UsageError('This directory is not revision-controlled.') + if options.dir == None: + if revision == None: # get the most recent revision + revision = bd.vcs.revision_id(-1) + old_bd = bd.duplicate_bugdir(revision) + else: + old_bd_current = bugdir.BugDir(root=os.path.abspath(options.dir), + from_disk=True, + manipulate_encodings=False) + if revision == None: # use the current working state + old_bd = old_bd_current + else: + if old_bd_current.vcs.versioned == False: + raise cmdutil.UsageError('%s is not revision-controlled.' + % options.dir) + old_bd = old_bd_current.duplicate_bugdir(revision) + d = diff.Diff(old_bd, bd) + tree = d.report_tree(subscriptions) + + if options.uuids == True: + uuids = [] + bugs = tree.child_by_path('/bugs') + for bug_type in bugs: + uuids.extend([bug.name for bug in bug_type]) + print '\n'.join(uuids) + else : + rep = tree.report_string() + if rep != None: + print rep + bd.remove_duplicate_bugdir() + if options.dir != None and revision != None: + old_bd_current.remove_duplicate_bugdir() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be diff [options] REVISION") + parser.add_option("-d", "--dir", dest="dir", metavar="DIR", + help="Compare with repository in DIR instead of the current directory.") + parser.add_option("-s", "--subscribe", dest="subscribe", metavar="SUBSCRIPTION", + help="Only print changes matching SUBSCRIPTION, subscription is a comma-separ\ated list of ID:TYPE tuples. See `be subscribe --help` for descriptions of ID and TYPE.") + parser.add_option("-u", "--uuids", action="store_true", dest="uuids", + help="Only print the bug UUIDS.", default=False) + return parser + +longhelp=""" +Uses the VCS to compare the current tree with a previous tree, and +prints a pretty report. If REVISION is given, it is a specifier for +the particular previous tree to use. Specifiers are specific to their +VCS. + +For Arch your specifier must be a fully-qualified revision name. + +Besides the standard summary output, you can use the options to output +UUIDS for the different categories. This output can be used as the +input to 'be show' to get an understanding of the current status. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/libbe/command/due.py b/libbe/command/due.py new file mode 100644 index 0000000..0b8d1e9 --- /dev/null +++ b/libbe/command/due.py @@ -0,0 +1,108 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Set bug due dates""" +from libbe import cmdutil, bugdir, utility +__desc__ = __doc__ + +DUE_TAG="DUE:" + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> bd.save() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + No due date assigned. + >>> execute(["a", "Thu, 01 Jan 1970 00:00:00 +0000"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + Thu, 01 Jan 1970 00:00:00 +0000 + >>> execute(["a", "none"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + >>> execute(["a"], manipulate_encodings=False) + No due date assigned. + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if len(args) not in (1, 2): + raise cmdutil.UsageError('Incorrect number of arguments.') + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bug = cmdutil.bug_from_id(bd, args[0]) + if len(args) == 1: + due_time = get_due(bug) + if due_time is None: + print "No due date assigned." + else: + print utility.time_to_str(due_time) + else: + if args[1] == "none": + remove_due(bug) + else: + due_time = utility.str_to_time(args[1]) + set_due(bug, due_time) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be due BUG-ID [DATE]") + return parser + +longhelp=""" +If no DATE is specified, the bug's current due date is printed. If +DATE is specified, it will be assigned to the bug. +""" + +def help(): + return get_parser().help_str() + longhelp + +# internal helper functions + +def _generate_due_string(time): + return "%s%s" % (DUE_TAG, utility.time_to_str(time)) + +def _parse_due_string(string): + assert string.startswith(DUE_TAG) + return utility.str_to_time(string[len(DUE_TAG):]) + +# functions exposed to other modules + +def get_due(bug): + matched = [] + for line in bug.extra_strings: + if line.startswith(DUE_TAG): + matched.append(_parse_due_string(line)) + if len(matched) == 0: + return None + if len(matched) > 1: + raise Exception('Several due dates for %s?:\n %s' + % (bug.uuid, '\n '.join(matched))) + return matched[0] + +def remove_due(bug): + estrs = bug.extra_strings + for due_str in [s for s in estrs if s.startswith(DUE_TAG)]: + estrs.remove(due_str) + bug.extra_strings = estrs # reassign to notice change + +def set_due(bug, time): + remove_due(bug) + estrs = bug.extra_strings + estrs.append(_generate_due_string(time)) + bug.extra_strings = estrs # reassign to notice change diff --git a/libbe/command/email_bugs.py b/libbe/command/email_bugs.py new file mode 100644 index 0000000..f6641e3 --- /dev/null +++ b/libbe/command/email_bugs.py @@ -0,0 +1,239 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Email specified bugs in a be-handle-mail compatible format.""" + +import copy +from cStringIO import StringIO +from email import Message +from email.mime.text import MIMEText +from email.generator import Generator +import sys +import time + +from libbe import cmdutil, bugdir +from libbe.subproc import invoke +from libbe.utility import time_to_str +from libbe.vcs import detect_vcs, installed_vcs +import show + +__desc__ = __doc__ + +sendmail='/usr/sbin/sendmail -t' + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> from libbe import bug + >>> bd = bugdir.SimpleBugDir() + >>> bd.encoding = 'utf-8' + >>> os.chdir(bd.root) + >>> import email.charset as c + >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8') + >>> execute(["-o", "--to", "a@b.com", "--from", "b@c.edu", "a", "b"], + ... manipulate_encodings=False) # doctest: +ELLIPSIS + Content-Type: text/xml; charset="utf-8" + MIME-Version: 1.0 + Content-Transfer-Encoding: quoted-printable + From: b@c.edu + To: a@b.com + Date: ... + Subject: [be-bug:xml] Updates to a, b + + + + + ... + ... + ... + ... + + + a + a + minor + open + John Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug A + + + b + b + minor + closed + Jane Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug B + + + >>> bd.cleanup() + + Note that the '=3D' bits in + + are the way quoted-printable escapes '='. + + The unclosed ... is because revision ids can be long + enough to cause line wraps, and we want to ensure we match even if + the closing is split by the wrapping. + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={-1: lambda bug : bug.active==True}) + if len(args) == 0: + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + xml = show.output(args, bd, as_xml=True, with_comments=True) + subject = options.subject + if subject == None: + subject = '[be-bug:xml] Updates to %s' % ', '.join(args) + submit_email = TextEmail(to_address=options.to_address, + from_address=options.from_address, + subject=subject, + body=xml, + encoding=bd.encoding, + subtype='xml') + if options.output == True: + print submit_email + else: + submit_email.send() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be email-bugs [options] ID [ID ...]") + parser.add_option("-t", "--to", metavar="EMAIL", dest="to_address", + help="Submission email address (%default)", + default="be-devel@bugseverywhere.org") + parser.add_option("-f", "--from", metavar="EMAIL", dest="from_address", + help="Senders email address, overriding auto-generated default", + default=None) + parser.add_option("-s", "--subject", metavar="STRING", dest="subject", + help="Subject line, overriding auto-generated default. If you use this option, remember that be-handle-mail probably want something like '[be-bug:xml] ...'", + default=None) + parser.add_option('-o', '--output', dest='output', action='store_true', + help="Don't mail the generated message, print it to stdout instead. Useful for testing functionality.") + return parser + +longhelp=""" +Email specified bugs in a be-handle-mail compatible format. This is +the prefered method for reporting bugs if you did not install bzr by +branching a bzr repository. + +If you _did_ install bzr by branching a bzr repository, we suggest you +commit any new bug information with + bzr commit --message "Reported bug in demuxulizer" +and then email a bzr merge directive with + bzr send --mail-to "be-devel@bugseverywhere.org" +rather than using this command. +""" + +def help(): + return get_parser().help_str() + longhelp + +class TextEmail (object): + """ + Make it very easy to compose and send single-part text emails. + >>> msg = TextEmail(to_address='Monty ', + ... from_address='Python ', + ... subject='Parrots', + ... header={'x-special-header':'your info here'}, + ... body="Remarkable bird, id'nit, squire?\\nLovely plumage!") + >>> print msg # doctest: +ELLIPSIS + Content-Type: text/plain; charset="utf-8" + MIME-Version: 1.0 + Content-Transfer-Encoding: base64 + From: Python + To: Monty + Date: ... + Subject: Parrots + x-special-header: your info here + + UmVtYXJrYWJsZSBiaXJkLCBpZCduaXQsIHNxdWlyZT8KTG92ZWx5IHBsdW1hZ2Uh + + >>> import email.charset as c + >>> c.add_charset('utf-8', c.SHORTEST, c.QP, 'utf-8') + >>> print msg # doctest: +ELLIPSIS + Content-Type: text/plain; charset="utf-8" + MIME-Version: 1.0 + Content-Transfer-Encoding: quoted-printable + From: Python + To: Monty + Date: ... + Subject: Parrots + x-special-header: your info here + + Remarkable bird, id'nit, squire? + Lovely plumage! + """ + def __init__(self, to_address, from_address=None, subject=None, + header=None, body=None, encoding='utf-8', subtype='plain'): + self.to_address = to_address + self.from_address = from_address + if self.from_address == None: + self.from_address = self._guess_from_address() + self.subject = subject + self.header = header + if self.header == None: + self.header = {} + self.body = body + self.encoding = encoding + self.subtype = subtype + def _guess_from_address(self): + vcs = detect_vcs('.') + if vcs.name == "None": + vcs = installed_vcs() + return vcs.get_user_id() + def encoded_MIME_body(self): + return MIMEText(self.body.encode(self.encoding), + self.subtype, + self.encoding) + def message(self): + response = self.encoded_MIME_body() + response['From'] = self.from_address + response['To'] = self.to_address + response['Date'] = time_to_str(time.time()) + response['Subject'] = self.subject + for k,v in self.header.items(): + response[k] = v + return response + def flatten(self, to_unicode=False): + """ + This is a simplified version of send_pgp_mime.flatten(). + """ + fp = StringIO() + g = Generator(fp, mangle_from_=False) + g.flatten(self.message()) + text = fp.getvalue() + if to_unicode == True: + encoding = msg.get_content_charset() or "utf-8" + text = unicode(text, encoding=encoding) + return text + def __str__(self): + return self.flatten() + def __unicode__(self): + return self.flatten(to_unicode=True) + def send(self, sendmail=None): + """ + This is a simplified version of send_pgp_mime.mail(). + + Send an email Message instance on its merry way by shelling + out to the user specified sendmail. + """ + if sendmail == None: + sendmail = SENDMAIL + invoke(sendmail, stdin=self.flatten()) diff --git a/libbe/command/help.py b/libbe/command/help.py new file mode 100644 index 0000000..9e6d1aa --- /dev/null +++ b/libbe/command/help.py @@ -0,0 +1,70 @@ +# Copyright (C) 2006-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# Thomas Gerigk +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Print help for given subcommand""" +from libbe import cmdutil, utility +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + Print help of specified command (the manipulate_encodings argument + is ignored). + + >>> execute(["help"]) + Usage: be help [COMMAND] + + Options: + -h, --help Print a help message + --complete Print a list of available completions + + Print help for specified command or list of all commands. + + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) > 1: + raise cmdutil.UsageError("Too many arguments.") + if len(args) == 0: + print cmdutil.help() + else: + try: + print cmdutil.help(args[0]) + except AttributeError: + print "No help available" + +def get_parser(): + parser = cmdutil.CmdOptionParser("be help [COMMAND]") + return parser + +longhelp=""" +Print help for specified command or list of all commands. +""" + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + if "--complete" in args: + cmds = [command for command,module in cmdutil.iter_commands()] + raise cmdutil.GetCompletions(cmds) diff --git a/libbe/command/html.py b/libbe/command/html.py new file mode 100644 index 0000000..d9e0d73 --- /dev/null +++ b/libbe/command/html.py @@ -0,0 +1,609 @@ +# Copyright (C) 2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Generate a static HTML dump of the current repository status""" +from libbe import cmdutil, bugdir, bug +import codecs, os, os.path, re, string, time +import xml.sax.saxutils, htmlentitydefs + +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute([], manipulate_encodings=False) + >>> os.path.exists("./html_export") + True + >>> os.path.exists("./html_export/index.html") + True + >>> os.path.exists("./html_export/index_inactive.html") + True + >>> os.path.exists("./html_export/bugs") + True + >>> os.path.exists("./html_export/bugs/a.html") + True + >>> os.path.exists("./html_export/bugs/b.html") + True + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + cmdutil.default_complete(options, args, parser) + + if len(args) > 0: + raise cmdutil.UsageError, 'Too many arguments.' + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bd.load_all_bugs() + + html_gen = HTMLGen(bd, template=options.template, verbose=options.verbose, + title=options.title, index_header=options.index_header) + if options.exp_template == True: + html_gen.write_default_template(options.exp_template_dir) + return + html_gen.run(options.out_dir) + +def get_parser(): + parser = cmdutil.CmdOptionParser('be html [options]') + parser.add_option('-o', '--output', metavar='DIR', dest='out_dir', + help='Set the output path (%default)', default='./html_export') + parser.add_option('-t', '--template-dir', metavar='DIR', dest='template', + help='Use a different template, defaults to internal templates', + default=None) + parser.add_option('--title', metavar='STRING', dest='title', + help='Set the bug repository title (%default)', + default='BugsEverywhere Issue Tracker') + parser.add_option('--index-header', metavar='STRING', dest='index_header', + help='Set the index page headers (%default)', + default='BugsEverywhere Bug List') + parser.add_option('-v', '--verbose', action='store_true', + metavar='verbose', dest='verbose', + help='Verbose output, default is %default', default=False) + parser.add_option('-e', '--export-template', action='store_true', + dest='exp_template', + help='Export the default template and exit.', default=False) + parser.add_option('-d', '--export-template-dir', metavar='DIR', + dest='exp_template_dir', default='./default-templates/', + help='Set the directory for the template export (%default)') + return parser + +longhelp=""" +Generate a set of html pages representing the current state of the bug +directory. +""" + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if "--complete" in args: + raise cmdutil.GetCompletions() # no positional arguments for list + +class HTMLGen (object): + def __init__(self, bd, template=None, verbose=False, encoding=None, + title="Site Title", index_header="Index Header", + ): + self.generation_time = time.ctime() + self.bd = bd + self.verbose = verbose + self.title = title + self.index_header = index_header + if encoding != None: + self.encoding = encoding + else: + self.encoding = self.bd.encoding + if template == None: + self.template = "default" + else: + self.template = os.path.abspath(os.path.expanduser(template)) + self._load_default_templates() + + if template != None: + self._load_user_templates() + + def run(self, out_dir): + if self.verbose == True: + print "Creating the html output in %s using templates in %s" \ + % (out_dir, self.template) + + bugs_active = [] + bugs_inactive = [] + bugs = [b for b in self.bd] + bugs.sort() + bugs_active = [b for b in bugs if b.active == True] + bugs_inactive = [b for b in bugs if b.active != True] + + self._create_output_directories(out_dir) + self._write_css_file() + for b in bugs: + if b.active: + up_link = "../index.html" + else: + up_link = "../index_inactive.html" + self._write_bug_file(b, up_link) + self._write_index_file( + bugs_active, title=self.title, + index_header=self.index_header, bug_type="active") + self._write_index_file( + bugs_inactive, title=self.title, + index_header=self.index_header, bug_type="inactive") + + def _create_output_directories(self, out_dir): + if self.verbose: + print "Creating output directories" + self.out_dir = self._make_dir(out_dir) + self.out_dir_bugs = self._make_dir( + os.path.join(self.out_dir, "bugs")) + + def _write_css_file(self): + if self.verbose: + print "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"]) + + def _write_bug_file(self, bug, up_link): + if self.verbose: + print "\tCreating bug file for %s" % self.bd.bug_shortname(bug) + assert hasattr(self, "out_dir_bugs"), \ + "Must run after ._create_output_directories()" + + bug.load_comments(load_full=True) + comment_entries = self._generate_bug_comment_entries(bug) + filename = "%s.html" % bug.uuid + fullpath = os.path.join(self.out_dir_bugs, filename) + template_info = {'title':self.title, + 'charset':self.encoding, + 'up_link':up_link, + 'shortname':self.bd.bug_shortname(bug), + 'comment_entries':comment_entries, + 'generation_time':self.generation_time} + for attr in ['uuid', 'severity', 'status', 'assigned', + 'reporter', 'creator', 'time_string', 'summary']: + template_info[attr] = self._escape(getattr(bug, attr)) + self._write_file(self.bug_file % template_info, [fullpath]) + + def _generate_bug_comment_entries(self, bug): + assert hasattr(self, "out_dir_bugs"), \ + "Must run after ._create_output_directories()" + + stack = [] + comment_entries = [] + 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
\n") + assert len(stack) == depth + stack.append(comment) + if depth == 0: + comment_entries.append('
') + else: + comment_entries.append('
') + template_info = {} + for attr in ['uuid', 'author', 'date', 'body']: + value = getattr(comment, attr) + if attr == 'body': + save_body = False + if comment.content_type == 'text/html': + pass # no need to escape html... + elif comment.content_type.startswith('text/'): + value = '
\n'+self._escape(value)+'\n
' + elif comment.content_type.startswith('image/'): + save_body = True + value = '' \ + % (bug.uuid, comment.uuid) + else: + save_body = True + value = 'Link to %s file.' \ + % (bug.uuid, comment.uuid, comment.content_type) + 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( + '\n ForceType %s\n' \ + % (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("
\n") # close every remaining
+ + + %(title)s + + + + + +
+

%(index_header)s

+

+ + + + + + + +
Active BugsInactive Bugs
+ + + + %(bug_entries)s + + +
+
+ + + + + + """ + + self.index_bug_entry =""" + + %(shortname)s + %(status)s + %(severity)s + %(summary)s + %(time_string)s + + """ + + self.bug_file = """ + + + + %(title)s + + + + + +
+

BugsEverywhere Bug List

+
Back to Index
+

Bug: %(shortname)s

+ + + + + + + + + + + + + + + + + + + + + + +
ID :%(uuid)s
Short name :%(shortname)s
Status :%(status)s
Severity :%(severity)s
Assigned :%(assigned)s
Reporter :%(reporter)s
Creator :%(creator)s
Created :%(time_string)s
Summary :%(summary)s
+ +
+ + %(comment_entries)s + +
+
Back to Index
+ + + + + + """ + + self.bug_comment_entry =""" + + + + + +
Comment: + --------- Comment ---------
+ Name: %(uuid)s
+ From: %(author)s
+ Date: %(date)s
+
+ %(body)s +
+ """ + + # 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') diff --git a/libbe/command/import_xml.py b/libbe/command/import_xml.py new file mode 100644 index 0000000..b985193 --- /dev/null +++ b/libbe/command/import_xml.py @@ -0,0 +1,434 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Import comments and bugs from XML""" +import libbe +from libbe import cmdutil, bugdir, bug, comment, utility +from becommands.comment import complete +import copy +import os +import sys +try: # import core module, Python >= 2.5 + from xml.etree import ElementTree +except ImportError: # look for non-core module + from elementtree import ElementTree +if libbe.TESTING == True: + import doctest + import unittest +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import time + >>> import StringIO + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> orig_stdin = sys.stdin + >>> sys.stdin = StringIO.StringIO("cThis is a comment about a") + >>> execute(["-c", "a", "-"], manipulate_encodings=False) + >>> sys.stdin = orig_stdin + >>> bd._clear_bugs() + >>> bug = cmdutil.bug_from_id(bd, "a") + >>> bug.load_comments(load_full=False) + >>> comment = bug.comment_root[0] + >>> print comment.body + This is a comment about a + + >>> comment.author == bd.user_id + True + >>> comment.time <= int(time.time()) + True + >>> comment.in_reply_to is None + True + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) < 1: + raise cmdutil.UsageError("Please specify an XML file.") + if len(args) > 1: + raise cmdutil.UsageError("Too many arguments.") + filename = args[0] + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + if options.comment_root != None: + croot_bug,croot_comment = \ + cmdutil.bug_comment_from_id(bd, options.comment_root) + croot_bug.load_comments(load_full=True) + croot_bug.set_sync_with_disk(False) + if croot_comment.uuid == comment.INVALID_UUID: + croot_comment = croot_bug.comment_root + else: + croot_comment = croot_bug.comment_from_uuid(croot_comment.uuid) + new_croot_bug = bug.Bug(bugdir=bd, uuid=croot_bug.uuid) + new_croot_bug.explicit_attrs = [] + new_croot_bug.comment_root = copy.deepcopy(croot_bug.comment_root) + if croot_comment.uuid == comment.INVALID_UUID: + new_croot_comment = new_croot_bug.comment_root + else: + new_croot_comment = \ + new_croot_bug.comment_from_uuid(croot_comment.uuid) + for new in new_croot_bug.comments(): + new.explicit_attrs = [] + else: + croot_bug,croot_comment = (None, None) + + if filename == '-': + xml = sys.stdin.read() + else: + if restrict_file_access == True: + cmdutil.restrict_file_access(bd, options.body) + xml = bd.vcs.get_file_contents(filename, allow_no_vcs=True) + str_xml = xml.encode('unicode_escape').replace(r'\n', '\n') + # unicode read + encode to string so we know the encoding, + # which might not be given (?) in a binary string read? + + # parse the xml + root_bugs = [] + root_comments = [] + version = {} + be_xml = ElementTree.XML(str_xml) + if be_xml.tag != 'be-xml': + raise utility.InvalidXML( + 'import-xml', be_xml, 'root element must be ') + for child in be_xml.getchildren(): + if child.tag == 'bug': + new = bug.Bug(bugdir=bd) + new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape")) + root_bugs.append(new) + elif child.tag == 'comment': + new = comment.Comment(croot_bug) + new.from_xml(unicode(ElementTree.tostring(child)).decode("unicode_escape")) + root_comments.append(new) + elif child.tag == 'version': + for gchild in child.getchildren(): + if child.tag in ['tag', 'nick', 'revision', 'revision-id']: + text = xml.sax.saxutils.unescape(child.text) + text = text.decode('unicode_escape').strip() + version[child.tag] = text + else: + print >> sys.stderr, 'ignoring unknown tag %s in %s' \ + % (gchild.tag, child.tag) + else: + print >> sys.stderr, 'ignoring unknown tag %s in %s' \ + % (child.tag, comment_list.tag) + + # merge the new root_comments + if options.add_only == True: + accept_changes = False + accept_extra_strings = False + else: + accept_changes = True + accept_extra_strings = True + accept_comments = True + if len(root_comments) > 0: + if croot_bug == None: + raise UserError( + '--comment-root option is required for your root comments:\n%s' + % '\n\n'.join([c.string() for c in root_comments])) + try: + # link new comments + new_croot_bug.add_comments(root_comments, + default_parent=new_croot_comment, + ignore_missing_references= \ + options.ignore_missing_references) + except comment.MissingReference, e: + raise cmdutil.UserError(e) + croot_bug.merge(new_croot_bug, accept_changes=accept_changes, + accept_extra_strings=accept_extra_strings, + accept_comments=accept_comments) + + # merge the new croot_bugs + merged_bugs = [] + old_bugs = [] + for new in root_bugs: + try: + old = bd.bug_from_uuid(new.alt_id) + except KeyError: + old = None + if old == None: + bd.append(new) + else: + old.load_comments(load_full=True) + old.merge(new, accept_changes=accept_changes, + accept_extra_strings=accept_extra_strings, + accept_comments=accept_comments) + merged_bugs.append(new) + old_bugs.append(old) + + # protect against programmer error causing data loss: + if croot_bug != None: + comms = [c.uuid for c in croot_comment.traverse()] + for new in root_comments: + assert new.uuid in comms, \ + "comment %s wasn't added to %s" % (new.uuid, croot_comment.uuid) + for new in root_bugs: + if not new in merged_bugs: + assert bd.has_bug(new.uuid), \ + "bug %s wasn't added" % (new.uuid) + + # save new information + if croot_bug != None: + croot_bug.save() + for new in root_bugs: + if not new in merged_bugs: + new.save() + for old in old_bugs: + old.save() + +def get_parser(): + parser = cmdutil.CmdOptionParser("be import-xml XMLFILE") + parser.add_option("-i", "--ignore-missing-references", action="store_true", + dest="ignore_missing_references", default=False, + help="If any comment's refers to a non-existent comment, ignore it (instead of raising an exception).") + parser.add_option("-a", "--add-only", action='store_true', + dest="add_only", default=False, + help="If any bug or comment listed in the XML file already exists in the bug repository, do not alter the repository version.") + parser.add_option("-c", "--comment-root", dest="comment_root", + help="Supply a bug or comment ID as the root of any elements that are direct children of the element. If any such elements exist, you are required to set this option.") + return parser + +longhelp=""" +Import comments and bugs from XMLFILE. If XMLFILE is '-', the file is +read from stdin. + +This command provides a fallback mechanism for passing bugs between +repositories, in case the repositories VCSs are incompatible. If the +VCSs are compatible, it's better to use their builtin merge/push/pull +to share this information, as that will preserve a more detailed +history. + +The XML file should be formatted similarly to + + + 1.0.0 + be + 446 + a@b.com-20091119214553-iqyw2cpqluww3zna + + + ... + ... + ... + + ... + ... + ... + +where the ellipses mark output commpatible with Bug.xml() and +Comment.xml(). Take a look at the output of `be show --xml` for some +explicit examples. Unrecognized tags are ignored. Missing tags are +left at the default value. The version tag is not required, but is +strongly recommended. + +The bug and comment UUIDs are always auto-generated, so if you set a + field, but no field, your will be used as the +comment's . An exception is raised if conflicts with +an existing comment. Bugs do not have a permantent alt-id, so they +the s you specify are not saved. The s _are_ used to +match agains prexisting bug and comment uuids, and comment alt-ids, +and fields explicitly given in the XML file will replace old versions +unless the --add-only flag. + +*.extra_strings recieves special treatment, and if --add-only is not +set, the resulting list concatenates both source lists and removes +repeats. + +Here's an example of import activity: + Repository + bug (uuid=B, creator=John, status=open) + estr (don't forget your towel) + estr (helps with space travel) + com (uuid=C1, author=Jane, body=Hello) + com (uuid=C2, author=Jess, body=World) + XML + bug (uuid=B, status=fixed) + estr (don't forget your towel) + estr (watch out for flying dolphins) + com (uuid=C1, body=So long) + com (uuid=C3, author=Jed, body=And thanks) + Result + bug (uuid=B, creator=John, status=fixed) + estr (don't forget your towel) + estr (helps with space travel) + estr (watch out for flying dolphins) + com (uuid=C1, author=Jane, body=So long) + com (uuid=C2, author=Jess, body=World) + com (uuid=C4, alt-id=C3, author=Jed, body=And thanks) + Result, with --add-only + bug (uuid=B, creator=John, status=open) + estr (don't forget your towel) + estr (helps with space travel) + com (uuid=C1, author=Jane, body=Hello) + com (uuid=C2, author=Jess, body=World) + com (uuid=C4, alt-id=C3, author=Jed, body=And thanks) + +Examples: + +Import comments (e.g. emails from an mbox) and append to bug XYZ + $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ - +Or you can append those emails underneath the prexisting comment XYZ-3 + $ be-mbox-to-xml mail.mbox | be import-xml --c XYZ-3 - + +User creates a new bug + user$ be new "The demuxulizer is broken" + Created bug with ID 48f + user$ be comment 48f + + ... +User exports bug as xml and emails it to the developers + user$ be show --xml 48f > 48f.xml + user$ cat 48f.xml | mail -s "Demuxulizer bug xml" devs@b.com +or equivalently (with a slightly fancier be-handle-mail compatible +email): + user$ be email-bugs 48f +Devs recieve email, and save it's contents as demux-bug.xml + dev$ cat demux-bug.xml | be import-xml - +""" + +def help(): + return get_parser().help_str() + longhelp + + +if libbe.TESTING == True: + class LonghelpTestCase (unittest.TestCase): + """ + Test import scenarios given in longhelp. + """ + def setUp(self): + self.bugdir = bugdir.SimpleBugDir() + self.original_working_dir = os.getcwd() + os.chdir(self.bugdir.root) + bugA = self.bugdir.bug_from_uuid('a') + self.bugdir.remove_bug(bugA) + self.bugdir.set_sync_with_disk(False) + bugB = self.bugdir.bug_from_uuid('b') + bugB.creator = 'John' + bugB.status = 'open' + bugB.extra_strings += ["don't forget your towel"] + bugB.extra_strings += ['helps with space travel'] + comm1 = bugB.comment_root.new_reply(body='Hello\n') + comm1.uuid = 'c1' + comm1.author = 'Jane' + comm2 = bugB.comment_root.new_reply(body='World\n') + comm2.uuid = 'c2' + comm2.author = 'Jess' + bugB.save() + self.bugdir.set_sync_with_disk(True) + self.xml = """ + + + b + fixed + a test bug + don't forget your towel + watch out for flying dolphins + + c1 + So long + + + c3 + Jed + And thanks + + + + """ + def tearDown(self): + os.chdir(self.original_working_dir) + self.bugdir.cleanup() + def _execute(self, *args): + import StringIO + orig_stdin = sys.stdin + sys.stdin = StringIO.StringIO(self.xml) + execute(list(args)+["-"], manipulate_encodings=False, + restrict_file_access=True) + sys.stdin = orig_stdin + self.bugdir._clear_bugs() + def testCleanBugdir(self): + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + def testNotAddOnly(self): + self._execute() + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + bugB = self.bugdir.bug_from_uuid('b') + self.failUnless(bugB.uuid == 'b', bugB.uuid) + self.failUnless(bugB.creator == 'John', bugB.creator) + self.failUnless(bugB.status == 'fixed', bugB.status) + estrs = ["don't forget your towel", + 'helps with space travel', + 'watch out for flying dolphins'] + self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) + comments = list(bugB.comments()) + self.failUnless(len(comments) == 3, + ['%s (%s, %s)' % (c.uuid, c.alt_id, c.body) + for c in comments]) + c1 = bugB.comment_from_uuid('c1') + comments.remove(c1) + self.failUnless(c1.uuid == 'c1', c1.uuid) + self.failUnless(c1.alt_id == None, c1.alt_id) + self.failUnless(c1.author == 'Jane', c1.author) + self.failUnless(c1.body == 'So long\n', c1.body) + c2 = bugB.comment_from_uuid('c2') + comments.remove(c2) + self.failUnless(c2.uuid == 'c2', c2.uuid) + self.failUnless(c2.alt_id == None, c2.alt_id) + self.failUnless(c2.author == 'Jess', c2.author) + self.failUnless(c2.body == 'World\n', c2.body) + c4 = comments[0] + self.failUnless(len(c4.uuid) == 36, c4.uuid) + self.failUnless(c4.alt_id == 'c3', c4.alt_id) + self.failUnless(c4.author == 'Jed', c4.author) + self.failUnless(c4.body == 'And thanks\n', c4.body) + def testAddOnly(self): + self._execute('--add-only') + uuids = list(self.bugdir.uuids()) + self.failUnless(uuids == ['b'], uuids) + bugB = self.bugdir.bug_from_uuid('b') + self.failUnless(bugB.uuid == 'b', bugB.uuid) + self.failUnless(bugB.creator == 'John', bugB.creator) + self.failUnless(bugB.status == 'open', bugB.status) + estrs = ["don't forget your towel", + 'helps with space travel'] + self.failUnless(bugB.extra_strings == estrs, bugB.extra_strings) + comments = list(bugB.comments()) + self.failUnless(len(comments) == 3, + ['%s (%s)' % (c.uuid, c.alt_id) for c in comments]) + c1 = bugB.comment_from_uuid('c1') + comments.remove(c1) + self.failUnless(c1.uuid == 'c1', c1.uuid) + self.failUnless(c1.alt_id == None, c1.alt_id) + self.failUnless(c1.author == 'Jane', c1.author) + self.failUnless(c1.body == 'Hello\n', c1.body) + c2 = bugB.comment_from_uuid('c2') + comments.remove(c2) + self.failUnless(c2.uuid == 'c2', c2.uuid) + self.failUnless(c2.alt_id == None, c2.alt_id) + self.failUnless(c2.author == 'Jess', c2.author) + self.failUnless(c2.body == 'World\n', c2.body) + c4 = comments[0] + self.failUnless(len(c4.uuid) == 36, c4.uuid) + self.failUnless(c4.alt_id == 'c3', c4.alt_id) + self.failUnless(c4.author == 'Jed', c4.author) + self.failUnless(c4.body == 'And thanks\n', c4.body) + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/command/init.py b/libbe/command/init.py new file mode 100644 index 0000000..ab9255b --- /dev/null +++ b/libbe/command/init.py @@ -0,0 +1,104 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Assign the root directory for bug tracking""" +import os.path +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> from libbe import utility, vcs + >>> import os + >>> dir = utility.Dir() + >>> try: + ... bugdir.BugDir(dir.path) + ... except bugdir.NoBugDir, e: + ... True + True + >>> execute([], manipulate_encodings=False, dir=dir.path) + No revision control detected. + Directory initialized. + >>> dir.cleanup() + + >>> dir = utility.Dir() + >>> os.chdir(dir.path) + >>> _vcs = vcs.installed_vcs() + >>> _vcs.init('.') + >>> _vcs.name in vcs.VCS_ORDER + True + >>> execute([], manipulate_encodings=False) # doctest: +ELLIPSIS + Using ... for revision control. + Directory initialized. + >>> _vcs.cleanup() + + >>> try: + ... execute([], manipulate_encodings=False, dir=".") + ... except cmdutil.UserError, e: + ... str(e).startswith("Directory already initialized: ") + True + >>> execute([], manipulate_encodings=False, + ... dir='/highly-unlikely-to-exist') + Traceback (most recent call last): + UserError: No such directory: /highly-unlikely-to-exist + >>> os.chdir('/') + >>> dir.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) + if len(args) > 0: + raise cmdutil.UsageError + try: + bd = bugdir.BugDir(from_disk=False, + sink_to_existing_root=False, + assert_new_BugDir=True, + manipulate_encodings=manipulate_encodings, + root=dir) + except bugdir.NoRootEntry: + raise cmdutil.UserError("No such directory: %s" % dir) + except bugdir.AlreadyInitialized: + raise cmdutil.UserError("Directory already initialized: %s" % dir) + bd.save() + if bd.vcs.name is not "None": + print "Using %s for revision control." % bd.vcs.name + else: + print "No revision control detected." + print "Directory initialized." + +def get_parser(): + parser = cmdutil.CmdOptionParser("be init") + return parser + +longhelp=""" +This command initializes Bugs Everywhere support for the specified directory +and all its subdirectories. It will auto-detect any supported revision control +system. You can use "be set vcs_name" to change the vcs being used. + +The directory defaults to your current working directory, but you can +change that by passing the --dir option to be + $ be --dir path/to/new/bug/root init + +It is usually a good idea to put the Bugs Everywhere root at the source code +root, but you can put it anywhere. If you root Bugs Everywhere in a +subdirectory, then only bugs created in that subdirectory (and its children) +will appear there. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/libbe/command/list.py b/libbe/command/list.py new file mode 100644 index 0000000..1c3e78d --- /dev/null +++ b/libbe/command/list.py @@ -0,0 +1,240 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# Oleg Romanyshyn +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""List bugs""" +from libbe import cmdutil, bugdir, bug +import os +import re +__desc__ = __doc__ + +# get a list of * for cmp_*() comparing two bugs. +AVAILABLE_CMPS = [fn[4:] for fn in dir(bug) if fn[:4] == 'cmp_'] +AVAILABLE_CMPS.remove("attr") # a cmp_* template. + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute([], manipulate_encodings=False) + a:om: Bug A + >>> execute(["--status", "closed"], manipulate_encodings=False) + b:cm: Bug B + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) > 0: + raise cmdutil.UsageError("Too many arguments.") + cmp_list = [] + if options.sort_by != None: + for cmp in options.sort_by.split(','): + if cmp not in AVAILABLE_CMPS: + raise cmdutil.UserError( + "Invalid sort on '%s'.\nValid sorts:\n %s" + % (cmp, '\n '.join(AVAILABLE_CMPS))) + cmp_list.append(eval('bug.cmp_%s' % cmp)) + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bd.load_all_bugs() + # select status + if options.status != None: + if options.status == "all": + status = bug.status_values + else: + status = cmdutil.select_values(options.status, bug.status_values) + else: + status = [] + if options.active == True: + status.extend(list(bug.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 = bug.active_status_values + # select severity + if options.severity != None: + if options.severity == "all": + severity = bug.severity_values + else: + severity = cmdutil.select_values(options.severity, + bug.severity_values) + else: + severity = [] + if options.wishlist == True: + severity.extend("wishlist") + if options.important == True: + serious = bug.severity_values.index("serious") + severity.append(list(bug.severity_values[serious:])) + if severity == []: # set the default value + severity = bug.severity_values + # select assigned + if options.assigned != None: + if options.assigned == "all": + assigned = "all" + else: + possible_assignees = [] + for _bug in bd: + if _bug.assigned != None \ + and not _bug.assigned in possible_assignees: + possible_assignees.append(_bug.assigned) + assigned = cmdutil.select_values(options.assigned, + possible_assignees) + print 'assigned', assigned + 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] = bd.user_id + if options.extra_strings != None: + extra_string_regexps = [re.compile(x) for x in options.extra_strings.split(',')] + + def filter(bug): + if status != "all" and not bug.status in status: + return False + if severity != "all" and not bug.severity in severity: + return False + if assigned != "all" and not bug.assigned in assigned: + return False + if options.extra_strings != None: + if len(bug.extra_strings) == 0 and len(extra_string_regexps) > 0: + return False + for string in bug.extra_strings: + for regexp in extra_string_regexps: + if not regexp.match(string): + return False + return True + + bugs = [b for b in bd if filter(b) ] + if len(bugs) == 0 and options.xml == False: + print "No matching bugs found" + + def list_bugs(cur_bugs, title=None, just_uuids=False, xml=False): + if xml == True: + print '' % bd.encoding + print "" + if len(cur_bugs) > 0: + if title != None and xml == False: + print cmdutil.underlined(title) + for bg in cur_bugs: + if xml == True: + print bg.xml(show_comments=True) + elif just_uuids: + print bg.uuid + else: + print bg.string(shortlist=True) + if xml == True: + print "" + + # sort bugs + cmp_list.extend(bug.DEFAULT_CMP_FULL_CMP_LIST) + cmp_fn = bug.BugCompoundComparator(cmp_list=cmp_list) + bugs.sort(cmp_fn) + + # print list of bugs + list_bugs(bugs, just_uuids=options.uuids, xml=options.xml) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be list [options]") + parser.add_option("--status", dest="status", metavar="STATUS", + help="Only show bugs matching the STATUS specifier") + parser.add_option("--severity", dest="severity", metavar="SEVERITY", + help="Only show bugs matching the SEVERITY specifier") + parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", + help="List bugs matching ASSIGNED", default=None) + parser.add_option("-e", "--extra-strings", metavar="STRINGS", dest="extra_strings", + help="List bugs matching _all_ extra strings in comma-seperated list STRINGS. e.g. --extra-strings TAG:working,TAG:xml", default=None) + parser.add_option("-S", "--sort", metavar="SORT-BY", dest="sort_by", + help="Adjust bug-sort criteria with comma-separated list SORT-BY. e.g. \"--sort creator,time\". Available criteria: %s" % ','.join(AVAILABLE_CMPS), default=None) + # boolean options. All but uuids and xml are special cases of long forms + bools = (("u", "uuids", "Only print the bug UUIDS"), + ("x", "xml", "Dump as XML"), + ("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")) + 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, default=False) + return parser + + +def help(): + longhelp=""" +This command lists bugs. Normally it prints a short string like + 576:om: Allow attachments +Where + 576 the bug id + o the bug status is 'open' (first letter) + m the bug severity is 'minor' (first letter) + Allo... the bug summary string + +You can optionally (-u) print only the bug ids. + +There are several criteria that you can filter by: + * status + * severity + * assigned (who the bug is assigned to) +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. As with the --status and +--severity options for `be depend`, starting the list with a minus +sign makes your selections a blacklist instead of the default +whitelist. + +status + %s +severity + %s +assigned + free form, with the string '-' being a shortcut for yourself. + +In addition, there are some shortcut options that set boolean flags. +The boolean options are ignored if the matching string option is used. +""" % (','.join(bug.status_values), + ','.join(bug.severity_values)) + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + if option == "status": + raise cmdutil.GetCompletions(bug.status_values) + elif option == "severity": + raise cmdutil.GetCompletions(bug.severity_values) + raise cmdutil.GetCompletions() + if "--complete" in args: + raise cmdutil.GetCompletions() # no positional arguments for list diff --git a/libbe/command/merge.py b/libbe/command/merge.py new file mode 100644 index 0000000..ac09b40 --- /dev/null +++ b/libbe/command/merge.py @@ -0,0 +1,166 @@ +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Merge duplicate bugs""" +from libbe import cmdutil, bugdir +import os, copy +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> from libbe import utility + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> a = bd.bug_from_shortname("a") + >>> a.comment_root.time = 0 + >>> dummy = a.new_comment("Testing") + >>> dummy.time = 1 + >>> dummy = dummy.new_reply("Testing...") + >>> dummy.time = 2 + >>> b = bd.bug_from_shortname("b") + >>> b.status = "open" + >>> b.comment_root.time = 0 + >>> dummy = b.new_comment("1 2") + >>> dummy.time = 1 + >>> dummy = dummy.new_reply("1 2 3 4") + >>> dummy.time = 2 + >>> os.chdir(bd.root) + >>> execute(["a", "b"], manipulate_encodings=False) + Merging bugs a and b + >>> bd._clear_bugs() + >>> a = bd.bug_from_shortname("a") + >>> a.load_comments() + >>> mergeA = a.comment_from_shortname(":3") + >>> mergeA.time = 3 + >>> print a.string(show_comments=True) # doctest: +ELLIPSIS + ID : a + Short name : a + Severity : minor + Status : open + Assigned : + Reporter : + Creator : John Doe + Created : ... + Bug A + --------- Comment --------- + Name: a:1 + From: ... + Date: ... + + Testing + --------- Comment --------- + Name: a:2 + From: ... + Date: ... + + Testing... + --------- Comment --------- + Name: a:3 + From: ... + Date: ... + + Merged from bug b + --------- Comment --------- + Name: a:4 + From: ... + Date: ... + + 1 2 + --------- Comment --------- + Name: a:5 + From: ... + Date: ... + + 1 2 3 4 + >>> b = bd.bug_from_shortname("b") + >>> b.load_comments() + >>> mergeB = b.comment_from_shortname(":3") + >>> mergeB.time = 3 + >>> print b.string(show_comments=True) # doctest: +ELLIPSIS + ID : b + Short name : b + Severity : minor + Status : closed + Assigned : + Reporter : + Creator : Jane Doe + Created : ... + Bug B + --------- Comment --------- + Name: b:1 + From: ... + Date: ... + + 1 2 + --------- Comment --------- + Name: b:2 + From: ... + Date: ... + + 1 2 3 4 + --------- Comment --------- + Name: b:3 + From: ... + Date: ... + + Merged into bug a + >>> print b.status + closed + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True, + 1: lambda bug : bug.active==True}) + + if len(args) < 2: + raise cmdutil.UsageError("Please specify two bug ids.") + if len(args) > 2: + help() + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bugA = cmdutil.bug_from_id(bd, args[0]) + bugA.load_comments() + bugB = cmdutil.bug_from_id(bd, args[1]) + bugB.load_comments() + mergeA = bugA.new_comment("Merged from bug %s" % bugB.uuid) + newCommTree = copy.deepcopy(bugB.comment_root) + for comment in newCommTree.traverse(): # all descendant comments + comment.bug = bugA + comment.save() # force onto disk under bugA + for comment in newCommTree: # just the child comments + mergeA.add_reply(comment, allow_time_inversion=True) + bugB.new_comment("Merged into bug %s" % bugA.uuid) + bugB.status = "closed" + print "Merging bugs %s and %s" % (bugA.uuid, bugB.uuid) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be merge BUG-ID BUG-ID") + return parser + +longhelp=""" +The second bug (B) is merged into the first (A). This adds merge +comments to both bugs, closes B, and appends B's comment tree to A's +merge comment. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/libbe/command/new.py b/libbe/command/new.py new file mode 100644 index 0000000..30f8834 --- /dev/null +++ b/libbe/command/new.py @@ -0,0 +1,83 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Create a new bug""" +from libbe import cmdutil, bugdir +import sys +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os, time + >>> from libbe import bug + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> bug.uuid_gen = lambda: "X" + >>> execute (["this is a test",], manipulate_encodings=False) + Created bug with ID X + >>> bd._clear_bugs() + >>> bug = bd.bug_from_uuid("X") + >>> print bug.summary + this is a test + >>> bug.time <= int(time.time()) + True + >>> print bug.severity + minor + >>> print bug.status + open + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) + if len(args) != 1: + raise cmdutil.UsageError("Please supply a summary message") + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + if args[0] == '-': # read summary from stdin + summary = sys.stdin.readline() + else: + summary = args[0] + bug = bd.new_bug(summary=summary.strip()) + if options.reporter != None: + bug.reporter = options.reporter + else: + bug.reporter = bug.creator + if options.assigned != None: + bug.assigned = options.assigned + elif bd.default_assignee != None: + bug.assigned = bd.default_assignee + print "Created bug with ID %s" % bd.bug_shortname(bug) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be new SUMMARY") + parser.add_option("-r", "--reporter", metavar="REPORTER", dest="reporter", + help="The user who reported the bug", default=None) + parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", + help="The developer in charge of the bug", default=None) + return parser + +longhelp=""" +Create a new bug, with a new ID. The summary specified on the +commandline is a string (only one line) that describes the bug briefly +or "-", in which case the string will be read from stdin. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/libbe/command/open.py b/libbe/command/open.py new file mode 100644 index 0000000..a6fe48d --- /dev/null +++ b/libbe/command/open.py @@ -0,0 +1,61 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# Marien Zwart +# Thomas Gerigk +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Re-open a bug""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("b").status + closed + >>> execute(["b"], manipulate_encodings=False) + >>> bd._clear_bugs() + >>> print bd.bug_from_shortname("b").status + open + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==False}) + if len(args) == 0: + raise cmdutil.UsageError, "Please specify a bug id." + if len(args) > 1: + raise cmdutil.UsageError, "Too many arguments." + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bug = cmdutil.bug_from_id(bd, args[0]) + bug.status = "open" + +def get_parser(): + parser = cmdutil.CmdOptionParser("be open BUG-ID") + return parser + +longhelp=""" +Mark a bug as 'open'. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/libbe/command/remove.py b/libbe/command/remove.py new file mode 100644 index 0000000..bac06c0 --- /dev/null +++ b/libbe/command/remove.py @@ -0,0 +1,65 @@ +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Remove (delete) a bug and its comments""" +from libbe import cmdutil, bugdir +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> from libbe import mapfile + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> print bd.bug_from_shortname("b").status + closed + >>> execute (["b"], manipulate_encodings=False) + Removed bug b + >>> bd._clear_bugs() + >>> try: + ... bd.bug_from_shortname("b") + ... except bugdir.NoBugMatches: + ... print "Bug not found" + Bug not found + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + if len(args) != 1: + raise cmdutil.UsageError, "Please specify a bug id." + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bug = cmdutil.bug_from_id(bd, args[0]) + bd.remove_bug(bug) + print "Removed bug %s" % bug.uuid + +def get_parser(): + parser = cmdutil.CmdOptionParser("be remove BUG-ID") + return parser + +longhelp=""" +Remove (delete) an existing bug. Use with caution: if you're not using a +revision control system, there may be no way to recover the lost information. +You should use this command, for example, to get rid of blank or otherwise +mangled bugs. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/libbe/command/set.py b/libbe/command/set.py new file mode 100644 index 0000000..4d54a59 --- /dev/null +++ b/libbe/command/set.py @@ -0,0 +1,132 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# Marien Zwart +# Thomas Gerigk +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Change tree settings""" +import textwrap +from libbe import cmdutil, bugdir, vcs, settings_object +__desc__ = __doc__ + +def _value_string(bd, setting): + val = bd.settings.get(setting, settings_object.EMPTY) + if val == settings_object.EMPTY: + default = getattr(bd, bd._setting_name_to_attr_name(setting)) + if default not in [None, settings_object.EMPTY]: + val = "None (%s)" % default + else: + val = None + return str(val) + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["target"], manipulate_encodings=False) + None + >>> execute(["target", "tomorrow"], manipulate_encodings=False) + >>> execute(["target"], manipulate_encodings=False) + tomorrow + >>> execute(["target", "none"], manipulate_encodings=False) + >>> execute(["target"], manipulate_encodings=False) + None + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) > 2: + raise cmdutil.UsageError, "Too many arguments" + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + if len(args) == 0: + keys = bd.settings_properties + keys.sort() + for key in keys: + print "%16s: %s" % (key, _value_string(bd, key)) + elif len(args) == 1: + print _value_string(bd, args[0]) + else: + if args[1] == "none": + setattr(bd, args[0], settings_object.EMPTY) + else: + if args[0] not in bd.settings_properties: + msg = "Invalid setting %s\n" % args[0] + msg += 'Allowed settings:\n ' + msg += '\n '.join(bd.settings_properties) + raise cmdutil.UserError(msg) + old_setting = bd.settings.get(args[0]) + setattr(bd, args[0], args[1]) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be set [NAME] [VALUE]") + return parser + +def get_bugdir_settings(): + settings = [] + for s in bugdir.BugDir.settings_properties: + settings.append(s) + settings.sort() + documented_settings = [] + for s in settings: + set = getattr(bugdir.BugDir, s) + dstr = set.__doc__.strip() + # per-setting comment adjustments + if s == "vcs_name": + lines = dstr.split('\n') + while lines[0].startswith("This property defaults to") == False: + lines.pop(0) + assert len(lines) != None, \ + "Unexpected vcs_name docstring:\n '%s'" % dstr + lines.insert( + 0, "The name of the revision control system to use.\n") + dstr = '\n'.join(lines) + doc = textwrap.wrap(dstr, width=70, initial_indent=' ', + subsequent_indent=' ') + documented_settings.append("%s\n%s" % (s, '\n'.join(doc))) + return documented_settings + +longhelp=""" +Show or change per-tree settings. + +If name and value are supplied, the name is set to a new value. +If no value is specified, the current value is printed. +If no arguments are provided, all names and values are listed. + +To unset a setting, set it to "none". + +Allowed settings are: + +%s""" % ('\n'.join(get_bugdir_settings()),) + +def help(): + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + if pos == 0: # first positional argument is a setting name + props = bugdir.BugDir.settings_properties + raise cmdutil.GetCompletions(props) + raise cmdutil.GetCompletions() # no positional arguments for list diff --git a/libbe/command/severity.py b/libbe/command/severity.py new file mode 100644 index 0000000..804dc4e --- /dev/null +++ b/libbe/command/severity.py @@ -0,0 +1,106 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# Marien Zwart +# Thomas Gerigk +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Show or change a bug's severity level""" +from libbe import cmdutil, bugdir, bug +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + minor + >>> execute(["a", "wishlist"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + wishlist + >>> execute(["a", "none"], manipulate_encodings=False) + Traceback (most recent call last): + UserError: Invalid severity level: none + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) not in (1,2): + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bug = cmdutil.bug_from_id(bd, args[0]) + if len(args) == 1: + print bug.severity + elif len(args) == 2: + try: + bug.severity = args[1] + except ValueError, e: + if e.name != "severity": + raise e + raise cmdutil.UserError ("Invalid severity level: %s" % e.value) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be severity BUG-ID [SEVERITY]") + return parser + +def help(): + 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: +"""] + try: # See if there are any per-tree severity configurations + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False) + except bugdir.NoBugDir, e: + pass # No tree, just show the defaults + longest_severity_len = max([len(s) for s in bug.severity_values]) + for severity in bug.severity_values : + description = bug.severity_description[severity] + s = "%*s : %s\n" % (longest_severity_len, severity, description) + longhelp.append(s) + longhelp = ''.join(longhelp) + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + try: # See if there are any per-tree severity configurations + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + except bugdir.NoBugDir: + bd = None + if pos == 0: # fist positional argument is a bug id + ids = [] + if bd != None: + bd.load_all_bugs() + filter = lambda bg : bg.active==True + bugs = [bg for bg in bd if filter(bg)==True] + ids = [bd.bug_shortname(bg) for bg in bugs] + raise cmdutil.GetCompletions(ids) + elif pos == 1: # second positional argument is a severity + raise cmdutil.GetCompletions(bug.severity_values) + raise cmdutil.GetCompletions() diff --git a/libbe/command/show.py b/libbe/command/show.py new file mode 100644 index 0000000..7757aaa --- /dev/null +++ b/libbe/command/show.py @@ -0,0 +1,183 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# Thomas Gerigk +# Thomas Habets +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Show a particular bug, comment, or combination of both.""" +import sys +from libbe import cmdutil, bugdir, comment, version, _version +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute (["a",], manipulate_encodings=False) # doctest: +ELLIPSIS + ID : a + Short name : a + Severity : minor + Status : open + Assigned : + Reporter : + Creator : John Doe + Created : ... + Bug A + + >>> execute (["--xml", "a"], manipulate_encodings=False) # doctest: +ELLIPSIS + + + + ... + ... + ... + ... + + + a + a + minor + open + John Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug A + + + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={-1: lambda bug : bug.active==True}) + if len(args) == 0: + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + + if options.only_raw_body == True: + if len(args) != 1: + raise cmdutil.UsageError( + 'only one ID accepted with --only-raw-body') + bug,comment = cmdutil.bug_comment_from_id(bd, args[0]) + if comment == bug.comment_root: + raise cmdutil.UsageError( + "--only-raw-body requires a comment ID, not '%s'" % args[0]) + sys.__stdout__.write(comment.body) + sys.exit(0) + print output(args, bd, as_xml=options.XML, with_comments=options.comments) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be show [options] ID [ID ...]") + parser.add_option("-x", "--xml", action="store_true", default=False, + dest='XML', help="Dump as XML") + parser.add_option("--only-raw-body", action="store_true", + dest='only_raw_body', + help="When printing only a single comment, just print it's body. This allows extraction of non-text content types.") + parser.add_option("-c", "--no-comments", dest="comments", + action="store_false", default=True, + help="Disable comment output. This is useful if you just want more details on a bug's current status.") + return parser + +longhelp=""" +Show all information about the bugs or comments whose IDs are given. + +Without the --xml flag set, it's probably not a good idea to mix bug +and comment IDs in a single call, but you're free to do so if you +like. With the --xml flag set, there will never be any root comments, +so mix and match away (the bug listings for directly requested +comments will be restricted to the bug uuid and the requested +comment(s)). + +Directly requested comments will be grouped by their parent bug and +placed at the end of the output, so the ordering may not match the +order of the listed IDs. +""" + +def help(): + return get_parser().help_str() + longhelp + +def _sort_ids(ids, with_comments=True): + bugs = [] + root_comments = {} + for id in ids: + bugname,commname = cmdutil.parse_id(id) + if commname == None: + bugs.append(bugname) + elif with_comments == True: + if bugname not in root_comments: + root_comments[bugname] = [commname] + else: + root_comments[bugname].append(commname) + for bugname in root_comments.keys(): + assert bugname not in bugs, \ + "specifically requested both '%s%s' and '%s'" \ + % (bugname, root_comments[bugname][0], bugname) + return (bugs, root_comments) + +def _xml_header(encoding): + lines = ['' % encoding, + '', + ' ', + ' %s' % version.version()] + for tag in ['branch-nick', 'revno', 'revision-id']: + value = _version.version_info[tag.replace('-', '_')] + lines.append(' <%s>%s' % (tag, value, tag)) + lines.append(' ') + return lines + +def _xml_footer(): + return [''] + +def output(ids, bd, as_xml=True, with_comments=True): + bugs,root_comments = _sort_ids(ids, with_comments) + lines = [] + if as_xml: + lines.extend(_xml_header(bd.encoding)) + else: + spaces_left = len(ids) - 1 + for bugname in bugs: + bug = cmdutil.bug_from_id(bd, bugname) + if as_xml: + lines.append(bug.xml(indent=2, show_comments=with_comments)) + else: + lines.append(bug.string(show_comments=with_comments)) + if spaces_left > 0: + spaces_left -= 1 + lines.append('') # add a blank line between bugs/comments + for bugname,comments in root_comments.items(): + bug = cmdutil.bug_from_id(bd, bugname) + if as_xml: + lines.extend([' ', ' %s' % bug.uuid]) + for commname in comments: + try: + comment = bug.comment_root.comment_from_shortname(commname) + except comment.InvalidShortname, e: + raise UserError(e.message) + if as_xml: + lines.append(comment.xml(indent=4, shortname=bugname)) + else: + lines.append(comment.string(shortname=bugname)) + if spaces_left > 0: + spaces_left -= 1 + lines.append('') # add a blank line between bugs/comments + if as_xml: + lines.append('') + if as_xml: + lines.extend(_xml_footer()) + return '\n'.join(lines) diff --git a/libbe/command/status.py b/libbe/command/status.py new file mode 100644 index 0000000..58b6f63 --- /dev/null +++ b/libbe/command/status.py @@ -0,0 +1,118 @@ +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Show or change a bug's status""" +from libbe import cmdutil, bugdir, bug +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + open + >>> execute(["a", "closed"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + closed + >>> execute(["a", "none"], manipulate_encodings=False) + Traceback (most recent call last): + UserError: Invalid status: none + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) + if len(args) not in (1,2): + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + bug = cmdutil.bug_from_id(bd, args[0]) + if len(args) == 1: + print bug.status + else: + try: + bug.status = args[1] + except ValueError, e: + if e.name != "status": + raise + raise cmdutil.UserError ("Invalid status: %s" % e.value) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be status BUG-ID [STATUS]") + return parser + + +def help(): + try: # See if there are any per-tree status configurations + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + except bugdir.NoBugDir, e: + pass # No tree, just show the defaults + longest_status_len = max([len(s) for s in bug.status_values]) + active_statuses = [] + for status in bug.active_status_values : + description = bug.status_description[status] + s = "%*s : %s" % (longest_status_len, status, description) + active_statuses.append(s) + inactive_statuses = [] + for status in bug.inactive_status_values : + description = bug.status_description[status] + s = "%*s : %s" % (longest_status_len, status, description) + inactive_statuses.append(s) + longhelp=""" +Show or change a bug's status. + +If no status is specified, the current value is printed. If a status +is specified, it will be assigned to the bug. + +There are two classes of statuses, active and inactive, which are only +important for commands like "be list" that show only active bugs by +default. + +Active status levels are: + %s +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. +""" % ('\n '.join(active_statuses), '\n '.join(inactive_statuses)) + return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + try: # See if there are any per-tree status configurations + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + except bugdir.NoBugDir: + bd = None + if pos == 0: # fist positional argument is a bug id + ids = [] + if bd != None: + bd.load_all_bugs() + ids = [bd.bug_shortname(bg) for bg in bd] + raise cmdutil.GetCompletions(ids) + elif pos == 1: # second positional argument is a status + raise cmdutil.GetCompletions(bug.status_values) + raise cmdutil.GetCompletions() diff --git a/libbe/command/subscribe.py b/libbe/command/subscribe.py new file mode 100644 index 0000000..69554f7 --- /dev/null +++ b/libbe/command/subscribe.py @@ -0,0 +1,360 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""(Un)subscribe to change notification""" +from libbe import cmdutil, bugdir, tree, diff +import os, copy +__desc__ = __doc__ + +TAG="SUBSCRIBE:" + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> os.chdir(bd.root) + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + [] + >>> execute(["-s","John Doe ", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + John Doe all * + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + ['SUBSCRIBE:John Doe \\tall\\t*'] + >>> execute(["-s","Jane Doe ", "-S", "a.com,b.net", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all a.com,b.net + John Doe all * + >>> execute(["-s","Jane Doe ", "-S", "a.edu", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all a.com,a.edu,b.net + John Doe all * + >>> execute(["-u", "-s","Jane Doe ", "-S", "a.com", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all a.edu,b.net + John Doe all * + >>> execute(["-s","Jane Doe ", "-S", "*", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + Jane Doe all * + John Doe all * + >>> execute(["-u", "-s","Jane Doe ", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for a: + John Doe all * + >>> execute(["-u", "-s","John Doe ", "a"], manipulate_encodings=False) + >>> execute(["-s","Jane Doe ", "-t", "new", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for bug directory: + Jane Doe new * + >>> execute(["-s","Jane Doe ", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for bug directory: + Jane Doe all * + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if len(args) > 1: + help() + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + + subscriber = options.subscriber + if subscriber == None: + subscriber = bd.user_id + if options.unsubscribe == True: + if options.servers == None: + options.servers = "INVALID" + if options.types == None: + options.types = "INVALID" + else: + if options.servers == None: + options.servers = "*" + if options.types == None: + options.types = "all" + servers = options.servers.split(",") + types = options.types.split(",") + + if len(args) == 0 or args[0] == diff.BUGDIR_ID: # directory-wide subscriptions + type_root = diff.BUGDIR_TYPE_ALL + entity = bd + entity_name = "bug directory" + else: # bug-specific subscriptions + type_root = diff.BUG_TYPE_ALL + bug = bd.bug_from_shortname(args[0]) + entity = bug + entity_name = bug.uuid + if options.list_all == True: + entity_name = "anything in the bug directory" + + types = [diff.type_from_name(name, type_root, default=diff.INVALID_TYPE, + default_ok=options.unsubscribe) + for name in types] + estrs = entity.extra_strings + if options.list == True or options.list_all == True: + pass + else: # alter subscriptions + if options.unsubscribe == True: + estrs = unsubscribe(estrs, subscriber, types, servers, type_root) + else: # add the tag + estrs = subscribe(estrs, subscriber, types, servers, type_root) + entity.extra_strings = estrs # reassign to notice change + + if options.list_all == True: + bd.load_all_bugs() + subscriptions = get_bugdir_subscribers(bd, servers[0]) + else: + subscriptions = [] + for estr in entity.extra_strings: + if estr.startswith(TAG): + subscriptions.append(estr[len(TAG):]) + + if len(subscriptions) > 0: + print "Subscriptions for %s:" % entity_name + print '\n'.join(subscriptions) + + +def get_parser(): + parser = cmdutil.CmdOptionParser("be subscribe ID") + parser.add_option("-u", "--unsubscribe", action="store_true", + dest="unsubscribe", default=False, + help="Unsubscribe instead of subscribing.") + parser.add_option("-a", "--list-all", action="store_true", + dest="list_all", default=False, + help="List all subscribers (no ID argument, read only action).") + parser.add_option("-l", "--list", action="store_true", + dest="list", default=False, + help="List subscribers (read only action).") + parser.add_option("-s", "--subscriber", dest="subscriber", + metavar="SUBSCRIBER", + help="Email address of the subscriber (defaults to bugdir.user_id).") + parser.add_option("-S", "--servers", dest="servers", metavar="SERVERS", + help="Servers from which you want notification.") + parser.add_option("-t", "--type", dest="types", metavar="TYPES", + help="Types of changes you wish to be notified about.") + return parser + +longhelp=""" +ID can be either a bug id, or blank/"DIR", in which case it refers to the +whole bug directory. + +SERVERS specifies the servers from which you would like to receive +notification. Multiple severs may be specified in a comma-separated +list, or you can use "*" to match all servers (the default). If you +have not selected a server, it should politely refrain from notifying +you of changes, although there is no way to guarantee this behavior. + +Available TYPES: + For bugs: +%s + For %s: +%s + +For unsubscription, any listed SERVERS and TYPES are removed from your +subscription. Either the catch-all server "*" or type "%s" will +remove SUBSCRIBER entirely from the specified ID. + +This command is intended for use primarily by public interfaces, since +if you're just hacking away on your private repository, you'll known +what's changed ;). This command just (un)sets the appropriate +subscriptions, and leaves it up to each interface to perform the +notification. +""" % (diff.BUG_TYPE_ALL.string_tree(6), diff.BUGDIR_ID, + diff.BUGDIR_TYPE_ALL.string_tree(6), + diff.BUGDIR_TYPE_ALL) + +def help(): + return get_parser().help_str() + longhelp + +# internal helper functions + +def _generate_string(subscriber, types, servers): + types = sorted([str(t) for t in types]) + servers = sorted(servers) + return "%s%s\t%s\t%s" % (TAG,subscriber,",".join(types),",".join(servers)) + +def _parse_string(string, type_root): + assert string.startswith(TAG), string + string = string[len(TAG):] + subscriber,types,servers = string.split("\t") + types = [diff.type_from_name(name, type_root) for name in types.split(",")] + return (subscriber,types,servers.split(",")) + +def _get_subscriber(extra_strings, subscriber, type_root): + for i,string in enumerate(extra_strings): + if string.startswith(TAG): + s,ts,srvs = _parse_string(string, type_root) + if s == subscriber: + return i,s,ts,srvs # match! + return None # no match + +# functions exposed to other modules + +def subscribe(extra_strings, subscriber, types, servers, type_root): + args = _get_subscriber(extra_strings, subscriber, type_root) + if args == None: # no match + extra_strings.append(_generate_string(subscriber, types, servers)) + return extra_strings + # Alter matched string + i,s,ts,srvs = args + for t in types: + if t not in ts: + ts.append(t) + # remove descendant types + all_ts = copy.copy(ts) + for t in all_ts: + for tt in all_ts: + if tt in ts and t.has_descendant(tt): + ts.remove(tt) + if "*" in servers+srvs: + srvs = ["*"] + else: + srvs = list(set(servers+srvs)) + extra_strings[i] = _generate_string(subscriber, ts, srvs) + return extra_strings + +def unsubscribe(extra_strings, subscriber, types, servers, type_root): + args = _get_subscriber(extra_strings, subscriber, type_root) + if args == None: # no match + return extra_strings # pass + # Remove matched string + i,s,ts,srvs = args + all_ts = copy.copy(ts) + for t in types: + for tt in all_ts: + if tt in ts and t.has_descendant(tt): + ts.remove(tt) + if "*" in servers+srvs: + srvs = [] + else: + for srv in servers: + if srv in srvs: + srvs.remove(srv) + if len(ts) == 0 or len(srvs) == 0: + extra_strings.pop(i) + else: + extra_strings[i] = _generate_string(subscriber, ts, srvs) + return extra_strings + +def get_subscribers(extra_strings, type, server, type_root, + match_ancestor_types=False, + match_descendant_types=False): + """ + Set match_ancestor_types=True if you want to find eveyone who + cares about your particular type. + + Set match_descendant_types=True if you want to find subscribers + who may only care about some subset of your type. This is useful + for generating lists of all the subscribers in a given set of + extra_strings. + + >>> def sgs(*args, **kwargs): + ... return sorted(get_subscribers(*args, **kwargs)) + >>> es = [] + >>> es = subscribe(es, "John Doe ", [diff.BUGDIR_TYPE_ALL], + ... ["a.com"], diff.BUGDIR_TYPE_ALL) + >>> es = subscribe(es, "Jane Doe ", [diff.BUGDIR_TYPE_NEW], + ... ["*"], diff.BUGDIR_TYPE_ALL) + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL) + ['John Doe '] + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL, + ... match_descendant_types=True) + ['Jane Doe ', 'John Doe '] + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "b.net", diff.BUGDIR_TYPE_ALL, + ... match_descendant_types=True) + ['Jane Doe '] + >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL) + ['Jane Doe '] + >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL, + ... match_ancestor_types=True) + ['Jane Doe ', 'John Doe '] + """ + for string in extra_strings: + if not string.startswith(TAG): + continue + subscriber,types,servers = _parse_string(string, type_root) + type_match = False + if type in types: + type_match = True + if type_match == False and match_ancestor_types == True: + for t in types: + if t.has_descendant(type): + type_match = True + break + if type_match == False and match_descendant_types == True: + for t in types: + if type.has_descendant(t): + type_match = True + break + server_match = False + if server in servers or servers == ["*"] or server == "*": + server_match = True + if type_match == True and server_match == True: + yield subscriber + +def get_bugdir_subscribers(bugdir, server): + """ + I have a bugdir. Who cares about it, and what do they care about? + Returns a dict of dicts: + subscribers[user][id] = types + where id is either a bug.uuid (in the case of a bug subscription) + or "%(bugdir_id)s" (in the case of a bugdir subscription). + + Only checks bugs that are currently in memory, so you might want + to call bugdir.load_all_bugs() first. + + >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) + >>> a = bd.bug_from_shortname("a") + >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe ", + ... [diff.BUGDIR_TYPE_ALL], ["a.com"], diff.BUGDIR_TYPE_ALL) + >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe ", + ... [diff.BUGDIR_TYPE_NEW], ["*"], diff.BUGDIR_TYPE_ALL) + >>> a.extra_strings = subscribe(a.extra_strings, "John Doe ", + ... [diff.BUG_TYPE_ALL], ["a.com"], diff.BUG_TYPE_ALL) + >>> subscribers = get_bugdir_subscribers(bd, "a.com") + >>> subscribers["Jane Doe "]["%(bugdir_id)s"] + [] + >>> subscribers["John Doe "]["%(bugdir_id)s"] + [] + >>> subscribers["John Doe "]["a"] + [] + >>> get_bugdir_subscribers(bd, "b.net") + {'Jane Doe ': {'%(bugdir_id)s': []}} + >>> bd.cleanup() + """ % {'bugdir_id':diff.BUGDIR_ID} + subscribers = {} + for sub in get_subscribers(bugdir.extra_strings, diff.BUGDIR_TYPE_ALL, + server, diff.BUGDIR_TYPE_ALL, + match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bugdir.extra_strings, sub, + diff.BUGDIR_TYPE_ALL) + subscribers[sub] = {"DIR":ts} + for bug in bugdir: + for sub in get_subscribers(bug.extra_strings, diff.BUG_TYPE_ALL, + server, diff.BUG_TYPE_ALL, + match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bug.extra_strings, sub, + diff.BUG_TYPE_ALL) + if sub in subscribers: + subscribers[sub][bug.uuid] = ts + else: + subscribers[sub] = {bug.uuid:ts} + return subscribers diff --git a/libbe/command/tag.py b/libbe/command/tag.py new file mode 100644 index 0000000..f3819bd --- /dev/null +++ b/libbe/command/tag.py @@ -0,0 +1,137 @@ +# Copyright (C) 2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Tag a bug, or search bugs for tags""" +from libbe import cmdutil, bugdir +import os, copy +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> from libbe import utility + >>> bd = bugdir.SimpleBugDir() + >>> bd.set_sync_with_disk(True) + >>> os.chdir(bd.root) + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + [] + >>> execute(["a", "GUI"], manipulate_encodings=False) + Tags for a: + GUI + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + ['TAG:GUI'] + >>> execute(["a", "later"], manipulate_encodings=False) + Tags for a: + GUI + later + >>> execute(["a"], manipulate_encodings=False) + Tags for a: + GUI + later + >>> execute(["--list"], manipulate_encodings=False) + GUI + later + >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) + Tags for a: + Alphabetically first + GUI + later + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + ['TAG:Alphabetically first', 'TAG:GUI', 'TAG:later'] + >>> a.extra_strings = [] + >>> print a.extra_strings + [] + >>> execute(["a"], manipulate_encodings=False) + >>> bd._clear_bugs() # resync our copy of bug + >>> a = bd.bug_from_shortname("a") + >>> print a.extra_strings + [] + >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) + Tags for a: + Alphabetically first + >>> execute(["--remove", "a", "Alphabetically first"], manipulate_encodings=False) + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if len(args) == 0 and options.list == False: + raise cmdutil.UsageError("Please specify a bug id.") + elif len(args) > 2 or (len(args) > 0 and options.list == True): + help() + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + if options.list: + bd.load_all_bugs() + tags = [] + for bug in bd: + for estr in bug.extra_strings: + if estr.startswith("TAG:"): + tag = estr[4:] + if tag not in tags: + tags.append(tag) + tags.sort() + if len(tags) > 0: + print '\n'.join(tags) + return + bug = cmdutil.bug_from_id(bd, args[0]) + if len(args) == 2: + given_tag = args[1] + estrs = bug.extra_strings + tag_string = "TAG:%s" % given_tag + if options.remove == True: + estrs.remove(tag_string) + else: # add the tag + estrs.append(tag_string) + bug.extra_strings = estrs # reassign to notice change + + tags = [] + for estr in bug.extra_strings: + if estr.startswith("TAG:"): + tags.append(estr[4:]) + + if len(tags) > 0: + print "Tags for %s:" % bug.uuid + print '\n'.join(tags) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be tag BUG-ID [TAG]\nor: be tag --list") + parser.add_option("-r", "--remove", action="store_true", dest="remove", + help="Remove TAG (instead of adding it)") + parser.add_option("-l", "--list", action="store_true", dest="list", + help="List all available tags and exit") + return parser + +longhelp=""" +If TAG is given, add TAG to BUG-ID. If it is not specified, just +print the tags for BUG-ID. + +To search for bugs with a particular tag, try + $ be list --extra-strings TAG: +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/libbe/command/target.py b/libbe/command/target.py new file mode 100644 index 0000000..5dd5d38 --- /dev/null +++ b/libbe/command/target.py @@ -0,0 +1,168 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Chris Ball +# Gianluca Montecchi +# Marien Zwart +# Thomas Gerigk +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Assorted bug target manipulations and queries""" +from libbe import cmdutil, bugdir +from becommands import depend +__desc__ = __doc__ + +def execute(args, manipulate_encodings=True, restrict_file_access=False, + dir="."): + """ + >>> import os, StringIO, sys + >>> bd = bugdir.SimpleBugDir() + >>> os.chdir(bd.root) + >>> execute(["a"], manipulate_encodings=False) + No target assigned. + >>> execute(["a", "tomorrow"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + tomorrow + + >>> orig_stdout = sys.stdout + >>> tmp_stdout = StringIO.StringIO() + >>> sys.stdout = tmp_stdout + >>> execute(["--resolve", "tomorrow"], manipulate_encodings=False) + >>> sys.stdout = orig_stdout + >>> output = tmp_stdout.getvalue().strip() + >>> target = bd.bug_from_uuid(output) + >>> print target.summary + tomorrow + >>> print target.severity + target + + >>> execute(["a", "none"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) + No target assigned. + >>> bd.cleanup() + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + + if (options.resolve == False and len(args) not in (1, 2)) \ + or (options.resolve == True and len(args) not in (0, 1)): + raise cmdutil.UsageError('Incorrect number of arguments.') + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings, + root=dir) + if options.resolve == True: + if len(args) == 0: + summary = None + else: + summary = args[0] + bug = bug_from_target_summary(bd, summary) + if bug == None: + print 'No target assigned.' + else: + print bug.uuid + return + bug = cmdutil.bug_from_id(bd, args[0]) + if len(args) == 1: + target = bug_target(bd, bug) + if target is None: + print "No target assigned." + else: + print target.summary + else: + if args[1] == "none": + target = remove_target(bd, bug) + else: + target = add_target(bd, bug, args[1]) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be target BUG-ID [TARGET]\nor: be target --resolve [TARGET]") + parser.add_option("-r", "--resolve", action="store_true", dest="resolve", + help="Print the UUID for the target bug whose summary matches TARGET. If TARGET is not given, print the UUID of the current bugdir target. If that is not set, don't print anything.", + default=False) + return parser + +longhelp=""" +Assorted bug target manipulations and queries. + +If no target is specified, the bug's current target is printed. If +TARGET is specified, it will be assigned to the bug, creating a new +target bug if necessary. + +Targets are free-form; any text may be specified. They will generally +be milestone names or release numbers. The value "none" can be used +to unset the target. + +In the alternative `be target --resolve TARGET` form, print the UUID +of the target-bug with summary TARGET. If target is not given, return +use the bugdir's current target (see `be set`). + +If you want to list all bugs blocking the current target, try + $ be depend --status -closed,fixed,wontfix --severity -target \ + $(be target --resolve) + +If you want to set the current bugdir target by summary (rather than +by UUID), try + $ be set target $(be target --resolve SUMMARY) +""" + +def help(): + return get_parser().help_str() + longhelp + +def bug_from_target_summary(bugdir, summary=None): + if summary == None: + if bugdir.target == None: + return None + else: + return bugdir.bug_from_uuid(bugdir.target) + matched = [] + for uuid in bugdir.uuids(): + bug = bugdir.bug_from_uuid(uuid) + if bug.severity == 'target' and bug.summary == summary: + matched.append(bug) + if len(matched) == 0: + return None + if len(matched) > 1: + raise Exception('Several targets with same summary: %s' + % '\n '.join([bug.uuid for bug in matched])) + return matched[0] + +def bug_target(bugdir, bug): + if bug.severity == 'target': + return bug + matched = [] + for blocked in depend.get_blocks(bugdir, bug): + if blocked.severity == 'target': + matched.append(blocked) + if len(matched) == 0: + return None + if len(matched) > 1: + raise Exception('This bug (%s) blocks several targets: %s' + % (bug.uuid, + '\n '.join([b.uuid for b in matched]))) + return matched[0] + +def remove_target(bugdir, bug): + target = bug_target(bugdir, bug) + depend.remove_block(target, bug) + return target + +def add_target(bugdir, bug, summary): + target = bug_from_target_summary(bugdir, summary) + if target == None: + target = bugdir.new_bug(summary=summary) + target.severity = 'target' + depend.add_block(target, bug) + return target diff --git a/libbe/config.py b/libbe/config.py deleted file mode 100644 index ccd236b..0000000 --- a/libbe/config.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Create, save, and load the per-user config file at path(). -""" - -import ConfigParser -import codecs -import locale -import os.path -import sys - -import libbe -if libbe.TESTING == True: - import doctest - - -default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() - -def path(): - """Return the path to the per-user config file""" - return os.path.expanduser("~/.bugs_everywhere") - -def set_val(name, value, section="DEFAULT", encoding=None): - """Set a value in the per-user config file - - :param name: The name of the value to set - :param value: The new value to set (or None to delete the value) - :param section: The section to store the name/value in - """ - if encoding == None: - encoding = default_encoding - config = ConfigParser.ConfigParser() - if os.path.exists(path()) == False: # touch file or config - open(path(), "w").close() # read chokes on missing file - f = codecs.open(path(), "r", encoding) - config.readfp(f, path()) - f.close() - if value is not None: - config.set(section, name, value) - else: - config.remove_option(section, name) - f = codecs.open(path(), "w", encoding) - config.write(f) - f.close() - -def get_val(name, section="DEFAULT", default=None, encoding=None): - """ - Get a value from the per-user config file - - :param name: The name of the value to get - :section: The section that the name is in - :return: The value, or None - >>> get_val("junk") is None - True - >>> set_val("junk", "random") - >>> get_val("junk") - u'random' - >>> set_val("junk", None) - >>> get_val("junk") is None - True - """ - if os.path.exists(path()): - if encoding == None: - encoding = default_encoding - config = ConfigParser.ConfigParser() - f = codecs.open(path(), "r", encoding) - config.readfp(f, path()) - f.close() - try: - return config.get(section, name) - except ConfigParser.NoOptionError: - return default - else: - return default - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() diff --git a/libbe/darcs.py b/libbe/darcs.py deleted file mode 100644 index d94eaef..0000000 --- a/libbe/darcs.py +++ /dev/null @@ -1,192 +0,0 @@ -# Copyright (C) 2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Darcs backend. -""" - -import codecs -import os -import re -import sys -try: # import core module, Python >= 2.5 - from xml.etree import ElementTree -except ImportError: # look for non-core module - from elementtree import ElementTree -from xml.sax.saxutils import unescape - -import libbe -import vcs -if libbe.TESTING == True: - import doctest - import unittest - - -def new(): - return Darcs() - -class Darcs(vcs.VCS): - name="darcs" - client="darcs" - versioned=True - def _vcs_version(self): - status,output,error = self._u_invoke_client("--version") - num_part = output.split(" ")[0] - self.parsed_version = [int(i) for i in num_part.split(".")] - return output - def _vcs_detect(self, path): - if self._u_search_parent_directories(path, "_darcs") != None : - return True - return False - def _vcs_root(self, path): - """Find the root of the deepest repository containing path.""" - # Assume that nothing funny is going on; in particular, that we aren't - # dealing with a bare repo. - if os.path.isdir(path) != True: - path = os.path.dirname(path) - darcs_dir = self._u_search_parent_directories(path, "_darcs") - if darcs_dir == None: - return None - return os.path.dirname(darcs_dir) - def _vcs_init(self, path): - self._u_invoke_client("init", cwd=path) - def _vcs_get_user_id(self): - # following http://darcs.net/manual/node4.html#SECTION00410030000000000000 - # as of June 29th, 2009 - if self.rootdir == None: - return None - darcs_dir = os.path.join(self.rootdir, "_darcs") - if darcs_dir != None: - for pref_file in ["author", "email"]: - pref_path = os.path.join(darcs_dir, "prefs", pref_file) - if os.path.exists(pref_path): - return self.get_file_contents(pref_path) - for env_variable in ["DARCS_EMAIL", "EMAIL"]: - if env_variable in os.environ: - return os.environ[env_variable] - return None - def _vcs_set_user_id(self, value): - if self.rootdir == None: - self.root(".") - if self.rootdir == None: - raise vcs.SettingIDnotSupported - author_path = os.path.join(self.rootdir, "_darcs", "prefs", "author") - f = codecs.open(author_path, "w", self.encoding) - f.write(value) - f.close() - def _vcs_add(self, path): - if os.path.isdir(path): - return - self._u_invoke_client("add", path) - def _vcs_remove(self, path): - if not os.path.isdir(self._u_abspath(path)): - os.remove(os.path.join(self.rootdir, path)) # darcs notices removal - def _vcs_update(self, path): - pass # darcs notices changes - def _vcs_get_file_contents(self, path, revision=None, binary=False): - if revision == None: - return vcs.VCS._vcs_get_file_contents(self, path, revision, - binary=binary) - else: - if self.parsed_version[0] >= 2: - status,output,error = self._u_invoke_client( \ - "show", "contents", "--patch", revision, path) - return output - else: - # Darcs versions < 2.0.0pre2 lack the "show contents" command - - status,output,error = self._u_invoke_client( \ - "diff", "--unified", "--from-patch", revision, path, - unicode_output=False) - major_patch = output - status,output,error = self._u_invoke_client( \ - "diff", "--unified", "--patch", revision, path, - unicode_output=False) - target_patch = output - - # "--output -" to be supported in GNU patch > 2.5.9 - # but that hasn't been released as of June 30th, 2009. - - # Rewrite path to status before the patch we want - args=["patch", "--reverse", path] - status,output,error = self._u_invoke(args, stdin=major_patch) - # Now apply the patch we want - args=["patch", path] - status,output,error = self._u_invoke(args, stdin=target_patch) - - if os.path.exists(os.path.join(self.rootdir, path)) == True: - contents = vcs.VCS._vcs_get_file_contents(self, path, - binary=binary) - else: - contents = "" - - # Now restore path to it's current incarnation - args=["patch", "--reverse", path] - status,output,error = self._u_invoke(args, stdin=target_patch) - args=["patch", path] - status,output,error = self._u_invoke(args, stdin=major_patch) - current_contents = vcs.VCS._vcs_get_file_contents(self, path, - binary=binary) - return contents - def _vcs_duplicate_repo(self, directory, revision=None): - if revision==None: - vcs.VCS._vcs_duplicate_repo(self, directory, revision) - else: - self._u_invoke_client("put", "--to-patch", revision, directory) - def _vcs_commit(self, commitfile, allow_empty=False): - id = self.get_user_id() - if '@' not in id: - id = "%s <%s@invalid.com>" % (id, id) - args = ['record', '--all', '--author', id, '--logfile', commitfile] - status,output,error = self._u_invoke_client(*args) - empty_strings = ["No changes!"] - if self._u_any_in_string(empty_strings, output) == True: - if allow_empty == False: - raise vcs.EmptyCommit() - # note that darcs does _not_ make an empty revision. - # this returns the last non-empty revision id... - revision = self._vcs_revision_id(-1) - else: - revline = re.compile("Finished recording patch '(.*)'") - match = revline.search(output) - assert match != None, output+error - assert len(match.groups()) == 1 - revision = match.groups()[0] - return revision - def _vcs_revision_id(self, index): - status,output,error = self._u_invoke_client("changes", "--xml") - revisions = [] - xml_str = output.encode("unicode_escape").replace(r"\n", "\n") - element = ElementTree.XML(xml_str) - assert element.tag == "changelog", element.tag - for patch in element.getchildren(): - assert patch.tag == "patch", patch.tag - for child in patch.getchildren(): - if child.tag == "name": - text = unescape(unicode(child.text).decode("unicode_escape").strip()) - revisions.append(text) - revisions.reverse() - try: - return revisions[index] - except IndexError: - return None - -if libbe.TESTING == True: - vcs.make_vcs_testcase_subclasses(Darcs, sys.modules[__name__]) - - unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) - suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/editor.py b/libbe/editor.py deleted file mode 100644 index 859cedc..0000000 --- a/libbe/editor.py +++ /dev/null @@ -1,113 +0,0 @@ -# Bugs Everywhere, a distributed bugtracker -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Define editor_string(), a function that invokes an editor to accept -user-produced text as a string. -""" - -import codecs -import locale -import os -import sys -import tempfile - -import libbe -if libbe.TESTING == True: - import doctest - - -default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() - -comment_marker = u"== Anything below this line will be ignored\n" - -class CantFindEditor(Exception): - def __init__(self): - Exception.__init__(self, "Can't find editor to get string from") - -def editor_string(comment=None, encoding=None): - """Invokes the editor, and returns the user-produced text as a string - - >>> if "EDITOR" in os.environ: - ... del os.environ["EDITOR"] - >>> if "VISUAL" in os.environ: - ... del os.environ["VISUAL"] - >>> editor_string() - Traceback (most recent call last): - CantFindEditor: Can't find editor to get string from - >>> os.environ["EDITOR"] = "echo bar > " - >>> editor_string() - u'bar\\n' - >>> os.environ["VISUAL"] = "echo baz > " - >>> editor_string() - u'baz\\n' - >>> del os.environ["EDITOR"] - >>> del os.environ["VISUAL"] - """ - if encoding == None: - encoding = default_encoding - for name in ('VISUAL', 'EDITOR'): - try: - editor = os.environ[name] - break - except KeyError: - pass - else: - raise CantFindEditor() - fhandle, fname = tempfile.mkstemp() - try: - if comment is not None: - cstring = u'\n'+comment_string(comment) - os.write(fhandle, cstring.encode(encoding)) - os.close(fhandle) - oldmtime = os.path.getmtime(fname) - os.system("%s %s" % (editor, fname)) - f = codecs.open(fname, "r", encoding) - output = trimmed_string(f.read()) - f.close() - if output.rstrip('\n') == "": - output = None - finally: - os.unlink(fname) - return output - - -def comment_string(comment): - """ - >>> comment_string('hello') == comment_marker+"hello" - True - """ - return comment_marker + comment - - -def trimmed_string(instring): - """ - >>> trimmed_string("hello\\n"+comment_marker) - u'hello\\n' - >>> trimmed_string("hi!\\n" + comment_string('Booga')) - u'hi!\\n' - """ - out = [] - for line in instring.splitlines(True): - if line.startswith(comment_marker): - break - out.append(line) - return ''.join(out) - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() diff --git a/libbe/encoding.py b/libbe/encoding.py deleted file mode 100644 index d09117f..0000000 --- a/libbe/encoding.py +++ /dev/null @@ -1,66 +0,0 @@ -# Bugs Everywhere, a distributed bugtracker -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Support input/output/filesystem encodings (e.g. UTF-8). -""" - -import codecs -import locale -import sys - -import libbe -if libbe.TESTING == True: - import doctest - - -ENCODING = None # override get_encoding() output by setting this - -def get_encoding(): - """ - Guess a useful input/output/filesystem encoding... Maybe we need - seperate encodings for input/output and filesystem? Hmm... - """ - if ENCODING != None: - return ENCODING - encoding = locale.getpreferredencoding() or sys.getdefaultencoding() - if sys.platform != 'win32' or sys.version_info[:2] > (2, 3): - encoding = locale.getlocale(locale.LC_TIME)[1] or encoding - # Python 2.3 on windows doesn't know about 'XYZ' alias for 'cpXYZ' - return encoding - -def known_encoding(encoding): - """ - >>> known_encoding("highly-unlikely-encoding") - False - >>> known_encoding(get_encoding()) - True - """ - try: - codecs.lookup(encoding) - return True - except LookupError: - return False - -def set_IO_stream_encodings(encoding): - sys.stdin = codecs.getreader(encoding)(sys.__stdin__) - sys.stdout = codecs.getwriter(encoding)(sys.__stdout__) - sys.stderr = codecs.getwriter(encoding)(sys.__stderr__) - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() diff --git a/libbe/git.py b/libbe/git.py deleted file mode 100644 index 7f6e53a..0000000 --- a/libbe/git.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (C) 2008-2009 Ben Finney -# Chris Ball -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Git backend. -""" - -import os -import re -import sys -import unittest - -import libbe -import vcs -if libbe.TESTING == True: - import doctest - - -def new(): - return Git() - -class Git(vcs.VCS): - name="git" - client="git" - versioned=True - def _vcs_version(self): - status,output,error = self._u_invoke_client("--version") - return output - def _vcs_detect(self, path): - if self._u_search_parent_directories(path, ".git") != None : - return True - return False - def _vcs_root(self, path): - """Find the root of the deepest repository containing path.""" - # Assume that nothing funny is going on; in particular, that we aren't - # dealing with a bare repo. - if os.path.isdir(path) != True: - path = os.path.dirname(path) - status,output,error = self._u_invoke_client("rev-parse", "--git-dir", - cwd=path) - gitdir = os.path.join(path, output.rstrip('\n')) - dirname = os.path.abspath(os.path.dirname(gitdir)) - return dirname - def _vcs_init(self, path): - self._u_invoke_client("init", cwd=path) - def _vcs_get_user_id(self): - status,output,error = \ - self._u_invoke_client("config", "user.name", expect=(0,1)) - if status == 0: - name = output.rstrip('\n') - else: - name = "" - status,output,error = \ - self._u_invoke_client("config", "user.email", expect=(0,1)) - if status == 0: - email = output.rstrip('\n') - else: - email = "" - if name != "" or email != "": # got something! - # guess missing info, if necessary - if name == "": - name = self._u_get_fallback_username() - if email == "": - email = self._u_get_fallback_email() - return self._u_create_id(name, email) - return None # Git has no infomation - def _vcs_set_user_id(self, value): - name,email = self._u_parse_id(value) - if email != None: - self._u_invoke_client("config", "user.email", email) - self._u_invoke_client("config", "user.name", name) - def _vcs_add(self, path): - if os.path.isdir(path): - return - self._u_invoke_client("add", path) - def _vcs_remove(self, path): - if not os.path.isdir(self._u_abspath(path)): - self._u_invoke_client("rm", "-f", path) - def _vcs_update(self, path): - self._vcs_add(path) - def _vcs_get_file_contents(self, path, revision=None, binary=False): - if revision == None: - return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) - else: - arg = "%s:%s" % (revision,path) - status,output,error = self._u_invoke_client("show", arg) - return output - def _vcs_duplicate_repo(self, directory, revision=None): - if revision==None: - vcs.VCS._vcs_duplicate_repo(self, directory, revision) - else: - self._u_invoke_client("clone", "--no-checkout", ".", directory) - self._u_invoke_client("checkout", revision, cwd=directory) - def _vcs_commit(self, commitfile, allow_empty=False): - args = ['commit', '--all', '--file', commitfile] - if allow_empty == True: - args.append("--allow-empty") - status,output,error = self._u_invoke_client(*args) - else: - kwargs = {"expect":(0,1)} - status,output,error = self._u_invoke_client(*args, **kwargs) - strings = ["nothing to commit", - "nothing added to commit"] - if self._u_any_in_string(strings, output) == True: - raise vcs.EmptyCommit() - revision = None - revline = re.compile("(.*) (.*)[:\]] (.*)") - match = revline.search(output) - assert match != None, output+error - assert len(match.groups()) == 3 - revision = match.groups()[1] - full_revision = self._vcs_revision_id(-1) - assert full_revision.startswith(revision), \ - "Mismatched revisions:\n%s\n%s" % (revision, full_revision) - return full_revision - def _vcs_revision_id(self, index): - args = ["rev-list", "--first-parent", "--reverse", "HEAD"] - kwargs = {"expect":(0,128)} - status,output,error = self._u_invoke_client(*args, **kwargs) - if status == 128: - if error.startswith("fatal: ambiguous argument 'HEAD': unknown "): - return None - raise vcs.CommandError(args, status, stderr=error) - commits = output.splitlines() - try: - return commits[index] - except IndexError: - return None - - -if libbe.TESTING == True: - vcs.make_vcs_testcase_subclasses(Git, sys.modules[__name__]) - - unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) - suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/hg.py b/libbe/hg.py deleted file mode 100644 index ed27717..0000000 --- a/libbe/hg.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright (C) 2007-2009 Aaron Bentley and Panometrics, Inc. -# Ben Finney -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Mercurial (hg) backend. -""" - -import os -import re -import sys - -import libbe -import vcs - -if libbe.TESTING == True: - import unittest - import doctest - - -def new(): - return Hg() - -class Hg(vcs.VCS): - name="hg" - client="hg" - versioned=True - def _vcs_version(self): - status,output,error = self._u_invoke_client("--version") - return output - def _vcs_detect(self, path): - """Detect whether a directory is revision-controlled using Mercurial""" - if self._u_search_parent_directories(path, ".hg") != None: - return True - return False - def _vcs_root(self, path): - status,output,error = self._u_invoke_client("root", cwd=path) - return output.rstrip('\n') - def _vcs_init(self, path): - self._u_invoke_client("init", cwd=path) - def _vcs_get_user_id(self): - status,output,error = self._u_invoke_client("showconfig","ui.username") - return output.rstrip('\n') - def _vcs_set_user_id(self, value): - """ - Supported by the Config Extension, but that is not part of - standard Mercurial. - http://www.selenic.com/mercurial/wiki/index.cgi/ConfigExtension - """ - raise vcs.SettingIDnotSupported - def _vcs_add(self, path): - self._u_invoke_client("add", path) - def _vcs_remove(self, path): - self._u_invoke_client("rm", "--force", path) - def _vcs_update(self, path): - pass - def _vcs_get_file_contents(self, path, revision=None, binary=False): - if revision == None: - return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) - else: - status,output,error = \ - self._u_invoke_client("cat","-r",revision,path) - return output - def _vcs_duplicate_repo(self, directory, revision=None): - if revision == None: - return vcs.VCS._vcs_duplicate_repo(self, directory, revision) - else: - self._u_invoke_client("archive", "--rev", revision, directory) - def _vcs_commit(self, commitfile, allow_empty=False): - args = ['commit', '--logfile', commitfile] - status,output,error = self._u_invoke_client(*args) - if allow_empty == False: - strings = ["nothing changed"] - if self._u_any_in_string(strings, output) == True: - raise vcs.EmptyCommit() - return self._vcs_revision_id(-1) - def _vcs_revision_id(self, index, style="id"): - args = ["identify", "--rev", str(int(index)), "--%s" % style] - kwargs = {"expect": (0,255)} - status,output,error = self._u_invoke_client(*args, **kwargs) - if status == 0: - id = output.strip() - if id == '000000000000': - return None # before initial commit. - return id - return None - - -if libbe.TESTING == True: - vcs.make_vcs_testcase_subclasses(Hg, sys.modules[__name__]) - - unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) - suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/mapfile.py b/libbe/mapfile.py deleted file mode 100644 index 8e1e279..0000000 --- a/libbe/mapfile.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Provide a means of saving and loading dictionaries of parameters. The -saved "mapfiles" should be clear, flat-text files, and allow easy merging of -independent/conflicting changes. -""" - -import errno -import os.path -import yaml - -import libbe -if libbe.TESTING == True: - import doctest - - -class IllegalKey(Exception): - def __init__(self, key): - Exception.__init__(self, 'Illegal key "%s"' % key) - self.key = key - -class IllegalValue(Exception): - def __init__(self, value): - Exception.__init__(self, 'Illegal value "%s"' % value) - self.value = value - -def generate(map): - """Generate a YAML mapfile content string. - >>> generate({"q":"p"}) - 'q: p\\n\\n' - >>> generate({"q":u"Fran\u00e7ais"}) - 'q: Fran\\xc3\\xa7ais\\n\\n' - >>> generate({"q":u"hello"}) - 'q: hello\\n\\n' - >>> generate({"q=":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "q=" - >>> generate({"q:":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "q:" - >>> generate({"q\\n":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "q\\n" - >>> generate({"":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "" - >>> generate({">q":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key ">q" - >>> generate({"q":"p\\n"}) - Traceback (most recent call last): - IllegalValue: Illegal value "p\\n" - """ - keys = map.keys() - keys.sort() - for key in keys: - try: - assert not key.startswith('>') - assert('\n' not in key) - assert('=' not in key) - assert(':' not in key) - assert(len(key) > 0) - except AssertionError: - raise IllegalKey(unicode(key).encode('unicode_escape')) - if '\n' in map[key]: - raise IllegalValue(unicode(map[key]).encode('unicode_escape')) - - lines = [] - for key in keys: - lines.append(yaml.safe_dump({key: map[key]}, - default_flow_style=False, - allow_unicode=True)) - lines.append('') - return '\n'.join(lines) - -def parse(contents): - """ - Parse a YAML mapfile string. - >>> parse('q: p\\n\\n')['q'] - 'p' - >>> parse('q: \\'p\\'\\n\\n')['q'] - 'p' - >>> contents = generate({"a":"b", "c":"d", "e":"f"}) - >>> dict = parse(contents) - >>> dict["a"] - 'b' - >>> dict["c"] - 'd' - >>> dict["e"] - 'f' - >>> contents = generate({"q":u"Fran\u00e7ais"}) - >>> dict = parse(contents) - >>> dict["q"] - u'Fran\\xe7ais' - """ - return yaml.load(contents) or {} - -def map_save(vcs, path, map, allow_no_vcs=False): - """Save the map as a mapfile to the specified path""" - contents = generate(map) - vcs.set_file_contents(path, contents, allow_no_vcs, binary=True) - -def map_load(vcs, path, allow_no_vcs=False): - contents = vcs.get_file_contents(path, allow_no_vcs=allow_no_vcs, - binary=True) - return parse(contents) - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() diff --git a/libbe/pager.py b/libbe/pager.py deleted file mode 100644 index 1ddc3fa..0000000 --- a/libbe/pager.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (C) 2009 W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Automatic pager for terminal output (a la Git). -""" - -import sys, os, select - -# see http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby -def run_pager(paginate='auto'): - """ - paginate should be one of 'never', 'auto', or 'always'. - - usage: just call this function and continue using sys.stdout like - you normally would. - """ - if paginate == 'never' \ - or sys.platform == 'win32' \ - or not hasattr(sys.stdout, 'isatty') \ - or sys.stdout.isatty() == False: - return - - if paginate == 'auto': - if 'LESS' not in os.environ: - os.environ['LESS'] = '' # += doesn't work on undefined var - # don't page if the input is short enough - os.environ['LESS'] += ' -FRX' - if 'PAGER' in os.environ: - pager = os.environ['PAGER'] - else: - pager = 'less' - - read_fd, write_fd = os.pipe() - if os.fork() == 0: - # child process - os.close(read_fd) - os.close(0) - os.dup2(write_fd, 1) - os.close(write_fd) - if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() == True: - os.dup2(1, 2) - return - - # parent process, become pager - os.close(write_fd) - os.dup2(read_fd, 0) - os.close(read_fd) - - # Wait until we have input before we start the pager - select.select([0], [], []) - os.execlp(pager, pager) diff --git a/libbe/properties.py b/libbe/properties.py deleted file mode 100644 index f756ff0..0000000 --- a/libbe/properties.py +++ /dev/null @@ -1,642 +0,0 @@ -# Bugs Everywhere - a distributed bugtracker -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -This module provides a series of useful decorators for defining -various types of properties. For example usage, consider the -unittests at the end of the module. - -See - http://www.python.org/dev/peps/pep-0318/ -and - http://www.phyast.pitt.edu/~micheles/python/documentation.html -for more information on decorators. -""" - -import copy -import types - -import libbe -if libbe.TESTING == True: - import unittest - - -class ValueCheckError (ValueError): - def __init__(self, name, value, allowed): - action = "in" # some list of allowed values - if type(allowed) == types.FunctionType: - action = "allowed by" # some allowed-value check function - msg = "%s not %s %s for %s" % (value, action, allowed, name) - ValueError.__init__(self, msg) - self.name = name - self.value = value - self.allowed = allowed - -def Property(funcs): - """ - End a chain of property decorators, returning a property. - """ - args = {} - args["fget"] = funcs.get("fget", None) - args["fset"] = funcs.get("fset", None) - args["fdel"] = funcs.get("fdel", None) - args["doc"] = funcs.get("doc", None) - - #print "Creating a property with" - #for key, val in args.items(): print key, value - return property(**args) - -def doc_property(doc=None): - """ - Add a docstring to a chain of property decorators. - """ - def decorator(funcs=None): - """ - Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...} - or a function fn() returning such a dict. - """ - if hasattr(funcs, "__call__"): - funcs = funcs() # convert from function-arg to dict - funcs["doc"] = doc - return funcs - return decorator - -def local_property(name, null=None, mutable_null=False): - """ - Define get/set access to per-parent-instance local storage. Uses - .__value to store the value for a particular owner instance. - If the .__value attribute does not exist, returns null. - - If mutable_null == True, we only release deepcopies of the null to - the outside world. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget", None) - fset = funcs.get("fset", None) - def _fget(self): - if fget is not None: - fget(self) - if mutable_null == True: - ret_null = copy.deepcopy(null) - else: - ret_null = null - value = getattr(self, "_%s_value" % name, ret_null) - return value - def _fset(self, value): - setattr(self, "_%s_value" % name, value) - if fset is not None: - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - funcs["name"] = name - return funcs - return decorator - -def settings_property(name, null=None): - """ - Similar to local_property, except where local_property stores the - value in instance.__value, settings_property stores the - value in instance.settings[name]. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget", None) - fset = funcs.get("fset", None) - def _fget(self): - if fget is not None: - fget(self) - value = self.settings.get(name, null) - return value - def _fset(self, value): - self.settings[name] = value - if fset is not None: - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - funcs["name"] = name - return funcs - return decorator - - -# Allow comparison and caching with _original_ values for mutables, -# since -# -# >>> a = [] -# >>> b = a -# >>> b.append(1) -# >>> a -# [1] -# >>> a==b -# True -def _hash_mutable_value(value): - return repr(value) -def _init_mutable_property_cache(self): - if not hasattr(self, "_mutable_property_cache_hash"): - # first call to _fget for any mutable property - self._mutable_property_cache_hash = {} - self._mutable_property_cache_copy = {} -def _set_cached_mutable_property(self, cacher_name, property_name, value): - _init_mutable_property_cache(self) - self._mutable_property_cache_hash[(cacher_name, property_name)] = \ - _hash_mutable_value(value) - self._mutable_property_cache_copy[(cacher_name, property_name)] = \ - copy.deepcopy(value) -def _get_cached_mutable_property(self, cacher_name, property_name, default=None): - _init_mutable_property_cache(self) - if (cacher_name, property_name) not in self._mutable_property_cache_copy: - return default - return self._mutable_property_cache_copy[(cacher_name, property_name)] -def _cmp_cached_mutable_property(self, cacher_name, property_name, value, default=None): - _init_mutable_property_cache(self) - if (cacher_name, property_name) not in self._mutable_property_cache_hash: - _set_cached_mutable_property(self, cacher_name, property_name, default) - old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)] - return cmp(_hash_mutable_value(value), old_hash) - - -def defaulting_property(default=None, null=None, - mutable_default=False): - """ - Define a default value for get access to a property. - If the stored value is null, then default is returned. - - If mutable_default == True, we only release deepcopies of the - default to the outside world. - - null should never escape to the outside world, so don't worry - about it being a mutable. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self): - value = fget(self) - if value == null: - if mutable_default == True: - return copy.deepcopy(default) - else: - return default - return value - def _fset(self, value): - if value == default: - value = null - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - -def fn_checked_property(value_allowed_fn): - """ - Define allowed values for get/set access to a property. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self): - value = fget(self) - if value_allowed_fn(value) != True: - raise ValueCheckError(name, value, value_allowed_fn) - return value - def _fset(self, value): - if value_allowed_fn(value) != True: - raise ValueCheckError(name, value, value_allowed_fn) - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - -def checked_property(allowed=[]): - """ - Define allowed values for get/set access to a property. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self): - value = fget(self) - if value not in allowed: - raise ValueCheckError(name, value, allowed) - return value - def _fset(self, value): - if value not in allowed: - raise ValueCheckError(name, value, allowed) - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - -def cached_property(generator, initVal=None, mutable=False): - """ - Allow caching of values generated by generator(instance), where - instance is the instance to which this property belongs. Uses - .__cache to store a cache flag for a particular owner - instance. - - When the cache flag is True or missing and the stored value is - initVal, the first fget call triggers the generator function, - whose output is stored in __cached_value. That and - subsequent calls to fget will return this cached value. - - If the input value is no longer initVal (e.g. a value has been - loaded from disk or set with fset), that value overrides any - cached value, and this property has no effect. - - When the cache flag is False and the stored value is initVal, the - generator is not cached, but is called on every fget. - - The cache flag is missing on initialization. Particular instances - may override by setting their own flag. - - In the case that mutable == True, all caching is disabled and the - generator is called whenever the cached value would otherwise be - used. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - name = funcs.get("name", "") - def _fget(self): - cache = getattr(self, "_%s_cache" % name, True) - value = fget(self) - if value == initVal: - if cache == True and mutable == False: - if hasattr(self, "_%s_cached_value" % name): - value = getattr(self, "_%s_cached_value" % name) - else: - value = generator(self) - setattr(self, "_%s_cached_value" % name, value) - else: - value = generator(self) - return value - funcs["fget"] = _fget - return funcs - return decorator - -def primed_property(primer, initVal=None): - """ - Just like a cached_property, except that instead of returning a - new value and running fset to cache it, the primer performs some - background manipulation (e.g. loads data into instance.settings) - such that a _second_ pass through fget succeeds. - - The 'cache' flag becomes a 'prime' flag, with priming taking place - whenever .__prime is True, or is False or missing and - value == initVal. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - name = funcs.get("name", "") - def _fget(self): - prime = getattr(self, "_%s_prime" % name, False) - if prime == False: - value = fget(self) - if prime == True or (prime == False and value == initVal): - primer(self) - value = fget(self) - return value - funcs["fget"] = _fget - return funcs - return decorator - -def change_hook_property(hook, mutable=False, default=None): - """ - Call the function hook(instance, old_value, new_value) whenever a - value different from the current value is set (instance is a a - reference to the class instance to which this property belongs). - This is useful for saving changes to disk, etc. This function is - called _after_ the new value has been stored, allowing you to - change the stored value if you want. - - In the case of mutables, things are slightly trickier. Because - the property-owning class has no way of knowing when the value - changes. We work around this by caching a private deepcopy of the - mutable value, and checking for changes whenever the property is - set (obviously) or retrieved (to check for external changes). So - long as you're conscientious about accessing the property after - making external modifications, mutability woln't be a problem. - t.x.append(5) # external modification - t.x # dummy access notices change and triggers hook - See testChangeHookMutableProperty for an example of the expected - behavior. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self, new_value=None, from_fset=False): # only used if mutable == True - if from_fset == True: - value = new_value # compare new value with cached - else: - value = fget(self) # compare current value with cached - if _cmp_cached_mutable_property(self, "change hook property", name, value, default) != 0: - # there has been a change, cache new value - old_value = _get_cached_mutable_property(self, "change hook property", name, default) - _set_cached_mutable_property(self, "change hook property", name, value) - if from_fset == True: # return previously cached value - value = old_value - else: # the value changed while we weren't looking - hook(self, old_value, value) - return value - def _fset(self, value): - if mutable == True: # get cached previous value - old_value = _fget(self, new_value=value, from_fset=True) - else: - old_value = fget(self) - fset(self, value) - if value != old_value: - hook(self, old_value, value) - if mutable == True: - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - -if libbe.TESTING == True: - class DecoratorTests(unittest.TestCase): - def testLocalDoc(self): - class Test(object): - @Property - @doc_property("A fancy property") - def x(): - return {} - self.failUnless(Test.x.__doc__ == "A fancy property", - Test.x.__doc__) - def testLocalProperty(self): - class Test(object): - @Property - @local_property(name="LOCAL") - def x(): - return {} - t = Test() - self.failUnless(t.x == None, str(t.x)) - t.x = 'z' # the first set initializes ._LOCAL_value - self.failUnless(t.x == 'z', str(t.x)) - self.failUnless("_LOCAL_value" in dir(t), dir(t)) - self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value) - def testSettingsProperty(self): - class Test(object): - @Property - @settings_property(name="attr") - def x(): - return {} - def __init__(self): - self.settings = {} - t = Test() - self.failUnless(t.x == None, str(t.x)) - t.x = 'z' # the first set initializes ._LOCAL_value - self.failUnless(t.x == 'z', str(t.x)) - self.failUnless("attr" in t.settings, t.settings) - self.failUnless(t.settings["attr"] == 'z', t.settings["attr"]) - def testDefaultingLocalProperty(self): - class Test(object): - @Property - @defaulting_property(default='y', null='x') - @local_property(name="DEFAULT", null=5) - def x(): return {} - t = Test() - self.failUnless(t.x == 5, str(t.x)) - t.x = 'x' - self.failUnless(t.x == 'y', str(t.x)) - t.x = 'y' - self.failUnless(t.x == 'y', str(t.x)) - t.x = 'z' - self.failUnless(t.x == 'z', str(t.x)) - t.x = 5 - self.failUnless(t.x == 5, str(t.x)) - def testCheckedLocalProperty(self): - class Test(object): - @Property - @checked_property(allowed=['x', 'y', 'z']) - @local_property(name="CHECKED") - def x(): return {} - def __init__(self): - self._CHECKED_value = 'x' - t = Test() - self.failUnless(t.x == 'x', str(t.x)) - try: - t.x = None - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - def testTwoCheckedLocalProperties(self): - class Test(object): - @Property - @checked_property(allowed=['x', 'y', 'z']) - @local_property(name="X") - def x(): return {} - - @Property - @checked_property(allowed=['a', 'b', 'c']) - @local_property(name="A") - def a(): return {} - def __init__(self): - self._A_value = 'a' - self._X_value = 'x' - t = Test() - try: - t.x = 'a' - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - t.x = 'x' - t.x = 'y' - t.x = 'z' - try: - t.a = 'x' - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - t.a = 'a' - t.a = 'b' - t.a = 'c' - def testFnCheckedLocalProperty(self): - class Test(object): - @Property - @fn_checked_property(lambda v : v in ['x', 'y', 'z']) - @local_property(name="CHECKED") - def x(): return {} - def __init__(self): - self._CHECKED_value = 'x' - t = Test() - self.failUnless(t.x == 'x', str(t.x)) - try: - t.x = None - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - def testCachedLocalProperty(self): - class Gen(object): - def __init__(self): - self.i = 0 - def __call__(self, owner): - self.i += 1 - return self.i - class Test(object): - @Property - @cached_property(generator=Gen(), initVal=None) - @local_property(name="CACHED") - def x(): return {} - t = Test() - self.failIf("_CACHED_cache" in dir(t), - getattr(t, "_CACHED_cache", None)) - self.failUnless(t.x == 1, t.x) - self.failUnless(t.x == 1, t.x) - self.failUnless(t.x == 1, t.x) - t.x = 8 - self.failUnless(t.x == 8, t.x) - self.failUnless(t.x == 8, t.x) - t._CACHED_cache = False # Caching is off, but the stored value - val = t.x # is 8, not the initVal (None), so we - self.failUnless(val == 8, val) # get 8. - t._CACHED_value = None # Now we've set the stored value to None - val = t.x # so future calls to fget (like this) - self.failUnless(val == 2, val) # will call the generator every time... - val = t.x - self.failUnless(val == 3, val) - val = t.x - self.failUnless(val == 4, val) - t._CACHED_cache = True # We turn caching back on, and get - self.failUnless(t.x == 1, str(t.x)) # the original cached value. - del t._CACHED_cached_value # Removing that value forces a - self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call - self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which - self.failUnless(t.x == 5, str(t.x)) # we get the new cached value. - def testPrimedLocalProperty(self): - class Test(object): - def prime(self): - self.settings["PRIMED"] = "initialized" - @Property - @primed_property(primer=prime, initVal=None) - @settings_property(name="PRIMED") - def x(): return {} - def __init__(self): - self.settings={} - t = Test() - self.failIf("_PRIMED_prime" in dir(t), - getattr(t, "_PRIMED_prime", None)) - self.failUnless(t.x == "initialized", t.x) - t.x = 1 - self.failUnless(t.x == 1, t.x) - t.x = None - self.failUnless(t.x == "initialized", t.x) - t._PRIMED_prime = True - t.x = 3 - self.failUnless(t.x == "initialized", t.x) - t._PRIMED_prime = False - t.x = 3 - self.failUnless(t.x == 3, t.x) - def testChangeHookLocalProperty(self): - class Test(object): - def _hook(self, old, new): - self.old = old - self.new = new - - @Property - @change_hook_property(_hook) - @local_property(name="HOOKED") - def x(): return {} - t = Test() - t.x = 1 - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == 1, t.new) - t.x = 1 - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == 1, t.new) - t.x = 2 - self.failUnless(t.old == 1, t.old) - self.failUnless(t.new == 2, t.new) - def testChangeHookMutableProperty(self): - class Test(object): - def _hook(self, old, new): - self.old = old - self.new = new - self.hook_calls += 1 - - @Property - @change_hook_property(_hook, mutable=True) - @local_property(name="HOOKED") - def x(): return {} - t = Test() - t.hook_calls = 0 - t.x = [] - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == [], t.new) - self.failUnless(t.hook_calls == 1, t.hook_calls) - a = t.x - a.append(5) - t.x = a - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5], t.new) - self.failUnless(t.hook_calls == 2, t.hook_calls) - t.x = [] - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [], t.new) - self.failUnless(t.hook_calls == 3, t.hook_calls) - # now append without reassigning. this doesn't trigger the - # change, since we don't ever set t.x, only get it and mess - # with it. It does, however, update our t.new, since t.new = - # t.x and is not a static copy. - t.x.append(5) - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [5], t.new) - self.failUnless(t.hook_calls == 3, t.hook_calls) - # however, the next t.x get _will_ notice the change... - a = t.x - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5], t.new) - self.failUnless(t.hook_calls == 4, t.hook_calls) - t.x.append(6) # this append(6) is not noticed yet - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5,6], t.new) - self.failUnless(t.hook_calls == 4, t.hook_calls) - # this append(7) is not noticed, but the t.x get causes the - # append(6) to be noticed - t.x.append(7) - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [5,6,7], t.new) - self.failUnless(t.hook_calls == 5, t.hook_calls) - a = t.x # now the append(7) is noticed - self.failUnless(t.old == [5,6], t.old) - self.failUnless(t.new == [5,6,7], t.new) - self.failUnless(t.hook_calls == 6, t.hook_calls) - - suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) diff --git a/libbe/settings_object.py b/libbe/settings_object.py deleted file mode 100644 index 6a00ba9..0000000 --- a/libbe/settings_object.py +++ /dev/null @@ -1,433 +0,0 @@ -# Bugs Everywhere - a distributed bugtracker -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -This module provides a base class implementing settings-dict based -property storage useful for BE objects with saved properties -(e.g. BugDir, Bug, Comment). For example usage, consider the -unittests at the end of the module. -""" - -import libbe -from properties import Property, doc_property, local_property, \ - defaulting_property, checked_property, fn_checked_property, \ - cached_property, primed_property, change_hook_property, \ - settings_property -if libbe.TESTING == True: - import doctest - import unittest - - -class _Token (object): - """ - `Control' value class for properties. We want values that only - mean something to the settings_object module. - """ - pass - -class UNPRIMED (_Token): - "Property has not been primed." - pass - -class EMPTY (_Token): - """ - Property has been primed but has no user-set value, so use - default/generator value. - """ - pass - - -def prop_save_settings(self, old, new): - """ - The default action undertaken when a property changes. - """ - if self.sync_with_disk==True: - self.save_settings() - -def prop_load_settings(self): - """ - The default action undertaken when an UNPRIMED property is accessed. - """ - if self.sync_with_disk==True and self._settings_loaded==False: - self.load_settings() - else: - self._setup_saved_settings(flag_as_loaded=False) - -# Some name-mangling routines for pretty printing setting names -def setting_name_to_attr_name(self, name): - """ - Convert keys to the .settings dict into their associated - SavedSettingsObject attribute names. - >>> print setting_name_to_attr_name(None,"User-id") - user_id - """ - return name.lower().replace('-', '_') - -def attr_name_to_setting_name(self, name): - """ - The inverse of setting_name_to_attr_name. - >>> print attr_name_to_setting_name(None, "user_id") - User-id - """ - return name.capitalize().replace('_', '-') - - -def versioned_property(name, doc, - default=None, generator=None, - change_hook=prop_save_settings, - mutable=False, - primer=prop_load_settings, - allowed=None, check_fn=None, - settings_properties=[], - required_saved_properties=[], - require_save=False): - """ - Combine the common decorators in a single function. - - Use zero or one (but not both) of default or generator, since a - working default will keep the generator from functioning. Use the - default if you know what you want the default value to be at - 'coding time'. Use the generator if you can write a function to - determine a valid default at run time. If both default and - generator are None, then the property will be a defaulting - property which defaults to None. - - allowed and check_fn have a similar relationship, although you can - use both of these if you want. allowed compares the proposed - value against a list determined at 'coding time' and check_fn - allows more flexible comparisons to take place at run time. - - Set require_save to True if you want to save the default/generated - value for a property, to protect against future changes. E.g., we - currently expect all comments to be 'text/plain' but in the future - we may want to default to 'text/html'. If we don't want the old - comments to be interpreted as 'text/html', we would require that - the content type be saved. - - change_hook, primer, settings_properties, and - required_saved_properties are only options to get their defaults - into our local scope. Don't mess with them. - - Set mutable=True if: - * default is a mutable - * your generator function may return mutables - * you set change_hook and might have mutable property values - See the docstrings in libbe.properties for details on how each of - these cases are handled. - """ - settings_properties.append(name) - if require_save == True: - required_saved_properties.append(name) - def decorator(funcs): - fulldoc = doc - if default != None or generator == None: - defaulting = defaulting_property(default=default, null=EMPTY, - mutable_default=mutable) - fulldoc += "\n\nThis property defaults to %s." % default - if generator != None: - cached = cached_property(generator=generator, initVal=EMPTY, - mutable=mutable) - fulldoc += "\n\nThis property is generated with %s." % generator - if check_fn != None: - fn_checked = fn_checked_property(value_allowed_fn=check_fn) - fulldoc += "\n\nThis property is checked with %s." % check_fn - if allowed != None: - checked = checked_property(allowed=allowed) - fulldoc += "\n\nThe allowed values for this property are: %s." \ - % (', '.join(allowed)) - hooked = change_hook_property(hook=change_hook, mutable=mutable, - default=EMPTY) - primed = primed_property(primer=primer, initVal=UNPRIMED) - settings = settings_property(name=name, null=UNPRIMED) - docp = doc_property(doc=fulldoc) - deco = hooked(primed(settings(docp(funcs)))) - if default != None or generator == None: - deco = defaulting(deco) - if generator != None: - deco = cached(deco) - if check_fn != None: - deco = fn_checked(deco) - if allowed != None: - deco = checked(deco) - return Property(deco) - return decorator - -class SavedSettingsObject(object): - - # Keep a list of properties that may be stored in the .settings dict. - #settings_properties = [] - - # A list of properties that we save to disk, even if they were - # never set (in which case we save the default value). This - # protects against future changes in default values. - #required_saved_properties = [] - - _setting_name_to_attr_name = setting_name_to_attr_name - _attr_name_to_setting_name = attr_name_to_setting_name - - def __init__(self): - self._settings_loaded = False - self.sync_with_disk = False - self.settings = {} - - def load_settings(self): - """Load the settings from disk.""" - # Override. Must call ._setup_saved_settings() after loading. - self.settings = {} - self._setup_saved_settings() - - def _setup_saved_settings(self, flag_as_loaded=True): - """ - To be run after setting self.settings up from disk. Marks all - settings as primed. - """ - for property in self.settings_properties: - if property not in self.settings: - self.settings[property] = EMPTY - elif self.settings[property] == UNPRIMED: - self.settings[property] = EMPTY - if flag_as_loaded == True: - self._settings_loaded = True - - def save_settings(self): - """Load the settings from disk.""" - # Override. Should save the dict output of ._get_saved_settings() - settings = self._get_saved_settings() - pass # write settings to disk.... - - def _get_saved_settings(self): - settings = {} - for k,v in self.settings.items(): - if v != None and v != EMPTY: - settings[k] = v - for k in self.required_saved_properties: - settings[k] = getattr(self, self._setting_name_to_attr_name(k)) - return settings - - def clear_cached_setting(self, setting=None): - "If setting=None, clear *all* cached settings" - if setting != None: - if hasattr(self, "_%s_cached_value" % setting): - delattr(self, "_%s_cached_value" % setting) - else: - for setting in settings_properties: - self.clear_cached_setting(setting) - - -if libbe.TESTING == True: - class SavedSettingsObjectTests(unittest.TestCase): - def testSimpleProperty(self): - """Testing a minimal versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - # access missing setting - self.failUnless(t._settings_loaded == False, t._settings_loaded) - self.failUnless(len(t.settings) == 0, len(t.settings)) - self.failUnless(t.content_type == None, t.content_type) - # accessing t.content_type triggers the priming, which runs - # t._setup_saved_settings, which fills out t.settings with - # EMPTY data. t._settings_loaded is still false though, since - # the default priming does not do any of the `official' loading - # that occurs in t.load_settings. - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - # load settings creates an EMPTY value in the settings array - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t.content_type == None, t.content_type) - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - # now we set a value - t.content_type = 5 - self.failUnless(t.settings["Content-type"] == 5, - t.settings["Content-type"]) - self.failUnless(t.content_type == 5, t.content_type) - self.failUnless(t.settings["Content-type"] == 5, - t.settings["Content-type"]) - # now we set another value - t.content_type = "text/plain" - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t.settings["Content-type"] == "text/plain", - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/plain"}, - t._get_saved_settings()) - # now we clear to the post-primed value - t.content_type = EMPTY - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t.content_type == None, t.content_type) - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - def testDefaultingProperty(self): - """Testing a defaulting versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings() == {}, - t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t.content_type == "text/html", - t.content_type) - self.failUnless(t.settings["Content-type"] == "text/html", - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/html"}, - t._get_saved_settings()) - def testRequiredDefaultingProperty(self): - """Testing a required defaulting versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties, - require_save=True) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/plain"}, - t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/html"}, - t._get_saved_settings()) - def testClassVersionedPropertyDefinition(self): - """Testing a class-specific _versioned property decorator""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - def _versioned_property(settings_properties= \ - settings_properties, - required_saved_properties= \ - required_saved_properties, - **kwargs): - if "settings_properties" not in kwargs: - kwargs["settings_properties"] = settings_properties - if "required_saved_properties" not in kwargs: - kwargs["required_saved_properties"] = \ - required_saved_properties - return versioned_property(**kwargs) - @_versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - require_save=True) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/plain"}, - t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/html"}, - t._get_saved_settings()) - def testMutableChangeHookedProperty(self): - """Testing a mutable change-hooked property""" - SAVES = [] - def prop_log_save_settings(self, old, new, saves=SAVES): - saves.append("'%s' -> '%s'" % (str(old), str(new))) - prop_save_settings(self, old, new) - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="List-type", - doc="A test property", - mutable=True, - change_hook=prop_log_save_settings, - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) - def list_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(SAVES == [], SAVES) - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.list_type == None, t.list_type) - self.failUnless(SAVES == [], SAVES) - self.failUnless(t.settings["List-type"]==EMPTY, - t.settings["List-type"]) - t.list_type = [] - self.failUnless(t.settings["List-type"] == [], - t.settings["List-type"]) - self.failUnless(SAVES == [ - "'' -> '[]'" - ], SAVES) - t.list_type.append(5) - self.failUnless(SAVES == [ - "'' -> '[]'", - ], SAVES) - self.failUnless(t.settings["List-type"] == [5], - t.settings["List-type"]) - self.failUnless(SAVES == [ # the append(5) has not yet been saved - "'' -> '[]'", - ], SAVES) - self.failUnless(t.list_type == [5], t.list_type)#get triggers saved - - self.failUnless(SAVES == [ # now the append(5) has been saved. - "'' -> '[]'", - "'[]' -> '[5]'" - ], SAVES) - - unitsuite = unittest.TestLoader().loadTestsFromTestCase( \ - SavedSettingsObjectTests) - suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/storage/properties.py b/libbe/storage/properties.py new file mode 100644 index 0000000..f756ff0 --- /dev/null +++ b/libbe/storage/properties.py @@ -0,0 +1,642 @@ +# Bugs Everywhere - a distributed bugtracker +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +This module provides a series of useful decorators for defining +various types of properties. For example usage, consider the +unittests at the end of the module. + +See + http://www.python.org/dev/peps/pep-0318/ +and + http://www.phyast.pitt.edu/~micheles/python/documentation.html +for more information on decorators. +""" + +import copy +import types + +import libbe +if libbe.TESTING == True: + import unittest + + +class ValueCheckError (ValueError): + def __init__(self, name, value, allowed): + action = "in" # some list of allowed values + if type(allowed) == types.FunctionType: + action = "allowed by" # some allowed-value check function + msg = "%s not %s %s for %s" % (value, action, allowed, name) + ValueError.__init__(self, msg) + self.name = name + self.value = value + self.allowed = allowed + +def Property(funcs): + """ + End a chain of property decorators, returning a property. + """ + args = {} + args["fget"] = funcs.get("fget", None) + args["fset"] = funcs.get("fset", None) + args["fdel"] = funcs.get("fdel", None) + args["doc"] = funcs.get("doc", None) + + #print "Creating a property with" + #for key, val in args.items(): print key, value + return property(**args) + +def doc_property(doc=None): + """ + Add a docstring to a chain of property decorators. + """ + def decorator(funcs=None): + """ + Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...} + or a function fn() returning such a dict. + """ + if hasattr(funcs, "__call__"): + funcs = funcs() # convert from function-arg to dict + funcs["doc"] = doc + return funcs + return decorator + +def local_property(name, null=None, mutable_null=False): + """ + Define get/set access to per-parent-instance local storage. Uses + .__value to store the value for a particular owner instance. + If the .__value attribute does not exist, returns null. + + If mutable_null == True, we only release deepcopies of the null to + the outside world. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget", None) + fset = funcs.get("fset", None) + def _fget(self): + if fget is not None: + fget(self) + if mutable_null == True: + ret_null = copy.deepcopy(null) + else: + ret_null = null + value = getattr(self, "_%s_value" % name, ret_null) + return value + def _fset(self, value): + setattr(self, "_%s_value" % name, value) + if fset is not None: + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + funcs["name"] = name + return funcs + return decorator + +def settings_property(name, null=None): + """ + Similar to local_property, except where local_property stores the + value in instance.__value, settings_property stores the + value in instance.settings[name]. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget", None) + fset = funcs.get("fset", None) + def _fget(self): + if fget is not None: + fget(self) + value = self.settings.get(name, null) + return value + def _fset(self, value): + self.settings[name] = value + if fset is not None: + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + funcs["name"] = name + return funcs + return decorator + + +# Allow comparison and caching with _original_ values for mutables, +# since +# +# >>> a = [] +# >>> b = a +# >>> b.append(1) +# >>> a +# [1] +# >>> a==b +# True +def _hash_mutable_value(value): + return repr(value) +def _init_mutable_property_cache(self): + if not hasattr(self, "_mutable_property_cache_hash"): + # first call to _fget for any mutable property + self._mutable_property_cache_hash = {} + self._mutable_property_cache_copy = {} +def _set_cached_mutable_property(self, cacher_name, property_name, value): + _init_mutable_property_cache(self) + self._mutable_property_cache_hash[(cacher_name, property_name)] = \ + _hash_mutable_value(value) + self._mutable_property_cache_copy[(cacher_name, property_name)] = \ + copy.deepcopy(value) +def _get_cached_mutable_property(self, cacher_name, property_name, default=None): + _init_mutable_property_cache(self) + if (cacher_name, property_name) not in self._mutable_property_cache_copy: + return default + return self._mutable_property_cache_copy[(cacher_name, property_name)] +def _cmp_cached_mutable_property(self, cacher_name, property_name, value, default=None): + _init_mutable_property_cache(self) + if (cacher_name, property_name) not in self._mutable_property_cache_hash: + _set_cached_mutable_property(self, cacher_name, property_name, default) + old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)] + return cmp(_hash_mutable_value(value), old_hash) + + +def defaulting_property(default=None, null=None, + mutable_default=False): + """ + Define a default value for get access to a property. + If the stored value is null, then default is returned. + + If mutable_default == True, we only release deepcopies of the + default to the outside world. + + null should never escape to the outside world, so don't worry + about it being a mutable. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self): + value = fget(self) + if value == null: + if mutable_default == True: + return copy.deepcopy(default) + else: + return default + return value + def _fset(self, value): + if value == default: + value = null + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +def fn_checked_property(value_allowed_fn): + """ + Define allowed values for get/set access to a property. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self): + value = fget(self) + if value_allowed_fn(value) != True: + raise ValueCheckError(name, value, value_allowed_fn) + return value + def _fset(self, value): + if value_allowed_fn(value) != True: + raise ValueCheckError(name, value, value_allowed_fn) + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +def checked_property(allowed=[]): + """ + Define allowed values for get/set access to a property. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self): + value = fget(self) + if value not in allowed: + raise ValueCheckError(name, value, allowed) + return value + def _fset(self, value): + if value not in allowed: + raise ValueCheckError(name, value, allowed) + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +def cached_property(generator, initVal=None, mutable=False): + """ + Allow caching of values generated by generator(instance), where + instance is the instance to which this property belongs. Uses + .__cache to store a cache flag for a particular owner + instance. + + When the cache flag is True or missing and the stored value is + initVal, the first fget call triggers the generator function, + whose output is stored in __cached_value. That and + subsequent calls to fget will return this cached value. + + If the input value is no longer initVal (e.g. a value has been + loaded from disk or set with fset), that value overrides any + cached value, and this property has no effect. + + When the cache flag is False and the stored value is initVal, the + generator is not cached, but is called on every fget. + + The cache flag is missing on initialization. Particular instances + may override by setting their own flag. + + In the case that mutable == True, all caching is disabled and the + generator is called whenever the cached value would otherwise be + used. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + name = funcs.get("name", "") + def _fget(self): + cache = getattr(self, "_%s_cache" % name, True) + value = fget(self) + if value == initVal: + if cache == True and mutable == False: + if hasattr(self, "_%s_cached_value" % name): + value = getattr(self, "_%s_cached_value" % name) + else: + value = generator(self) + setattr(self, "_%s_cached_value" % name, value) + else: + value = generator(self) + return value + funcs["fget"] = _fget + return funcs + return decorator + +def primed_property(primer, initVal=None): + """ + Just like a cached_property, except that instead of returning a + new value and running fset to cache it, the primer performs some + background manipulation (e.g. loads data into instance.settings) + such that a _second_ pass through fget succeeds. + + The 'cache' flag becomes a 'prime' flag, with priming taking place + whenever .__prime is True, or is False or missing and + value == initVal. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + name = funcs.get("name", "") + def _fget(self): + prime = getattr(self, "_%s_prime" % name, False) + if prime == False: + value = fget(self) + if prime == True or (prime == False and value == initVal): + primer(self) + value = fget(self) + return value + funcs["fget"] = _fget + return funcs + return decorator + +def change_hook_property(hook, mutable=False, default=None): + """ + Call the function hook(instance, old_value, new_value) whenever a + value different from the current value is set (instance is a a + reference to the class instance to which this property belongs). + This is useful for saving changes to disk, etc. This function is + called _after_ the new value has been stored, allowing you to + change the stored value if you want. + + In the case of mutables, things are slightly trickier. Because + the property-owning class has no way of knowing when the value + changes. We work around this by caching a private deepcopy of the + mutable value, and checking for changes whenever the property is + set (obviously) or retrieved (to check for external changes). So + long as you're conscientious about accessing the property after + making external modifications, mutability woln't be a problem. + t.x.append(5) # external modification + t.x # dummy access notices change and triggers hook + See testChangeHookMutableProperty for an example of the expected + behavior. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self, new_value=None, from_fset=False): # only used if mutable == True + if from_fset == True: + value = new_value # compare new value with cached + else: + value = fget(self) # compare current value with cached + if _cmp_cached_mutable_property(self, "change hook property", name, value, default) != 0: + # there has been a change, cache new value + old_value = _get_cached_mutable_property(self, "change hook property", name, default) + _set_cached_mutable_property(self, "change hook property", name, value) + if from_fset == True: # return previously cached value + value = old_value + else: # the value changed while we weren't looking + hook(self, old_value, value) + return value + def _fset(self, value): + if mutable == True: # get cached previous value + old_value = _fget(self, new_value=value, from_fset=True) + else: + old_value = fget(self) + fset(self, value) + if value != old_value: + hook(self, old_value, value) + if mutable == True: + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +if libbe.TESTING == True: + class DecoratorTests(unittest.TestCase): + def testLocalDoc(self): + class Test(object): + @Property + @doc_property("A fancy property") + def x(): + return {} + self.failUnless(Test.x.__doc__ == "A fancy property", + Test.x.__doc__) + def testLocalProperty(self): + class Test(object): + @Property + @local_property(name="LOCAL") + def x(): + return {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("_LOCAL_value" in dir(t), dir(t)) + self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value) + def testSettingsProperty(self): + class Test(object): + @Property + @settings_property(name="attr") + def x(): + return {} + def __init__(self): + self.settings = {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("attr" in t.settings, t.settings) + self.failUnless(t.settings["attr"] == 'z', t.settings["attr"]) + def testDefaultingLocalProperty(self): + class Test(object): + @Property + @defaulting_property(default='y', null='x') + @local_property(name="DEFAULT", null=5) + def x(): return {} + t = Test() + self.failUnless(t.x == 5, str(t.x)) + t.x = 'x' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'y' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'z' + self.failUnless(t.x == 'z', str(t.x)) + t.x = 5 + self.failUnless(t.x == 5, str(t.x)) + def testCheckedLocalProperty(self): + class Test(object): + @Property + @checked_property(allowed=['x', 'y', 'z']) + @local_property(name="CHECKED") + def x(): return {} + def __init__(self): + self._CHECKED_value = 'x' + t = Test() + self.failUnless(t.x == 'x', str(t.x)) + try: + t.x = None + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + def testTwoCheckedLocalProperties(self): + class Test(object): + @Property + @checked_property(allowed=['x', 'y', 'z']) + @local_property(name="X") + def x(): return {} + + @Property + @checked_property(allowed=['a', 'b', 'c']) + @local_property(name="A") + def a(): return {} + def __init__(self): + self._A_value = 'a' + self._X_value = 'x' + t = Test() + try: + t.x = 'a' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + t.x = 'x' + t.x = 'y' + t.x = 'z' + try: + t.a = 'x' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + t.a = 'a' + t.a = 'b' + t.a = 'c' + def testFnCheckedLocalProperty(self): + class Test(object): + @Property + @fn_checked_property(lambda v : v in ['x', 'y', 'z']) + @local_property(name="CHECKED") + def x(): return {} + def __init__(self): + self._CHECKED_value = 'x' + t = Test() + self.failUnless(t.x == 'x', str(t.x)) + try: + t.x = None + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + def testCachedLocalProperty(self): + class Gen(object): + def __init__(self): + self.i = 0 + def __call__(self, owner): + self.i += 1 + return self.i + class Test(object): + @Property + @cached_property(generator=Gen(), initVal=None) + @local_property(name="CACHED") + def x(): return {} + t = Test() + self.failIf("_CACHED_cache" in dir(t), + getattr(t, "_CACHED_cache", None)) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + t.x = 8 + self.failUnless(t.x == 8, t.x) + self.failUnless(t.x == 8, t.x) + t._CACHED_cache = False # Caching is off, but the stored value + val = t.x # is 8, not the initVal (None), so we + self.failUnless(val == 8, val) # get 8. + t._CACHED_value = None # Now we've set the stored value to None + val = t.x # so future calls to fget (like this) + self.failUnless(val == 2, val) # will call the generator every time... + val = t.x + self.failUnless(val == 3, val) + val = t.x + self.failUnless(val == 4, val) + t._CACHED_cache = True # We turn caching back on, and get + self.failUnless(t.x == 1, str(t.x)) # the original cached value. + del t._CACHED_cached_value # Removing that value forces a + self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call + self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which + self.failUnless(t.x == 5, str(t.x)) # we get the new cached value. + def testPrimedLocalProperty(self): + class Test(object): + def prime(self): + self.settings["PRIMED"] = "initialized" + @Property + @primed_property(primer=prime, initVal=None) + @settings_property(name="PRIMED") + def x(): return {} + def __init__(self): + self.settings={} + t = Test() + self.failIf("_PRIMED_prime" in dir(t), + getattr(t, "_PRIMED_prime", None)) + self.failUnless(t.x == "initialized", t.x) + t.x = 1 + self.failUnless(t.x == 1, t.x) + t.x = None + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = True + t.x = 3 + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = False + t.x = 3 + self.failUnless(t.x == 3, t.x) + def testChangeHookLocalProperty(self): + class Test(object): + def _hook(self, old, new): + self.old = old + self.new = new + + @Property + @change_hook_property(_hook) + @local_property(name="HOOKED") + def x(): return {} + t = Test() + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 2 + self.failUnless(t.old == 1, t.old) + self.failUnless(t.new == 2, t.new) + def testChangeHookMutableProperty(self): + class Test(object): + def _hook(self, old, new): + self.old = old + self.new = new + self.hook_calls += 1 + + @Property + @change_hook_property(_hook, mutable=True) + @local_property(name="HOOKED") + def x(): return {} + t = Test() + t.hook_calls = 0 + t.x = [] + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == [], t.new) + self.failUnless(t.hook_calls == 1, t.hook_calls) + a = t.x + a.append(5) + t.x = a + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 2, t.hook_calls) + t.x = [] + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [], t.new) + self.failUnless(t.hook_calls == 3, t.hook_calls) + # now append without reassigning. this doesn't trigger the + # change, since we don't ever set t.x, only get it and mess + # with it. It does, however, update our t.new, since t.new = + # t.x and is not a static copy. + t.x.append(5) + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 3, t.hook_calls) + # however, the next t.x get _will_ notice the change... + a = t.x + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 4, t.hook_calls) + t.x.append(6) # this append(6) is not noticed yet + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5,6], t.new) + self.failUnless(t.hook_calls == 4, t.hook_calls) + # this append(7) is not noticed, but the t.x get causes the + # append(6) to be noticed + t.x.append(7) + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [5,6,7], t.new) + self.failUnless(t.hook_calls == 5, t.hook_calls) + a = t.x # now the append(7) is noticed + self.failUnless(t.old == [5,6], t.old) + self.failUnless(t.new == [5,6,7], t.new) + self.failUnless(t.hook_calls == 6, t.hook_calls) + + suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) diff --git a/libbe/storage/settings_object.py b/libbe/storage/settings_object.py new file mode 100644 index 0000000..6a00ba9 --- /dev/null +++ b/libbe/storage/settings_object.py @@ -0,0 +1,433 @@ +# Bugs Everywhere - a distributed bugtracker +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +This module provides a base class implementing settings-dict based +property storage useful for BE objects with saved properties +(e.g. BugDir, Bug, Comment). For example usage, consider the +unittests at the end of the module. +""" + +import libbe +from properties import Property, doc_property, local_property, \ + defaulting_property, checked_property, fn_checked_property, \ + cached_property, primed_property, change_hook_property, \ + settings_property +if libbe.TESTING == True: + import doctest + import unittest + + +class _Token (object): + """ + `Control' value class for properties. We want values that only + mean something to the settings_object module. + """ + pass + +class UNPRIMED (_Token): + "Property has not been primed." + pass + +class EMPTY (_Token): + """ + Property has been primed but has no user-set value, so use + default/generator value. + """ + pass + + +def prop_save_settings(self, old, new): + """ + The default action undertaken when a property changes. + """ + if self.sync_with_disk==True: + self.save_settings() + +def prop_load_settings(self): + """ + The default action undertaken when an UNPRIMED property is accessed. + """ + if self.sync_with_disk==True and self._settings_loaded==False: + self.load_settings() + else: + self._setup_saved_settings(flag_as_loaded=False) + +# Some name-mangling routines for pretty printing setting names +def setting_name_to_attr_name(self, name): + """ + Convert keys to the .settings dict into their associated + SavedSettingsObject attribute names. + >>> print setting_name_to_attr_name(None,"User-id") + user_id + """ + return name.lower().replace('-', '_') + +def attr_name_to_setting_name(self, name): + """ + The inverse of setting_name_to_attr_name. + >>> print attr_name_to_setting_name(None, "user_id") + User-id + """ + return name.capitalize().replace('_', '-') + + +def versioned_property(name, doc, + default=None, generator=None, + change_hook=prop_save_settings, + mutable=False, + primer=prop_load_settings, + allowed=None, check_fn=None, + settings_properties=[], + required_saved_properties=[], + require_save=False): + """ + Combine the common decorators in a single function. + + Use zero or one (but not both) of default or generator, since a + working default will keep the generator from functioning. Use the + default if you know what you want the default value to be at + 'coding time'. Use the generator if you can write a function to + determine a valid default at run time. If both default and + generator are None, then the property will be a defaulting + property which defaults to None. + + allowed and check_fn have a similar relationship, although you can + use both of these if you want. allowed compares the proposed + value against a list determined at 'coding time' and check_fn + allows more flexible comparisons to take place at run time. + + Set require_save to True if you want to save the default/generated + value for a property, to protect against future changes. E.g., we + currently expect all comments to be 'text/plain' but in the future + we may want to default to 'text/html'. If we don't want the old + comments to be interpreted as 'text/html', we would require that + the content type be saved. + + change_hook, primer, settings_properties, and + required_saved_properties are only options to get their defaults + into our local scope. Don't mess with them. + + Set mutable=True if: + * default is a mutable + * your generator function may return mutables + * you set change_hook and might have mutable property values + See the docstrings in libbe.properties for details on how each of + these cases are handled. + """ + settings_properties.append(name) + if require_save == True: + required_saved_properties.append(name) + def decorator(funcs): + fulldoc = doc + if default != None or generator == None: + defaulting = defaulting_property(default=default, null=EMPTY, + mutable_default=mutable) + fulldoc += "\n\nThis property defaults to %s." % default + if generator != None: + cached = cached_property(generator=generator, initVal=EMPTY, + mutable=mutable) + fulldoc += "\n\nThis property is generated with %s." % generator + if check_fn != None: + fn_checked = fn_checked_property(value_allowed_fn=check_fn) + fulldoc += "\n\nThis property is checked with %s." % check_fn + if allowed != None: + checked = checked_property(allowed=allowed) + fulldoc += "\n\nThe allowed values for this property are: %s." \ + % (', '.join(allowed)) + hooked = change_hook_property(hook=change_hook, mutable=mutable, + default=EMPTY) + primed = primed_property(primer=primer, initVal=UNPRIMED) + settings = settings_property(name=name, null=UNPRIMED) + docp = doc_property(doc=fulldoc) + deco = hooked(primed(settings(docp(funcs)))) + if default != None or generator == None: + deco = defaulting(deco) + if generator != None: + deco = cached(deco) + if check_fn != None: + deco = fn_checked(deco) + if allowed != None: + deco = checked(deco) + return Property(deco) + return decorator + +class SavedSettingsObject(object): + + # Keep a list of properties that may be stored in the .settings dict. + #settings_properties = [] + + # A list of properties that we save to disk, even if they were + # never set (in which case we save the default value). This + # protects against future changes in default values. + #required_saved_properties = [] + + _setting_name_to_attr_name = setting_name_to_attr_name + _attr_name_to_setting_name = attr_name_to_setting_name + + def __init__(self): + self._settings_loaded = False + self.sync_with_disk = False + self.settings = {} + + def load_settings(self): + """Load the settings from disk.""" + # Override. Must call ._setup_saved_settings() after loading. + self.settings = {} + self._setup_saved_settings() + + def _setup_saved_settings(self, flag_as_loaded=True): + """ + To be run after setting self.settings up from disk. Marks all + settings as primed. + """ + for property in self.settings_properties: + if property not in self.settings: + self.settings[property] = EMPTY + elif self.settings[property] == UNPRIMED: + self.settings[property] = EMPTY + if flag_as_loaded == True: + self._settings_loaded = True + + def save_settings(self): + """Load the settings from disk.""" + # Override. Should save the dict output of ._get_saved_settings() + settings = self._get_saved_settings() + pass # write settings to disk.... + + def _get_saved_settings(self): + settings = {} + for k,v in self.settings.items(): + if v != None and v != EMPTY: + settings[k] = v + for k in self.required_saved_properties: + settings[k] = getattr(self, self._setting_name_to_attr_name(k)) + return settings + + def clear_cached_setting(self, setting=None): + "If setting=None, clear *all* cached settings" + if setting != None: + if hasattr(self, "_%s_cached_value" % setting): + delattr(self, "_%s_cached_value" % setting) + else: + for setting in settings_properties: + self.clear_cached_setting(setting) + + +if libbe.TESTING == True: + class SavedSettingsObjectTests(unittest.TestCase): + def testSimpleProperty(self): + """Testing a minimal versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + # access missing setting + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(len(t.settings) == 0, len(t.settings)) + self.failUnless(t.content_type == None, t.content_type) + # accessing t.content_type triggers the priming, which runs + # t._setup_saved_settings, which fills out t.settings with + # EMPTY data. t._settings_loaded is still false though, since + # the default priming does not do any of the `official' loading + # that occurs in t.load_settings. + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + # load settings creates an EMPTY value in the settings array + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + # now we set a value + t.content_type = 5 + self.failUnless(t.settings["Content-type"] == 5, + t.settings["Content-type"]) + self.failUnless(t.content_type == 5, t.content_type) + self.failUnless(t.settings["Content-type"] == 5, + t.settings["Content-type"]) + # now we set another value + t.content_type = "text/plain" + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == "text/plain", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + # now we clear to the post-primed value + t.content_type = EMPTY + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + def testDefaultingProperty(self): + """Testing a defaulting versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == {}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t.content_type == "text/html", + t.content_type) + self.failUnless(t.settings["Content-type"] == "text/html", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testRequiredDefaultingProperty(self): + """Testing a required defaulting versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties, + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testClassVersionedPropertyDefinition(self): + """Testing a class-specific _versioned property decorator""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + def _versioned_property(settings_properties= \ + settings_properties, + required_saved_properties= \ + required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"] = \ + required_saved_properties + return versioned_property(**kwargs) + @_versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testMutableChangeHookedProperty(self): + """Testing a mutable change-hooked property""" + SAVES = [] + def prop_log_save_settings(self, old, new, saves=SAVES): + saves.append("'%s' -> '%s'" % (str(old), str(new))) + prop_save_settings(self, old, new) + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="List-type", + doc="A test property", + mutable=True, + change_hook=prop_log_save_settings, + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def list_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._settings_loaded == False, t._settings_loaded) + t.load_settings() + self.failUnless(SAVES == [], SAVES) + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.list_type == None, t.list_type) + self.failUnless(SAVES == [], SAVES) + self.failUnless(t.settings["List-type"]==EMPTY, + t.settings["List-type"]) + t.list_type = [] + self.failUnless(t.settings["List-type"] == [], + t.settings["List-type"]) + self.failUnless(SAVES == [ + "'' -> '[]'" + ], SAVES) + t.list_type.append(5) + self.failUnless(SAVES == [ + "'' -> '[]'", + ], SAVES) + self.failUnless(t.settings["List-type"] == [5], + t.settings["List-type"]) + self.failUnless(SAVES == [ # the append(5) has not yet been saved + "'' -> '[]'", + ], SAVES) + self.failUnless(t.list_type == [5], t.list_type)#get triggers saved + + self.failUnless(SAVES == [ # now the append(5) has been saved. + "'' -> '[]'", + "'[]' -> '[5]'" + ], SAVES) + + unitsuite = unittest.TestLoader().loadTestsFromTestCase( \ + SavedSettingsObjectTests) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/storage/vcs/arch.py b/libbe/storage/vcs/arch.py new file mode 100644 index 0000000..45a3284 --- /dev/null +++ b/libbe/storage/vcs/arch.py @@ -0,0 +1,315 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Ben Finney +# Gianluca Montecchi +# James Rowe +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +GNU Arch (tla) backend. +""" + +import codecs +import os +import re +import shutil +import sys +import time + +import libbe +from beuuid import uuid_gen +import config +import vcs +if libbe.TESTING == True: + import unittest + import doctest + + +DEFAULT_CLIENT = "tla" + +client = config.get_val("arch_client", default=DEFAULT_CLIENT) + +def new(): + return Arch() + +class Arch(vcs.VCS): + name = "arch" + client = client + versioned = True + _archive_name = None + _archive_dir = None + _tmp_archive = False + _project_name = None + _tmp_project = False + _arch_paramdir = os.path.expanduser("~/.arch-params") + def _vcs_version(self): + status,output,error = self._u_invoke_client("--version") + return output + def _vcs_detect(self, path): + """Detect whether a directory is revision-controlled using Arch""" + if self._u_search_parent_directories(path, "{arch}") != None : + config.set_val("arch_client", client) + return True + return False + def _vcs_init(self, path): + self._create_archive(path) + self._create_project(path) + self._add_project_code(path) + def _create_archive(self, path): + """ + Create a temporary Arch archive in the directory PATH. This + archive will be removed by + cleanup->_vcs_cleanup->_remove_archive + """ + # http://regexps.srparish.net/tutorial-tla/new-archive.html#Creating_a_New_Archive + assert self._archive_name == None + id = self.get_user_id() + name, email = self._u_parse_id(id) + if email == None: + email = "%s@example.com" % name + trailer = "%s-%s" % ("bugs-everywhere-auto", uuid_gen()[0:8]) + self._archive_name = "%s--%s" % (email, trailer) + self._archive_dir = "/tmp/%s" % trailer + self._tmp_archive = True + self._u_invoke_client("make-archive", self._archive_name, + self._archive_dir, cwd=path) + def _invoke_client(self, *args, **kwargs): + """ + Invoke the client on our archive. + """ + assert self._archive_name != None + command = args[0] + if len(args) > 1: + tailargs = args[1:] + else: + tailargs = [] + arglist = [command, "-A", self._archive_name] + arglist.extend(tailargs) + args = tuple(arglist) + return self._u_invoke_client(*args, **kwargs) + def _remove_archive(self): + assert self._tmp_archive == True + assert self._archive_dir != None + assert self._archive_name != None + os.remove(os.path.join(self._arch_paramdir, + "=locations", self._archive_name)) + shutil.rmtree(self._archive_dir) + self._tmp_archive = False + self._archive_dir = False + self._archive_name = False + def _create_project(self, path): + """ + Create a temporary Arch project in the directory PATH. This + project will be removed by + cleanup->_vcs_cleanup->_remove_project + """ + # http://mwolson.org/projects/GettingStartedWithArch.html + # http://regexps.srparish.net/tutorial-tla/new-project.html#Starting_a_New_Project + category = "bugs-everywhere" + branch = "mainline" + version = "0.1" + self._project_name = "%s--%s--%s" % (category, branch, version) + self._invoke_client("archive-setup", self._project_name, + cwd=path) + self._tmp_project = True + def _remove_project(self): + assert self._tmp_project == True + assert self._project_name != None + assert self._archive_dir != None + shutil.rmtree(os.path.join(self._archive_dir, self._project_name)) + self._tmp_project = False + self._project_name = False + def _archive_project_name(self): + assert self._archive_name != None + assert self._project_name != None + return "%s/%s" % (self._archive_name, self._project_name) + def _adjust_naming_conventions(self, path): + """ + By default, Arch restricts source code filenames to + ^[_=a-zA-Z0-9].*$ + See + http://regexps.srparish.net/tutorial-tla/naming-conventions.html + Since our bug directory '.be' doesn't satisfy these conventions, + we need to adjust them. + + The conventions are specified in + project-root/{arch}/=tagging-method + """ + tagpath = os.path.join(path, "{arch}", "=tagging-method") + lines_out = [] + f = codecs.open(tagpath, "r", self.encoding) + for line in f: + if line.startswith("source "): + lines_out.append("source ^[._=a-zA-X0-9].*$\n") + else: + lines_out.append(line) + f.close() + f = codecs.open(tagpath, "w", self.encoding) + f.write("".join(lines_out)) + f.close() + + def _add_project_code(self, path): + # http://mwolson.org/projects/GettingStartedWithArch.html + # http://regexps.srparish.net/tutorial-tla/new-source.html + # http://regexps.srparish.net/tutorial-tla/importing-first.html + self._invoke_client("init-tree", self._project_name, + cwd=path) + self._adjust_naming_conventions(path) + self._invoke_client("import", "--summary", "Began versioning", + cwd=path) + def _vcs_cleanup(self): + if self._tmp_project == True: + self._remove_project() + if self._tmp_archive == True: + self._remove_archive() + + def _vcs_root(self, path): + if not os.path.isdir(path): + dirname = os.path.dirname(path) + else: + dirname = path + status,output,error = self._u_invoke_client("tree-root", dirname) + root = output.rstrip('\n') + + self._get_archive_project_name(root) + + return root + def _get_archive_name(self, root): + status,output,error = self._u_invoke_client("archives") + lines = output.split('\n') + # e.g. output: + # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52 + # /tmp/BEtestXXXXXX/rootdir + # (+ repeats) + for archive,location in zip(lines[::2], lines[1::2]): + if os.path.realpath(location) == os.path.realpath(root): + self._archive_name = archive + assert self._archive_name != None + def _get_archive_project_name(self, root): + # get project names + status,output,error = self._u_invoke_client("tree-version", cwd=root) + # e.g output + # jdoe@example.com--bugs-everywhere-auto-2008.22.24.52/be--mainline--0.1 + archive_name,project_name = output.rstrip('\n').split('/') + self._archive_name = archive_name + self._project_name = project_name + def _vcs_get_user_id(self): + try: + status,output,error = self._u_invoke_client('my-id') + return output.rstrip('\n') + except Exception, e: + if 'no arch user id set' in e.args[0]: + return None + else: + raise + def _vcs_set_user_id(self, value): + self._u_invoke_client('my-id', value) + def _vcs_add(self, path): + self._u_invoke_client("add-id", path) + realpath = os.path.realpath(self._u_abspath(path)) + pathAdded = realpath in self._list_added(self.rootdir) + if self.paranoid and not pathAdded: + self._force_source(path) + def _list_added(self, root): + assert os.path.exists(root) + assert os.access(root, os.X_OK) + root = os.path.realpath(root) + status,output,error = self._u_invoke_client("inventory", "--source", + "--both", "--all", root) + inv_str = output.rstrip('\n') + return [os.path.join(root, p) for p in inv_str.split('\n')] + def _add_dir_rule(self, rule, dirname, root): + inv_path = os.path.join(dirname, '.arch-inventory') + f = codecs.open(inv_path, "a", self.encoding) + f.write(rule) + f.close() + if os.path.realpath(inv_path) not in self._list_added(root): + paranoid = self.paranoid + self.paranoid = False + self.add(inv_path) + self.paranoid = paranoid + def _force_source(self, path): + rule = "source %s\n" % self._u_rel_path(path) + self._add_dir_rule(rule, os.path.dirname(path), self.rootdir) + if os.path.realpath(path) not in self._list_added(self.rootdir): + raise CantAddFile(path) + def _vcs_remove(self, path): + if not '.arch-ids' in path: + self._u_invoke_client("delete-id", path) + def _vcs_update(self, path): + pass + def _vcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) + else: + status,output,error = \ + self._invoke_client("file-find", path, revision) + relpath = output.rstrip('\n') + abspath = os.path.join(self.rootdir, relpath) + f = codecs.open(abspath, "r", self.encoding) + contents = f.read() + f.close() + return contents + def _vcs_duplicate_repo(self, directory, revision=None): + if revision == None: + vcs.VCS._vcs_duplicate_repo(self, directory, revision) + else: + status,output,error = \ + self._u_invoke_client("get", revision, directory) + def _vcs_commit(self, commitfile, allow_empty=False): + if allow_empty == False: + # arch applies empty commits without complaining, so check first + status,output,error = self._u_invoke_client("changes",expect=(0,1)) + if status == 0: + raise vcs.EmptyCommit() + summary,body = self._u_parse_commitfile(commitfile) + args = ["commit", "--summary", summary] + if body != None: + args.extend(["--log-message",body]) + status,output,error = self._u_invoke_client(*args) + revision = None + revline = re.compile("[*] committed (.*)") + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 1 + revpath = match.groups()[0] + assert not " " in revpath, revpath + assert revpath.startswith(self._archive_project_name()+'--') + revision = revpath[len(self._archive_project_name()+'--'):] + return revpath + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("logs") + logs = output.splitlines() + first_log = logs.pop(0) + assert first_log == "base-0", first_log + try: + log = logs[index] + except IndexError: + return None + return "%s--%s" % (self._archive_project_name(), log) + +class CantAddFile(Exception): + def __init__(self, file): + self.file = file + Exception.__init__(self, "Can't automatically add file %s" % file) + + + +if libbe.TESTING == True: + vcs.make_vcs_testcase_subclasses(Arch, sys.modules[__name__]) + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py new file mode 100644 index 0000000..44643a4 --- /dev/null +++ b/libbe/storage/vcs/base.py @@ -0,0 +1,941 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Alexander Belchenko +# Ben Finney +# Chris Ball +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Define the base VCS (Version Control System) class, which should be +subclassed by other Version Control System backends. The base class +implements a "do not version" VCS. +""" + +import codecs +import os +import os.path +import re +from socket import gethostname +import shutil +import sys +import tempfile + +import libbe +from utility import Dir, search_parent_directories +from subproc import CommandError, invoke +from plugin import get_plugin + +if libbe.TESTING == True: + import unittest + import doctest + + +# List VCS modules in order of preference. +# Don't list this module, it is implicitly last. +VCS_ORDER = ['arch', 'bzr', 'darcs', 'git', 'hg'] + +def set_preferred_vcs(name): + global VCS_ORDER + assert name in VCS_ORDER, \ + 'unrecognized VCS %s not in\n %s' % (name, VCS_ORDER) + VCS_ORDER.remove(name) + VCS_ORDER.insert(0, name) + +def _get_matching_vcs(matchfn): + """Return the first module for which matchfn(VCS_instance) is true""" + for submodname in VCS_ORDER: + module = get_plugin('libbe', submodname) + vcs = module.new() + if matchfn(vcs) == True: + return vcs + vcs.cleanup() + return VCS() + +def vcs_by_name(vcs_name): + """Return the module for the VCS with the given name""" + return _get_matching_vcs(lambda vcs: vcs.name == vcs_name) + +def detect_vcs(dir): + """Return an VCS instance for the vcs being used in this directory""" + return _get_matching_vcs(lambda vcs: vcs.detect(dir)) + +def installed_vcs(): + """Return an instance of an installed VCS""" + return _get_matching_vcs(lambda vcs: vcs.installed()) + + + +class SettingIDnotSupported(NotImplementedError): + pass + +class VCSnotRooted(Exception): + def __init__(self): + msg = "VCS not rooted" + Exception.__init__(self, msg) + +class PathNotInRoot(Exception): + def __init__(self, path, root): + msg = "Path '%s' not in root '%s'" % (path, root) + Exception.__init__(self, msg) + self.path = path + self.root = root + +class NoSuchFile(Exception): + def __init__(self, pathname, root="."): + path = os.path.abspath(os.path.join(root, pathname)) + Exception.__init__(self, "No such file: %s" % path) + +class EmptyCommit(Exception): + def __init__(self): + Exception.__init__(self, "No changes to commit") + + +def new(): + return VCS() + +class VCS(object): + """ + This class implements a 'no-vcs' interface. + + Support for other VCSs can be added by subclassing this class, and + overriding methods _vcs_*() with code appropriate for your VCS. + + The methods _u_*() are utility methods available to the _vcs_*() + methods. + """ + name = "None" + client = "" # command-line tool for _u_invoke_client + versioned = False + def __init__(self, paranoid=False, encoding=sys.getdefaultencoding()): + self.paranoid = paranoid + self.verboseInvoke = False + self.rootdir = None + self._duplicateBasedir = None + self._duplicateDirname = None + self.encoding = encoding + def __str__(self): + return "<%s %s>" % (self.__class__.__name__, id(self)) + def __repr__(self): + return str(self) + def _vcs_version(self): + """ + Return the VCS version string. + """ + return "0.0" + def _vcs_detect(self, path=None): + """ + Detect whether a directory is revision controlled with this VCS. + """ + return True + def _vcs_root(self, path): + """ + Get the VCS root. This is the default working directory for + future invocations. You would normally set this to the root + directory for your VCS. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + if path == "": + path = os.path.abspath(".") + return path + def _vcs_init(self, path): + """ + Begin versioning the tree based at path. + """ + pass + def _vcs_cleanup(self): + """ + Remove any cruft that _vcs_init() created outside of the + versioned tree. + """ + pass + def _vcs_get_user_id(self): + """ + Get the VCS's suggested user id (e.g. "John Doe "). + If the VCS has not been configured with a username, return None. + """ + return None + def _vcs_set_user_id(self, value): + """ + Set the VCS's suggested user id (e.g "John Doe "). + This is run if the VCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + raise SettingIDnotSupported + def _vcs_add(self, path): + """ + Add the already created file at path to version control. + """ + pass + def _vcs_remove(self, path): + """ + Remove the file at path from version control. Optionally + remove the file from the filesystem as well. + """ + pass + def _vcs_update(self, path): + """ + Notify the versioning system of changes to the versioned file + at path. + """ + pass + def _vcs_get_file_contents(self, path, revision=None, binary=False): + """ + Get the file contents as they were in a given revision. + Revision==None specifies the current revision. + """ + assert revision == None, \ + "The %s VCS does not support revision specifiers" % self.name + if binary == False: + f = codecs.open(os.path.join(self.rootdir, path), "r", self.encoding) + else: + f = open(os.path.join(self.rootdir, path), "rb") + contents = f.read() + f.close() + return contents + def _vcs_duplicate_repo(self, directory, revision=None): + """ + Get the repository as it was in a given revision. + revision==None specifies the current revision. + dir specifies a directory to create the duplicate in. + """ + shutil.copytree(self.rootdir, directory, True) + def _vcs_commit(self, commitfile, allow_empty=False): + """ + Commit the current working directory, using the contents of + commitfile as the comment. Return the name of the old + revision (or None if commits are not supported). + + If allow_empty == False, raise EmptyCommit if there are no + changes to commit. + """ + return None + def _vcs_revision_id(self, index): + """ + Return the name of the th revision. Index will be an + integer (possibly <= 0). The choice of which branch to follow + when crossing branches/merges is not defined. + + Return None if revision IDs are not supported, or if the + specified revision does not exist. + """ + return None + def version(self): + """Cache version string for efficiency.""" + if not hasattr(self, '_version'): + self._version = self._get_version() + return self._version + def _get_version(self): + try: + ret = self._vcs_version() + return ret + except OSError, e: + if e.errno == errno.ENOENT: + return None + else: + raise OSError, e + except CommandError: + return None + def installed(self): + if self.version() != None: + return True + return False + def detect(self, path="."): + """ + Detect whether a directory is revision controlled with this VCS. + """ + return self._vcs_detect(path) + def root(self, path): + """ + Set the root directory to the path's VCS root. This is the + default working directory for future invocations. + """ + self.rootdir = self._vcs_root(path) + def init(self, path): + """ + Begin versioning the tree based at path. + Also roots the vcs at path. + """ + if os.path.isdir(path)==False: + path = os.path.dirname(path) + self._vcs_init(path) + self.root(path) + def cleanup(self): + self._vcs_cleanup() + def get_user_id(self): + """ + Get the VCS's suggested user id (e.g. "John Doe "). + If the VCS has not been configured with a username, return the user's + id. You can override the automatic lookup procedure by setting the + VCS.user_id attribute to a string of your choice. + """ + if hasattr(self, "user_id"): + if self.user_id != None: + return self.user_id + id = self._vcs_get_user_id() + if id == None: + name = self._u_get_fallback_username() + email = self._u_get_fallback_email() + id = self._u_create_id(name, email) + print >> sys.stderr, "Guessing id '%s'" % id + try: + self.set_user_id(id) + except SettingIDnotSupported: + pass + return id + def set_user_id(self, value): + """ + Set the VCS's suggested user id (e.g "John Doe "). + This is run if the VCS has not been configured with a usename, so + that commits will have a reasonable FROM value. + """ + self._vcs_set_user_id(value) + def add(self, path): + """ + Add the already created file at path to version control. + """ + self._vcs_add(self._u_rel_path(path)) + def remove(self, path): + """ + Remove a file from both version control and the filesystem. + """ + self._vcs_remove(self._u_rel_path(path)) + if os.path.exists(path): + os.remove(path) + def recursive_remove(self, dirname): + """ + Remove a file/directory and all its decendents from both + version control and the filesystem. + """ + if not os.path.exists(dirname): + raise NoSuchFile(dirname) + for dirpath,dirnames,filenames in os.walk(dirname, topdown=False): + filenames.extend(dirnames) + for path in filenames: + fullpath = os.path.join(dirpath, path) + if os.path.exists(fullpath) == False: + continue + self._vcs_remove(self._u_rel_path(fullpath)) + if os.path.exists(dirname): + shutil.rmtree(dirname) + def update(self, path): + """ + Notify the versioning system of changes to the versioned file + at path. + """ + self._vcs_update(self._u_rel_path(path)) + def get_file_contents(self, path, revision=None, allow_no_vcs=False, binary=False): + """ + Get the file as it was in a given revision. + Revision==None specifies the current revision. + + allow_no_vcs==True allows direct access to files through + codecs.open() or open() if the vcs decides it can't handle the + given path. + """ + if not os.path.exists(path): + raise NoSuchFile(path) + if self._use_vcs(path, allow_no_vcs): + relpath = self._u_rel_path(path) + contents = self._vcs_get_file_contents(relpath,revision,binary=binary) + else: + if binary == True: + f = codecs.open(path, "r", self.encoding) + else: + f = open(path, "rb") + contents = f.read() + f.close() + return contents + def set_file_contents(self, path, contents, allow_no_vcs=False, binary=False): + """ + Set the file contents under version control. + """ + add = not os.path.exists(path) + if binary == False: + f = codecs.open(path, "w", self.encoding) + else: + f = open(path, "wb") + f.write(contents) + f.close() + + if self._use_vcs(path, allow_no_vcs): + if add: + self.add(path) + else: + self.update(path) + def mkdir(self, path, allow_no_vcs=False, check_parents=True): + """ + Create (if neccessary) a directory at path under version + control. + """ + if check_parents == True: + parent = os.path.dirname(path) + if not os.path.exists(parent): # recurse through parents + self.mkdir(parent, allow_no_vcs, check_parents) + if not os.path.exists(path): + os.mkdir(path) + if self._use_vcs(path, allow_no_vcs): + self.add(path) + else: + assert os.path.isdir(path) + if self._use_vcs(path, allow_no_vcs): + #self.update(path)# Don't update directories. Changing files + pass # underneath them should be sufficient. + + def duplicate_repo(self, revision=None): + """ + Get the repository as it was in a given revision. + revision==None specifies the current revision. + Return the path to the arbitrary directory at the base of the new repo. + """ + # Dirname in Basedir to protect against simlink attacks. + if self._duplicateBasedir == None: + self._duplicateBasedir = tempfile.mkdtemp(prefix='BEvcs') + self._duplicateDirname = \ + os.path.join(self._duplicateBasedir, "duplicate") + self._vcs_duplicate_repo(directory=self._duplicateDirname, + revision=revision) + return self._duplicateDirname + def remove_duplicate_repo(self): + """ + Clean up a duplicate repo created with duplicate_repo(). + """ + if self._duplicateBasedir != None: + shutil.rmtree(self._duplicateBasedir) + self._duplicateBasedir = None + self._duplicateDirname = None + def commit(self, summary, body=None, allow_empty=False): + """ + Commit the current working directory, with a commit message + string summary and body. Return the name of the old revision + (or None if versioning is not supported). + + If allow_empty == False (the default), raise EmptyCommit if + there are no changes to commit. + """ + summary = summary.strip()+'\n' + if body is not None: + summary += '\n' + body.strip() + '\n' + descriptor, filename = tempfile.mkstemp() + revision = None + try: + temp_file = os.fdopen(descriptor, 'wb') + temp_file.write(summary) + temp_file.flush() + self.precommit() + revision = self._vcs_commit(filename, allow_empty=allow_empty) + temp_file.close() + self.postcommit() + finally: + os.remove(filename) + return revision + def precommit(self): + """ + Executed before all attempted commits. + """ + pass + def postcommit(self): + """ + Only executed after successful commits. + """ + pass + def revision_id(self, index=None): + """ + Return the name of the th revision. The choice of + which branch to follow when crossing branches/merges is not + defined. + + Return None if index==None, revision IDs are not supported, or + if the specified revision does not exist. + """ + if index == None: + return None + return self._vcs_revision_id(index) + def _u_any_in_string(self, list, string): + """ + Return True if any of the strings in list are in string. + Otherwise return False. + """ + for list_string in list: + if list_string in string: + return True + return False + def _u_invoke(self, *args, **kwargs): + if 'cwd' not in kwargs: + kwargs['cwd'] = self.rootdir + if 'verbose' not in kwargs: + kwargs['verbose'] = self.verboseInvoke + if 'encoding' not in kwargs: + kwargs['encoding'] = self.encoding + return invoke(*args, **kwargs) + def _u_invoke_client(self, *args, **kwargs): + cl_args = [self.client] + cl_args.extend(args) + return self._u_invoke(cl_args, **kwargs) + def _u_search_parent_directories(self, path, filename): + """ + Find the file (or directory) named filename in path or in any + of path's parents. + + e.g. + search_parent_directories("/a/b/c", ".be") + will return the path to the first existing file from + /a/b/c/.be + /a/b/.be + /a/.be + /.be + or None if none of those files exist. + """ + return search_parent_directories(path, filename) + def _use_vcs(self, path, allow_no_vcs): + """ + Try and decide if _vcs_add/update/mkdir/etc calls will + succeed. Returns True is we think the vcs_call would + succeeed, and False otherwise. + """ + use_vcs = True + exception = None + if self.rootdir != None: + if self.path_in_root(path) == False: + use_vcs = False + exception = PathNotInRoot(path, self.rootdir) + else: + use_vcs = False + exception = VCSnotRooted + if use_vcs == False and allow_no_vcs==False: + raise exception + return use_vcs + def path_in_root(self, path, root=None): + """ + Return the relative path to path from root. + >>> vcs = new() + >>> vcs.path_in_root("/a.b/c/.be", "/a.b/c") + True + >>> vcs.path_in_root("/a.b/.be", "/a.b/c") + False + """ + if root == None: + if self.rootdir == None: + raise VCSnotRooted + root = self.rootdir + path = os.path.abspath(path) + absRoot = os.path.abspath(root) + absRootSlashedDir = os.path.join(absRoot,"") + if not path.startswith(absRootSlashedDir): + return False + return True + def _u_rel_path(self, path, root=None): + """ + Return the relative path to path from root. + >>> vcs = new() + >>> vcs._u_rel_path("/a.b/c/.be", "/a.b/c") + '.be' + """ + if root == None: + if self.rootdir == None: + raise VCSnotRooted + root = self.rootdir + path = os.path.abspath(path) + absRoot = os.path.abspath(root) + absRootSlashedDir = os.path.join(absRoot,"") + if not path.startswith(absRootSlashedDir): + raise PathNotInRoot(path, absRootSlashedDir) + assert path != absRootSlashedDir, \ + "file %s == root directory %s" % (path, absRootSlashedDir) + relpath = path[len(absRootSlashedDir):] + return relpath + def _u_abspath(self, path, root=None): + """ + Return the absolute path from a path realtive to root. + >>> vcs = new() + >>> vcs._u_abspath(".be", "/a.b/c") + '/a.b/c/.be' + """ + if root == None: + assert self.rootdir != None, "VCS not rooted" + root = self.rootdir + return os.path.abspath(os.path.join(root, path)) + def _u_create_id(self, name, email=None): + """ + >>> vcs = new() + >>> vcs._u_create_id("John Doe", "jdoe@example.com") + 'John Doe ' + >>> vcs._u_create_id("John Doe") + 'John Doe' + """ + assert len(name) > 0 + if email == None or len(email) == 0: + return name + else: + return "%s <%s>" % (name, email) + def _u_parse_id(self, value): + """ + >>> vcs = new() + >>> vcs._u_parse_id("John Doe ") + ('John Doe', 'jdoe@example.com') + >>> vcs._u_parse_id("John Doe") + ('John Doe', None) + >>> try: + ... vcs._u_parse_id("John Doe ") + ... except AssertionError: + ... print "Invalid match" + Invalid match + """ + emailexp = re.compile("(.*) <([^>]*)>(.*)") + match = emailexp.search(value) + if match == None: + email = None + name = value + else: + assert len(match.groups()) == 3 + assert match.groups()[2] == "", match.groups() + email = match.groups()[1] + name = match.groups()[0] + assert name != None + assert len(name) > 0 + return (name, email) + def _u_get_fallback_username(self): + name = None + for envariable in ["LOGNAME", "USERNAME"]: + if os.environ.has_key(envariable): + name = os.environ[envariable] + break + assert name != None + return name + def _u_get_fallback_email(self): + hostname = gethostname() + name = self._u_get_fallback_username() + return "%s@%s" % (name, hostname) + def _u_parse_commitfile(self, commitfile): + """ + Split the commitfile created in self.commit() back into + summary and header lines. + """ + f = codecs.open(commitfile, "r", self.encoding) + summary = f.readline() + body = f.read() + body.lstrip('\n') + if len(body) == 0: + body = None + f.close() + return (summary, body) + + +if libbe.TESTING == True: + def setup_vcs_test_fixtures(testcase): + """Set up test fixtures for VCS test case.""" + testcase.vcs = testcase.Class() + testcase.dir = Dir() + testcase.dirname = testcase.dir.path + + vcs_not_supporting_uninitialized_user_id = [] + vcs_not_supporting_set_user_id = ["None", "hg"] + testcase.vcs_supports_uninitialized_user_id = ( + testcase.vcs.name not in vcs_not_supporting_uninitialized_user_id) + testcase.vcs_supports_set_user_id = ( + testcase.vcs.name not in vcs_not_supporting_set_user_id) + + if not testcase.vcs.installed(): + testcase.fail( + "%(name)s VCS not found" % vars(testcase.Class)) + + if testcase.Class.name != "None": + testcase.failIf( + testcase.vcs.detect(testcase.dirname), + "Detected %(name)s VCS before initialising" + % vars(testcase.Class)) + + testcase.vcs.init(testcase.dirname) + + class VCSTestCase(unittest.TestCase): + """Test cases for base VCS class.""" + + Class = VCS + + def __init__(self, *args, **kwargs): + super(VCSTestCase, self).__init__(*args, **kwargs) + self.dirname = None + + def setUp(self): + super(VCSTestCase, self).setUp() + setup_vcs_test_fixtures(self) + + def tearDown(self): + self.vcs.cleanup() + self.dir.cleanup() + super(VCSTestCase, self).tearDown() + + def full_path(self, rel_path): + return os.path.join(self.dirname, rel_path) + + + class VCS_init_TestCase(VCSTestCase): + """Test cases for VCS.init method.""" + + def test_detect_should_succeed_after_init(self): + """Should detect VCS in directory after initialization.""" + self.failUnless( + self.vcs.detect(self.dirname), + "Did not detect %(name)s VCS after initialising" + % vars(self.Class)) + + def test_vcs_rootdir_in_specified_root_path(self): + """VCS root directory should be in specified root path.""" + rp = os.path.realpath(self.vcs.rootdir) + dp = os.path.realpath(self.dirname) + vcs_name = self.Class.name + self.failUnless( + dp == rp or rp == None, + "%(vcs_name)s VCS root in wrong dir (%(dp)s %(rp)s)" % vars()) + + + class VCS_get_user_id_TestCase(VCSTestCase): + """Test cases for VCS.get_user_id method.""" + + def test_gets_existing_user_id(self): + """Should get the existing user ID.""" + if not self.vcs_supports_uninitialized_user_id: + return + + user_id = self.vcs.get_user_id() + self.failUnless( + user_id is not None, + "unable to get a user id") + + + class VCS_set_user_id_TestCase(VCSTestCase): + """Test cases for VCS.set_user_id method.""" + + def setUp(self): + super(VCS_set_user_id_TestCase, self).setUp() + + if self.vcs_supports_uninitialized_user_id: + self.prev_user_id = self.vcs.get_user_id() + else: + self.prev_user_id = "Uninitialized identity " + + if self.vcs_supports_set_user_id: + self.test_new_user_id = "John Doe " + self.vcs.set_user_id(self.test_new_user_id) + + def tearDown(self): + if self.vcs_supports_set_user_id: + self.vcs.set_user_id(self.prev_user_id) + super(VCS_set_user_id_TestCase, self).tearDown() + + def test_raises_error_in_unsupported_vcs(self): + """Should raise an error in a VCS that doesn't support it.""" + if self.vcs_supports_set_user_id: + return + self.assertRaises( + SettingIDnotSupported, + self.vcs.set_user_id, "foo") + + def test_updates_user_id_in_supporting_vcs(self): + """Should update the user ID in an VCS that supports it.""" + if not self.vcs_supports_set_user_id: + return + user_id = self.vcs.get_user_id() + self.failUnlessEqual( + self.test_new_user_id, user_id, + "user id not set correctly (expected %s, got %s)" + % (self.test_new_user_id, user_id)) + + + def setup_vcs_revision_test_fixtures(testcase): + """Set up revision test fixtures for VCS test case.""" + testcase.test_dirs = ['a', 'a/b', 'c'] + for path in testcase.test_dirs: + testcase.vcs.mkdir(testcase.full_path(path)) + + testcase.test_files = ['a/text', 'a/b/text'] + + testcase.test_contents = { + 'rev_1': "Lorem ipsum", + 'uncommitted': "dolor sit amet", + } + + + class VCS_mkdir_TestCase(VCSTestCase): + """Test cases for VCS.mkdir method.""" + + def setUp(self): + super(VCS_mkdir_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_mkdir_TestCase, self).tearDown() + + def test_mkdir_creates_directory(self): + """Should create specified directory in filesystem.""" + for path in self.test_dirs: + full_path = self.full_path(path) + self.failUnless( + os.path.exists(full_path), + "path %(full_path)s does not exist" % vars()) + + + class VCS_commit_TestCase(VCSTestCase): + """Test cases for VCS.commit method.""" + + def setUp(self): + super(VCS_commit_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_commit_TestCase, self).tearDown() + + def test_file_contents_as_specified(self): + """Should set file contents as specified.""" + test_contents = self.test_contents['rev_1'] + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents(full_path, test_contents) + current_contents = self.vcs.get_file_contents(full_path) + self.failUnlessEqual(test_contents, current_contents) + + def test_file_contents_as_committed(self): + """Should have file contents as specified after commit.""" + test_contents = self.test_contents['rev_1'] + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents(full_path, test_contents) + revision = self.vcs.commit("Initial file contents.") + current_contents = self.vcs.get_file_contents(full_path) + self.failUnlessEqual(test_contents, current_contents) + + def test_file_contents_as_set_when_uncommitted(self): + """Should set file contents as specified after commit.""" + if not self.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial file contents.") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + current_contents = self.vcs.get_file_contents(full_path) + self.failUnlessEqual( + self.test_contents['uncommitted'], current_contents) + + def test_revision_file_contents_as_committed(self): + """Should get file contents as committed to specified revision.""" + if not self.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial file contents.") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + committed_contents = self.vcs.get_file_contents( + full_path, revision) + self.failUnlessEqual( + self.test_contents['rev_1'], committed_contents) + + def test_revision_id_as_committed(self): + """Check for compatibility between .commit() and .revision_id()""" + if not self.vcs.versioned: + self.failUnlessEqual(self.vcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Initial %s contents." % path) + committed_revisions.append(revision) + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + revision = self.vcs.commit("Altered %s contents." % path) + committed_revisions.append(revision) + for i,revision in enumerate(committed_revisions): + self.failUnlessEqual(self.vcs.revision_id(i), revision) + i += -len(committed_revisions) # check negative indices + self.failUnlessEqual(self.vcs.revision_id(i), revision) + i = len(committed_revisions) + self.failUnlessEqual(self.vcs.revision_id(i), None) + self.failUnlessEqual(self.vcs.revision_id(-i-1), None) + + def test_revision_id_as_committed(self): + """Check revision id before first commit""" + if not self.vcs.versioned: + self.failUnlessEqual(self.vcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + self.failUnlessEqual(self.vcs.revision_id(0), None) + + + class VCS_duplicate_repo_TestCase(VCSTestCase): + """Test cases for VCS.duplicate_repo method.""" + + def setUp(self): + super(VCS_duplicate_repo_TestCase, self).setUp() + setup_vcs_revision_test_fixtures(self) + + def tearDown(self): + self.vcs.remove_duplicate_repo() + for path in reversed(sorted(self.test_dirs)): + self.vcs.recursive_remove(self.full_path(path)) + super(VCS_duplicate_repo_TestCase, self).tearDown() + + def test_revision_file_contents_as_committed(self): + """Should match file contents as committed to specified revision. + """ + if not self.vcs.versioned: + return + for path in self.test_files: + full_path = self.full_path(path) + self.vcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.vcs.commit("Commit current status") + self.vcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + dup_repo_path = self.vcs.duplicate_repo(revision) + dup_file_path = os.path.join(dup_repo_path, path) + dup_file_contents = file(dup_file_path, 'rb').read() + self.failUnlessEqual( + self.test_contents['rev_1'], dup_file_contents) + self.vcs.remove_duplicate_repo() + + + def make_vcs_testcase_subclasses(vcs_class, namespace): + """Make VCSTestCase subclasses for vcs_class in the namespace.""" + vcs_testcase_classes = [ + c for c in ( + ob for ob in globals().values() if isinstance(ob, type)) + if issubclass(c, VCSTestCase)] + + for base_class in vcs_testcase_classes: + testcase_class_name = vcs_class.__name__ + base_class.__name__ + testcase_class_bases = (base_class,) + testcase_class_dict = dict(base_class.__dict__) + testcase_class_dict['Class'] = vcs_class + testcase_class = type( + testcase_class_name, testcase_class_bases, testcase_class_dict) + setattr(namespace, testcase_class_name, testcase_class) + + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/storage/vcs/bzr.py b/libbe/storage/vcs/bzr.py new file mode 100644 index 0000000..62a9b11 --- /dev/null +++ b/libbe/storage/vcs/bzr.py @@ -0,0 +1,117 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Ben Finney +# Gianluca Montecchi +# Marien Zwart +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Bazaar (bzr) backend. +""" + +import os +import re +import sys +import unittest + +import libbe +import vcs +if libbe.TESTING == True: + import doctest + + +def new(): + return Bzr() + +class Bzr(vcs.VCS): + name = "bzr" + client = "bzr" + versioned = True + def _vcs_version(self): + status,output,error = self._u_invoke_client("--version") + return output + def _vcs_detect(self, path): + if self._u_search_parent_directories(path, ".bzr") != None : + return True + return False + def _vcs_root(self, path): + """Find the root of the deepest repository containing path.""" + status,output,error = self._u_invoke_client("root", path) + return output.rstrip('\n') + def _vcs_init(self, path): + self._u_invoke_client("init", cwd=path) + def _vcs_get_user_id(self): + status,output,error = self._u_invoke_client("whoami") + return output.rstrip('\n') + def _vcs_set_user_id(self, value): + self._u_invoke_client("whoami", value) + def _vcs_add(self, path): + self._u_invoke_client("add", path) + def _vcs_remove(self, path): + # --force to also remove unversioned files. + self._u_invoke_client("remove", "--force", path) + def _vcs_update(self, path): + pass + def _vcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) + else: + status,output,error = \ + self._u_invoke_client("cat","-r",revision,path) + return output + def _vcs_duplicate_repo(self, directory, revision=None): + if revision == None: + vcs.VCS._vcs_duplicate_repo(self, directory, revision) + else: + self._u_invoke_client("branch", "--revision", revision, + ".", directory) + def _vcs_commit(self, commitfile, allow_empty=False): + args = ["commit", "--file", commitfile] + if allow_empty == True: + args.append("--unchanged") + status,output,error = self._u_invoke_client(*args) + else: + kwargs = {"expect":(0,3)} + status,output,error = self._u_invoke_client(*args, **kwargs) + if status != 0: + strings = ["ERROR: no changes to commit.", # bzr 1.3.1 + "ERROR: No changes to commit."] # bzr 1.15.1 + if self._u_any_in_string(strings, error) == True: + raise vcs.EmptyCommit() + else: + raise vcs.CommandError(args, status, stderr=error) + revision = None + revline = re.compile("Committed revision (.*)[.]") + match = revline.search(error) + assert match != None, output+error + assert len(match.groups()) == 1 + revision = match.groups()[0] + return revision + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("revno") + current_revision = int(output) + if index >= current_revision or index < -current_revision: + return None + if index >= 0: + return str(index+1) # bzr commit 0 is the empty tree. + return str(current_revision+index+1) + + +if libbe.TESTING == True: + vcs.make_vcs_testcase_subclasses(Bzr, sys.modules[__name__]) + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/storage/vcs/darcs.py b/libbe/storage/vcs/darcs.py new file mode 100644 index 0000000..d94eaef --- /dev/null +++ b/libbe/storage/vcs/darcs.py @@ -0,0 +1,192 @@ +# Copyright (C) 2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Darcs backend. +""" + +import codecs +import os +import re +import sys +try: # import core module, Python >= 2.5 + from xml.etree import ElementTree +except ImportError: # look for non-core module + from elementtree import ElementTree +from xml.sax.saxutils import unescape + +import libbe +import vcs +if libbe.TESTING == True: + import doctest + import unittest + + +def new(): + return Darcs() + +class Darcs(vcs.VCS): + name="darcs" + client="darcs" + versioned=True + def _vcs_version(self): + status,output,error = self._u_invoke_client("--version") + num_part = output.split(" ")[0] + self.parsed_version = [int(i) for i in num_part.split(".")] + return output + def _vcs_detect(self, path): + if self._u_search_parent_directories(path, "_darcs") != None : + return True + return False + def _vcs_root(self, path): + """Find the root of the deepest repository containing path.""" + # Assume that nothing funny is going on; in particular, that we aren't + # dealing with a bare repo. + if os.path.isdir(path) != True: + path = os.path.dirname(path) + darcs_dir = self._u_search_parent_directories(path, "_darcs") + if darcs_dir == None: + return None + return os.path.dirname(darcs_dir) + def _vcs_init(self, path): + self._u_invoke_client("init", cwd=path) + def _vcs_get_user_id(self): + # following http://darcs.net/manual/node4.html#SECTION00410030000000000000 + # as of June 29th, 2009 + if self.rootdir == None: + return None + darcs_dir = os.path.join(self.rootdir, "_darcs") + if darcs_dir != None: + for pref_file in ["author", "email"]: + pref_path = os.path.join(darcs_dir, "prefs", pref_file) + if os.path.exists(pref_path): + return self.get_file_contents(pref_path) + for env_variable in ["DARCS_EMAIL", "EMAIL"]: + if env_variable in os.environ: + return os.environ[env_variable] + return None + def _vcs_set_user_id(self, value): + if self.rootdir == None: + self.root(".") + if self.rootdir == None: + raise vcs.SettingIDnotSupported + author_path = os.path.join(self.rootdir, "_darcs", "prefs", "author") + f = codecs.open(author_path, "w", self.encoding) + f.write(value) + f.close() + def _vcs_add(self, path): + if os.path.isdir(path): + return + self._u_invoke_client("add", path) + def _vcs_remove(self, path): + if not os.path.isdir(self._u_abspath(path)): + os.remove(os.path.join(self.rootdir, path)) # darcs notices removal + def _vcs_update(self, path): + pass # darcs notices changes + def _vcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return vcs.VCS._vcs_get_file_contents(self, path, revision, + binary=binary) + else: + if self.parsed_version[0] >= 2: + status,output,error = self._u_invoke_client( \ + "show", "contents", "--patch", revision, path) + return output + else: + # Darcs versions < 2.0.0pre2 lack the "show contents" command + + status,output,error = self._u_invoke_client( \ + "diff", "--unified", "--from-patch", revision, path, + unicode_output=False) + major_patch = output + status,output,error = self._u_invoke_client( \ + "diff", "--unified", "--patch", revision, path, + unicode_output=False) + target_patch = output + + # "--output -" to be supported in GNU patch > 2.5.9 + # but that hasn't been released as of June 30th, 2009. + + # Rewrite path to status before the patch we want + args=["patch", "--reverse", path] + status,output,error = self._u_invoke(args, stdin=major_patch) + # Now apply the patch we want + args=["patch", path] + status,output,error = self._u_invoke(args, stdin=target_patch) + + if os.path.exists(os.path.join(self.rootdir, path)) == True: + contents = vcs.VCS._vcs_get_file_contents(self, path, + binary=binary) + else: + contents = "" + + # Now restore path to it's current incarnation + args=["patch", "--reverse", path] + status,output,error = self._u_invoke(args, stdin=target_patch) + args=["patch", path] + status,output,error = self._u_invoke(args, stdin=major_patch) + current_contents = vcs.VCS._vcs_get_file_contents(self, path, + binary=binary) + return contents + def _vcs_duplicate_repo(self, directory, revision=None): + if revision==None: + vcs.VCS._vcs_duplicate_repo(self, directory, revision) + else: + self._u_invoke_client("put", "--to-patch", revision, directory) + def _vcs_commit(self, commitfile, allow_empty=False): + id = self.get_user_id() + if '@' not in id: + id = "%s <%s@invalid.com>" % (id, id) + args = ['record', '--all', '--author', id, '--logfile', commitfile] + status,output,error = self._u_invoke_client(*args) + empty_strings = ["No changes!"] + if self._u_any_in_string(empty_strings, output) == True: + if allow_empty == False: + raise vcs.EmptyCommit() + # note that darcs does _not_ make an empty revision. + # this returns the last non-empty revision id... + revision = self._vcs_revision_id(-1) + else: + revline = re.compile("Finished recording patch '(.*)'") + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 1 + revision = match.groups()[0] + return revision + def _vcs_revision_id(self, index): + status,output,error = self._u_invoke_client("changes", "--xml") + revisions = [] + xml_str = output.encode("unicode_escape").replace(r"\n", "\n") + element = ElementTree.XML(xml_str) + assert element.tag == "changelog", element.tag + for patch in element.getchildren(): + assert patch.tag == "patch", patch.tag + for child in patch.getchildren(): + if child.tag == "name": + text = unescape(unicode(child.text).decode("unicode_escape").strip()) + revisions.append(text) + revisions.reverse() + try: + return revisions[index] + except IndexError: + return None + +if libbe.TESTING == True: + vcs.make_vcs_testcase_subclasses(Darcs, sys.modules[__name__]) + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/storage/vcs/git.py b/libbe/storage/vcs/git.py new file mode 100644 index 0000000..7f6e53a --- /dev/null +++ b/libbe/storage/vcs/git.py @@ -0,0 +1,151 @@ +# Copyright (C) 2008-2009 Ben Finney +# Chris Ball +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Git backend. +""" + +import os +import re +import sys +import unittest + +import libbe +import vcs +if libbe.TESTING == True: + import doctest + + +def new(): + return Git() + +class Git(vcs.VCS): + name="git" + client="git" + versioned=True + def _vcs_version(self): + status,output,error = self._u_invoke_client("--version") + return output + def _vcs_detect(self, path): + if self._u_search_parent_directories(path, ".git") != None : + return True + return False + def _vcs_root(self, path): + """Find the root of the deepest repository containing path.""" + # Assume that nothing funny is going on; in particular, that we aren't + # dealing with a bare repo. + if os.path.isdir(path) != True: + path = os.path.dirname(path) + status,output,error = self._u_invoke_client("rev-parse", "--git-dir", + cwd=path) + gitdir = os.path.join(path, output.rstrip('\n')) + dirname = os.path.abspath(os.path.dirname(gitdir)) + return dirname + def _vcs_init(self, path): + self._u_invoke_client("init", cwd=path) + def _vcs_get_user_id(self): + status,output,error = \ + self._u_invoke_client("config", "user.name", expect=(0,1)) + if status == 0: + name = output.rstrip('\n') + else: + name = "" + status,output,error = \ + self._u_invoke_client("config", "user.email", expect=(0,1)) + if status == 0: + email = output.rstrip('\n') + else: + email = "" + if name != "" or email != "": # got something! + # guess missing info, if necessary + if name == "": + name = self._u_get_fallback_username() + if email == "": + email = self._u_get_fallback_email() + return self._u_create_id(name, email) + return None # Git has no infomation + def _vcs_set_user_id(self, value): + name,email = self._u_parse_id(value) + if email != None: + self._u_invoke_client("config", "user.email", email) + self._u_invoke_client("config", "user.name", name) + def _vcs_add(self, path): + if os.path.isdir(path): + return + self._u_invoke_client("add", path) + def _vcs_remove(self, path): + if not os.path.isdir(self._u_abspath(path)): + self._u_invoke_client("rm", "-f", path) + def _vcs_update(self, path): + self._vcs_add(path) + def _vcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) + else: + arg = "%s:%s" % (revision,path) + status,output,error = self._u_invoke_client("show", arg) + return output + def _vcs_duplicate_repo(self, directory, revision=None): + if revision==None: + vcs.VCS._vcs_duplicate_repo(self, directory, revision) + else: + self._u_invoke_client("clone", "--no-checkout", ".", directory) + self._u_invoke_client("checkout", revision, cwd=directory) + def _vcs_commit(self, commitfile, allow_empty=False): + args = ['commit', '--all', '--file', commitfile] + if allow_empty == True: + args.append("--allow-empty") + status,output,error = self._u_invoke_client(*args) + else: + kwargs = {"expect":(0,1)} + status,output,error = self._u_invoke_client(*args, **kwargs) + strings = ["nothing to commit", + "nothing added to commit"] + if self._u_any_in_string(strings, output) == True: + raise vcs.EmptyCommit() + revision = None + revline = re.compile("(.*) (.*)[:\]] (.*)") + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 3 + revision = match.groups()[1] + full_revision = self._vcs_revision_id(-1) + assert full_revision.startswith(revision), \ + "Mismatched revisions:\n%s\n%s" % (revision, full_revision) + return full_revision + def _vcs_revision_id(self, index): + args = ["rev-list", "--first-parent", "--reverse", "HEAD"] + kwargs = {"expect":(0,128)} + status,output,error = self._u_invoke_client(*args, **kwargs) + if status == 128: + if error.startswith("fatal: ambiguous argument 'HEAD': unknown "): + return None + raise vcs.CommandError(args, status, stderr=error) + commits = output.splitlines() + try: + return commits[index] + except IndexError: + return None + + +if libbe.TESTING == True: + vcs.make_vcs_testcase_subclasses(Git, sys.modules[__name__]) + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/storage/vcs/hg.py b/libbe/storage/vcs/hg.py new file mode 100644 index 0000000..ed27717 --- /dev/null +++ b/libbe/storage/vcs/hg.py @@ -0,0 +1,108 @@ +# Copyright (C) 2007-2009 Aaron Bentley and Panometrics, Inc. +# Ben Finney +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Mercurial (hg) backend. +""" + +import os +import re +import sys + +import libbe +import vcs + +if libbe.TESTING == True: + import unittest + import doctest + + +def new(): + return Hg() + +class Hg(vcs.VCS): + name="hg" + client="hg" + versioned=True + def _vcs_version(self): + status,output,error = self._u_invoke_client("--version") + return output + def _vcs_detect(self, path): + """Detect whether a directory is revision-controlled using Mercurial""" + if self._u_search_parent_directories(path, ".hg") != None: + return True + return False + def _vcs_root(self, path): + status,output,error = self._u_invoke_client("root", cwd=path) + return output.rstrip('\n') + def _vcs_init(self, path): + self._u_invoke_client("init", cwd=path) + def _vcs_get_user_id(self): + status,output,error = self._u_invoke_client("showconfig","ui.username") + return output.rstrip('\n') + def _vcs_set_user_id(self, value): + """ + Supported by the Config Extension, but that is not part of + standard Mercurial. + http://www.selenic.com/mercurial/wiki/index.cgi/ConfigExtension + """ + raise vcs.SettingIDnotSupported + def _vcs_add(self, path): + self._u_invoke_client("add", path) + def _vcs_remove(self, path): + self._u_invoke_client("rm", "--force", path) + def _vcs_update(self, path): + pass + def _vcs_get_file_contents(self, path, revision=None, binary=False): + if revision == None: + return vcs.VCS._vcs_get_file_contents(self, path, revision, binary=binary) + else: + status,output,error = \ + self._u_invoke_client("cat","-r",revision,path) + return output + def _vcs_duplicate_repo(self, directory, revision=None): + if revision == None: + return vcs.VCS._vcs_duplicate_repo(self, directory, revision) + else: + self._u_invoke_client("archive", "--rev", revision, directory) + def _vcs_commit(self, commitfile, allow_empty=False): + args = ['commit', '--logfile', commitfile] + status,output,error = self._u_invoke_client(*args) + if allow_empty == False: + strings = ["nothing changed"] + if self._u_any_in_string(strings, output) == True: + raise vcs.EmptyCommit() + return self._vcs_revision_id(-1) + def _vcs_revision_id(self, index, style="id"): + args = ["identify", "--rev", str(int(index)), "--%s" % style] + kwargs = {"expect": (0,255)} + status,output,error = self._u_invoke_client(*args, **kwargs) + if status == 0: + id = output.strip() + if id == '000000000000': + return None # before initial commit. + return id + return None + + +if libbe.TESTING == True: + vcs.make_vcs_testcase_subclasses(Hg, sys.modules[__name__]) + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/storage/vcs/util/config.py b/libbe/storage/vcs/util/config.py new file mode 100644 index 0000000..ccd236b --- /dev/null +++ b/libbe/storage/vcs/util/config.py @@ -0,0 +1,94 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Create, save, and load the per-user config file at path(). +""" + +import ConfigParser +import codecs +import locale +import os.path +import sys + +import libbe +if libbe.TESTING == True: + import doctest + + +default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() + +def path(): + """Return the path to the per-user config file""" + return os.path.expanduser("~/.bugs_everywhere") + +def set_val(name, value, section="DEFAULT", encoding=None): + """Set a value in the per-user config file + + :param name: The name of the value to set + :param value: The new value to set (or None to delete the value) + :param section: The section to store the name/value in + """ + if encoding == None: + encoding = default_encoding + config = ConfigParser.ConfigParser() + if os.path.exists(path()) == False: # touch file or config + open(path(), "w").close() # read chokes on missing file + f = codecs.open(path(), "r", encoding) + config.readfp(f, path()) + f.close() + if value is not None: + config.set(section, name, value) + else: + config.remove_option(section, name) + f = codecs.open(path(), "w", encoding) + config.write(f) + f.close() + +def get_val(name, section="DEFAULT", default=None, encoding=None): + """ + Get a value from the per-user config file + + :param name: The name of the value to get + :section: The section that the name is in + :return: The value, or None + >>> get_val("junk") is None + True + >>> set_val("junk", "random") + >>> get_val("junk") + u'random' + >>> set_val("junk", None) + >>> get_val("junk") is None + True + """ + if os.path.exists(path()): + if encoding == None: + encoding = default_encoding + config = ConfigParser.ConfigParser() + f = codecs.open(path(), "r", encoding) + config.readfp(f, path()) + f.close() + try: + return config.get(section, name) + except ConfigParser.NoOptionError: + return default + else: + return default + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/storage/vcs/util/mapfile.py b/libbe/storage/vcs/util/mapfile.py new file mode 100644 index 0000000..8e1e279 --- /dev/null +++ b/libbe/storage/vcs/util/mapfile.py @@ -0,0 +1,126 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Provide a means of saving and loading dictionaries of parameters. The +saved "mapfiles" should be clear, flat-text files, and allow easy merging of +independent/conflicting changes. +""" + +import errno +import os.path +import yaml + +import libbe +if libbe.TESTING == True: + import doctest + + +class IllegalKey(Exception): + def __init__(self, key): + Exception.__init__(self, 'Illegal key "%s"' % key) + self.key = key + +class IllegalValue(Exception): + def __init__(self, value): + Exception.__init__(self, 'Illegal value "%s"' % value) + self.value = value + +def generate(map): + """Generate a YAML mapfile content string. + >>> generate({"q":"p"}) + 'q: p\\n\\n' + >>> generate({"q":u"Fran\u00e7ais"}) + 'q: Fran\\xc3\\xa7ais\\n\\n' + >>> generate({"q":u"hello"}) + 'q: hello\\n\\n' + >>> generate({"q=":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "q=" + >>> generate({"q:":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "q:" + >>> generate({"q\\n":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "q\\n" + >>> generate({"":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "" + >>> generate({">q":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key ">q" + >>> generate({"q":"p\\n"}) + Traceback (most recent call last): + IllegalValue: Illegal value "p\\n" + """ + keys = map.keys() + keys.sort() + for key in keys: + try: + assert not key.startswith('>') + assert('\n' not in key) + assert('=' not in key) + assert(':' not in key) + assert(len(key) > 0) + except AssertionError: + raise IllegalKey(unicode(key).encode('unicode_escape')) + if '\n' in map[key]: + raise IllegalValue(unicode(map[key]).encode('unicode_escape')) + + lines = [] + for key in keys: + lines.append(yaml.safe_dump({key: map[key]}, + default_flow_style=False, + allow_unicode=True)) + lines.append('') + return '\n'.join(lines) + +def parse(contents): + """ + Parse a YAML mapfile string. + >>> parse('q: p\\n\\n')['q'] + 'p' + >>> parse('q: \\'p\\'\\n\\n')['q'] + 'p' + >>> contents = generate({"a":"b", "c":"d", "e":"f"}) + >>> dict = parse(contents) + >>> dict["a"] + 'b' + >>> dict["c"] + 'd' + >>> dict["e"] + 'f' + >>> contents = generate({"q":u"Fran\u00e7ais"}) + >>> dict = parse(contents) + >>> dict["q"] + u'Fran\\xe7ais' + """ + return yaml.load(contents) or {} + +def map_save(vcs, path, map, allow_no_vcs=False): + """Save the map as a mapfile to the specified path""" + contents = generate(map) + vcs.set_file_contents(path, contents, allow_no_vcs, binary=True) + +def map_load(vcs, path, allow_no_vcs=False): + contents = vcs.get_file_contents(path, allow_no_vcs=allow_no_vcs, + binary=True) + return parse(contents) + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/storage/vcs/util/upgrade.py b/libbe/storage/vcs/util/upgrade.py new file mode 100644 index 0000000..dc9d54f --- /dev/null +++ b/libbe/storage/vcs/util/upgrade.py @@ -0,0 +1,246 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Handle conversion between the various on-disk images. +""" + +import os, os.path +import sys + +import libbe +import bug +import encoding +import mapfile +import vcs +if libbe.TESTING == True: + import doctest + +# a list of all past versions +BUGDIR_DISK_VERSIONS = ["Bugs Everywhere Tree 1 0", + "Bugs Everywhere Directory v1.1", + "Bugs Everywhere Directory v1.2", + "Bugs Everywhere Directory v1.3"] + +# the current version +BUGDIR_DISK_VERSION = BUGDIR_DISK_VERSIONS[-1] + +class Upgrader (object): + "Class for converting " + initial_version = None + final_version = None + def __init__(self, root): + self.root = root + # use the "None" VCS to ensure proper encoding/decoding and + # simplify path construction. + self.vcs = vcs.vcs_by_name("None") + self.vcs.root(self.root) + self.vcs.encoding = encoding.get_encoding() + + def get_path(self, *args): + """ + Return a path relative to .root. + """ + dir = os.path.join(self.root, ".be") + if len(args) == 0: + return dir + assert args[0] in ["version", "settings", "bugs"], str(args) + return os.path.join(dir, *args) + + def check_initial_version(self): + path = self.get_path("version") + version = self.vcs.get_file_contents(path).rstrip("\n") + assert version == self.initial_version, version + + def set_version(self): + path = self.get_path("version") + self.vcs.set_file_contents(path, self.final_version+"\n") + + def upgrade(self): + print >> sys.stderr, "upgrading bugdir from '%s' to '%s'" \ + % (self.initial_version, self.final_version) + self.check_initial_version() + self.set_version() + self._upgrade() + + def _upgrade(self): + raise NotImplementedError + + +class Upgrade_1_0_to_1_1 (Upgrader): + initial_version = "Bugs Everywhere Tree 1 0" + final_version = "Bugs Everywhere Directory v1.1" + def _upgrade_mapfile(self, path): + contents = self.vcs.get_file_contents(path) + old_format = False + for line in contents.splitlines(): + if len(line.split("=")) == 2: + old_format = True + break + if old_format == True: + # translate to YAML. + newlines = [] + for line in contents.splitlines(): + line = line.rstrip('\n') + if len(line) == 0: + continue + fields = line.split("=") + if len(fields) == 2: + key,value = fields + newlines.append('%s: "%s"' % (key, value.replace('"','\\"'))) + else: + newlines.append(line) + contents = '\n'.join(newlines) + # load the YAML and save + map = mapfile.parse(contents) + mapfile.map_save(self.vcs, path, map) + + def _upgrade(self): + """ + Comment value field "From" -> "Author". + Homegrown mapfile -> YAML. + """ + path = self.get_path("settings") + self._upgrade_mapfile(path) + for bug_uuid in os.listdir(self.get_path("bugs")): + path = self.get_path("bugs", bug_uuid, "values") + self._upgrade_mapfile(path) + c_path = ["bugs", bug_uuid, "comments"] + if not os.path.exists(self.get_path(*c_path)): + continue # no comments for this bug + for comment_uuid in os.listdir(self.get_path(*c_path)): + path_list = c_path + [comment_uuid, "values"] + path = self.get_path(*path_list) + self._upgrade_mapfile(path) + settings = mapfile.map_load(self.vcs, path) + if "From" in settings: + settings["Author"] = settings.pop("From") + mapfile.map_save(self.vcs, path, settings) + + +class Upgrade_1_1_to_1_2 (Upgrader): + initial_version = "Bugs Everywhere Directory v1.1" + final_version = "Bugs Everywhere Directory v1.2" + def _upgrade(self): + """ + BugDir settings field "rcs_name" -> "vcs_name". + """ + path = self.get_path("settings") + settings = mapfile.map_load(self.vcs, path) + if "rcs_name" in settings: + settings["vcs_name"] = settings.pop("rcs_name") + mapfile.map_save(self.vcs, path, settings) + +class Upgrade_1_2_to_1_3 (Upgrader): + initial_version = "Bugs Everywhere Directory v1.2" + final_version = "Bugs Everywhere Directory v1.3" + def __init__(self, *args, **kwargs): + Upgrader.__init__(self, *args, **kwargs) + self._targets = {} # key: target text,value: new target bug + path = self.get_path('settings') + settings = mapfile.map_load(self.vcs, path) + if 'vcs_name' in settings: + old_vcs = self.vcs + self.vcs = vcs.vcs_by_name(settings['vcs_name']) + self.vcs.root(self.root) + self.vcs.encoding = old_vcs.encoding + + def _target_bug(self, target_text): + if target_text not in self._targets: + _bug = bug.Bug(bugdir=self, summary=target_text) + # note: we're not a bugdir, but all Bug.save() needs is + # .root, .vcs, and .get_path(), which we have. + _bug.severity = 'target' + self._targets[target_text] = _bug + return self._targets[target_text] + + def _upgrade_bugdir_mapfile(self): + path = self.get_path('settings') + settings = mapfile.map_load(self.vcs, path) + if 'target' in settings: + settings['target'] = self._target_bug(settings['target']).uuid + mapfile.map_save(self.vcs, path, settings) + + def _upgrade_bug_mapfile(self, bug_uuid): + import becommands.depend + path = self.get_path('bugs', bug_uuid, 'values') + settings = mapfile.map_load(self.vcs, path) + if 'target' in settings: + target_bug = self._target_bug(settings['target']) + _bug = bug.Bug(bugdir=self, uuid=bug_uuid, from_disk=True) + # note: we're not a bugdir, but all Bug.load_settings() + # needs is .root, .vcs, and .get_path(), which we have. + becommands.depend.add_block(target_bug, _bug) + _bug.settings.pop('target') + _bug.save() + + def _upgrade(self): + """ + Bug value field "target" -> target bugs. + Bugdir value field "target" -> pointer to current target bug. + """ + for bug_uuid in os.listdir(self.get_path('bugs')): + self._upgrade_bug_mapfile(bug_uuid) + self._upgrade_bugdir_mapfile() + for _bug in self._targets.values(): + _bug.save() + +upgraders = [Upgrade_1_0_to_1_1, + Upgrade_1_1_to_1_2, + Upgrade_1_2_to_1_3] +upgrade_classes = {} +for upgrader in upgraders: + upgrade_classes[(upgrader.initial_version,upgrader.final_version)]=upgrader + +def upgrade(path, current_version, + target_version=BUGDIR_DISK_VERSION): + """ + Call the appropriate upgrade function to convert current_version + to target_version. If a direct conversion function does not exist, + use consecutive conversion functions. + """ + if current_version not in BUGDIR_DISK_VERSIONS: + raise NotImplementedError, \ + "Cannot handle version '%s' yet." % version + if target_version not in BUGDIR_DISK_VERSIONS: + raise NotImplementedError, \ + "Cannot handle version '%s' yet." % version + + if (current_version, target_version) in upgrade_classes: + # direct conversion + upgrade_class = upgrade_classes[(current_version, target_version)] + u = upgrade_class(path) + u.upgrade() + else: + # consecutive single-step conversion + i = BUGDIR_DISK_VERSIONS.index(current_version) + while True: + version_a = BUGDIR_DISK_VERSIONS[i] + version_b = BUGDIR_DISK_VERSIONS[i+1] + try: + upgrade_class = upgrade_classes[(version_a, version_b)] + except KeyError: + raise NotImplementedError, \ + "Cannot convert version '%s' to '%s' yet." \ + % (version_a, version_b) + u = upgrade_class(path) + u.upgrade() + if version_b == target_version: + break + i += 1 + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/subproc.py b/libbe/subproc.py deleted file mode 100644 index 8806e26..0000000 --- a/libbe/subproc.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright (C) 2009 W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Functions for running external commands in subprocesses. -""" - -from subprocess import Popen, PIPE -import sys - -import libbe -from encoding import get_encoding -if libbe.TESTING == True: - import doctest - -_MSWINDOWS = sys.platform == 'win32' -_POSIX = not _MSWINDOWS - -if _POSIX == True: - import os - import select - -class CommandError(Exception): - def __init__(self, command, status, stdout=None, stderr=None): - strerror = ['Command failed (%d):\n %s\n' % (status, stderr), - 'while executing\n %s' % command] - Exception.__init__(self, '\n'.join(strerror)) - self.command = command - self.status = status - self.stdout = stdout - self.stderr = stderr - -def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,), - cwd=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 - unicode_output == True, convert stdout and stdin strings to - unicode before returing them. - """ - if cwd == None: - cwd = '.' - if verbose == True: - print >> sys.stderr, '%s$ %s' % (cwd, ' '.join(args)) - try : - if _POSIX: - q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd) - else: - assert _MSWINDOWS==True, 'invalid platform' - # 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) - except OSError, e: - raise CommandError(args, status=e.args[0], stderr=e) - stdout,stderr = q.communicate(input=stdin) - status = q.wait() - if unicode_output == True: - if encoding == None: - encoding = get_encoding() - if stdout != None: - stdout = unicode(stdout, encoding) - if stderr != None: - stderr = unicode(stderr, encoding) - if verbose == True: - print >> sys.stderr, '%d\n%s%s' % (status, stdout, stderr) - if status not in expect: - raise CommandError(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. - - >>> p = Pipe([['find', '/etc/'], ['grep', '^/etc/ssh$']]) - >>> p.stdout - '/etc/ssh\\n' - >>> p.status - 1 - >>> p.statuses - [1, 0] - >>> p.stderrs # doctest: +ELLIPSIS - [...find: ...: Permission denied..., ''] - """ - def __init__(self, cmds, stdin=None): - # spawn processes - self._procs = [] - for cmd in cmds: - if len(self._procs) != 0: - stdin = self._procs[-1].stdout - self._procs.append(Popen(cmd, stdin=stdin, stdout=PIPE, stderr=PIPE)) - - self.stdout,self.stderrs = self._communicate(input=None) - - # collect process statuses - self.statuses = [] - self.status = 0 - for proc in self._procs: - self.statuses.append(proc.wait()) - if self.statuses[-1] != 0: - self.status = self.statuses[-1] - - # Code excerpted from subprocess.Popen._communicate() - if _MSWINDOWS == True: - def _communicate(self, input=None): - assert input == None, 'stdin != None not yet supported' - # listen to each process' stderr - threads = [] - std_X_arrays = [] - for proc in self._procs: - stderr_array = [] - thread = Thread(target=proc._readerthread, - args=(proc.stderr, stderr_array)) - thread.setDaemon(True) - thread.start() - threads.append(thread) - std_X_arrays.append(stderr_array) - - # also listen to the last processes stdout - stdout_array = [] - thread = Thread(target=proc._readerthread, - args=(proc.stdout, stdout_array)) - thread.setDaemon(True) - thread.start() - threads.append(thread) - std_X_arrays.append(stdout_array) - - # join threads as they die - for thread in threads: - thread.join() - - # read output from reader threads - std_X_strings = [] - for std_X_array in std_X_arrays: - std_X_strings.append(std_X_array[0]) - - stdout = std_X_strings.pop(-1) - stderrs = std_X_strings - return (stdout, stderrs) - else: - assert _POSIX==True, 'invalid platform' - def _communicate(self, input=None): - read_set = [] - write_set = [] - read_arrays = [] - stdout = None # Return - stderr = None # Return - - if self._procs[0].stdin: - # Flush stdio buffer. This might block, if the user has - # been writing to .stdin in an uncontrolled fashion. - self._procs[0].stdin.flush() - if input: - write_set.append(self._procs[0].stdin) - else: - self._procs[0].stdin.close() - for proc in self._procs: - read_set.append(proc.stderr) - read_arrays.append([]) - read_set.append(self._procs[-1].stdout) - read_arrays.append([]) - - input_offset = 0 - while read_set or write_set: - try: - rlist, wlist, xlist = select.select(read_set, write_set, []) - except select.error, e: - if e.args[0] == errno.EINTR: - continue - raise - if self._procs[0].stdin in wlist: - # When select has indicated that the file is writable, - # we can write up to PIPE_BUF bytes without risk - # blocking. POSIX defines PIPE_BUF >= 512 - chunk = input[input_offset : input_offset + 512] - bytes_written = os.write(self.stdin.fileno(), chunk) - input_offset += bytes_written - if input_offset >= len(input): - self._procs[0].stdin.close() - write_set.remove(self._procs[0].stdin) - if self._procs[-1].stdout in rlist: - data = os.read(self._procs[-1].stdout.fileno(), 1024) - if data == '': - self._procs[-1].stdout.close() - read_set.remove(self._procs[-1].stdout) - read_arrays[-1].append(data) - for i,proc in enumerate(self._procs): - if proc.stderr in rlist: - data = os.read(proc.stderr.fileno(), 1024) - if data == '': - proc.stderr.close() - read_set.remove(proc.stderr) - read_arrays[i].append(data) - - # All data exchanged. Translate lists into strings. - read_strings = [] - for read_array in read_arrays: - read_strings.append(''.join(read_array)) - - stdout = read_strings.pop(-1) - stderrs = read_strings - return (stdout, stderrs) - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() diff --git a/libbe/tree.py b/libbe/tree.py deleted file mode 100644 index 1daac44..0000000 --- a/libbe/tree.py +++ /dev/null @@ -1,193 +0,0 @@ -# Bugs Everywhere, a distributed bugtracker -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Define a traversable tree structure. -""" - -import libbe -if libbe.TESTING == True: - import doctest - -class Tree(list): - """ - Construct - +-b---d-g - a-+ +-e - +-c-+-f-h-i - with - >>> i = Tree(); i.n = "i" - >>> h = Tree([i]); h.n = "h" - >>> f = Tree([h]); f.n = "f" - >>> e = Tree(); e.n = "e" - >>> c = Tree([f,e]); c.n = "c" - >>> g = Tree(); g.n = "g" - >>> d = Tree([g]); d.n = "d" - >>> b = Tree([d]); b.n = "b" - >>> a = Tree(); a.n = "a" - >>> a.append(c) - >>> a.append(b) - - >>> a.branch_len() - 5 - >>> a.sort(key=lambda node : -node.branch_len()) - >>> "".join([node.n for node in a.traverse()]) - 'acfhiebdg' - >>> a.sort(key=lambda node : node.branch_len()) - >>> "".join([node.n for node in a.traverse()]) - 'abdgcefhi' - >>> "".join([node.n for node in a.traverse(depth_first=False)]) - 'abcdefghi' - >>> for depth,node in a.thread(): - ... print "%*s" % (2*depth+1, node.n) - a - b - d - g - c - e - f - h - i - >>> for depth,node in a.thread(flatten=True): - ... print "%*s" % (2*depth+1, node.n) - a - b - d - g - c - e - f - h - i - >>> a.has_descendant(g) - True - >>> c.has_descendant(g) - False - >>> a.has_descendant(a) - False - >>> a.has_descendant(a, match_self=True) - True - """ - def __cmp__(self, other): - return cmp(id(self), id(other)) - - def __eq__(self, other): - return self.__cmp__(other) == 0 - - def __ne__(self, other): - return self.__cmp__(other) != 0 - - def branch_len(self): - """ - Exhaustive search every time == SLOW. - - Use only on small trees, or reimplement by overriding - child-addition methods to allow accurate caching. - - For the tree - +-b---d-g - a-+ +-e - +-c-+-f-h-i - this method returns 5. - """ - if len(self) == 0: - return 1 - else: - return 1 + max([child.branch_len() for child in self]) - - def sort(self, *args, **kwargs): - """ - This method can be slow, e.g. on a branch_len() sort, since a - node at depth N from the root has it's branch_len() method - called N times. - """ - list.sort(self, *args, **kwargs) - for child in self: - child.sort(*args, **kwargs) - - def traverse(self, depth_first=True): - """ - Note: you might want to sort() your tree first. - """ - if depth_first == True: - yield self - for child in self: - for descendant in child.traverse(): - yield descendant - else: # breadth first, Wikipedia algorithm - # http://en.wikipedia.org/wiki/Breadth-first_search - queue = [self] - while len(queue) > 0: - node = queue.pop(0) - yield node - queue.extend(node) - - def thread(self, flatten=False): - """ - When flatten==False, the depth of any node is one greater than - the depth of its parent. That way the inheritance is - explicit, but you can end up with highly indented threads. - - When flatten==True, the depth of any node is only greater than - the depth of its parent when there is a branch, and the node - is not the last child. This can lead to ancestry ambiguity, - but keeps the total indentation down. E.g. - +-b +-b-c - a-+-c and a-+ - +-d-e-f +-d-e-f - would both produce (after sorting by branch_len()) - (0, a) - (1, b) - (1, c) - (0, d) - (0, e) - (0, f) - """ - stack = [] # ancestry of the current node - if flatten == True: - depthDict = {} - - for node in self.traverse(depth_first=True): - while len(stack) > 0 \ - and id(node) not in [id(c) for c in stack[-1]]: - stack.pop(-1) - if flatten == False: - depth = len(stack) - else: - if len(stack) == 0: - depth = 0 - else: - parent = stack[-1] - depth = depthDict[id(parent)] - if len(parent) > 1 and node != parent[-1]: - depth += 1 - depthDict[id(node)] = depth - yield (depth,node) - stack.append(node) - - def has_descendant(self, descendant, depth_first=True, match_self=False): - if descendant == self: - return match_self - for d in self.traverse(depth_first): - if descendant == d: - return True - return False - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() diff --git a/libbe/ui/util/cmdutil.py b/libbe/ui/util/cmdutil.py new file mode 100644 index 0000000..c567984 --- /dev/null +++ b/libbe/ui/util/cmdutil.py @@ -0,0 +1,356 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# Oleg Romanyshyn +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Define assorted utilities to make command-line handling easier. +""" + +import glob +import optparse +import os +from textwrap import TextWrapper +from StringIO import StringIO +import sys + +import libbe +import bugdir +import comment +import plugin +import encoding +if libbe.TESTING == True: + import doctest + + +class UserError(Exception): + def __init__(self, msg): + Exception.__init__(self, msg) + +class UnknownCommand(UserError): + def __init__(self, cmd): + Exception.__init__(self, "Unknown command '%s'" % cmd) + self.cmd = cmd + +class UsageError(Exception): + pass + +class GetHelp(Exception): + pass + +class GetCompletions(Exception): + def __init__(self, completions=[]): + msg = "Get allowed completions" + Exception.__init__(self, msg) + self.completions = completions + +def iter_commands(): + for name, module in plugin.iter_plugins("becommands"): + yield name.replace("_", "-"), module + +def get_command(command_name): + """Retrieves the module for a user command + + >>> try: + ... get_command("asdf") + ... except UnknownCommand, e: + ... print e + Unknown command 'asdf' + >>> repr(get_command("list")).startswith(" 0: + max_pos_arg = max(bugid_args.keys()) + else: + max_pos_arg = -1 + for pos,value in enumerate(args): + if value == "--complete": + filter = None + if pos in bugid_args: + filter = bugid_args[pos] + if pos > max_pos_arg and -1 in bugid_args: + filter = bugid_args[-1] + if filter != None: + bugshortnames = [] + try: + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + bd.load_all_bugs() + bugs = [bug for bug in bd if filter(bug) == True] + bugshortnames = [bd.bug_shortname(bug) for bug in bugs] + except bugdir.NoBugDir: + pass + raise GetCompletions(bugshortnames) + raise GetCompletions() + +def complete_path(path): + """List possible path completions for path.""" + comps = glob.glob(path+"*") + glob.glob(path+"/*") + if len(comps) == 1 and os.path.isdir(comps[0]): + comps.extend(glob.glob(comps[0]+"/*")) + return comps + +def underlined(instring): + """Produces a version of a string that is underlined with '=' + + >>> underlined("Underlined String") + 'Underlined String\\n=================' + """ + + return "%s\n%s" % (instring, "="*len(instring)) + +def select_values(string, possible_values, name="unkown"): + """ + This function allows the user to select values from a list of + possible values. The default is to select all the values: + + >>> select_values(None, ['abc', 'def', 'hij']) + ['abc', 'def', 'hij'] + + The user selects values with a comma-separated limit_string. + Prepending a minus sign to such a list denotes blacklist mode: + + >>> select_values('-abc,hij', ['abc', 'def', 'hij']) + ['def'] + + Without the leading -, the selection is in whitelist mode: + + >>> select_values('abc,hij', ['abc', 'def', 'hij']) + ['abc', 'hij'] + + In either case, appropriate errors are raised if on of the + user-values is not in the list of possible values. The name + parameter lets you make the error message more clear: + + >>> select_values('-xyz,hij', ['abc', 'def', 'hij'], name="foobar") + Traceback (most recent call last): + ... + UserError: Invalid foobar xyz + ['abc', 'def', 'hij'] + >>> select_values('xyz,hij', ['abc', 'def', 'hij'], name="foobar") + Traceback (most recent call last): + ... + UserError: Invalid foobar xyz + ['abc', 'def', 'hij'] + """ + possible_values = list(possible_values) # don't alter the original + if string == None: + pass + elif string.startswith('-'): + blacklisted_values = set(string[1:].split(',')) + for value in blacklisted_values: + if value not in possible_values: + raise UserError('Invalid %s %s\n %s' + % (name, value, possible_values)) + possible_values.remove(value) + else: + whitelisted_values = string.split(',') + for value in whitelisted_values: + if value not in possible_values: + raise UserError('Invalid %s %s\n %s' + % (name, value, possible_values)) + possible_values = whitelisted_values + return possible_values + +def restrict_file_access(bugdir, path): + """ + Check that the file at path is inside bugdir.root. This is + important if you allow other users to execute becommands with your + username (e.g. if you're running be-handle-mail through your + ~/.procmailrc). If this check wasn't made, a user could e.g. + run + be commit -b ~/.ssh/id_rsa "Hack to expose ssh key" + which would expose your ssh key to anyone who could read the VCS + log. + """ + in_root = bugdir.vcs.path_in_root(path, bugdir.root) + if in_root == False: + raise UserError('file access restricted!\n %s not in %s' + % (path, bugdir.root)) + +def parse_id(id): + """ + Return (bug_id, comment_id) tuple. + Basically inverts Comment.comment_shortnames() + >>> parse_id('XYZ') + ('XYZ', None) + >>> parse_id('XYZ:123') + ('XYZ', ':123') + >>> parse_id('') + Traceback (most recent call last): + ... + UserError: invalid id ''. + >>> parse_id('::') + Traceback (most recent call last): + ... + UserError: invalid id '::'. + """ + if len(id) == 0: + raise UserError("invalid id '%s'." % id) + if id.count(':') > 1: + raise UserError("invalid id '%s'." % id) + elif id.count(':') == 1: + # Split shortname generated by Comment.comment_shortnames() + bug_id,comment_id = id.split(':') + comment_id = ':'+comment_id + else: + bug_id = id + comment_id = None + return (bug_id, comment_id) + +def bug_from_id(bdir, id): + """ + Exception translation for the command-line interface. + id can be either the bug shortname or the full uuid. + """ + try: + bug = bdir.bug_from_shortname(id) + except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: + raise UserError(e.message) + return bug + +def bug_comment_from_id(bdir, id): + """ + Return (bug,comment) tuple matching shortname. id can be either + the bug/comment shortname or the full uuid. If there is no + comment part to the id, the returned comment is the bug's + .comment_root. + """ + bug_id,comment_id = parse_id(id) + try: + bug = bdir.bug_from_shortname(bug_id) + except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: + raise UserError(e.message) + if comment_id == None: + comm = bug.comment_root + else: + #bug.load_comments(load_full=False) + try: + comm = bug.comment_root.comment_from_shortname(comment_id) + except comment.InvalidShortname, e: + raise UserError(e.message) + return (bug, comm) + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/ui/util/editor.py b/libbe/ui/util/editor.py new file mode 100644 index 0000000..859cedc --- /dev/null +++ b/libbe/ui/util/editor.py @@ -0,0 +1,113 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Define editor_string(), a function that invokes an editor to accept +user-produced text as a string. +""" + +import codecs +import locale +import os +import sys +import tempfile + +import libbe +if libbe.TESTING == True: + import doctest + + +default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() + +comment_marker = u"== Anything below this line will be ignored\n" + +class CantFindEditor(Exception): + def __init__(self): + Exception.__init__(self, "Can't find editor to get string from") + +def editor_string(comment=None, encoding=None): + """Invokes the editor, and returns the user-produced text as a string + + >>> if "EDITOR" in os.environ: + ... del os.environ["EDITOR"] + >>> if "VISUAL" in os.environ: + ... del os.environ["VISUAL"] + >>> editor_string() + Traceback (most recent call last): + CantFindEditor: Can't find editor to get string from + >>> os.environ["EDITOR"] = "echo bar > " + >>> editor_string() + u'bar\\n' + >>> os.environ["VISUAL"] = "echo baz > " + >>> editor_string() + u'baz\\n' + >>> del os.environ["EDITOR"] + >>> del os.environ["VISUAL"] + """ + if encoding == None: + encoding = default_encoding + for name in ('VISUAL', 'EDITOR'): + try: + editor = os.environ[name] + break + except KeyError: + pass + else: + raise CantFindEditor() + fhandle, fname = tempfile.mkstemp() + try: + if comment is not None: + cstring = u'\n'+comment_string(comment) + os.write(fhandle, cstring.encode(encoding)) + os.close(fhandle) + oldmtime = os.path.getmtime(fname) + os.system("%s %s" % (editor, fname)) + f = codecs.open(fname, "r", encoding) + output = trimmed_string(f.read()) + f.close() + if output.rstrip('\n') == "": + output = None + finally: + os.unlink(fname) + return output + + +def comment_string(comment): + """ + >>> comment_string('hello') == comment_marker+"hello" + True + """ + return comment_marker + comment + + +def trimmed_string(instring): + """ + >>> trimmed_string("hello\\n"+comment_marker) + u'hello\\n' + >>> trimmed_string("hi!\\n" + comment_string('Booga')) + u'hi!\\n' + """ + out = [] + for line in instring.splitlines(True): + if line.startswith(comment_marker): + break + out.append(line) + return ''.join(out) + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/ui/util/pager.py b/libbe/ui/util/pager.py new file mode 100644 index 0000000..1ddc3fa --- /dev/null +++ b/libbe/ui/util/pager.py @@ -0,0 +1,65 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Automatic pager for terminal output (a la Git). +""" + +import sys, os, select + +# see http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby +def run_pager(paginate='auto'): + """ + paginate should be one of 'never', 'auto', or 'always'. + + usage: just call this function and continue using sys.stdout like + you normally would. + """ + if paginate == 'never' \ + or sys.platform == 'win32' \ + or not hasattr(sys.stdout, 'isatty') \ + or sys.stdout.isatty() == False: + return + + if paginate == 'auto': + if 'LESS' not in os.environ: + os.environ['LESS'] = '' # += doesn't work on undefined var + # don't page if the input is short enough + os.environ['LESS'] += ' -FRX' + if 'PAGER' in os.environ: + pager = os.environ['PAGER'] + else: + pager = 'less' + + read_fd, write_fd = os.pipe() + if os.fork() == 0: + # child process + os.close(read_fd) + os.close(0) + os.dup2(write_fd, 1) + os.close(write_fd) + if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() == True: + os.dup2(1, 2) + return + + # parent process, become pager + os.close(write_fd) + os.dup2(read_fd, 0) + os.close(read_fd) + + # Wait until we have input before we start the pager + select.select([0], [], []) + os.execlp(pager, pager) diff --git a/libbe/upgrade.py b/libbe/upgrade.py deleted file mode 100644 index dc9d54f..0000000 --- a/libbe/upgrade.py +++ /dev/null @@ -1,246 +0,0 @@ -# Copyright (C) 2009 W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Handle conversion between the various on-disk images. -""" - -import os, os.path -import sys - -import libbe -import bug -import encoding -import mapfile -import vcs -if libbe.TESTING == True: - import doctest - -# a list of all past versions -BUGDIR_DISK_VERSIONS = ["Bugs Everywhere Tree 1 0", - "Bugs Everywhere Directory v1.1", - "Bugs Everywhere Directory v1.2", - "Bugs Everywhere Directory v1.3"] - -# the current version -BUGDIR_DISK_VERSION = BUGDIR_DISK_VERSIONS[-1] - -class Upgrader (object): - "Class for converting " - initial_version = None - final_version = None - def __init__(self, root): - self.root = root - # use the "None" VCS to ensure proper encoding/decoding and - # simplify path construction. - self.vcs = vcs.vcs_by_name("None") - self.vcs.root(self.root) - self.vcs.encoding = encoding.get_encoding() - - def get_path(self, *args): - """ - Return a path relative to .root. - """ - dir = os.path.join(self.root, ".be") - if len(args) == 0: - return dir - assert args[0] in ["version", "settings", "bugs"], str(args) - return os.path.join(dir, *args) - - def check_initial_version(self): - path = self.get_path("version") - version = self.vcs.get_file_contents(path).rstrip("\n") - assert version == self.initial_version, version - - def set_version(self): - path = self.get_path("version") - self.vcs.set_file_contents(path, self.final_version+"\n") - - def upgrade(self): - print >> sys.stderr, "upgrading bugdir from '%s' to '%s'" \ - % (self.initial_version, self.final_version) - self.check_initial_version() - self.set_version() - self._upgrade() - - def _upgrade(self): - raise NotImplementedError - - -class Upgrade_1_0_to_1_1 (Upgrader): - initial_version = "Bugs Everywhere Tree 1 0" - final_version = "Bugs Everywhere Directory v1.1" - def _upgrade_mapfile(self, path): - contents = self.vcs.get_file_contents(path) - old_format = False - for line in contents.splitlines(): - if len(line.split("=")) == 2: - old_format = True - break - if old_format == True: - # translate to YAML. - newlines = [] - for line in contents.splitlines(): - line = line.rstrip('\n') - if len(line) == 0: - continue - fields = line.split("=") - if len(fields) == 2: - key,value = fields - newlines.append('%s: "%s"' % (key, value.replace('"','\\"'))) - else: - newlines.append(line) - contents = '\n'.join(newlines) - # load the YAML and save - map = mapfile.parse(contents) - mapfile.map_save(self.vcs, path, map) - - def _upgrade(self): - """ - Comment value field "From" -> "Author". - Homegrown mapfile -> YAML. - """ - path = self.get_path("settings") - self._upgrade_mapfile(path) - for bug_uuid in os.listdir(self.get_path("bugs")): - path = self.get_path("bugs", bug_uuid, "values") - self._upgrade_mapfile(path) - c_path = ["bugs", bug_uuid, "comments"] - if not os.path.exists(self.get_path(*c_path)): - continue # no comments for this bug - for comment_uuid in os.listdir(self.get_path(*c_path)): - path_list = c_path + [comment_uuid, "values"] - path = self.get_path(*path_list) - self._upgrade_mapfile(path) - settings = mapfile.map_load(self.vcs, path) - if "From" in settings: - settings["Author"] = settings.pop("From") - mapfile.map_save(self.vcs, path, settings) - - -class Upgrade_1_1_to_1_2 (Upgrader): - initial_version = "Bugs Everywhere Directory v1.1" - final_version = "Bugs Everywhere Directory v1.2" - def _upgrade(self): - """ - BugDir settings field "rcs_name" -> "vcs_name". - """ - path = self.get_path("settings") - settings = mapfile.map_load(self.vcs, path) - if "rcs_name" in settings: - settings["vcs_name"] = settings.pop("rcs_name") - mapfile.map_save(self.vcs, path, settings) - -class Upgrade_1_2_to_1_3 (Upgrader): - initial_version = "Bugs Everywhere Directory v1.2" - final_version = "Bugs Everywhere Directory v1.3" - def __init__(self, *args, **kwargs): - Upgrader.__init__(self, *args, **kwargs) - self._targets = {} # key: target text,value: new target bug - path = self.get_path('settings') - settings = mapfile.map_load(self.vcs, path) - if 'vcs_name' in settings: - old_vcs = self.vcs - self.vcs = vcs.vcs_by_name(settings['vcs_name']) - self.vcs.root(self.root) - self.vcs.encoding = old_vcs.encoding - - def _target_bug(self, target_text): - if target_text not in self._targets: - _bug = bug.Bug(bugdir=self, summary=target_text) - # note: we're not a bugdir, but all Bug.save() needs is - # .root, .vcs, and .get_path(), which we have. - _bug.severity = 'target' - self._targets[target_text] = _bug - return self._targets[target_text] - - def _upgrade_bugdir_mapfile(self): - path = self.get_path('settings') - settings = mapfile.map_load(self.vcs, path) - if 'target' in settings: - settings['target'] = self._target_bug(settings['target']).uuid - mapfile.map_save(self.vcs, path, settings) - - def _upgrade_bug_mapfile(self, bug_uuid): - import becommands.depend - path = self.get_path('bugs', bug_uuid, 'values') - settings = mapfile.map_load(self.vcs, path) - if 'target' in settings: - target_bug = self._target_bug(settings['target']) - _bug = bug.Bug(bugdir=self, uuid=bug_uuid, from_disk=True) - # note: we're not a bugdir, but all Bug.load_settings() - # needs is .root, .vcs, and .get_path(), which we have. - becommands.depend.add_block(target_bug, _bug) - _bug.settings.pop('target') - _bug.save() - - def _upgrade(self): - """ - Bug value field "target" -> target bugs. - Bugdir value field "target" -> pointer to current target bug. - """ - for bug_uuid in os.listdir(self.get_path('bugs')): - self._upgrade_bug_mapfile(bug_uuid) - self._upgrade_bugdir_mapfile() - for _bug in self._targets.values(): - _bug.save() - -upgraders = [Upgrade_1_0_to_1_1, - Upgrade_1_1_to_1_2, - Upgrade_1_2_to_1_3] -upgrade_classes = {} -for upgrader in upgraders: - upgrade_classes[(upgrader.initial_version,upgrader.final_version)]=upgrader - -def upgrade(path, current_version, - target_version=BUGDIR_DISK_VERSION): - """ - Call the appropriate upgrade function to convert current_version - to target_version. If a direct conversion function does not exist, - use consecutive conversion functions. - """ - if current_version not in BUGDIR_DISK_VERSIONS: - raise NotImplementedError, \ - "Cannot handle version '%s' yet." % version - if target_version not in BUGDIR_DISK_VERSIONS: - raise NotImplementedError, \ - "Cannot handle version '%s' yet." % version - - if (current_version, target_version) in upgrade_classes: - # direct conversion - upgrade_class = upgrade_classes[(current_version, target_version)] - u = upgrade_class(path) - u.upgrade() - else: - # consecutive single-step conversion - i = BUGDIR_DISK_VERSIONS.index(current_version) - while True: - version_a = BUGDIR_DISK_VERSIONS[i] - version_b = BUGDIR_DISK_VERSIONS[i+1] - try: - upgrade_class = upgrade_classes[(version_a, version_b)] - except KeyError: - raise NotImplementedError, \ - "Cannot convert version '%s' to '%s' yet." \ - % (version_a, version_b) - u = upgrade_class(path) - u.upgrade() - if version_b == target_version: - break - i += 1 - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() diff --git a/libbe/util/beuuid.py b/libbe/util/beuuid.py new file mode 100644 index 0000000..a3a3b6c --- /dev/null +++ b/libbe/util/beuuid.py @@ -0,0 +1,67 @@ +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Backwards compatibility support for Python 2.4. Once people give up +on 2.4 ;), the uuid call should be merged into bugdir.py +""" + +import libbe +if libbe.TESTING == True: + import unittest + + +try: + from uuid import uuid4 # Python >= 2.5 + def uuid_gen(): + id = uuid4() + idstr = id.urn + start = "urn:uuid:" + assert idstr.startswith(start) + return idstr[len(start):] +except ImportError: + import os + import sys + from subprocess import Popen, PIPE + + def uuid_gen(): + # Shell-out to system uuidgen + args = ['uuidgen', 'r'] + try: + if sys.platform != "win32": + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) + else: + # win32 don't have os.execvp() so have to run command in a shell + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, + shell=True, cwd=cwd) + except OSError, e : + strerror = "%s\nwhile executing %s" % (e.args[1], args) + raise OSError, strerror + output, error = q.communicate() + status = q.wait() + if status != 0: + strerror = "%s\nwhile executing %s" % (status, args) + raise Exception, strerror + return output.rstrip('\n') + +if libbe.TESTING == True: + class UUIDtestCase(unittest.TestCase): + def testUUID_gen(self): + id = uuid_gen() + self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) + + suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) diff --git a/libbe/util/encoding.py b/libbe/util/encoding.py new file mode 100644 index 0000000..d09117f --- /dev/null +++ b/libbe/util/encoding.py @@ -0,0 +1,66 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Support input/output/filesystem encodings (e.g. UTF-8). +""" + +import codecs +import locale +import sys + +import libbe +if libbe.TESTING == True: + import doctest + + +ENCODING = None # override get_encoding() output by setting this + +def get_encoding(): + """ + Guess a useful input/output/filesystem encoding... Maybe we need + seperate encodings for input/output and filesystem? Hmm... + """ + if ENCODING != None: + return ENCODING + encoding = locale.getpreferredencoding() or sys.getdefaultencoding() + if sys.platform != 'win32' or sys.version_info[:2] > (2, 3): + encoding = locale.getlocale(locale.LC_TIME)[1] or encoding + # Python 2.3 on windows doesn't know about 'XYZ' alias for 'cpXYZ' + return encoding + +def known_encoding(encoding): + """ + >>> known_encoding("highly-unlikely-encoding") + False + >>> known_encoding(get_encoding()) + True + """ + try: + codecs.lookup(encoding) + return True + except LookupError: + return False + +def set_IO_stream_encodings(encoding): + sys.stdin = codecs.getreader(encoding)(sys.__stdin__) + sys.stdout = codecs.getwriter(encoding)(sys.__stdout__) + sys.stderr = codecs.getwriter(encoding)(sys.__stderr__) + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/util/subproc.py b/libbe/util/subproc.py new file mode 100644 index 0000000..8806e26 --- /dev/null +++ b/libbe/util/subproc.py @@ -0,0 +1,223 @@ +# Copyright (C) 2009 W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Functions for running external commands in subprocesses. +""" + +from subprocess import Popen, PIPE +import sys + +import libbe +from encoding import get_encoding +if libbe.TESTING == True: + import doctest + +_MSWINDOWS = sys.platform == 'win32' +_POSIX = not _MSWINDOWS + +if _POSIX == True: + import os + import select + +class CommandError(Exception): + def __init__(self, command, status, stdout=None, stderr=None): + strerror = ['Command failed (%d):\n %s\n' % (status, stderr), + 'while executing\n %s' % command] + Exception.__init__(self, '\n'.join(strerror)) + self.command = command + self.status = status + self.stdout = stdout + self.stderr = stderr + +def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,), + cwd=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 + unicode_output == True, convert stdout and stdin strings to + unicode before returing them. + """ + if cwd == None: + cwd = '.' + if verbose == True: + print >> sys.stderr, '%s$ %s' % (cwd, ' '.join(args)) + try : + if _POSIX: + q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd) + else: + assert _MSWINDOWS==True, 'invalid platform' + # 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) + except OSError, e: + raise CommandError(args, status=e.args[0], stderr=e) + stdout,stderr = q.communicate(input=stdin) + status = q.wait() + if unicode_output == True: + if encoding == None: + encoding = get_encoding() + if stdout != None: + stdout = unicode(stdout, encoding) + if stderr != None: + stderr = unicode(stderr, encoding) + if verbose == True: + print >> sys.stderr, '%d\n%s%s' % (status, stdout, stderr) + if status not in expect: + raise CommandError(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. + + >>> p = Pipe([['find', '/etc/'], ['grep', '^/etc/ssh$']]) + >>> p.stdout + '/etc/ssh\\n' + >>> p.status + 1 + >>> p.statuses + [1, 0] + >>> p.stderrs # doctest: +ELLIPSIS + [...find: ...: Permission denied..., ''] + """ + def __init__(self, cmds, stdin=None): + # spawn processes + self._procs = [] + for cmd in cmds: + if len(self._procs) != 0: + stdin = self._procs[-1].stdout + self._procs.append(Popen(cmd, stdin=stdin, stdout=PIPE, stderr=PIPE)) + + self.stdout,self.stderrs = self._communicate(input=None) + + # collect process statuses + self.statuses = [] + self.status = 0 + for proc in self._procs: + self.statuses.append(proc.wait()) + if self.statuses[-1] != 0: + self.status = self.statuses[-1] + + # Code excerpted from subprocess.Popen._communicate() + if _MSWINDOWS == True: + def _communicate(self, input=None): + assert input == None, 'stdin != None not yet supported' + # listen to each process' stderr + threads = [] + std_X_arrays = [] + for proc in self._procs: + stderr_array = [] + thread = Thread(target=proc._readerthread, + args=(proc.stderr, stderr_array)) + thread.setDaemon(True) + thread.start() + threads.append(thread) + std_X_arrays.append(stderr_array) + + # also listen to the last processes stdout + stdout_array = [] + thread = Thread(target=proc._readerthread, + args=(proc.stdout, stdout_array)) + thread.setDaemon(True) + thread.start() + threads.append(thread) + std_X_arrays.append(stdout_array) + + # join threads as they die + for thread in threads: + thread.join() + + # read output from reader threads + std_X_strings = [] + for std_X_array in std_X_arrays: + std_X_strings.append(std_X_array[0]) + + stdout = std_X_strings.pop(-1) + stderrs = std_X_strings + return (stdout, stderrs) + else: + assert _POSIX==True, 'invalid platform' + def _communicate(self, input=None): + read_set = [] + write_set = [] + read_arrays = [] + stdout = None # Return + stderr = None # Return + + if self._procs[0].stdin: + # Flush stdio buffer. This might block, if the user has + # been writing to .stdin in an uncontrolled fashion. + self._procs[0].stdin.flush() + if input: + write_set.append(self._procs[0].stdin) + else: + self._procs[0].stdin.close() + for proc in self._procs: + read_set.append(proc.stderr) + read_arrays.append([]) + read_set.append(self._procs[-1].stdout) + read_arrays.append([]) + + input_offset = 0 + while read_set or write_set: + try: + rlist, wlist, xlist = select.select(read_set, write_set, []) + except select.error, e: + if e.args[0] == errno.EINTR: + continue + raise + if self._procs[0].stdin in wlist: + # When select has indicated that the file is writable, + # we can write up to PIPE_BUF bytes without risk + # blocking. POSIX defines PIPE_BUF >= 512 + chunk = input[input_offset : input_offset + 512] + bytes_written = os.write(self.stdin.fileno(), chunk) + input_offset += bytes_written + if input_offset >= len(input): + self._procs[0].stdin.close() + write_set.remove(self._procs[0].stdin) + if self._procs[-1].stdout in rlist: + data = os.read(self._procs[-1].stdout.fileno(), 1024) + if data == '': + self._procs[-1].stdout.close() + read_set.remove(self._procs[-1].stdout) + read_arrays[-1].append(data) + for i,proc in enumerate(self._procs): + if proc.stderr in rlist: + data = os.read(proc.stderr.fileno(), 1024) + if data == '': + proc.stderr.close() + read_set.remove(proc.stderr) + read_arrays[i].append(data) + + # All data exchanged. Translate lists into strings. + read_strings = [] + for read_array in read_arrays: + read_strings.append(''.join(read_array)) + + stdout = read_strings.pop(-1) + stderrs = read_strings + return (stdout, stderrs) + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/util/tree.py b/libbe/util/tree.py new file mode 100644 index 0000000..1daac44 --- /dev/null +++ b/libbe/util/tree.py @@ -0,0 +1,193 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Define a traversable tree structure. +""" + +import libbe +if libbe.TESTING == True: + import doctest + +class Tree(list): + """ + Construct + +-b---d-g + a-+ +-e + +-c-+-f-h-i + with + >>> i = Tree(); i.n = "i" + >>> h = Tree([i]); h.n = "h" + >>> f = Tree([h]); f.n = "f" + >>> e = Tree(); e.n = "e" + >>> c = Tree([f,e]); c.n = "c" + >>> g = Tree(); g.n = "g" + >>> d = Tree([g]); d.n = "d" + >>> b = Tree([d]); b.n = "b" + >>> a = Tree(); a.n = "a" + >>> a.append(c) + >>> a.append(b) + + >>> a.branch_len() + 5 + >>> a.sort(key=lambda node : -node.branch_len()) + >>> "".join([node.n for node in a.traverse()]) + 'acfhiebdg' + >>> a.sort(key=lambda node : node.branch_len()) + >>> "".join([node.n for node in a.traverse()]) + 'abdgcefhi' + >>> "".join([node.n for node in a.traverse(depth_first=False)]) + 'abcdefghi' + >>> for depth,node in a.thread(): + ... print "%*s" % (2*depth+1, node.n) + a + b + d + g + c + e + f + h + i + >>> for depth,node in a.thread(flatten=True): + ... print "%*s" % (2*depth+1, node.n) + a + b + d + g + c + e + f + h + i + >>> a.has_descendant(g) + True + >>> c.has_descendant(g) + False + >>> a.has_descendant(a) + False + >>> a.has_descendant(a, match_self=True) + True + """ + def __cmp__(self, other): + return cmp(id(self), id(other)) + + def __eq__(self, other): + return self.__cmp__(other) == 0 + + def __ne__(self, other): + return self.__cmp__(other) != 0 + + def branch_len(self): + """ + Exhaustive search every time == SLOW. + + Use only on small trees, or reimplement by overriding + child-addition methods to allow accurate caching. + + For the tree + +-b---d-g + a-+ +-e + +-c-+-f-h-i + this method returns 5. + """ + if len(self) == 0: + return 1 + else: + return 1 + max([child.branch_len() for child in self]) + + def sort(self, *args, **kwargs): + """ + This method can be slow, e.g. on a branch_len() sort, since a + node at depth N from the root has it's branch_len() method + called N times. + """ + list.sort(self, *args, **kwargs) + for child in self: + child.sort(*args, **kwargs) + + def traverse(self, depth_first=True): + """ + Note: you might want to sort() your tree first. + """ + if depth_first == True: + yield self + for child in self: + for descendant in child.traverse(): + yield descendant + else: # breadth first, Wikipedia algorithm + # http://en.wikipedia.org/wiki/Breadth-first_search + queue = [self] + while len(queue) > 0: + node = queue.pop(0) + yield node + queue.extend(node) + + def thread(self, flatten=False): + """ + When flatten==False, the depth of any node is one greater than + the depth of its parent. That way the inheritance is + explicit, but you can end up with highly indented threads. + + When flatten==True, the depth of any node is only greater than + the depth of its parent when there is a branch, and the node + is not the last child. This can lead to ancestry ambiguity, + but keeps the total indentation down. E.g. + +-b +-b-c + a-+-c and a-+ + +-d-e-f +-d-e-f + would both produce (after sorting by branch_len()) + (0, a) + (1, b) + (1, c) + (0, d) + (0, e) + (0, f) + """ + stack = [] # ancestry of the current node + if flatten == True: + depthDict = {} + + for node in self.traverse(depth_first=True): + while len(stack) > 0 \ + and id(node) not in [id(c) for c in stack[-1]]: + stack.pop(-1) + if flatten == False: + depth = len(stack) + else: + if len(stack) == 0: + depth = 0 + else: + parent = stack[-1] + depth = depthDict[id(parent)] + if len(parent) > 1 and node != parent[-1]: + depth += 1 + depthDict[id(node)] = depth + yield (depth,node) + stack.append(node) + + def has_descendant(self, descendant, depth_first=True, match_self=False): + if descendant == self: + return match_self + for d in self.traverse(depth_first): + if descendant == d: + return True + return False + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/util/utility.py b/libbe/util/utility.py new file mode 100644 index 0000000..f954422 --- /dev/null +++ b/libbe/util/utility.py @@ -0,0 +1,151 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Assorted utility functions that don't fit in anywhere else. +""" + +import calendar +import codecs +import os +import shutil +import tempfile +import time +import types + +import libbe +if libbe.TESTING == True: + import doctest + +class InvalidXML(ValueError): + """ + Invalid XML while parsing for a *.from_xml() method. + type - string identifying *, e.g. "bug", "comment", ... + element - ElementTree.Element instance which caused the error + error - string describing the error + """ + def __init__(self, type, element, error): + msg = 'Invalid %s xml: %s\n %s\n' \ + % (type, error, ElementTree.tostring(element)) + ValueError.__init__(self, msg) + self.type = type + self.element = element + self.error = error + +def search_parent_directories(path, filename): + """ + Find the file (or directory) named filename in path or in any + of path's parents. + + e.g. + search_parent_directories("/a/b/c", ".be") + will return the path to the first existing file from + /a/b/c/.be + /a/b/.be + /a/.be + /.be + or None if none of those files exist. + """ + path = os.path.realpath(path) + assert os.path.exists(path) + old_path = None + while True: + check_path = os.path.join(path, filename) + if os.path.exists(check_path): + return check_path + if path == old_path: + return None + old_path = path + path = os.path.dirname(path) + +class Dir (object): + "A temporary directory for testing use" + def __init__(self): + self.path = tempfile.mkdtemp(prefix="BEtest") + self.removed = False + def cleanup(self): + if self.removed == False: + shutil.rmtree(self.path) + self.removed = True + def __call__(self): + return self.path + +RFC_2822_TIME_FMT = "%a, %d %b %Y %H:%M:%S +0000" + + +def time_to_str(time_val): + """Convert a time value into an RFC 2822-formatted string. This format + lacks sub-second data. + >>> time_to_str(0) + 'Thu, 01 Jan 1970 00:00:00 +0000' + """ + return time.strftime(RFC_2822_TIME_FMT, time.gmtime(time_val)) + +def str_to_time(str_time): + """Convert an RFC 2822-fomatted string into a time value. + >>> str_to_time("Thu, 01 Jan 1970 00:00:00 +0000") + 0 + >>> q = time.time() + >>> str_to_time(time_to_str(q)) == int(q) + True + >>> str_to_time("Thu, 01 Jan 1970 00:00:00 -1000") + 36000 + """ + timezone_str = str_time[-5:] + if timezone_str != "+0000": + str_time = str_time.replace(timezone_str, "+0000") + time_val = calendar.timegm(time.strptime(str_time, RFC_2822_TIME_FMT)) + timesign = -int(timezone_str[0]+"1") # "+" -> time_val ahead of GMT + timezone_tuple = time.strptime(timezone_str[1:], "%H%M") + timezone = timezone_tuple.tm_hour*3600 + timezone_tuple.tm_min*60 + return time_val + timesign*timezone + +def handy_time(time_val): + return time.strftime("%a, %d %b %Y %H:%M", time.localtime(time_val)) + +def time_to_gmtime(str_time): + """Convert an RFC 2822-fomatted string to a GMT string. + >>> time_to_gmtime("Thu, 01 Jan 1970 00:00:00 -1000") + 'Thu, 01 Jan 1970 10:00:00 +0000' + """ + time_val = str_to_time(str_time) + return time_to_str(time_val) + +def iterable_full_of_strings(value, alternative=None): + """ + Require an iterable full of strings. + >>> iterable_full_of_strings([]) + True + >>> iterable_full_of_strings(["abc", "def", u"hij"]) + True + >>> iterable_full_of_strings(["abc", None, u"hij"]) + False + >>> iterable_full_of_strings(None, alternative=None) + True + """ + if value == alternative: + return True + elif not hasattr(value, "__iter__"): + return False + for x in value: + if type(x) not in types.StringTypes: + return False + return True + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/utility.py b/libbe/utility.py deleted file mode 100644 index f954422..0000000 --- a/libbe/utility.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Assorted utility functions that don't fit in anywhere else. -""" - -import calendar -import codecs -import os -import shutil -import tempfile -import time -import types - -import libbe -if libbe.TESTING == True: - import doctest - -class InvalidXML(ValueError): - """ - Invalid XML while parsing for a *.from_xml() method. - type - string identifying *, e.g. "bug", "comment", ... - element - ElementTree.Element instance which caused the error - error - string describing the error - """ - def __init__(self, type, element, error): - msg = 'Invalid %s xml: %s\n %s\n' \ - % (type, error, ElementTree.tostring(element)) - ValueError.__init__(self, msg) - self.type = type - self.element = element - self.error = error - -def search_parent_directories(path, filename): - """ - Find the file (or directory) named filename in path or in any - of path's parents. - - e.g. - search_parent_directories("/a/b/c", ".be") - will return the path to the first existing file from - /a/b/c/.be - /a/b/.be - /a/.be - /.be - or None if none of those files exist. - """ - path = os.path.realpath(path) - assert os.path.exists(path) - old_path = None - while True: - check_path = os.path.join(path, filename) - if os.path.exists(check_path): - return check_path - if path == old_path: - return None - old_path = path - path = os.path.dirname(path) - -class Dir (object): - "A temporary directory for testing use" - def __init__(self): - self.path = tempfile.mkdtemp(prefix="BEtest") - self.removed = False - def cleanup(self): - if self.removed == False: - shutil.rmtree(self.path) - self.removed = True - def __call__(self): - return self.path - -RFC_2822_TIME_FMT = "%a, %d %b %Y %H:%M:%S +0000" - - -def time_to_str(time_val): - """Convert a time value into an RFC 2822-formatted string. This format - lacks sub-second data. - >>> time_to_str(0) - 'Thu, 01 Jan 1970 00:00:00 +0000' - """ - return time.strftime(RFC_2822_TIME_FMT, time.gmtime(time_val)) - -def str_to_time(str_time): - """Convert an RFC 2822-fomatted string into a time value. - >>> str_to_time("Thu, 01 Jan 1970 00:00:00 +0000") - 0 - >>> q = time.time() - >>> str_to_time(time_to_str(q)) == int(q) - True - >>> str_to_time("Thu, 01 Jan 1970 00:00:00 -1000") - 36000 - """ - timezone_str = str_time[-5:] - if timezone_str != "+0000": - str_time = str_time.replace(timezone_str, "+0000") - time_val = calendar.timegm(time.strptime(str_time, RFC_2822_TIME_FMT)) - timesign = -int(timezone_str[0]+"1") # "+" -> time_val ahead of GMT - timezone_tuple = time.strptime(timezone_str[1:], "%H%M") - timezone = timezone_tuple.tm_hour*3600 + timezone_tuple.tm_min*60 - return time_val + timesign*timezone - -def handy_time(time_val): - return time.strftime("%a, %d %b %Y %H:%M", time.localtime(time_val)) - -def time_to_gmtime(str_time): - """Convert an RFC 2822-fomatted string to a GMT string. - >>> time_to_gmtime("Thu, 01 Jan 1970 00:00:00 -1000") - 'Thu, 01 Jan 1970 10:00:00 +0000' - """ - time_val = str_to_time(str_time) - return time_to_str(time_val) - -def iterable_full_of_strings(value, alternative=None): - """ - Require an iterable full of strings. - >>> iterable_full_of_strings([]) - True - >>> iterable_full_of_strings(["abc", "def", u"hij"]) - True - >>> iterable_full_of_strings(["abc", None, u"hij"]) - False - >>> iterable_full_of_strings(None, alternative=None) - True - """ - if value == alternative: - return True - elif not hasattr(value, "__iter__"): - return False - for x in value: - if type(x) not in types.StringTypes: - return False - return True - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() diff --git a/libbe/vcs.py b/libbe/vcs.py deleted file mode 100644 index 44643a4..0000000 --- a/libbe/vcs.py +++ /dev/null @@ -1,941 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Alexander Belchenko -# Ben Finney -# Chris Ball -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Define the base VCS (Version Control System) class, which should be -subclassed by other Version Control System backends. The base class -implements a "do not version" VCS. -""" - -import codecs -import os -import os.path -import re -from socket import gethostname -import shutil -import sys -import tempfile - -import libbe -from utility import Dir, search_parent_directories -from subproc import CommandError, invoke -from plugin import get_plugin - -if libbe.TESTING == True: - import unittest - import doctest - - -# List VCS modules in order of preference. -# Don't list this module, it is implicitly last. -VCS_ORDER = ['arch', 'bzr', 'darcs', 'git', 'hg'] - -def set_preferred_vcs(name): - global VCS_ORDER - assert name in VCS_ORDER, \ - 'unrecognized VCS %s not in\n %s' % (name, VCS_ORDER) - VCS_ORDER.remove(name) - VCS_ORDER.insert(0, name) - -def _get_matching_vcs(matchfn): - """Return the first module for which matchfn(VCS_instance) is true""" - for submodname in VCS_ORDER: - module = get_plugin('libbe', submodname) - vcs = module.new() - if matchfn(vcs) == True: - return vcs - vcs.cleanup() - return VCS() - -def vcs_by_name(vcs_name): - """Return the module for the VCS with the given name""" - return _get_matching_vcs(lambda vcs: vcs.name == vcs_name) - -def detect_vcs(dir): - """Return an VCS instance for the vcs being used in this directory""" - return _get_matching_vcs(lambda vcs: vcs.detect(dir)) - -def installed_vcs(): - """Return an instance of an installed VCS""" - return _get_matching_vcs(lambda vcs: vcs.installed()) - - - -class SettingIDnotSupported(NotImplementedError): - pass - -class VCSnotRooted(Exception): - def __init__(self): - msg = "VCS not rooted" - Exception.__init__(self, msg) - -class PathNotInRoot(Exception): - def __init__(self, path, root): - msg = "Path '%s' not in root '%s'" % (path, root) - Exception.__init__(self, msg) - self.path = path - self.root = root - -class NoSuchFile(Exception): - def __init__(self, pathname, root="."): - path = os.path.abspath(os.path.join(root, pathname)) - Exception.__init__(self, "No such file: %s" % path) - -class EmptyCommit(Exception): - def __init__(self): - Exception.__init__(self, "No changes to commit") - - -def new(): - return VCS() - -class VCS(object): - """ - This class implements a 'no-vcs' interface. - - Support for other VCSs can be added by subclassing this class, and - overriding methods _vcs_*() with code appropriate for your VCS. - - The methods _u_*() are utility methods available to the _vcs_*() - methods. - """ - name = "None" - client = "" # command-line tool for _u_invoke_client - versioned = False - def __init__(self, paranoid=False, encoding=sys.getdefaultencoding()): - self.paranoid = paranoid - self.verboseInvoke = False - self.rootdir = None - self._duplicateBasedir = None - self._duplicateDirname = None - self.encoding = encoding - def __str__(self): - return "<%s %s>" % (self.__class__.__name__, id(self)) - def __repr__(self): - return str(self) - def _vcs_version(self): - """ - Return the VCS version string. - """ - return "0.0" - def _vcs_detect(self, path=None): - """ - Detect whether a directory is revision controlled with this VCS. - """ - return True - def _vcs_root(self, path): - """ - Get the VCS root. This is the default working directory for - future invocations. You would normally set this to the root - directory for your VCS. - """ - if os.path.isdir(path)==False: - path = os.path.dirname(path) - if path == "": - path = os.path.abspath(".") - return path - def _vcs_init(self, path): - """ - Begin versioning the tree based at path. - """ - pass - def _vcs_cleanup(self): - """ - Remove any cruft that _vcs_init() created outside of the - versioned tree. - """ - pass - def _vcs_get_user_id(self): - """ - Get the VCS's suggested user id (e.g. "John Doe "). - If the VCS has not been configured with a username, return None. - """ - return None - def _vcs_set_user_id(self, value): - """ - Set the VCS's suggested user id (e.g "John Doe "). - This is run if the VCS has not been configured with a usename, so - that commits will have a reasonable FROM value. - """ - raise SettingIDnotSupported - def _vcs_add(self, path): - """ - Add the already created file at path to version control. - """ - pass - def _vcs_remove(self, path): - """ - Remove the file at path from version control. Optionally - remove the file from the filesystem as well. - """ - pass - def _vcs_update(self, path): - """ - Notify the versioning system of changes to the versioned file - at path. - """ - pass - def _vcs_get_file_contents(self, path, revision=None, binary=False): - """ - Get the file contents as they were in a given revision. - Revision==None specifies the current revision. - """ - assert revision == None, \ - "The %s VCS does not support revision specifiers" % self.name - if binary == False: - f = codecs.open(os.path.join(self.rootdir, path), "r", self.encoding) - else: - f = open(os.path.join(self.rootdir, path), "rb") - contents = f.read() - f.close() - return contents - def _vcs_duplicate_repo(self, directory, revision=None): - """ - Get the repository as it was in a given revision. - revision==None specifies the current revision. - dir specifies a directory to create the duplicate in. - """ - shutil.copytree(self.rootdir, directory, True) - def _vcs_commit(self, commitfile, allow_empty=False): - """ - Commit the current working directory, using the contents of - commitfile as the comment. Return the name of the old - revision (or None if commits are not supported). - - If allow_empty == False, raise EmptyCommit if there are no - changes to commit. - """ - return None - def _vcs_revision_id(self, index): - """ - Return the name of the th revision. Index will be an - integer (possibly <= 0). The choice of which branch to follow - when crossing branches/merges is not defined. - - Return None if revision IDs are not supported, or if the - specified revision does not exist. - """ - return None - def version(self): - """Cache version string for efficiency.""" - if not hasattr(self, '_version'): - self._version = self._get_version() - return self._version - def _get_version(self): - try: - ret = self._vcs_version() - return ret - except OSError, e: - if e.errno == errno.ENOENT: - return None - else: - raise OSError, e - except CommandError: - return None - def installed(self): - if self.version() != None: - return True - return False - def detect(self, path="."): - """ - Detect whether a directory is revision controlled with this VCS. - """ - return self._vcs_detect(path) - def root(self, path): - """ - Set the root directory to the path's VCS root. This is the - default working directory for future invocations. - """ - self.rootdir = self._vcs_root(path) - def init(self, path): - """ - Begin versioning the tree based at path. - Also roots the vcs at path. - """ - if os.path.isdir(path)==False: - path = os.path.dirname(path) - self._vcs_init(path) - self.root(path) - def cleanup(self): - self._vcs_cleanup() - def get_user_id(self): - """ - Get the VCS's suggested user id (e.g. "John Doe "). - If the VCS has not been configured with a username, return the user's - id. You can override the automatic lookup procedure by setting the - VCS.user_id attribute to a string of your choice. - """ - if hasattr(self, "user_id"): - if self.user_id != None: - return self.user_id - id = self._vcs_get_user_id() - if id == None: - name = self._u_get_fallback_username() - email = self._u_get_fallback_email() - id = self._u_create_id(name, email) - print >> sys.stderr, "Guessing id '%s'" % id - try: - self.set_user_id(id) - except SettingIDnotSupported: - pass - return id - def set_user_id(self, value): - """ - Set the VCS's suggested user id (e.g "John Doe "). - This is run if the VCS has not been configured with a usename, so - that commits will have a reasonable FROM value. - """ - self._vcs_set_user_id(value) - def add(self, path): - """ - Add the already created file at path to version control. - """ - self._vcs_add(self._u_rel_path(path)) - def remove(self, path): - """ - Remove a file from both version control and the filesystem. - """ - self._vcs_remove(self._u_rel_path(path)) - if os.path.exists(path): - os.remove(path) - def recursive_remove(self, dirname): - """ - Remove a file/directory and all its decendents from both - version control and the filesystem. - """ - if not os.path.exists(dirname): - raise NoSuchFile(dirname) - for dirpath,dirnames,filenames in os.walk(dirname, topdown=False): - filenames.extend(dirnames) - for path in filenames: - fullpath = os.path.join(dirpath, path) - if os.path.exists(fullpath) == False: - continue - self._vcs_remove(self._u_rel_path(fullpath)) - if os.path.exists(dirname): - shutil.rmtree(dirname) - def update(self, path): - """ - Notify the versioning system of changes to the versioned file - at path. - """ - self._vcs_update(self._u_rel_path(path)) - def get_file_contents(self, path, revision=None, allow_no_vcs=False, binary=False): - """ - Get the file as it was in a given revision. - Revision==None specifies the current revision. - - allow_no_vcs==True allows direct access to files through - codecs.open() or open() if the vcs decides it can't handle the - given path. - """ - if not os.path.exists(path): - raise NoSuchFile(path) - if self._use_vcs(path, allow_no_vcs): - relpath = self._u_rel_path(path) - contents = self._vcs_get_file_contents(relpath,revision,binary=binary) - else: - if binary == True: - f = codecs.open(path, "r", self.encoding) - else: - f = open(path, "rb") - contents = f.read() - f.close() - return contents - def set_file_contents(self, path, contents, allow_no_vcs=False, binary=False): - """ - Set the file contents under version control. - """ - add = not os.path.exists(path) - if binary == False: - f = codecs.open(path, "w", self.encoding) - else: - f = open(path, "wb") - f.write(contents) - f.close() - - if self._use_vcs(path, allow_no_vcs): - if add: - self.add(path) - else: - self.update(path) - def mkdir(self, path, allow_no_vcs=False, check_parents=True): - """ - Create (if neccessary) a directory at path under version - control. - """ - if check_parents == True: - parent = os.path.dirname(path) - if not os.path.exists(parent): # recurse through parents - self.mkdir(parent, allow_no_vcs, check_parents) - if not os.path.exists(path): - os.mkdir(path) - if self._use_vcs(path, allow_no_vcs): - self.add(path) - else: - assert os.path.isdir(path) - if self._use_vcs(path, allow_no_vcs): - #self.update(path)# Don't update directories. Changing files - pass # underneath them should be sufficient. - - def duplicate_repo(self, revision=None): - """ - Get the repository as it was in a given revision. - revision==None specifies the current revision. - Return the path to the arbitrary directory at the base of the new repo. - """ - # Dirname in Basedir to protect against simlink attacks. - if self._duplicateBasedir == None: - self._duplicateBasedir = tempfile.mkdtemp(prefix='BEvcs') - self._duplicateDirname = \ - os.path.join(self._duplicateBasedir, "duplicate") - self._vcs_duplicate_repo(directory=self._duplicateDirname, - revision=revision) - return self._duplicateDirname - def remove_duplicate_repo(self): - """ - Clean up a duplicate repo created with duplicate_repo(). - """ - if self._duplicateBasedir != None: - shutil.rmtree(self._duplicateBasedir) - self._duplicateBasedir = None - self._duplicateDirname = None - def commit(self, summary, body=None, allow_empty=False): - """ - Commit the current working directory, with a commit message - string summary and body. Return the name of the old revision - (or None if versioning is not supported). - - If allow_empty == False (the default), raise EmptyCommit if - there are no changes to commit. - """ - summary = summary.strip()+'\n' - if body is not None: - summary += '\n' + body.strip() + '\n' - descriptor, filename = tempfile.mkstemp() - revision = None - try: - temp_file = os.fdopen(descriptor, 'wb') - temp_file.write(summary) - temp_file.flush() - self.precommit() - revision = self._vcs_commit(filename, allow_empty=allow_empty) - temp_file.close() - self.postcommit() - finally: - os.remove(filename) - return revision - def precommit(self): - """ - Executed before all attempted commits. - """ - pass - def postcommit(self): - """ - Only executed after successful commits. - """ - pass - def revision_id(self, index=None): - """ - Return the name of the th revision. The choice of - which branch to follow when crossing branches/merges is not - defined. - - Return None if index==None, revision IDs are not supported, or - if the specified revision does not exist. - """ - if index == None: - return None - return self._vcs_revision_id(index) - def _u_any_in_string(self, list, string): - """ - Return True if any of the strings in list are in string. - Otherwise return False. - """ - for list_string in list: - if list_string in string: - return True - return False - def _u_invoke(self, *args, **kwargs): - if 'cwd' not in kwargs: - kwargs['cwd'] = self.rootdir - if 'verbose' not in kwargs: - kwargs['verbose'] = self.verboseInvoke - if 'encoding' not in kwargs: - kwargs['encoding'] = self.encoding - return invoke(*args, **kwargs) - def _u_invoke_client(self, *args, **kwargs): - cl_args = [self.client] - cl_args.extend(args) - return self._u_invoke(cl_args, **kwargs) - def _u_search_parent_directories(self, path, filename): - """ - Find the file (or directory) named filename in path or in any - of path's parents. - - e.g. - search_parent_directories("/a/b/c", ".be") - will return the path to the first existing file from - /a/b/c/.be - /a/b/.be - /a/.be - /.be - or None if none of those files exist. - """ - return search_parent_directories(path, filename) - def _use_vcs(self, path, allow_no_vcs): - """ - Try and decide if _vcs_add/update/mkdir/etc calls will - succeed. Returns True is we think the vcs_call would - succeeed, and False otherwise. - """ - use_vcs = True - exception = None - if self.rootdir != None: - if self.path_in_root(path) == False: - use_vcs = False - exception = PathNotInRoot(path, self.rootdir) - else: - use_vcs = False - exception = VCSnotRooted - if use_vcs == False and allow_no_vcs==False: - raise exception - return use_vcs - def path_in_root(self, path, root=None): - """ - Return the relative path to path from root. - >>> vcs = new() - >>> vcs.path_in_root("/a.b/c/.be", "/a.b/c") - True - >>> vcs.path_in_root("/a.b/.be", "/a.b/c") - False - """ - if root == None: - if self.rootdir == None: - raise VCSnotRooted - root = self.rootdir - path = os.path.abspath(path) - absRoot = os.path.abspath(root) - absRootSlashedDir = os.path.join(absRoot,"") - if not path.startswith(absRootSlashedDir): - return False - return True - def _u_rel_path(self, path, root=None): - """ - Return the relative path to path from root. - >>> vcs = new() - >>> vcs._u_rel_path("/a.b/c/.be", "/a.b/c") - '.be' - """ - if root == None: - if self.rootdir == None: - raise VCSnotRooted - root = self.rootdir - path = os.path.abspath(path) - absRoot = os.path.abspath(root) - absRootSlashedDir = os.path.join(absRoot,"") - if not path.startswith(absRootSlashedDir): - raise PathNotInRoot(path, absRootSlashedDir) - assert path != absRootSlashedDir, \ - "file %s == root directory %s" % (path, absRootSlashedDir) - relpath = path[len(absRootSlashedDir):] - return relpath - def _u_abspath(self, path, root=None): - """ - Return the absolute path from a path realtive to root. - >>> vcs = new() - >>> vcs._u_abspath(".be", "/a.b/c") - '/a.b/c/.be' - """ - if root == None: - assert self.rootdir != None, "VCS not rooted" - root = self.rootdir - return os.path.abspath(os.path.join(root, path)) - def _u_create_id(self, name, email=None): - """ - >>> vcs = new() - >>> vcs._u_create_id("John Doe", "jdoe@example.com") - 'John Doe ' - >>> vcs._u_create_id("John Doe") - 'John Doe' - """ - assert len(name) > 0 - if email == None or len(email) == 0: - return name - else: - return "%s <%s>" % (name, email) - def _u_parse_id(self, value): - """ - >>> vcs = new() - >>> vcs._u_parse_id("John Doe ") - ('John Doe', 'jdoe@example.com') - >>> vcs._u_parse_id("John Doe") - ('John Doe', None) - >>> try: - ... vcs._u_parse_id("John Doe ") - ... except AssertionError: - ... print "Invalid match" - Invalid match - """ - emailexp = re.compile("(.*) <([^>]*)>(.*)") - match = emailexp.search(value) - if match == None: - email = None - name = value - else: - assert len(match.groups()) == 3 - assert match.groups()[2] == "", match.groups() - email = match.groups()[1] - name = match.groups()[0] - assert name != None - assert len(name) > 0 - return (name, email) - def _u_get_fallback_username(self): - name = None - for envariable in ["LOGNAME", "USERNAME"]: - if os.environ.has_key(envariable): - name = os.environ[envariable] - break - assert name != None - return name - def _u_get_fallback_email(self): - hostname = gethostname() - name = self._u_get_fallback_username() - return "%s@%s" % (name, hostname) - def _u_parse_commitfile(self, commitfile): - """ - Split the commitfile created in self.commit() back into - summary and header lines. - """ - f = codecs.open(commitfile, "r", self.encoding) - summary = f.readline() - body = f.read() - body.lstrip('\n') - if len(body) == 0: - body = None - f.close() - return (summary, body) - - -if libbe.TESTING == True: - def setup_vcs_test_fixtures(testcase): - """Set up test fixtures for VCS test case.""" - testcase.vcs = testcase.Class() - testcase.dir = Dir() - testcase.dirname = testcase.dir.path - - vcs_not_supporting_uninitialized_user_id = [] - vcs_not_supporting_set_user_id = ["None", "hg"] - testcase.vcs_supports_uninitialized_user_id = ( - testcase.vcs.name not in vcs_not_supporting_uninitialized_user_id) - testcase.vcs_supports_set_user_id = ( - testcase.vcs.name not in vcs_not_supporting_set_user_id) - - if not testcase.vcs.installed(): - testcase.fail( - "%(name)s VCS not found" % vars(testcase.Class)) - - if testcase.Class.name != "None": - testcase.failIf( - testcase.vcs.detect(testcase.dirname), - "Detected %(name)s VCS before initialising" - % vars(testcase.Class)) - - testcase.vcs.init(testcase.dirname) - - class VCSTestCase(unittest.TestCase): - """Test cases for base VCS class.""" - - Class = VCS - - def __init__(self, *args, **kwargs): - super(VCSTestCase, self).__init__(*args, **kwargs) - self.dirname = None - - def setUp(self): - super(VCSTestCase, self).setUp() - setup_vcs_test_fixtures(self) - - def tearDown(self): - self.vcs.cleanup() - self.dir.cleanup() - super(VCSTestCase, self).tearDown() - - def full_path(self, rel_path): - return os.path.join(self.dirname, rel_path) - - - class VCS_init_TestCase(VCSTestCase): - """Test cases for VCS.init method.""" - - def test_detect_should_succeed_after_init(self): - """Should detect VCS in directory after initialization.""" - self.failUnless( - self.vcs.detect(self.dirname), - "Did not detect %(name)s VCS after initialising" - % vars(self.Class)) - - def test_vcs_rootdir_in_specified_root_path(self): - """VCS root directory should be in specified root path.""" - rp = os.path.realpath(self.vcs.rootdir) - dp = os.path.realpath(self.dirname) - vcs_name = self.Class.name - self.failUnless( - dp == rp or rp == None, - "%(vcs_name)s VCS root in wrong dir (%(dp)s %(rp)s)" % vars()) - - - class VCS_get_user_id_TestCase(VCSTestCase): - """Test cases for VCS.get_user_id method.""" - - def test_gets_existing_user_id(self): - """Should get the existing user ID.""" - if not self.vcs_supports_uninitialized_user_id: - return - - user_id = self.vcs.get_user_id() - self.failUnless( - user_id is not None, - "unable to get a user id") - - - class VCS_set_user_id_TestCase(VCSTestCase): - """Test cases for VCS.set_user_id method.""" - - def setUp(self): - super(VCS_set_user_id_TestCase, self).setUp() - - if self.vcs_supports_uninitialized_user_id: - self.prev_user_id = self.vcs.get_user_id() - else: - self.prev_user_id = "Uninitialized identity " - - if self.vcs_supports_set_user_id: - self.test_new_user_id = "John Doe " - self.vcs.set_user_id(self.test_new_user_id) - - def tearDown(self): - if self.vcs_supports_set_user_id: - self.vcs.set_user_id(self.prev_user_id) - super(VCS_set_user_id_TestCase, self).tearDown() - - def test_raises_error_in_unsupported_vcs(self): - """Should raise an error in a VCS that doesn't support it.""" - if self.vcs_supports_set_user_id: - return - self.assertRaises( - SettingIDnotSupported, - self.vcs.set_user_id, "foo") - - def test_updates_user_id_in_supporting_vcs(self): - """Should update the user ID in an VCS that supports it.""" - if not self.vcs_supports_set_user_id: - return - user_id = self.vcs.get_user_id() - self.failUnlessEqual( - self.test_new_user_id, user_id, - "user id not set correctly (expected %s, got %s)" - % (self.test_new_user_id, user_id)) - - - def setup_vcs_revision_test_fixtures(testcase): - """Set up revision test fixtures for VCS test case.""" - testcase.test_dirs = ['a', 'a/b', 'c'] - for path in testcase.test_dirs: - testcase.vcs.mkdir(testcase.full_path(path)) - - testcase.test_files = ['a/text', 'a/b/text'] - - testcase.test_contents = { - 'rev_1': "Lorem ipsum", - 'uncommitted': "dolor sit amet", - } - - - class VCS_mkdir_TestCase(VCSTestCase): - """Test cases for VCS.mkdir method.""" - - def setUp(self): - super(VCS_mkdir_TestCase, self).setUp() - setup_vcs_revision_test_fixtures(self) - - def tearDown(self): - for path in reversed(sorted(self.test_dirs)): - self.vcs.recursive_remove(self.full_path(path)) - super(VCS_mkdir_TestCase, self).tearDown() - - def test_mkdir_creates_directory(self): - """Should create specified directory in filesystem.""" - for path in self.test_dirs: - full_path = self.full_path(path) - self.failUnless( - os.path.exists(full_path), - "path %(full_path)s does not exist" % vars()) - - - class VCS_commit_TestCase(VCSTestCase): - """Test cases for VCS.commit method.""" - - def setUp(self): - super(VCS_commit_TestCase, self).setUp() - setup_vcs_revision_test_fixtures(self) - - def tearDown(self): - for path in reversed(sorted(self.test_dirs)): - self.vcs.recursive_remove(self.full_path(path)) - super(VCS_commit_TestCase, self).tearDown() - - def test_file_contents_as_specified(self): - """Should set file contents as specified.""" - test_contents = self.test_contents['rev_1'] - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents(full_path, test_contents) - current_contents = self.vcs.get_file_contents(full_path) - self.failUnlessEqual(test_contents, current_contents) - - def test_file_contents_as_committed(self): - """Should have file contents as specified after commit.""" - test_contents = self.test_contents['rev_1'] - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents(full_path, test_contents) - revision = self.vcs.commit("Initial file contents.") - current_contents = self.vcs.get_file_contents(full_path) - self.failUnlessEqual(test_contents, current_contents) - - def test_file_contents_as_set_when_uncommitted(self): - """Should set file contents as specified after commit.""" - if not self.vcs.versioned: - return - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents( - full_path, self.test_contents['rev_1']) - revision = self.vcs.commit("Initial file contents.") - self.vcs.set_file_contents( - full_path, self.test_contents['uncommitted']) - current_contents = self.vcs.get_file_contents(full_path) - self.failUnlessEqual( - self.test_contents['uncommitted'], current_contents) - - def test_revision_file_contents_as_committed(self): - """Should get file contents as committed to specified revision.""" - if not self.vcs.versioned: - return - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents( - full_path, self.test_contents['rev_1']) - revision = self.vcs.commit("Initial file contents.") - self.vcs.set_file_contents( - full_path, self.test_contents['uncommitted']) - committed_contents = self.vcs.get_file_contents( - full_path, revision) - self.failUnlessEqual( - self.test_contents['rev_1'], committed_contents) - - def test_revision_id_as_committed(self): - """Check for compatibility between .commit() and .revision_id()""" - if not self.vcs.versioned: - self.failUnlessEqual(self.vcs.revision_id(5), None) - return - committed_revisions = [] - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents( - full_path, self.test_contents['rev_1']) - revision = self.vcs.commit("Initial %s contents." % path) - committed_revisions.append(revision) - self.vcs.set_file_contents( - full_path, self.test_contents['uncommitted']) - revision = self.vcs.commit("Altered %s contents." % path) - committed_revisions.append(revision) - for i,revision in enumerate(committed_revisions): - self.failUnlessEqual(self.vcs.revision_id(i), revision) - i += -len(committed_revisions) # check negative indices - self.failUnlessEqual(self.vcs.revision_id(i), revision) - i = len(committed_revisions) - self.failUnlessEqual(self.vcs.revision_id(i), None) - self.failUnlessEqual(self.vcs.revision_id(-i-1), None) - - def test_revision_id_as_committed(self): - """Check revision id before first commit""" - if not self.vcs.versioned: - self.failUnlessEqual(self.vcs.revision_id(5), None) - return - committed_revisions = [] - for path in self.test_files: - self.failUnlessEqual(self.vcs.revision_id(0), None) - - - class VCS_duplicate_repo_TestCase(VCSTestCase): - """Test cases for VCS.duplicate_repo method.""" - - def setUp(self): - super(VCS_duplicate_repo_TestCase, self).setUp() - setup_vcs_revision_test_fixtures(self) - - def tearDown(self): - self.vcs.remove_duplicate_repo() - for path in reversed(sorted(self.test_dirs)): - self.vcs.recursive_remove(self.full_path(path)) - super(VCS_duplicate_repo_TestCase, self).tearDown() - - def test_revision_file_contents_as_committed(self): - """Should match file contents as committed to specified revision. - """ - if not self.vcs.versioned: - return - for path in self.test_files: - full_path = self.full_path(path) - self.vcs.set_file_contents( - full_path, self.test_contents['rev_1']) - revision = self.vcs.commit("Commit current status") - self.vcs.set_file_contents( - full_path, self.test_contents['uncommitted']) - dup_repo_path = self.vcs.duplicate_repo(revision) - dup_file_path = os.path.join(dup_repo_path, path) - dup_file_contents = file(dup_file_path, 'rb').read() - self.failUnlessEqual( - self.test_contents['rev_1'], dup_file_contents) - self.vcs.remove_duplicate_repo() - - - def make_vcs_testcase_subclasses(vcs_class, namespace): - """Make VCSTestCase subclasses for vcs_class in the namespace.""" - vcs_testcase_classes = [ - c for c in ( - ob for ob in globals().values() if isinstance(ob, type)) - if issubclass(c, VCSTestCase)] - - for base_class in vcs_testcase_classes: - testcase_class_name = vcs_class.__name__ + base_class.__name__ - testcase_class_bases = (base_class,) - testcase_class_dict = dict(base_class.__dict__) - testcase_class_dict['Class'] = vcs_class - testcase_class = type( - testcase_class_name, testcase_class_bases, testcase_class_dict) - setattr(namespace, testcase_class_name, testcase_class) - - - unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) - suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) -- cgit From fe1d6dff22f73671928eaadbf4c83bdaa21d9bb9 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 01:49:06 -0500 Subject: Added libbe.storage.base and test suite. --- libbe/error.py | 12 + libbe/storage/base.py | 687 +++++++++++++++++++++++++++++++++++++++++++++++++ libbe/util/__init__.py | 10 + 3 files changed, 709 insertions(+) create mode 100644 libbe/error.py create mode 100644 libbe/storage/base.py create mode 100644 libbe/util/__init__.py diff --git a/libbe/error.py b/libbe/error.py new file mode 100644 index 0000000..fa5678d --- /dev/null +++ b/libbe/error.py @@ -0,0 +1,12 @@ +# Copyright + +""" +General error classes for Bugs-Everywhere. +""" + +class NotSupported (NotImplementedError): + def __init__(self, action, message): + msg = '%s not supported: %s' % (action, message) + NotImplementedError.__init__(self, msg) + self.action = action + self.message = message diff --git a/libbe/storage/base.py b/libbe/storage/base.py new file mode 100644 index 0000000..002cc0f --- /dev/null +++ b/libbe/storage/base.py @@ -0,0 +1,687 @@ +# Copyright + +""" +Abstract bug repository data storage to easily support multiple backends. +""" + +import copy +import os +import pickle + +from libbe.error import NotSupported +from libbe.util.tree import Tree +from libbe.util import InvalidObject +from libbe import TESTING + +if TESTING == True: + import doctest + import os.path + import sys + import unittest + + from libbe.util.utility import Dir + +class ConnectionError (Exception): + pass + +class InvalidID (KeyError): + pass + +class InvalidRevision (KeyError): + pass + +class EmptyCommit(Exception): + def __init__(self): + Exception.__init__(self, 'No changes to commit') + +class Entry (Tree): + def __init__(self, id, value=None, parent=None, children=None): + if children == None: + Tree.__init__(self) + else: + Tree.__init__(self, children) + self.id = id + self.value = value + self.parent = parent + if self.parent != None: + parent.append(self) + + def __str__(self): + return '' % (self.id, self.value) + + def __repr__(self): + return str(self) + + def __cmp__(self, other, local=False): + if other == None: + return cmp(1, None) + if cmp(self.id, other.id) != 0: + return cmp(self.id, other.id) + if cmp(self.value, other.value) != 0: + return cmp(self.value, other.value) + if local == False: + if self.parent == None: + if cmp(self.parent, other.parent) != 0: + return cmp(self.parent, other.parent) + elif self.parent.__cmp__(other.parent, local=True) != 0: + return self.parent.__cmp__(other.parent, local=True) + for sc,oc in zip(self, other): + if sc.__cmp__(oc, local=True) != 0: + return sc.__cmp__(oc, local=True) + return 0 + + def _objects_to_ids(self): + if self.parent != None: + self.parent = self.parent.id + for i,c in enumerate(self): + self[i] = c.id + return self + + def _ids_to_objects(self, dict): + if self.parent != None: + self.parent = dict[self.parent] + for i,c in enumerate(self): + self[i] = dict[c] + return self + +class Storage (object): + """ + This class declares all the methods required by a Storage + interface. This implementation just keeps the data in a + dictionary and uses pickle for persistent storage. + """ + name = 'Storage' + + def __init__(self, repo, options=None): + self.repo = repo + self.options = options + self.read_only = False + self.versioned = False + self.can_init = True + + def __str__(self): + return '<%s %s>' % (self.__class__.__name__, id(self)) + + def __repr__(self): + return str(self) + + def version(self): + """Return a version string for this backend.""" + return '0' + + def init(self): + """Create a new storage repository.""" + if self.can_init == False: + raise NotSupported('init', + 'Cannot initialize this repository format.') + if self.read_only == True: + raise NotSupported('init', 'Cannot initialize read only storage.') + return self._init() + + def _init(self): + f = open(self.repo, 'wb') + root = Entry(id='__ROOT__') + d = {root.id:root} + pickle.dump(dict((k,v._objects_to_ids()) for k,v in d.items()), f, -1) + f.close() + + def destroy(self): + """Remove the storage repository.""" + if self.read_only == True: + raise NotSupported('destroy', 'Cannot destroy read only storage.') + return self._destroy() + + def _destroy(self): + os.remove(self.repo) + + def connect(self): + """Open a connection to the repository.""" + try: + f = open(self.repo, 'rb') + except IOError: + raise ConnectionError(self) + d = pickle.load(f) + self._data = dict((k,v._ids_to_objects(d)) for k,v in d.items()) + f.close() + + def disconnect(self): + """Close the connection to the repository.""" + if self.read_only == True: + return + f = open(self.repo, 'wb') + pickle.dump(dict((k,v._objects_to_ids()) + for k,v in self._data.items()), f, -1) + f.close() + self._data = None + + def add(self, *args, **kwargs): + """Add an entry""" + if self.read_only == True: + raise NotSupported('add', 'Cannot add entry to read only storage.') + self._add(*args, **kwargs) + + def _add(self, id, parent=None): + if parent == None: + parent = '__ROOT__' + p = self._data[parent] + self._data[id] = Entry(id, parent=p) + + def remove(self, *args, **kwargs): + """Remove an entry.""" + if self.read_only == True: + raise NotSupported('remove', + 'Cannot remove entry from read only storage.') + self._remove(*args, **kwargs) + + def _remove(self, id): + e = self._data.pop(id) + e.parent.remove(e) + + def recursive_remove(self, *args, **kwargs): + """Remove an entry and all its decendents.""" + if self.read_only == True: + raise NotSupported('recursive_remove', + 'Cannot remove entries from read only storage.') + self._recursive_remove(*args, **kwargs) + + def _recursive_remove(self, id): + for entry in self._data[id].traverse(): + self._remove(entry.id) + + def children(self, id=None, revision=None): + """Return a list of specified entry's children's ids.""" + if id == None: + id = '__ROOT__' + return [c.id for c in self._data[id] if not c.id.startswith('__')] + + def get(self, id, default=InvalidObject, revision=None): + """ + Get contents of and entry as they were in a given revision. + revision==None specifies the current revision. + + If there is no id, return default, unless default is not + given, in which case raise InvalidID. + """ + if id in self._data: + return self._data[id].value + elif default == InvalidObject: + raise InvalidID(id) + return default + + def set(self, *args, **kwargs): + """ + Set the entry contents. + """ + if self.read_only == True: + raise NotSupported('set', 'Cannot set entry in read only storage.') + self._set(*args, **kwargs) + + def _set(self, id, value): + if id not in self._data: + raise InvalidID(id) + self._data[id].value = value + +class VersionedStorage (Storage): + """ + This class declares all the methods required by a Storage + interface that supports versioning. This implementation just + keeps the data in a list and uses pickle for persistent + storage. + """ + name = 'VersionedStorage' + + def __init__(self, *args, **kwargs): + Storage.__init__(self, *args, **kwargs) + self.versioned = True + + def _init(self): + f = open(self.repo, 'wb') + root = Entry(id='__ROOT__') + summary = Entry(id='__COMMIT__SUMMARY__', value='Initial commit') + body = Entry(id='__COMMIT__BODY__') + initial_commit = {root.id:root, summary.id:summary, body.id:body} + d = dict((k,v._objects_to_ids()) for k,v in initial_commit.items()) + pickle.dump([d, copy.deepcopy(d)], f, -1) # [inital tree, working tree] + f.close() + + def connect(self): + """Open a connection to the repository.""" + try: + f = open(self.repo, 'rb') + except IOError: + raise ConnectionError(self) + d = pickle.load(f) + self._data = [dict((k,v._ids_to_objects(t)) for k,v in t.items()) + for t in d] + f.close() + + def disconnect(self): + """Close the connection to the repository.""" + if self.read_only == True: + return + f = open(self.repo, 'wb') + pickle.dump([dict((k,v._objects_to_ids()) + for k,v in t.items()) for t in self._data], f, -1) + f.close() + self._data = None + + def _add(self, id, parent=None): + if parent == None: + parent = '__ROOT__' + p = self._data[-1][parent] + self._data[-1][id] = Entry(id, parent=p) + + def _remove(self, id): + e = self._data[-1].pop(id) + e.parent.remove(e) + + def _recursive_remove(self, id): + for entry in self._data[-1][id].traverse(): + self._remove(entry.id) + + def children(self, id=None, revision=None): + """Return a list of specified entry's children's ids.""" + if id == None: + id = '__ROOT__' + if revision == None: + revision = -1 + return [c.id for c in self._data[revision][id] + if not c.id.startswith('__')] + + def get(self, id, default=InvalidObject, revision=None): + """ + Get contents of and entry as they were in a given revision. + revision==None specifies the current revision. + + If there is no id, return default, unless default is not + given, in which case raise InvalidID. + """ + if revision == None: + revision = -1 + if id in self._data[revision]: + return self._data[revision][id].value + elif default == InvalidObject: + raise InvalidID(id) + return default + + def _set(self, id, value): + if id not in self._data[-1]: + raise InvalidID(id) + self._data[-1][id].value = value + + def commit(self, *args, **kwargs): + """ + Commit the current repository, with a commit message string + summary and body. Return the name of the new revision. + + If allow_empty == False (the default), raise EmptyCommit if + there are no changes to commit. + """ + if self.read_only == True: + raise NotSupported('commit', 'Cannot commit to read only storage.') + return self._commit(*args, **kwargs) + + def _commit(self, summary, body=None, allow_empty=False): + if self._data[-1] == self._data[-2] and allow_empty == False: + raise EmptyCommit + self._data[-1]["__COMMIT__SUMMARY__"].value = summary + self._data[-1]["__COMMIT__BODY__"].value = body + rev = len(self._data)-1 + self._data.append(copy.deepcopy(self._data[-1])) + return rev + + def revision_id(self, index=None): + """ + Return the name of the th revision. The choice of + which branch to follow when crossing branches/merges is not + defined. Revision indices start at 1; ID 0 is the blank + repository. + + Return None if index==None. + + If the specified revision does not exist, raise InvalidRevision. + """ + if index == None: + return None + try: + if int(index) != index: + raise InvalidRevision(index) + except ValueError: + raise InvalidRevision(index) + L = len(self._data) - 1 # -1 b/c of initial commit + if index >= -L and index <= L: + return index % L + raise InvalidRevision(i) + +if TESTING == True: + class StorageTestCase (unittest.TestCase): + """Test cases for base Storage class.""" + + Class = Storage + + def __init__(self, *args, **kwargs): + super(StorageTestCase, self).__init__(*args, **kwargs) + self.dirname = None + + def setUp(self): + """Set up test fixtures for Storage test case.""" + super(StorageTestCase, self).setUp() + self.dir = Dir() + self.dirname = self.dir.path + self.s = self.Class(repo=os.path.join(self.dirname, 'repo.pkl')) + self.assert_failed_connect() + self.s.init() + self.s.connect() + + def tearDown(self): + super(StorageTestCase, self).tearDown() + self.s.disconnect() + self.s.destroy() + self.assert_failed_connect() + + def assert_failed_connect(self): + try: + self.s.connect() + self.fail( + "Connected to %(name)s repository before initialising" + % vars(self.Class)) + except ConnectionError: + pass + + class Storage_init_TestCase (StorageTestCase): + """Test cases for Storage.init method.""" + + def test_connect_should_succeed_after_init(self): + """Should connect after initialization.""" + self.s.connect() + + class Storage_add_remove_TestCase (StorageTestCase): + """Test cases for Storage.add, .remove, and .recursive_remove methods.""" + + def test_initially_empty(self): + """New repository should be empty.""" + self.failUnless(len(self.s.children()) == 0, self.s.children()) + + def test_add_rooted(self): + """ + Adding entries should increase the number of children (rooted). + """ + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1]) + s = sorted(self.s.children()) + self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) + + def test_add_nonrooted(self): + """ + Adding entries should increase the number of children (nonrooted). + """ + self.s.add('parent') + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1], 'parent') + s = sorted(self.s.children('parent')) + self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) + s = self.s.children() + self.failUnless(s == ['parent'], s) + + def test_remove_rooted(self): + """ + Removing entries should decrease the number of children (rooted). + """ + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1]) + for i in range(10): + self.s.remove(ids.pop()) + s = sorted(self.s.children()) + self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) + + def test_remove_nonrooted(self): + """ + Removing entries should decrease the number of children (nonrooted). + """ + self.s.add('parent') + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1], 'parent') + for i in range(10): + self.s.remove(ids.pop()) + s = sorted(self.s.children('parent')) + self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) + s = self.s.children() + self.failUnless(s == ['parent'], s) + + def test_recursive_remove(self): + """ + Recursive remove should empty the tree. + """ + self.s.add('parent') + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1], 'parent') + for j in range(10): # add some grandkids + self.s.add(str(20*i+j), ids[-i]) + self.s.recursive_remove('parent') + s = sorted(self.s.children()) + self.failUnless(s == [], s) + + class Storage_get_set_TestCase (StorageTestCase): + """Test cases for Storage.get and .set methods.""" + + id = 'unlikely id' + val = 'unlikely value' + + def test_get_default(self): + """ + Get should return specified default if id not in Storage. + """ + ret = self.s.get(self.id, default=self.val) + self.failUnless(ret == self.val, + "%s.get() returned %s not %s" + % (vars(self.Class)['name'], ret, self.val)) + + def test_get_default_exception(self): + """ + Get should raise exception if id not in Storage and no default. + """ + try: + ret = self.s.get(self.id) + self.fail( + "%s.get() returned %s instead of raising InvalidID" + % (vars(self.Class)['name'], ret)) + except InvalidID: + pass + + def test_get_initial_value(self): + """ + Data value should be None before any value has been set. + """ + self.s.add(self.id) + ret = self.s.get(self.id) + self.failUnless(ret == None, + "%s.get() returned %s not None" + % (vars(self.Class)['name'], ret)) + + def test_set_exception(self): + """ + Set should raise exception if id not in Storage. + """ + try: + self.s.set(self.id, self.val) + self.fail( + "%(name)s.set() did not raise InvalidID" + % vars(self.Class)) + except InvalidID: + pass + + def test_set(self): + """ + Set should define the value returned by get. + """ + self.s.add(self.id) + self.s.set(self.id, self.val) + ret = self.s.get(self.id) + self.failUnless(ret == self.val, + "%s.get() returned %s not %s" + % (vars(self.Class)['name'], ret, self.val)) + + class Storage_persistence_TestCase (StorageTestCase): + """Test cases for Storage.disconnect and .connect methods.""" + + id = 'unlikely id' + val = 'unlikely value' + + def test_get_set_persistence(self): + """ + Set should define the value returned by get after reconnect. + """ + self.s.add(self.id) + self.s.set(self.id, self.val) + self.s.disconnect() + self.s.connect() + ret = self.s.get(self.id) + self.failUnless(ret == self.val, + "%s.get() returned %s not %s" + % (vars(self.Class)['name'], ret, self.val)) + + def test_add_nonrooted_persistence(self): + """ + Adding entries should increase the number of children after reconnect. + """ + self.s.add('parent') + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1], 'parent') + self.s.disconnect() + self.s.connect() + s = sorted(self.s.children('parent')) + self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) + s = self.s.children() + self.failUnless(s == ['parent'], s) + + class VersionedStorageTestCase (StorageTestCase): + """Test cases for base VersionedStorage class.""" + + Class = VersionedStorage + + class VersionedStorage_commit_TestCase (VersionedStorageTestCase): + """Test cases for VersionedStorage methods.""" + + id = 'I' #unlikely id' + val = 'X' + commit_msg = 'C' #ommitting something interesting' + commit_body = 'B' #ome\nlonger\ndescription\n' + + def test_revision_id_exception(self): + """ + Invalid revision id should raise InvalidRevision. + """ + try: + rev = self.s.revision_id('highly unlikely revision id') + self.fail( + "%s.revision_id() didn't raise InvalidRevision, returned %s." + % (vars(self.Class)['name'], rev)) + except InvalidRevision: + pass + + def test_empty_commit_raises_exception(self): + """ + Empty commit should raise exception. + """ + try: + self.s.commit(self.commit_msg, self.commit_body) + self.fail( + "Empty %(name)s.commit() didn't raise EmptyCommit." + % vars(self.Class)) + except EmptyCommit: + pass + + def test_empty_commit_allowed(self): + """ + Empty commit should _not_ raise exception if allow_empty=True. + """ + self.s.commit(self.commit_msg, self.commit_body, + allow_empty=True) + + def test_commit_revision_ids(self): + """ + Commit / revision_id should agree on revision ids. + """ + revs = [] + for s in range(10): + revs.append(self.s.commit(self.commit_msg, + self.commit_body, + allow_empty=True)) + for i in range(10): + rev = self.s.revision_id(i+1) + self.failUnless(rev == revs[i], + "%s.revision_id(%d) returned %s not %s" + % (vars(self.Class)['name'], i+1, rev, revs[i])) + for i in range(-1, -9, -1): + rev = self.s.revision_id(i) + self.failUnless(rev == revs[i], + "%s.revision_id(%d) returned %s not %s" + % (vars(self.Class)['name'], i, rev, revs[i])) + + def test_get_previous_version(self): + """ + Get should be able to return the previous version. + """ + def val(i): + return '%s:%d' % (self.val, i+1) + self.s.add(self.id) + revs = [] + for i in range(10): + self.s.set(self.id, val(i)) + revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), + self.commit_body)) + for i in range(10): + ret = self.s.get(self.id, revision=revs[i]) + self.failUnless(ret == val(i), + "%s.get() returned %s not %s for revision %d" + % (vars(self.Class)['name'], ret, val(i), revs[i])) + + def make_storage_testcase_subclasses(storage_class, namespace): + """Make StorageTestCase subclasses for storage_class in namespace.""" + storage_testcase_classes = [ + c for c in ( + ob for ob in globals().values() if isinstance(ob, type)) + if issubclass(c, StorageTestCase) \ + and not issubclass(c, VersionedStorageTestCase)] + + for base_class in storage_testcase_classes: + testcase_class_name = storage_class.__name__ + base_class.__name__ + testcase_class_bases = (base_class,) + testcase_class_dict = dict(base_class.__dict__) + testcase_class_dict['Class'] = storage_class + testcase_class = type( + testcase_class_name, testcase_class_bases, testcase_class_dict) + setattr(namespace, testcase_class_name, testcase_class) + + def make_versioned_storage_testcase_subclasses(storage_class, namespace): + """Make VersionedStorageTestCase subclasses for storage_class in namespace.""" + storage_testcase_classes = [ + c for c in ( + ob for ob in globals().values() if isinstance(ob, type)) + if issubclass(c, StorageTestCase)] + + for base_class in storage_testcase_classes: + testcase_class_name = storage_class.__name__ + base_class.__name__ + testcase_class_bases = (base_class,) + testcase_class_dict = dict(base_class.__dict__) + testcase_class_dict['Class'] = storage_class + testcase_class = type( + testcase_class_name, testcase_class_bases, testcase_class_dict) + setattr(namespace, testcase_class_name, testcase_class) + + make_storage_testcase_subclasses(VersionedStorage, sys.modules[__name__]) + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/util/__init__.py b/libbe/util/__init__.py new file mode 100644 index 0000000..5604c09 --- /dev/null +++ b/libbe/util/__init__.py @@ -0,0 +1,10 @@ +# Copyright + +""" +Miscellaneous utilities. +""" + +class InvalidObject (object): + """An object that won't come up by accident.""" + pass + -- cgit From e5177b9150290004f472d08c13dfe78075f029e8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 03:51:27 -0500 Subject: Extend libbe.util.id to handle id (path) creation. --- libbe/util/beuuid.py | 67 ------------------------------- libbe/util/id.py | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 67 deletions(-) delete mode 100644 libbe/util/beuuid.py create mode 100644 libbe/util/id.py diff --git a/libbe/util/beuuid.py b/libbe/util/beuuid.py deleted file mode 100644 index a3a3b6c..0000000 --- a/libbe/util/beuuid.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Backwards compatibility support for Python 2.4. Once people give up -on 2.4 ;), the uuid call should be merged into bugdir.py -""" - -import libbe -if libbe.TESTING == True: - import unittest - - -try: - from uuid import uuid4 # Python >= 2.5 - def uuid_gen(): - id = uuid4() - idstr = id.urn - start = "urn:uuid:" - assert idstr.startswith(start) - return idstr[len(start):] -except ImportError: - import os - import sys - from subprocess import Popen, PIPE - - def uuid_gen(): - # Shell-out to system uuidgen - args = ['uuidgen', 'r'] - try: - if sys.platform != "win32": - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) - else: - # win32 don't have os.execvp() so have to run command in a shell - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, - shell=True, cwd=cwd) - except OSError, e : - strerror = "%s\nwhile executing %s" % (e.args[1], args) - raise OSError, strerror - output, error = q.communicate() - status = q.wait() - if status != 0: - strerror = "%s\nwhile executing %s" % (status, args) - raise Exception, strerror - return output.rstrip('\n') - -if libbe.TESTING == True: - class UUIDtestCase(unittest.TestCase): - def testUUID_gen(self): - id = uuid_gen() - self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) - - suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) diff --git a/libbe/util/id.py b/libbe/util/id.py new file mode 100644 index 0000000..0f1576c --- /dev/null +++ b/libbe/util/id.py @@ -0,0 +1,110 @@ +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Handle ID creation and parsing. +""" + +import libbe + +if libbe.TESTING == True: + import unittest + +try: + from uuid import uuid4 # Python >= 2.5 + def uuid_gen(): + id = uuid4() + idstr = id.urn + start = "urn:uuid:" + assert idstr.startswith(start) + return idstr[len(start):] +except ImportError: + import os + import sys + from subprocess import Popen, PIPE + + def uuid_gen(): + # Shell-out to system uuidgen + args = ['uuidgen', 'r'] + try: + if sys.platform != "win32": + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) + else: + # win32 don't have os.execvp() so have to run command in a shell + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, + shell=True, cwd=cwd) + except OSError, e : + strerror = "%s\nwhile executing %s" % (e.args[1], args) + raise OSError, strerror + output, error = q.communicate() + status = q.wait() + if status != 0: + strerror = "%s\nwhile executing %s" % (status, args) + raise Exception, strerror + return output.rstrip('\n') + + +def _assemble(*args): + for i,arg in enumerate(args): + if arg == None: + args[i] = '' + return '/'.join(args) + +def _split(id): + args = id.split('/') + for i,arg in enumerate(args): + if arg == '': + args[i] = None + return args + + +def bugdir_id(bugdir, *args): + return _assemble(bugdir.uuid, args) + +def bug_id(bug, *args): + if bug.bug == None: + bugdir_id = None + else: + bugdir_id = bugdir_id(bug.bugdir) + return _assemble(bugdir_id, bug.uuid, args) + +def comment_id(comment, *args): + if comment.bug == None: + bug_id = None + else: + bug_id = bug_id(comment.bug) + return _assemble(bug_id, comment.uuid, args) + +def parse_id(id): + args = _split(id) + ret = {'bugdir':args.pop(0)} + type = 'bugdir' + for child_name in ['bug', 'comment']: + if len(args) > 0 and is_a_uuid(args[0]): + ret[child_name] = args.pop(0) + type = child_name + ret['type'] = type + ret['remaining'] = os.path.join(args) + return ret + +if libbe.TESTING == True: + class UUIDtestCase(unittest.TestCase): + def testUUID_gen(self): + id = uuid_gen() + self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) + + suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) -- cgit From d64b6336f75078445f2b730b31598817ac1cdb7a Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 03:52:37 -0500 Subject: Extended libbe.storage.base for separate read/write control. Rather than just having .read_only to set write permissions and assuming that read was always legal. We also added user and backend control of both readable and writeable: do you want to read/write? and can you read/write? Specialized NotSupported into NotWriteable and NotReadable. Added automatic unicode encoding on .set(), and decode option on .get(). --- libbe/storage/base.py | 130 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 36 deletions(-) diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 002cc0f..3526462 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -7,6 +7,7 @@ Abstract bug repository data storage to easily support multiple backends. import copy import os import pickle +import types from libbe.error import NotSupported from libbe.util.tree import Tree @@ -30,6 +31,14 @@ class InvalidID (KeyError): class InvalidRevision (KeyError): pass +class NotWriteable (NotSupported): + def __init__(self, msg): + NotSupported.__init__('write', msg) + +class NotReadable (NotSupported): + def __init__(self, msg): + NotSupported.__init__('read', msg) + class EmptyCommit(Exception): def __init__(self): Exception.__init__(self, 'No changes to commit') @@ -92,10 +101,14 @@ class Storage (object): """ name = 'Storage' - def __init__(self, repo, options=None): + def __init__(self, repo, encoding='utf-8', options=None): self.repo = repo + self.encoding = encoding self.options = options - self.read_only = False + self.readable = True # soft limit (user choice) + self._readable = True # hard limit (backend choice) + self.writeable = True # soft limit (user choice) + self._writeable = True # hard limit (backend choice) self.versioned = False self.can_init = True @@ -109,13 +122,19 @@ class Storage (object): """Return a version string for this backend.""" return '0' + def is_readable(self): + return self.readable and self._readable + + def is_writeable(self): + return self.writeable and self._writeable + def init(self): """Create a new storage repository.""" if self.can_init == False: raise NotSupported('init', 'Cannot initialize this repository format.') - if self.read_only == True: - raise NotSupported('init', 'Cannot initialize read only storage.') + if self.is_writeable() == False: + raise NotWriteable('Cannot initialize unwriteable storage.') return self._init() def _init(self): @@ -127,8 +146,8 @@ class Storage (object): def destroy(self): """Remove the storage repository.""" - if self.read_only == True: - raise NotSupported('destroy', 'Cannot destroy read only storage.') + if self.is_writeable() == False: + raise NotWriteable('Cannot destroy unwriteable storage.') return self._destroy() def _destroy(self): @@ -136,6 +155,11 @@ class Storage (object): def connect(self): """Open a connection to the repository.""" + if self.is_readable() == False: + raise NotReadable('Cannot connect to unreadable storage.') + self._connect() + + def _connect(self): try: f = open(self.repo, 'rb') except IOError: @@ -146,7 +170,7 @@ class Storage (object): def disconnect(self): """Close the connection to the repository.""" - if self.read_only == True: + if self.is_writeable() == False: return f = open(self.repo, 'wb') pickle.dump(dict((k,v._objects_to_ids()) @@ -156,8 +180,8 @@ class Storage (object): def add(self, *args, **kwargs): """Add an entry""" - if self.read_only == True: - raise NotSupported('add', 'Cannot add entry to read only storage.') + if self.is_writeable() == False: + raise NotWriteable('Cannot add entry to unwriteable storage.') self._add(*args, **kwargs) def _add(self, id, parent=None): @@ -168,9 +192,9 @@ class Storage (object): def remove(self, *args, **kwargs): """Remove an entry.""" - if self.read_only == True: - raise NotSupported('remove', - 'Cannot remove entry from read only storage.') + if self.is_writeable() == False: + raise NotSupported('write', + 'Cannot remove entry from unwriteable storage.') self._remove(*args, **kwargs) def _remove(self, id): @@ -179,22 +203,27 @@ class Storage (object): def recursive_remove(self, *args, **kwargs): """Remove an entry and all its decendents.""" - if self.read_only == True: - raise NotSupported('recursive_remove', - 'Cannot remove entries from read only storage.') + if self.is_writeable() == False: + raise NotSupported('write', + 'Cannot remove entries from unwriteable storage.') self._recursive_remove(*args, **kwargs) def _recursive_remove(self, id): for entry in self._data[id].traverse(): self._remove(entry.id) - def children(self, id=None, revision=None): + def children(self, *args, **kwargs): """Return a list of specified entry's children's ids.""" + if self.is_readable() == False: + raise NotReadable('Cannot list children with unreadable storage.') + return self._children(*args, **kwargs) + + def _children(self, id=None, revision=None): if id == None: id = '__ROOT__' return [c.id for c in self._data[id] if not c.id.startswith('__')] - def get(self, id, default=InvalidObject, revision=None): + def get(self, *args, **kwargs): """ Get contents of and entry as they were in a given revision. revision==None specifies the current revision. @@ -202,19 +231,33 @@ class Storage (object): If there is no id, return default, unless default is not given, in which case raise InvalidID. """ + if self.is_readable() == False: + raise NotReadable('Cannot get entry with unreadable storage.') + if 'decode' in kwargs: + decode = kwargs.pop('decode') + else: + decode = False + value = self._get(*args, **kwargs) + if decode == True: + return unicode(value, self.encoding) + return value + + def _get(self, id, default=InvalidObject, revision=None): if id in self._data: return self._data[id].value elif default == InvalidObject: raise InvalidID(id) return default - def set(self, *args, **kwargs): + def set(self, id, value, *args, **kwargs): """ Set the entry contents. """ - if self.read_only == True: - raise NotSupported('set', 'Cannot set entry in read only storage.') - self._set(*args, **kwargs) + if self.is_writeable() == False: + raise NotWriteable('Cannot set entry in unwriteable storage.') + if type(value) == types.UnicodeType: + value = value.encode(self.encoding) + self._set(id, value, *args, **kwargs) def _set(self, id, value): if id not in self._data: @@ -244,8 +287,7 @@ class VersionedStorage (Storage): pickle.dump([d, copy.deepcopy(d)], f, -1) # [inital tree, working tree] f.close() - def connect(self): - """Open a connection to the repository.""" + def _connect(self): try: f = open(self.repo, 'rb') except IOError: @@ -257,7 +299,7 @@ class VersionedStorage (Storage): def disconnect(self): """Close the connection to the repository.""" - if self.read_only == True: + if self.is_writeable() == False: return f = open(self.repo, 'wb') pickle.dump([dict((k,v._objects_to_ids()) @@ -279,8 +321,7 @@ class VersionedStorage (Storage): for entry in self._data[-1][id].traverse(): self._remove(entry.id) - def children(self, id=None, revision=None): - """Return a list of specified entry's children's ids.""" + def _children(self, id=None, revision=None): if id == None: id = '__ROOT__' if revision == None: @@ -288,14 +329,7 @@ class VersionedStorage (Storage): return [c.id for c in self._data[revision][id] if not c.id.startswith('__')] - def get(self, id, default=InvalidObject, revision=None): - """ - Get contents of and entry as they were in a given revision. - revision==None specifies the current revision. - - If there is no id, return default, unless default is not - given, in which case raise InvalidID. - """ + def _get(self, id, default=InvalidObject, revision=None): if revision == None: revision = -1 if id in self._data[revision]: @@ -317,8 +351,8 @@ class VersionedStorage (Storage): If allow_empty == False (the default), raise EmptyCommit if there are no changes to commit. """ - if self.read_only == True: - raise NotSupported('commit', 'Cannot commit to read only storage.') + if self.is_writeable() == False: + raise NotWriteable('Cannot commit to unwriteable storage.') return self._commit(*args, **kwargs) def _commit(self, summary, body=None, allow_empty=False): @@ -531,6 +565,30 @@ if TESTING == True: "%s.get() returned %s not %s" % (vars(self.Class)['name'], ret, self.val)) + def test_unicode_set(self): + """ + Set should define the value returned by get. + """ + val = u'Fran\xe7ois' + self.s.add(self.id) + self.s.set(self.id, val) + ret = self.s.get(self.id, decode=True) + self.failUnless(type(ret) == types.UnicodeType, + "%s.get() returned %s not UnicodeType" + % (vars(self.Class)['name'], type(ret))) + self.failUnless(ret == val, + "%s.get() returned %s not %s" + % (vars(self.Class)['name'], ret, self.val)) + ret = self.s.get(self.id) + self.failUnless(type(ret) == types.StringType, + "%s.get() returned %s not StringType" + % (vars(self.Class)['name'], type(ret))) + s = unicode(ret, self.s.encoding) + self.failUnless(s == val, + "%s.get() returned %s not %s" + % (vars(self.Class)['name'], s, self.val)) + + class Storage_persistence_TestCase (StorageTestCase): """Test cases for Storage.disconnect and .connect methods.""" -- cgit From eedd308ff46fb9d0529f4480d2d4ae17e435795d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 03:58:36 -0500 Subject: Use mapfile to only create & parse mapfile strings, not files --- libbe/storage/util/mapfile.py | 116 +++++++++++++++++++++++++++++++++++ libbe/storage/vcs/util/mapfile.py | 126 -------------------------------------- 2 files changed, 116 insertions(+), 126 deletions(-) create mode 100644 libbe/storage/util/mapfile.py delete mode 100644 libbe/storage/vcs/util/mapfile.py diff --git a/libbe/storage/util/mapfile.py b/libbe/storage/util/mapfile.py new file mode 100644 index 0000000..a8d5516 --- /dev/null +++ b/libbe/storage/util/mapfile.py @@ -0,0 +1,116 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Provide a means of saving and loading dictionaries of parameters. The +saved "mapfiles" should be clear, flat-text files, and allow easy merging of +independent/conflicting changes. +""" + +import errno +import os.path +import yaml + +import libbe +if libbe.TESTING == True: + import doctest + + +class IllegalKey(Exception): + def __init__(self, key): + Exception.__init__(self, 'Illegal key "%s"' % key) + self.key = key + +class IllegalValue(Exception): + def __init__(self, value): + Exception.__init__(self, 'Illegal value "%s"' % value) + self.value = value + +def generate(map): + """Generate a YAML mapfile content string. + >>> generate({"q":"p"}) + 'q: p\\n\\n' + >>> generate({"q":u"Fran\u00e7ais"}) + 'q: Fran\\xc3\\xa7ais\\n\\n' + >>> generate({"q":u"hello"}) + 'q: hello\\n\\n' + >>> generate({"q=":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "q=" + >>> generate({"q:":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "q:" + >>> generate({"q\\n":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "q\\n" + >>> generate({"":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "" + >>> generate({">q":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key ">q" + >>> generate({"q":"p\\n"}) + Traceback (most recent call last): + IllegalValue: Illegal value "p\\n" + """ + keys = map.keys() + keys.sort() + for key in keys: + try: + assert not key.startswith('>') + assert('\n' not in key) + assert('=' not in key) + assert(':' not in key) + assert(len(key) > 0) + except AssertionError: + raise IllegalKey(unicode(key).encode('unicode_escape')) + if '\n' in map[key]: + raise IllegalValue(unicode(map[key]).encode('unicode_escape')) + + lines = [] + for key in keys: + lines.append(yaml.safe_dump({key: map[key]}, + default_flow_style=False, + allow_unicode=True)) + lines.append('') + return '\n'.join(lines) + +def parse(contents): + """ + Parse a YAML mapfile string. + >>> parse('q: p\\n\\n')['q'] + 'p' + >>> parse('q: \\'p\\'\\n\\n')['q'] + 'p' + >>> contents = generate({"a":"b", "c":"d", "e":"f"}) + >>> dict = parse(contents) + >>> dict["a"] + 'b' + >>> dict["c"] + 'd' + >>> dict["e"] + 'f' + >>> contents = generate({"q":u"Fran\u00e7ais"}) + >>> dict = parse(contents) + >>> dict["q"] + u'Fran\\xe7ais' + """ + return yaml.load(contents) or {} + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/storage/vcs/util/mapfile.py b/libbe/storage/vcs/util/mapfile.py deleted file mode 100644 index 8e1e279..0000000 --- a/libbe/storage/vcs/util/mapfile.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Provide a means of saving and loading dictionaries of parameters. The -saved "mapfiles" should be clear, flat-text files, and allow easy merging of -independent/conflicting changes. -""" - -import errno -import os.path -import yaml - -import libbe -if libbe.TESTING == True: - import doctest - - -class IllegalKey(Exception): - def __init__(self, key): - Exception.__init__(self, 'Illegal key "%s"' % key) - self.key = key - -class IllegalValue(Exception): - def __init__(self, value): - Exception.__init__(self, 'Illegal value "%s"' % value) - self.value = value - -def generate(map): - """Generate a YAML mapfile content string. - >>> generate({"q":"p"}) - 'q: p\\n\\n' - >>> generate({"q":u"Fran\u00e7ais"}) - 'q: Fran\\xc3\\xa7ais\\n\\n' - >>> generate({"q":u"hello"}) - 'q: hello\\n\\n' - >>> generate({"q=":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "q=" - >>> generate({"q:":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "q:" - >>> generate({"q\\n":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "q\\n" - >>> generate({"":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key "" - >>> generate({">q":"p"}) - Traceback (most recent call last): - IllegalKey: Illegal key ">q" - >>> generate({"q":"p\\n"}) - Traceback (most recent call last): - IllegalValue: Illegal value "p\\n" - """ - keys = map.keys() - keys.sort() - for key in keys: - try: - assert not key.startswith('>') - assert('\n' not in key) - assert('=' not in key) - assert(':' not in key) - assert(len(key) > 0) - except AssertionError: - raise IllegalKey(unicode(key).encode('unicode_escape')) - if '\n' in map[key]: - raise IllegalValue(unicode(map[key]).encode('unicode_escape')) - - lines = [] - for key in keys: - lines.append(yaml.safe_dump({key: map[key]}, - default_flow_style=False, - allow_unicode=True)) - lines.append('') - return '\n'.join(lines) - -def parse(contents): - """ - Parse a YAML mapfile string. - >>> parse('q: p\\n\\n')['q'] - 'p' - >>> parse('q: \\'p\\'\\n\\n')['q'] - 'p' - >>> contents = generate({"a":"b", "c":"d", "e":"f"}) - >>> dict = parse(contents) - >>> dict["a"] - 'b' - >>> dict["c"] - 'd' - >>> dict["e"] - 'f' - >>> contents = generate({"q":u"Fran\u00e7ais"}) - >>> dict = parse(contents) - >>> dict["q"] - u'Fran\\xe7ais' - """ - return yaml.load(contents) or {} - -def map_save(vcs, path, map, allow_no_vcs=False): - """Save the map as a mapfile to the specified path""" - contents = generate(map) - vcs.set_file_contents(path, contents, allow_no_vcs, binary=True) - -def map_load(vcs, path, allow_no_vcs=False): - contents = vcs.get_file_contents(path, allow_no_vcs=allow_no_vcs, - binary=True) - return parse(contents) - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() -- cgit From 2ffef9d0ba47a48a048eed27a9ec35859008a649 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 04:01:38 -0500 Subject: Use .storage.is_read/writeable() rather than .sync_with_disk() in settings_object.py --- libbe/storage/settings_object.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libbe/storage/settings_object.py b/libbe/storage/settings_object.py index 6a00ba9..be119dd 100644 --- a/libbe/storage/settings_object.py +++ b/libbe/storage/settings_object.py @@ -32,7 +32,6 @@ if libbe.TESTING == True: import doctest import unittest - class _Token (object): """ `Control' value class for properties. We want values that only @@ -56,14 +55,15 @@ def prop_save_settings(self, old, new): """ The default action undertaken when a property changes. """ - if self.sync_with_disk==True: + if self.storage != None and self.storage.is_writeable(): self.save_settings() def prop_load_settings(self): """ The default action undertaken when an UNPRIMED property is accessed. """ - if self.sync_with_disk==True and self._settings_loaded==False: + if self.storage != None and self.storage.is_readable() \ + and self._settings_loaded==False: self.load_settings() else: self._setup_saved_settings(flag_as_loaded=False) @@ -182,7 +182,7 @@ class SavedSettingsObject(object): def __init__(self): self._settings_loaded = False - self.sync_with_disk = False + self.storage = None self.settings = {} def load_settings(self): @@ -410,21 +410,21 @@ if libbe.TESTING == True: self.failUnless(t.settings["List-type"] == [], t.settings["List-type"]) self.failUnless(SAVES == [ - "'' -> '[]'" + "'' -> '[]'" ], SAVES) t.list_type.append(5) self.failUnless(SAVES == [ - "'' -> '[]'", + "'' -> '[]'", ], SAVES) self.failUnless(t.settings["List-type"] == [5], t.settings["List-type"]) self.failUnless(SAVES == [ # the append(5) has not yet been saved - "'' -> '[]'", + "'' -> '[]'", ], SAVES) self.failUnless(t.list_type == [5], t.list_type)#get triggers saved self.failUnless(SAVES == [ # now the append(5) has been saved. - "'' -> '[]'", + "'' -> '[]'", "'[]' -> '[5]'" ], SAVES) -- cgit From df3a136010c3a05166a4a77028fd00a948d7cdeb Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 04:10:10 -0500 Subject: Transitioned comment.py to new storage format. --- libbe/comment.py | 126 ++++++++++++++++----------------------- libbe/storage/util/__init__.py | 0 libbe/storage/util/config.py | 94 +++++++++++++++++++++++++++++ libbe/storage/vcs/util/config.py | 94 ----------------------------- 4 files changed, 147 insertions(+), 167 deletions(-) create mode 100644 libbe/storage/util/__init__.py create mode 100644 libbe/storage/util/config.py delete mode 100644 libbe/storage/vcs/util/config.py diff --git a/libbe/comment.py b/libbe/comment.py index 32536d4..0e23d3c 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -1,4 +1,3 @@ -# Bugs Everywhere, a distributed bugtracker # Copyright (C) 2008-2009 Gianluca Montecchi # Thomas Habets # W. Trevor King @@ -34,14 +33,15 @@ except ImportError: # look for non-core module import xml.sax.saxutils import libbe -from beuuid import uuid_gen -from properties import Property, doc_property, local_property, \ +import libbe.util.id +from libbe.storage.properties import Property, doc_property, local_property, \ defaulting_property, checked_property, cached_property, \ primed_property, change_hook_property, settings_property -import settings_object -import mapfile -from tree import Tree -import utility +import libbe.storage.settings_object as settings_object +import libbe.storage.util.mapfile as mapfile +from libbe.util.tree import Tree +import libbe.util.utility as utility + if libbe.TESTING == True: import doctest @@ -72,17 +72,14 @@ def loadComments(bug, load_full=False): Set load_full=True when you want to load the comment completely from disk *now*, rather than waiting and lazy loading as required. """ - if bug.sync_with_disk == False: - raise DiskAccessRequired("load comments") - path = bug.get_path("comments") - if not os.path.exists(path): - return Comment(bug, uuid=INVALID_UUID) + uuids = [] + for id in bug.storage.children(): + parsed = libbe.util.id.parse_id(id) + if parsed['type'] == 'comment': + uuids.append(parsed['comment']) comments = [] - for uuid in os.listdir(path): - if uuid.startswith('.'): - continue - comm = Comment(bug, uuid, from_disk=True) - comm.set_sync_with_disk(bug.sync_with_disk) + for uuid in uuids: + comm = Comment(bug, uuid, from_storage=True) if load_full == True: comm.load_settings() dummy = comm.body # force the body to load @@ -92,8 +89,6 @@ def loadComments(bug, load_full=False): return bug.comment_root def saveComments(bug): - if bug.sync_with_disk == False: - raise DiskAccessRequired("save comments") for comment in bug.comment_root.traverse(): comment.save() @@ -154,15 +149,14 @@ class Comment(Tree, settings_object.SavedSettingsObject): doc="An integer version of .date") def _get_comment_body(self): - if self.vcs != None and self.sync_with_disk == True: - import vcs - binary = not self.content_type.startswith("text/") - return self.vcs.get_file_contents(self.get_path("body"), binary=binary) + if self.storage != None and self.storage.readable: + return self.storage.get(self.id("body"), + decode=self.content_type.startswith("text/")) def _set_comment_body(self, old=None, new=None, force=False): - if (self.vcs != None and self.sync_with_disk == True) or force==True: + if (self.storage != None and self.storage.writeable == True) \ + or force==True: assert new != None, "Can't save empty comment" - binary = not self.content_type.startswith("text/") - self.vcs.set_file_contents(self.get_path("body"), new, binary=binary) + self.storage.set(self.id("body"), new) @Property @change_hook_property(hook=_set_comment_body) @@ -171,15 +165,15 @@ class Comment(Tree, settings_object.SavedSettingsObject): @doc_property(doc="The meat of the comment") def body(): return {} - def _get_vcs(self): - if hasattr(self.bug, "vcs"): - return self.bug.vcs + def _get_storage(self): + if hasattr(self.bug, "storage"): + return self.bug.storage @Property - @cached_property(generator=_get_vcs) - @local_property("vcs") + @cached_property(generator=_get_storage) + @local_property("storage") @doc_property(doc="A revision control system instance.") - def vcs(): return {} + def storage(): return {} def _extra_strings_check_fn(value): return utility.iterable_full_of_strings(value, \ @@ -213,15 +207,14 @@ class Comment(Tree, settings_object.SavedSettingsObject): settings_object.SavedSettingsObject.__init__(self) self.bug = bug self.uuid = uuid - if from_disk == True: - self.sync_with_disk = True - else: - self.sync_with_disk = False + if from_disk == False: if uuid == None: - self.uuid = uuid_gen() + self.uuid = libbe.util.id.uuid_gen() + self.settings = {} + self._setup_saved_settings() self.time = int(time.time()) # only save to second precision - if self.vcs != None: - self.author = self.vcs.get_user_id() + if self.bug != None: + self.author = self.bug.get_user_id() self.in_reply_to = in_reply_to self.body = body @@ -587,53 +580,42 @@ class Comment(Tree, settings_object.SavedSettingsObject): # methods for saving/loading/acessing settings and properties. - def get_path(self, *args): - dir = os.path.join(self.bug.get_path("comments"), self.uuid) - if len(args) == 0: - return dir + def id(self, *args): + assert len(args) <= 1, str(args) assert args[0] in ["values", "body"], str(args) - return os.path.join(dir, *args) - - def set_sync_with_disk(self, value): - self.sync_with_disk = value + return libbe.util.id.comment_id(self, args) def load_settings(self): - if self.sync_with_disk == False: - raise DiskAccessRequired("load settings") - self.settings = mapfile.map_load(self.vcs, self.get_path("values")) + mf = self.storage.get(self.id("values"), default="\n") + self.settings = mapfile.parse(mf) self._setup_saved_settings() def save_settings(self): - if self.sync_with_disk == False: - raise DiskAccessRequired("save settings") - self.vcs.mkdir(self.get_path()) - path = self.get_path("values") - mapfile.map_save(self.vcs, path, self._get_saved_settings()) + mf = mapfile.generate(self._get_saved_settings()) + self.storage.set(self.id("values"), mf) def save(self): """ - Save any loaded contents to disk. + Save any loaded contents to storage. - However, if self.sync_with_disk = True, then any changes are - automatically written to disk as soon as they happen, so - calling this method will just waste time (unless something - else has been messing with your on-disk files). + However, if self.storage.writeable = True, then any changes + are automatically written to storage as soon as they happen, + so calling this method will just waste time (unless something + else has been messing with your stored files). """ - sync_with_disk = self.sync_with_disk - if sync_with_disk == False: - self.set_sync_with_disk(True) + assert self.storage != None, "Can't save without storage" assert self.body != None, "Can't save blank comment" + self.storage.add(self.id()) + self.storage.add(self.id('values')) + self.storage.add(self.id('body')) self.save_settings() self._set_comment_body(new=self.body, force=True) - if sync_with_disk == False: - self.set_sync_with_disk(False) def remove(self): - if self.sync_with_disk == False and self.uuid != INVALID_UUID: - raise DiskAccessRequired("remove") - for comment in self.traverse(): - path = comment.get_path() - self.vcs.recursive_remove(path) + for comment in self: + comment.remove() + if self.uuid != INVALID_UUID: + self.storage.recursive_remove(self.id()) def add_reply(self, reply, allow_time_inversion=False): if self.uuid != INVALID_UUID: @@ -651,9 +633,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): reply = Comment(self.bug, body=body) if content_type != None: # set before saving body to decide binary format reply.content_type = content_type - if self.bug != None: - reply.set_sync_with_disk(self.bug.sync_with_disk) - if reply.sync_with_disk == True: + if reply.storage != None and reply.storage.is_writeable(): reply.save() self.add_reply(reply) return reply diff --git a/libbe/storage/util/__init__.py b/libbe/storage/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libbe/storage/util/config.py b/libbe/storage/util/config.py new file mode 100644 index 0000000..ccd236b --- /dev/null +++ b/libbe/storage/util/config.py @@ -0,0 +1,94 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Create, save, and load the per-user config file at path(). +""" + +import ConfigParser +import codecs +import locale +import os.path +import sys + +import libbe +if libbe.TESTING == True: + import doctest + + +default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() + +def path(): + """Return the path to the per-user config file""" + return os.path.expanduser("~/.bugs_everywhere") + +def set_val(name, value, section="DEFAULT", encoding=None): + """Set a value in the per-user config file + + :param name: The name of the value to set + :param value: The new value to set (or None to delete the value) + :param section: The section to store the name/value in + """ + if encoding == None: + encoding = default_encoding + config = ConfigParser.ConfigParser() + if os.path.exists(path()) == False: # touch file or config + open(path(), "w").close() # read chokes on missing file + f = codecs.open(path(), "r", encoding) + config.readfp(f, path()) + f.close() + if value is not None: + config.set(section, name, value) + else: + config.remove_option(section, name) + f = codecs.open(path(), "w", encoding) + config.write(f) + f.close() + +def get_val(name, section="DEFAULT", default=None, encoding=None): + """ + Get a value from the per-user config file + + :param name: The name of the value to get + :section: The section that the name is in + :return: The value, or None + >>> get_val("junk") is None + True + >>> set_val("junk", "random") + >>> get_val("junk") + u'random' + >>> set_val("junk", None) + >>> get_val("junk") is None + True + """ + if os.path.exists(path()): + if encoding == None: + encoding = default_encoding + config = ConfigParser.ConfigParser() + f = codecs.open(path(), "r", encoding) + config.readfp(f, path()) + f.close() + try: + return config.get(section, name) + except ConfigParser.NoOptionError: + return default + else: + return default + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/storage/vcs/util/config.py b/libbe/storage/vcs/util/config.py deleted file mode 100644 index ccd236b..0000000 --- a/libbe/storage/vcs/util/config.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Create, save, and load the per-user config file at path(). -""" - -import ConfigParser -import codecs -import locale -import os.path -import sys - -import libbe -if libbe.TESTING == True: - import doctest - - -default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() - -def path(): - """Return the path to the per-user config file""" - return os.path.expanduser("~/.bugs_everywhere") - -def set_val(name, value, section="DEFAULT", encoding=None): - """Set a value in the per-user config file - - :param name: The name of the value to set - :param value: The new value to set (or None to delete the value) - :param section: The section to store the name/value in - """ - if encoding == None: - encoding = default_encoding - config = ConfigParser.ConfigParser() - if os.path.exists(path()) == False: # touch file or config - open(path(), "w").close() # read chokes on missing file - f = codecs.open(path(), "r", encoding) - config.readfp(f, path()) - f.close() - if value is not None: - config.set(section, name, value) - else: - config.remove_option(section, name) - f = codecs.open(path(), "w", encoding) - config.write(f) - f.close() - -def get_val(name, section="DEFAULT", default=None, encoding=None): - """ - Get a value from the per-user config file - - :param name: The name of the value to get - :section: The section that the name is in - :return: The value, or None - >>> get_val("junk") is None - True - >>> set_val("junk", "random") - >>> get_val("junk") - u'random' - >>> set_val("junk", None) - >>> get_val("junk") is None - True - """ - if os.path.exists(path()): - if encoding == None: - encoding = default_encoding - config = ConfigParser.ConfigParser() - f = codecs.open(path(), "r", encoding) - config.readfp(f, path()) - f.close() - try: - return config.get(section, name) - except ConfigParser.NoOptionError: - return default - else: - return default - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() -- cgit From 7a8b1223fac612ef8b3dffd0e4c6832a97aa222d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 04:33:49 -0500 Subject: Transitioned bug.py to new storage format. --- libbe/bug.py | 107 ++++++++++++++++++++++++------------------------------- libbe/comment.py | 18 ++++++---- 2 files changed, 57 insertions(+), 68 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index 06c2cc5..1aa34fd 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -33,14 +33,15 @@ except ImportError: # look for non-core module import xml.sax.saxutils import libbe -from beuuid import uuid_gen -from properties import Property, doc_property, local_property, \ +import libbe.util.id +from libbe.storage.properties import Property, doc_property, local_property, \ defaulting_property, checked_property, cached_property, \ primed_property, change_hook_property, settings_property -import settings_object -import mapfile -import comment -import utility +import libbe.storage.settings_object as settings_object +import libbe.storage.util.mapfile as mapfile +import libbe.comment as comment +import libbe.util.utility as utility + if libbe.TESTING == True: import doctest @@ -174,8 +175,14 @@ class Bug(settings_object.SavedSettingsObject): def active(self): return self.status in active_status_values + def _get_user_id(self): + if self.bugdir != None: + return self.bugdir.get_user_id() + return None + @_versioned_property(name="creator", - doc="The user who entered the bug into the system") + doc="The user who entered the bug into the system", + generator=_get_user_id) def creator(): return {} @_versioned_property(name="reporter", @@ -219,7 +226,7 @@ class Bug(settings_object.SavedSettingsObject): def summary(): return {} def _get_comment_root(self, load_full=False): - if self.sync_with_disk: + if self.storage != None and self.storage.is_readable(): return comment.loadComments(self, load_full=load_full) else: return comment.Comment(self, uuid=comment.INVALID_UUID) @@ -230,30 +237,27 @@ class Bug(settings_object.SavedSettingsObject): @doc_property(doc="The trunk of the comment tree. We use a dummy root comment by default, because there can be several comment threads rooted on the same parent bug. To simplify comment interaction, we condense these threads into a single thread with a Comment dummy root.") def comment_root(): return {} - def _get_vcs(self): - if hasattr(self.bugdir, "vcs"): - return self.bugdir.vcs + def _get_storage(self): + if hasattr(self.bugdir, "storage"): + return self.bugdir.storage @Property - @cached_property(generator=_get_vcs) - @local_property("vcs") + @cached_property(generator=_get_storage) + @local_property("storage") @doc_property(doc="A revision control system instance.") - def vcs(): return {} + def storage(): return {} def __init__(self, bugdir=None, uuid=None, from_disk=False, load_comments=False, summary=None): settings_object.SavedSettingsObject.__init__(self) self.bugdir = bugdir self.uuid = uuid - if from_disk == True: - self.sync_with_disk = True - else: - self.sync_with_disk = False + if from_disk == False: if uuid == None: - self.uuid = uuid_gen() + self.uuid = libbe.util.id.uuid_gen() + self.settings = {} + self._setup_saved_settings() self.time = int(time.time()) # only save to second precision - if self.vcs != None: - self.creator = self.vcs.get_user_id() self.summary = summary def __repr__(self): @@ -641,72 +645,53 @@ class Bug(settings_object.SavedSettingsObject): # methods for saving/loading/acessing settings and properties. - def get_path(self, *args): - dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid) - if len(args) == 0: - return dir - assert args[0] in ["values", "comments"], str(args) - return os.path.join(dir, *args) - - def set_sync_with_disk(self, value): - self.sync_with_disk = value - for comment in self.comments(): - comment.set_sync_with_disk(value) + def id(self, *args): + assert len(args) <= 1, str(args) + assert args[0] in ["values"], str(args) + return libbe.util.id.comment_id(self, args) def load_settings(self): - if self.sync_with_disk == False: - raise DiskAccessRequired("load settings") - self.settings = mapfile.map_load(self.vcs, self.get_path("values")) + mf = self.storage.get(self.id("values"), default="\n") + self.settings = mapfile.parse(mf) self._setup_saved_settings() def save_settings(self): - if self.sync_with_disk == False: - raise DiskAccessRequired("save settings") - assert self.summary != None, "Can't save blank bug" - self.vcs.mkdir(self.get_path()) - path = self.get_path("values") - mapfile.map_save(self.vcs, path, self._get_saved_settings()) + mf = mapfile.generate(self._get_saved_settings()) + self.storage.set(self.id("values"), mf) def save(self): """ Save any loaded contents to disk. Because of lazy loading of comments, this is actually not too inefficient. - However, if self.sync_with_disk = True, then any changes are - automatically written to disk as soon as they happen, so - calling this method will just waste time (unless something - else has been messing with your on-disk files). + However, if self.storage.is_writeable() == True, then any + changes are automatically written to storage as soon as they + happen, so calling this method will just waste time (unless + something else has been messing with your stored files). """ - sync_with_disk = self.sync_with_disk - if sync_with_disk == False: - self.set_sync_with_disk(True) + assert self.storage != None, "Can't save without storage" + self.storage.add(self.id()) + self.storage.add(self.id('values')) self.save_settings() if len(self.comment_root) > 0: comment.saveComments(self) - if sync_with_disk == False: - self.set_sync_with_disk(False) def load_comments(self, load_full=True): - if self.sync_with_disk == False: - raise DiskAccessRequired("load comments") if load_full == True: # Force a complete load of the whole comment tree self.comment_root = self._get_comment_root(load_full=True) else: # Setup for fresh lazy-loading. Clear _comment_root, so - # _get_comment_root returns a fresh version. Turn of - # syncing temporarily so we don't write our blank comment + # next _get_comment_root returns a fresh version. Turn of + # writing temporarily so we don't write our blank comment # tree to disk. - self.sync_with_disk = False + w = self.storage.writeable + self.storage.writeable = False self.comment_root = None - self.sync_with_disk = True + self.storage.writeable = w def remove(self): - if self.sync_with_disk == False: - raise DiskAccessRequired("remove") - self.comment_root.remove() - path = self.get_path() - self.vcs.recursive_remove(path) + self.storage.recursive_remove(self.id()) # methods for managing comments diff --git a/libbe/comment.py b/libbe/comment.py index 0e23d3c..fc87c9d 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -120,8 +120,14 @@ class Comment(Tree, settings_object.SavedSettingsObject): doc="Alternate ID for linking imported comments. Internally comments are linked (via In-reply-to) to the parent's UUID. However, these UUIDs are generated internally, so Alt-id is provided as a user-controlled linking target.") def alt_id(): return {} + def _get_user_id(self): + if self.bug != None: + return self.bug._get_user_id() + return None + @_versioned_property(name="Author", - doc="The author of the comment") + doc="The author of the comment", + generator=_get_user_id) def author(): return {} @_versioned_property(name="In-reply-to", @@ -213,8 +219,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): self.settings = {} self._setup_saved_settings() self.time = int(time.time()) # only save to second precision - if self.bug != None: - self.author = self.bug.get_user_id() self.in_reply_to = in_reply_to self.body = body @@ -598,10 +602,10 @@ class Comment(Tree, settings_object.SavedSettingsObject): """ Save any loaded contents to storage. - However, if self.storage.writeable = True, then any changes - are automatically written to storage as soon as they happen, - so calling this method will just waste time (unless something - else has been messing with your stored files). + However, if self.storage.is_writeable() == True, then any + changes are automatically written to storage as soon as they + happen, so calling this method will just waste time (unless + something else has been messing with your stored files). """ assert self.storage != None, "Can't save without storage" assert self.body != None, "Can't save blank comment" -- cgit From 44b4e3f8b6405d0e1e0ebf6cb526ab62cdbbdb25 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 08:54:50 -0500 Subject: Transitioned bugdir.py to new storage format. --- libbe/bug.py | 48 ++- libbe/bugdir.py | 903 ++++++++++++++++---------------------------- libbe/comment.py | 57 ++- libbe/storage/base.py | 19 +- libbe/storage/properties.py | 2 +- libbe/storage/vcs/base.py | 156 ++++++++ libbe/util/encoding.py | 23 +- libbe/util/id.py | 35 +- 8 files changed, 618 insertions(+), 625 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index 1aa34fd..7bb52bc 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -177,7 +177,7 @@ class Bug(settings_object.SavedSettingsObject): def _get_user_id(self): if self.bugdir != None: - return self.bugdir.get_user_id() + return self.bugdir._get_user_id() return None @_versioned_property(name="creator", @@ -227,7 +227,7 @@ class Bug(settings_object.SavedSettingsObject): def _get_comment_root(self, load_full=False): if self.storage != None and self.storage.is_readable(): - return comment.loadComments(self, load_full=load_full) + return comment.load_comments(self, load_full=load_full) else: return comment.Comment(self, uuid=comment.INVALID_UUID) @@ -247,18 +247,26 @@ class Bug(settings_object.SavedSettingsObject): @doc_property(doc="A revision control system instance.") def storage(): return {} - def __init__(self, bugdir=None, uuid=None, from_disk=False, + def __init__(self, bugdir=None, uuid=None, from_storage=False, load_comments=False, summary=None): settings_object.SavedSettingsObject.__init__(self) self.bugdir = bugdir self.uuid = uuid - if from_disk == False: + if from_storage == False: if uuid == None: self.uuid = libbe.util.id.uuid_gen() self.settings = {} self._setup_saved_settings() + if self.storage != None and self.storage.is_writeable(): + self.storage.writeable = False + set_writeable = True + else: + set_writeable = False self.time = int(time.time()) # only save to second precision self.summary = summary + if set_writeable == True: + self.storage.writeable = True + self.save() def __repr__(self): return "Bug(uuid=%r)" % self.uuid @@ -462,8 +470,9 @@ class Bug(settings_object.SavedSettingsObject): if c.alt_id != None: uuid_map[c.alt_id] = c uuid_map[None] = self.comment_root + uuid_map[comment.INVALID_UUID] = self.comment_root if default_parent != self.comment_root: - assert default_parent.uuid in uuid_map, default_parent + assert default_parent.uuid in uuid_map, default_parent.uuid for c in comments: if c.in_reply_to == None \ and default_parent.uuid != comment.INVALID_UUID: @@ -647,12 +656,15 @@ class Bug(settings_object.SavedSettingsObject): def id(self, *args): assert len(args) <= 1, str(args) - assert args[0] in ["values"], str(args) - return libbe.util.id.comment_id(self, args) - - def load_settings(self): - mf = self.storage.get(self.id("values"), default="\n") - self.settings = mapfile.parse(mf) + if len(args) == 1: + assert args[0] in ["values"], str(args) + return libbe.util.id.bug_id(self, *args) + + def load_settings(self, settings_mapfile=None): + if settings_mapfile == None: + settings_mapfile = \ + self.storage.get(self.id("values"), default="\n") + self.settings = mapfile.parse(settings_mapfile) self._setup_saved_settings() def save_settings(self): @@ -661,8 +673,8 @@ class Bug(settings_object.SavedSettingsObject): def save(self): """ - Save any loaded contents to disk. Because of lazy loading of - comments, this is actually not too inefficient. + Save any loaded contents to storage. Because of lazy loading + of comments, this is actually not too inefficient. However, if self.storage.is_writeable() == True, then any changes are automatically written to storage as soon as they @@ -670,11 +682,15 @@ class Bug(settings_object.SavedSettingsObject): something else has been messing with your stored files). """ assert self.storage != None, "Can't save without storage" - self.storage.add(self.id()) - self.storage.add(self.id('values')) + if self.bugdir != None: + parent = self.bugdir.id() + else: + parent = None + self.storage.add(self.id(), parent=parent) + self.storage.add(self.id('values'), parent=self.id()) self.save_settings() if len(self.comment_root) > 0: - comment.saveComments(self) + comment.save_comments(self) def load_comments(self, load_full=True): if load_full == True: diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 7005181..cf42747 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -27,24 +27,26 @@ import copy import errno import os import os.path -import sys import time import libbe -import bug -import encoding -from properties import Property, doc_property, local_property, \ +import libbe.util.encoding as encoding +import libbe.storage as storage +from libbe.storage.properties import Property, doc_property, local_property, \ defaulting_property, checked_property, fn_checked_property, \ cached_property, primed_property, change_hook_property, \ settings_property -import mapfile -import vcs -import settings_object -import upgrade -import utility +import libbe.storage.settings_object as settings_object +import libbe.storage.util.mapfile as mapfile +import libbe.bug as bug +import libbe.util.utility as utility + if libbe.TESTING == True: - import unittest import doctest + import sys + import unittest + + import libbe.storage.base class NoBugDir(Exception): @@ -86,69 +88,7 @@ class DiskAccessRequired (Exception): class BugDir (list, settings_object.SavedSettingsObject): """ - Sink to existing root - ====================== - - Consider the following usage case: - You have a bug directory rooted in - /path/to/source - by which I mean the '.be' directory is at - /path/to/source/.be - However, you're of in some subdirectory like - /path/to/source/GUI/testing - and you want to comment on a bug. Setting sink_to_root=True wen - you initialize your BugDir will cause it to search for the '.be' - file in the ancestors of the path you passed in as 'root'. - /path/to/source/GUI/testing/.be miss - /path/to/source/GUI/.be miss - /path/to/source/.be hit! - So it still roots itself appropriately without much work for you. - - File-system access - ================== - - BugDirs live completely in memory when .sync_with_disk is False. - This is the default configuration setup by BugDir(from_disk=False). - If .sync_with_disk == True (e.g. BugDir(from_disk=True)), then - any changes to the BugDir will be immediately written to disk. - - If you want to change .sync_with_disk, we suggest you use - .set_sync_with_disk(), which propogates the new setting through to - all bugs/comments/etc. that have been loaded into memory. If - you've been living in memory and want to move to - .sync_with_disk==True, but you're not sure if anything has been - changed in memory, a call to .save() immediately before the - .set_sync_with_disk(True) call is a safe move. - - Regardless of .sync_with_disk, a call to .save() will write out - all the contents that the BugDir instance has loaded into memory. - If sync_with_disk has been True over the course of all interesting - changes, this .save() call will be a waste of time. - - The BugDir will only load information from the file system when it - loads new settings/bugs/comments that it doesn't already have in - memory and .sync_with_disk == True. - - Allow VCS initialization - ======================== - - This one is for testing purposes. Setting it to True allows the - BugDir to search for an installed VCS backend and initialize it in - the root directory. This is a convenience option for supporting - tests of versioning functionality (e.g. .duplicate_bugdir). - - Disable encoding manipulation - ============================= - - This one is for testing purposed. You might have non-ASCII - Unicode in your bugs, comments, files, etc. BugDir instances try - and support your preferred encoding scheme (e.g. "utf-8") when - dealing with stream and file input/output. For stream output, - this involves replacing sys.stdout and sys.stderr - (libbe.encode.set_IO_stream_encodings). However this messes up - doctest's output catching. In order to support doctest tests - using BugDirs, set manipulate_encodings=False, and stick to ASCII - in your tests. + TODO: simple bugdir manipulation examples... """ settings_properties = [] @@ -168,104 +108,6 @@ class BugDir (list, settings_object.SavedSettingsObject): doc="The current project development target.") def target(): return {} - def _guess_encoding(self): - return encoding.get_encoding() - def _check_encoding(value): - if value != None: - return encoding.known_encoding(value) - def _setup_encoding(self, new_encoding): - # change hook called before generator. - if new_encoding not in [None, settings_object.EMPTY]: - if self._manipulate_encodings == True: - encoding.set_IO_stream_encodings(new_encoding) - def _set_encoding(self, old_encoding, new_encoding): - self._setup_encoding(new_encoding) - self._prop_save_settings(old_encoding, new_encoding) - - @_versioned_property(name="encoding", - doc="""The default input/output encoding to use (e.g. "utf-8").""", - change_hook=_set_encoding, - generator=_guess_encoding, - check_fn=_check_encoding) - def encoding(): return {} - - def _setup_user_id(self, user_id): - self.vcs.user_id = user_id - def _guess_user_id(self): - return self.vcs.get_user_id() - def _set_user_id(self, old_user_id, new_user_id): - self._setup_user_id(new_user_id) - self._prop_save_settings(old_user_id, new_user_id) - - @_versioned_property(name="user_id", - doc= -"""The user's prefered name, e.g. 'John Doe '. Note -that the Arch VCS backend *enforces* ids with this format.""", - change_hook=_set_user_id, - generator=_guess_user_id) - def user_id(): return {} - - @_versioned_property(name="default_assignee", - doc= -"""The default assignee for new bugs e.g. 'John Doe '.""") - def default_assignee(): return {} - - @_versioned_property(name="vcs_name", - doc="""The name of the current VCS. Kept seperate to make saving/loading -settings easy. Don't set this attribute. Set .vcs instead, and -.vcs_name will be automatically adjusted.""", - default="None", - allowed=["None"]+vcs.VCS_ORDER) - def vcs_name(): return {} - - def _get_vcs(self, vcs_name=None): - """Get and root a new revision control system""" - if vcs_name == None: - vcs_name = self.vcs_name - new_vcs = vcs.vcs_by_name(vcs_name) - self._change_vcs(None, new_vcs) - return new_vcs - def _change_vcs(self, old_vcs, new_vcs): - new_vcs.encoding = self.encoding - new_vcs.root(self.root) - self.vcs_name = new_vcs.name - - @Property - @change_hook_property(hook=_change_vcs) - @cached_property(generator=_get_vcs) - @local_property("vcs") - @doc_property(doc="A revision control system instance.") - def vcs(): return {} - - def _bug_map_gen(self): - map = {} - for bug in self: - map[bug.uuid] = bug - for uuid in self.uuids(): - if uuid not in map: - map[uuid] = None - self._bug_map_value = map # ._bug_map_value used by @local_property - - def _extra_strings_check_fn(value): - return utility.iterable_full_of_strings(value, \ - alternative=settings_object.EMPTY) - def _extra_strings_change_hook(self, old, new): - self.extra_strings.sort() # to make merging easier - self._prop_save_settings(old, new) - @_versioned_property(name="extra_strings", - doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/.py.", - default=[], - check_fn=_extra_strings_check_fn, - change_hook=_extra_strings_change_hook, - mutable=True) - def extra_strings(): return {} - - @Property - @primed_property(primer=_bug_map_gen) - @local_property("bug_map") - @doc_property(doc="A dict of (bug-uuid, bug-instance) pairs.") - def _bug_map(): return {} - def _setup_severities(self, severities): if severities not in [None, settings_object.EMPTY]: bug.load_severities(severities) @@ -295,278 +137,114 @@ settings easy. Don't set this attribute. Set .vcs instead, and change_hook=_set_inactive_status) def inactive_status(): return {} + def _extra_strings_check_fn(value): + return utility.iterable_full_of_strings(value, \ + alternative=settings_object.EMPTY) + def _extra_strings_change_hook(self, old, new): + self.extra_strings.sort() # to make merging easier + self._prop_save_settings(old, new) + @_versioned_property(name="extra_strings", + doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/.py.", + default=[], + check_fn=_extra_strings_check_fn, + change_hook=_extra_strings_change_hook, + mutable=True) + def extra_strings(): return {} - def __init__(self, root=None, sink_to_existing_root=True, - assert_new_BugDir=False, allow_vcs_init=False, - manipulate_encodings=True, from_disk=False, vcs=None): - list.__init__(self) - settings_object.SavedSettingsObject.__init__(self) - self._manipulate_encodings = manipulate_encodings - if root == None: - root = os.getcwd() - if sink_to_existing_root == True: - self.root = self._find_root(root) - else: - if not os.path.exists(root): - self.root = None - raise NoRootEntry(root) - self.root = root - # get a temporary vcs until we've loaded settings - self.sync_with_disk = False - self.vcs = self._guess_vcs() - - if from_disk == True: - self.sync_with_disk = True - self.load() - else: - self.sync_with_disk = False - if assert_new_BugDir == True: - if os.path.exists(self.get_path()): - raise AlreadyInitialized, self.get_path() - if vcs == None: - vcs = self._guess_vcs(allow_vcs_init) - self.vcs = vcs - self._setup_user_id(self.user_id) + def _bug_map_gen(self): + map = {} + for bug in self: + map[bug.uuid] = bug + for uuid in self.uuids(): + if uuid not in map: + map[uuid] = None + self._bug_map_value = map # ._bug_map_value used by @local_property - def cleanup(self): - self.vcs.cleanup() + @Property + @primed_property(primer=_bug_map_gen) + @local_property("bug_map") + @doc_property(doc="A dict of (bug-uuid, bug-instance) pairs.") + def _bug_map(): return {} - # methods for getting the BugDir situated in the filesystem + def _get_user_id(self): + return "X" - def _find_root(self, path): - """ - Search for an existing bug database dir and it's ancestors and - return a BugDir rooted there. Only called by __init__, and - then only if sink_to_existing_root == True. - """ - if not os.path.exists(path): - self.root = None - raise NoRootEntry(path) - versionfile=utility.search_parent_directories(path, - os.path.join(".be", "version")) - if versionfile != None: - beroot = os.path.dirname(versionfile) - root = os.path.dirname(beroot) - return root + def __init__(self, storage, uuid=None, from_storage=False): + list.__init__(self) + settings_object.SavedSettingsObject.__init__(self) + self.storage = storage + if from_storage == True: + self.load_settings() else: - beroot = utility.search_parent_directories(path, ".be") - if beroot == None: - self.root = None - raise NoBugDir(path) - return beroot - - def _guess_vcs(self, allow_vcs_init=False): - """ - Only called by __init__. - """ - deepdir = self.get_path() - if not os.path.exists(deepdir): - deepdir = os.path.dirname(deepdir) - new_vcs = vcs.detect_vcs(deepdir) - install = False - if new_vcs.name == "None": - if allow_vcs_init == True: - new_vcs = vcs.installed_vcs() - new_vcs.init(self.root) - return new_vcs + if uuid == None: + self.uuid = libbe.util.id.uuid_gen() + self.settings = {} + self._setup_saved_settings() + if self.storage != None and self.storage.is_writeable(): + self.save() # methods for saving/loading/accessing settings and properties. - def get_path(self, *args): - """ - Return a path relative to .root. - """ - dir = os.path.join(self.root, ".be") - if len(args) == 0: - return dir - assert args[0] in ["version", "settings", "bugs"], str(args) - return os.path.join(dir, *args) - - def _get_settings(self, settings_path, for_duplicate_bugdir=False): - allow_no_vcs = not self.vcs.path_in_root(settings_path) - if allow_no_vcs == True: - assert for_duplicate_bugdir == True - if self.sync_with_disk == False and for_duplicate_bugdir == False: - # duplicates can ignore this bugdir's .sync_with_disk status - raise DiskAccessRequired("_get settings") - try: - settings = mapfile.map_load(self.vcs, settings_path, allow_no_vcs) - except vcs.NoSuchFile: - settings = {"vcs_name": "None"} - return settings - - def _save_settings(self, settings_path, settings, - for_duplicate_bugdir=False): - allow_no_vcs = not self.vcs.path_in_root(settings_path) - if allow_no_vcs == True: - assert for_duplicate_bugdir == True - if self.sync_with_disk == False and for_duplicate_bugdir == False: - # duplicates can ignore this bugdir's .sync_with_disk status - raise DiskAccessRequired("_save settings") - self.vcs.mkdir(self.get_path(), allow_no_vcs) - mapfile.map_save(self.vcs, settings_path, settings, allow_no_vcs) - - def load_settings(self): - self.settings = self._get_settings(self.get_path("settings")) + def id(self, *args): + assert len(args) <= 1, str(args) + if len(args) == 1: + assert args[0] in ['settings'], str(args) + return libbe.util.id.bugdir_id(self, *args) + + def load_settings(self, settings_mapfile=None): + if settings_mapfile == None: + settings_mapfile = \ + self.storage.get(self.id('settings'), default='\n') + self.settings = mapfile.parse(settings_mapfile) self._setup_saved_settings() self._setup_user_id(self.user_id) self._setup_encoding(self.encoding) self._setup_severities(self.severities) self._setup_status(self.active_status, self.inactive_status) - if self.vcs_name != self.vcs.name: - self.vcs = vcs.vcs_by_name(self.vcs_name) - self._setup_user_id(self.user_id) def save_settings(self): - settings = self._get_saved_settings() - self._save_settings(self.get_path("settings"), settings) - - def get_version(self, path=None, use_none_vcs=False, - for_duplicate_bugdir=False): - """ - Requires disk access. - """ - if self.sync_with_disk == False: - raise DiskAccessRequired("get version") - if use_none_vcs == True: - VCS = vcs.vcs_by_name("None") - VCS.root(self.root) - VCS.encoding = encoding.get_encoding() - else: - VCS = self.vcs - - if path == None: - path = self.get_path("version") - allow_no_vcs = not VCS.path_in_root(path) - if allow_no_vcs == True: - assert for_duplicate_bugdir == True - version = VCS.get_file_contents( - path, allow_no_vcs=allow_no_vcs).rstrip("\n") - return version - - def set_version(self): - """ - Requires disk access. - """ - if self.sync_with_disk == False: - raise DiskAccessRequired("set version") - self.vcs.mkdir(self.get_path()) - self.vcs.set_file_contents(self.get_path("version"), - upgrade.BUGDIR_DISK_VERSION+"\n") - - # methods controlling disk access - - def set_sync_with_disk(self, value): - """ - Adjust .sync_with_disk for the BugDir and all it's children. - See the BugDir docstring for a description of the role of - .sync_with_disk. - """ - self.sync_with_disk = value - for bug in self: - bug.set_sync_with_disk(value) - - def load(self): - """ - Reqires disk access - """ - version = self.get_version(use_none_vcs=True) - if version != upgrade.BUGDIR_DISK_VERSION: - upgrade.upgrade(self.root, version) - else: - if not os.path.exists(self.get_path()): - raise NoBugDir(self.get_path()) - self.load_settings() + mf = mapfile.generate(self._get_saved_settings()) + self.storage.set(self.id('settings'), mf) def load_all_bugs(self): """ - Requires disk access. Warning: this could take a while. """ - if self.sync_with_disk == False: - raise DiskAccessRequired("load all bugs") self._clear_bugs() for uuid in self.uuids(): self._load_bug(uuid) def save(self): """ - Note that this command writes to disk _regardless_ of the - status of .sync_with_disk. - - Save any loaded contents to disk. Because of lazy loading of - bugs and comments, this is actually not too inefficient. - - However, if .sync_with_disk = True, then any changes are - automatically written to disk as soon as they happen, so - calling this method will just waste time (unless something - else has been messing with your on-disk files). + Save any loaded contents to storage. Because of lazy loading + of bugs and comments, this is actually not too inefficient. - Requires disk access. + However, if self.storage.is_writeable() == True, then any + changes are automatically written to storage as soon as they + happen, so calling this method will just waste time (unless + something else has been messing with your stored files). """ - sync_with_disk = self.sync_with_disk - if sync_with_disk == False: - self.set_sync_with_disk(True) - self.set_version() + self.uuid = 'BD' + self.storage.add(self.id()) + self.storage.add(self.id('settings'), parent=self.id()) self.save_settings() for bug in self: bug.save() - if sync_with_disk == False: - self.set_sync_with_disk(sync_with_disk) - - # methods for managing duplicate BugDirs - - def duplicate_bugdir(self, revision): - duplicate_path = self.vcs.duplicate_repo(revision) - - duplicate_version_path = os.path.join(duplicate_path, ".be", "version") - try: - version = self.get_version(duplicate_version_path, - for_duplicate_bugdir=True) - except DiskAccessRequired: - self.sync_with_disk = True # temporarily allow access - version = self.get_version(duplicate_version_path, - for_duplicate_bugdir=True) - self.sync_with_disk = False - if version != upgrade.BUGDIR_DISK_VERSION: - upgrade.upgrade(duplicate_path, version) - - # setup revision VCS as None, since the duplicate may not be - # initialized for versioning - duplicate_settings_path = os.path.join(duplicate_path, - ".be", "settings") - duplicate_settings = self._get_settings(duplicate_settings_path, - for_duplicate_bugdir=True) - if "vcs_name" in duplicate_settings: - duplicate_settings["vcs_name"] = "None" - duplicate_settings["user_id"] = self.user_id - if "disabled" in bug.status_values: - # Hack to support old versions of BE bugs - duplicate_settings["inactive_status"] = self.inactive_status - self._save_settings(duplicate_settings_path, duplicate_settings, - for_duplicate_bugdir=True) - - return BugDir(duplicate_path, from_disk=True, manipulate_encodings=self._manipulate_encodings) - - def remove_duplicate_bugdir(self): - self.vcs.remove_duplicate_repo() # methods for managing bugs def uuids(self): uuids = [] - if self.sync_with_disk == True and os.path.exists(self.get_path()): - # list the uuids on disk - if os.path.exists(self.get_path("bugs")): - for uuid in os.listdir(self.get_path("bugs")): - if not (uuid.startswith('.')): - uuids.append(uuid) - yield uuid - # and the ones that are still just in memory + # list the uuids in memory for bug in self: - if bug.uuid not in uuids: - uuids.append(bug.uuid) - yield bug.uuid + uuids.append(bug.uuid) + yield bug.uuid + if self.storage != None and self.storage.is_readable(): + # and the ones that are still just in storage + for id in self.storage.children(self.id()): + parsed = libbe.util.id.parse_id(id) + if parsed['type'] == 'bug' and parsed['bug'] not in uuids: + yield parsed['bug'] def _clear_bugs(self): while len(self) > 0: @@ -574,25 +252,20 @@ settings easy. Don't set this attribute. Set .vcs instead, and self._bug_map_gen() def _load_bug(self, uuid): - if self.sync_with_disk == False: - raise DiskAccessRequired("_load bug") - bg = bug.Bug(bugdir=self, uuid=uuid, from_disk=True) + bg = bug.Bug(bugdir=self, uuid=uuid, from_storage=True) self.append(bg) self._bug_map_gen() return bg - def new_bug(self, uuid=None, summary=None): - bg = bug.Bug(bugdir=self, uuid=uuid, summary=summary) - bg.set_sync_with_disk(self.sync_with_disk) - if bg.sync_with_disk == True: - bg.save() + def new_bug(self, summary=None, _uuid=None): + bg = bug.Bug(bugdir=self, uuid=_uuid, summary=summary) self.append(bg) self._bug_map_gen() return bg def remove_bug(self, bug): self.remove(bug) - if bug.sync_with_disk == True: + if self.storage.is_writeable(): bug.remove() def bug_shortname(self, bug): @@ -615,7 +288,7 @@ settings easy. Don't set this attribute. Set .vcs instead, and def bug_from_shortname(self, shortname): """ - >>> bd = SimpleBugDir(sync_with_disk=False) + >>> bd = SimpleBugDir(memory=True) >>> bug_a = bd.bug_from_shortname('a') >>> print type(bug_a) @@ -649,175 +322,235 @@ settings easy. Don't set this attribute. Set .vcs instead, and return False return True + # methods for managing duplicate BugDirs -class SimpleBugDir (BugDir): - """ - For testing. Set sync_with_disk==False for a memory-only bugdir. - >>> bugdir = SimpleBugDir() - >>> uuids = list(bugdir.uuids()) - >>> uuids.sort() - >>> print uuids - ['a', 'b'] - >>> bugdir.cleanup() - """ - def __init__(self, sync_with_disk=True): - if sync_with_disk == True: - dir = utility.Dir() - assert os.path.exists(dir.path) - root = dir.path - assert_new_BugDir = True - vcs_init = True - else: - root = "/" - assert_new_BugDir = False - vcs_init = False - BugDir.__init__(self, root, sink_to_existing_root=False, - assert_new_BugDir=assert_new_BugDir, - allow_vcs_init=vcs_init, - manipulate_encodings=False) - if sync_with_disk == True: # postpone cleanup since dir.cleanup() removes dir. - self._dir_ref = dir - bug_a = self.new_bug("a", summary="Bug A") - bug_a.creator = "John Doe " - bug_a.time = 0 - bug_b = self.new_bug("b", summary="Bug B") - bug_b.creator = "Jane Doe " - bug_b.time = 0 - bug_b.status = "closed" - if sync_with_disk == True: - self.save() - self.set_sync_with_disk(True) - def cleanup(self): - if hasattr(self, "_dir_ref"): - self._dir_ref.cleanup() - BugDir.cleanup(self) + def duplicate_bugdir(self, revision): + """ + Duplicate bugdirs are read-only copies used for generating + diffs between revisions. + """ + dbd = copy.copy(self) + dbd.storage = copy.copy(self.storage) + dbd._bug_map = copy.copy(self._bug_map) + dbd.storage.writeable = False + added,changed,removed = self.storage.changed_since(revision) + for id in added: + + pass + for id in removed: + pass + for id in changed: + parsed = libbe.util.id.parse_id(id) + if parsed['type'] == 'bugdir': + assert parsed['remaining'] == ['settings'], parsed['remaining'] + dbd._settings = copy.copy(self._settings) + mf = self.storage.get(self.id('settings'), default='\n', + revision=revision) + dbd.load_settings(mf) + else: + if parsed['bug'] not in self: + self._load_bug(parsed['bug']) + dbd._load_bug(parsed['bug']) + else: + bug = copy.copy(self._bug_map[parsed['bug']]) + bug.settings = copy.copy(bug.settings) + dbd._bug_map[parsed['bug']] = bug + if parsed['type'] == 'bug': + assert parsed['remaining'] == ['values'], parsed['remaining'] + mf = self.storage.get(self.id('values'), default='\n', + revision=revision) + bug.load_settings(mf) + elif parsed['type'] == 'comment': + assert parsed['remaining'] in [['values'], ['body']], \ + parsed['remaining'] + bug.comment_root = copy.deepcopy(bug.comment_root) + comment = bug.comment_from_uuid(parsed['comment']) + if parsed['remaining'] == ['values']: + mf = self.storage.get(self.id('values'), default='\n', + revision=revision) + comment.load_settings(mf) + else: + body = self.storage.get(self.id('body'), default='\n', + revision=revision) + comment.body = body + else: + assert 1==0, 'Unkown type "%s" for id "%s"' % (type, id) + dbd.storage.readable = False # so we won't read in added bugs, etc. + return dbd if libbe.TESTING == True: - class BugDirTestCase(unittest.TestCase): - def setUp(self): - self.dir = utility.Dir() - self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, - allow_vcs_init=True) - self.vcs = self.bugdir.vcs - def tearDown(self): - self.bugdir.cleanup() - self.dir.cleanup() - def fullPath(self, path): - return os.path.join(self.dir.path, path) - def assertPathExists(self, path): - fullpath = self.fullPath(path) - self.failUnless(os.path.exists(fullpath)==True, - "path %s does not exist" % fullpath) - self.assertRaises(AlreadyInitialized, BugDir, - self.dir.path, assertNewBugDir=True) - def versionTest(self): - if self.vcs.versioned == False: - return - original = self.bugdir.vcs.commit("Began versioning") - bugA = self.bugdir.bug_from_uuid("a") - bugA.status = "fixed" - self.bugdir.save() - new = self.vcs.commit("Fixed bug a") - dupdir = self.bugdir.duplicate_bugdir(original) - self.failUnless(dupdir.root != self.bugdir.root, - "%s, %s" % (dupdir.root, self.bugdir.root)) - bugAorig = dupdir.bug_from_uuid("a") - self.failUnless(bugA != bugAorig, - "\n%s\n%s" % (bugA.string(), bugAorig.string())) - bugAorig.status = "fixed" - self.failUnless(bug.cmp_status(bugA, bugAorig)==0, - "%s, %s" % (bugA.status, bugAorig.status)) - self.failUnless(bug.cmp_severity(bugA, bugAorig)==0, - "%s, %s" % (bugA.severity, bugAorig.severity)) - self.failUnless(bug.cmp_assigned(bugA, bugAorig)==0, - "%s, %s" % (bugA.assigned, bugAorig.assigned)) - self.failUnless(bug.cmp_time(bugA, bugAorig)==0, - "%s, %s" % (bugA.time, bugAorig.time)) - self.failUnless(bug.cmp_creator(bugA, bugAorig)==0, - "%s, %s" % (bugA.creator, bugAorig.creator)) - self.failUnless(bugA == bugAorig, - "\n%s\n%s" % (bugA.string(), bugAorig.string())) - self.bugdir.remove_duplicate_bugdir() - self.failUnless(os.path.exists(dupdir.root)==False, - str(dupdir.root)) - def testRun(self): - self.bugdir.new_bug(uuid="a", summary="Ant") - self.bugdir.new_bug(uuid="b", summary="Cockroach") - self.bugdir.new_bug(uuid="c", summary="Praying mantis") - length = len(self.bugdir) - self.failUnless(length == 3, "%d != 3 bugs" % length) - uuids = list(self.bugdir.uuids()) - self.failUnless(len(uuids) == 3, "%d != 3 uuids" % len(uuids)) - self.failUnless(uuids == ["a","b","c"], str(uuids)) - bugA = self.bugdir.bug_from_uuid("a") - bugAprime = self.bugdir.bug_from_shortname("a") - self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime)) - self.bugdir.save() - self.versionTest() - def testComments(self, sync_with_disk=False): - if sync_with_disk == True: - self.bugdir.set_sync_with_disk(True) - self.bugdir.new_bug(uuid="a", summary="Ant") - bug = self.bugdir.bug_from_uuid("a") - comm = bug.comment_root - rep = comm.new_reply("Ants are small.") - rep.new_reply("And they have six legs.") - if sync_with_disk == False: - self.bugdir.save() - self.bugdir.set_sync_with_disk(True) - self.bugdir._clear_bugs() - bug = self.bugdir.bug_from_uuid("a") - bug.load_comments() - if sync_with_disk == False: - self.bugdir.set_sync_with_disk(False) - self.failUnless(len(bug.comment_root)==1, len(bug.comment_root)) - for index,comment in enumerate(bug.comments()): - if index == 0: - repLoaded = comment - self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid) - self.failUnless(comment.sync_with_disk == sync_with_disk, - comment.sync_with_disk) - self.failUnless(comment.content_type == "text/plain", - comment.content_type) - self.failUnless(repLoaded.settings["Content-type"] == \ - "text/plain", - repLoaded.settings) - self.failUnless(repLoaded.body == "Ants are small.", - repLoaded.body) - elif index == 1: - self.failUnless(comment.in_reply_to == repLoaded.uuid, - repLoaded.uuid) - self.failUnless(comment.body == "And they have six legs.", - comment.body) - else: - self.failIf(True, - "Invalid comment: %d\n%s" % (index, comment)) - def testSyncedComments(self): - self.testComments(sync_with_disk=True) + class SimpleBugDir (BugDir): + """ + For testing. Set memory=True for a memory-only bugdir. + >>> bugdir = SimpleBugDir() + >>> uuids = list(bugdir.uuids()) + >>> uuids.sort() + >>> print uuids + ['a', 'b'] + >>> bugdir.cleanup() + """ + def __init__(self, memory=True): + if memory == True: + storage = None + else: + dir = utility.Dir() + self._dir_ref = dir # postpone cleanup since dir.cleanup() removes dir. + storage = libbe.storage.base.Storage( \ + os.path.join(dir.path, 'repo.pkl')) + storage.init() + storage.connect() + BugDir.__init__(self, storage=storage) + bug_a = self.new_bug(summary="Bug A", _uuid="a") + bug_a.creator = "John Doe " + bug_a.time = 0 + bug_b = self.new_bug(summary="Bug B", _uuid="b") + bug_b.creator = "Jane Doe " + bug_b.time = 0 + bug_b.status = "closed" + if self.storage != None: + self.storage.disconnect() # flush to storage + self.storage.connect() + def cleanup(self): + if self.storage != None: + self.storage.disconnect() + self.storage.destroy() + if hasattr(self, "_dir_ref"): + self._dir_ref.cleanup() + +# class BugDirTestCase(unittest.TestCase): +# def setUp(self): +# self.dir = utility.Dir() +# self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, +# allow_storage_init=True) +# self.storage = self.bugdir.storage +# def tearDown(self): +# self.bugdir.cleanup() +# self.dir.cleanup() +# def fullPath(self, path): +# return os.path.join(self.dir.path, path) +# def assertPathExists(self, path): +# fullpath = self.fullPath(path) +# self.failUnless(os.path.exists(fullpath)==True, +# "path %s does not exist" % fullpath) +# self.assertRaises(AlreadyInitialized, BugDir, +# self.dir.path, assertNewBugDir=True) +# def versionTest(self): +# if self.storage.versioned == False: +# return +# original = self.bugdir.storage.commit("Began versioning") +# bugA = self.bugdir.bug_from_uuid("a") +# bugA.status = "fixed" +# self.bugdir.save() +# new = self.storage.commit("Fixed bug a") +# dupdir = self.bugdir.duplicate_bugdir(original) +# self.failUnless(dupdir.root != self.bugdir.root, +# "%s, %s" % (dupdir.root, self.bugdir.root)) +# bugAorig = dupdir.bug_from_uuid("a") +# self.failUnless(bugA != bugAorig, +# "\n%s\n%s" % (bugA.string(), bugAorig.string())) +# bugAorig.status = "fixed" +# self.failUnless(bug.cmp_status(bugA, bugAorig)==0, +# "%s, %s" % (bugA.status, bugAorig.status)) +# self.failUnless(bug.cmp_severity(bugA, bugAorig)==0, +# "%s, %s" % (bugA.severity, bugAorig.severity)) +# self.failUnless(bug.cmp_assigned(bugA, bugAorig)==0, +# "%s, %s" % (bugA.assigned, bugAorig.assigned)) +# self.failUnless(bug.cmp_time(bugA, bugAorig)==0, +# "%s, %s" % (bugA.time, bugAorig.time)) +# self.failUnless(bug.cmp_creator(bugA, bugAorig)==0, +# "%s, %s" % (bugA.creator, bugAorig.creator)) +# self.failUnless(bugA == bugAorig, +# "\n%s\n%s" % (bugA.string(), bugAorig.string())) +# self.bugdir.remove_duplicate_bugdir() +# self.failUnless(os.path.exists(dupdir.root)==False, +# str(dupdir.root)) +# def testRun(self): +# self.bugdir.new_bug(uuid="a", summary="Ant") +# self.bugdir.new_bug(uuid="b", summary="Cockroach") +# self.bugdir.new_bug(uuid="c", summary="Praying mantis") +# length = len(self.bugdir) +# self.failUnless(length == 3, "%d != 3 bugs" % length) +# uuids = list(self.bugdir.uuids()) +# self.failUnless(len(uuids) == 3, "%d != 3 uuids" % len(uuids)) +# self.failUnless(uuids == ["a","b","c"], str(uuids)) +# bugA = self.bugdir.bug_from_uuid("a") +# bugAprime = self.bugdir.bug_from_shortname("a") +# self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime)) +# self.bugdir.save() +# self.versionTest() +# def testComments(self, sync_with_disk=False): +# if sync_with_disk == True: +# self.bugdir.set_sync_with_disk(True) +# self.bugdir.new_bug(uuid="a", summary="Ant") +# bug = self.bugdir.bug_from_uuid("a") +# comm = bug.comment_root +# rep = comm.new_reply("Ants are small.") +# rep.new_reply("And they have six legs.") +# if sync_with_disk == False: +# self.bugdir.save() +# self.bugdir.set_sync_with_disk(True) +# self.bugdir._clear_bugs() +# bug = self.bugdir.bug_from_uuid("a") +# bug.load_comments() +# if sync_with_disk == False: +# self.bugdir.set_sync_with_disk(False) +# self.failUnless(len(bug.comment_root)==1, len(bug.comment_root)) +# for index,comment in enumerate(bug.comments()): +# if index == 0: +# repLoaded = comment +# self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid) +# self.failUnless(comment.sync_with_disk == sync_with_disk, +# comment.sync_with_disk) +# self.failUnless(comment.content_type == "text/plain", +# comment.content_type) +# self.failUnless(repLoaded.settings["Content-type"] == \ +# "text/plain", +# repLoaded.settings) +# self.failUnless(repLoaded.body == "Ants are small.", +# repLoaded.body) +# elif index == 1: +# self.failUnless(comment.in_reply_to == repLoaded.uuid, +# repLoaded.uuid) +# self.failUnless(comment.body == "And they have six legs.", +# comment.body) +# else: +# self.failIf(True, +# "Invalid comment: %d\n%s" % (index, comment)) +# def testSyncedComments(self): +# self.testComments(sync_with_disk=True) class SimpleBugDirTestCase (unittest.TestCase): def setUp(self): # create a pre-existing bugdir in a temporary directory self.dir = utility.Dir() - self.original_working_dir = os.getcwd() - os.chdir(self.dir.path) - self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, - allow_vcs_init=True) - self.bugdir.new_bug("preexisting",summary="Hopefully not imported") - self.bugdir.save() + self.storage = libbe.storage.base.Storage( \ + os.path.join(self.dir.path, 'repo.pkl')) + self.storage.init() + self.storage.connect() + self.bugdir = BugDir(self.storage) + self.bugdir.new_bug(summary="Hopefully not imported", + _uuid="preexisting") + self.storage.disconnect() + self.storage.connect() def tearDown(self): - os.chdir(self.original_working_dir) - self.bugdir.cleanup() + if self.storage != None: + self.storage.disconnect() + self.storage.destroy() self.dir.cleanup() def testOnDiskCleanLoad(self): """ - SimpleBugDir(sync_with_disk==True) should not import + SimpleBugDir(memory==False) should not import preexisting bugs. """ - bugdir = SimpleBugDir(sync_with_disk=True) - self.failUnless(bugdir.sync_with_disk==True, bugdir.sync_with_disk) + bugdir = SimpleBugDir(memory=False) + self.failUnless(bugdir.storage.is_readable() == True, + bugdir.storage.is_readable()) + self.failUnless(bugdir.storage.is_writeable() == True, + bugdir.storage.is_writeable()) uuids = sorted([bug.uuid for bug in bugdir]) self.failUnless(uuids == ['a', 'b'], uuids) + self.storage.disconnect() # flush + self.storage.connect() bugdir._clear_bugs() uuids = sorted([bug.uuid for bug in bugdir]) self.failUnless(uuids == [], uuids) @@ -827,15 +560,13 @@ if libbe.TESTING == True: bugdir.cleanup() def testInMemoryCleanLoad(self): """ - SimpleBugDir(sync_with_disk==False) should not import + SimpleBugDir(memory==True) should not import preexisting bugs. """ - bugdir = SimpleBugDir(sync_with_disk=False) - self.failUnless(bugdir.sync_with_disk==False, - bugdir.sync_with_disk) + bugdir = SimpleBugDir(memory=True) + self.failUnless(bugdir.storage == None, bugdir.storage) uuids = sorted([bug.uuid for bug in bugdir]) self.failUnless(uuids == ['a', 'b'], uuids) - self.failUnlessRaises(DiskAccessRequired, bugdir.load_all_bugs) uuids = sorted([bug.uuid for bug in bugdir]) self.failUnless(uuids == ['a', 'b'], uuids) bugdir._clear_bugs() @@ -845,3 +576,27 @@ if libbe.TESTING == True: unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + +# def _get_settings(self, settings_path, for_duplicate_bugdir=False): +# allow_no_storage = not self.storage.path_in_root(settings_path) +# if allow_no_storage == True: +# assert for_duplicate_bugdir == True +# if self.sync_with_disk == False and for_duplicate_bugdir == False: +# # duplicates can ignore this bugdir's .sync_with_disk status +# raise DiskAccessRequired("_get settings") +# try: +# settings = mapfile.map_load(self.storage, settings_path, allow_no_storage) +# except storage.NoSuchFile: +# settings = {"storage_name": "None"} +# return settings + +# def _save_settings(self, settings_path, settings, +# for_duplicate_bugdir=False): +# allow_no_storage = not self.storage.path_in_root(settings_path) +# if allow_no_storage == True: +# assert for_duplicate_bugdir == True +# if self.sync_with_disk == False and for_duplicate_bugdir == False: +# # duplicates can ignore this bugdir's .sync_with_disk status +# raise DiskAccessRequired("_save settings") +# self.storage.mkdir(self.get_path(), allow_no_storage) +# mapfile.map_save(self.storage, settings_path, settings, allow_no_storage) diff --git a/libbe/comment.py b/libbe/comment.py index fc87c9d..e77235a 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -67,7 +67,7 @@ class DiskAccessRequired (Exception): INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!" -def loadComments(bug, load_full=False): +def load_comments(bug, load_full=False): """ Set load_full=True when you want to load the comment completely from disk *now*, rather than waiting and lazy loading as required. @@ -88,7 +88,7 @@ def loadComments(bug, load_full=False): bug.add_comments(comments) return bug.comment_root -def saveComments(bug): +def save_comments(bug): for comment in bug.comment_root.traverse(): comment.save() @@ -155,10 +155,12 @@ class Comment(Tree, settings_object.SavedSettingsObject): doc="An integer version of .date") def _get_comment_body(self): - if self.storage != None and self.storage.readable: + if self.storage != None and self.storage.is_readable() \ + and self.uuid != INVALID_UUID: return self.storage.get(self.id("body"), decode=self.content_type.startswith("text/")) def _set_comment_body(self, old=None, new=None, force=False): + assert self.uuid != INVALID_UUID, self if (self.storage != None and self.storage.writeable == True) \ or force==True: assert new != None, "Can't save empty comment" @@ -195,17 +197,17 @@ class Comment(Tree, settings_object.SavedSettingsObject): mutable=True) def extra_strings(): return {} - def __init__(self, bug=None, uuid=None, from_disk=False, + def __init__(self, bug=None, uuid=None, from_storage=False, in_reply_to=None, body=None): """ - Set from_disk=True to load an old comment. - Set from_disk=False to create a new comment. + Set from_storage=True to load an old comment. + Set from_storage=False to create a new comment. - The uuid option is required when from_disk==True. + The uuid option is required when from_storage==True. The in_reply_to and body options are only used if - from_disk==False (the default). When from_disk==True, they are - loaded from the bug database. + from_storage==False (the default). When from_storage==True, + they are loaded from the bug database. in_reply_to should be the uuid string of the parent comment. """ @@ -213,14 +215,22 @@ class Comment(Tree, settings_object.SavedSettingsObject): settings_object.SavedSettingsObject.__init__(self) self.bug = bug self.uuid = uuid - if from_disk == False: + if from_storage == False: if uuid == None: self.uuid = libbe.util.id.uuid_gen() self.settings = {} self._setup_saved_settings() + if self.storage != None and self.storage.is_writeable(): + self.storage.writeable = False + set_writeable = True + else: + set_writeable = False self.time = int(time.time()) # only save to second precision self.in_reply_to = in_reply_to self.body = body + if set_writeable == True: + self.storage.writeable = True + self.save() def __cmp__(self, other): return cmp_full(self, other) @@ -586,12 +596,15 @@ class Comment(Tree, settings_object.SavedSettingsObject): def id(self, *args): assert len(args) <= 1, str(args) - assert args[0] in ["values", "body"], str(args) - return libbe.util.id.comment_id(self, args) - - def load_settings(self): - mf = self.storage.get(self.id("values"), default="\n") - self.settings = mapfile.parse(mf) + if len(args) == 1: + assert args[0] in ["values", "body"], str(args) + return libbe.util.id.comment_id(self, *args) + + def load_settings(self, settings_mapfile=None): + if settings_mapfile == None: + settings_mapfile = \ + self.storage.get(self.id("values"), default="\n") + self.settings = mapfile.parse(settings_mapfile) self._setup_saved_settings() def save_settings(self): @@ -607,11 +620,17 @@ class Comment(Tree, settings_object.SavedSettingsObject): happen, so calling this method will just waste time (unless something else has been messing with your stored files). """ + if self.uuid == INVALID_UUID: + return assert self.storage != None, "Can't save without storage" assert self.body != None, "Can't save blank comment" - self.storage.add(self.id()) - self.storage.add(self.id('values')) - self.storage.add(self.id('body')) + if self.bug != None: + parent = self.bug.id() + else: + parent = None + self.storage.add(self.id(), parent=parent) + self.storage.add(self.id('values'), parent=self.id()) + self.storage.add(self.id('body'), parent=self.id()) self.save_settings() self._set_comment_body(new=self.body, force=True) diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 3526462..eb2b94c 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -33,11 +33,11 @@ class InvalidRevision (KeyError): class NotWriteable (NotSupported): def __init__(self, msg): - NotSupported.__init__('write', msg) + NotSupported.__init__(self, 'write', msg) class NotReadable (NotSupported): def __init__(self, msg): - NotSupported.__init__('read', msg) + NotSupported.__init__(self, 'read', msg) class EmptyCommit(Exception): def __init__(self): @@ -182,7 +182,11 @@ class Storage (object): """Add an entry""" if self.is_writeable() == False: raise NotWriteable('Cannot add entry to unwriteable storage.') - self._add(*args, **kwargs) + try: # Maybe we've already added that id? + self.get(id) + pass # yup, no need to add another + except InvalidID: + self._add(*args, **kwargs) def _add(self, id, parent=None): if parent == None: @@ -436,6 +440,15 @@ if TESTING == True: """New repository should be empty.""" self.failUnless(len(self.s.children()) == 0, self.s.children()) + def test_add_rooted(self): + """ + Adding entries with the same ID should not increase the number of children. + """ + for i in range(10): + self.s.add('some id') + s = sorted(self.s.children()) + self.failUnless(s == ['some id'], s) + def test_add_rooted(self): """ Adding entries should increase the number of children (rooted). diff --git a/libbe/storage/properties.py b/libbe/storage/properties.py index f756ff0..ddd7b25 100644 --- a/libbe/storage/properties.py +++ b/libbe/storage/properties.py @@ -346,7 +346,7 @@ def change_hook_property(hook, mutable=False, default=None): mutable value, and checking for changes whenever the property is set (obviously) or retrieved (to check for external changes). So long as you're conscientious about accessing the property after - making external modifications, mutability woln't be a problem. + making external modifications, mutability won't be a problem. t.x.append(5) # external modification t.x # dummy access notices change and triggers hook See testChangeHookMutableProperty for an example of the expected diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 44643a4..abc7a01 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -116,6 +116,134 @@ class VCS(object): The methods _u_*() are utility methods available to the _vcs_*() methods. + + Sink to existing root + ====================== + + Consider the following usage case: + You have a bug directory rooted in + /path/to/source + by which I mean the '.be' directory is at + /path/to/source/.be + However, you're of in some subdirectory like + /path/to/source/GUI/testing + and you want to comment on a bug. Setting sink_to_root=True wen + you initialize your BugDir will cause it to search for the '.be' + file in the ancestors of the path you passed in as 'root'. + /path/to/source/GUI/testing/.be miss + /path/to/source/GUI/.be miss + /path/to/source/.be hit! + So it still roots itself appropriately without much work for you. + + File-system access + ================== + + BugDirs live completely in memory when .sync_with_disk is False. + This is the default configuration setup by BugDir(from_disk=False). + If .sync_with_disk == True (e.g. BugDir(from_disk=True)), then + any changes to the BugDir will be immediately written to disk. + + If you want to change .sync_with_disk, we suggest you use + .set_sync_with_disk(), which propogates the new setting through to + all bugs/comments/etc. that have been loaded into memory. If + you've been living in memory and want to move to + .sync_with_disk==True, but you're not sure if anything has been + changed in memory, a call to .save() immediately before the + .set_sync_with_disk(True) call is a safe move. + + Regardless of .sync_with_disk, a call to .save() will write out + all the contents that the BugDir instance has loaded into memory. + If sync_with_disk has been True over the course of all interesting + changes, this .save() call will be a waste of time. + + The BugDir will only load information from the file system when it + loads new settings/bugs/comments that it doesn't already have in + memory and .sync_with_disk == True. + + Allow storage initialization + ======================== + + This one is for testing purposes. Setting it to True allows the + BugDir to search for an installed Storage backend and initialize + it in the root directory. This is a convenience option for + supporting tests of versioning functionality + (e.g. .duplicate_bugdir). + + Disable encoding manipulation + ============================= + + This one is for testing purposed. You might have non-ASCII + Unicode in your bugs, comments, files, etc. BugDir instances try + and support your preferred encoding scheme (e.g. "utf-8") when + dealing with stream and file input/output. For stream output, + this involves replacing sys.stdout and sys.stderr + (libbe.encode.set_IO_stream_encodings). However this messes up + doctest's output catching. In order to support doctest tests + using BugDirs, set manipulate_encodings=False, and stick to ASCII + in your tests. + + if root == None: + root = os.getcwd() + if sink_to_existing_root == True: + self.root = self._find_root(root) + else: + if not os.path.exists(root): + self.root = None + raise NoRootEntry(root) + self.root = root + # get a temporary storage until we've loaded settings + self.sync_with_disk = False + self.storage = self._guess_storage() + + if assert_new_BugDir == True: + if os.path.exists(self.get_path()): + raise AlreadyInitialized, self.get_path() + if storage == None: + storage = self._guess_storage(allow_storage_init) + self.storage = storage + self._setup_user_id(self.user_id) + + + # methods for getting the BugDir situated in the filesystem + + def _find_root(self, path): + """ + Search for an existing bug database dir and it's ancestors and + return a BugDir rooted there. Only called by __init__, and + then only if sink_to_existing_root == True. + """ + if not os.path.exists(path): + self.root = None + raise NoRootEntry(path) + versionfile=utility.search_parent_directories(path, + os.path.join(".be", "version")) + if versionfile != None: + beroot = os.path.dirname(versionfile) + root = os.path.dirname(beroot) + return root + else: + beroot = utility.search_parent_directories(path, ".be") + if beroot == None: + self.root = None + raise NoBugDir(path) + return beroot + + def _guess_storage(self, allow_storage_init=False): + """ + Only called by __init__. + """ + deepdir = self.get_path() + if not os.path.exists(deepdir): + deepdir = os.path.dirname(deepdir) + new_storage = storage.detect_storage(deepdir) + install = False + if new_storage.name == "None": + if allow_storage_init == True: + new_storage = storage.installed_storage() + new_storage.init(self.root) + return new_storage + +os.listdir(self.get_path("bugs")): """ name = "None" client = "" # command-line tool for _u_invoke_client @@ -633,6 +761,34 @@ class VCS(object): body = None f.close() return (summary, body) + + def check_disk_version(self): + version = self.get_version() + if version != upgrade.BUGDIR_DISK_VERSION: + upgrade.upgrade(self.root, version) + + def disk_version(self, path=None, use_none_vcs=False, + for_duplicate_bugdir=False): + """ + Requires disk access. + """ + if path == None: + path = self.get_path("version") + allow_no_vcs = not VCS.path_in_root(path) + if allow_no_vcs == True: + assert for_duplicate_bugdir == True + return self.get(path, allow_no_vcs=allow_no_vcs).rstrip("\n") + + def set_disk_version(self): + """ + Requires disk access. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("set version") + self.vcs.mkdir(self.get_path()) + self.vcs.set_file_contents(self.get_path("version"), + upgrade.BUGDIR_DISK_VERSION+"\n") + if libbe.TESTING == True: diff --git a/libbe/util/encoding.py b/libbe/util/encoding.py index d09117f..21e40cf 100644 --- a/libbe/util/encoding.py +++ b/libbe/util/encoding.py @@ -1,4 +1,3 @@ -# Bugs Everywhere, a distributed bugtracker # Copyright (C) 2008-2009 Gianluca Montecchi # W. Trevor King # @@ -62,5 +61,27 @@ def set_IO_stream_encodings(encoding): sys.stdout = codecs.getwriter(encoding)(sys.__stdout__) sys.stderr = codecs.getwriter(encoding)(sys.__stderr__) + + def _guess_encoding(self): + return encoding.get_encoding() + def _check_encoding(value): + if value != None: + return encoding.known_encoding(value) + def _setup_encoding(self, new_encoding): + # change hook called before generator. + if new_encoding not in [None, settings_object.EMPTY]: + if self._manipulate_encodings == True: + encoding.set_IO_stream_encodings(new_encoding) + def _set_encoding(self, old_encoding, new_encoding): + self._setup_encoding(new_encoding) + self._prop_save_settings(old_encoding, new_encoding) + + @_versioned_property(name="encoding", + doc="""The default input/output encoding to use (e.g. "utf-8").""", + change_hook=_set_encoding, + generator=_guess_encoding, + check_fn=_check_encoding) + def encoding(): return {} + if libbe.TESTING == True: suite = doctest.DocTestSuite() diff --git a/libbe/util/id.py b/libbe/util/id.py index 0f1576c..d57205f 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -19,6 +19,8 @@ Handle ID creation and parsing. """ +import os.path + import libbe if libbe.TESTING == True: @@ -59,6 +61,7 @@ except ImportError: def _assemble(*args): + args = list(args) for i,arg in enumerate(args): if arg == None: args[i] = '' @@ -71,31 +74,41 @@ def _split(id): args[i] = None return args +def _is_a_uuid(id): + if id.startswith('uuid:'): + return True + return False + +def _uuid_to_id(id): + return 'uuid:' + id + +def _id_to_uuid(id): + return id[len('uuid:'):] def bugdir_id(bugdir, *args): - return _assemble(bugdir.uuid, args) + return _assemble(_uuid_to_id(bugdir.uuid), *args) def bug_id(bug, *args): - if bug.bug == None: - bugdir_id = None + if bug.bugdir == None: + bdid = None else: - bugdir_id = bugdir_id(bug.bugdir) - return _assemble(bugdir_id, bug.uuid, args) + bdid = bugdir_id(bug.bugdir) + return _assemble(bdid, _uuid_to_id(bug.uuid), *args) def comment_id(comment, *args): if comment.bug == None: - bug_id = None + bid = None else: - bug_id = bug_id(comment.bug) - return _assemble(bug_id, comment.uuid, args) + bid = bug_id(comment.bug) + return _assemble(bid, _uuid_to_id(comment.uuid), *args) def parse_id(id): args = _split(id) - ret = {'bugdir':args.pop(0)} + ret = {'bugdir':_id_to_uuid(args.pop(0))} type = 'bugdir' for child_name in ['bug', 'comment']: - if len(args) > 0 and is_a_uuid(args[0]): - ret[child_name] = args.pop(0) + if len(args) > 0 and _is_a_uuid(args[0]): + ret[child_name] = _id_to_uuid(args.pop(0)) type = child_name ret['type'] = type ret['remaining'] = os.path.join(args) -- cgit From 79154201c1c012063aa3fe1881ff06a3f239fdc5 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 09:01:26 -0500 Subject: Moved properties.py and settings_object.py to libbe/storage/util/ --- libbe/bug.py | 4 +- libbe/bugdir.py | 8 +- libbe/comment.py | 4 +- libbe/storage/__init__.py | 13 + libbe/storage/properties.py | 642 ---------------------------------- libbe/storage/settings_object.py | 433 ----------------------- libbe/storage/util/properties.py | 642 ++++++++++++++++++++++++++++++++++ libbe/storage/util/settings_object.py | 433 +++++++++++++++++++++++ 8 files changed, 1096 insertions(+), 1083 deletions(-) create mode 100644 libbe/storage/__init__.py delete mode 100644 libbe/storage/properties.py delete mode 100644 libbe/storage/settings_object.py create mode 100644 libbe/storage/util/properties.py create mode 100644 libbe/storage/util/settings_object.py diff --git a/libbe/bug.py b/libbe/bug.py index 7bb52bc..84eb1ed 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -34,8 +34,8 @@ import xml.sax.saxutils import libbe import libbe.util.id -from libbe.storage.properties import Property, doc_property, local_property, \ - defaulting_property, checked_property, cached_property, \ +from libbe.storage.util.properties import Property, doc_property, + local_property, defaulting_property, checked_property, cached_property, \ primed_property, change_hook_property, settings_property import libbe.storage.settings_object as settings_object import libbe.storage.util.mapfile as mapfile diff --git a/libbe/bugdir.py b/libbe/bugdir.py index cf42747..4435624 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -32,10 +32,10 @@ import time import libbe import libbe.util.encoding as encoding import libbe.storage as storage -from libbe.storage.properties import Property, doc_property, local_property, \ - defaulting_property, checked_property, fn_checked_property, \ - cached_property, primed_property, change_hook_property, \ - settings_property +from libbe.storage.util.properties import Property, doc_property, + local_property, defaulting_property, checked_property, \ + fn_checked_property, cached_property, primed_property, \ + change_hook_property, settings_property import libbe.storage.settings_object as settings_object import libbe.storage.util.mapfile as mapfile import libbe.bug as bug diff --git a/libbe/comment.py b/libbe/comment.py index e77235a..987e39c 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -34,8 +34,8 @@ import xml.sax.saxutils import libbe import libbe.util.id -from libbe.storage.properties import Property, doc_property, local_property, \ - defaulting_property, checked_property, cached_property, \ +from libbe.storage.util.properties import Property, doc_property, + local_property, defaulting_property, checked_property, cached_property, \ primed_property, change_hook_property, settings_property import libbe.storage.settings_object as settings_object import libbe.storage.util.mapfile as mapfile diff --git a/libbe/storage/__init__.py b/libbe/storage/__init__.py new file mode 100644 index 0000000..9c954ee --- /dev/null +++ b/libbe/storage/__init__.py @@ -0,0 +1,13 @@ +# Copyright + +import base + +ConnectionError = base.ConnectionError +InvalidID = base.InvalidID +InvalidRevision = base.InvalidRevision +NotWriteable = base.NotWriteable +NotReadable = base.NotReadable +EmptyCommit = base.EmptyCommit + +__all__ = [ConnectionError, InvalidID, InvalidRevision, + NotWriteable, NotReadable, EmptyCommit] diff --git a/libbe/storage/properties.py b/libbe/storage/properties.py deleted file mode 100644 index ddd7b25..0000000 --- a/libbe/storage/properties.py +++ /dev/null @@ -1,642 +0,0 @@ -# Bugs Everywhere - a distributed bugtracker -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -This module provides a series of useful decorators for defining -various types of properties. For example usage, consider the -unittests at the end of the module. - -See - http://www.python.org/dev/peps/pep-0318/ -and - http://www.phyast.pitt.edu/~micheles/python/documentation.html -for more information on decorators. -""" - -import copy -import types - -import libbe -if libbe.TESTING == True: - import unittest - - -class ValueCheckError (ValueError): - def __init__(self, name, value, allowed): - action = "in" # some list of allowed values - if type(allowed) == types.FunctionType: - action = "allowed by" # some allowed-value check function - msg = "%s not %s %s for %s" % (value, action, allowed, name) - ValueError.__init__(self, msg) - self.name = name - self.value = value - self.allowed = allowed - -def Property(funcs): - """ - End a chain of property decorators, returning a property. - """ - args = {} - args["fget"] = funcs.get("fget", None) - args["fset"] = funcs.get("fset", None) - args["fdel"] = funcs.get("fdel", None) - args["doc"] = funcs.get("doc", None) - - #print "Creating a property with" - #for key, val in args.items(): print key, value - return property(**args) - -def doc_property(doc=None): - """ - Add a docstring to a chain of property decorators. - """ - def decorator(funcs=None): - """ - Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...} - or a function fn() returning such a dict. - """ - if hasattr(funcs, "__call__"): - funcs = funcs() # convert from function-arg to dict - funcs["doc"] = doc - return funcs - return decorator - -def local_property(name, null=None, mutable_null=False): - """ - Define get/set access to per-parent-instance local storage. Uses - .__value to store the value for a particular owner instance. - If the .__value attribute does not exist, returns null. - - If mutable_null == True, we only release deepcopies of the null to - the outside world. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget", None) - fset = funcs.get("fset", None) - def _fget(self): - if fget is not None: - fget(self) - if mutable_null == True: - ret_null = copy.deepcopy(null) - else: - ret_null = null - value = getattr(self, "_%s_value" % name, ret_null) - return value - def _fset(self, value): - setattr(self, "_%s_value" % name, value) - if fset is not None: - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - funcs["name"] = name - return funcs - return decorator - -def settings_property(name, null=None): - """ - Similar to local_property, except where local_property stores the - value in instance.__value, settings_property stores the - value in instance.settings[name]. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget", None) - fset = funcs.get("fset", None) - def _fget(self): - if fget is not None: - fget(self) - value = self.settings.get(name, null) - return value - def _fset(self, value): - self.settings[name] = value - if fset is not None: - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - funcs["name"] = name - return funcs - return decorator - - -# Allow comparison and caching with _original_ values for mutables, -# since -# -# >>> a = [] -# >>> b = a -# >>> b.append(1) -# >>> a -# [1] -# >>> a==b -# True -def _hash_mutable_value(value): - return repr(value) -def _init_mutable_property_cache(self): - if not hasattr(self, "_mutable_property_cache_hash"): - # first call to _fget for any mutable property - self._mutable_property_cache_hash = {} - self._mutable_property_cache_copy = {} -def _set_cached_mutable_property(self, cacher_name, property_name, value): - _init_mutable_property_cache(self) - self._mutable_property_cache_hash[(cacher_name, property_name)] = \ - _hash_mutable_value(value) - self._mutable_property_cache_copy[(cacher_name, property_name)] = \ - copy.deepcopy(value) -def _get_cached_mutable_property(self, cacher_name, property_name, default=None): - _init_mutable_property_cache(self) - if (cacher_name, property_name) not in self._mutable_property_cache_copy: - return default - return self._mutable_property_cache_copy[(cacher_name, property_name)] -def _cmp_cached_mutable_property(self, cacher_name, property_name, value, default=None): - _init_mutable_property_cache(self) - if (cacher_name, property_name) not in self._mutable_property_cache_hash: - _set_cached_mutable_property(self, cacher_name, property_name, default) - old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)] - return cmp(_hash_mutable_value(value), old_hash) - - -def defaulting_property(default=None, null=None, - mutable_default=False): - """ - Define a default value for get access to a property. - If the stored value is null, then default is returned. - - If mutable_default == True, we only release deepcopies of the - default to the outside world. - - null should never escape to the outside world, so don't worry - about it being a mutable. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self): - value = fget(self) - if value == null: - if mutable_default == True: - return copy.deepcopy(default) - else: - return default - return value - def _fset(self, value): - if value == default: - value = null - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - -def fn_checked_property(value_allowed_fn): - """ - Define allowed values for get/set access to a property. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self): - value = fget(self) - if value_allowed_fn(value) != True: - raise ValueCheckError(name, value, value_allowed_fn) - return value - def _fset(self, value): - if value_allowed_fn(value) != True: - raise ValueCheckError(name, value, value_allowed_fn) - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - -def checked_property(allowed=[]): - """ - Define allowed values for get/set access to a property. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self): - value = fget(self) - if value not in allowed: - raise ValueCheckError(name, value, allowed) - return value - def _fset(self, value): - if value not in allowed: - raise ValueCheckError(name, value, allowed) - fset(self, value) - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - -def cached_property(generator, initVal=None, mutable=False): - """ - Allow caching of values generated by generator(instance), where - instance is the instance to which this property belongs. Uses - .__cache to store a cache flag for a particular owner - instance. - - When the cache flag is True or missing and the stored value is - initVal, the first fget call triggers the generator function, - whose output is stored in __cached_value. That and - subsequent calls to fget will return this cached value. - - If the input value is no longer initVal (e.g. a value has been - loaded from disk or set with fset), that value overrides any - cached value, and this property has no effect. - - When the cache flag is False and the stored value is initVal, the - generator is not cached, but is called on every fget. - - The cache flag is missing on initialization. Particular instances - may override by setting their own flag. - - In the case that mutable == True, all caching is disabled and the - generator is called whenever the cached value would otherwise be - used. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - name = funcs.get("name", "") - def _fget(self): - cache = getattr(self, "_%s_cache" % name, True) - value = fget(self) - if value == initVal: - if cache == True and mutable == False: - if hasattr(self, "_%s_cached_value" % name): - value = getattr(self, "_%s_cached_value" % name) - else: - value = generator(self) - setattr(self, "_%s_cached_value" % name, value) - else: - value = generator(self) - return value - funcs["fget"] = _fget - return funcs - return decorator - -def primed_property(primer, initVal=None): - """ - Just like a cached_property, except that instead of returning a - new value and running fset to cache it, the primer performs some - background manipulation (e.g. loads data into instance.settings) - such that a _second_ pass through fget succeeds. - - The 'cache' flag becomes a 'prime' flag, with priming taking place - whenever .__prime is True, or is False or missing and - value == initVal. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - name = funcs.get("name", "") - def _fget(self): - prime = getattr(self, "_%s_prime" % name, False) - if prime == False: - value = fget(self) - if prime == True or (prime == False and value == initVal): - primer(self) - value = fget(self) - return value - funcs["fget"] = _fget - return funcs - return decorator - -def change_hook_property(hook, mutable=False, default=None): - """ - Call the function hook(instance, old_value, new_value) whenever a - value different from the current value is set (instance is a a - reference to the class instance to which this property belongs). - This is useful for saving changes to disk, etc. This function is - called _after_ the new value has been stored, allowing you to - change the stored value if you want. - - In the case of mutables, things are slightly trickier. Because - the property-owning class has no way of knowing when the value - changes. We work around this by caching a private deepcopy of the - mutable value, and checking for changes whenever the property is - set (obviously) or retrieved (to check for external changes). So - long as you're conscientious about accessing the property after - making external modifications, mutability won't be a problem. - t.x.append(5) # external modification - t.x # dummy access notices change and triggers hook - See testChangeHookMutableProperty for an example of the expected - behavior. - """ - def decorator(funcs): - if hasattr(funcs, "__call__"): - funcs = funcs() - fget = funcs.get("fget") - fset = funcs.get("fset") - name = funcs.get("name", "") - def _fget(self, new_value=None, from_fset=False): # only used if mutable == True - if from_fset == True: - value = new_value # compare new value with cached - else: - value = fget(self) # compare current value with cached - if _cmp_cached_mutable_property(self, "change hook property", name, value, default) != 0: - # there has been a change, cache new value - old_value = _get_cached_mutable_property(self, "change hook property", name, default) - _set_cached_mutable_property(self, "change hook property", name, value) - if from_fset == True: # return previously cached value - value = old_value - else: # the value changed while we weren't looking - hook(self, old_value, value) - return value - def _fset(self, value): - if mutable == True: # get cached previous value - old_value = _fget(self, new_value=value, from_fset=True) - else: - old_value = fget(self) - fset(self, value) - if value != old_value: - hook(self, old_value, value) - if mutable == True: - funcs["fget"] = _fget - funcs["fset"] = _fset - return funcs - return decorator - -if libbe.TESTING == True: - class DecoratorTests(unittest.TestCase): - def testLocalDoc(self): - class Test(object): - @Property - @doc_property("A fancy property") - def x(): - return {} - self.failUnless(Test.x.__doc__ == "A fancy property", - Test.x.__doc__) - def testLocalProperty(self): - class Test(object): - @Property - @local_property(name="LOCAL") - def x(): - return {} - t = Test() - self.failUnless(t.x == None, str(t.x)) - t.x = 'z' # the first set initializes ._LOCAL_value - self.failUnless(t.x == 'z', str(t.x)) - self.failUnless("_LOCAL_value" in dir(t), dir(t)) - self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value) - def testSettingsProperty(self): - class Test(object): - @Property - @settings_property(name="attr") - def x(): - return {} - def __init__(self): - self.settings = {} - t = Test() - self.failUnless(t.x == None, str(t.x)) - t.x = 'z' # the first set initializes ._LOCAL_value - self.failUnless(t.x == 'z', str(t.x)) - self.failUnless("attr" in t.settings, t.settings) - self.failUnless(t.settings["attr"] == 'z', t.settings["attr"]) - def testDefaultingLocalProperty(self): - class Test(object): - @Property - @defaulting_property(default='y', null='x') - @local_property(name="DEFAULT", null=5) - def x(): return {} - t = Test() - self.failUnless(t.x == 5, str(t.x)) - t.x = 'x' - self.failUnless(t.x == 'y', str(t.x)) - t.x = 'y' - self.failUnless(t.x == 'y', str(t.x)) - t.x = 'z' - self.failUnless(t.x == 'z', str(t.x)) - t.x = 5 - self.failUnless(t.x == 5, str(t.x)) - def testCheckedLocalProperty(self): - class Test(object): - @Property - @checked_property(allowed=['x', 'y', 'z']) - @local_property(name="CHECKED") - def x(): return {} - def __init__(self): - self._CHECKED_value = 'x' - t = Test() - self.failUnless(t.x == 'x', str(t.x)) - try: - t.x = None - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - def testTwoCheckedLocalProperties(self): - class Test(object): - @Property - @checked_property(allowed=['x', 'y', 'z']) - @local_property(name="X") - def x(): return {} - - @Property - @checked_property(allowed=['a', 'b', 'c']) - @local_property(name="A") - def a(): return {} - def __init__(self): - self._A_value = 'a' - self._X_value = 'x' - t = Test() - try: - t.x = 'a' - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - t.x = 'x' - t.x = 'y' - t.x = 'z' - try: - t.a = 'x' - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - t.a = 'a' - t.a = 'b' - t.a = 'c' - def testFnCheckedLocalProperty(self): - class Test(object): - @Property - @fn_checked_property(lambda v : v in ['x', 'y', 'z']) - @local_property(name="CHECKED") - def x(): return {} - def __init__(self): - self._CHECKED_value = 'x' - t = Test() - self.failUnless(t.x == 'x', str(t.x)) - try: - t.x = None - e = None - except ValueCheckError, e: - pass - self.failUnless(type(e) == ValueCheckError, type(e)) - def testCachedLocalProperty(self): - class Gen(object): - def __init__(self): - self.i = 0 - def __call__(self, owner): - self.i += 1 - return self.i - class Test(object): - @Property - @cached_property(generator=Gen(), initVal=None) - @local_property(name="CACHED") - def x(): return {} - t = Test() - self.failIf("_CACHED_cache" in dir(t), - getattr(t, "_CACHED_cache", None)) - self.failUnless(t.x == 1, t.x) - self.failUnless(t.x == 1, t.x) - self.failUnless(t.x == 1, t.x) - t.x = 8 - self.failUnless(t.x == 8, t.x) - self.failUnless(t.x == 8, t.x) - t._CACHED_cache = False # Caching is off, but the stored value - val = t.x # is 8, not the initVal (None), so we - self.failUnless(val == 8, val) # get 8. - t._CACHED_value = None # Now we've set the stored value to None - val = t.x # so future calls to fget (like this) - self.failUnless(val == 2, val) # will call the generator every time... - val = t.x - self.failUnless(val == 3, val) - val = t.x - self.failUnless(val == 4, val) - t._CACHED_cache = True # We turn caching back on, and get - self.failUnless(t.x == 1, str(t.x)) # the original cached value. - del t._CACHED_cached_value # Removing that value forces a - self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call - self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which - self.failUnless(t.x == 5, str(t.x)) # we get the new cached value. - def testPrimedLocalProperty(self): - class Test(object): - def prime(self): - self.settings["PRIMED"] = "initialized" - @Property - @primed_property(primer=prime, initVal=None) - @settings_property(name="PRIMED") - def x(): return {} - def __init__(self): - self.settings={} - t = Test() - self.failIf("_PRIMED_prime" in dir(t), - getattr(t, "_PRIMED_prime", None)) - self.failUnless(t.x == "initialized", t.x) - t.x = 1 - self.failUnless(t.x == 1, t.x) - t.x = None - self.failUnless(t.x == "initialized", t.x) - t._PRIMED_prime = True - t.x = 3 - self.failUnless(t.x == "initialized", t.x) - t._PRIMED_prime = False - t.x = 3 - self.failUnless(t.x == 3, t.x) - def testChangeHookLocalProperty(self): - class Test(object): - def _hook(self, old, new): - self.old = old - self.new = new - - @Property - @change_hook_property(_hook) - @local_property(name="HOOKED") - def x(): return {} - t = Test() - t.x = 1 - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == 1, t.new) - t.x = 1 - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == 1, t.new) - t.x = 2 - self.failUnless(t.old == 1, t.old) - self.failUnless(t.new == 2, t.new) - def testChangeHookMutableProperty(self): - class Test(object): - def _hook(self, old, new): - self.old = old - self.new = new - self.hook_calls += 1 - - @Property - @change_hook_property(_hook, mutable=True) - @local_property(name="HOOKED") - def x(): return {} - t = Test() - t.hook_calls = 0 - t.x = [] - self.failUnless(t.old == None, t.old) - self.failUnless(t.new == [], t.new) - self.failUnless(t.hook_calls == 1, t.hook_calls) - a = t.x - a.append(5) - t.x = a - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5], t.new) - self.failUnless(t.hook_calls == 2, t.hook_calls) - t.x = [] - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [], t.new) - self.failUnless(t.hook_calls == 3, t.hook_calls) - # now append without reassigning. this doesn't trigger the - # change, since we don't ever set t.x, only get it and mess - # with it. It does, however, update our t.new, since t.new = - # t.x and is not a static copy. - t.x.append(5) - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [5], t.new) - self.failUnless(t.hook_calls == 3, t.hook_calls) - # however, the next t.x get _will_ notice the change... - a = t.x - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5], t.new) - self.failUnless(t.hook_calls == 4, t.hook_calls) - t.x.append(6) # this append(6) is not noticed yet - self.failUnless(t.old == [], t.old) - self.failUnless(t.new == [5,6], t.new) - self.failUnless(t.hook_calls == 4, t.hook_calls) - # this append(7) is not noticed, but the t.x get causes the - # append(6) to be noticed - t.x.append(7) - self.failUnless(t.old == [5], t.old) - self.failUnless(t.new == [5,6,7], t.new) - self.failUnless(t.hook_calls == 5, t.hook_calls) - a = t.x # now the append(7) is noticed - self.failUnless(t.old == [5,6], t.old) - self.failUnless(t.new == [5,6,7], t.new) - self.failUnless(t.hook_calls == 6, t.hook_calls) - - suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) diff --git a/libbe/storage/settings_object.py b/libbe/storage/settings_object.py deleted file mode 100644 index be119dd..0000000 --- a/libbe/storage/settings_object.py +++ /dev/null @@ -1,433 +0,0 @@ -# Bugs Everywhere - a distributed bugtracker -# Copyright (C) 2008-2009 Gianluca Montecchi -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -This module provides a base class implementing settings-dict based -property storage useful for BE objects with saved properties -(e.g. BugDir, Bug, Comment). For example usage, consider the -unittests at the end of the module. -""" - -import libbe -from properties import Property, doc_property, local_property, \ - defaulting_property, checked_property, fn_checked_property, \ - cached_property, primed_property, change_hook_property, \ - settings_property -if libbe.TESTING == True: - import doctest - import unittest - -class _Token (object): - """ - `Control' value class for properties. We want values that only - mean something to the settings_object module. - """ - pass - -class UNPRIMED (_Token): - "Property has not been primed." - pass - -class EMPTY (_Token): - """ - Property has been primed but has no user-set value, so use - default/generator value. - """ - pass - - -def prop_save_settings(self, old, new): - """ - The default action undertaken when a property changes. - """ - if self.storage != None and self.storage.is_writeable(): - self.save_settings() - -def prop_load_settings(self): - """ - The default action undertaken when an UNPRIMED property is accessed. - """ - if self.storage != None and self.storage.is_readable() \ - and self._settings_loaded==False: - self.load_settings() - else: - self._setup_saved_settings(flag_as_loaded=False) - -# Some name-mangling routines for pretty printing setting names -def setting_name_to_attr_name(self, name): - """ - Convert keys to the .settings dict into their associated - SavedSettingsObject attribute names. - >>> print setting_name_to_attr_name(None,"User-id") - user_id - """ - return name.lower().replace('-', '_') - -def attr_name_to_setting_name(self, name): - """ - The inverse of setting_name_to_attr_name. - >>> print attr_name_to_setting_name(None, "user_id") - User-id - """ - return name.capitalize().replace('_', '-') - - -def versioned_property(name, doc, - default=None, generator=None, - change_hook=prop_save_settings, - mutable=False, - primer=prop_load_settings, - allowed=None, check_fn=None, - settings_properties=[], - required_saved_properties=[], - require_save=False): - """ - Combine the common decorators in a single function. - - Use zero or one (but not both) of default or generator, since a - working default will keep the generator from functioning. Use the - default if you know what you want the default value to be at - 'coding time'. Use the generator if you can write a function to - determine a valid default at run time. If both default and - generator are None, then the property will be a defaulting - property which defaults to None. - - allowed and check_fn have a similar relationship, although you can - use both of these if you want. allowed compares the proposed - value against a list determined at 'coding time' and check_fn - allows more flexible comparisons to take place at run time. - - Set require_save to True if you want to save the default/generated - value for a property, to protect against future changes. E.g., we - currently expect all comments to be 'text/plain' but in the future - we may want to default to 'text/html'. If we don't want the old - comments to be interpreted as 'text/html', we would require that - the content type be saved. - - change_hook, primer, settings_properties, and - required_saved_properties are only options to get their defaults - into our local scope. Don't mess with them. - - Set mutable=True if: - * default is a mutable - * your generator function may return mutables - * you set change_hook and might have mutable property values - See the docstrings in libbe.properties for details on how each of - these cases are handled. - """ - settings_properties.append(name) - if require_save == True: - required_saved_properties.append(name) - def decorator(funcs): - fulldoc = doc - if default != None or generator == None: - defaulting = defaulting_property(default=default, null=EMPTY, - mutable_default=mutable) - fulldoc += "\n\nThis property defaults to %s." % default - if generator != None: - cached = cached_property(generator=generator, initVal=EMPTY, - mutable=mutable) - fulldoc += "\n\nThis property is generated with %s." % generator - if check_fn != None: - fn_checked = fn_checked_property(value_allowed_fn=check_fn) - fulldoc += "\n\nThis property is checked with %s." % check_fn - if allowed != None: - checked = checked_property(allowed=allowed) - fulldoc += "\n\nThe allowed values for this property are: %s." \ - % (', '.join(allowed)) - hooked = change_hook_property(hook=change_hook, mutable=mutable, - default=EMPTY) - primed = primed_property(primer=primer, initVal=UNPRIMED) - settings = settings_property(name=name, null=UNPRIMED) - docp = doc_property(doc=fulldoc) - deco = hooked(primed(settings(docp(funcs)))) - if default != None or generator == None: - deco = defaulting(deco) - if generator != None: - deco = cached(deco) - if check_fn != None: - deco = fn_checked(deco) - if allowed != None: - deco = checked(deco) - return Property(deco) - return decorator - -class SavedSettingsObject(object): - - # Keep a list of properties that may be stored in the .settings dict. - #settings_properties = [] - - # A list of properties that we save to disk, even if they were - # never set (in which case we save the default value). This - # protects against future changes in default values. - #required_saved_properties = [] - - _setting_name_to_attr_name = setting_name_to_attr_name - _attr_name_to_setting_name = attr_name_to_setting_name - - def __init__(self): - self._settings_loaded = False - self.storage = None - self.settings = {} - - def load_settings(self): - """Load the settings from disk.""" - # Override. Must call ._setup_saved_settings() after loading. - self.settings = {} - self._setup_saved_settings() - - def _setup_saved_settings(self, flag_as_loaded=True): - """ - To be run after setting self.settings up from disk. Marks all - settings as primed. - """ - for property in self.settings_properties: - if property not in self.settings: - self.settings[property] = EMPTY - elif self.settings[property] == UNPRIMED: - self.settings[property] = EMPTY - if flag_as_loaded == True: - self._settings_loaded = True - - def save_settings(self): - """Load the settings from disk.""" - # Override. Should save the dict output of ._get_saved_settings() - settings = self._get_saved_settings() - pass # write settings to disk.... - - def _get_saved_settings(self): - settings = {} - for k,v in self.settings.items(): - if v != None and v != EMPTY: - settings[k] = v - for k in self.required_saved_properties: - settings[k] = getattr(self, self._setting_name_to_attr_name(k)) - return settings - - def clear_cached_setting(self, setting=None): - "If setting=None, clear *all* cached settings" - if setting != None: - if hasattr(self, "_%s_cached_value" % setting): - delattr(self, "_%s_cached_value" % setting) - else: - for setting in settings_properties: - self.clear_cached_setting(setting) - - -if libbe.TESTING == True: - class SavedSettingsObjectTests(unittest.TestCase): - def testSimpleProperty(self): - """Testing a minimal versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - # access missing setting - self.failUnless(t._settings_loaded == False, t._settings_loaded) - self.failUnless(len(t.settings) == 0, len(t.settings)) - self.failUnless(t.content_type == None, t.content_type) - # accessing t.content_type triggers the priming, which runs - # t._setup_saved_settings, which fills out t.settings with - # EMPTY data. t._settings_loaded is still false though, since - # the default priming does not do any of the `official' loading - # that occurs in t.load_settings. - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - # load settings creates an EMPTY value in the settings array - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t.content_type == None, t.content_type) - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - # now we set a value - t.content_type = 5 - self.failUnless(t.settings["Content-type"] == 5, - t.settings["Content-type"]) - self.failUnless(t.content_type == 5, t.content_type) - self.failUnless(t.settings["Content-type"] == 5, - t.settings["Content-type"]) - # now we set another value - t.content_type = "text/plain" - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t.settings["Content-type"] == "text/plain", - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/plain"}, - t._get_saved_settings()) - # now we clear to the post-primed value - t.content_type = EMPTY - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t.content_type == None, t.content_type) - self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - def testDefaultingProperty(self): - """Testing a defaulting versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings() == {}, - t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t.content_type == "text/html", - t.content_type) - self.failUnless(t.settings["Content-type"] == "text/html", - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/html"}, - t._get_saved_settings()) - def testRequiredDefaultingProperty(self): - """Testing a required defaulting versioned property""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties, - require_save=True) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/plain"}, - t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/html"}, - t._get_saved_settings()) - def testClassVersionedPropertyDefinition(self): - """Testing a class-specific _versioned property decorator""" - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - def _versioned_property(settings_properties= \ - settings_properties, - required_saved_properties= \ - required_saved_properties, - **kwargs): - if "settings_properties" not in kwargs: - kwargs["settings_properties"] = settings_properties - if "required_saved_properties" not in kwargs: - kwargs["required_saved_properties"] = \ - required_saved_properties - return versioned_property(**kwargs) - @_versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - require_save=True) - def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/plain"}, - t._get_saved_settings()) - t.content_type = "text/html" - self.failUnless(t._get_saved_settings() == \ - {"Content-type":"text/html"}, - t._get_saved_settings()) - def testMutableChangeHookedProperty(self): - """Testing a mutable change-hooked property""" - SAVES = [] - def prop_log_save_settings(self, old, new, saves=SAVES): - saves.append("'%s' -> '%s'" % (str(old), str(new))) - prop_save_settings(self, old, new) - class Test(SavedSettingsObject): - settings_properties = [] - required_saved_properties = [] - @versioned_property(name="List-type", - doc="A test property", - mutable=True, - change_hook=prop_log_save_settings, - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) - def list_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) - t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(SAVES == [], SAVES) - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.list_type == None, t.list_type) - self.failUnless(SAVES == [], SAVES) - self.failUnless(t.settings["List-type"]==EMPTY, - t.settings["List-type"]) - t.list_type = [] - self.failUnless(t.settings["List-type"] == [], - t.settings["List-type"]) - self.failUnless(SAVES == [ - "'' -> '[]'" - ], SAVES) - t.list_type.append(5) - self.failUnless(SAVES == [ - "'' -> '[]'", - ], SAVES) - self.failUnless(t.settings["List-type"] == [5], - t.settings["List-type"]) - self.failUnless(SAVES == [ # the append(5) has not yet been saved - "'' -> '[]'", - ], SAVES) - self.failUnless(t.list_type == [5], t.list_type)#get triggers saved - - self.failUnless(SAVES == [ # now the append(5) has been saved. - "'' -> '[]'", - "'[]' -> '[5]'" - ], SAVES) - - unitsuite = unittest.TestLoader().loadTestsFromTestCase( \ - SavedSettingsObjectTests) - suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/storage/util/properties.py b/libbe/storage/util/properties.py new file mode 100644 index 0000000..ddd7b25 --- /dev/null +++ b/libbe/storage/util/properties.py @@ -0,0 +1,642 @@ +# Bugs Everywhere - a distributed bugtracker +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +This module provides a series of useful decorators for defining +various types of properties. For example usage, consider the +unittests at the end of the module. + +See + http://www.python.org/dev/peps/pep-0318/ +and + http://www.phyast.pitt.edu/~micheles/python/documentation.html +for more information on decorators. +""" + +import copy +import types + +import libbe +if libbe.TESTING == True: + import unittest + + +class ValueCheckError (ValueError): + def __init__(self, name, value, allowed): + action = "in" # some list of allowed values + if type(allowed) == types.FunctionType: + action = "allowed by" # some allowed-value check function + msg = "%s not %s %s for %s" % (value, action, allowed, name) + ValueError.__init__(self, msg) + self.name = name + self.value = value + self.allowed = allowed + +def Property(funcs): + """ + End a chain of property decorators, returning a property. + """ + args = {} + args["fget"] = funcs.get("fget", None) + args["fset"] = funcs.get("fset", None) + args["fdel"] = funcs.get("fdel", None) + args["doc"] = funcs.get("doc", None) + + #print "Creating a property with" + #for key, val in args.items(): print key, value + return property(**args) + +def doc_property(doc=None): + """ + Add a docstring to a chain of property decorators. + """ + def decorator(funcs=None): + """ + Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...} + or a function fn() returning such a dict. + """ + if hasattr(funcs, "__call__"): + funcs = funcs() # convert from function-arg to dict + funcs["doc"] = doc + return funcs + return decorator + +def local_property(name, null=None, mutable_null=False): + """ + Define get/set access to per-parent-instance local storage. Uses + .__value to store the value for a particular owner instance. + If the .__value attribute does not exist, returns null. + + If mutable_null == True, we only release deepcopies of the null to + the outside world. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget", None) + fset = funcs.get("fset", None) + def _fget(self): + if fget is not None: + fget(self) + if mutable_null == True: + ret_null = copy.deepcopy(null) + else: + ret_null = null + value = getattr(self, "_%s_value" % name, ret_null) + return value + def _fset(self, value): + setattr(self, "_%s_value" % name, value) + if fset is not None: + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + funcs["name"] = name + return funcs + return decorator + +def settings_property(name, null=None): + """ + Similar to local_property, except where local_property stores the + value in instance.__value, settings_property stores the + value in instance.settings[name]. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget", None) + fset = funcs.get("fset", None) + def _fget(self): + if fget is not None: + fget(self) + value = self.settings.get(name, null) + return value + def _fset(self, value): + self.settings[name] = value + if fset is not None: + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + funcs["name"] = name + return funcs + return decorator + + +# Allow comparison and caching with _original_ values for mutables, +# since +# +# >>> a = [] +# >>> b = a +# >>> b.append(1) +# >>> a +# [1] +# >>> a==b +# True +def _hash_mutable_value(value): + return repr(value) +def _init_mutable_property_cache(self): + if not hasattr(self, "_mutable_property_cache_hash"): + # first call to _fget for any mutable property + self._mutable_property_cache_hash = {} + self._mutable_property_cache_copy = {} +def _set_cached_mutable_property(self, cacher_name, property_name, value): + _init_mutable_property_cache(self) + self._mutable_property_cache_hash[(cacher_name, property_name)] = \ + _hash_mutable_value(value) + self._mutable_property_cache_copy[(cacher_name, property_name)] = \ + copy.deepcopy(value) +def _get_cached_mutable_property(self, cacher_name, property_name, default=None): + _init_mutable_property_cache(self) + if (cacher_name, property_name) not in self._mutable_property_cache_copy: + return default + return self._mutable_property_cache_copy[(cacher_name, property_name)] +def _cmp_cached_mutable_property(self, cacher_name, property_name, value, default=None): + _init_mutable_property_cache(self) + if (cacher_name, property_name) not in self._mutable_property_cache_hash: + _set_cached_mutable_property(self, cacher_name, property_name, default) + old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)] + return cmp(_hash_mutable_value(value), old_hash) + + +def defaulting_property(default=None, null=None, + mutable_default=False): + """ + Define a default value for get access to a property. + If the stored value is null, then default is returned. + + If mutable_default == True, we only release deepcopies of the + default to the outside world. + + null should never escape to the outside world, so don't worry + about it being a mutable. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self): + value = fget(self) + if value == null: + if mutable_default == True: + return copy.deepcopy(default) + else: + return default + return value + def _fset(self, value): + if value == default: + value = null + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +def fn_checked_property(value_allowed_fn): + """ + Define allowed values for get/set access to a property. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self): + value = fget(self) + if value_allowed_fn(value) != True: + raise ValueCheckError(name, value, value_allowed_fn) + return value + def _fset(self, value): + if value_allowed_fn(value) != True: + raise ValueCheckError(name, value, value_allowed_fn) + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +def checked_property(allowed=[]): + """ + Define allowed values for get/set access to a property. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self): + value = fget(self) + if value not in allowed: + raise ValueCheckError(name, value, allowed) + return value + def _fset(self, value): + if value not in allowed: + raise ValueCheckError(name, value, allowed) + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +def cached_property(generator, initVal=None, mutable=False): + """ + Allow caching of values generated by generator(instance), where + instance is the instance to which this property belongs. Uses + .__cache to store a cache flag for a particular owner + instance. + + When the cache flag is True or missing and the stored value is + initVal, the first fget call triggers the generator function, + whose output is stored in __cached_value. That and + subsequent calls to fget will return this cached value. + + If the input value is no longer initVal (e.g. a value has been + loaded from disk or set with fset), that value overrides any + cached value, and this property has no effect. + + When the cache flag is False and the stored value is initVal, the + generator is not cached, but is called on every fget. + + The cache flag is missing on initialization. Particular instances + may override by setting their own flag. + + In the case that mutable == True, all caching is disabled and the + generator is called whenever the cached value would otherwise be + used. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + name = funcs.get("name", "") + def _fget(self): + cache = getattr(self, "_%s_cache" % name, True) + value = fget(self) + if value == initVal: + if cache == True and mutable == False: + if hasattr(self, "_%s_cached_value" % name): + value = getattr(self, "_%s_cached_value" % name) + else: + value = generator(self) + setattr(self, "_%s_cached_value" % name, value) + else: + value = generator(self) + return value + funcs["fget"] = _fget + return funcs + return decorator + +def primed_property(primer, initVal=None): + """ + Just like a cached_property, except that instead of returning a + new value and running fset to cache it, the primer performs some + background manipulation (e.g. loads data into instance.settings) + such that a _second_ pass through fget succeeds. + + The 'cache' flag becomes a 'prime' flag, with priming taking place + whenever .__prime is True, or is False or missing and + value == initVal. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + name = funcs.get("name", "") + def _fget(self): + prime = getattr(self, "_%s_prime" % name, False) + if prime == False: + value = fget(self) + if prime == True or (prime == False and value == initVal): + primer(self) + value = fget(self) + return value + funcs["fget"] = _fget + return funcs + return decorator + +def change_hook_property(hook, mutable=False, default=None): + """ + Call the function hook(instance, old_value, new_value) whenever a + value different from the current value is set (instance is a a + reference to the class instance to which this property belongs). + This is useful for saving changes to disk, etc. This function is + called _after_ the new value has been stored, allowing you to + change the stored value if you want. + + In the case of mutables, things are slightly trickier. Because + the property-owning class has no way of knowing when the value + changes. We work around this by caching a private deepcopy of the + mutable value, and checking for changes whenever the property is + set (obviously) or retrieved (to check for external changes). So + long as you're conscientious about accessing the property after + making external modifications, mutability won't be a problem. + t.x.append(5) # external modification + t.x # dummy access notices change and triggers hook + See testChangeHookMutableProperty for an example of the expected + behavior. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "") + def _fget(self, new_value=None, from_fset=False): # only used if mutable == True + if from_fset == True: + value = new_value # compare new value with cached + else: + value = fget(self) # compare current value with cached + if _cmp_cached_mutable_property(self, "change hook property", name, value, default) != 0: + # there has been a change, cache new value + old_value = _get_cached_mutable_property(self, "change hook property", name, default) + _set_cached_mutable_property(self, "change hook property", name, value) + if from_fset == True: # return previously cached value + value = old_value + else: # the value changed while we weren't looking + hook(self, old_value, value) + return value + def _fset(self, value): + if mutable == True: # get cached previous value + old_value = _fget(self, new_value=value, from_fset=True) + else: + old_value = fget(self) + fset(self, value) + if value != old_value: + hook(self, old_value, value) + if mutable == True: + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +if libbe.TESTING == True: + class DecoratorTests(unittest.TestCase): + def testLocalDoc(self): + class Test(object): + @Property + @doc_property("A fancy property") + def x(): + return {} + self.failUnless(Test.x.__doc__ == "A fancy property", + Test.x.__doc__) + def testLocalProperty(self): + class Test(object): + @Property + @local_property(name="LOCAL") + def x(): + return {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("_LOCAL_value" in dir(t), dir(t)) + self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value) + def testSettingsProperty(self): + class Test(object): + @Property + @settings_property(name="attr") + def x(): + return {} + def __init__(self): + self.settings = {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("attr" in t.settings, t.settings) + self.failUnless(t.settings["attr"] == 'z', t.settings["attr"]) + def testDefaultingLocalProperty(self): + class Test(object): + @Property + @defaulting_property(default='y', null='x') + @local_property(name="DEFAULT", null=5) + def x(): return {} + t = Test() + self.failUnless(t.x == 5, str(t.x)) + t.x = 'x' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'y' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'z' + self.failUnless(t.x == 'z', str(t.x)) + t.x = 5 + self.failUnless(t.x == 5, str(t.x)) + def testCheckedLocalProperty(self): + class Test(object): + @Property + @checked_property(allowed=['x', 'y', 'z']) + @local_property(name="CHECKED") + def x(): return {} + def __init__(self): + self._CHECKED_value = 'x' + t = Test() + self.failUnless(t.x == 'x', str(t.x)) + try: + t.x = None + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + def testTwoCheckedLocalProperties(self): + class Test(object): + @Property + @checked_property(allowed=['x', 'y', 'z']) + @local_property(name="X") + def x(): return {} + + @Property + @checked_property(allowed=['a', 'b', 'c']) + @local_property(name="A") + def a(): return {} + def __init__(self): + self._A_value = 'a' + self._X_value = 'x' + t = Test() + try: + t.x = 'a' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + t.x = 'x' + t.x = 'y' + t.x = 'z' + try: + t.a = 'x' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + t.a = 'a' + t.a = 'b' + t.a = 'c' + def testFnCheckedLocalProperty(self): + class Test(object): + @Property + @fn_checked_property(lambda v : v in ['x', 'y', 'z']) + @local_property(name="CHECKED") + def x(): return {} + def __init__(self): + self._CHECKED_value = 'x' + t = Test() + self.failUnless(t.x == 'x', str(t.x)) + try: + t.x = None + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + def testCachedLocalProperty(self): + class Gen(object): + def __init__(self): + self.i = 0 + def __call__(self, owner): + self.i += 1 + return self.i + class Test(object): + @Property + @cached_property(generator=Gen(), initVal=None) + @local_property(name="CACHED") + def x(): return {} + t = Test() + self.failIf("_CACHED_cache" in dir(t), + getattr(t, "_CACHED_cache", None)) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + t.x = 8 + self.failUnless(t.x == 8, t.x) + self.failUnless(t.x == 8, t.x) + t._CACHED_cache = False # Caching is off, but the stored value + val = t.x # is 8, not the initVal (None), so we + self.failUnless(val == 8, val) # get 8. + t._CACHED_value = None # Now we've set the stored value to None + val = t.x # so future calls to fget (like this) + self.failUnless(val == 2, val) # will call the generator every time... + val = t.x + self.failUnless(val == 3, val) + val = t.x + self.failUnless(val == 4, val) + t._CACHED_cache = True # We turn caching back on, and get + self.failUnless(t.x == 1, str(t.x)) # the original cached value. + del t._CACHED_cached_value # Removing that value forces a + self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call + self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which + self.failUnless(t.x == 5, str(t.x)) # we get the new cached value. + def testPrimedLocalProperty(self): + class Test(object): + def prime(self): + self.settings["PRIMED"] = "initialized" + @Property + @primed_property(primer=prime, initVal=None) + @settings_property(name="PRIMED") + def x(): return {} + def __init__(self): + self.settings={} + t = Test() + self.failIf("_PRIMED_prime" in dir(t), + getattr(t, "_PRIMED_prime", None)) + self.failUnless(t.x == "initialized", t.x) + t.x = 1 + self.failUnless(t.x == 1, t.x) + t.x = None + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = True + t.x = 3 + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = False + t.x = 3 + self.failUnless(t.x == 3, t.x) + def testChangeHookLocalProperty(self): + class Test(object): + def _hook(self, old, new): + self.old = old + self.new = new + + @Property + @change_hook_property(_hook) + @local_property(name="HOOKED") + def x(): return {} + t = Test() + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 2 + self.failUnless(t.old == 1, t.old) + self.failUnless(t.new == 2, t.new) + def testChangeHookMutableProperty(self): + class Test(object): + def _hook(self, old, new): + self.old = old + self.new = new + self.hook_calls += 1 + + @Property + @change_hook_property(_hook, mutable=True) + @local_property(name="HOOKED") + def x(): return {} + t = Test() + t.hook_calls = 0 + t.x = [] + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == [], t.new) + self.failUnless(t.hook_calls == 1, t.hook_calls) + a = t.x + a.append(5) + t.x = a + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 2, t.hook_calls) + t.x = [] + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [], t.new) + self.failUnless(t.hook_calls == 3, t.hook_calls) + # now append without reassigning. this doesn't trigger the + # change, since we don't ever set t.x, only get it and mess + # with it. It does, however, update our t.new, since t.new = + # t.x and is not a static copy. + t.x.append(5) + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 3, t.hook_calls) + # however, the next t.x get _will_ notice the change... + a = t.x + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 4, t.hook_calls) + t.x.append(6) # this append(6) is not noticed yet + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5,6], t.new) + self.failUnless(t.hook_calls == 4, t.hook_calls) + # this append(7) is not noticed, but the t.x get causes the + # append(6) to be noticed + t.x.append(7) + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [5,6,7], t.new) + self.failUnless(t.hook_calls == 5, t.hook_calls) + a = t.x # now the append(7) is noticed + self.failUnless(t.old == [5,6], t.old) + self.failUnless(t.new == [5,6,7], t.new) + self.failUnless(t.hook_calls == 6, t.hook_calls) + + suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py new file mode 100644 index 0000000..be119dd --- /dev/null +++ b/libbe/storage/util/settings_object.py @@ -0,0 +1,433 @@ +# Bugs Everywhere - a distributed bugtracker +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +This module provides a base class implementing settings-dict based +property storage useful for BE objects with saved properties +(e.g. BugDir, Bug, Comment). For example usage, consider the +unittests at the end of the module. +""" + +import libbe +from properties import Property, doc_property, local_property, \ + defaulting_property, checked_property, fn_checked_property, \ + cached_property, primed_property, change_hook_property, \ + settings_property +if libbe.TESTING == True: + import doctest + import unittest + +class _Token (object): + """ + `Control' value class for properties. We want values that only + mean something to the settings_object module. + """ + pass + +class UNPRIMED (_Token): + "Property has not been primed." + pass + +class EMPTY (_Token): + """ + Property has been primed but has no user-set value, so use + default/generator value. + """ + pass + + +def prop_save_settings(self, old, new): + """ + The default action undertaken when a property changes. + """ + if self.storage != None and self.storage.is_writeable(): + self.save_settings() + +def prop_load_settings(self): + """ + The default action undertaken when an UNPRIMED property is accessed. + """ + if self.storage != None and self.storage.is_readable() \ + and self._settings_loaded==False: + self.load_settings() + else: + self._setup_saved_settings(flag_as_loaded=False) + +# Some name-mangling routines for pretty printing setting names +def setting_name_to_attr_name(self, name): + """ + Convert keys to the .settings dict into their associated + SavedSettingsObject attribute names. + >>> print setting_name_to_attr_name(None,"User-id") + user_id + """ + return name.lower().replace('-', '_') + +def attr_name_to_setting_name(self, name): + """ + The inverse of setting_name_to_attr_name. + >>> print attr_name_to_setting_name(None, "user_id") + User-id + """ + return name.capitalize().replace('_', '-') + + +def versioned_property(name, doc, + default=None, generator=None, + change_hook=prop_save_settings, + mutable=False, + primer=prop_load_settings, + allowed=None, check_fn=None, + settings_properties=[], + required_saved_properties=[], + require_save=False): + """ + Combine the common decorators in a single function. + + Use zero or one (but not both) of default or generator, since a + working default will keep the generator from functioning. Use the + default if you know what you want the default value to be at + 'coding time'. Use the generator if you can write a function to + determine a valid default at run time. If both default and + generator are None, then the property will be a defaulting + property which defaults to None. + + allowed and check_fn have a similar relationship, although you can + use both of these if you want. allowed compares the proposed + value against a list determined at 'coding time' and check_fn + allows more flexible comparisons to take place at run time. + + Set require_save to True if you want to save the default/generated + value for a property, to protect against future changes. E.g., we + currently expect all comments to be 'text/plain' but in the future + we may want to default to 'text/html'. If we don't want the old + comments to be interpreted as 'text/html', we would require that + the content type be saved. + + change_hook, primer, settings_properties, and + required_saved_properties are only options to get their defaults + into our local scope. Don't mess with them. + + Set mutable=True if: + * default is a mutable + * your generator function may return mutables + * you set change_hook and might have mutable property values + See the docstrings in libbe.properties for details on how each of + these cases are handled. + """ + settings_properties.append(name) + if require_save == True: + required_saved_properties.append(name) + def decorator(funcs): + fulldoc = doc + if default != None or generator == None: + defaulting = defaulting_property(default=default, null=EMPTY, + mutable_default=mutable) + fulldoc += "\n\nThis property defaults to %s." % default + if generator != None: + cached = cached_property(generator=generator, initVal=EMPTY, + mutable=mutable) + fulldoc += "\n\nThis property is generated with %s." % generator + if check_fn != None: + fn_checked = fn_checked_property(value_allowed_fn=check_fn) + fulldoc += "\n\nThis property is checked with %s." % check_fn + if allowed != None: + checked = checked_property(allowed=allowed) + fulldoc += "\n\nThe allowed values for this property are: %s." \ + % (', '.join(allowed)) + hooked = change_hook_property(hook=change_hook, mutable=mutable, + default=EMPTY) + primed = primed_property(primer=primer, initVal=UNPRIMED) + settings = settings_property(name=name, null=UNPRIMED) + docp = doc_property(doc=fulldoc) + deco = hooked(primed(settings(docp(funcs)))) + if default != None or generator == None: + deco = defaulting(deco) + if generator != None: + deco = cached(deco) + if check_fn != None: + deco = fn_checked(deco) + if allowed != None: + deco = checked(deco) + return Property(deco) + return decorator + +class SavedSettingsObject(object): + + # Keep a list of properties that may be stored in the .settings dict. + #settings_properties = [] + + # A list of properties that we save to disk, even if they were + # never set (in which case we save the default value). This + # protects against future changes in default values. + #required_saved_properties = [] + + _setting_name_to_attr_name = setting_name_to_attr_name + _attr_name_to_setting_name = attr_name_to_setting_name + + def __init__(self): + self._settings_loaded = False + self.storage = None + self.settings = {} + + def load_settings(self): + """Load the settings from disk.""" + # Override. Must call ._setup_saved_settings() after loading. + self.settings = {} + self._setup_saved_settings() + + def _setup_saved_settings(self, flag_as_loaded=True): + """ + To be run after setting self.settings up from disk. Marks all + settings as primed. + """ + for property in self.settings_properties: + if property not in self.settings: + self.settings[property] = EMPTY + elif self.settings[property] == UNPRIMED: + self.settings[property] = EMPTY + if flag_as_loaded == True: + self._settings_loaded = True + + def save_settings(self): + """Load the settings from disk.""" + # Override. Should save the dict output of ._get_saved_settings() + settings = self._get_saved_settings() + pass # write settings to disk.... + + def _get_saved_settings(self): + settings = {} + for k,v in self.settings.items(): + if v != None and v != EMPTY: + settings[k] = v + for k in self.required_saved_properties: + settings[k] = getattr(self, self._setting_name_to_attr_name(k)) + return settings + + def clear_cached_setting(self, setting=None): + "If setting=None, clear *all* cached settings" + if setting != None: + if hasattr(self, "_%s_cached_value" % setting): + delattr(self, "_%s_cached_value" % setting) + else: + for setting in settings_properties: + self.clear_cached_setting(setting) + + +if libbe.TESTING == True: + class SavedSettingsObjectTests(unittest.TestCase): + def testSimpleProperty(self): + """Testing a minimal versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + # access missing setting + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(len(t.settings) == 0, len(t.settings)) + self.failUnless(t.content_type == None, t.content_type) + # accessing t.content_type triggers the priming, which runs + # t._setup_saved_settings, which fills out t.settings with + # EMPTY data. t._settings_loaded is still false though, since + # the default priming does not do any of the `official' loading + # that occurs in t.load_settings. + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + # load settings creates an EMPTY value in the settings array + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + # now we set a value + t.content_type = 5 + self.failUnless(t.settings["Content-type"] == 5, + t.settings["Content-type"]) + self.failUnless(t.content_type == 5, t.content_type) + self.failUnless(t.settings["Content-type"] == 5, + t.settings["Content-type"]) + # now we set another value + t.content_type = "text/plain" + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == "text/plain", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + # now we clear to the post-primed value + t.content_type = EMPTY + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + def testDefaultingProperty(self): + """Testing a defaulting versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == {}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t.content_type == "text/html", + t.content_type) + self.failUnless(t.settings["Content-type"] == "text/html", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testRequiredDefaultingProperty(self): + """Testing a required defaulting versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties, + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testClassVersionedPropertyDefinition(self): + """Testing a class-specific _versioned property decorator""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + def _versioned_property(settings_properties= \ + settings_properties, + required_saved_properties= \ + required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"] = \ + required_saved_properties + return versioned_property(**kwargs) + @_versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testMutableChangeHookedProperty(self): + """Testing a mutable change-hooked property""" + SAVES = [] + def prop_log_save_settings(self, old, new, saves=SAVES): + saves.append("'%s' -> '%s'" % (str(old), str(new))) + prop_save_settings(self, old, new) + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="List-type", + doc="A test property", + mutable=True, + change_hook=prop_log_save_settings, + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def list_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._settings_loaded == False, t._settings_loaded) + t.load_settings() + self.failUnless(SAVES == [], SAVES) + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.list_type == None, t.list_type) + self.failUnless(SAVES == [], SAVES) + self.failUnless(t.settings["List-type"]==EMPTY, + t.settings["List-type"]) + t.list_type = [] + self.failUnless(t.settings["List-type"] == [], + t.settings["List-type"]) + self.failUnless(SAVES == [ + "'' -> '[]'" + ], SAVES) + t.list_type.append(5) + self.failUnless(SAVES == [ + "'' -> '[]'", + ], SAVES) + self.failUnless(t.settings["List-type"] == [5], + t.settings["List-type"]) + self.failUnless(SAVES == [ # the append(5) has not yet been saved + "'' -> '[]'", + ], SAVES) + self.failUnless(t.list_type == [5], t.list_type)#get triggers saved + + self.failUnless(SAVES == [ # now the append(5) has been saved. + "'' -> '[]'", + "'[]' -> '[5]'" + ], SAVES) + + unitsuite = unittest.TestLoader().loadTestsFromTestCase( \ + SavedSettingsObjectTests) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) -- cgit From 3e6096fb5bcb9c9e8a50faa76461da96d145ca8f Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 20:02:34 -0500 Subject: Reworked test.py to handle deeper directory structure --- libbe/bug.py | 2 +- libbe/bugdir.py | 2 +- libbe/comment.py | 2 +- libbe/plugin.py | 75 ------------------------------------ libbe/util/plugin.py | 75 ++++++++++++++++++++++++++++++++++++ test.py | 105 +++++++++++++++++++++++++++++++-------------------- 6 files changed, 143 insertions(+), 118 deletions(-) delete mode 100644 libbe/plugin.py create mode 100644 libbe/util/plugin.py diff --git a/libbe/bug.py b/libbe/bug.py index 84eb1ed..5755b63 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -34,7 +34,7 @@ import xml.sax.saxutils import libbe import libbe.util.id -from libbe.storage.util.properties import Property, doc_property, +from libbe.storage.util.properties import Property, doc_property, \ local_property, defaulting_property, checked_property, cached_property, \ primed_property, change_hook_property, settings_property import libbe.storage.settings_object as settings_object diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 4435624..02ff96f 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -32,7 +32,7 @@ import time import libbe import libbe.util.encoding as encoding import libbe.storage as storage -from libbe.storage.util.properties import Property, doc_property, +from libbe.storage.util.properties import Property, doc_property, \ local_property, defaulting_property, checked_property, \ fn_checked_property, cached_property, primed_property, \ change_hook_property, settings_property diff --git a/libbe/comment.py b/libbe/comment.py index 987e39c..f5a6309 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -34,7 +34,7 @@ import xml.sax.saxutils import libbe import libbe.util.id -from libbe.storage.util.properties import Property, doc_property, +from libbe.storage.util.properties import Property, doc_property, \ local_property, defaulting_property, checked_property, cached_property, \ primed_property, change_hook_property, settings_property import libbe.storage.settings_object as settings_object diff --git a/libbe/plugin.py b/libbe/plugin.py deleted file mode 100644 index 03f68fc..0000000 --- a/libbe/plugin.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# Marien Zwart -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Allow simple listing and loading of the various becommands and libbe -submodules (i.e. "plugins"). -""" - -import os -import os.path -import sys - -import libbe -if libbe.TESTING == True: - import doctest - -def my_import(mod_name): - module = __import__(mod_name) - components = mod_name.split('.') - for comp in components[1:]: - module = getattr(module, comp) - return module - -def iter_plugins(prefix): - """ - >>> "list" in [n for n,m in iter_plugins("becommands")] - True - >>> "plugin" in [n for n,m in iter_plugins("libbe")] - True - """ - modfiles = os.listdir(os.path.join(plugin_path, prefix)) - modfiles.sort() - for modfile in modfiles: - if modfile.startswith('.'): - continue # the occasional emacs temporary file - if modfile.endswith(".py") and modfile != "__init__.py": - yield modfile[:-3], my_import(prefix+"."+modfile[:-3]) - - -def get_plugin(prefix, name): - """ - >>> get_plugin("becommands", "asdf") is None - True - >>> q = repr(get_plugin("becommands", "list")) - >>> q.startswith(" +# Marien Zwart +# W. Trevor King +# +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Allow simple listing and loading of the various becommands and libbe +submodules (i.e. "plugins"). +""" + +import os +import os.path +import sys + +import libbe +if libbe.TESTING == True: + import doctest + +def import_by_name(mod_name): + module = __import__(mod_name) + components = mod_name.split('.') + for comp in components[1:]: + module = getattr(module, comp) + return module + +def iter_plugins(prefix): + """ + >>> "list" in [n for n,m in iter_plugins("becommands")] + True + >>> "plugin" in [n for n,m in iter_plugins("libbe")] + True + """ + modfiles = os.listdir(os.path.join(plugin_path, prefix)) + modfiles.sort() + for modfile in modfiles: + if modfile.startswith('.'): + continue # the occasional emacs temporary file + if modfile.endswith(".py") and modfile != "__init__.py": + yield modfile[:-3], my_import(prefix+"."+modfile[:-3]) + + +def get_plugin(prefix, name): + """ + >>> get_plugin("becommands", "asdf") is None + True + >>> q = repr(get_plugin("becommands", "list")) + >>> q.startswith(" 1: - for submodname in sys.argv[1:]: - match = False - mod = plugin.get_plugin("libbe", submodname) - if mod is not None: - if hasattr(mod, "suite"): - suite.addTest(mod.suite) - match = True - else: - print "Module \"%s\" has no test suite" % submodname - mod = plugin.get_plugin("becommands", submodname) - if mod is not None: - if hasattr(mod, "suite"): - suite.addTest(mod.suite) - else: - suite.addTest(doctest.DocTestSuite(mod)) - match = True - if not match: - print "No modules match \"%s\"" % submodname - sys.exit(1) -else: - failed = False - for modname,module in plugin.iter_plugins("libbe"): - if not hasattr(module, "suite"): +def python_tree(root_path='libbe', root_modname='libbe'): + tree = Tree() + tree.path = root_path + tree.parent = None + stack = [tree] + while len(stack) > 0: + f = stack.pop(0) + if f.path.endswith('.py'): + f.name = os.path.basename(f.path)[:-len('.py')] + elif os.path.isdir(f.path) \ + and os.path.exists(os.path.join(f.path, '__init__.py')): + f.name = os.path.basename(f.path) + f.is_module = True + for child in os.listdir(f.path): + if child == '__init__.py': + continue + c = Tree() + c.path = os.path.join(f.path, child) + c.parent = f + stack.append(c) + else: continue - suite.addTest(module.suite) - for modname,module in plugin.iter_plugins("becommands"): - suite.addTest(doctest.DocTestSuite(module)) + if f.parent == None: + f.modname = root_modname + else: + f.modname = f.parent.modname + '.' + f.name + f.parent.append(f) + return tree + +def add_module_tests(suite, module_name): + mod = import_by_name(module_name) + if mod == None: + raise KeyError, 'module "%s" not found' % module_name + if hasattr(mod, 'suite'): + s = mod.suite + else: + s = unittest.TestLoader().loadTestsFromModule(mod) + suite.addTest(s) -_vcs = vcs.installed_vcs() -vcs.set_preferred_vcs(_vcs.name) -print 'Using %s as the testing VCS' % _vcs.name +suite = unittest.TestSuite() +tree = python_tree() +if len(sys.argv) <= 1: + for node in tree.traverse(): + add_module_tests(suite, node.modname) +else: + added = [] + for modname in sys.argv[1:]: + for node in tree.traverse(): + if node.modname == modname: + for n in node.traverse(): + if n.modname not in added: + add_module_tests(suite, n.modname) + added.append(n.modname) + break result = unittest.TextTestRunner(verbosity=2).run(suite) -- cgit From f52fc3a243edf5ccef2dcdfd0c4b4cded4357e13 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Wed, 9 Dec 2009 07:23:54 -0500 Subject: Rethought libbe.util.id module --- libbe/util/id.py | 294 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 254 insertions(+), 40 deletions(-) diff --git a/libbe/util/id.py b/libbe/util/id.py index d57205f..d443706 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -20,10 +20,13 @@ Handle ID creation and parsing. """ import os.path +import re import libbe if libbe.TESTING == True: + import doctest + import sys import unittest try: @@ -60,6 +63,25 @@ except ImportError: return output.rstrip('\n') +HIERARCHY = ['bugdir', 'bug', 'comment'] + + +class MultipleIDMatches (ValueError): + def __init__(self, id, matches): + msg = ("More than one id matches %s. " + "Please be more specific.\n%s" % (id, matches)) + ValueError.__init__(self, msg) + self.id = id + self.matches = matches + +class NoIDMatches (KeyError): + def __init__(self, id, possible_ids): + msg = "No id matches %s.\n%s" % (id, possible_ids) + KeyError.__init__(self, msg) + self.id = id + self.possible_ids = possible_ids + + def _assemble(*args): args = list(args) for i,arg in enumerate(args): @@ -74,50 +96,242 @@ def _split(id): args[i] = None return args -def _is_a_uuid(id): - if id.startswith('uuid:'): - return True - return False - -def _uuid_to_id(id): - return 'uuid:' + id - -def _id_to_uuid(id): - return id[len('uuid:'):] - -def bugdir_id(bugdir, *args): - return _assemble(_uuid_to_id(bugdir.uuid), *args) - -def bug_id(bug, *args): - if bug.bugdir == None: - bdid = None - else: - bdid = bugdir_id(bug.bugdir) - return _assemble(bdid, _uuid_to_id(bug.uuid), *args) - -def comment_id(comment, *args): - if comment.bug == None: - bid = None - else: - bid = bug_id(comment.bug) - return _assemble(bid, _uuid_to_id(comment.uuid), *args) - -def parse_id(id): - args = _split(id) - ret = {'bugdir':_id_to_uuid(args.pop(0))} - type = 'bugdir' - for child_name in ['bug', 'comment']: - if len(args) > 0 and _is_a_uuid(args[0]): - ret[child_name] = _id_to_uuid(args.pop(0)) - type = child_name - ret['type'] = type - ret['remaining'] = os.path.join(args) +def _truncate(uuid, other_uuids, min_length=3): + chars = min_length + for id in other_uuids: + if id == uuid: + continue + while (id[:chars] == uuid[:chars]): + chars+=1 + return uuid[:chars] + +def _expand(truncated_id, other_ids): + matches = [] + for id in other_ids: + if id.startswith(truncated_id): + matches.append(id) + if len(matches) > 1: + raise MultipleIDMatches(truncated_id, matches) + if len(matches) == 0: + raise NoIDMatches(truncated_id, other_ids) + return matches[0] + + +class ID (object): + """ + IDs have several formats specialized for different uses. + + In storage, all objects are represented by their uuid alone, + because that is the simplest globally unique identifier. You can + generate ids of this sort with the .storage() method. Because an + object's storage may be distributed across several chunks, and the + chunks may not have their own uuid, we generate chunk ids by + prepending the objects uuid to the chunk name. The user id types + do not support this chunk extension feature. + + For users, the full uuids are a bit overwhelming, so we truncate + them while retaining local uniqueness (with regards to the other + objects currently in storage). We also prepend truncated parent + ids for two reasons: + (1) so that a user can locate the repository containing the + referenced object. It would be hard to find bug 'XYZ' if + that's all you knew. Much easier with 'ABC/XYZ', where ABC + is the bugdir. Each project can publish a list of bugdir-id +x - to - location mappings, e.g. + ABC...(full uuid)...DEF https://server.com/projectX/be/ + which is easier than publishing all-object-ids-to-location + mappings. + (2) because it's easier to generate and parse truncated ids if + you don't have to fetch all the ids in the storage + repository, but can restrict yourself to a specific branch. + You can generate ids of this sort with the .user() method, + although in order to preform the truncation, your object (and its + parents must define a .sibling_uuids() method. + + + While users can use the convenient short user ids in the short + term, the truncation will inevitably lead to name collision. To + avoid that, we provide a non-truncated form of the short user ids + via the .long_user() method. These long user ids should be + converted to short user ids by intelligent user interfaces. + + Related tools: + * get uuids back out of the user ids: + parse_user() + * scan text for user ids & convert to long user ids: + short_to_long_user() + * scan text for long user ids & convert to short user ids: + long_to_short_user() + + Supported types: 'bugdir', 'bug', 'comment' + """ + def __init__(self, object, type): + self._object = object + self._type = type + assert self._type in HIERARCHY, self._type + self.uuid = self._object.uuid + + def storage(self, *args): + return _assemble(self._object.uuid, *args) + + def _ancestors(self): + ret = [self._object] + index = HIERARCHY.index(self._type) + if index == 0: + return ret + o = self._object + for i in range(index, 0, -1): + parent_name = HIERARCHY[i-1] + o = getattr(o, parent_name) + ret.insert(0, o) + return ret + + def long_user(self): + return _assemble(*[o.uuid for o in self._ancestors()]) + + def user(self): + return _assemble(*[_truncate(o.uuid, o.sibling_uuids()) + for o in self._ancestors()]) + +def parse_user(id): + """ + >>> parse_user('ABC/DEF/GHI') == \\ + ... {'bugdir':'ABC', 'bug':'DEF', 'comment':'GHI', 'type':'comment'} + True + >>> parse_user('ABC/DEF') == \\ + ... {'bugdir':'ABC', 'bug':'DEF', 'type':'bug'} + True + >>> parse_user('ABC') == \\ + ... {'bugdir':'ABC', 'type':'bugdir'} + True + """ + ret = {} + args = _split(id) + assert len(args) > 0 and len(args) < 4, 'Invalid id "%s"' % id + for type,arg in zip(HIERARCHY, args): + assert len(arg) > 0, 'Invalid part "%s" of id "%s"' % (arg, id) + ret['type'] = type + ret[type] = arg return ret +REGEXP = '#([-a-f0-9]*)(/[-a-g0-9]*)?(/[-a-g0-9]*)?#' + +class IDreplacer (object): + def __init__(self, bugdirs, direction): + self.bugdirs = bugdirs + self.direction = direction + def __call__(self, match): + ids = [m.lstrip('/') for m in match.groups() if m != None] + ids = self.switch_ids(ids) + return '#' + '/'.join(ids) + '#' + def switch_id(self, id, sibling_uuids): + if id == None: + return None + if self.direction == 'long_to_short': + return _truncate(id, sibling_uuids) + return _expand(id, sibling_uuids) + def switch_ids(self, ids): + assert ids[0] != None, ids + if self.direction == 'long_to_short': + bugdir = [bd for bd in self.bugdirs if bd.uuid == ids[0]][0] + objects = [bugdir] + if len(ids) >= 2: + bug = bugdir.bug_from_uuid(ids[1]) + objects.append(bug) + if len(ids) >= 3: + comment = bug.comment_from_uuid(ids[2]) + objects.append(comment) + for i,obj in enumerate(objects): + ids[i] = self.switch_id(ids[i], obj.sibling_uuids()) + else: + ids[0] = self.switch_id(ids[0], [bd.uuid for bd in self.bugdirs]) + if len(ids) == 1: + return ids + bugdir = [bd for bd in self.bugdirs if bd.uuid == ids[0]][0] + ids[1] = self.switch_id(ids[1], bugdir.uuids()) + if len(ids) == 2: + return ids + bug = bugdir.bug_from_uuid(ids[1]) + ids[2] = self.switch_id(ids[2], bug.uuids()) + return ids + +def short_to_long_user(bugdirs, text): + return re.sub(REGEXP, IDreplacer(bugdirs, 'short_to_long'), text) +def long_to_short_user(bugdirs, text): + return re.sub(REGEXP, IDreplacer(bugdirs, 'long_to_short'), text) + if libbe.TESTING == True: class UUIDtestCase(unittest.TestCase): def testUUID_gen(self): id = uuid_gen() - self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) + self.failUnless(len(id) == 36, 'invalid UUID "%s"' % id) + + class DummyObject (object): + def __init__(self, uuid, siblings=[]): + self.uuid = uuid + self._siblings = siblings + def sibling_uuids(self): + return self._siblings + + class IDtestCase(unittest.TestCase): + def setUp(self): + self.bugdir = DummyObject('1234abcd') + self.bug = DummyObject('abcdef', ['a1234', 'ab9876']) + self.bug.bugdir = self.bugdir + self.comment = DummyObject('12345678', ['1234abcd', '1234cdef']) + self.comment.bug = self.bug + self.bd_id = ID(self.bugdir, 'bugdir') + self.b_id = ID(self.bug, 'bug') + self.c_id = ID(self.comment, 'comment') + def test_storage(self): + self.failUnless(self.bd_id.storage() == self.bugdir.uuid, + self.bd_id.storage()) + self.failUnless(self.b_id.storage() == self.bug.uuid, + self.b_id.storage()) + self.failUnless(self.c_id.storage() == self.comment.uuid, + self.c_id.storage()) + self.failUnless(self.bd_id.storage('x','y','z') == \ + '1234abcd/x/y/z', self.bd_id.storage()) + def test_long_user(self): + self.failUnless(self.bd_id.long_user() == self.bugdir.uuid, + self.bd_id.long_user()) + self.failUnless(self.b_id.long_user() == \ + '/'.join([self.bugdir.uuid, self.bug.uuid]), + self.b_id.long_user()) + self.failUnless(self.c_id.long_user() == + '/'.join([self.bugdir.uuid, self.bug.uuid, + self.comment.uuid]), + self.c_id.long_user) + def test_user(self): + self.failUnless(self.bd_id.user() == '123', + self.bd_id.user()) + self.failUnless(self.b_id.user() == '123/abc', + self.b_id.user()) + self.failUnless(self.c_id.user() == '123/abc/12345', + self.c_id.user()) + + class IDtestCase(unittest.TestCase): + def setUp(self): + self.bugdir = DummyObject('1234abcd') + self.bug = DummyObject('abcdef', ['a1234', 'ab9876']) + self.bug.bugdir = self.bugdir + self.bugdir.bug_from_uuid = lambda uuid: self.bug + self.bugdir.uuids = lambda : self.bug.sibling_uuids() + [self.bug.uuid] + self.comment = DummyObject('12345678', ['1234abcd', '1234cdef']) + self.comment.bug = self.bug + self.bug.comment_from_uuid = lambda uuid: self.comment + self.bug.uuids = lambda : self.comment.sibling_uuids() + [self.comment.uuid] + self.bd_id = ID(self.bugdir, 'bugdir') + self.b_id = ID(self.bug, 'bug') + self.c_id = ID(self.comment, 'comment') + self.short = 'bla bla #123/abc# bla bla #123/abc/12345# bla bla' + self.long = 'bla bla #1234abcd/abcdef# bla bla #1234abcd/abcdef/12345678# bla bla' + def test_short_to_long(self): + self.failUnless(short_to_long_user([self.bugdir], self.short) == self.long, + '\n' + self.short + '\n' + short_to_long_user([self.bugdir], self.short) + '\n' + self.long) + def test_long_to_short(self): + self.failUnless(long_to_short_user([self.bugdir], self.long) == self.short, + '\n' + long_to_short_user([self.bugdir], self.long) + '\n' + self.short) - suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) -- cgit From a153347564e4c6baa0388fda05530f5548d16ac5 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 10 Dec 2009 19:31:47 -0500 Subject: Moved bugdir, bug, and comment over to new id implementation. --- libbe/bug.py | 168 ++++++++++++++++++++----------------------------- libbe/bugdir.py | 84 ++++++++----------------- libbe/comment.py | 187 ++++++++++++++++--------------------------------------- libbe/util/id.py | 28 +++++++-- 4 files changed, 171 insertions(+), 296 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index 5755b63..1f96779 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -237,35 +237,25 @@ class Bug(settings_object.SavedSettingsObject): @doc_property(doc="The trunk of the comment tree. We use a dummy root comment by default, because there can be several comment threads rooted on the same parent bug. To simplify comment interaction, we condense these threads into a single thread with a Comment dummy root.") def comment_root(): return {} - def _get_storage(self): - if hasattr(self.bugdir, "storage"): - return self.bugdir.storage - - @Property - @cached_property(generator=_get_storage) - @local_property("storage") - @doc_property(doc="A revision control system instance.") - def storage(): return {} - def __init__(self, bugdir=None, uuid=None, from_storage=False, load_comments=False, summary=None): settings_object.SavedSettingsObject.__init__(self) self.bugdir = bugdir + self.storage = None self.uuid = uuid + self.id = libbe.util.id.ID(self, 'bug') if from_storage == False: if uuid == None: self.uuid = libbe.util.id.uuid_gen() self.settings = {} self._setup_saved_settings() - if self.storage != None and self.storage.is_writeable(): - self.storage.writeable = False - set_writeable = True - else: - set_writeable = False self.time = int(time.time()) # only save to second precision self.summary = summary - if set_writeable == True: - self.storage.writeable = True + dummy = self.comment_root + if self.bugdir != None: + self.storage = self.bugdir.storage + if from_storage == False: + if self.storage != None and self.storage.is_writeable(): self.save() def __repr__(self): @@ -287,20 +277,47 @@ class Bug(settings_object.SavedSettingsObject): return str(value) return value - def xml(self, indent=0, shortname=None, show_comments=False): - if shortname == None: - if self.bugdir == None: - shortname = self.uuid + def string(self, shortlist=False, show_comments=False): + if shortlist == False: + if self.time == None: + timestring = "" else: - shortname = self.bugdir.bug_shortname(self) + htime = utility.handy_time(self.time) + timestring = "%s (%s)" % (htime, self.time_string) + info = [("ID", self.uuid), + ("Short name", self.id.user()), + ("Severity", self.severity), + ("Status", self.status), + ("Assigned", self._setting_attr_string("assigned")), + ("Reporter", self._setting_attr_string("reporter")), + ("Creator", self._setting_attr_string("creator")), + ("Created", timestring)] + 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') + else: + statuschar = self.status[0] + severitychar = self.severity[0] + chars = "%c%c" % (statuschar, severitychar) + bugout = "%s:%s: %s" % (self.id.user(),chars,self.summary.rstrip('\n')) + + if show_comments == True: + # take advantage of the string_thread(auto_name_map=True) + # SIDE-EFFECT of sorting by comment time. + comout = self.comment_root.string_thread(flatten=False) + output = bugout + '\n' + comout.rstrip('\n') + else : + output = bugout + return output + def xml(self, indent=0, show_comments=False): if self.time == None: timestring = "" else: timestring = utility.time_to_str(self.time) info = [('uuid', self.uuid), - ('short-name', shortname), + ('short-name', self.id.user()), ('severity', self.severity), ('status', self.status), ('assigned', self.assigned), @@ -315,9 +332,7 @@ class Bug(settings_object.SavedSettingsObject): for estr in self.extra_strings: lines.append(' %s' % estr) if show_comments == True: - comout = self.comment_root.xml_thread(indent=indent+2, - auto_name_map=True, - bug_shortname=shortname) + comout = self.comment_root.xml_thread(indent=indent+2) if len(comout) > 0: lines.append(comout) lines.append('') @@ -335,16 +350,16 @@ class Bug(settings_object.SavedSettingsObject): >>> commA = bugA.comment_root.new_reply(body='comment A') >>> commB = bugA.comment_root.new_reply(body='comment B') >>> commC = commA.new_reply(body='comment C') - >>> xml = bugA.xml(shortname="bug-1", show_comments=True) + >>> xml = bugA.xml(show_comments=True) >>> bugB = Bug() >>> bugB.from_xml(xml, verbose=True) - >>> bugB.xml(shortname="bug-1", show_comments=True) == xml + >>> bugB.xml(show_comments=True) == xml False >>> bugB.uuid = bugB.alt_id >>> for comm in bugB.comments(): ... comm.uuid = comm.alt_id ... comm.alt_id = None - >>> bugB.xml(shortname="bug-1", show_comments=True) == xml + >>> bugB.xml(show_comments=True) == xml True >>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE ['severity', 'status', 'creator', 'created', 'summary'] @@ -414,10 +429,10 @@ class Bug(settings_object.SavedSettingsObject): >>> commC.uuid = 'commC' >>> commC.in_reply_to = commA.uuid >>> bugA.add_comment(commC) - >>> print bugA.xml(shortname="bug-1", show_comments=True) # doctest: +ELLIPSIS + >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS 0123 - bug-1 + /012 minor open Jack @@ -425,7 +440,7 @@ class Bug(settings_object.SavedSettingsObject): Need to test Bug.add_comment() commA - bug-1:1 + /012/commA ... text/plain @@ -433,7 +448,7 @@ class Bug(settings_object.SavedSettingsObject): commC - bug-1:2 + /012/commC commA ... @@ -442,7 +457,7 @@ class Bug(settings_object.SavedSettingsObject): commB - bug-1:3 + /012/commB ... text/plain @@ -546,7 +561,7 @@ class Bug(settings_object.SavedSettingsObject): >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS 0123 - 0123 + /012 minor open John @@ -557,7 +572,7 @@ class Bug(settings_object.SavedSettingsObject): TAG: very helpful uuid-commA - 0123:1 + /012/uuid-commA ... text/plain @@ -565,7 +580,7 @@ class Bug(settings_object.SavedSettingsObject): uuid-commB - 0123:2 + /012/uuid-commB ... text/plain @@ -603,6 +618,7 @@ class Bug(settings_object.SavedSettingsObject): if accept_comments == True: o_comm_copy = copy.copy(o_comm) o_comm_copy.bug = self + o_comm_copy.id = libbe.util.id.ID(o_comm_copy, 'comment') self.comment_root.add_reply(o_comm_copy) elif change_exception == True: raise ValueError, \ @@ -613,63 +629,18 @@ class Bug(settings_object.SavedSettingsObject): accept_extra_strings=accept_extra_strings, change_exception=change_exception) - def string(self, shortlist=False, show_comments=False): - if self.bugdir == None: - shortname = self.uuid - else: - shortname = self.bugdir.bug_shortname(self) - if shortlist == False: - if self.time == None: - timestring = "" - else: - htime = utility.handy_time(self.time) - timestring = "%s (%s)" % (htime, self.time_string) - info = [("ID", self.uuid), - ("Short name", shortname), - ("Severity", self.severity), - ("Status", self.status), - ("Assigned", self._setting_attr_string("assigned")), - ("Reporter", self._setting_attr_string("reporter")), - ("Creator", self._setting_attr_string("creator")), - ("Created", timestring)] - 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') - else: - statuschar = self.status[0] - severitychar = self.severity[0] - chars = "%c%c" % (statuschar, severitychar) - bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n')) - - if show_comments == True: - # take advantage of the string_thread(auto_name_map=True) - # SIDE-EFFECT of sorting by comment time. - comout = self.comment_root.string_thread(flatten=False, - auto_name_map=True, - bug_shortname=shortname) - output = bugout + '\n' + comout.rstrip('\n') - else : - output = bugout - return output - # methods for saving/loading/acessing settings and properties. - def id(self, *args): - assert len(args) <= 1, str(args) - if len(args) == 1: - assert args[0] in ["values"], str(args) - return libbe.util.id.bug_id(self, *args) - def load_settings(self, settings_mapfile=None): if settings_mapfile == None: settings_mapfile = \ - self.storage.get(self.id("values"), default="\n") + self.storage.get(self.id.storage("values"), default="\n") self.settings = mapfile.parse(settings_mapfile) self._setup_saved_settings() def save_settings(self): mf = mapfile.generate(self._get_saved_settings()) - self.storage.set(self.id("values"), mf) + self.storage.set(self.id.storage("values"), mf) def save(self): """ @@ -683,11 +654,11 @@ class Bug(settings_object.SavedSettingsObject): """ assert self.storage != None, "Can't save without storage" if self.bugdir != None: - parent = self.bugdir.id() + parent = self.bugdir.id.storage() else: parent = None - self.storage.add(self.id(), parent=parent) - self.storage.add(self.id('values'), parent=self.id()) + self.storage.add(self.id.storage(), parent=parent) + self.storage.add(self.id.storage('values'), parent=self.id.storage()) self.save_settings() if len(self.comment_root) > 0: comment.save_comments(self) @@ -707,10 +678,14 @@ class Bug(settings_object.SavedSettingsObject): self.storage.writeable = w def remove(self): - self.storage.recursive_remove(self.id()) + self.storage.recursive_remove(self.id.storage()) # methods for managing comments + def uuids(self): + for comment in self.comments(): + yield comment.uuid + def comments(self): for comment in self.comment_root.traverse(): yield comment @@ -719,20 +694,15 @@ class Bug(settings_object.SavedSettingsObject): comm = self.comment_root.new_reply(body=body) return comm - def comment_from_shortname(self, shortname, *args, **kwargs): - return self.comment_root.comment_from_shortname(shortname, - *args, **kwargs) - def comment_from_uuid(self, uuid, *args, **kwargs): return self.comment_root.comment_from_uuid(uuid, *args, **kwargs) - def comment_shortnames(self, shortname=None): - """ - SIDE-EFFECT : Comment.comment_shortnames will sort the comment - tree by comment.time - """ - for id, comment in self.comment_root.comment_shortnames(shortname): - yield (id, comment) + # methods for id generation + + def sibling_uuids(self): + if self.bugdir != None: + return self.bugdir.uuids() + return [] # The general rule for bug sorting is that "more important" bugs are diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 02ff96f..39eface 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -173,6 +173,7 @@ class BugDir (list, settings_object.SavedSettingsObject): list.__init__(self) settings_object.SavedSettingsObject.__init__(self) self.storage = storage + self.id = libbe.util.id.ID(self, 'bugdir') if from_storage == True: self.load_settings() else: @@ -185,16 +186,10 @@ class BugDir (list, settings_object.SavedSettingsObject): # methods for saving/loading/accessing settings and properties. - def id(self, *args): - assert len(args) <= 1, str(args) - if len(args) == 1: - assert args[0] in ['settings'], str(args) - return libbe.util.id.bugdir_id(self, *args) - def load_settings(self, settings_mapfile=None): if settings_mapfile == None: settings_mapfile = \ - self.storage.get(self.id('settings'), default='\n') + self.storage.get(self.id.storage('settings'), default='\n') self.settings = mapfile.parse(settings_mapfile) self._setup_saved_settings() self._setup_user_id(self.user_id) @@ -204,7 +199,7 @@ class BugDir (list, settings_object.SavedSettingsObject): def save_settings(self): mf = mapfile.generate(self._get_saved_settings()) - self.storage.set(self.id('settings'), mf) + self.storage.set(self.id.storage('settings'), mf) def load_all_bugs(self): """ @@ -224,9 +219,8 @@ class BugDir (list, settings_object.SavedSettingsObject): happen, so calling this method will just waste time (unless something else has been messing with your stored files). """ - self.uuid = 'BD' - self.storage.add(self.id()) - self.storage.add(self.id('settings'), parent=self.id()) + self.storage.add(self.id.storage()) + self.storage.add(self.id.storage('settings'), parent=self.id.storage()) self.save_settings() for bug in self: bug.save() @@ -241,10 +235,11 @@ class BugDir (list, settings_object.SavedSettingsObject): yield bug.uuid if self.storage != None and self.storage.is_readable(): # and the ones that are still just in storage - for id in self.storage.children(self.id()): - parsed = libbe.util.id.parse_id(id) - if parsed['type'] == 'bug' and parsed['bug'] not in uuids: - yield parsed['bug'] + child_uuids = libbe.util.id.child_uuids( + self.storage.children(self.id.storage())) + for id in child_uuids: + if id not in uuids: + yield id def _clear_bugs(self): while len(self) > 0: @@ -258,7 +253,8 @@ class BugDir (list, settings_object.SavedSettingsObject): return bg def new_bug(self, summary=None, _uuid=None): - bg = bug.Bug(bugdir=self, uuid=_uuid, summary=summary) + bg = bug.Bug(bugdir=self, uuid=_uuid, summary=summary, + from_storage=False) self.append(bg) self._bug_map_gen() return bg @@ -268,45 +264,6 @@ class BugDir (list, settings_object.SavedSettingsObject): if self.storage.is_writeable(): bug.remove() - def bug_shortname(self, bug): - """ - 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 uuid in self._bug_map.keys(): - if bug.uuid == uuid: - continue - while (bug.uuid[:chars] == uuid[:chars]): - chars+=1 - return bug.uuid[:chars] - - def bug_from_shortname(self, shortname): - """ - >>> bd = SimpleBugDir(memory=True) - >>> bug_a = bd.bug_from_shortname('a') - >>> print type(bug_a) - - >>> print bug_a - a:om: Bug A - >>> bd.cleanup() - """ - matches = [] - self._bug_map_gen() - for uuid in self._bug_map.keys(): - if uuid.startswith(shortname): - matches.append(uuid) - if len(matches) > 1: - raise MultipleBugMatches(shortname, matches) - if len(matches) == 1: - return self.bug_from_uuid(matches[0]) - raise NoBugMatches(shortname) - def bug_from_uuid(self, uuid): if not self.has_bug(uuid): raise KeyError("No bug matches %s\n bug map: %s\n root: %s" \ @@ -322,6 +279,11 @@ class BugDir (list, settings_object.SavedSettingsObject): return False return True + # methods for id generation + + def sibling_uuids(self): + return [] + # methods for managing duplicate BugDirs def duplicate_bugdir(self, revision): @@ -344,7 +306,7 @@ class BugDir (list, settings_object.SavedSettingsObject): if parsed['type'] == 'bugdir': assert parsed['remaining'] == ['settings'], parsed['remaining'] dbd._settings = copy.copy(self._settings) - mf = self.storage.get(self.id('settings'), default='\n', + mf = self.storage.get(self.id.storage('settings'), default='\n', revision=revision) dbd.load_settings(mf) else: @@ -357,7 +319,7 @@ class BugDir (list, settings_object.SavedSettingsObject): dbd._bug_map[parsed['bug']] = bug if parsed['type'] == 'bug': assert parsed['remaining'] == ['values'], parsed['remaining'] - mf = self.storage.get(self.id('values'), default='\n', + mf = self.storage.get(self.id.storage('values'), default='\n', revision=revision) bug.load_settings(mf) elif parsed['type'] == 'comment': @@ -366,11 +328,11 @@ class BugDir (list, settings_object.SavedSettingsObject): bug.comment_root = copy.deepcopy(bug.comment_root) comment = bug.comment_from_uuid(parsed['comment']) if parsed['remaining'] == ['values']: - mf = self.storage.get(self.id('values'), default='\n', + mf = self.storage.get(self.id.storage('values'), default='\n', revision=revision) comment.load_settings(mf) else: - body = self.storage.get(self.id('body'), default='\n', + body = self.storage.get(self.id.storage('body'), default='\n', revision=revision) comment.body = body else: @@ -552,6 +514,8 @@ if libbe.TESTING == True: self.storage.disconnect() # flush self.storage.connect() bugdir._clear_bugs() + uuids = sorted(bugdir.uuids()) + self.failUnless(uuids == ['a', 'b'], uuids) uuids = sorted([bug.uuid for bug in bugdir]) self.failUnless(uuids == [], uuids) bugdir.load_all_bugs() @@ -570,6 +534,8 @@ if libbe.TESTING == True: uuids = sorted([bug.uuid for bug in bugdir]) self.failUnless(uuids == ['a', 'b'], uuids) bugdir._clear_bugs() + uuids = sorted(bugdir.uuids()) + self.failUnless(uuids == [], uuids) uuids = sorted([bug.uuid for bug in bugdir]) self.failUnless(uuids == [], uuids) bugdir.cleanup() diff --git a/libbe/comment.py b/libbe/comment.py index f5a6309..ebfde23 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -73,10 +73,8 @@ def load_comments(bug, load_full=False): from disk *now*, rather than waiting and lazy loading as required. """ uuids = [] - for id in bug.storage.children(): - parsed = libbe.util.id.parse_id(id) - if parsed['type'] == 'comment': - uuids.append(parsed['comment']) + for id in libbe.util.id.child_uuids(bug.storage.children()): + uuids.append(id) comments = [] for uuid in uuids: comm = Comment(bug, uuid, from_storage=True) @@ -157,14 +155,14 @@ class Comment(Tree, settings_object.SavedSettingsObject): def _get_comment_body(self): if self.storage != None and self.storage.is_readable() \ and self.uuid != INVALID_UUID: - return self.storage.get(self.id("body"), + return self.storage.get(self.id.storage("body"), decode=self.content_type.startswith("text/")) def _set_comment_body(self, old=None, new=None, force=False): assert self.uuid != INVALID_UUID, self if (self.storage != None and self.storage.writeable == True) \ or force==True: assert new != None, "Can't save empty comment" - self.storage.set(self.id("body"), new) + self.storage.set(self.id.storage("body"), new) @Property @change_hook_property(hook=_set_comment_body) @@ -173,16 +171,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): @doc_property(doc="The meat of the comment") def body(): return {} - def _get_storage(self): - if hasattr(self.bug, "storage"): - return self.bug.storage - - @Property - @cached_property(generator=_get_storage) - @local_property("storage") - @doc_property(doc="A revision control system instance.") - def storage(): return {} - def _extra_strings_check_fn(value): return utility.iterable_full_of_strings(value, \ alternative=settings_object.EMPTY) @@ -214,22 +202,21 @@ class Comment(Tree, settings_object.SavedSettingsObject): Tree.__init__(self) settings_object.SavedSettingsObject.__init__(self) self.bug = bug + self.storage = None self.uuid = uuid + self.id = libbe.util.id.ID(self, 'comment') if from_storage == False: if uuid == None: self.uuid = libbe.util.id.uuid_gen() self.settings = {} self._setup_saved_settings() - if self.storage != None and self.storage.is_writeable(): - self.storage.writeable = False - set_writeable = True - else: - set_writeable = False self.time = int(time.time()) # only save to second precision self.in_reply_to = in_reply_to self.body = body - if set_writeable == True: - self.storage.writeable = True + if self.bug != None: + self.storage = self.bug.storage + if from_storage == False: + if self.storage != None and self.storage.is_writeable(): self.save() def __cmp__(self, other): @@ -243,7 +230,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> comm.author = "Jane Doe " >>> print comm --------- Comment --------- - Name: com-1 + Name: //com From: Jane Doe Date: Thu, 20 Nov 2008 15:55:11 +0000 @@ -268,15 +255,15 @@ class Comment(Tree, settings_object.SavedSettingsObject): return str(value) return value - def xml(self, indent=0, shortname=None): + def xml(self, indent=0): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> comm.uuid = "0123" >>> comm.date = "Thu, 01 Jan 1970 00:00:00 +0000" - >>> print comm.xml(indent=2, shortname="com-1") + >>> print comm.xml(indent=2) 0123 - com-1 + //012 Thu, 01 Jan 1970 00:00:00 +0000 text/plain @@ -285,8 +272,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): remarks """ - if shortname == None: - shortname = self.uuid if self.content_type.startswith('text/'): body = (self.body or '').rstrip('\n') else: @@ -297,7 +282,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): body = base64.encodestring(self.body or '') info = [('uuid', self.uuid), ('alt-id', self.alt_id), - ('short-name', shortname), + ('short-name', self.id.user()), ('in-reply-to', self.in_reply_to), ('author', self._setting_attr_string('author')), ('date', self.date), @@ -323,16 +308,16 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> commA.date = "Thu, 01 Jan 1970 00:00:00 +0000" >>> commA.author = u'Fran\xe7ois' >>> commA.extra_strings += ['TAG: very helpful'] - >>> xml = commA.xml(shortname="com-1") + >>> xml = commA.xml() >>> commB = Comment() >>> commB.from_xml(xml, verbose=True) >>> commB.explicit_attrs ['author', 'date', 'content_type', 'body', 'alt_id'] - >>> commB.xml(shortname="com-1") == xml + >>> commB.xml() == xml False >>> commB.uuid = commB.alt_id >>> commB.alt_id = None - >>> commB.xml(shortname="com-1") == xml + >>> commB.xml() == xml True """ if type(xml_string) == types.UnicodeType: @@ -426,7 +411,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> print commA.xml() 0123 - 0123 + //012 John Thu, 01 Jan 1970 00:00:00 +0000 text/plain @@ -457,13 +442,14 @@ class Comment(Tree, settings_object.SavedSettingsObject): 'Merge would add extra string "%s" to comment %s' \ % (estr, self.uuid) - def string(self, indent=0, shortname=None): + def string(self, indent=0): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") + >>> comm.uuid = 'abcdef' >>> comm.date = "Thu, 01 Jan 1970 00:00:00 +0000" - >>> print comm.string(indent=2, shortname="com-1") + >>> print comm.string(indent=2) --------- Comment --------- - Name: com-1 + Name: //abc From: Date: Thu, 01 Jan 1970 00:00:00 +0000 @@ -471,11 +457,9 @@ class Comment(Tree, settings_object.SavedSettingsObject): insightful remarks """ - if shortname == None: - shortname = self.uuid lines = [] lines.append("--------- Comment ---------") - lines.append("Name: %s" % shortname) + lines.append("Name: %s" % self.id.user()) lines.append("From: %s" % (self._setting_attr_string("author"))) lines.append("Date: %s" % self.date) lines.append("") @@ -488,9 +472,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): sep = '\n' + istring return istring + sep.join(lines).rstrip('\n') - def string_thread(self, string_method_name="string", name_map={}, - indent=0, flatten=True, - auto_name_map=False, bug_shortname=None): + def string_thread(self, string_method_name="string", + indent=0, flatten=True): """ Return a string displaying a thread of comments. bug_shortname is only used if auto_name_map == True. @@ -522,94 +505,77 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> a.sort(key=lambda comm : comm.time) >>> print a.string_thread(flatten=True) --------- Comment --------- - Name: a + Name: //a From: Date: Thu, 20 Nov 2008 01:00:00 +0000 Insightful remarks --------- Comment --------- - Name: b + Name: //b From: Date: Thu, 20 Nov 2008 02:00:00 +0000 Critique original comment --------- Comment --------- - Name: c + Name: //c From: Date: Thu, 20 Nov 2008 03:00:00 +0000 Begin flamewar :p --------- Comment --------- - Name: d + Name: //d From: Date: Thu, 20 Nov 2008 04:00:00 +0000 Useful examples - >>> print a.string_thread(auto_name_map=True, bug_shortname="bug-1") + >>> print a.string_thread() --------- Comment --------- - Name: bug-1:1 + Name: //a From: Date: Thu, 20 Nov 2008 01:00:00 +0000 Insightful remarks --------- Comment --------- - Name: bug-1:2 + Name: //b From: Date: Thu, 20 Nov 2008 02:00:00 +0000 Critique original comment --------- Comment --------- - Name: bug-1:3 + Name: //c From: Date: Thu, 20 Nov 2008 03:00:00 +0000 Begin flamewar :p --------- Comment --------- - Name: bug-1:4 + Name: //d From: Date: Thu, 20 Nov 2008 04:00:00 +0000 Useful examples """ - if auto_name_map == True: - name_map = {} - for shortname,comment in self.comment_shortnames(bug_shortname): - name_map[comment.uuid] = shortname stringlist = [] for depth,comment in self.thread(flatten=flatten): ind = 2*depth+indent - if comment.uuid in name_map: - sname = name_map[comment.uuid] - else: - sname = None string_fn = getattr(comment, string_method_name) - stringlist.append(string_fn(indent=ind, shortname=sname)) + stringlist.append(string_fn(indent=ind)) return '\n'.join(stringlist) - def xml_thread(self, name_map={}, indent=0, - auto_name_map=False, bug_shortname=None): - return self.string_thread(string_method_name="xml", name_map=name_map, - indent=indent, auto_name_map=auto_name_map, - bug_shortname=bug_shortname) + def xml_thread(self, indent=0): + return self.string_thread(string_method_name="xml", indent=indent) # methods for saving/loading/acessing settings and properties. - def id(self, *args): - assert len(args) <= 1, str(args) - if len(args) == 1: - assert args[0] in ["values", "body"], str(args) - return libbe.util.id.comment_id(self, *args) - def load_settings(self, settings_mapfile=None): if settings_mapfile == None: settings_mapfile = \ - self.storage.get(self.id("values"), default="\n") + self.storage.get(self.id.storage("values"), default="\n") self.settings = mapfile.parse(settings_mapfile) self._setup_saved_settings() def save_settings(self): mf = mapfile.generate(self._get_saved_settings()) - self.storage.set(self.id("values"), mf) + self.storage.set(self.id.storage("values"), mf) def save(self): """ @@ -625,12 +591,12 @@ class Comment(Tree, settings_object.SavedSettingsObject): assert self.storage != None, "Can't save without storage" assert self.body != None, "Can't save blank comment" if self.bug != None: - parent = self.bug.id() + parent = self.bug.id.storage() else: parent = None - self.storage.add(self.id(), parent=parent) - self.storage.add(self.id('values'), parent=self.id()) - self.storage.add(self.id('body'), parent=self.id()) + self.storage.add(self.id.storage(), parent=parent) + self.storage.add(self.id.storage('values'), parent=self.id.storage()) + self.storage.add(self.id.storage('body'), parent=self.id.storage()) self.save_settings() self._set_comment_body(new=self.body, force=True) @@ -638,7 +604,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): for comment in self: comment.remove() if self.uuid != INVALID_UUID: - self.storage.recursive_remove(self.id()) + self.storage.recursive_remove(self.id.storage()) def add_reply(self, reply, allow_time_inversion=False): if self.uuid != INVALID_UUID: @@ -661,62 +627,9 @@ class Comment(Tree, settings_object.SavedSettingsObject): self.add_reply(reply) return reply - def comment_shortnames(self, bug_shortname=None): - """ - Iterate through (id, comment) pairs, in time order. - (This is a user-friendly id, not the comment uuid). - - SIDE-EFFECT : will sort the comment tree by comment.time - - >>> a = Comment(bug=None, uuid="a") - >>> b = a.new_reply() - >>> b.uuid = "b" - >>> c = b.new_reply() - >>> c.uuid = "c" - >>> d = a.new_reply() - >>> d.uuid = "d" - >>> for id,name in a.comment_shortnames("bug-1"): - ... print id, name.uuid - bug-1:1 a - bug-1:2 b - bug-1:3 c - bug-1:4 d - >>> for id,name in a.comment_shortnames(): - ... print id, name.uuid - :1 a - :2 b - :3 c - :4 d - """ - if bug_shortname == None: - bug_shortname = "" - self.sort(key=lambda comm : comm.time) - for num,comment in enumerate(self.traverse()): - yield ("%s:%d" % (bug_shortname, num+1), comment) - - def comment_from_shortname(self, comment_shortname, *args, **kwargs): - """ - Use a comment shortname to look up a comment. - >>> a = Comment(bug=None, uuid="a") - >>> b = a.new_reply() - >>> b.uuid = "b" - >>> c = b.new_reply() - >>> c.uuid = "c" - >>> d = a.new_reply() - >>> d.uuid = "d" - >>> comm = a.comment_from_shortname("bug-1:3", bug_shortname="bug-1") - >>> id(comm) == id(c) - True - """ - for cur_name, comment in self.comment_shortnames(*args, **kwargs): - if comment_shortname == cur_name: - return comment - raise InvalidShortname(comment_shortname, - list(self.comment_shortnames(*args, **kwargs))) - def comment_from_uuid(self, uuid, match_alt_id=True): """ - Use a comment shortname to look up a comment. + Use a uuid to look up a comment. >>> a = Comment(bug=None, uuid="a") >>> b = a.new_reply() >>> b.uuid = "b" @@ -744,6 +657,14 @@ class Comment(Tree, settings_object.SavedSettingsObject): return comment raise KeyError(uuid) + # methods for id generation + + def sibling_uuids(self): + if self.bug != None: + return self.bug.uuids() + return [] + + def cmp_attr(comment_1, comment_2, attr, invert=False): """ Compare a general attribute between two comments using the conventional diff --git a/libbe/util/id.py b/libbe/util/id.py index d443706..ab62359 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -137,7 +137,7 @@ class ID (object): referenced object. It would be hard to find bug 'XYZ' if that's all you knew. Much easier with 'ABC/XYZ', where ABC is the bugdir. Each project can publish a list of bugdir-id -x - to - location mappings, e.g. + - to - location mappings, e.g. ABC...(full uuid)...DEF https://server.com/projectX/be/ which is easier than publishing all-object-ids-to-location mappings. @@ -169,9 +169,9 @@ x - to - location mappings, e.g. self._object = object self._type = type assert self._type in HIERARCHY, self._type - self.uuid = self._object.uuid def storage(self, *args): + import libbe.comment return _assemble(self._object.uuid, *args) def _ancestors(self): @@ -182,7 +182,7 @@ x - to - location mappings, e.g. o = self._object for i in range(index, 0, -1): parent_name = HIERARCHY[i-1] - o = getattr(o, parent_name) + o = getattr(o, parent_name, None) ret.insert(0, o) return ret @@ -190,8 +190,26 @@ x - to - location mappings, e.g. return _assemble(*[o.uuid for o in self._ancestors()]) def user(self): - return _assemble(*[_truncate(o.uuid, o.sibling_uuids()) - for o in self._ancestors()]) + ids = [] + for o in self._ancestors(): + if o == None: + ids.append(None) + else: + ids.append(_truncate(o.uuid, o.sibling_uuids())) + return _assemble(*ids) + +def child_uuids(child_storage_ids): + """ + Extract uuid children from other children generated by the + ID.storage() method. + >>> list(child_uuids(['abc123/values', '123abc', '123def'])) + ['123abc', '123def'] + """ + for id in child_storage_ids: + fields = libbe.util.id._split(id) + if len(fields) == 1: + yield fields[0] + def parse_user(id): """ -- cgit From bf3d434b244c57556bec979acbc658c30eb58221 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 12 Dec 2009 00:31:55 -0500 Subject: Added libbe.command.base (with Command class) and moved list command to new format. --- be | 5 + libbe/command/__init__.py | 13 ++ libbe/command/base.py | 224 ++++++++++++++++++++++++++++++ libbe/command/list.py | 340 ++++++++++++++++++++++++---------------------- libbe/ui/__init__.py | 1 + libbe/ui/base.py | 23 ++++ libbe/ui/util/__init__.py | 69 ++++++++++ libbe/ui/util/cmdutil.py | 141 +++---------------- libbe/ui/util/repo.py | 4 + libbe/util/plugin.py | 60 ++++---- libbe/util/utility.py | 9 ++ test.py | 10 +- 12 files changed, 579 insertions(+), 320 deletions(-) create mode 100644 libbe/command/base.py create mode 100644 libbe/ui/__init__.py create mode 100644 libbe/ui/base.py create mode 100644 libbe/ui/util/__init__.py create mode 100644 libbe/ui/util/repo.py diff --git a/be b/be index f026c05..5e3088e 100755 --- a/be +++ b/be @@ -43,6 +43,11 @@ parser.add_option("--no-pager", dest="no_pager", default=False, action='store_true', help="Do not pipe git output into a pager.") +# Option(name='repo', short_name='r', +# help='Select BE repository (see `be help repo`) rather than' +# 'the current directory.', +# arg=Argument(name='repo', metavar='REPO', default='.', +# completion_callback=libbe.ui.util.repo.complete)), try: options,args = parser.parse_args() diff --git a/libbe/command/__init__.py b/libbe/command/__init__.py index 794013c..344a8a2 100644 --- a/libbe/command/__init__.py +++ b/libbe/command/__init__.py @@ -14,3 +14,16 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import base + +UserError = base.UserError +UnkownCommand = base.UnknownCommand +get_command = base.get_command +commands = base.commands +Option = base.Option +Argument = base.Argument +Command = base.Command + +__all__ = [UserError, UnkownCommand, get_command, commands, + Option, Argument, Command] diff --git a/libbe/command/base.py b/libbe/command/base.py new file mode 100644 index 0000000..973b840 --- /dev/null +++ b/libbe/command/base.py @@ -0,0 +1,224 @@ +# Copyright + +import optparse +import sys + +import libbe +import libbe.util.plugin +import libbe.ui.util.repo + +class UserError(Exception): + pass + +class UnknownCommand(UserError): + def __init__(self, cmd): + Exception.__init__(self, "Unknown command '%s'" % cmd) + self.cmd = cmd + + +def get_command(command_name): + """Retrieves the module for a user command + + >>> try: + ... get_command('asdf') + ... except UnknownCommand, e: + ... print e + Unknown command 'asdf' + >>> repr(get_command('list')).startswith(">> c = Command() + >>> print c.help() + usage: be command [options] + + Options: + -h HELP, --help=HELP Print a help message + + --complete=STRING Print a list of possible completions + + -r REPO, --repo=REPO Select BE repository (see `be help repo`) rather + thanthe current directory. + + A detailed help message. + """ + + name = 'command' + + def __init__(self, input_encoding=None, output_encoding=None): + self.status = None + self.result = None + self.input_encoding = None + self.output_encoding = None + self.options = [ + Option(name='help', short_name='h', + help='Print a help message', + option_callback=self.help), + Option(name='complete', type='string', + help='Print a list of possible completions', + arg=Argument(name='complete', metavar='STRING', optional=True)), + ] + self.args = [] + + def run(self, bugdir, options=None, args=None): + if options == None: + options = {} + if args == None: + args = [] + params = {} + for option in self.options: + if option.name in options: + params[option.name] = options.pop(option.name) + elif option.arg != None: + params[option.name] = option.arg.default + else: # non-arg options are flags, set to default flag value + params[option.name] = False + if len(options) > 0: + raise UserError, 'Invalid options passed to command %s:\n %s' \ + % (self.name, '\n '.join(['%s: %s' % (k,v) + for k,v in options.items()])) + for arg in self.args: + pass + if params['help'] == True: + pass + else: + params.pop('help') + if params['complete'] != None: + pass + else: + params.pop('complete') + self._setup_io(self.input_encoding, self.output_encoding) + self.status = self._run(bugdir, **params) + return self.status + + def _run(self, bugdir, **kwargs): + pass + + def _setup_io(self, input_encoding=None, output_encoding=None): + if input_encoding == None: + input_encoding = get_terminal_encoding() + if output_encoding == None: + output_encoding = get_terminal_encoding() + self.stdin = codecs.getwriter(input_encoding)(sys.stdin) + self.stdin.encoding = input_encoding + self.stdout = codecs.getwriter(output_encoding)(sys.stdout) + self.stdout.encoding = output_encoding + + def help(self, *args): + return '\n\n'.join([self._usage(), + self._option_help(), + self._long_help()]) +# if cmd != None: +# return get_command(cmd).help() +# cmdlist = [] +# for name in commands(): +# module = get_command(name) +# cmdlist.append((name, module.__desc__)) +# cmdlist.sort() +# longest_cmd_len = max([len(name) for name,desc in cmdlist]) +# ret = ["Bugs Everywhere - Distributed bug tracking", +# "", "Supported commands"] +# for name, desc in cmdlist: +# numExtraSpaces = longest_cmd_len-len(name) +# ret.append("be %s%*s %s" % (name, numExtraSpaces, "", desc)) +# ret.extend(["", "Run", " be help [command]", "for more information."]) +# longhelp = "\n".join(ret) +# if parser == None: +# return longhelp +# return parser.help_str() + "\n" + longhelp + + def _usage(self): + usage = 'usage: be %s [options]' % self.name + num_optional = 0 + for arg in self.args: + usage += ' ' + if arg.optional == True: + usage += '[' + num_optional += 1 + usage += arg.metavar + if arg.repeatable == True: + usage += ' ...' + usage += ']'*num_optional + return usage + + def _option_help(self): + o = OptionFormatter(self.options) + return o.option_help().strip('\n') + + def _long_help(self): + return "A detailed help message." diff --git a/libbe/command/list.py b/libbe/command/list.py index 1c3e78d..508cc70 100644 --- a/libbe/command/list.py +++ b/libbe/command/list.py @@ -16,184 +16,208 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""List bugs""" -from libbe import cmdutil, bugdir, bug + import os import re -__desc__ = __doc__ + +import libbe +import libbe.command +import libbe.bug +import libbe.util.utility +import libbe.ui.util # get a list of * for cmp_*() comparing two bugs. -AVAILABLE_CMPS = [fn[4:] for fn in dir(bug) if fn[:4] == 'cmp_'] -AVAILABLE_CMPS.remove("attr") # a cmp_* template. +AVAILABLE_CMPS = [fn[4:] for fn in dir(libbe.bug) if fn[:4] == 'cmp_'] +AVAILABLE_CMPS.remove('attr') # a cmp_* template. -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute([], manipulate_encodings=False) - a:om: Bug A - >>> execute(["--status", "closed"], manipulate_encodings=False) - b:cm: Bug B - >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - if len(args) > 0: - raise cmdutil.UsageError("Too many arguments.") - cmp_list = [] - if options.sort_by != None: - for cmp in options.sort_by.split(','): - if cmp not in AVAILABLE_CMPS: - raise cmdutil.UserError( - "Invalid sort on '%s'.\nValid sorts:\n %s" - % (cmp, '\n '.join(AVAILABLE_CMPS))) - cmp_list.append(eval('bug.cmp_%s' % cmp)) - - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bd.load_all_bugs() - # select status - if options.status != None: - if options.status == "all": - status = bug.status_values - else: - status = cmdutil.select_values(options.status, bug.status_values) - else: - status = [] - if options.active == True: - status.extend(list(bug.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 = bug.active_status_values - # select severity - if options.severity != None: - if options.severity == "all": - severity = bug.severity_values - else: - severity = cmdutil.select_values(options.severity, - bug.severity_values) - else: - severity = [] - if options.wishlist == True: - severity.extend("wishlist") - if options.important == True: - serious = bug.severity_values.index("serious") - severity.append(list(bug.severity_values[serious:])) - if severity == []: # set the default value - severity = bug.severity_values - # select assigned - if options.assigned != None: - if options.assigned == "all": - assigned = "all" - else: - possible_assignees = [] - for _bug in bd: - if _bug.assigned != None \ - and not _bug.assigned in possible_assignees: - possible_assignees.append(_bug.assigned) - assigned = cmdutil.select_values(options.assigned, - possible_assignees) - print 'assigned', assigned - 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] = bd.user_id - if options.extra_strings != None: - extra_string_regexps = [re.compile(x) for x in options.extra_strings.split(',')] - - def filter(bug): - if status != "all" and not bug.status in status: +class Filter (object): + def __init__(self, status, severity, assigned, extra_strings_regexps): + self.status = status + self.severity = severity + self.assigned = assigned + self.extra_strings_regexps = extra_strings_regexps + + def __call__(self, bug): + if self.status != "all" and not bug.status in self.status: return False - if severity != "all" and not bug.severity in severity: + if self.severity != "all" and not bug.severity in self.severity: return False - if assigned != "all" and not bug.assigned in assigned: + if self.assigned != "all" and not bug.assigned in self.assigned: return False - if options.extra_strings != None: - if len(bug.extra_strings) == 0 and len(extra_string_regexps) > 0: + if len(bug.extra_strings) == 0: + if len(self.extra_strings_regexps) > 0: return False + else: for string in bug.extra_strings: - for regexp in extra_string_regexps: + for regexp in self.extra_strings_regexps: if not regexp.match(string): return False return True - bugs = [b for b in bd if filter(b) ] - if len(bugs) == 0 and options.xml == False: - print "No matching bugs found" +class List (libbe.command.Command): + """List bugs + + >>> import libbe.bugdir + >>> bd = libbe.bugdir.SimpleBugDir() + >>> bd.uuid = '1234abcd' + >>> cmd = List() + >>> cmd._setup_io = lambda i_enc,o_enc : None + >>> cmd.run(bd) + 123/a:om: Bug A + >>> cmd.run(bd, {'status':'closed'}) + 123/b:cm: Bug B + >>> bd.cleanup() + """ + + name = 'list' + + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.options.extend([ + libbe.command.Option(name='status', + help='Only show bugs matching the STATUS specifier', + arg=libbe.command.Argument( + name='status', metavar='STATUS', default='active', + completion_callback=libbe.ui.util.complete_status)), + libbe.command.Option(name='severity', + help='Only show bugs matching the SEVERITY specifier', + arg=libbe.command.Argument( + name='severity', metavar='SEVERITY', default='all', + completion_callback=libbe.ui.util.complete_severity)), + libbe.command.Option(name='assigned', short_name='a', + help='Only show bugs matching ASSIGNED', + arg=libbe.command.Argument( + name='assigned', metavar='ASSIGNED', default='all', + completion_callback=libbe.ui.util.complete_assigned)), + libbe.command.Option(name='extra-strings', short_name='e', + help='Only show bugs matching STRINGS, e.g. --extra-strings' + ' TAG:working,TAG:xml', + arg=libbe.command.Argument( + name='extra-strings', metavar='STRINGS', default=None, + completion_callback=libbe.ui.util.complete_extra_strings)), + libbe.command.Option(name='sort', short_name='S', + help='Adjust bug-sort criteria with comma-separated list ' + 'SORT. e.g. "--sort creator,time". ' + 'Available criteria: %s' % ','.join(AVAILABLE_CMPS), + arg=libbe.command.Argument( + name='sort', metavar='SORT', default=None, + completion_callback=libbe.ui.util.Completer(AVAILABLE_CMPS))), + libbe.command.Option(name='uuids', short_name='u', + help='Only print the bug UUIDS'), + libbe.command.Option(name='xml', short_name='x', + help='Dump output in XML format'), + ]) +# parser.add_option("-S", "--sort", metavar="SORT-BY", dest="sort_by", +# help="Adjust bug-sort criteria with comma-separated list SORT-BY. e.g. \"--sort creator,time\". Available criteria: %s" % ','.join(AVAILABLE_CMPS), default=None) +# # boolean options. All but uuids and xml are special cases of long forms +# ("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")) +# 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, default=False) +# return parser +# +# ]) + + def _run(self, bugdir, **params): + cmp_list, status, severity, assigned, extra_strings_regexps = \ + self._parse_params(params) + filter = Filter(status, severity, assigned, extra_strings_regexps) + bugs = [bugdir.bug_from_uuid(uuid) for uuid in bugdir.uuids()] + bugs = [b for b in bugs if filter(b) == True] + self.result = bugs + if len(bugs) == 0 and params['xml'] == False: + print "No matching bugs found" - def list_bugs(cur_bugs, title=None, just_uuids=False, xml=False): + # sort bugs + bugs = self._sort_bugs(bugs, cmp_list) + + # print list of bugs + if params['uuids'] == True: + for bug in bugs: + print bug.uuid + else: + self._list_bugs(bugs, xml=params['xml']) + + def _parse_params(self, params): + cmp_list = [] + if params['sort'] != None: + for cmp in params['sort'].sort_by.split(','): + if cmp not in AVAILABLE_CMPS: + raise libbe.command.UserError( + "Invalid sort on '%s'.\nValid sorts:\n %s" + % (cmp, '\n '.join(AVAILABLE_CMPS))) + cmp_list.append(eval('libbe.bug.cmp_%s' % cmp)) + # select status + if params['status'] == 'all': + status = libbe.bug.status_values + elif params['status'] == 'active': + status = list(libbe.bug.active_status_values) + elif params['status'] == 'inactive': + status = list(libbe.bug.inactive_status_values) + else: + status = libbe.ui.util.select_values( + params['status'], libbe.bug.status_values) + # select severity + if params['severity'] == 'all': + severity = libbe.bug.severity_values + elif params['important'] == True: + serious = libbe.bug.severity_values.index('serious') + severity.append(list(libbe.bug.severity_values[serious:])) + else: + severity = libbe.ui.util.select_values( + params['severity'], bug.severity_values) + # select assigned + if params['assigned'] == "all": + assigned = "all" + else: + possible_assignees = [] + for bug in self.bugdir: + if bug.assigned != None \ + and not bug.assigned in possible_assignees: + possible_assignees.append(bug.assigned) + assigned = libbe.ui.util.select_values( + params['assigned'], possible_assignees) + for i in range(len(assigned)): + if assigned[i] == '-': + assigned[i] = params['user-id'] + if params['extra-strings'] == None: + extra_strings_regexps = [] + else: + extra_strings_regexps = [re.compile(x) + for x in params['extra-strings'].split(',')] + return (cmp_list, status, severity, assigned, extra_strings_regexps) + + def _sort_bugs(self, bugs, cmp_list=[]): + cmp_list.extend(libbe.bug.DEFAULT_CMP_FULL_CMP_LIST) + cmp_fn = libbe.bug.BugCompoundComparator(cmp_list=cmp_list) + bugs.sort(cmp_fn) + return bugs + + def _list_bugs(self, bugs, xml=False): if xml == True: - print '' % bd.encoding + print '' % self.stdout.encoding print "" - if len(cur_bugs) > 0: - if title != None and xml == False: - print cmdutil.underlined(title) - for bg in cur_bugs: + if len(bugs) > 0: + for bug in bugs: if xml == True: - print bg.xml(show_comments=True) - elif just_uuids: - print bg.uuid + print bug.xml(show_comments=True) else: - print bg.string(shortlist=True) + print bug.string(shortlist=True) if xml == True: print "" - # sort bugs - cmp_list.extend(bug.DEFAULT_CMP_FULL_CMP_LIST) - cmp_fn = bug.BugCompoundComparator(cmp_list=cmp_list) - bugs.sort(cmp_fn) - - # print list of bugs - list_bugs(bugs, just_uuids=options.uuids, xml=options.xml) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be list [options]") - parser.add_option("--status", dest="status", metavar="STATUS", - help="Only show bugs matching the STATUS specifier") - parser.add_option("--severity", dest="severity", metavar="SEVERITY", - help="Only show bugs matching the SEVERITY specifier") - parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", - help="List bugs matching ASSIGNED", default=None) - parser.add_option("-e", "--extra-strings", metavar="STRINGS", dest="extra_strings", - help="List bugs matching _all_ extra strings in comma-seperated list STRINGS. e.g. --extra-strings TAG:working,TAG:xml", default=None) - parser.add_option("-S", "--sort", metavar="SORT-BY", dest="sort_by", - help="Adjust bug-sort criteria with comma-separated list SORT-BY. e.g. \"--sort creator,time\". Available criteria: %s" % ','.join(AVAILABLE_CMPS), default=None) - # boolean options. All but uuids and xml are special cases of long forms - bools = (("u", "uuids", "Only print the bug UUIDS"), - ("x", "xml", "Dump as XML"), - ("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")) - 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, default=False) - return parser - - -def help(): - longhelp=""" + def _long_help(self): + return """ This command lists bugs. Normally it prints a short string like 576:om: Allow attachments Where @@ -224,9 +248,7 @@ assigned In addition, there are some shortcut options that set boolean flags. The boolean options are ignored if the matching string option is used. -""" % (','.join(bug.status_values), - ','.join(bug.severity_values)) - return get_parser().help_str() + longhelp +""" % (','.join(bug.status_values), ','.join(bug.severity_values)) def complete(options, args, parser): for option, value in cmdutil.option_value_pairs(options, parser): diff --git a/libbe/ui/__init__.py b/libbe/ui/__init__.py new file mode 100644 index 0000000..b98f164 --- /dev/null +++ b/libbe/ui/__init__.py @@ -0,0 +1 @@ +# Copyright diff --git a/libbe/ui/base.py b/libbe/ui/base.py new file mode 100644 index 0000000..d26115f --- /dev/null +++ b/libbe/ui/base.py @@ -0,0 +1,23 @@ + def _setup_user_id(self, user_id): + if isinstance(self.storage, storage.vcs.base.VCS): + self.storage.user_id = user_id + def _guess_user_id(self): + if isinstance(self.storage, storage.vcs.base.VCS): + return self.storage.get_user_id() + def _set_user_id(self, old_user_id, new_user_id): + self._setup_user_id(new_user_id) + self._prop_save_settings(old_user_id, new_user_id) + + @_versioned_property(name="user_id", + doc= +"""The user's prefered name, e.g. 'John Doe '. Note +that the Arch VCS backend *enforces* ids with this format.""", + change_hook=_set_user_id, + generator=_guess_user_id) + def user_id(): return {} + + @_versioned_property(name="default_assignee", + doc= +"""The default assignee for new bugs e.g. 'John Doe '.""") + def default_assignee(): return {} + diff --git a/libbe/ui/util/__init__.py b/libbe/ui/util/__init__.py new file mode 100644 index 0000000..a650d33 --- /dev/null +++ b/libbe/ui/util/__init__.py @@ -0,0 +1,69 @@ +# Copyright + +class Completer (object): + def __init__(self, options): + self.options = options + def __call__(self, bugdir, fragment=None): + return [fragment] + +def complete_status(bugdir, fragment=None): + return [fragment] +def complete_severity(bugdir, fragment=None): + return [fragment] +def complete_assigned(bugdir, fragment=None): + return [fragment] +def complete_extra_strings(bugdir, fragment=None): + return [fragment] + +def select_values(string, possible_values, name="unkown"): + """ + This function allows the user to select values from a list of + possible values. The default is to select all the values: + + >>> select_values(None, ['abc', 'def', 'hij']) + ['abc', 'def', 'hij'] + + The user selects values with a comma-separated limit_string. + Prepending a minus sign to such a list denotes blacklist mode: + + >>> select_values('-abc,hij', ['abc', 'def', 'hij']) + ['def'] + + Without the leading -, the selection is in whitelist mode: + + >>> select_values('abc,hij', ['abc', 'def', 'hij']) + ['abc', 'hij'] + + In either case, appropriate errors are raised if on of the + user-values is not in the list of possible values. The name + parameter lets you make the error message more clear: + + >>> select_values('-xyz,hij', ['abc', 'def', 'hij'], name="foobar") + Traceback (most recent call last): + ... + UserError: Invalid foobar xyz + ['abc', 'def', 'hij'] + >>> select_values('xyz,hij', ['abc', 'def', 'hij'], name="foobar") + Traceback (most recent call last): + ... + UserError: Invalid foobar xyz + ['abc', 'def', 'hij'] + """ + possible_values = list(possible_values) # don't alter the original + if string == None: + pass + elif string.startswith('-'): + blacklisted_values = set(string[1:].split(',')) + for value in blacklisted_values: + if value not in possible_values: + raise UserError('Invalid %s %s\n %s' + % (name, value, possible_values)) + possible_values.remove(value) + else: + whitelisted_values = string.split(',') + for value in whitelisted_values: + if value not in possible_values: + raise UserError('Invalid %s %s\n %s' + % (name, value, possible_values)) + possible_values = whitelisted_values + return possible_values diff --git a/libbe/ui/util/cmdutil.py b/libbe/ui/util/cmdutil.py index c567984..b2d8a99 100644 --- a/libbe/ui/util/cmdutil.py +++ b/libbe/ui/util/cmdutil.py @@ -37,48 +37,11 @@ if libbe.TESTING == True: import doctest -class UserError(Exception): - def __init__(self, msg): - Exception.__init__(self, msg) - -class UnknownCommand(UserError): - def __init__(self, cmd): - Exception.__init__(self, "Unknown command '%s'" % cmd) - self.cmd = cmd - -class UsageError(Exception): - pass - -class GetHelp(Exception): - pass - -class GetCompletions(Exception): - def __init__(self, completions=[]): - msg = "Get allowed completions" - Exception.__init__(self, msg) - self.completions = completions def iter_commands(): for name, module in plugin.iter_plugins("becommands"): yield name.replace("_", "-"), module -def get_command(command_name): - """Retrieves the module for a user command - - >>> try: - ... get_command("asdf") - ... except UnknownCommand, e: - ... print e - Unknown command 'asdf' - >>> repr(get_command("list")).startswith(">> select_values(None, ['abc', 'def', 'hij']) - ['abc', 'def', 'hij'] - - The user selects values with a comma-separated limit_string. - Prepending a minus sign to such a list denotes blacklist mode: - - >>> select_values('-abc,hij', ['abc', 'def', 'hij']) - ['def'] - - Without the leading -, the selection is in whitelist mode: - - >>> select_values('abc,hij', ['abc', 'def', 'hij']) - ['abc', 'hij'] - - In either case, appropriate errors are raised if on of the - user-values is not in the list of possible values. The name - parameter lets you make the error message more clear: - - >>> select_values('-xyz,hij', ['abc', 'def', 'hij'], name="foobar") - Traceback (most recent call last): - ... - UserError: Invalid foobar xyz - ['abc', 'def', 'hij'] - >>> select_values('xyz,hij', ['abc', 'def', 'hij'], name="foobar") - Traceback (most recent call last): - ... - UserError: Invalid foobar xyz - ['abc', 'def', 'hij'] - """ - possible_values = list(possible_values) # don't alter the original - if string == None: - pass - elif string.startswith('-'): - blacklisted_values = set(string[1:].split(',')) - for value in blacklisted_values: - if value not in possible_values: - raise UserError('Invalid %s %s\n %s' - % (name, value, possible_values)) - possible_values.remove(value) - else: - whitelisted_values = string.split(',') - for value in whitelisted_values: - if value not in possible_values: - raise UserError('Invalid %s %s\n %s' - % (name, value, possible_values)) - possible_values = whitelisted_values - return possible_values def restrict_file_access(bugdir, path): """ @@ -352,5 +245,7 @@ def bug_comment_from_id(bdir, id): raise UserError(e.message) return (bug, comm) + + if libbe.TESTING == True: suite = doctest.DocTestSuite() diff --git a/libbe/ui/util/repo.py b/libbe/ui/util/repo.py new file mode 100644 index 0000000..174c5b1 --- /dev/null +++ b/libbe/ui/util/repo.py @@ -0,0 +1,4 @@ +# Copyright + +def complete(string): + pass diff --git a/libbe/util/plugin.py b/libbe/util/plugin.py index edb4922..982c5ca 100644 --- a/libbe/util/plugin.py +++ b/libbe/util/plugin.py @@ -26,50 +26,42 @@ import os import os.path import sys -import libbe -if libbe.TESTING == True: - import doctest -def import_by_name(mod_name): - module = __import__(mod_name) - components = mod_name.split('.') +_PLUGIN_PATH = os.path.realpath( + os.path.dirname( + os.path.dirname( + os.path.dirname(__file__)))) +if _PLUGIN_PATH not in sys.path: + sys.path.append(_PLUGIN_PATH) + +def import_by_name(modname): + """ + >>> mod = import_by_name('libbe.bugdir') + >>> 'BugDir' in dir(mod) + True + >>> import_by_name('libbe.highly_unlikely') + Traceback (most recent call last): + ... + ImportError: No module named highly_unlikely + """ + module = __import__(modname) + components = modname.split('.') for comp in components[1:]: module = getattr(module, comp) return module -def iter_plugins(prefix): +def modnames(prefix): """ - >>> "list" in [n for n,m in iter_plugins("becommands")] + >>> 'list' in [n for n in modnames('libbe.command')] True - >>> "plugin" in [n for n,m in iter_plugins("libbe")] + >>> 'plugin' in [n for n in modnames('libbe.util')] True """ - modfiles = os.listdir(os.path.join(plugin_path, prefix)) + components = prefix.split('.') + modfiles = os.listdir(os.path.join(_PLUGIN_PATH, *components)) modfiles.sort() for modfile in modfiles: if modfile.startswith('.'): continue # the occasional emacs temporary file - if modfile.endswith(".py") and modfile != "__init__.py": - yield modfile[:-3], my_import(prefix+"."+modfile[:-3]) - - -def get_plugin(prefix, name): - """ - >>> get_plugin("becommands", "asdf") is None - True - >>> q = repr(get_plugin("becommands", "list")) - >>> q.startswith(" Date: Sat, 12 Dec 2009 01:12:17 -0500 Subject: Moved command completion from libbe.ui.util to libbe.command.util --- libbe/command/base.py | 1 - libbe/command/list.py | 21 +++++++-------- libbe/command/util.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++ libbe/ui/util/__init__.py | 68 ---------------------------------------------- 4 files changed, 79 insertions(+), 80 deletions(-) create mode 100644 libbe/command/util.py diff --git a/libbe/command/base.py b/libbe/command/base.py index 973b840..d18e070 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -5,7 +5,6 @@ import sys import libbe import libbe.util.plugin -import libbe.ui.util.repo class UserError(Exception): pass diff --git a/libbe/command/list.py b/libbe/command/list.py index 508cc70..9527779 100644 --- a/libbe/command/list.py +++ b/libbe/command/list.py @@ -21,10 +21,9 @@ import os import re import libbe -import libbe.command import libbe.bug -import libbe.util.utility -import libbe.ui.util +import libbe.command +import libbe.command.util # get a list of * for cmp_*() comparing two bugs. AVAILABLE_CMPS = [fn[4:] for fn in dir(libbe.bug) if fn[:4] == 'cmp_'] @@ -78,30 +77,30 @@ class List (libbe.command.Command): help='Only show bugs matching the STATUS specifier', arg=libbe.command.Argument( name='status', metavar='STATUS', default='active', - completion_callback=libbe.ui.util.complete_status)), + completion_callback=libbe.command.util.complete_status)), libbe.command.Option(name='severity', help='Only show bugs matching the SEVERITY specifier', arg=libbe.command.Argument( name='severity', metavar='SEVERITY', default='all', - completion_callback=libbe.ui.util.complete_severity)), + completion_callback=libbe.command.util.complete_severity)), libbe.command.Option(name='assigned', short_name='a', help='Only show bugs matching ASSIGNED', arg=libbe.command.Argument( name='assigned', metavar='ASSIGNED', default='all', - completion_callback=libbe.ui.util.complete_assigned)), + completion_callback=libbe.command.util.complete_assigned)), libbe.command.Option(name='extra-strings', short_name='e', help='Only show bugs matching STRINGS, e.g. --extra-strings' ' TAG:working,TAG:xml', arg=libbe.command.Argument( name='extra-strings', metavar='STRINGS', default=None, - completion_callback=libbe.ui.util.complete_extra_strings)), + completion_callback=libbe.command.util.complete_extra_strings)), libbe.command.Option(name='sort', short_name='S', help='Adjust bug-sort criteria with comma-separated list ' 'SORT. e.g. "--sort creator,time". ' 'Available criteria: %s' % ','.join(AVAILABLE_CMPS), arg=libbe.command.Argument( name='sort', metavar='SORT', default=None, - completion_callback=libbe.ui.util.Completer(AVAILABLE_CMPS))), + completion_callback=libbe.command.util.Completer(AVAILABLE_CMPS))), libbe.command.Option(name='uuids', short_name='u', help='Only print the bug UUIDS'), libbe.command.Option(name='xml', short_name='x', @@ -165,7 +164,7 @@ class List (libbe.command.Command): elif params['status'] == 'inactive': status = list(libbe.bug.inactive_status_values) else: - status = libbe.ui.util.select_values( + status = libbe.command.util.select_values( params['status'], libbe.bug.status_values) # select severity if params['severity'] == 'all': @@ -174,7 +173,7 @@ class List (libbe.command.Command): serious = libbe.bug.severity_values.index('serious') severity.append(list(libbe.bug.severity_values[serious:])) else: - severity = libbe.ui.util.select_values( + severity = libbe.command.util.select_values( params['severity'], bug.severity_values) # select assigned if params['assigned'] == "all": @@ -185,7 +184,7 @@ class List (libbe.command.Command): if bug.assigned != None \ and not bug.assigned in possible_assignees: possible_assignees.append(bug.assigned) - assigned = libbe.ui.util.select_values( + assigned = libbe.command.util.select_values( params['assigned'], possible_assignees) for i in range(len(assigned)): if assigned[i] == '-': diff --git a/libbe/command/util.py b/libbe/command/util.py new file mode 100644 index 0000000..a650d33 --- /dev/null +++ b/libbe/command/util.py @@ -0,0 +1,69 @@ +# Copyright + +class Completer (object): + def __init__(self, options): + self.options = options + def __call__(self, bugdir, fragment=None): + return [fragment] + +def complete_status(bugdir, fragment=None): + return [fragment] +def complete_severity(bugdir, fragment=None): + return [fragment] +def complete_assigned(bugdir, fragment=None): + return [fragment] +def complete_extra_strings(bugdir, fragment=None): + return [fragment] + +def select_values(string, possible_values, name="unkown"): + """ + This function allows the user to select values from a list of + possible values. The default is to select all the values: + + >>> select_values(None, ['abc', 'def', 'hij']) + ['abc', 'def', 'hij'] + + The user selects values with a comma-separated limit_string. + Prepending a minus sign to such a list denotes blacklist mode: + + >>> select_values('-abc,hij', ['abc', 'def', 'hij']) + ['def'] + + Without the leading -, the selection is in whitelist mode: + + >>> select_values('abc,hij', ['abc', 'def', 'hij']) + ['abc', 'hij'] + + In either case, appropriate errors are raised if on of the + user-values is not in the list of possible values. The name + parameter lets you make the error message more clear: + + >>> select_values('-xyz,hij', ['abc', 'def', 'hij'], name="foobar") + Traceback (most recent call last): + ... + UserError: Invalid foobar xyz + ['abc', 'def', 'hij'] + >>> select_values('xyz,hij', ['abc', 'def', 'hij'], name="foobar") + Traceback (most recent call last): + ... + UserError: Invalid foobar xyz + ['abc', 'def', 'hij'] + """ + possible_values = list(possible_values) # don't alter the original + if string == None: + pass + elif string.startswith('-'): + blacklisted_values = set(string[1:].split(',')) + for value in blacklisted_values: + if value not in possible_values: + raise UserError('Invalid %s %s\n %s' + % (name, value, possible_values)) + possible_values.remove(value) + else: + whitelisted_values = string.split(',') + for value in whitelisted_values: + if value not in possible_values: + raise UserError('Invalid %s %s\n %s' + % (name, value, possible_values)) + possible_values = whitelisted_values + return possible_values diff --git a/libbe/ui/util/__init__.py b/libbe/ui/util/__init__.py index a650d33..b98f164 100644 --- a/libbe/ui/util/__init__.py +++ b/libbe/ui/util/__init__.py @@ -1,69 +1 @@ # Copyright - -class Completer (object): - def __init__(self, options): - self.options = options - def __call__(self, bugdir, fragment=None): - return [fragment] - -def complete_status(bugdir, fragment=None): - return [fragment] -def complete_severity(bugdir, fragment=None): - return [fragment] -def complete_assigned(bugdir, fragment=None): - return [fragment] -def complete_extra_strings(bugdir, fragment=None): - return [fragment] - -def select_values(string, possible_values, name="unkown"): - """ - This function allows the user to select values from a list of - possible values. The default is to select all the values: - - >>> select_values(None, ['abc', 'def', 'hij']) - ['abc', 'def', 'hij'] - - The user selects values with a comma-separated limit_string. - Prepending a minus sign to such a list denotes blacklist mode: - - >>> select_values('-abc,hij', ['abc', 'def', 'hij']) - ['def'] - - Without the leading -, the selection is in whitelist mode: - - >>> select_values('abc,hij', ['abc', 'def', 'hij']) - ['abc', 'hij'] - - In either case, appropriate errors are raised if on of the - user-values is not in the list of possible values. The name - parameter lets you make the error message more clear: - - >>> select_values('-xyz,hij', ['abc', 'def', 'hij'], name="foobar") - Traceback (most recent call last): - ... - UserError: Invalid foobar xyz - ['abc', 'def', 'hij'] - >>> select_values('xyz,hij', ['abc', 'def', 'hij'], name="foobar") - Traceback (most recent call last): - ... - UserError: Invalid foobar xyz - ['abc', 'def', 'hij'] - """ - possible_values = list(possible_values) # don't alter the original - if string == None: - pass - elif string.startswith('-'): - blacklisted_values = set(string[1:].split(',')) - for value in blacklisted_values: - if value not in possible_values: - raise UserError('Invalid %s %s\n %s' - % (name, value, possible_values)) - possible_values.remove(value) - else: - whitelisted_values = string.split(',') - for value in whitelisted_values: - if value not in possible_values: - raise UserError('Invalid %s %s\n %s' - % (name, value, possible_values)) - possible_values = whitelisted_values - return possible_values -- cgit From f8a498f76d7bbcb42cf7bbc80164d98bfe57f8ab Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 12 Dec 2009 01:43:20 -0500 Subject: Added libbe.ui.util.user for managing user ids. --- libbe/storage/util/config.py | 5 ++- libbe/storage/vcs/base.py | 8 ++--- libbe/ui/base.py | 23 ------------ libbe/ui/util/user.py | 84 ++++++++++++++++++++++++++++++++++++++++++++ libbe/util/encoding.py | 36 +++++-------------- 5 files changed, 99 insertions(+), 57 deletions(-) create mode 100644 libbe/ui/util/user.py diff --git a/libbe/storage/util/config.py b/libbe/storage/util/config.py index ccd236b..a0fea0c 100644 --- a/libbe/storage/util/config.py +++ b/libbe/storage/util/config.py @@ -22,16 +22,15 @@ Create, save, and load the per-user config file at path(). import ConfigParser import codecs -import locale import os.path -import sys import libbe +import libbe.util.encoding if libbe.TESTING == True: import doctest -default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() +default_encoding = libbe.util.encoding.get_filesystem_encoding() def path(): """Return the path to the per-user config file""" diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index abc7a01..f8b0727 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -207,11 +207,11 @@ class VCS(object): # methods for getting the BugDir situated in the filesystem def _find_root(self, path): - """ + ''' Search for an existing bug database dir and it's ancestors and return a BugDir rooted there. Only called by __init__, and then only if sink_to_existing_root == True. - """ + ''' if not os.path.exists(path): self.root = None raise NoRootEntry(path) @@ -229,9 +229,9 @@ class VCS(object): return beroot def _guess_storage(self, allow_storage_init=False): - """ + ''' Only called by __init__. - """ + ''' deepdir = self.get_path() if not os.path.exists(deepdir): deepdir = os.path.dirname(deepdir) diff --git a/libbe/ui/base.py b/libbe/ui/base.py index d26115f..e69de29 100644 --- a/libbe/ui/base.py +++ b/libbe/ui/base.py @@ -1,23 +0,0 @@ - def _setup_user_id(self, user_id): - if isinstance(self.storage, storage.vcs.base.VCS): - self.storage.user_id = user_id - def _guess_user_id(self): - if isinstance(self.storage, storage.vcs.base.VCS): - return self.storage.get_user_id() - def _set_user_id(self, old_user_id, new_user_id): - self._setup_user_id(new_user_id) - self._prop_save_settings(old_user_id, new_user_id) - - @_versioned_property(name="user_id", - doc= -"""The user's prefered name, e.g. 'John Doe '. Note -that the Arch VCS backend *enforces* ids with this format.""", - change_hook=_set_user_id, - generator=_guess_user_id) - def user_id(): return {} - - @_versioned_property(name="default_assignee", - doc= -"""The default assignee for new bugs e.g. 'John Doe '.""") - def default_assignee(): return {} - diff --git a/libbe/ui/util/user.py b/libbe/ui/util/user.py new file mode 100644 index 0000000..de2138c --- /dev/null +++ b/libbe/ui/util/user.py @@ -0,0 +1,84 @@ +# Copyright + +""" +Tools for getting, setting, creating, and parsing the user's id. For +example, + 'John Doe ' +Note that the Arch VCS backend *enforces* ids with this format. +""" + +import libbe.storage.util.config + +def _get_fallback_username(self): + name = None + for env in ["LOGNAME", "USERNAME"]: + if os.environ.has_key(env): + name = os.environ[env] + break + assert name != None + return name + +def _get_fallback_email(self): + hostname = gethostname() + name = _get_fallback_username() + return "%s@%s" % (name, hostname) + +def create_user_id(self, name, email=None): + """ + >>> create_id("John Doe", "jdoe@example.com") + 'John Doe ' + >>> create_id("John Doe") + 'John Doe' + """ + assert len(name) > 0 + if email == None or len(email) == 0: + return name + else: + return "%s <%s>" % (name, email) + +def parse_user_id(self, value): + """ + >>> parse_id("John Doe ") + ('John Doe', 'jdoe@example.com') + >>> parse_id("John Doe") + ('John Doe', None) + >>> try: + ... parse_id("John Doe ") + ... except AssertionError: + ... print "Invalid match" + Invalid match + """ + emailexp = re.compile("(.*) <([^>]*)>(.*)") + match = emailexp.search(value) + if match == None: + email = None + name = value + else: + assert len(match.groups()) == 3 + assert match.groups()[2] == "", match.groups() + email = match.groups()[1] + name = match.groups()[0] + assert name != None + assert len(name) > 0 + return (name, email) + +def get_user_id(self, storage=None): + """ + Sometimes the storage will also keep track of the user id (e.g. most VCSs). + """ + user = libbe.storage.util.config.get_val('user') + if user != None: + return user + if storage != None and hasattr(storage, 'get_user_id'): + user = vcs.get_user_id() + if user != None: + return user + name = _get_fallback_username() + email = _get_fallback_email() + user = _create_user_id(name, email) + return user + +def set_user_id(self, user_id): + """ + """ + user = libbe.storage.util.config.set_val('user', user_id) diff --git a/libbe/util/encoding.py b/libbe/util/encoding.py index 21e40cf..67131bf 100644 --- a/libbe/util/encoding.py +++ b/libbe/util/encoding.py @@ -43,6 +43,15 @@ def get_encoding(): # Python 2.3 on windows doesn't know about 'XYZ' alias for 'cpXYZ' return encoding +def get_input_encoding(): + return get_encoding() + +def get_output_encoding(): + return get_encoding(): + +def get_filesystem_encoding(): + return get_encoding() + def known_encoding(encoding): """ >>> known_encoding("highly-unlikely-encoding") @@ -56,32 +65,5 @@ def known_encoding(encoding): except LookupError: return False -def set_IO_stream_encodings(encoding): - sys.stdin = codecs.getreader(encoding)(sys.__stdin__) - sys.stdout = codecs.getwriter(encoding)(sys.__stdout__) - sys.stderr = codecs.getwriter(encoding)(sys.__stderr__) - - - def _guess_encoding(self): - return encoding.get_encoding() - def _check_encoding(value): - if value != None: - return encoding.known_encoding(value) - def _setup_encoding(self, new_encoding): - # change hook called before generator. - if new_encoding not in [None, settings_object.EMPTY]: - if self._manipulate_encodings == True: - encoding.set_IO_stream_encodings(new_encoding) - def _set_encoding(self, old_encoding, new_encoding): - self._setup_encoding(new_encoding) - self._prop_save_settings(old_encoding, new_encoding) - - @_versioned_property(name="encoding", - doc="""The default input/output encoding to use (e.g. "utf-8").""", - change_hook=_set_encoding, - generator=_guess_encoding, - check_fn=_check_encoding) - def encoding(): return {} - if libbe.TESTING == True: suite = doctest.DocTestSuite() -- cgit From 86f886399813d37f3cfcf74a824d352e01eb0d8c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 12 Dec 2009 01:46:22 -0500 Subject: Use get_input/output_encoding() in libbe.command.base.Command --- libbe/command/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libbe/command/base.py b/libbe/command/base.py index d18e070..0db156b 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -4,6 +4,7 @@ import optparse import sys import libbe +import libbe.util.encoding import libbe.util.plugin class UserError(Exception): @@ -170,9 +171,9 @@ class Command (object): def _setup_io(self, input_encoding=None, output_encoding=None): if input_encoding == None: - input_encoding = get_terminal_encoding() + input_encoding = libbe.util.get_input_encoding() if output_encoding == None: - output_encoding = get_terminal_encoding() + output_encoding = libbe.util.get_output_encoding() self.stdin = codecs.getwriter(input_encoding)(sys.stdin) self.stdin.encoding = input_encoding self.stdout = codecs.getwriter(output_encoding)(sys.stdout) -- cgit From dff6bd9bf89ca80e2265696a478e540476718c9c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 12 Dec 2009 20:57:59 -0500 Subject: Moved be to libbe.ui.command_line and transitioned to Command format. --- be | 102 +------------ libbe/bug.py | 2 +- libbe/bugdir.py | 2 +- libbe/command/__init__.py | 4 +- libbe/command/base.py | 102 +++++++------ libbe/command/list.py | 1 + libbe/command/util.py | 35 ++++- libbe/comment.py | 2 +- libbe/storage/util/settings_object.py | 8 +- libbe/ui/base.py | 1 + libbe/ui/command_line.py | 270 ++++++++++++++++++++++++++++++++++ libbe/ui/util/cmdutil.py | 108 -------------- libbe/util/encoding.py | 2 +- setup.py | 4 +- 14 files changed, 375 insertions(+), 268 deletions(-) create mode 100755 libbe/ui/command_line.py diff --git a/be b/be index 5e3088e..5f7bd1b 100755 --- a/be +++ b/be @@ -1,103 +1,7 @@ #!/usr/bin/env python -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Chris Ball -# Gianluca Montecchi -# Oleg Romanyshyn -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# Copyright -import os import sys +import libbe.ui.command_line -from libbe import cmdutil, version, pager - -__doc__ = cmdutil.help() - -usage = "be [options] [command] [command_options ...] [command_args ...]" - -parser = cmdutil.CmdOptionParser(usage) -parser.command = "be" -parser.add_option("--version", action="store_true", dest="version", - help="Print version string and exit.") -parser.add_option("--verbose-version", action="store_true", dest="verbose_version", - help="Print verbose version information and exit.") -parser.add_option("-d", "--dir", dest="dir", metavar="DIR", default=".", - help="Run this command on the repository in DIR instead of the current directory.") -parser.add_option("-p", "--paginate", dest="paginate", default=False, - action='store_true', - help="Pipe all output into less (or if set, $PAGER).") -parser.add_option("--no-pager", dest="no_pager", default=False, - action='store_true', - help="Do not pipe git output into a pager.") - -# Option(name='repo', short_name='r', -# help='Select BE repository (see `be help repo`) rather than' -# 'the current directory.', -# arg=Argument(name='repo', metavar='REPO', default='.', -# completion_callback=libbe.ui.util.repo.complete)), - -try: - options,args = parser.parse_args() - for option,value in cmdutil.option_value_pairs(options, parser): - if value == "--complete": - if option == "dir": - if len(args) == 0: - args = ["."] - paths = cmdutil.complete_path(args[0]) - raise cmdutil.GetCompletions(paths) -except cmdutil.GetHelp: - print cmdutil.help(parser=parser) - sys.exit(0) -except cmdutil.GetCompletions, e: - print '\n'.join(e.completions) - sys.exit(0) - -if options.version == True or options.verbose_version == True: - print version.version(verbose=options.verbose_version) - sys.exit(0) - -paginate = 'auto' -if options.paginate == True: - paginate = 'always' -if options.no_pager== True: - paginate = 'never' -pager.run_pager(paginate) - -try: - if len(args) == 0: - raise cmdutil.UsageError, "must supply a command" - sys.exit(cmdutil.execute(args[0], args=args[1:], dir=options.dir)) -except cmdutil.GetHelp: - print cmdutil.help(args[0]) - sys.exit(0) -except cmdutil.GetCompletions, e: - print '\n'.join(e.completions) - sys.exit(0) -except cmdutil.UnknownCommand, e: - print e - sys.exit(1) -except cmdutil.UsageError, e: - print "Invalid usage:", e - if len(args) == 0: - print cmdutil.help(parser=parser) - else: - print "\nArgs:", args - print cmdutil.help(args[0]) - sys.exit(1) -except cmdutil.UserError, e: - print "ERROR:" - print e - sys.exit(1) +sys.exit(libbe.ui.command_line.main()) diff --git a/libbe/bug.py b/libbe/bug.py index 1f96779..d62de49 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -37,7 +37,7 @@ import libbe.util.id from libbe.storage.util.properties import Property, doc_property, \ local_property, defaulting_property, checked_property, cached_property, \ primed_property, change_hook_property, settings_property -import libbe.storage.settings_object as settings_object +import libbe.storage.util.settings_object as settings_object import libbe.storage.util.mapfile as mapfile import libbe.comment as comment import libbe.util.utility as utility diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 39eface..5f76d3c 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -36,7 +36,7 @@ from libbe.storage.util.properties import Property, doc_property, \ local_property, defaulting_property, checked_property, \ fn_checked_property, cached_property, primed_property, \ change_hook_property, settings_property -import libbe.storage.settings_object as settings_object +import libbe.storage.util.settings_object as settings_object import libbe.storage.util.mapfile as mapfile import libbe.bug as bug import libbe.util.utility as utility diff --git a/libbe/command/__init__.py b/libbe/command/__init__.py index 344a8a2..8558882 100644 --- a/libbe/command/__init__.py +++ b/libbe/command/__init__.py @@ -18,12 +18,12 @@ import base UserError = base.UserError -UnkownCommand = base.UnknownCommand +UnknownCommand = base.UnknownCommand get_command = base.get_command commands = base.commands Option = base.Option Argument = base.Argument Command = base.Command -__all__ = [UserError, UnkownCommand, get_command, commands, +__all__ = [UserError, UnknownCommand, get_command, commands, Option, Argument, Command] diff --git a/libbe/command/base.py b/libbe/command/base.py index 0db156b..252dd24 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -36,7 +36,7 @@ def get_command(command_name): def commands(): for modname in libbe.util.plugin.modnames('libbe.command'): - if modname != 'base': + if modname not in ['base', 'util']: yield modname class CommandInput (object): @@ -44,28 +44,48 @@ class CommandInput (object): self.name = name self.help = help -class Option (CommandInput): - def __init__(self, option_callback=None, short_name=None, arg=None, - type=None, *args, **kwargs): - CommandInput.__init__(self, *args, **kwargs) - self.option_callback = option_callback - self.short_name = short_name - self.arg = arg - self.type = type - if self.arg != None: - assert self.arg.name == self.name, \ - 'Name missmatch: %s != %s' % (self.arg.name, self.name) - class Argument (CommandInput): - def __init__(self, metavar=None, default=None, + def __init__(self, metavar=None, default=None, type='string', optional=False, repeatable=False, completion_callback=None, *args, **kwargs): CommandInput.__init__(self, *args, **kwargs) self.metavar = metavar self.default = default + self.type = type self.optional = optional self.repeatable = repeatable self.completion_callback = completion_callback + if self.metavar == None: + self.metavar = self.name.upper() + +class Option (CommandInput): + def __init__(self, callback=None, short_name=None, arg=None, + *args, **kwargs): + CommandInput.__init__(self, *args, **kwargs) + self.callback = callback + self.short_name = short_name + self.arg = arg + if self.arg == None and self.callback == None: + # use an implicit boolean argument + self.arg = Argument(name=self.name, help=self.help, + default=False, type='bool') + self.validate() + + def validate(self): + if self.arg == None: + assert self.callback != None + return + assert self.callback == None, self.callback + assert self.arg.name == self.name, \ + 'Name missmatch: %s != %s' % (self.arg.name, self.name) + assert self.arg.optional == False + assert self.arg.repeatable == False + + def __str__(self): + return '--%s' % self.name + + def __repr__(self): + return '
\n") # close every remaining
> 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 bug_entries = self._generate_index_bug_entries(bugs) - if bug_type == "active": - filename = "index.html" - elif bug_type == "inactive": - filename = "index_inactive.html" + if bug_type == 'active': + filename = 'index.html' + elif bug_type == 'inactive': + filename = 'index_inactive.html' else: - raise Exception, "Unrecognized bug_type: '%s'" % bug_type + raise Exception, 'Unrecognized bug_type: "%s"' % bug_type template_info = {'title':title, 'index_header':index_header, 'charset':self.encoding, @@ -262,7 +290,7 @@ class HTMLGen (object): 'inactive_class':'tab nsel', 'bug_entries':bug_entries, 'generation_time':self.generation_time} - if bug_type == "inactive": + if bug_type == 'inactive': template_info['active_class'] = 'tab nsel' template_info['inactive_class'] = 'tab sel' @@ -273,8 +301,8 @@ class HTMLGen (object): bug_entries = [] for bug in bugs: if self.verbose: - print "\tCreating bug entry for %s" % self.bd.bug_shortname(bug) - template_info = {'shortname':self.bd.bug_shortname(bug)} + 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)) @@ -283,15 +311,15 @@ class HTMLGen (object): def _escape(self, string): if string == None: - return "" + return '' chars = [] for char in string: codepoint = ord(char) if codepoint in htmlentitydefs.codepoint2name: - char = "&%s;" % htmlentitydefs.codepoint2name[codepoint] + char = '&%s;' % htmlentitydefs.codepoint2name[codepoint] #else: xml.sax.saxutils.escape(char) chars.append(char) - return "".join(chars) + return ''.join(chars) def _load_user_templates(self): for filename,attr in [('style.css','css_file'), @@ -309,7 +337,7 @@ class HTMLGen (object): try: os.makedirs(dir_path) except: - raise cmdutil.UsageError, "Cannot create output directory '%s'." % dir_path + raise cmdutil.UsageError, 'Cannot create output directory "%s".' % dir_path return dir_path def _write_file(self, content, path_array, mode='w'): @@ -322,27 +350,27 @@ class HTMLGen (object): def write_default_template(self, out_dir): if self.verbose: - print "Creating output directories" + print >> self.stdout, 'Creating output directories' self.out_dir = self._make_dir(out_dir) if self.verbose: - print "Creating css file" + print >> self.stdout, 'Creating css file' self._write_css_file() if self.verbose: - print "Creating index_file.tpl file" + print >> self.stdout, 'Creating index_file.tpl file' self._write_file(self.index_file, - [self.out_dir, "index_file.tpl"]) + [self.out_dir, 'index_file.tpl']) if self.verbose: - print "Creating index_bug_entry.tpl file" + print >> self.stdout, 'Creating index_bug_entry.tpl file' self._write_file(self.index_bug_entry, - [self.out_dir, "index_bug_entry.tpl"]) + [self.out_dir, 'index_bug_entry.tpl']) if self.verbose: - print "Creating bug_file.tpl file" + print >> self.stdout, 'Creating bug_file.tpl file' self._write_file(self.bug_file, - [self.out_dir, "bug_file.tpl"]) + [self.out_dir, 'bug_file.tpl']) if self.verbose: - print "Creating bug_comment_entry.tpl file" + print >> self.stdout, 'Creating bug_comment_entry.tpl file' self._write_file(self.bug_comment_entry, - [self.out_dir, "bug_comment_entry.tpl"]) + [self.out_dir, 'bug_comment_entry.tpl']) def _load_default_templates(self): self.css_file = """ diff --git a/libbe/util/encoding.py b/libbe/util/encoding.py index 434bae7..dcc41f8 100644 --- a/libbe/util/encoding.py +++ b/libbe/util/encoding.py @@ -78,7 +78,7 @@ def get_file_contents(path, mode='r', encoding=None, decode=False): return contents def set_file_contents(path, contents, mode='w', encoding=None): - if type(value) == types.UnicodeType: + if type(contents) == types.UnicodeType: if encoding == None: encoding = get_filesystem_encoding() f = codecs.open(path, mode, encoding) -- cgit From af5c37f7ddfb5e4b9428a1a8e7bd7dc882ec1690 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 14 Dec 2009 21:18:06 -0500 Subject: Transitioned new to Command-format --- libbe/command/html.py | 1 + libbe/command/new.py | 107 +++++++++++++++++++++++++++----------------------- 2 files changed, 59 insertions(+), 49 deletions(-) diff --git a/libbe/command/html.py b/libbe/command/html.py index cd6e81f..485753f 100644 --- a/libbe/command/html.py +++ b/libbe/command/html.py @@ -108,6 +108,7 @@ class HTML (libbe.command.Command): verbose=params['verbose'], stdout=self.stdout) html_gen.run(params['output']) + return 0 def _long_help(self): return """ diff --git a/libbe/command/new.py b/libbe/command/new.py index 30f8834..d840503 100644 --- a/libbe/command/new.py +++ b/libbe/command/new.py @@ -15,23 +15,30 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Create a new bug""" -from libbe import cmdutil, bugdir -import sys -__desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os, time - >>> from libbe import bug - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> bug.uuid_gen = lambda: "X" - >>> execute (["this is a test",], manipulate_encodings=False) - Created bug with ID X - >>> bd._clear_bugs() - >>> bug = bd.bug_from_uuid("X") +import libbe +import libbe.command +import libbe.command.util + + +class New (libbe.command.Command): + """Create a new bug + + >>> import os + >>> import sys + >>> import time + >>> import libbe.bugdir + >>> import libbe.util.id + >>> bd = libbe.bugdir.SimpleBugDir(memory=False) + >>> cmd = New() + >>> cmd._setup_io = lambda i_enc,o_enc : None + >>> cmd.stdout = sys.stdout + + >>> libbe.util.id.uuid_gen = lambda: 'X' + >>> ret = cmd.run(bd.storage, bd, args=['this is a test',]) + Created bug with ID abc/X + >>> bd.flush_reload() + >>> bug = bd.bug_from_uuid('X') >>> print bug.summary this is a test >>> bug.time <= int(time.time()) @@ -42,42 +49,44 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False, open >>> bd.cleanup() """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser) - if len(args) != 1: - raise cmdutil.UsageError("Please supply a summary message") - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if args[0] == '-': # read summary from stdin - summary = sys.stdin.readline() - else: - summary = args[0] - bug = bd.new_bug(summary=summary.strip()) - if options.reporter != None: - bug.reporter = options.reporter - else: - bug.reporter = bug.creator - if options.assigned != None: - bug.assigned = options.assigned - elif bd.default_assignee != None: - bug.assigned = bd.default_assignee - print "Created bug with ID %s" % bd.bug_shortname(bug) + name = 'new' + + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.requires_bugdir = True + self.options.extend([ + libbe.command.Option(name='reporter', short_name='r', + help='The user who reported the bug', + arg=libbe.command.Argument( + name='reporter', metavar='NAME')), + libbe.command.Option(name='assigned', short_name='a', + help='The developer in charge of the bug', + arg=libbe.command.Argument( + name='assigned', metavar='NAME', + completion_callback=libbe.command.util.complete_assigned)), + ]) + self.args.extend([ + libbe.command.Argument(name='summary', metavar='SUMMARY') + ]) -def get_parser(): - parser = cmdutil.CmdOptionParser("be new SUMMARY") - parser.add_option("-r", "--reporter", metavar="REPORTER", dest="reporter", - help="The user who reported the bug", default=None) - parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", - help="The developer in charge of the bug", default=None) - return parser + def _run(self, storage, bugdir, **params): + if params['summary'] == '-': # read summary from stdin + summary = self.stdin.readline() + else: + summary = params['summary'] + bug = bugdir.new_bug(summary=summary.strip()) + if params['reporter'] != None: + bug.reporter = params['reporter'] + else: + bug.reporter = bug.creator + if params['assigned'] != None: + bug.assigned = params['assigned'] + print >> self.stdout, 'Created bug with ID %s' % bug.id.user() + return 0 -longhelp=""" + def _long_help(self): + return """ Create a new bug, with a new ID. The summary specified on the commandline is a string (only one line) that describes the bug briefly or "-", in which case the string will be read from stdin. """ - -def help(): - return get_parser().help_str() + longhelp -- cgit From 0786fc6693e40cdfaca7876b504acb3e5e7dc4d2 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 14 Dec 2009 21:29:48 -0500 Subject: Transitioned remove to Command-format --- libbe/bugdir.py | 4 +-- libbe/command/remove.py | 91 +++++++++++++++++++++++++++---------------------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/libbe/bugdir.py b/libbe/bugdir.py index aa4d990..5bb69e2 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -266,8 +266,8 @@ class BugDir (list, settings_object.SavedSettingsObject): def bug_from_uuid(self, uuid): if not self.has_bug(uuid): - raise KeyError("No bug matches %s\n bug map: %s\n root: %s" \ - % (uuid, self._bug_map, self.root)) + raise NoBugMatches('No bug matches %s\n bug map: %s\n repo: %s' \ + % (uuid, self._bug_map, self.storage)) if self._bug_map[uuid] == None: self._load_bug(uuid) return self._bug_map[uuid] diff --git a/libbe/command/remove.py b/libbe/command/remove.py index bac06c0..e516443 100644 --- a/libbe/command/remove.py +++ b/libbe/command/remove.py @@ -14,52 +14,63 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Remove (delete) a bug and its comments""" -from libbe import cmdutil, bugdir -__desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> from libbe import mapfile - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> print bd.bug_from_shortname("b").status +import libbe +import libbe.command +import libbe.command.util + + +class Remove (libbe.command.Command): + """Remove (delete) a bug and its comments + + >>> import sys + >>> import libbe.bugdir + >>> bd = libbe.bugdir.SimpleBugDir(memory=False) + >>> cmd = Remove() + >>> cmd._setup_io = lambda i_enc,o_enc : None + >>> cmd.stdout = sys.stdout + + >>> print bd.bug_from_uuid('b').status closed - >>> execute (["b"], manipulate_encodings=False) - Removed bug b - >>> bd._clear_bugs() + >>> ret = cmd.run(bd.storage, bd, args=['/b']) + Removed bug abc/b + >>> bd.flush_reload() >>> try: - ... bd.bug_from_shortname("b") - ... except bugdir.NoBugMatches: - ... print "Bug not found" + ... bd.bug_from_uuid('b') + ... except libbe.bugdir.NoBugMatches: + ... print 'Bug not found' Bug not found >>> bd.cleanup() """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) - if len(args) != 1: - raise cmdutil.UsageError, "Please specify a bug id." - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bug = cmdutil.bug_from_id(bd, args[0]) - bd.remove_bug(bug) - print "Removed bug %s" % bug.uuid + name = 'remove' -def get_parser(): - parser = cmdutil.CmdOptionParser("be remove BUG-ID") - return parser + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.requires_bugdir = True + self.args.extend([ + libbe.command.Argument( + name='bug-id', metavar='BUG-ID', default=None, + repeatable=True, + completion_callback=libbe.command.util.complete_bug_id), + ]) -longhelp=""" -Remove (delete) an existing bug. Use with caution: if you're not using a -revision control system, there may be no way to recover the lost information. -You should use this command, for example, to get rid of blank or otherwise -mangled bugs. -""" + def _run(self, storage, bugdir, **params): + user_ids = [] + for bug_id in params['bug-id']: + bug,dummy_comment = libbe.command.util.bug_comment_from_user_id( + bugdir, bug_id) + user_ids.append(bug.id.user()) + bugdir.remove_bug(bug) + if len(user_ids) == 1: + print >> self.stdout, 'Removed bug %s' % user_ids[0] + else: + print >> self.stdout, 'Removed bugs %s' % ', '.join(user_ids) + return 0 -def help(): - return get_parser().help_str() + longhelp + def _long_help(self): + return """ +Remove (delete) existing bugs. Use with caution: if you're not using +a revision control system, there may be no way to recover the lost +information. You should use this command, for example, to get rid of +blank or otherwise mangled bugs. +""" -- cgit From 1b9c628529848af370adbc67b5ba298236a1b86d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 14 Dec 2009 23:15:58 -0500 Subject: Transitioned severity to Command-format, also added Command._get_*() The old .requires_* thing was rediculous. The new ._get_*() callbacks allow the caller to provide a means for getting the expensive structures, which the command can use, or not, as required. This will also make it easier to implement the completion callbacks. The callbacks should probably have matching .set_*() methods, to avoid the current cache tweaking cmd._storage = ... etc. But that can wait for now... --- libbe/command/assign.py | 31 +++++----- libbe/command/base.py | 73 +++++++++++++++++------- libbe/command/comment.py | 13 +++-- libbe/command/commit.py | 10 ++-- libbe/command/depend.py | 21 +++---- libbe/command/due.py | 15 ++--- libbe/command/help.py | 5 +- libbe/command/html.py | 7 ++- libbe/command/import_xml.py | 12 ++-- libbe/command/init.py | 10 ++-- libbe/command/list.py | 10 ++-- libbe/command/merge.py | 9 +-- libbe/command/new.py | 7 ++- libbe/command/remove.py | 7 ++- libbe/command/severity.py | 135 ++++++++++++++++++++------------------------ libbe/ui/command_line.py | 23 ++------ libbe/ui/util/cmdutil.py | 14 ----- 17 files changed, 208 insertions(+), 194 deletions(-) diff --git a/libbe/command/assign.py b/libbe/command/assign.py index 241c815..6cf74bc 100644 --- a/libbe/command/assign.py +++ b/libbe/command/assign.py @@ -30,24 +30,25 @@ class Assign (libbe.command.Command): >>> import libbe.bugdir >>> bd = libbe.bugdir.SimpleBugDir(memory=False) >>> cmd = Assign() + >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout >>> bd.bug_from_uuid('a').assigned is None True - >>> ret = cmd.run(bd.storage, bd, {'user-id':u'Fran\xe7ois'}, ['-', '/a']) + >>> ret = cmd.run({'user-id':u'Fran\xe7ois'}, ['-', '/a']) >>> bd.flush_reload() >>> bd.bug_from_uuid('a').assigned u'Fran\\xe7ois' - >>> ret = cmd.run(bd.storage, bd, args=['someone', '/a', '/b']) + >>> ret = cmd.run(args=['someone', '/a', '/b']) >>> bd.flush_reload() >>> bd.bug_from_uuid('a').assigned 'someone' >>> bd.bug_from_uuid('b').assigned 'someone' - >>> ret = cmd.run(bd.storage, bd, args=['none', '/a']) + >>> ret = cmd.run(args=['none', '/a']) >>> bd.flush_reload() >>> bd.bug_from_uuid('a').assigned is None True @@ -57,10 +58,9 @@ class Assign (libbe.command.Command): def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_bugdir = True self.args.extend([ libbe.command.Argument( - name='assignee', metavar='ASSIGNEE', default=None, + name='assigned', metavar='ASSIGNED', default=None, completion_callback=libbe.command.util.complete_assigned), libbe.command.Argument( name='bug-id', metavar='BUG-ID', default=None, @@ -68,27 +68,28 @@ class Assign (libbe.command.Command): completion_callback=libbe.command.util.complete_bug_id), ]) - def _run(self, storage, bugdir, **params): - assignee = params['assignee'] - if assignee == 'none': - assignee = None - elif assignee == '-': - assignee = params['user-id'] + def _run(self, **params): + assigned = params['assigned'] + if assigned == 'none': + assigned = None + elif assigned == '-': + assigned = self._get_user_id() + bugdir = self._get_bugdir() for bug_id in params['bug-id']: bug,dummy_comment = \ libbe.command.util.bug_comment_from_user_id(bugdir, bug_id) - if bug.assigned != assignee: - bug.assigned = assignee + if bug.assigned != assigned: + bug.assigned = assigned return 0 def _long_help(self): return """ Assign a person to fix a bug. -Assignees should be the person's Bugs Everywhere identity, the same +Assigneds should be the person's Bugs Everywhere identity, the same string that appears in Creator fields. -Special assignee strings: +Special assigned strings: "-" assign the bug to yourself "none" un-assigns the bug """ diff --git a/libbe/command/base.py b/libbe/command/base.py index 54463c8..2aaf51e 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -177,16 +177,15 @@ class Command (object): name = 'command' - def __init__(self, input_encoding=None, output_encoding=None): + def __init__(self, input_encoding=None, output_encoding=None, + get_unconnected_storage=None, ui=None): + self.input_encoding = input_encoding + self.output_encoding = output_encoding + self.get_unconnected_storage = get_unconnected_storage + self.ui = ui # calling user-interface, e.g. for Help() self.status = None self.result = None - self.ui = None # calling user-interface, e.g. for Help() - self.requires_bugdir = False - self.requires_storage = False - self.requires_unconnected_storage = False self.restrict_file_access = True - self.input_encoding = None - self.output_encoding = None self.options = [ Option(name='help', short_name='h', help='Print a help message.', @@ -197,7 +196,7 @@ class Command (object): ] self.args = [] - def run(self, storage=None, bugdir=None, options=None, args=None): + def run(self, options=None, args=None): if options == None: options = {} if args == None: @@ -213,9 +212,7 @@ class Command (object): params[option.name] = False assert 'user-id' not in params, params['user-id'] if 'user-id' in options: - params['user-id'] = options.pop('user-id') - else: - params['user-id'] = libbe.ui.util.user.get_user_id(storage) + self._user_id = options.pop('user-id') if len(options) > 0: raise UserError, 'Invalid option passed to command %s:\n %s' \ % (self.name, '\n '.join(['%s: %s' % (k,v) @@ -253,11 +250,11 @@ class Command (object): params.pop('complete') self._setup_io(self.input_encoding, self.output_encoding) - self.status = self._run(storage, bugdir, **params) + self.status = self._run(**params) return self.status - def _run(self, storage, bugdir, **kwargs): - pass + def _run(self, **kwargs): + raise NotImplementedError def _setup_io(self, input_encoding=None, output_encoding=None): if input_encoding == None: @@ -306,7 +303,7 @@ class Command (object): return argument.completion_callback(self, argument, fragment) return [] # the particular argument doesn't supply completion info - def check_restricted_access(self, storage, path): + def _check_restricted_access(self, storage, path): """ Check that the file at path is inside bugdir.root. This is important if you allow other users to execute becommands with @@ -322,15 +319,15 @@ class Command (object): >>> s.repo = os.path.expanduser('~/x/') >>> c = Command() >>> try: - ... c.check_restricted_access(s, os.path.expanduser('~/.ssh/id_rsa')) + ... c._check_restricted_access(s, os.path.expanduser('~/.ssh/id_rsa')) ... except UserError, e: ... assert str(e).startswith('file access restricted!'), str(e) ... print 'we got the expected error' we got the expected error - >>> c.check_restricted_access(s, os.path.expanduser('~/x')) - >>> c.check_restricted_access(s, os.path.expanduser('~/x/y')) + >>> c._check_restricted_access(s, os.path.expanduser('~/x')) + >>> c._check_restricted_access(s, os.path.expanduser('~/x/y')) >>> c.restrict_file_access = False - >>> c.check_restricted_access(s, os.path.expanduser('~/.ssh/id_rsa')) + >>> c._check_restricted_access(s, os.path.expanduser('~/.ssh/id_rsa')) """ if self.restrict_file_access == True: path = os.path.abspath(path) @@ -339,3 +336,41 @@ class Command (object): return raise UserError('file access restricted!\n %s not in %s' % (path, repo)) + + def _get_unconnected_storage(self): + """Callback for use by commands that need it.""" + if not hasattr(self, '_unconnected_storage'): + if self.get_unconnected_storage == None: + raise NotImplementedError + self._unconnected_storage = self.get_unconnected_storage() + return self._unconnected_storage + + def _get_storage(self): + """ + Callback for use by commands that need it. + + Note that with the current implementation, + _get_unconnected_storage() will not work after this method + runs, but that shouldn't be an issue for any command I can + think of... + """ + if not hasattr(self, '_storage'): + self._storage = self._get_unconnected_storage() + self._storage.connect() + return self._storage + + def _get_bugdir(self): + """Callback for use by commands that need it.""" + if not hasattr(self, '_bugdir'): + self._bugdir = libbe.bugdir.BugDir(self._get_storage(), from_storage=True) + return self._bugdir + + def _get_user_id(self): + """Callback for use by commands that need it.""" + if not hasattr(self, '_user_id'): + self._user_id = libbe.ui.util.user.get_user_id(self._get_storage()) + return self._user_id + + def cleanup(self): + if hasattr(self, '_storage'): + self._storage.disconnect() diff --git a/libbe/command/comment.py b/libbe/command/comment.py index 23def57..cb0dcbb 100644 --- a/libbe/command/comment.py +++ b/libbe/command/comment.py @@ -34,10 +34,11 @@ class Comment (libbe.command.Command): >>> import libbe.bugdir >>> bd = libbe.bugdir.SimpleBugDir(memory=False) >>> cmd = Comment() + >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout - >>> ret = cmd.run(bd.storage, bd, {'user-id':u'Fran\\xe7ois'}, + >>> ret = cmd.run({'user-id':u'Fran\\xe7ois'}, ... ['/a', 'This is a comment about a']) >>> bd.flush_reload() >>> bug = bd.bug_from_uuid('a') @@ -57,12 +58,12 @@ class Comment (libbe.command.Command): >>> if 'EDITOR' in os.environ: ... del os.environ['EDITOR'] - >>> ret = cmd.run(bd.storage, bd, {'user-id':u'Frank'}, ['/b']) + >>> ret = cmd.run({'user-id':u'Frank'}, ['/b']) Traceback (most recent call last): UserError: No comment supplied, and EDITOR not specified. >>> os.environ['EDITOR'] = "echo 'I like cheese' > " - >>> ret = cmd.run(bd.storage, bd, {'user-id':u'Frank'}, ['/b']) + >>> ret = cmd.run({'user-id':u'Frank'}, ['/b']) >>> bd.flush_reload() >>> bug = bd.bug_from_uuid('b') >>> bug.load_comments(load_full=False) @@ -76,7 +77,6 @@ class Comment (libbe.command.Command): def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_bugdir = True self.options.extend([ libbe.command.Option(name='author', short_name='a', help='Set the comment author', @@ -101,7 +101,8 @@ class Comment (libbe.command.Command): completion_callback=libbe.command.util.complete_assigned), ]) - def _run(self, storage, bugdir, **params): + def _run(self, **params): + bugdir = self._get_bugdir() bug,parent = \ libbe.command.util.bug_comment_from_user_id(bugdir, params['id']) if params['comment'] == None: @@ -133,7 +134,7 @@ class Comment (libbe.command.Command): if not body.endswith('\n'): body+='\n' if params['author'] == None: - params['author'] = params['user-id'] + params['author'] = self._get_user_id() new = parent.new_reply(body=body) for key in ['alt-id', 'author', 'content-type']: diff --git a/libbe/command/commit.py b/libbe/command/commit.py index f795e80..4ef619c 100644 --- a/libbe/command/commit.py +++ b/libbe/command/commit.py @@ -40,11 +40,11 @@ class Commit (libbe.command.Command): >>> vcs.repo = dir.path >>> vcs.init() >>> vcs.connect() + >>> cmd._storage = vcs >>> if vcs.name in libbe.storage.vcs.base.VCS_ORDER: ... bd = libbe.bugdir.BugDir(vcs, from_storage=False) ... bd.extra_strings = ['hi there'] - ... cmd.run(vcs, None, {'user-id':'Joe'}, - ... ['Making a commit']) # doctest: +ELLIPSIS + ... cmd.run({'user-id':'Joe'}, ['Making a commit']) # doctest: +ELLIPSIS ... else: ... print 'Committed ...' Committed ... @@ -56,7 +56,6 @@ class Commit (libbe.command.Command): def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_storage = True self.options.extend([ libbe.command.Option(name='body', short_name='b', help='Provide the detailed body for the commit message. In the special case that FILE == "EDITOR", spawn an editor to enter the body text (in which case you cannot use stdin for the summary)', @@ -70,20 +69,21 @@ class Commit (libbe.command.Command): name='comment', metavar='COMMENT', default=None), ]) - def _run(self, storage, bugdir=None, **params): + def _run(self, **params): if params['comment'] == '-': # 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'] + storage = self._get_storage() if params['body'] == None: body = None elif params['body'] == 'EDITOR': body = libbe.ui.util.editor.editor_string( 'Please enter your commit message above') else: - self.check_restricted_access(storage, params['body']) + self._check_restricted_access(storage, params['body']) body = libbe.util.encoding.get_file_contents( params['body'], decode=True) try: diff --git a/libbe/command/depend.py b/libbe/command/depend.py index 471fd28..5e476fc 100644 --- a/libbe/command/depend.py +++ b/libbe/command/depend.py @@ -46,40 +46,40 @@ class Depend (libbe.command.Command): >>> import libbe.bugdir >>> bd = libbe.bugdir.SimpleBugDir(memory=False) >>> cmd = Depend() + >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout - >>> ret = cmd.run(bd.storage, bd, {}, ['/a', '/b']) + >>> ret = cmd.run(args=['/a', '/b']) a blocked by: b - >>> ret = cmd.run(bd.storage, bd, {}, ['/a']) + >>> ret = cmd.run(args=['/a']) a blocked by: b - >>> ret = cmd.run(bd.storage, bd, {'show-status':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE + >>> ret = cmd.run({'show-status':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE a blocked by: b closed - >>> ret = cmd.run(bd.storage, bd, {}, ['/b', '/a']) + >>> ret = cmd.run(args=['/b', '/a']) b blocked by: a b blocks: a - >>> ret = cmd.run(bd.storage, bd, {'show-status':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE + >>> ret = cmd.run({'show-status':True}, ['/a']) # doctest: +NORMALIZE_WHITESPACE a blocked by: b closed a blocks: b closed - >>> ret = cmd.run(bd.storage, bd, {'repair':True}) - >>> ret = cmd.run(bd.storage, bd, {'remove':True}, ['/b', '/a']) + >>> ret = cmd.run({'repair':True}) + >>> ret = cmd.run({'remove':True}, ['/b', '/a']) b blocks: a - >>> ret = cmd.run(bd.storage, bd, {'remove':True}, ['/a', '/b']) + >>> ret = cmd.run({'remove':True}, ['/a', '/b']) >>> bd.cleanup() """ name = 'depend' def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_bugdir = True self.options.extend([ libbe.command.Option(name='remove', short_name='r', help='Remove dependency (instead of adding it)'), @@ -114,7 +114,7 @@ class Depend (libbe.command.Command): completion_callback=libbe.command.util.complete_bug_id), ]) - def _run(self, storage, bugdir, **params): + def _run(self, **params): if params['repair'] == True and params['bug-id'] != None: raise libbe.command.UsageError( 'No arguments with --repair calls.') @@ -125,6 +125,7 @@ class Depend (libbe.command.Command): and params['blocking-bug-id'] != None: raise libbe.command.UsageError( 'Only one bug id used in tree mode.') + bugdir = self._get_bugdir() if params['repair'] == True: good,fixed,broken = check_dependencies(bugdir, repair_broken_links=True) assert len(broken) == 0, broken diff --git a/libbe/command/due.py b/libbe/command/due.py index 372f10a..119115c 100644 --- a/libbe/command/due.py +++ b/libbe/command/due.py @@ -28,16 +28,17 @@ class Due (libbe.command.Command): >>> import libbe.bugdir >>> bd = libbe.bugdir.SimpleBugDir(memory=False) >>> cmd = Due() + >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout - >>> ret = cmd.run(bd.storage, bd, {}, ['/a']) + >>> ret = cmd.run(args=['/a']) No due date assigned. - >>> ret = cmd.run(bd.storage, bd, {}, ['/a', 'Thu, 01 Jan 1970 00:00:00 +0000']) - >>> ret = cmd.run(bd.storage, bd, {}, ['/a']) + >>> ret = cmd.run(args=['/a', 'Thu, 01 Jan 1970 00:00:00 +0000']) + >>> ret = cmd.run(args=['/a']) Thu, 01 Jan 1970 00:00:00 +0000 - >>> ret = cmd.run(bd.storage, bd, {}, ['/a', 'none']) - >>> ret = cmd.run(bd.storage, bd, {}, ['/a']) + >>> ret = cmd.run(args=['/a', 'none']) + >>> ret = cmd.run(args=['/a']) No due date assigned. >>> bd.cleanup() """ @@ -45,7 +46,6 @@ class Due (libbe.command.Command): def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_bugdir = True self.args.extend([ libbe.command.Argument( name='bug-id', metavar='BUG-ID', @@ -54,7 +54,8 @@ class Due (libbe.command.Command): name='due', metavar='DUE', optional=True), ]) - def _run(self, storage, bugdir, **params): + def _run(self, **params): + bugdir = self._get_bugdir() bug,dummy_comment = libbe.command.util.bug_comment_from_user_id( bugdir, params['bug-id']) if params['due'] == None: diff --git a/libbe/command/help.py b/libbe/command/help.py index c8d700d..6e55598 100644 --- a/libbe/command/help.py +++ b/libbe/command/help.py @@ -54,14 +54,15 @@ class Help (libbe.command.Command): completion_callback=self.complete_topic) ]) - def _run(self, storage, bugdir, **params): + def _run(self, **params): if params['topic'] == None: if hasattr(self.ui, 'help'): self.ui.help() elif params['topic'] in libbe.command.commands(): module = libbe.command.get_command(params['topic']) Class = libbe.command.get_command_class(module,params['topic']) - c = Class() + c = Class(get_unconnected_storage=self.get_unconnected_storage, + ui=self.ui) print >> self.stdout, c.help().rstrip('\n') elif params['topic'] in TOPICS: print >> self.stdout, TOPICS[params['topic']].rstrip('\n') diff --git a/libbe/command/html.py b/libbe/command/html.py index 485753f..0f993ae 100644 --- a/libbe/command/html.py +++ b/libbe/command/html.py @@ -37,12 +37,13 @@ class HTML (libbe.command.Command): >>> import libbe.bugdir >>> bd = libbe.bugdir.SimpleBugDir(memory=False) >>> cmd = HTML() + >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout >>> cwd = os.getcwd() >>> os.chdir(bd.storage.repo) - >>> ret = cmd.run(bd.storage, bd) + >>> ret = cmd.run() >>> os.path.exists('./html_export') True >>> os.path.exists('./html_export/index.html') @@ -62,7 +63,6 @@ class HTML (libbe.command.Command): def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_bugdir = True self.options.extend([ libbe.command.Option(name='output', short_name='o', help='Set the output path (%default)', @@ -96,10 +96,11 @@ class HTML (libbe.command.Command): help='Verbose output, default is %default'), ]) - def _run(self, storage, bugdir, **params): + def _run(self, **params): if params['export-template'] == True: html_gen.write_default_template(params['export-template-dir']) return 0 + bugdir = self._get_bugdir() bugdir.load_all_bugs() html_gen = HTMLGen(bugdir, template=params['template-dir'], diff --git a/libbe/command/import_xml.py b/libbe/command/import_xml.py index f06c741..e73d90f 100644 --- a/libbe/command/import_xml.py +++ b/libbe/command/import_xml.py @@ -45,12 +45,13 @@ class Import_XML (libbe.command.Command): >>> import libbe.bugdir >>> bd = libbe.bugdir.SimpleBugDir(memory=False) >>> cmd = Import_XML() + >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout >>> cmd.stdin = StringIO.StringIO('cThis is a comment about a') >>> cmd.stdin.encoding = 'ascii' - >>> ret = cmd.run(bd.storage, bd, {'comment-root':'/a'}, ['-']) + >>> ret = cmd.run({'comment-root':'/a'}, ['-']) >>> bd.flush_reload() >>> bug = bd.bug_from_uuid('a') >>> bug.load_comments(load_full=False) @@ -68,7 +69,6 @@ class Import_XML (libbe.command.Command): def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_bugdir = True self.options.extend([ libbe.command.Option(name='ignore-missing-references', short_name='i', help="If any comment's refers to a non-existent comment, ignore it (instead of raising an exception)."), @@ -85,7 +85,8 @@ class Import_XML (libbe.command.Command): name='xml-file', metavar='XML-FILE'), ]) - def _run(self, storage, bugdir, **params): + def _run(self, **params): + bugdir = self._get_bugdir() writeable = bugdir.storage.writeable bugdir.storage.writeable = False if params['comment-root'] != None: @@ -113,7 +114,7 @@ class Import_XML (libbe.command.Command): if params['xml-file'] == '-': xml = self.stdin.read().encode(self.stdin.encoding) else: - self.check_restricted_access(storage, params['xml-file']) + self._check_restricted_access(storage, params['xml-file']) xml = libbe.util.encoding.get_file_contents( params['xml-file']) @@ -321,6 +322,7 @@ if libbe.TESTING == True: def setUp(self): self.bugdir = libbe.bugdir.SimpleBugDir(memory=False) self.cmd = Import_XML() + self.cmd._storage = self.bugdir.storage self.cmd._setup_io = lambda i_enc,o_enc : None bugA = self.bugdir.bug_from_uuid('a') self.bugdir.remove_bug(bugA) @@ -364,7 +366,7 @@ if libbe.TESTING == True: def tearDown(self): self.bugdir.cleanup() def _execute(self, params={}, args=[]): - self.cmd.run(self.bugdir.storage, self.bugdir, params, args) + self.cmd.run(params, args) self.bugdir.flush_reload() def testCleanBugdir(self): uuids = list(self.bugdir.uuids()) diff --git a/libbe/command/init.py b/libbe/command/init.py index 017cdc3..b52d8e8 100644 --- a/libbe/command/init.py +++ b/libbe/command/init.py @@ -42,7 +42,8 @@ class Init (libbe.command.Command): ... except libbe.storage.ConnectionError: ... True True - >>> cmd.run(vcs) + >>> cmd._unconnected_storage = vcs + >>> cmd.run() No revision control detected. BE repository initialized. >>> bd = libbe.bugdir.BugDir(vcs) @@ -54,8 +55,9 @@ class Init (libbe.command.Command): >>> vcs = libbe.storage.vcs.installed_vcs() >>> vcs.repo = dir.path >>> vcs._vcs_init(vcs.repo) + >>> cmd._unconnected_storage = vcs >>> if vcs.name in libbe.storage.vcs.base.VCS_ORDER: - ... cmd.run(vcs) # doctest: +ELLIPSIS + ... cmd.run() # doctest: +ELLIPSIS ... else: ... vcs.init() ... vcs.connect() @@ -70,9 +72,9 @@ class Init (libbe.command.Command): def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_unconnected_storage = True - def _run(self, storage, bugdir=None, **params): + def _run(self, **params): + storage = self._get_unconnected_storage() if not os.path.isdir(storage.repo): raise libbe.command.UserError( 'No such directory: %s' % storage.repo) diff --git a/libbe/command/list.py b/libbe/command/list.py index 7c3f5af..8bdeaae 100644 --- a/libbe/command/list.py +++ b/libbe/command/list.py @@ -60,11 +60,13 @@ class List (libbe.command.Command): >>> import libbe.bugdir >>> bd = libbe.bugdir.SimpleBugDir(memory=False) >>> cmd = List() + >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout - >>> ret = cmd.run(bd.storage, bd) + + >>> ret = cmd.run() abc/a:om: Bug A - >>> ret = cmd.run(bd.storage, bd, {'status':'closed'}) + >>> ret = cmd.run({'status':'closed'}) abc/b:cm: Bug B >>> bd.storage.writeable True @@ -75,7 +77,6 @@ class List (libbe.command.Command): def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_bugdir = True self.options.extend([ libbe.command.Option(name='status', help='Only show bugs matching the STATUS specifier', @@ -131,7 +132,8 @@ class List (libbe.command.Command): # # ]) - def _run(self, storage, bugdir, **params): + def _run(self, **params): + bugdir = self._get_bugdir() writeable = bugdir.storage.writeable bugdir.storage.writeable = False cmp_list, status, severity, assigned, extra_strings_regexps = \ diff --git a/libbe/command/merge.py b/libbe/command/merge.py index 4624ab7..e3bf943 100644 --- a/libbe/command/merge.py +++ b/libbe/command/merge.py @@ -31,6 +31,7 @@ class Merge (libbe.command.Command): >>> import libbe.comment >>> bd = libbe.bugdir.SimpleBugDir(memory=False) >>> cmd = Merge() + >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout @@ -48,7 +49,7 @@ class Merge (libbe.command.Command): >>> dummy = dummy.new_reply('1 2 3 4') >>> dummy.time = 2 - >>> ret = cmd.run(bd.storage, bd, {}, ['/a', '/b']) + >>> ret = cmd.run(args=['/a', '/b']) Merged bugs #abc/a# and #abc/b# >>> bd.flush_reload() >>> a = bd.bug_from_uuid('a') @@ -139,7 +140,6 @@ class Merge (libbe.command.Command): def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_bugdir = True self.args.extend([ libbe.command.Argument( name='bug-id', metavar='BUG-ID', default=None, @@ -149,7 +149,8 @@ class Merge (libbe.command.Command): completion_callback=libbe.command.util.complete_bug_id), ]) - def _run(self, storage, bugdir, **params): + def _run(self, **params): + bugdir = self._get_bugdir() bugA,dummy_comment = \ libbe.command.util.bug_comment_from_user_id( bugdir, params['bug-id']) @@ -166,7 +167,7 @@ class Merge (libbe.command.Command): if comment.alt_id == None: comment.storage = None comment.alt_id = comment.uuid - comment.storage = storage + comment.storage = bugdir.storage comment.uuid = libbe.util.id.uuid_gen() comment.save() # force onto disk under bugA diff --git a/libbe/command/new.py b/libbe/command/new.py index d840503..de215fa 100644 --- a/libbe/command/new.py +++ b/libbe/command/new.py @@ -31,11 +31,12 @@ class New (libbe.command.Command): >>> import libbe.util.id >>> bd = libbe.bugdir.SimpleBugDir(memory=False) >>> cmd = New() + >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout >>> libbe.util.id.uuid_gen = lambda: 'X' - >>> ret = cmd.run(bd.storage, bd, args=['this is a test',]) + >>> ret = cmd.run(args=['this is a test',]) Created bug with ID abc/X >>> bd.flush_reload() >>> bug = bd.bug_from_uuid('X') @@ -53,7 +54,6 @@ class New (libbe.command.Command): def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_bugdir = True self.options.extend([ libbe.command.Option(name='reporter', short_name='r', help='The user who reported the bug', @@ -69,11 +69,12 @@ class New (libbe.command.Command): libbe.command.Argument(name='summary', metavar='SUMMARY') ]) - def _run(self, storage, bugdir, **params): + def _run(self, **params): if params['summary'] == '-': # read summary from stdin summary = self.stdin.readline() else: summary = params['summary'] + bugdir = self._get_bugdir() bug = bugdir.new_bug(summary=summary.strip()) if params['reporter'] != None: bug.reporter = params['reporter'] diff --git a/libbe/command/remove.py b/libbe/command/remove.py index e516443..3b9d3f3 100644 --- a/libbe/command/remove.py +++ b/libbe/command/remove.py @@ -27,12 +27,13 @@ class Remove (libbe.command.Command): >>> import libbe.bugdir >>> bd = libbe.bugdir.SimpleBugDir(memory=False) >>> cmd = Remove() + >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout >>> print bd.bug_from_uuid('b').status closed - >>> ret = cmd.run(bd.storage, bd, args=['/b']) + >>> ret = cmd.run(args=['/b']) Removed bug abc/b >>> bd.flush_reload() >>> try: @@ -46,7 +47,6 @@ class Remove (libbe.command.Command): def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) - self.requires_bugdir = True self.args.extend([ libbe.command.Argument( name='bug-id', metavar='BUG-ID', default=None, @@ -54,7 +54,8 @@ class Remove (libbe.command.Command): completion_callback=libbe.command.util.complete_bug_id), ]) - def _run(self, storage, bugdir, **params): + def _run(self, **params): + bugdir = self._get_bugdir() user_ids = [] for bug_id in params['bug-id']: bug,dummy_comment = libbe.command.util.bug_comment_from_user_id( diff --git a/libbe/command/severity.py b/libbe/command/severity.py index 804dc4e..f6b586f 100644 --- a/libbe/command/severity.py +++ b/libbe/command/severity.py @@ -17,51 +17,65 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Show or change a bug's severity level""" -from libbe import cmdutil, bugdir, bug -__desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute(["a"], manipulate_encodings=False) - minor - >>> execute(["a", "wishlist"], manipulate_encodings=False) - >>> execute(["a"], manipulate_encodings=False) - wishlist - >>> execute(["a", "none"], manipulate_encodings=False) +import libbe +import libbe.bug +import libbe.command +import libbe.command.util + +class Severity (libbe.command.Command): + """Change a bug's severity level + + >>> import sys + >>> import libbe.bugdir + >>> bd = libbe.bugdir.SimpleBugDir(memory=False) + >>> cmd = Severity() + >>> cmd._storage = bd.storage + >>> cmd._setup_io = lambda i_enc,o_enc : None + >>> cmd.stdout = sys.stdout + + >>> bd.bug_from_uuid('a').severity + 'minor' + >>> ret = cmd.run(args=['wishlist', '/a']) + >>> bd.flush_reload() + >>> bd.bug_from_uuid('a').severity + 'wishlist' + >>> ret = cmd.run(args=['none', '/a']) Traceback (most recent call last): UserError: Invalid severity level: none >>> bd.cleanup() """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - if len(args) not in (1,2): - raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bug = cmdutil.bug_from_id(bd, args[0]) - if len(args) == 1: - print bug.severity - elif len(args) == 2: - try: - bug.severity = args[1] - except ValueError, e: - if e.name != "severity": - raise e - raise cmdutil.UserError ("Invalid severity level: %s" % e.value) + name = 'severity' -def get_parser(): - parser = cmdutil.CmdOptionParser("be severity BUG-ID [SEVERITY]") - return parser + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.args.extend([ + libbe.command.Argument( + name='severity', metavar='SEVERITY', default=None, + completion_callback=libbe.command.util.complete_severity), + libbe.command.Argument( + name='bug-id', metavar='BUG-ID', default=None, + repeatable=True, + completion_callback=libbe.command.util.complete_bug_id), + ]) + + def _run(self, **params): + bugdir = self._get_bugdir() + for bug_id in params['bug-id']: + bug,dummy_comment = \ + libbe.command.util.bug_comment_from_user_id(bugdir, bug_id) + if bug.severity != params['severity']: + try: + bug.severity = params['severity'] + except ValueError, e: + if e.name != 'severity': + raise e + raise libbe.command.UserError( + 'Invalid severity level: %s' % e.value) + return 0 -def help(): - longhelp=[""" + 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 @@ -69,38 +83,13 @@ is specified, it will be assigned to the bug. Severity levels are: """] - try: # See if there are any per-tree severity configurations - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False) - except bugdir.NoBugDir, e: - pass # No tree, just show the defaults - longest_severity_len = max([len(s) for s in bug.severity_values]) - for severity in bug.severity_values : - description = bug.severity_description[severity] - s = "%*s : %s\n" % (longest_severity_len, severity, description) - longhelp.append(s) - longhelp = ''.join(longhelp) - return get_parser().help_str() + longhelp - -def complete(options, args, parser): - for option,value in cmdutil.option_value_pairs(options, parser): - if value == "--complete": - # no argument-options at the moment, so this is future-proofing - raise cmdutil.GetCompletions() - for pos,value in enumerate(args): - if value == "--complete": - try: # See if there are any per-tree severity configurations - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=False) - except bugdir.NoBugDir: - bd = None - if pos == 0: # fist positional argument is a bug id - ids = [] - if bd != None: - bd.load_all_bugs() - filter = lambda bg : bg.active==True - bugs = [bg for bg in bd if filter(bg)==True] - ids = [bd.bug_shortname(bg) for bg in bugs] - raise cmdutil.GetCompletions(ids) - elif pos == 1: # second positional argument is a severity - raise cmdutil.GetCompletions(bug.severity_values) - raise cmdutil.GetCompletions() + 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]) + for severity in bug.severity_values : + description = bug.severity_description[severity] + ret.append('%*s : %s\n' \ + % (longest_severity_len, severity, description)) + return ''.join(ret) diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py index 84f9450..0aa34f7 100755 --- a/libbe/ui/command_line.py +++ b/libbe/ui/command_line.py @@ -264,29 +264,18 @@ def main(): print e return 1 Class = getattr(module, command_name.capitalize()) - command = Class() - command.ui = self + def gucs(): + return libbe.storage.get_storage(options['repo']) + command = Class(get_unconnected_storage=gucs, ui=ui) parser = CmdOptionParser(command) - storage = None - bugdir = None - if command.requires_bugdir == True: - assert command.requires_unconnected_storage == False - storage = libbe.storage.get_storage(options['repo']) - storage.connect() - bugdir = libbe.bugdir.BugDir(storage, from_storage=True) - elif command.requires_storage == True \ - or command.requires_unconnected_storage == True: - storage = libbe.storage.get_storage(options['repo']) - if command.requires_unconnected_storage == False: - storage.connect() try: options,args = parser.parse_args(args[1:]) - command.run(storage, bugdir, options, args) + command.run(options, args) except CallbackExit: - if storage != None: storage.disconnect() + command.cleanup() return 0 except libbe.command.UserError, e: - if storage != None: storage.disconnect() + command.cleanup() print 'ERROR:\n', e return 1 if storage != None: storage.disconnect() diff --git a/libbe/ui/util/cmdutil.py b/libbe/ui/util/cmdutil.py index 86ff9fc..f2eb5b9 100644 --- a/libbe/ui/util/cmdutil.py +++ b/libbe/ui/util/cmdutil.py @@ -59,20 +59,6 @@ def execute(cmd, args, def restrict_file_access(bugdir, path): - """ - Check that the file at path is inside bugdir.root. This is - important if you allow other users to execute becommands with your - username (e.g. if you're running be-handle-mail through your - ~/.procmailrc). If this check wasn't made, a user could e.g. - run - be commit -b ~/.ssh/id_rsa "Hack to expose ssh key" - which would expose your ssh key to anyone who could read the VCS - log. - """ - in_root = bugdir.vcs.path_in_root(path, bugdir.root) - if in_root == False: - raise UserError('file access restricted!\n %s not in %s' - % (path, bugdir.root)) def parse_id(id): """ -- cgit From 6e474b0dc04efa60aaeb82c09d4fd4f4b10678da Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 14 Dec 2009 23:31:57 -0500 Subject: Transitioned status to Command-format --- libbe/command/severity.py | 5 +- libbe/command/status.py | 145 +++++++++++++++++++++------------------------- libbe/ui/command_line.py | 14 +++-- 3 files changed, 78 insertions(+), 86 deletions(-) diff --git a/libbe/command/severity.py b/libbe/command/severity.py index f6b586f..9289138 100644 --- a/libbe/command/severity.py +++ b/libbe/command/severity.py @@ -23,6 +23,7 @@ import libbe.bug import libbe.command import libbe.command.util + class Severity (libbe.command.Command): """Change a bug's severity level @@ -88,8 +89,8 @@ Severity levels are: except NotImplementedError: pass # No tree, just show the defaults longest_severity_len = max([len(s) for s in libbe.bug.severity_values]) - for severity in bug.severity_values : - description = bug.severity_description[severity] + 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) diff --git a/libbe/command/status.py b/libbe/command/status.py index 58b6f63..38aa541 100644 --- a/libbe/command/status.py +++ b/libbe/command/status.py @@ -14,68 +14,77 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Show or change a bug's status""" -from libbe import cmdutil, bugdir, bug -__desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute(["a"], manipulate_encodings=False) - open - >>> execute(["a", "closed"], manipulate_encodings=False) - >>> execute(["a"], manipulate_encodings=False) - closed - >>> execute(["a", "none"], manipulate_encodings=False) +import libbe +import libbe.bug +import libbe.command +import libbe.command.util + + +class Status (libbe.command.Command): + """Change a bug's status level + + >>> import sys + >>> import libbe.bugdir + >>> bd = libbe.bugdir.SimpleBugDir(memory=False) + >>> cmd = Status() + >>> cmd._storage = bd.storage + >>> cmd._setup_io = lambda i_enc,o_enc : None + >>> cmd.stdout = sys.stdout + + >>> bd.bug_from_uuid('a').status + 'open' + >>> ret = cmd.run(args=['closed', '/a']) + >>> bd.flush_reload() + >>> bd.bug_from_uuid('a').status + 'closed' + >>> ret = cmd.run(args=['none', '/a']) Traceback (most recent call last): - UserError: Invalid status: none + UserError: Invalid status level: none >>> bd.cleanup() """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - if len(args) not in (1,2): - raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - bug = cmdutil.bug_from_id(bd, args[0]) - if len(args) == 1: - print bug.status - else: - try: - bug.status = args[1] - except ValueError, e: - if e.name != "status": - raise - raise cmdutil.UserError ("Invalid status: %s" % e.value) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be status BUG-ID [STATUS]") - return parser + name = 'status' + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.args.extend([ + libbe.command.Argument( + name='status', metavar='STATUS', default=None, + completion_callback=libbe.command.util.complete_status), + libbe.command.Argument( + name='bug-id', metavar='BUG-ID', default=None, + repeatable=True, + completion_callback=libbe.command.util.complete_bug_id), + ]) + + def _run(self, **params): + bugdir = self._get_bugdir() + for bug_id in params['bug-id']: + bug,dummy_comment = \ + libbe.command.util.bug_comment_from_user_id(bugdir, bug_id) + if bug.status != params['status']: + try: + bug.status = params['status'] + except ValueError, e: + if e.name != 'status': + raise e + raise libbe.command.UserError( + 'Invalid status level: %s' % e.value) + return 0 -def help(): - try: # See if there are any per-tree status configurations - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=False) - except bugdir.NoBugDir, e: - pass # No tree, just show the defaults - longest_status_len = max([len(s) for s in bug.status_values]) - active_statuses = [] - for status in bug.active_status_values : - description = bug.status_description[status] - s = "%*s : %s" % (longest_status_len, status, description) - active_statuses.append(s) - inactive_statuses = [] - for status in bug.inactive_status_values : - description = bug.status_description[status] - s = "%*s : %s" % (longest_status_len, status, description) - inactive_statuses.append(s) - longhelp=""" + def _long_help(self): + longest_status_len = max([len(s) for s in libbe.bug.status_values]) + active_statuses = [] + for status in libbe.bug.active_status_values : + description = libbe.bug.status_description[status] + s = "%*s : %s" % (longest_status_len, status, description) + active_statuses.append(s) + inactive_statuses = [] + for status in libbe.bug.inactive_status_values : + description = libbe.bug.status_description[status] + s = "%*s : %s" % (longest_status_len, status, description) + inactive_statuses.append(s) + ret = """ Show or change a bug's status. If no status is specified, the current value is printed. If a status @@ -93,26 +102,4 @@ Inactive status levels are: You can overide the list of allowed statuses on a per-repository basis. See "be set --help" for more details. """ % ('\n '.join(active_statuses), '\n '.join(inactive_statuses)) - return get_parser().help_str() + longhelp - -def complete(options, args, parser): - for option,value in cmdutil.option_value_pairs(options, parser): - if value == "--complete": - # no argument-options at the moment, so this is future-proofing - raise cmdutil.GetCompletions() - for pos,value in enumerate(args): - if value == "--complete": - try: # See if there are any per-tree status configurations - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=False) - except bugdir.NoBugDir: - bd = None - if pos == 0: # fist positional argument is a bug id - ids = [] - if bd != None: - bd.load_all_bugs() - ids = [bd.bug_shortname(bg) for bg in bd] - raise cmdutil.GetCompletions(ids) - elif pos == 1: # second positional argument is a status - raise cmdutil.GetCompletions(bug.status_values) - raise cmdutil.GetCompletions() + return ret diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py index 0aa34f7..bb38be8 100755 --- a/libbe/ui/command_line.py +++ b/libbe/ui/command_line.py @@ -241,7 +241,8 @@ class BE (libbe.command.Command): return libbe.version.version(verbose=True) def main(): - parser = CmdOptionParser(BE()) + be = BE() + parser = CmdOptionParser(be) try: options,args = parser.parse_args() except CallbackExit: @@ -264,9 +265,12 @@ def main(): print e return 1 Class = getattr(module, command_name.capitalize()) - def gucs(): - return libbe.storage.get_storage(options['repo']) - command = Class(get_unconnected_storage=gucs, ui=ui) + class GUCS (object): + def __init__(self, repo): + self.repo = repo + def __call__(self): + return libbe.storage.get_storage(self.repo) + command = Class(get_unconnected_storage=GUCS(options['repo']), ui=be) parser = CmdOptionParser(command) try: options,args = parser.parse_args(args[1:]) @@ -278,7 +282,7 @@ def main(): command.cleanup() print 'ERROR:\n', e return 1 - if storage != None: storage.disconnect() + command.cleanup() return 0 if __name__ == '__main__': -- cgit From 21c3bf5ce2fcb9fdd4493b2385c6623979746829 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 15 Dec 2009 00:04:55 -0500 Subject: Transitioned show to Command-format --- libbe/command/show.py | 164 +++++++++++++++++++++++++++--------------------- libbe/command/status.py | 4 +- 2 files changed, 95 insertions(+), 73 deletions(-) diff --git a/libbe/command/show.py b/libbe/command/show.py index 7757aaa..1a569a6 100644 --- a/libbe/command/show.py +++ b/libbe/command/show.py @@ -17,20 +17,32 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Show a particular bug, comment, or combination of both.""" + import sys -from libbe import cmdutil, bugdir, comment, version, _version -__desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute (["a",], manipulate_encodings=False) # doctest: +ELLIPSIS +import libbe +import libbe.command +import libbe.command.util +import libbe.util.id +import libbe.version +import libbe._version + + +class Show (libbe.command.Command): + """Show a particular bug, comment, or combination of both. + + >>> import sys + >>> import libbe.bugdir + >>> bd = libbe.bugdir.SimpleBugDir(memory=False) + >>> cmd = Show() + >>> cmd._storage = bd.storage + >>> cmd._setup_io = lambda i_enc,o_enc : None + >>> cmd.stdout = sys.stdout + >>> cmd.stdout.encoding = 'ascii' + + >>> ret = cmd.run(args=['/a',]) # doctest: +ELLIPSIS ID : a - Short name : a + Short name : abc/a Severity : minor Status : open Assigned : @@ -39,7 +51,8 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False, Created : ... Bug A - >>> execute (["--xml", "a"], manipulate_encodings=False) # doctest: +ELLIPSIS + + >>> ret = cmd.run({'xml':True}, ['/a']) # doctest: +ELLIPSIS @@ -50,7 +63,7 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False, a - a + abc/a minor open John Doe <jdoe@example.com> @@ -60,42 +73,51 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False, >>> bd.cleanup() """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={-1: lambda bug : bug.active==True}) - if len(args) == 0: - raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - - if options.only_raw_body == True: - if len(args) != 1: - raise cmdutil.UsageError( - 'only one ID accepted with --only-raw-body') - bug,comment = cmdutil.bug_comment_from_id(bd, args[0]) - if comment == bug.comment_root: - raise cmdutil.UsageError( - "--only-raw-body requires a comment ID, not '%s'" % args[0]) - sys.__stdout__.write(comment.body) - sys.exit(0) - print output(args, bd, as_xml=options.XML, with_comments=options.comments) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be show [options] ID [ID ...]") - parser.add_option("-x", "--xml", action="store_true", default=False, - dest='XML', help="Dump as XML") - parser.add_option("--only-raw-body", action="store_true", - dest='only_raw_body', - help="When printing only a single comment, just print it's body. This allows extraction of non-text content types.") - parser.add_option("-c", "--no-comments", dest="comments", - action="store_false", default=True, - help="Disable comment output. This is useful if you just want more details on a bug's current status.") - return parser - -longhelp=""" + name = 'show' + + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.options.extend([ + libbe.command.Option(name='xml', short_name='x', + help='Dump as XML'), + libbe.command.Option(name='only-raw-body', + help="When printing only a single comment, just print it's" + " body. This allows extraction of non-text content types."), + libbe.command.Option(name='no-comments', short_name='c', + help="Disable comment output. This is useful if you just " + "want more details on a bug's current status."), + ]) + self.args.extend([ + libbe.command.Argument( + name='id', metavar='ID', default=None, + optional=True, repeatable=True, + completion_callback=libbe.command.util.complete_bug_comment_id), + ]) + + def _run(self, **params): + bugdir = self._get_bugdir() + if params['only-raw-body'] == True: + if len(params['id']) != 1: + raise libbe.command.UsageError( + 'only one ID accepted with --only-raw-body') + bug,comment = libbe.command.util.bug_comment_from_user_id( + bugdir, params['id'][0]) + if comment == bug.comment_root: + raise libbe.command.UsageError( + "--only-raw-body requires a comment ID, not '%s'" + % params['id'][0]) + sys.__stdout__.write(comment.body) + return 0 + print >> self.stdout, \ + output(bugdir, params['id'], encoding=self.stdout.encoding, + as_xml=params['xml'], + with_comments=not params['no-comments']) + return 0 + + def _long_help(self): + return """ Show all information about the bugs or comments whose IDs are given. +If no IDs are given, show the entire repository. Without the --xml flag set, it's probably not a good idea to mix bug and comment IDs in a single call, but you're free to do so if you @@ -109,24 +131,21 @@ placed at the end of the output, so the ordering may not match the order of the listed IDs. """ -def help(): - return get_parser().help_str() + longhelp - -def _sort_ids(ids, with_comments=True): +def _sort_ids(bugdir, ids, with_comments=True): bugs = [] root_comments = {} for id in ids: - bugname,commname = cmdutil.parse_id(id) - if commname == None: - bugs.append(bugname) + p = libbe.util.id.parse_user(bugdir, id) + if p['type'] == 'bug': + bugs.append(p['bug']) elif with_comments == True: - if bugname not in root_comments: - root_comments[bugname] = [commname] + if p['bug'] not in root_comments: + root_comments[p['bug']] = [p['comment']] else: - root_comments[bugname].append(commname) + root_comments[p['bug']].append(p['comment']) for bugname in root_comments.keys(): assert bugname not in bugs, \ - "specifically requested both '%s%s' and '%s'" \ + 'specifically requested both #/%s/%s# and #/%s#' \ % (bugname, root_comments[bugname][0], bugname) return (bugs, root_comments) @@ -134,9 +153,9 @@ def _xml_header(encoding): lines = ['' % encoding, '', ' ', - ' %s' % version.version()] + ' %s' % libbe.version.version()] for tag in ['branch-nick', 'revno', 'revision-id']: - value = _version.version_info[tag.replace('-', '_')] + value = libbe._version.version_info[tag.replace('-', '_')] lines.append(' <%s>%s' % (tag, value, tag)) lines.append(' ') return lines @@ -144,15 +163,18 @@ def _xml_header(encoding): def _xml_footer(): return [''] -def output(ids, bd, as_xml=True, with_comments=True): - bugs,root_comments = _sort_ids(ids, with_comments) +def output(bd, ids, encoding, as_xml=True, with_comments=True): + if len(ids) == 0: + bd.load_all_bugs() + ids = [bug.id.user() for bug in bd] + bugs,root_comments = _sort_ids(bd, ids, with_comments) lines = [] if as_xml: - lines.extend(_xml_header(bd.encoding)) + lines.extend(_xml_header(encoding)) else: spaces_left = len(ids) - 1 for bugname in bugs: - bug = cmdutil.bug_from_id(bd, bugname) + bug = bd.bug_from_uuid(bugname) if as_xml: lines.append(bug.xml(indent=2, show_comments=with_comments)) else: @@ -161,18 +183,18 @@ def output(ids, bd, as_xml=True, with_comments=True): spaces_left -= 1 lines.append('') # add a blank line between bugs/comments for bugname,comments in root_comments.items(): - bug = cmdutil.bug_from_id(bd, bugname) + bug = bd.bug_from_uuid(bugname) if as_xml: lines.extend([' ', ' %s' % bug.uuid]) for commname in comments: try: - comment = bug.comment_root.comment_from_shortname(commname) - except comment.InvalidShortname, e: - raise UserError(e.message) + comment = bug.comment_root.comment_from_uuid(commname) + except KeyError, e: + raise libbe.command.UserError(e.message) if as_xml: - lines.append(comment.xml(indent=4, shortname=bugname)) + lines.append(comment.xml(indent=4)) else: - lines.append(comment.string(shortname=bugname)) + lines.append(comment.string()) if spaces_left > 0: spaces_left -= 1 lines.append('') # add a blank line between bugs/comments diff --git a/libbe/command/status.py b/libbe/command/status.py index 38aa541..7cf5858 100644 --- a/libbe/command/status.py +++ b/libbe/command/status.py @@ -77,12 +77,12 @@ class Status (libbe.command.Command): active_statuses = [] for status in libbe.bug.active_status_values : description = libbe.bug.status_description[status] - s = "%*s : %s" % (longest_status_len, status, description) + s = '%*s : %s' % (longest_status_len, status, description) active_statuses.append(s) inactive_statuses = [] for status in libbe.bug.inactive_status_values : description = libbe.bug.status_description[status] - s = "%*s : %s" % (longest_status_len, status, description) + s = '%*s : %s' % (longest_status_len, status, description) inactive_statuses.append(s) ret = """ Show or change a bug's status. -- cgit From 66343859b94ab7417bd4560ccc1c023d9ba6d1d2 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 15 Dec 2009 01:07:18 -0500 Subject: Transitioned diff and subscribe to Command-format" They don't work yet, since I still need to fix up libbe.diff and replace BugDir.duplicate_bugdir() with something based on the new Storage backend. --- libbe/command/diff.py | 183 +++++++++++++++------------- libbe/command/subscribe.py | 292 ++++++++++++++++++++++++--------------------- libbe/diff.py | 17 +-- 3 files changed, 263 insertions(+), 229 deletions(-) diff --git a/libbe/command/diff.py b/libbe/command/diff.py index c5c34f9..de8cf67 100644 --- a/libbe/command/diff.py +++ b/libbe/command/diff.py @@ -16,107 +16,121 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Compare bug reports with older tree""" -from libbe import cmdutil, bugdir, diff -import os -__desc__ = __doc__ +import libbe +import libbe.bug +import libbe.command +import libbe.command.util +import libbe.storage -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> bd.set_sync_with_disk(True) - >>> original = bd.vcs.commit("Original status") - >>> bug = bd.bug_from_uuid("a") - >>> bug.status = "closed" - >>> changed = bd.vcs.commit("Closed bug a") - >>> os.chdir(bd.root) +import libbe.diff + +class Diff (libbe.command.Command): + """Compare bug reports with older tree + + >>> import sys + >>> import libbe.bugdir + >>> bd = libbe.bugdir.SimpleBugDir(memory=False) + >>> cmd = Subscribe() + >>> cmd._storage = bd.storage + >>> cmd._setup_io = lambda i_enc,o_enc : None + >>> cmd.stdout = sys.stdout + + >>> original = bd.storage.commit('Original status') + >>> bug = bd.bug_from_uuid('a') + >>> bug.status = 'closed' + >>> changed = bd.vcs.commit('Closed bug a') >>> if bd.vcs.versioned == True: - ... execute([original], manipulate_encodings=False) + ... ret = cmd.run(args=[original]) ... else: - ... print "Modified bugs:\\n a:cm: Bug A\\n Changed bug settings:\\n status: open -> closed" + ... print 'Modified bugs:\\n a:cm: Bug A\\n Changed bug settings:\\n status: open -> closed' Modified bugs: a:cm: Bug A Changed bug settings: status: open -> closed >>> if bd.vcs.versioned == True: - ... execute(["--subscribe", "%(bugdir_id)s:mod", "--uuids", original], - ... manipulate_encodings=False) + ... ret = cmd.run({'subscribe':'%(bugdir_id)s:mod', 'uuids':True}, [original]) ... else: - ... print "a" + ... print 'a' a >>> if bd.vcs.versioned == False: - ... execute([original], manipulate_encodings=False) + ... ret = cmd.run(args=[original]) ... else: - ... raise cmdutil.UsageError('This directory is not revision-controlled.') + ... raise libbe.command.UserError('This repository not revision-controlled.') Traceback (most recent call last): ... - UsageError: This directory is not revision-controlled. + UserError: This repository is not revision-controlled. >>> bd.cleanup() - """ % {'bugdir_id':diff.BUGDIR_ID} - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser) - if len(args) == 0: - revision = None - if len(args) == 1: - revision = args[0] - if len(args) > 1: - raise cmdutil.UsageError('Too many arguments.') - try: - subscriptions = diff.subscriptions_from_string( - options.subscribe) - except ValueError, e: - raise cmdutil.UsageError(e.msg) - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if bd.vcs.versioned == False: - raise cmdutil.UsageError('This directory is not revision-controlled.') - if options.dir == None: - if revision == None: # get the most recent revision - revision = bd.vcs.revision_id(-1) - old_bd = bd.duplicate_bugdir(revision) - else: - old_bd_current = bugdir.BugDir(root=os.path.abspath(options.dir), - from_disk=True, - manipulate_encodings=False) - if revision == None: # use the current working state - old_bd = old_bd_current - else: - if old_bd_current.vcs.versioned == False: - raise cmdutil.UsageError('%s is not revision-controlled.' - % options.dir) - old_bd = old_bd_current.duplicate_bugdir(revision) - d = diff.Diff(old_bd, bd) - tree = d.report_tree(subscriptions) + """ % {'bugdir_id':libbe.diff.BUGDIR_ID} + name = 'diff' - if options.uuids == True: - uuids = [] - bugs = tree.child_by_path('/bugs') - for bug_type in bugs: - uuids.extend([bug.name for bug in bug_type]) - print '\n'.join(uuids) - else : - rep = tree.report_string() - if rep != None: - print rep - bd.remove_duplicate_bugdir() - if options.dir != None and revision != None: - old_bd_current.remove_duplicate_bugdir() + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.options.extend([ + libbe.command.Option(name='repo', short_name='r', + help='Compare with repository in REPO instead' + ' of the current repository.', + arg=libbe.command.Argument( + name='repo', metavar='REPO', + completion_callback=libbe.command.util.complete_path)), + libbe.command.Option(name='subscribe', short_name='s', + help='Only print changes matching SUBSCRIPTION, ' + 'subscription is a comma-separ\ated list of ID:TYPE ' + 'tuples. See `be subscribe --help` for descriptions ' + 'of ID and TYPE.', + arg=libbe.command.Argument( + name='subscribe', metavar='SUBSCRIPTION')), + libbe.command.Option(name='uuids', short_name='u', + help='Only print the changed bug UUIDS.'), + ]) + self.args.extend([ + libbe.command.Argument( + name='revision', metavar='REVISION', default=None, + optional=True) + ]) -def get_parser(): - parser = cmdutil.CmdOptionParser("be diff [options] REVISION") - parser.add_option("-d", "--dir", dest="dir", metavar="DIR", - help="Compare with repository in DIR instead of the current directory.") - parser.add_option("-s", "--subscribe", dest="subscribe", metavar="SUBSCRIPTION", - help="Only print changes matching SUBSCRIPTION, subscription is a comma-separ\ated list of ID:TYPE tuples. See `be subscribe --help` for descriptions of ID and TYPE.") - parser.add_option("-u", "--uuids", action="store_true", dest="uuids", - help="Only print the bug UUIDS.", default=False) - return parser + def _run(self, **params): + try: + subscriptions = libbe.diff.subscriptions_from_string( + params['subscribe']) + except ValueError, e: + raise libbe.command.UserError(e.msg) + bugdir = self._get_bugdir() + if bugdir.storage.versioned == False: + raise libbe.command.UserError( + 'This repository is not revision-controlled.') + if params['repo'] == None: + if params['revision'] == None: # get the most recent revision + params['revision'] = bugdir.storage.revision_id(-1) + old_bd = bugdir.duplicate_bugdir(params['revision']) # TODO + else: + old_storage = libbe.storage.get_storage(params['repo']) + old_storage.connect() + old_bd_current = bugdir.BugDir(old_storage, from_disk=True) + if params['revision'] == None: # use the current working state + old_bd = old_bd_current + else: + if old_bd_current.storage.versioned == False: + raise libbe.command.UserError( + '%s is not revision-controlled.' + % storage.repo) + old_bd = old_bd_current.duplicate_bugdir(revision) # TODO + d = libbe.diff.Diff(old_bd, bugir) + tree = d.report_tree(subscriptions) + + if params['uuids'] == True: + uuids = [] + bugs = tree.child_by_path('/bugs') + for bug_type in bugs: + uuids.extend([bug.name for bug in bug_type]) + print >> self.stdout, '\n'.join(uuids) + else : + rep = tree.report_string() + if rep != None: + print >> self.stdout, rep + return 0 -longhelp=""" + def _long_help(self): + return """ Uses the VCS to compare the current tree with a previous tree, and prints a pretty report. If REVISION is given, it is a specifier for the particular previous tree to use. Specifiers are specific to their @@ -128,6 +142,3 @@ Besides the standard summary output, you can use the options to output UUIDS for the different categories. This output can be used as the input to 'be show' to get an understanding of the current status. """ - -def help(): - return get_parser().help_str() + longhelp diff --git a/libbe/command/subscribe.py b/libbe/command/subscribe.py index 69554f7..5c5acdb 100644 --- a/libbe/command/subscribe.py +++ b/libbe/command/subscribe.py @@ -13,146 +13,170 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""(Un)subscribe to change notification""" -from libbe import cmdutil, bugdir, tree, diff -import os, copy -__desc__ = __doc__ + +import copy +import os + +import libbe +import libbe.bug +import libbe.command +import libbe.diff +import libbe.command.util +import libbe.util.tree + TAG="SUBSCRIBE:" -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> bd = bugdir.SimpleBugDir() - >>> bd.set_sync_with_disk(True) - >>> os.chdir(bd.root) - >>> a = bd.bug_from_shortname("a") + +class Subscribe (libbe.command.Command): + """(Un)subscribe to change notification + + >>> import sys + >>> import libbe.bugdir + >>> bd = libbe.bugdir.SimpleBugDir(memory=False) + >>> cmd = Subscribe() + >>> cmd._storage = bd.storage + >>> cmd._setup_io = lambda i_enc,o_enc : None + >>> cmd.stdout = sys.stdout + + >>> a = bd.bug_from_uuid('a') >>> print a.extra_strings [] - >>> execute(["-s","John Doe ", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: + >>> ret = cmd.run({'subscriber':'John Doe '], ['/a']) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for abc/a: John Doe all * - >>> bd._clear_bugs() # resync our copy of bug - >>> a = bd.bug_from_shortname("a") + >>> bd.flush_reload() + >>> a = bd.bug_from_uuid('a') >>> print a.extra_strings ['SUBSCRIBE:John Doe \\tall\\t*'] - >>> execute(["-s","Jane Doe ", "-S", "a.com,b.net", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: + >>> ret = cmd.run({'subscriber':'Jane Doe ', 'servers':'a.com,b.net'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE + Subscriptions for abc/a: Jane Doe all a.com,b.net John Doe all * - >>> execute(["-s","Jane Doe ", "-S", "a.edu", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + >>> ret = cmd.run({'subscriber':'Jane Doe ', 'servers':'a.edu'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE Subscriptions for a: Jane Doe all a.com,a.edu,b.net John Doe all * - >>> execute(["-u", "-s","Jane Doe ", "-S", "a.com", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + >>> ret = cmd.run({'-u', 'subscriber':'Jane Doe ', 'servers':'a.com'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE Subscriptions for a: Jane Doe all a.edu,b.net John Doe all * - >>> execute(["-s","Jane Doe ", "-S", "*", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + >>> ret = cmd.run({'subscriber':'Jane Doe ', 'servers':'*'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE Subscriptions for a: Jane Doe all * John Doe all * - >>> execute(["-u", "-s","Jane Doe ", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + >>> ret = cmd.run({'unsubscribe':True, 'subscriber':'Jane Doe '}, ['/a']) # doctest: +NORMALIZE_WHITESPACE Subscriptions for a: John Doe all * - >>> execute(["-u", "-s","John Doe ", "a"], manipulate_encodings=False) - >>> execute(["-s","Jane Doe ", "-t", "new", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + >>> ret = cmd.run({'unsubscribe':True, 'subscriber':'John Doe '}, ['/a']) + >>> ret = cmd.run({'subscriber':'Jane Doe ', '-t':'new'}, 'DIR']) # doctest: +NORMALIZE_WHITESPACE Subscriptions for bug directory: Jane Doe new * - >>> execute(["-s","Jane Doe ", "DIR"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE + >>> ret = cmd.run({'subscriber':'Jane Doe '}, ['DIR']) # doctest: +NORMALIZE_WHITESPACE Subscriptions for bug directory: Jane Doe all * >>> bd.cleanup() """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) + name = 'subscribe' - if len(args) > 1: - help() - raise cmdutil.UsageError("Too many arguments.") + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.options.extend([ + libbe.command.Option(name='unsubscribe', short_name='u', + help='Unsubscribe instead of subscribing'), + libbe.command.Option(name='list-all', short_name='a', + help='List all subscribers (no ID argument, read only action)'), + libbe.command.Option(name='list', short_name='l', + help='List subscribers (read only action).'), + libbe.command.Option(name='subscriber', short_name='s', + help='Email address of the subscriber (defaults to bugdir.user_id).', + arg=libbe.command.Argument( + name='subscriber', metavar='EMAIL')), + libbe.command.Option(name='servers', short_name='S', + help='Servers from which you want notification.', + arg=libbe.command.Argument( + name='servers', metavar='STRING')), + libbe.command.Option(name='types', short_name='t', + help='Types of changes you wish to be notified about.', + arg=libbe.command.Argument( + name='types', metavar='STRING')), + ]) + self.args.extend([ + libbe.command.Argument( + name='id', metavar='ID', default=None, + optional=True, repeatable=True, + completion_callback=libbe.command.util.complete_bug_comment_id), + ]) - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - - subscriber = options.subscriber - if subscriber == None: - subscriber = bd.user_id - if options.unsubscribe == True: - if options.servers == None: - options.servers = "INVALID" - if options.types == None: - options.types = "INVALID" - else: - if options.servers == None: - options.servers = "*" - if options.types == None: - options.types = "all" - servers = options.servers.split(",") - types = options.types.split(",") - - if len(args) == 0 or args[0] == diff.BUGDIR_ID: # directory-wide subscriptions - type_root = diff.BUGDIR_TYPE_ALL - entity = bd - entity_name = "bug directory" - else: # bug-specific subscriptions - type_root = diff.BUG_TYPE_ALL - bug = bd.bug_from_shortname(args[0]) - entity = bug - entity_name = bug.uuid - if options.list_all == True: - entity_name = "anything in the bug directory" - - types = [diff.type_from_name(name, type_root, default=diff.INVALID_TYPE, - default_ok=options.unsubscribe) - for name in types] - estrs = entity.extra_strings - if options.list == True or options.list_all == True: - pass - else: # alter subscriptions - if options.unsubscribe == True: - estrs = unsubscribe(estrs, subscriber, types, servers, type_root) - else: # add the tag - estrs = subscribe(estrs, subscriber, types, servers, type_root) - entity.extra_strings = estrs # reassign to notice change - - if options.list_all == True: - bd.load_all_bugs() - subscriptions = get_bugdir_subscribers(bd, servers[0]) - else: - subscriptions = [] - for estr in entity.extra_strings: - if estr.startswith(TAG): - subscriptions.append(estr[len(TAG):]) - - if len(subscriptions) > 0: - print "Subscriptions for %s:" % entity_name - print '\n'.join(subscriptions) - - -def get_parser(): - parser = cmdutil.CmdOptionParser("be subscribe ID") - parser.add_option("-u", "--unsubscribe", action="store_true", - dest="unsubscribe", default=False, - help="Unsubscribe instead of subscribing.") - parser.add_option("-a", "--list-all", action="store_true", - dest="list_all", default=False, - help="List all subscribers (no ID argument, read only action).") - parser.add_option("-l", "--list", action="store_true", - dest="list", default=False, - help="List subscribers (read only action).") - parser.add_option("-s", "--subscriber", dest="subscriber", - metavar="SUBSCRIBER", - help="Email address of the subscriber (defaults to bugdir.user_id).") - parser.add_option("-S", "--servers", dest="servers", metavar="SERVERS", - help="Servers from which you want notification.") - parser.add_option("-t", "--type", dest="types", metavar="TYPES", - help="Types of changes you wish to be notified about.") - return parser + def _run(self, **params): + bugdir = self._get_bugdir() + if params['list-all'] == True or params['list'] == True: + writeable = bugdir.storage.writeable + bugdir.storage.writeable = False + if params['list-all'] == True: + assert len(params['id']) == 0, params['id'] + subscriber = params['subscriber'] + if subscriber == None: + subscriber = self._get_user_id() + if params['unsubscribe'] == True: + if params['servers'] == None: + params['servers'] = 'INVALID' + if params['types'] == None: + params['types'] = 'INVALID' + else: + if params['servers'] == None: + params['servers'] = '*' + if params['types'] == None: + params['types'] = 'all' + servers = params['servers'].split(',') + types = params['types'].split(',') + + if params['id'] == None: + params['id'] = libbe.diff.BUGDIR_ID + for id in params['id']: + if id == libbe.diff.BUGDIR_ID: # directory-wide subscriptions + type_root = libbe.diff.BUGDIR_TYPE_ALL + entity = bugdir + entity_name = 'bug directory' + else: # bug-specific subscriptions + type_root = libbe.diff.BUG_TYPE_ALL + bug,dummy_comment = libbe.command.util.bug_comment_from_user_id( + bugdir, params['id']) + entity = bug + entity_name = bug.id.user() + if params['list-all'] == True: + entity_name = 'anything in the bug directory' + types = [libbe.diff.type_from_name(name, type_root, default=libbe.diff.INVALID_TYPE, + default_ok=params['unsubscribe']) + for name in types] + estrs = entity.extra_strings + if params['list'] == True or params['list-all'] == True: + pass + else: # alter subscriptions + if params['unsubscribe'] == True: + estrs = unsubscribe(estrs, subscriber, types, servers, type_root) + else: # add the tag + estrs = subscribe(estrs, subscriber, types, servers, type_root) + entity.extra_strings = estrs # reassign to notice change + + if params['list-all'] == True: + bugdir.load_all_bugs() + subscriptions = get_bugdir_subscribers(bugdir, servers[0]) + else: + subscriptions = [] + for estr in entity.extra_strings: + if estr.startswith(TAG): + subscriptions.append(estr[len(TAG):]) + + if len(subscriptions) > 0: + print >> self.stdout, 'Subscriptions for %s:' % entity_name + print >> self.stdout, '\n'.join(subscriptions) + if params['list-all'] == True or params['list'] == True: + bugdir.storage.writeable = writeable + return 0 -longhelp=""" + def _long_help(self): + return """ ID can be either a bug id, or blank/"DIR", in which case it refers to the whole bug directory. @@ -177,12 +201,10 @@ if you're just hacking away on your private repository, you'll known what's changed ;). This command just (un)sets the appropriate subscriptions, and leaves it up to each interface to perform the notification. -""" % (diff.BUG_TYPE_ALL.string_tree(6), diff.BUGDIR_ID, - diff.BUGDIR_TYPE_ALL.string_tree(6), - diff.BUGDIR_TYPE_ALL) +""" % (libbe.diff.BUG_TYPE_ALL.string_tree(6), libbe.diff.BUGDIR_ID, + libbe.diff.BUGDIR_TYPE_ALL.string_tree(6), + libbe.diff.BUGDIR_TYPE_ALL) -def help(): - return get_parser().help_str() + longhelp # internal helper functions @@ -195,7 +217,7 @@ def _parse_string(string, type_root): assert string.startswith(TAG), string string = string[len(TAG):] subscriber,types,servers = string.split("\t") - types = [diff.type_from_name(name, type_root) for name in types.split(",")] + types = [libbe.diff.type_from_name(name, type_root) for name in types.split(",")] return (subscriber,types,servers.split(",")) def _get_subscriber(extra_strings, subscriber, type_root): @@ -269,21 +291,21 @@ def get_subscribers(extra_strings, type, server, type_root, >>> def sgs(*args, **kwargs): ... return sorted(get_subscribers(*args, **kwargs)) >>> es = [] - >>> es = subscribe(es, "John Doe ", [diff.BUGDIR_TYPE_ALL], - ... ["a.com"], diff.BUGDIR_TYPE_ALL) - >>> es = subscribe(es, "Jane Doe ", [diff.BUGDIR_TYPE_NEW], - ... ["*"], diff.BUGDIR_TYPE_ALL) - >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL) + >>> es = subscribe(es, "John Doe ", [libbe.diff.BUGDIR_TYPE_ALL], + ... ["a.com"], libbe.diff.BUGDIR_TYPE_ALL) + >>> es = subscribe(es, "Jane Doe ", [libbe.diff.BUGDIR_TYPE_NEW], + ... ["*"], libbe.diff.BUGDIR_TYPE_ALL) + >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "a.com", libbe.diff.BUGDIR_TYPE_ALL) ['John Doe '] - >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL, + >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "a.com", libbe.diff.BUGDIR_TYPE_ALL, ... match_descendant_types=True) ['Jane Doe ', 'John Doe '] - >>> sgs(es, diff.BUGDIR_TYPE_ALL, "b.net", diff.BUGDIR_TYPE_ALL, + >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "b.net", libbe.diff.BUGDIR_TYPE_ALL, ... match_descendant_types=True) ['Jane Doe '] - >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL) + >>> sgs(es, libbe.diff.BUGDIR_TYPE_NEW, "a.com", libbe.diff.BUGDIR_TYPE_ALL) ['Jane Doe '] - >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL, + >>> sgs(es, libbe.diff.BUGDIR_TYPE_NEW, "a.com", libbe.diff.BUGDIR_TYPE_ALL, ... match_ancestor_types=True) ['Jane Doe ', 'John Doe '] """ @@ -324,11 +346,11 @@ def get_bugdir_subscribers(bugdir, server): >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) >>> a = bd.bug_from_shortname("a") >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe ", - ... [diff.BUGDIR_TYPE_ALL], ["a.com"], diff.BUGDIR_TYPE_ALL) + ... [libbe.diff.BUGDIR_TYPE_ALL], ["a.com"], libbe.diff.BUGDIR_TYPE_ALL) >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe ", - ... [diff.BUGDIR_TYPE_NEW], ["*"], diff.BUGDIR_TYPE_ALL) + ... [libbe.diff.BUGDIR_TYPE_NEW], ["*"], libbe.diff.BUGDIR_TYPE_ALL) >>> a.extra_strings = subscribe(a.extra_strings, "John Doe ", - ... [diff.BUG_TYPE_ALL], ["a.com"], diff.BUG_TYPE_ALL) + ... [libbe.diff.BUG_TYPE_ALL], ["a.com"], libbe.diff.BUG_TYPE_ALL) >>> subscribers = get_bugdir_subscribers(bd, "a.com") >>> subscribers["Jane Doe "]["%(bugdir_id)s"] [] @@ -339,20 +361,20 @@ def get_bugdir_subscribers(bugdir, server): >>> get_bugdir_subscribers(bd, "b.net") {'Jane Doe ': {'%(bugdir_id)s': []}} >>> bd.cleanup() - """ % {'bugdir_id':diff.BUGDIR_ID} + """ % {'bugdir_id':libbe.diff.BUGDIR_ID} subscribers = {} - for sub in get_subscribers(bugdir.extra_strings, diff.BUGDIR_TYPE_ALL, - server, diff.BUGDIR_TYPE_ALL, + for sub in get_subscribers(bugdir.extra_strings, libbe.diff.BUGDIR_TYPE_ALL, + server, libbe.diff.BUGDIR_TYPE_ALL, match_descendant_types=True): i,s,ts,srvs = _get_subscriber(bugdir.extra_strings, sub, - diff.BUGDIR_TYPE_ALL) + libbe.diff.BUGDIR_TYPE_ALL) subscribers[sub] = {"DIR":ts} for bug in bugdir: - for sub in get_subscribers(bug.extra_strings, diff.BUG_TYPE_ALL, - server, diff.BUG_TYPE_ALL, + for sub in get_subscribers(bug.extra_strings, libbe.diff.BUG_TYPE_ALL, + server, libbe.diff.BUG_TYPE_ALL, match_descendant_types=True): i,s,ts,srvs = _get_subscriber(bug.extra_strings, sub, - diff.BUG_TYPE_ALL) + libbe.diff.BUG_TYPE_ALL) if sub in subscribers: subscribers[sub][bug.uuid] = ts else: diff --git a/libbe/diff.py b/libbe/diff.py index c0132ff..7acce54 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -22,19 +22,20 @@ import difflib import types import libbe -from libbe import bugdir, bug, settings_object, tree -from libbe.utility import time_to_str -if libbe.TESTING == True: - import doctest +import libbe.bugdir +import libbe.bug +import libbe.storage.util.settings_object +import libbe.util.tree +from libbe.util.utility import time_to_str -class SubscriptionType (tree.Tree): +class SubscriptionType (libbe.util.tree.Tree): """ Trees of subscription types to allow users to select exactly what notifications they want to subscribe to. """ def __init__(self, type_name, *args, **kwargs): - tree.Tree.__init__(self, *args, **kwargs) + libbe.util.tree.Tree.__init__(self, *args, **kwargs) self.type = type_name def __str__(self): return self.type @@ -133,7 +134,7 @@ def subscriptions_from_string(string=None, subscription_sep=',', id_sep=':'): subscriptions.append(Subscription(id, type)) return subscriptions -class DiffTree (tree.Tree): +class DiffTree (libbe.util.tree.Tree): """ A tree holding difference data for easy report generation. >>> bugdir = DiffTree("bugdir") @@ -174,7 +175,7 @@ class DiffTree (tree.Tree): """ def __init__(self, name, data=None, data_part_fn=str, requires_children=False, masked=False): - tree.Tree.__init__(self) + libbe.util.tree.Tree.__init__(self) self.name = name self.data = data self.data_part_fn = data_part_fn -- cgit From a1bd5432ffbf28bf2fadfed8a5b2db917f243344 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 15 Dec 2009 01:52:17 -0500 Subject: Transitioned tag to Command-format --- libbe/command/base.py | 5 +- libbe/command/due.py | 2 + libbe/command/tag.py | 194 +++++++++++++++++++++++++---------------------- libbe/ui/command_line.py | 2 +- 4 files changed, 109 insertions(+), 94 deletions(-) diff --git a/libbe/command/base.py b/libbe/command/base.py index 2aaf51e..74573a3 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -232,10 +232,7 @@ class Command (object): params[arg.name] = args[i] else: # no value given assert in_optional_args == True, arg.name - if arg.repeatable == True: - params[arg.name] = [arg.default] - else: - params[arg.name] = arg.default + params[arg.name] = arg.default if len(args) > len(self.args): # add some additional repeats assert self.args[-1].repeatable == True, self.args[-1].name params[self.args[-1].name].extend(args[len(self.args):]) diff --git a/libbe/command/due.py b/libbe/command/due.py index 119115c..f3ad2f1 100644 --- a/libbe/command/due.py +++ b/libbe/command/due.py @@ -19,8 +19,10 @@ import libbe.command import libbe.command.util import libbe.util.utility + DUE_TAG = 'DUE:' + class Due (libbe.command.Command): """Set bug due dates diff --git a/libbe/command/tag.py b/libbe/command/tag.py index f3819bd..191dbc9 100644 --- a/libbe/command/tag.py +++ b/libbe/command/tag.py @@ -14,124 +14,140 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Tag a bug, or search bugs for tags""" -from libbe import cmdutil, bugdir + +import libbe +import libbe.command +import libbe.command.util +import libbe.util.utility + + +TAG_TAG = 'TAG:' + + import os, copy -__desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> from libbe import utility - >>> bd = bugdir.SimpleBugDir() - >>> bd.set_sync_with_disk(True) - >>> os.chdir(bd.root) - >>> a = bd.bug_from_shortname("a") +class Tag (libbe.command.Command): + __doc__ = """Tag a bug, or search bugs for tags + + >>> import sys + >>> import libbe.bugdir + >>> bd = libbe.bugdir.SimpleBugDir(memory=False) + >>> cmd = Tag() + >>> cmd._bugdir = bd + >>> cmd._setup_io = lambda i_enc,o_enc : None + >>> cmd.stdout = sys.stdout + + >>> a = bd.bug_from_uuid('a') >>> print a.extra_strings [] - >>> execute(["a", "GUI"], manipulate_encodings=False) - Tags for a: + >>> ret = cmd.run(args=['/a', 'GUI']) + Tags for abc/a: GUI - >>> bd._clear_bugs() # resync our copy of bug - >>> a = bd.bug_from_shortname("a") + >>> bd.flush_reload() + >>> a = bd.bug_from_uuid('a') >>> print a.extra_strings - ['TAG:GUI'] - >>> execute(["a", "later"], manipulate_encodings=False) - Tags for a: + ['%(tag_tag)sGUI'] + >>> ret = cmd.run(args=['/a', 'later']) + Tags for abc/a: GUI later - >>> execute(["a"], manipulate_encodings=False) - Tags for a: + >>> ret = cmd.run(args=['/a']) + Tags for abc/a: GUI later - >>> execute(["--list"], manipulate_encodings=False) + >>> ret = cmd.run({'list':True}) GUI later - >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) - Tags for a: + >>> ret = cmd.run(args=['/a', 'Alphabetically first']) + Tags for abc/a: Alphabetically first GUI later - >>> bd._clear_bugs() # resync our copy of bug - >>> a = bd.bug_from_shortname("a") + >>> bd.flush_reload() + >>> a = bd.bug_from_uuid('a') >>> print a.extra_strings - ['TAG:Alphabetically first', 'TAG:GUI', 'TAG:later'] + ['%(tag_tag)sAlphabetically first', '%(tag_tag)sGUI', '%(tag_tag)slater'] >>> a.extra_strings = [] >>> print a.extra_strings [] - >>> execute(["a"], manipulate_encodings=False) - >>> bd._clear_bugs() # resync our copy of bug - >>> a = bd.bug_from_shortname("a") + >>> ret = cmd.run(args=['/a']) + >>> bd.flush_reload() + >>> a = bd.bug_from_uuid('a') >>> print a.extra_strings [] - >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) - Tags for a: + >>> ret = cmd.run(args=['/a', 'Alphabetically first']) + Tags for abc/a: Alphabetically first - >>> execute(["--remove", "a", "Alphabetically first"], manipulate_encodings=False) + >>> ret = cmd.run({'remove':True}, ['/a', 'Alphabetically first']) >>> bd.cleanup() - """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) + """ % {'tag_tag':TAG_TAG} + name = 'tag' - if len(args) == 0 and options.list == False: - raise cmdutil.UsageError("Please specify a bug id.") - elif len(args) > 2 or (len(args) > 0 and options.list == True): - help() - raise cmdutil.UsageError("Too many arguments.") - - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if options.list: - bd.load_all_bugs() - tags = [] - for bug in bd: - for estr in bug.extra_strings: - if estr.startswith("TAG:"): - tag = estr[4:] - if tag not in tags: - tags.append(tag) - tags.sort() - if len(tags) > 0: - print '\n'.join(tags) - return - bug = cmdutil.bug_from_id(bd, args[0]) - if len(args) == 2: - given_tag = args[1] - estrs = bug.extra_strings - tag_string = "TAG:%s" % given_tag - if options.remove == True: - estrs.remove(tag_string) - else: # add the tag - estrs.append(tag_string) - bug.extra_strings = estrs # reassign to notice change + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.options.extend([ + libbe.command.Option(name='remove', short_name='r', + help='Remove TAG (instead of adding it)'), + libbe.command.Option(name='list', short_name='l', + help='List all available tags and exit'), + ]) + self.args.extend([ + libbe.command.Argument( + name='id', metavar='BUG-ID', optional=True, + completion_callback=libbe.command.util.complete_bug_id), + libbe.command.Argument( + name='tag', metavar='TAG', default=tuple(), + optional=True, repeatable=True), + ]) - tags = [] - for estr in bug.extra_strings: - if estr.startswith("TAG:"): - tags.append(estr[4:]) + def _run(self, **params): + if params['id'] == None and params['list'] == False: + raise libbe.command.UserError('Please specify a bug id.') + if params['id'] != None and params['list'] == True: + raise libbe.command.UserError( + 'Do not specify a bug id with the --list option.') + bugdir = self._get_bugdir() + if params['list'] == True: + bugdir.load_all_bugs() + tags = [] + for bug in bugdir: + for estr in bug.extra_strings: + if estr.startswith(TAG_TAG): + tag = estr[len(TAG_TAG):] + if tag not in tags: + tags.append(tag) + tags.sort() + if len(tags) > 0: + print >> self.stdout, '\n'.join(tags) + return 0 - if len(tags) > 0: - print "Tags for %s:" % bug.uuid - print '\n'.join(tags) + bug,dummy_comment = libbe.command.util.bug_comment_from_user_id( + bugdir, params['id']) + if len(params['tag']) > 0: + estrs = bug.extra_strings + for tag in params['tag']: + tag_string = '%s%s' % (TAG_TAG, tag) + if params['remove'] == True: + estrs.remove(tag_string) + else: # add the tag + estrs.append(tag_string) + bug.extra_strings = estrs # reassign to notice change -def get_parser(): - parser = cmdutil.CmdOptionParser("be tag BUG-ID [TAG]\nor: be tag --list") - parser.add_option("-r", "--remove", action="store_true", dest="remove", - help="Remove TAG (instead of adding it)") - parser.add_option("-l", "--list", action="store_true", dest="list", - help="List all available tags and exit") - return parser + tags = [] + for estr in bug.extra_strings: + if estr.startswith(TAG_TAG): + tags.append(estr[len(TAG_TAG):]) + + if len(tags) > 0: + print "Tags for %s:" % bug.id.user() + print '\n'.join(tags) + return 0 -longhelp=""" + def _long_help(self): + return """ If TAG is given, add TAG to BUG-ID. If it is not specified, just print the tags for BUG-ID. To search for bugs with a particular tag, try - $ be list --extra-strings TAG: -""" - -def help(): - return get_parser().help_str() + longhelp + $ be list --extra-strings %s +""" % TAG_TAG diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py index bb38be8..2dff930 100755 --- a/libbe/ui/command_line.py +++ b/libbe/ui/command_line.py @@ -122,7 +122,7 @@ class CmdOptionParser(optparse.OptionParser): fragment = args[i+1] self.complete(argument, fragment) if len(parsed_args) > len(self.command.args) \ - and self.command.args[-1] == False: + 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: -- cgit From 4370270929db62a32d168ae221ecc70a2d80269e Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 15 Dec 2009 02:38:51 -0500 Subject: Transitioned target to Command-format --- libbe/command/diff.py | 2 +- libbe/command/tag.py | 3 - libbe/command/target.py | 162 ++++++++++++++++++++++++++++------------------- libbe/ui/command_line.py | 2 + 4 files changed, 100 insertions(+), 69 deletions(-) diff --git a/libbe/command/diff.py b/libbe/command/diff.py index de8cf67..05242db 100644 --- a/libbe/command/diff.py +++ b/libbe/command/diff.py @@ -25,7 +25,7 @@ import libbe.storage import libbe.diff class Diff (libbe.command.Command): - """Compare bug reports with older tree + __doc__ = """Compare bug reports with older tree >>> import sys >>> import libbe.bugdir diff --git a/libbe/command/tag.py b/libbe/command/tag.py index 191dbc9..26ff1b5 100644 --- a/libbe/command/tag.py +++ b/libbe/command/tag.py @@ -18,14 +18,11 @@ import libbe import libbe.command import libbe.command.util -import libbe.util.utility TAG_TAG = 'TAG:' -import os, copy - class Tag (libbe.command.Command): __doc__ = """Tag a bug, or search bugs for tags diff --git a/libbe/command/target.py b/libbe/command/target.py index 5dd5d38..39e12b2 100644 --- a/libbe/command/target.py +++ b/libbe/command/target.py @@ -18,83 +18,104 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Assorted bug target manipulations and queries""" -from libbe import cmdutil, bugdir -from becommands import depend -__desc__ = __doc__ -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ +import libbe +import libbe.command +import libbe.command.util +import libbe.command.depend + + + +class Target (libbe.command.Command): + """Assorted bug target manipulations and queries + >>> import os, StringIO, sys - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute(["a"], manipulate_encodings=False) + >>> import libbe.bugdir + >>> bd = libbe.bugdir.SimpleBugDir(memory=False) + >>> cmd = Target() + >>> cmd._storage = bd.storage + >>> cmd._setup_io = lambda i_enc,o_enc : None + >>> cmd.stdout = sys.stdout + + >>> ret = cmd.run(args=['/a']) No target assigned. - >>> execute(["a", "tomorrow"], manipulate_encodings=False) - >>> execute(["a"], manipulate_encodings=False) + >>> ret = cmd.run(args=['/a', 'tomorrow']) + >>> ret = cmd.run(args=['/a']) tomorrow - >>> orig_stdout = sys.stdout - >>> tmp_stdout = StringIO.StringIO() - >>> sys.stdout = tmp_stdout - >>> execute(["--resolve", "tomorrow"], manipulate_encodings=False) - >>> sys.stdout = orig_stdout - >>> output = tmp_stdout.getvalue().strip() + >>> cmd.stdout = StringIO.StringIO() + >>> ret = cmd.run({'resolve':True}, ['tomorrow']) + >>> output = cmd.stdout.getvalue().strip() >>> target = bd.bug_from_uuid(output) >>> print target.summary tomorrow >>> print target.severity target - >>> execute(["a", "none"], manipulate_encodings=False) - >>> execute(["a"], manipulate_encodings=False) + >>> cmd.stdout = sys.stdout + >>> ret = cmd.run(args=['/a', 'none']) + >>> ret = cmd.run(args=['/a']) No target assigned. >>> bd.cleanup() """ - parser = get_parser() - options, args = parser.parse_args(args) - cmdutil.default_complete(options, args, parser, - bugid_args={0: lambda bug : bug.active==True}) - - if (options.resolve == False and len(args) not in (1, 2)) \ - or (options.resolve == True and len(args) not in (0, 1)): - raise cmdutil.UsageError('Incorrect number of arguments.') - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if options.resolve == True: - if len(args) == 0: - summary = None - else: - summary = args[0] - bug = bug_from_target_summary(bd, summary) - if bug == None: - print 'No target assigned.' - else: - print bug.uuid - return - bug = cmdutil.bug_from_id(bd, args[0]) - if len(args) == 1: - target = bug_target(bd, bug) - if target is None: - print "No target assigned." + name = 'target' + + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.options.extend([ + libbe.command.Option(name='resolve', short_name='r', + help="Print the UUID for the target bug whose summary " + "matches TARGET. If TARGET is not given, print the UUID " + "of the current bugdir target."), + ]) + self.args.extend([ + libbe.command.Argument( + name='id', metavar='BUG-ID', default=None, + optional=True, + completion_callback=libbe.command.util.complete_bug_id), + libbe.command.Argument( + name='target', metavar='TARGET', default=None, + optional=True, + completion_callback=complete_target), + ]) + + def _run(self, **params): + if params['resolve'] == False: + if params['id'] == None: + raise libbe.command.UserError('Please specify a bug id.') else: - print target.summary - else: - if args[1] == "none": - target = remove_target(bd, bug) + if params['target'] != None: + raise libbe.command.UserError('Too many arguments') + params['target'] = params.pop('id') + bugdir = self._get_bugdir() + if params['resolve'] == True: + bug = bug_from_target_summary(bugdir, params['target']) + if bug == None: + print >> self.stdout, 'No target assigned.' + else: + print >> self.stdout, bug.uuid + return 0 + bug,dummy_comment = libbe.command.util.bug_comment_from_user_id( + bugdir, params['id']) + if params['target'] == None: + target = bug_target(bugdir, bug) + if target == None: + print >> self.stdout, 'No target assigned.' + else: + print >> self.stdout, target.summary else: - target = add_target(bd, bug, args[1]) + if params['target'] == 'none': + target = remove_target(bugdir, bug) + else: + target = add_target(bugdir, bug, params['target']) + return 0 -def get_parser(): - parser = cmdutil.CmdOptionParser("be target BUG-ID [TARGET]\nor: be target --resolve [TARGET]") - parser.add_option("-r", "--resolve", action="store_true", dest="resolve", - help="Print the UUID for the target bug whose summary matches TARGET. If TARGET is not given, print the UUID of the current bugdir target. If that is not set, don't print anything.", - default=False) - return parser + def _usage(self): + return 'usage: be %(name)s BUG-ID [TARGET]\nor: be %(name)s --resolve [TARGET]' \ + % vars(self) -longhelp=""" + def _long_help(self): + return """ Assorted bug target manipulations and queries. If no target is specified, the bug's current target is printed. If @@ -118,9 +139,6 @@ by UUID), try $ be set target $(be target --resolve SUMMARY) """ -def help(): - return get_parser().help_str() + longhelp - def bug_from_target_summary(bugdir, summary=None): if summary == None: if bugdir.target == None: @@ -143,7 +161,7 @@ def bug_target(bugdir, bug): if bug.severity == 'target': return bug matched = [] - for blocked in depend.get_blocks(bugdir, bug): + for blocked in libbe.command.depend.get_blocks(bugdir, bug): if blocked.severity == 'target': matched.append(blocked) if len(matched) == 0: @@ -156,7 +174,7 @@ def bug_target(bugdir, bug): def remove_target(bugdir, bug): target = bug_target(bugdir, bug) - depend.remove_block(target, bug) + libbe.command.depend.remove_block(target, bug) return target def add_target(bugdir, bug, summary): @@ -164,5 +182,19 @@ def add_target(bugdir, bug, summary): if target == None: target = bugdir.new_bug(summary=summary) target.severity = 'target' - depend.add_block(target, bug) + libbe.command.depend.add_block(target, bug) return target + +def targets(bugdir): + bugdir.load_all_bugs() + for bug in bugdir: + if bug.severity == 'target': + yield bug.summary + +def complete_target(command, argument, fragment=None): + """ + List possible command completions for fragment. + + argument argument is not used. + """ + return targets(command._get_bugdir()) diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py index 2dff930..ce0e55e 100755 --- a/libbe/ui/command_line.py +++ b/libbe/ui/command_line.py @@ -223,6 +223,8 @@ class BE (libbe.command.Command): for name in libbe.command.commands(): module = libbe.command.get_command(name) Class = libbe.command.get_command_class(module, name) + assert hasattr(Class, '__doc__') and Class.__doc__ != None, \ + 'Command class %s missing docstring' % Class cmdlist.append((name, Class.__doc__.splitlines()[0])) cmdlist.sort() longest_cmd_len = max([len(name) for name,desc in cmdlist]) -- cgit From eb26ca7e4a6886f97740c45e9e3b7bdd2d08d76f Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 15 Dec 2009 03:05:34 -0500 Subject: Transitioned set to Command-format --- libbe/command/set.py | 160 +++++++++++++++++++++++++----------------------- libbe/command/target.py | 7 +-- 2 files changed, 87 insertions(+), 80 deletions(-) diff --git a/libbe/command/set.py b/libbe/command/set.py index 4d54a59..cea6fb9 100644 --- a/libbe/command/set.py +++ b/libbe/command/set.py @@ -17,90 +17,96 @@ # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Change tree settings""" + + import textwrap -from libbe import cmdutil, bugdir, vcs, settings_object -__desc__ = __doc__ - -def _value_string(bd, setting): - val = bd.settings.get(setting, settings_object.EMPTY) - if val == settings_object.EMPTY: - default = getattr(bd, bd._setting_name_to_attr_name(setting)) - if default not in [None, settings_object.EMPTY]: - val = "None (%s)" % default - else: - val = None - return str(val) -def execute(args, manipulate_encodings=True, restrict_file_access=False, - dir="."): - """ - >>> import os - >>> bd = bugdir.SimpleBugDir() - >>> os.chdir(bd.root) - >>> execute(["target"], manipulate_encodings=False) +import libbe +import libbe.bugdir +import libbe.command +import libbe.command.util +from libbe.storage.util.settings_object import EMPTY + + +class Set (libbe.command.Command): + """Change bug directory settings + + >>> import sys + >>> import libbe.bugdir + >>> bd = libbe.bugdir.SimpleBugDir(memory=False) + >>> cmd = Set() + >>> cmd._storage = bd.storage + >>> cmd._setup_io = lambda i_enc,o_enc : None + >>> cmd.stdout = sys.stdout + + >>> ret = cmd.run(args=['target']) None - >>> execute(["target", "tomorrow"], manipulate_encodings=False) - >>> execute(["target"], manipulate_encodings=False) - tomorrow - >>> execute(["target", "none"], manipulate_encodings=False) - >>> execute(["target"], manipulate_encodings=False) + >>> ret = cmd.run(args=['target', 'abcdefg']) + >>> ret = cmd.run(args=['target']) + abcdefg + >>> ret = cmd.run(args=['target', 'none']) + >>> ret = cmd.run(args=['target']) None >>> bd.cleanup() """ - parser = get_parser() - options, args = parser.parse_args(args) - complete(options, args, parser) - if len(args) > 2: - raise cmdutil.UsageError, "Too many arguments" - bd = bugdir.BugDir(from_disk=True, - manipulate_encodings=manipulate_encodings, - root=dir) - if len(args) == 0: - keys = bd.settings_properties - keys.sort() - for key in keys: - print "%16s: %s" % (key, _value_string(bd, key)) - elif len(args) == 1: - print _value_string(bd, args[0]) - else: - if args[1] == "none": - setattr(bd, args[0], settings_object.EMPTY) + name = 'set' + + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.args.extend([ + libbe.command.Argument( + name='setting', metavar='SETTING', optional=True, + completion_callback=complete_bugdir_settings), + libbe.command.Argument( + name='value', metavar='VALUE', optional=True) + ]) + + def _run(self, **params): + bugdir = self._get_bugdir() + if params['setting'] == None: + keys = bugdir.settings_properties + keys.sort() + for key in keys: + print >> self.stdout, \ + '%16s: %s' % (key, _value_string(bugdir, key)) + return 0 + if params['setting'] not in bugdir.settings_properties: + msg = 'Invalid setting %s\n' % params['setting'] + msg += 'Allowed settings:\n ' + msg += '\n '.join(bugdir.settings_properties) + raise libbe.command.UserError(msg) + if params['value'] == None: + print _value_string(bugdir, params['setting']) else: - if args[0] not in bd.settings_properties: - msg = "Invalid setting %s\n" % args[0] - msg += 'Allowed settings:\n ' - msg += '\n '.join(bd.settings_properties) - raise cmdutil.UserError(msg) - old_setting = bd.settings.get(args[0]) - setattr(bd, args[0], args[1]) - -def get_parser(): - parser = cmdutil.CmdOptionParser("be set [NAME] [VALUE]") - return parser + if params['value'] == 'none': + params['value'] = EMPTY + old_setting = bugdir.settings.get(params['setting']) + attr = bugdir._setting_name_to_attr_name(params['setting']) + setattr(bugdir, attr, params['value']) + return 0 def get_bugdir_settings(): settings = [] - for s in bugdir.BugDir.settings_properties: + for s in libbe.bugdir.BugDir.settings_properties: settings.append(s) settings.sort() documented_settings = [] for s in settings: - set = getattr(bugdir.BugDir, s) + set = getattr(libbe.bugdir.BugDir, s) dstr = set.__doc__.strip() # per-setting comment adjustments - if s == "vcs_name": + if s == 'vcs_name': lines = dstr.split('\n') - while lines[0].startswith("This property defaults to") == False: + while lines[0].startswith('This property defaults to') == False: lines.pop(0) assert len(lines) != None, \ - "Unexpected vcs_name docstring:\n '%s'" % dstr + 'Unexpected vcs_name docstring:\n "%s"' % dstr lines.insert( - 0, "The name of the revision control system to use.\n") + 0, 'The name of the revision control system to use.\n') dstr = '\n'.join(lines) doc = textwrap.wrap(dstr, width=70, initial_indent=' ', subsequent_indent=' ') - documented_settings.append("%s\n%s" % (s, '\n'.join(doc))) + documented_settings.append('%s\n%s' % (s, '\n'.join(doc))) return documented_settings longhelp=""" @@ -116,17 +122,21 @@ Allowed settings are: %s""" % ('\n'.join(get_bugdir_settings()),) -def help(): - return get_parser().help_str() + longhelp - -def complete(options, args, parser): - for option, value in cmdutil.option_value_pairs(options, parser): - if value == "--complete": - # no argument-options at the moment, so this is future-proofing - raise cmdutil.GetCompletions() - for pos,value in enumerate(args): - if value == "--complete": - if pos == 0: # first positional argument is a setting name - props = bugdir.BugDir.settings_properties - raise cmdutil.GetCompletions(props) - raise cmdutil.GetCompletions() # no positional arguments for list + +def _value_string(bugdir, setting): + val = bugdir.settings.get(setting, EMPTY) + if val == EMPTY: + default = getattr(bugdir, bugdir._setting_name_to_attr_name(setting)) + if default not in [None, EMPTY]: + val = 'None (%s)' % default + else: + val = None + return str(val) + +def complete_bugdir_settings(command, argument, fragment=None): + """ + List possible command completions for fragment. + + Neither the command nor argument arguments are used. + """ + return libbe.bugdir.BugDir.settings_properties diff --git a/libbe/command/target.py b/libbe/command/target.py index 39e12b2..034c532 100644 --- a/libbe/command/target.py +++ b/libbe/command/target.py @@ -25,7 +25,6 @@ import libbe.command.util import libbe.command.depend - class Target (libbe.command.Command): """Assorted bug target manipulations and queries @@ -70,12 +69,10 @@ class Target (libbe.command.Command): ]) self.args.extend([ libbe.command.Argument( - name='id', metavar='BUG-ID', default=None, - optional=True, + name='id', metavar='BUG-ID', optional=True, completion_callback=libbe.command.util.complete_bug_id), libbe.command.Argument( - name='target', metavar='TARGET', default=None, - optional=True, + name='target', metavar='TARGET', optional=True, completion_callback=complete_target), ]) -- cgit From 58bedebfddbb8e1fc8f0a441163526feaecb753b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 15 Dec 2009 03:31:48 -0500 Subject: Transition to Command-format complete. Well, except for going through and updating the _long_help() strings. $ python test.py libbe.command succeeds for everything except Diff and Subscribe, which is expected since I haven't fixed up libbe.diff yet. --- libbe/command/base.py | 2 +- libbe/command/diff.py | 18 +++++++++--------- libbe/command/new.py | 2 ++ libbe/command/set.py | 28 ++++++++++++++-------------- libbe/command/subscribe.py | 8 ++++---- libbe/command/util.py | 2 +- libbe/diff.py | 4 ---- 7 files changed, 31 insertions(+), 33 deletions(-) diff --git a/libbe/command/base.py b/libbe/command/base.py index 74573a3..9f50632 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -266,7 +266,7 @@ class Command (object): def help(self, *args): return '\n\n'.join([self._usage(), self._option_help(), - self._long_help()]) + self._long_help().rstrip('\n')]) def _usage(self): usage = 'usage: be %s [options]' % self.name diff --git a/libbe/command/diff.py b/libbe/command/diff.py index 05242db..6a7c36b 100644 --- a/libbe/command/diff.py +++ b/libbe/command/diff.py @@ -30,7 +30,7 @@ class Diff (libbe.command.Command): >>> import sys >>> import libbe.bugdir >>> bd = libbe.bugdir.SimpleBugDir(memory=False) - >>> cmd = Subscribe() + >>> cmd = Diff() >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout @@ -38,8 +38,8 @@ class Diff (libbe.command.Command): >>> original = bd.storage.commit('Original status') >>> bug = bd.bug_from_uuid('a') >>> bug.status = 'closed' - >>> changed = bd.vcs.commit('Closed bug a') - >>> if bd.vcs.versioned == True: + >>> changed = bd.storage.commit('Closed bug a') + >>> if bd.storage.versioned == True: ... ret = cmd.run(args=[original]) ... else: ... print 'Modified bugs:\\n a:cm: Bug A\\n Changed bug settings:\\n status: open -> closed' @@ -47,12 +47,12 @@ class Diff (libbe.command.Command): a:cm: Bug A Changed bug settings: status: open -> closed - >>> if bd.vcs.versioned == True: + >>> if bd.storage.versioned == True: ... ret = cmd.run({'subscribe':'%(bugdir_id)s:mod', 'uuids':True}, [original]) ... else: ... print 'a' a - >>> if bd.vcs.versioned == False: + >>> if bd.storage.versioned == False: ... ret = cmd.run(args=[original]) ... else: ... raise libbe.command.UserError('This repository not revision-controlled.') @@ -131,10 +131,10 @@ class Diff (libbe.command.Command): def _long_help(self): return """ -Uses the VCS to compare the current tree with a previous tree, and -prints a pretty report. If REVISION is given, it is a specifier for -the particular previous tree to use. Specifiers are specific to their -VCS. +Uses the storage backend to compare the current tree with a previous +tree, and prints a pretty report. If REVISION is given, it is a +specifier for the particular previous tree to use. Specifiers are +specific to their storage backend. For Arch your specifier must be a fully-qualified revision name. diff --git a/libbe/command/new.py b/libbe/command/new.py index de215fa..57ff5dc 100644 --- a/libbe/command/new.py +++ b/libbe/command/new.py @@ -35,9 +35,11 @@ class New (libbe.command.Command): >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout + >>> uuid_gen = libbe.util.id.uuid_gen >>> libbe.util.id.uuid_gen = lambda: 'X' >>> ret = cmd.run(args=['this is a test',]) Created bug with ID abc/X + >>> libbe.util.id.uuid_gen = uuid_gen >>> bd.flush_reload() >>> bug = bd.bug_from_uuid('X') >>> print bug.summary diff --git a/libbe/command/set.py b/libbe/command/set.py index cea6fb9..aaf2b58 100644 --- a/libbe/command/set.py +++ b/libbe/command/set.py @@ -85,6 +85,20 @@ class Set (libbe.command.Command): setattr(bugdir, attr, params['value']) return 0 + def _long_help(self): + return """ +Show or change per-tree settings. + +If name and value are supplied, the name is set to a new value. +If no value is specified, the current value is printed. +If no arguments are provided, all names and values are listed. + +To unset a setting, set it to "none". + +Allowed settings are: + +%s""" % ('\n'.join(get_bugdir_settings()),) + def get_bugdir_settings(): settings = [] for s in libbe.bugdir.BugDir.settings_properties: @@ -109,20 +123,6 @@ def get_bugdir_settings(): documented_settings.append('%s\n%s' % (s, '\n'.join(doc))) return documented_settings -longhelp=""" -Show or change per-tree settings. - -If name and value are supplied, the name is set to a new value. -If no value is specified, the current value is printed. -If no arguments are provided, all names and values are listed. - -To unset a setting, set it to "none". - -Allowed settings are: - -%s""" % ('\n'.join(get_bugdir_settings()),) - - def _value_string(bugdir, setting): val = bugdir.settings.get(setting, EMPTY) if val == EMPTY: diff --git a/libbe/command/subscribe.py b/libbe/command/subscribe.py index 5c5acdb..e86a9c8 100644 --- a/libbe/command/subscribe.py +++ b/libbe/command/subscribe.py @@ -35,14 +35,14 @@ class Subscribe (libbe.command.Command): >>> import libbe.bugdir >>> bd = libbe.bugdir.SimpleBugDir(memory=False) >>> cmd = Subscribe() - >>> cmd._storage = bd.storage + >>> cmd._bugdir = bd >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout >>> a = bd.bug_from_uuid('a') >>> print a.extra_strings [] - >>> ret = cmd.run({'subscriber':'John Doe '], ['/a']) # doctest: +NORMALIZE_WHITESPACE + >>> ret = cmd.run({'subscriber':'John Doe '}, ['/a']) # doctest: +NORMALIZE_WHITESPACE Subscriptions for abc/a: John Doe all * >>> bd.flush_reload() @@ -57,7 +57,7 @@ class Subscribe (libbe.command.Command): Subscriptions for a: Jane Doe all a.com,a.edu,b.net John Doe all * - >>> ret = cmd.run({'-u', 'subscriber':'Jane Doe ', 'servers':'a.com'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE + >>> ret = cmd.run({'unsubscribe':True, 'subscriber':'Jane Doe ', 'servers':'a.com'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE Subscriptions for a: Jane Doe all a.edu,b.net John Doe all * @@ -69,7 +69,7 @@ class Subscribe (libbe.command.Command): Subscriptions for a: John Doe all * >>> ret = cmd.run({'unsubscribe':True, 'subscriber':'John Doe '}, ['/a']) - >>> ret = cmd.run({'subscriber':'Jane Doe ', '-t':'new'}, 'DIR']) # doctest: +NORMALIZE_WHITESPACE + >>> ret = cmd.run({'subscriber':'Jane Doe ', 'types':'new'}, ['DIR']) # doctest: +NORMALIZE_WHITESPACE Subscriptions for bug directory: Jane Doe new * >>> ret = cmd.run({'subscriber':'Jane Doe '}, ['DIR']) # doctest: +NORMALIZE_WHITESPACE diff --git a/libbe/command/util.py b/libbe/command/util.py index f6734d5..a4aaf5f 100644 --- a/libbe/command/util.py +++ b/libbe/command/util.py @@ -87,7 +87,7 @@ def select_values(string, possible_values, name="unkown"): blacklisted_values = set(string[1:].split(',')) for value in blacklisted_values: if value not in possible_values: - raise UserError('Invalid %s %s\n %s' + raise libbe.command.UserError('Invalid %s %s\n %s' % (name, value, possible_values)) possible_values.remove(value) else: diff --git a/libbe/diff.py b/libbe/diff.py index 7acce54..f8e5f91 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -638,7 +638,3 @@ class Diff (object): def comment_body_change_string(self, bodies): old_body,new_body = bodies return difflib.unified_diff(old_body, new_body) - - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() -- cgit From c7c88ee199a3fd6d95a1de4f778bdab3f6f43e38 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 15 Dec 2009 03:57:54 -0500 Subject: Fixed up libbe.diff --- libbe/bugdir.py | 4 +- libbe/diff.py | 168 ++++++++++++++++++++++++++++---------------------------- 2 files changed, 85 insertions(+), 87 deletions(-) diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 5bb69e2..defa250 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -261,7 +261,7 @@ class BugDir (list, settings_object.SavedSettingsObject): def remove_bug(self, bug): self.remove(bug) - if self.storage.is_writeable(): + if self.storage != None and self.storage.is_writeable(): bug.remove() def bug_from_uuid(self, uuid): @@ -403,7 +403,7 @@ if libbe.TESTING == True: # self.assertRaises(AlreadyInitialized, BugDir, # self.dir.path, assertNewBugDir=True) # def versionTest(self): -# if self.storage.versioned == False: +# if self.storage != None and self.storage.versioned == False: # return # original = self.bugdir.storage.commit("Began versioning") # bugA = self.bugdir.bug_from_uuid("a") diff --git a/libbe/diff.py b/libbe/diff.py index f8e5f91..32e8836 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -24,8 +24,8 @@ import types import libbe import libbe.bugdir import libbe.bug -import libbe.storage.util.settings_object import libbe.util.tree +from libbe.storage.util.settings_object import setting_name_to_attr_name from libbe.util.utility import time_to_str @@ -42,28 +42,28 @@ class SubscriptionType (libbe.util.tree.Tree): def __cmp__(self, other): return cmp(self.type, other.type) def __repr__(self): - return "" % str(self) + return '' % str(self) def string_tree(self, indent=0): lines = [] for depth,node in self.thread(): - lines.append("%s%s" % (" "*(indent+2*depth), node)) - return "\n".join(lines) + lines.append('%s%s' % (' '*(indent+2*depth), node)) + return '\n'.join(lines) -BUGDIR_ID = "DIR" -BUGDIR_TYPE_NEW = SubscriptionType("new") -BUGDIR_TYPE_MOD = SubscriptionType("mod") -BUGDIR_TYPE_REM = SubscriptionType("rem") -BUGDIR_TYPE_ALL = SubscriptionType("all", +BUGDIR_ID = 'DIR' +BUGDIR_TYPE_NEW = SubscriptionType('new') +BUGDIR_TYPE_MOD = SubscriptionType('mod') +BUGDIR_TYPE_REM = SubscriptionType('rem') +BUGDIR_TYPE_ALL = SubscriptionType('all', [BUGDIR_TYPE_NEW, BUGDIR_TYPE_MOD, BUGDIR_TYPE_REM]) # same name as BUGDIR_TYPE_ALL for consistency BUG_TYPE_ALL = SubscriptionType(str(BUGDIR_TYPE_ALL)) -INVALID_TYPE = SubscriptionType("INVALID") +INVALID_TYPE = SubscriptionType('INVALID') class InvalidType (ValueError): def __init__(self, type_name, type_root): - msg = "Invalid type %s for tree:\n%s" \ + msg = 'Invalid type %s for tree:\n%s' \ % (type_name, type_root.string_tree(4)) ValueError.__init__(self, msg) self.type_name = type_name @@ -109,7 +109,7 @@ class Subscription (object): def __str__(self): return str(self.type) def __repr__(self): - return "" % (self.id, self.type) + return '' % (self.id, self.type) def subscriptions_from_string(string=None, subscription_sep=',', id_sep=':'): """ @@ -137,39 +137,39 @@ def subscriptions_from_string(string=None, subscription_sep=',', id_sep=':'): class DiffTree (libbe.util.tree.Tree): """ A tree holding difference data for easy report generation. - >>> bugdir = DiffTree("bugdir") - >>> bdsettings = DiffTree("settings", data="target: None -> 1.0") + >>> bugdir = DiffTree('bugdir') + >>> bdsettings = DiffTree('settings', data='target: None -> 1.0') >>> bugdir.append(bdsettings) - >>> bugs = DiffTree("bugs", "bug-count: 5 -> 6") + >>> bugs = DiffTree('bugs', 'bug-count: 5 -> 6') >>> bugdir.append(bugs) - >>> new = DiffTree("new", "new bugs: ABC, DEF") + >>> new = DiffTree('new', 'new bugs: ABC, DEF') >>> bugs.append(new) - >>> rem = DiffTree("rem", "removed bugs: RST, UVW") + >>> rem = DiffTree('rem', 'removed bugs: RST, UVW') >>> bugs.append(rem) >>> print bugdir.report_string() target: None -> 1.0 bug-count: 5 -> 6 new bugs: ABC, DEF removed bugs: RST, UVW - >>> print "\\n".join(bugdir.paths()) + >>> print '\\n'.join(bugdir.paths()) bugdir bugdir/settings bugdir/bugs bugdir/bugs/new bugdir/bugs/rem - >>> bugdir.child_by_path("/") == bugdir + >>> bugdir.child_by_path('/') == bugdir True - >>> bugdir.child_by_path("/bugs") == bugs + >>> bugdir.child_by_path('/bugs') == bugs True - >>> bugdir.child_by_path("/bugs/rem") == rem + >>> bugdir.child_by_path('/bugs/rem') == rem True - >>> bugdir.child_by_path("bugdir") == bugdir + >>> bugdir.child_by_path('bugdir') == bugdir True - >>> bugdir.child_by_path("bugdir/") == bugdir + >>> bugdir.child_by_path('bugdir/') == bugdir True - >>> bugdir.child_by_path("bugdir/bugs") == bugs + >>> bugdir.child_by_path('bugdir/bugs') == bugs True - >>> bugdir.child_by_path("/bugs").masked = True + >>> bugdir.child_by_path('/bugs').masked = True >>> print bugdir.report_string() target: None -> 1.0 """ @@ -186,17 +186,17 @@ class DiffTree (libbe.util.tree.Tree): if parent_path == None: path = self.name else: - path = "%s/%s" % (parent_path, self.name) + path = '%s/%s' % (parent_path, self.name) paths.append(path) for child in self: paths.extend(child.paths(path)) return paths def child_by_path(self, path): - if hasattr(path, "split"): # convert string path to a list of names - names = path.split("/") - if names[0] == "": + if hasattr(path, 'split'): # convert string path to a list of names + names = path.split('/') + if names[0] == '': names[0] = self.name # replace root with self - if len(names) > 1 and names[-1] == "": + if len(names) > 1 and names[-1] == '': names = names[:-1] # strip empty tail else: # it was already an array names = path @@ -209,7 +209,7 @@ class DiffTree (libbe.util.tree.Tree): return child.child_by_path(names[1:]) if len(names) == 1: raise KeyError, "%s doesn't match '%s'" % (names, self.name) - raise KeyError, "%s points to child not in %s" % (names, [c.name for c in self]) + raise KeyError, '%s points to child not in %s' % (names, [c.name for c in self]) def report_string(self): report = self.report() if report == None: @@ -239,13 +239,13 @@ class DiffTree (libbe.util.tree.Tree): def data_part(self, depth, indent=True): if self.data == None: return None - if hasattr(self, "_cached_data_part"): + if hasattr(self, '_cached_data_part'): return self._cached_data_part data_part = self.data_part_fn(self.data) if indent == True: data_part_lines = data_part.splitlines() - indent = " "*(depth) - line_sep = "\n"+indent + indent = ' '*(depth) + line_sep = '\n'+indent data_part = indent+line_sep.join(data_part_lines) self._cached_data_part = data_part return data_part @@ -254,21 +254,21 @@ class Diff (object): """ Difference tree generator for BugDirs. >>> import copy - >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) - >>> bd.user_id = "John Doe " + >>> bd = libbe.bugdir.SimpleBugDir(memory=True) >>> bd_new = copy.deepcopy(bd) - >>> bd_new.target = "1.0" - >>> a = bd_new.bug_from_uuid("a") + >>> bd_new.target = '1.0' + >>> a = bd_new.bug_from_uuid('a') >>> rep = a.comment_root.new_reply("I'm closing this bug") - >>> rep.uuid = "acom" - >>> rep.date = "Thu, 01 Jan 1970 00:00:00 +0000" - >>> a.status = "closed" - >>> b = bd_new.bug_from_uuid("b") + >>> rep.uuid = 'acom' + >>> rep.author = 'John Doe ' + >>> rep.date = 'Thu, 01 Jan 1970 00:00:00 +0000' + >>> a.status = 'closed' + >>> b = bd_new.bug_from_uuid('b') >>> bd_new.remove_bug(b) - >>> c = bd_new.new_bug("c", "Bug C") + >>> c = bd_new.new_bug('Bug C', _uuid='c') >>> d = Diff(bd, bd_new) >>> r = d.report_tree() - >>> print "\\n".join(r.paths()) + >>> print '\\n'.join(r.paths()) bugdir bugdir/settings bugdir/bugs @@ -288,11 +288,11 @@ class Diff (object): Changed bug directory settings: target: None -> 1.0 New bugs: - c:om: Bug C + abc/c:om: Bug C Removed bugs: - b:cm: Bug B + abc/b:cm: Bug B Modified bugs: - a:cm: Bug A + abc/a:cm: Bug A Changed bug settings: status: open -> closed New comments: @@ -307,9 +307,9 @@ class Diff (object): >>> r = d.report_tree(subscriptions) >>> print r.report_string() New bugs: - c:om: Bug C + abc/c:om: Bug C Removed bugs: - b:cm: Bug B + abc/b:cm: Bug B While sending subscriptions to report_tree() makes the report generation more efficient (because you may not need to compare @@ -320,10 +320,10 @@ class Diff (object): >>> d.full_report() >>> print d.report_tree([subscriptions[0]]).report_string() New bugs: - c:om: Bug C + abc/c:om: Bug C >>> print d.report_tree([subscriptions[1]]).report_string() Removed bugs: - b:cm: Bug B + abc/b:cm: Bug B >>> bd.cleanup() """ @@ -357,9 +357,9 @@ class Diff (object): for s in subscriptions: if s.id != BUGDIR_ID: try: - bug = self.new_bugdir.bug_from_shortname(s.id) - except bugdir.NoBugMatches: - bug = self.old_bugdir.bug_from_shortname(s.id) + bug = self.new_bugdir.bug_from_uuid(s.id) + except libbe.bugdir.NoBugMatches: + bug = self.old_bugdir.bug_from_uuid(s.id) subscribed_bugs.append(bug.uuid) new_uuids.extend([s for s in subscribed_bugs if self.new_bugdir.has_bug(s)]) @@ -383,9 +383,9 @@ class Diff (object): if BUGDIR_TYPE_ALL in bugdir_types \ or BUGDIR_TYPE_MOD in bugdir_types \ or uuid in subscribed_bugs: - if old_bug.sync_with_disk == True: + if old_bug.storage != None and old_bug.storage.is_readable(): old_bug.load_comments() - if new_bug.sync_with_disk == True: + if new_bug.storage != None and new_bug.storage.is_readable(): new_bug.load_comments() if old_bug != new_bug: modified.append((old_bug, new_bug)) @@ -406,7 +406,7 @@ class Diff (object): (added_comments, modified_comments, removed_comments) analogous to ._changed_bugs. """ - if hasattr(self, "__changed_comments"): + if hasattr(self, '__changed_comments'): if new.uuid in self.__changed_comments: return self.__changed_comments[new.uuid] else: @@ -454,13 +454,12 @@ class Diff (object): properties = sorted(new.settings_properties) for p in hidden_properties: properties.remove(p) - attributes = [settings_object.setting_name_to_attr_name(None, p) + attributes = [setting_name_to_attr_name(None, p) for p in properties] return self._attribute_changes(old, new, attributes) def _bugdir_attribute_changes(self): return self._settings_properties_attribute_changes( \ - self.old_bugdir, self.new_bugdir, - ["vcs_name"]) # tweaked by bugdir.duplicate_bugdir + self.old_bugdir, self.new_bugdir) def _bug_attribute_changes(self, old, new): return self._settings_properties_attribute_changes(old, new) def _comment_attribute_changes(self, old, new): @@ -530,65 +529,64 @@ class Diff (object): if subscriptions == None: subscriptions = [Subscription(BUGDIR_ID, BUGDIR_TYPE_ALL)] bugdir_settings = sorted(self.new_bugdir.settings_properties) - bugdir_settings.remove("vcs_name") # tweaked by bugdir.duplicate_bugdir - root = diff_tree("bugdir") + root = diff_tree('bugdir') bugdir_subscriptions = [s.type for s in subscriptions if s.id == BUGDIR_ID] if BUGDIR_TYPE_ALL in bugdir_subscriptions: bugdir_attribute_changes = self._bugdir_attribute_changes() if len(bugdir_attribute_changes) > 0: - bugdir = diff_tree("settings", bugdir_attribute_changes, + bugdir = diff_tree('settings', bugdir_attribute_changes, self.bugdir_attribute_change_string) root.append(bugdir) - bug_root = diff_tree("bugs") + bug_root = diff_tree('bugs') root.append(bug_root) add,mod,rem = self._changed_bugs(subscriptions) - bnew = diff_tree("new", "New bugs:", requires_children=True) + bnew = diff_tree('new', 'New bugs:', requires_children=True) bug_root.append(bnew) for bug in add: b = diff_tree(bug.uuid, bug, self.bug_add_string) bnew.append(b) - brem = diff_tree("rem", "Removed bugs:", requires_children=True) + brem = diff_tree('rem', 'Removed bugs:', requires_children=True) bug_root.append(brem) for bug in rem: b = diff_tree(bug.uuid, bug, self.bug_rem_string) brem.append(b) - bmod = diff_tree("mod", "Modified bugs:", requires_children=True) + bmod = diff_tree('mod', 'Modified bugs:', requires_children=True) bug_root.append(bmod) for old,new in mod: b = diff_tree(new.uuid, (old,new), self.bug_mod_string) bmod.append(b) bug_attribute_changes = self._bug_attribute_changes(old, new) if len(bug_attribute_changes) > 0: - bset = diff_tree("settings", bug_attribute_changes, + bset = diff_tree('settings', bug_attribute_changes, self.bug_attribute_change_string) b.append(bset) if old.summary != new.summary: data = (old.summary, new.summary) - bsum = diff_tree("summary", data, self.bug_summary_change_string) + bsum = diff_tree('summary', data, self.bug_summary_change_string) b.append(bsum) - cr = diff_tree("comments") + cr = diff_tree('comments') b.append(cr) a,m,d = self._changed_comments(old, new) - cnew = diff_tree("new", "New comments:", requires_children=True) + cnew = diff_tree('new', 'New comments:', requires_children=True) for comment in a: c = diff_tree(comment.uuid, comment, self.comment_add_string) cnew.append(c) - crem = diff_tree("rem", "Removed comments:",requires_children=True) + crem = diff_tree('rem', 'Removed comments:',requires_children=True) for comment in d: c = diff_tree(comment.uuid, comment, self.comment_rem_string) crem.append(c) - cmod = diff_tree("mod","Modified comments:",requires_children=True) + cmod = diff_tree('mod','Modified comments:',requires_children=True) for o,n in m: c = diff_tree(n.uuid, (o,n), self.comment_mod_string) cmod.append(c) comm_attribute_changes = self._comment_attribute_changes(o, n) if len(comm_attribute_changes) > 0: - cset = diff_tree("settings", comm_attribute_changes, + cset = diff_tree('settings', comm_attribute_changes, self.comment_attribute_change_string) if o.body != n.body: data = (o.body, n.body) - cbody = diff_tree("cbody", data, + cbody = diff_tree('cbody', data, self.comment_body_change_string) c.append(cbody) cr.extend([cnew, crem, cmod]) @@ -598,19 +596,19 @@ class Diff (object): # Feel free to play with these in subclasses. def attribute_change_string(self, attribute_changes, indent=0): - indent_string = " "*indent - change_strings = [u"%s: %s -> %s" % f for f in attribute_changes] + indent_string = ' '*indent + change_strings = [u'%s: %s -> %s' % f for f in attribute_changes] for i,change_string in enumerate(change_strings): change_strings[i] = indent_string+change_string - return u"\n".join(change_strings) + return u'\n'.join(change_strings) def bugdir_attribute_change_string(self, attribute_changes): - return "Changed bug directory settings:\n%s" % \ + return 'Changed bug directory settings:\n%s' % \ self.attribute_change_string(attribute_changes, indent=1) def bug_attribute_change_string(self, attribute_changes): - return "Changed bug settings:\n%s" % \ + return 'Changed bug settings:\n%s' % \ self.attribute_change_string(attribute_changes, indent=1) def comment_attribute_change_string(self, attribute_changes): - return "Changed comment settings:\n%s" % \ + return 'Changed comment settings:\n%s' % \ self.attribute_change_string(attribute_changes, indent=1) def bug_add_string(self, bug): return bug.string(shortlist=True) @@ -621,17 +619,17 @@ class Diff (object): return new_bug.string(shortlist=True) def bug_summary_change_string(self, summaries): old_summary,new_summary = summaries - return "summary changed:\n %s\n %s" % (old_summary, new_summary) + return 'summary changed:\n %s\n %s' % (old_summary, new_summary) def _comment_summary_string(self, comment): - return "from %s on %s" % (comment.author, time_to_str(comment.time)) + return 'from %s on %s' % (comment.author, time_to_str(comment.time)) def comment_add_string(self, comment): summary = self._comment_summary_string(comment) first_line = comment.body.splitlines()[0] - return "%s\n %s..." % (summary, first_line) + return '%s\n %s...' % (summary, first_line) def comment_rem_string(self, comment): summary = self._comment_summary_string(comment) first_line = comment.body.splitlines()[0] - return "%s\n %s..." % (summary, first_line) + return '%s\n %s...' % (summary, first_line) def comment_mod_string(self, comments): old_comment,new_comment = comments return self._comment_summary_string(new_comment) -- cgit From 380889988b6d7881c4e0b5968053f85676d27211 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 15 Dec 2009 04:32:19 -0500 Subject: Fixed libbe.command.subscribe --- libbe/command/diff.py | 2 +- libbe/command/subscribe.py | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/libbe/command/diff.py b/libbe/command/diff.py index 6a7c36b..c8b5777 100644 --- a/libbe/command/diff.py +++ b/libbe/command/diff.py @@ -74,7 +74,7 @@ class Diff (libbe.command.Command): completion_callback=libbe.command.util.complete_path)), libbe.command.Option(name='subscribe', short_name='s', help='Only print changes matching SUBSCRIPTION, ' - 'subscription is a comma-separ\ated list of ID:TYPE ' + 'subscription is a comma-separated list of ID:TYPE ' 'tuples. See `be subscribe --help` for descriptions ' 'of ID and TYPE.', arg=libbe.command.Argument( diff --git a/libbe/command/subscribe.py b/libbe/command/subscribe.py index e86a9c8..4f72624 100644 --- a/libbe/command/subscribe.py +++ b/libbe/command/subscribe.py @@ -22,6 +22,7 @@ import libbe.bug import libbe.command import libbe.diff import libbe.command.util +import libbe.util.id import libbe.util.tree @@ -54,19 +55,19 @@ class Subscribe (libbe.command.Command): Jane Doe all a.com,b.net John Doe all * >>> ret = cmd.run({'subscriber':'Jane Doe ', 'servers':'a.edu'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: + Subscriptions for abc/a: Jane Doe all a.com,a.edu,b.net John Doe all * >>> ret = cmd.run({'unsubscribe':True, 'subscriber':'Jane Doe ', 'servers':'a.com'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: + Subscriptions for abc/a: Jane Doe all a.edu,b.net John Doe all * >>> ret = cmd.run({'subscriber':'Jane Doe ', 'servers':'*'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: + Subscriptions for abc/a: Jane Doe all * John Doe all * >>> ret = cmd.run({'unsubscribe':True, 'subscriber':'Jane Doe '}, ['/a']) # doctest: +NORMALIZE_WHITESPACE - Subscriptions for a: + Subscriptions for abc/a: John Doe all * >>> ret = cmd.run({'unsubscribe':True, 'subscriber':'John Doe '}, ['/a']) >>> ret = cmd.run({'subscriber':'Jane Doe ', 'types':'new'}, ['DIR']) # doctest: +NORMALIZE_WHITESPACE @@ -103,7 +104,7 @@ class Subscribe (libbe.command.Command): ]) self.args.extend([ libbe.command.Argument( - name='id', metavar='ID', default=None, + name='id', metavar='ID', default=tuple(), optional=True, repeatable=True, completion_callback=libbe.command.util.complete_bug_comment_id), ]) @@ -131,17 +132,17 @@ class Subscribe (libbe.command.Command): servers = params['servers'].split(',') types = params['types'].split(',') - if params['id'] == None: - params['id'] = libbe.diff.BUGDIR_ID - for id in params['id']: - if id == libbe.diff.BUGDIR_ID: # directory-wide subscriptions + if len(params['id']) == 0: + params['id'] = [libbe.diff.BUGDIR_ID] + for _id in params['id']: + if _id == libbe.diff.BUGDIR_ID: # directory-wide subscriptions type_root = libbe.diff.BUGDIR_TYPE_ALL entity = bugdir entity_name = 'bug directory' else: # bug-specific subscriptions type_root = libbe.diff.BUG_TYPE_ALL bug,dummy_comment = libbe.command.util.bug_comment_from_user_id( - bugdir, params['id']) + bugdir, _id) entity = bug entity_name = bug.id.user() if params['list-all'] == True: -- cgit From 89b7a1411e4658e831f5d635534b24355dbb941d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 15 Dec 2009 06:44:20 -0500 Subject: Fixed libbe.command.diff + ugly BugDir.duplicate_bugdir implementation duplicate_bugdir() works, but for the vcs backends, it could require shelling out for _every_ file read. This could, and probably will, be horribly slow. Still it works ;). I'm not sure what a better implementation would be. The old implementation checked out the entire earlier state into a temporary directory pros: single shell out, simple upgrade implementation cons: wouldn't work well for HTTP backens I think a good solution would run along the lines of the currently commented out code in duplicate_bugdir(), where a VersionedStorage.changed_since(revision) call would give you a list of changed files. diff could work off of that directly, without the need to generate a whole duplicate bugdir. I'm stuck on how to handle upgrades though... Also removed trailing whitespace from all python files. --- libbe/bug.py | 27 +++++--- libbe/bugdir.py | 125 ++++++++++++++++++++-------------- libbe/command/base.py | 4 +- libbe/command/commit.py | 27 +++----- libbe/command/diff.py | 26 +++---- libbe/command/html.py | 2 +- libbe/command/import_xml.py | 12 ++-- libbe/command/list.py | 6 +- libbe/command/merge.py | 10 +-- libbe/command/set.py | 4 +- libbe/command/severity.py | 2 +- libbe/command/show.py | 4 +- libbe/command/status.py | 2 +- libbe/command/subscribe.py | 6 +- libbe/command/tag.py | 2 +- libbe/command/target.py | 2 +- libbe/command/util.py | 2 +- libbe/comment.py | 46 +++++++------ libbe/diff.py | 4 +- libbe/storage/base.py | 10 +-- libbe/storage/util/config.py | 2 +- libbe/storage/util/mapfile.py | 48 ++++++++----- libbe/storage/util/settings_object.py | 5 +- libbe/storage/util/upgrade.py | 36 +++++----- libbe/storage/vcs/arch.py | 4 +- libbe/storage/vcs/base.py | 5 +- libbe/storage/vcs/bzr.py | 6 +- libbe/storage/vcs/darcs.py | 6 +- libbe/storage/vcs/git.py | 4 +- libbe/storage/vcs/hg.py | 2 +- libbe/ui/command_line.py | 2 +- libbe/util/encoding.py | 2 +- libbe/util/id.py | 10 +-- libbe/util/plugin.py | 2 +- libbe/util/subproc.py | 8 +-- libbe/util/utility.py | 6 +- 36 files changed, 250 insertions(+), 221 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index da9a1a2..6ab4d78 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -24,6 +24,7 @@ import copy import os import os.path import errno +import sys import time import types try: # import core module, Python >= 2.5 @@ -170,7 +171,7 @@ class Bug(settings_object.SavedSettingsObject): check_fn=lambda s: s in status_values, require_save=True) def status(): return {} - + @property def active(self): return self.status in active_status_values @@ -249,7 +250,7 @@ class Bug(settings_object.SavedSettingsObject): if self.bugdir != None: self.storage = self.bugdir.storage if from_storage == False: - if self.storage != None and self.storage.is_writeable(): + if self.storage != None and self.storage.is_writeable(): self.save() def __repr__(self): @@ -294,7 +295,7 @@ class Bug(settings_object.SavedSettingsObject): severitychar = self.severity[0] chars = "%c%c" % (statuschar, severitychar) bugout = "%s:%s: %s" % (self.id.user(),chars,self.summary.rstrip('\n')) - + if show_comments == True: self.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True) comout = self.comment_root.string_thread(flatten=False) @@ -398,7 +399,7 @@ class Bug(settings_object.SavedSettingsObject): self.explicit_attrs.append(attr_name) setattr(self, attr_name, text) elif verbose == True: - print >> sys.stderr, "Ignoring unknown tag %s in %s" \ + print >> sys.stderr, 'Ignoring unknown tag %s in %s' \ % (child.tag, comment.tag) if uuid != self.uuid: if not hasattr(self, 'alt_id') or self.alt_id == None: @@ -492,7 +493,7 @@ class Bug(settings_object.SavedSettingsObject): except KeyError: if ignore_missing_references == True: print >> sys.stderr, \ - "Ignoring missing reference to %s" % c.in_reply_to + 'Ignoring missing reference to %s' % c.in_reply_to parent = default_parent if parent.uuid != comment.INVALID_UUID: c.in_reply_to = parent.uuid @@ -628,7 +629,11 @@ class Bug(settings_object.SavedSettingsObject): if settings_mapfile == None: settings_mapfile = \ self.storage.get(self.id.storage("values"), default="\n") - self.settings = mapfile.parse(settings_mapfile) + try: + self.settings = mapfile.parse(settings_mapfile) + except mapfile.InvalidMapfileContents, e: + raise Exception('Invalid settings file for bug %s\n' + '(BE version missmatch?)' % self.id.user()) self._setup_saved_settings() def save_settings(self): @@ -639,7 +644,7 @@ class Bug(settings_object.SavedSettingsObject): """ Save any loaded contents to storage. Because of lazy loading of comments, this is actually not too inefficient. - + However, if self.storage.is_writeable() == True, then any changes are automatically written to storage as soon as they happen, so calling this method will just waste time (unless @@ -666,14 +671,14 @@ class Bug(settings_object.SavedSettingsObject): # next _get_comment_root returns a fresh version. Turn of # writing temporarily so we don't write our blank comment # tree to disk. - w = self.storage.writeable + w = self.storage.writeable self.storage.writeable = False self.comment_root = None self.storage.writeable = w def remove(self): self.storage.recursive_remove(self.id.storage()) - + # methods for managing comments def uuids(self): @@ -770,7 +775,7 @@ def cmp_attr(bug_1, bug_2, attr, invert=False): val_2 = getattr(bug_2, attr) if val_1 == None: val_1 = None if val_2 == None: val_2 = None - + if invert == True : return -cmp(val_1, val_2) else : @@ -816,7 +821,7 @@ class BugCompoundComparator (object): if val != 0 : return val return 0 - + cmp_full = BugCompoundComparator() diff --git a/libbe/bugdir.py b/libbe/bugdir.py index defa250..9d90a70 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -190,7 +190,11 @@ class BugDir (list, settings_object.SavedSettingsObject): if settings_mapfile == None: settings_mapfile = \ self.storage.get(self.id.storage('settings'), default='\n') - self.settings = mapfile.parse(settings_mapfile) + try: + self.settings = mapfile.parse(settings_mapfile) + except mapfile.InvalidMapfileContents, e: + raise Exception('Invalid settings file for bugdir %s\n' + '(BE version missmatch?)' % self.id.user()) self._setup_saved_settings() #self._setup_user_id(self.user_id) self._setup_severities(self.severities) @@ -291,52 +295,66 @@ class BugDir (list, settings_object.SavedSettingsObject): Duplicate bugdirs are read-only copies used for generating diffs between revisions. """ - dbd = copy.copy(self) - dbd.storage = copy.copy(self.storage) - dbd._bug_map = copy.copy(self._bug_map) - dbd.storage.writeable = False - added,changed,removed = self.storage.changed_since(revision) - for id in added: - pass - for id in removed: - pass - for id in changed: - parsed = libbe.util.id.parse_id(id) - if parsed['type'] == 'bugdir': - assert parsed['remaining'] == ['settings'], parsed['remaining'] - dbd._settings = copy.copy(self._settings) - mf = self.storage.get(self.id.storage('settings'), default='\n', - revision=revision) - dbd.load_settings(mf) - else: - if parsed['bug'] not in self: - self._load_bug(parsed['bug']) - dbd._load_bug(parsed['bug']) - else: - bug = copy.copy(self._bug_map[parsed['bug']]) - bug.settings = copy.copy(bug.settings) - dbd._bug_map[parsed['bug']] = bug - if parsed['type'] == 'bug': - assert parsed['remaining'] == ['values'], parsed['remaining'] - mf = self.storage.get(self.id.storage('values'), default='\n', - revision=revision) - bug.load_settings(mf) - elif parsed['type'] == 'comment': - assert parsed['remaining'] in [['values'], ['body']], \ - parsed['remaining'] - bug.comment_root = copy.deepcopy(bug.comment_root) - comment = bug.comment_from_uuid(parsed['comment']) - if parsed['remaining'] == ['values']: - mf = self.storage.get(self.id.storage('values'), default='\n', - revision=revision) - comment.load_settings(mf) - else: - body = self.storage.get(self.id.storage('body'), default='\n', - revision=revision) - comment.body = body - else: - assert 1==0, 'Unkown type "%s" for id "%s"' % (type, id) - dbd.storage.readable = False # so we won't read in added bugs, etc. + s = copy.deepcopy(self.storage) + s.writeable = False + class RevisionedStorageGet (object): + def __init__(self, storage, default_revision): + self.s = storage + self.sget = self.s.get + self.r = default_revision + def get(self, *args, **kwargs): + if not 'revision' in kwargs or kwargs['revision'] == None: + kwargs['revision'] = self.r + return self.sget(*args, **kwargs) + rsg = RevisionedStorageGet(s, revision) + s.get = rsg.get + dbd = BugDir(s, from_storage=True) +# dbd = copy.copy(self) +# dbd.storage = copy.copy(self.storage) +# dbd._bug_map = copy.copy(self._bug_map) +# dbd.storage.writeable = False +# added,changed,removed = self.storage.changed_since(revision) +# for id in added: +# pass +# for id in removed: +# pass +# for id in changed: +# parsed = libbe.util.id.parse_id(id) +# if parsed['type'] == 'bugdir': +# assert parsed['remaining'] == ['settings'], parsed['remaining'] +# dbd._settings = copy.copy(self._settings) +# mf = self.storage.get(self.id.storage('settings'), default='\n', +# revision=revision) +# dbd.load_settings(mf) +# else: +# if parsed['bug'] not in self: +# self._load_bug(parsed['bug']) +# dbd._load_bug(parsed['bug']) +# else: +# bug = copy.copy(self._bug_map[parsed['bug']]) +# bug.settings = copy.copy(bug.settings) +# dbd._bug_map[parsed['bug']] = bug +# if parsed['type'] == 'bug': +# assert parsed['remaining'] == ['values'], parsed['remaining'] +# mf = self.storage.get(self.id.storage('values'), default='\n', +# revision=revision) +# bug.load_settings(mf) +# elif parsed['type'] == 'comment': +# assert parsed['remaining'] in [['values'], ['body']], \ +# parsed['remaining'] +# bug.comment_root = copy.deepcopy(bug.comment_root) +# comment = bug.comment_from_uuid(parsed['comment']) +# if parsed['remaining'] == ['values']: +# mf = self.storage.get(self.id.storage('values'), default='\n', +# revision=revision) +# comment.load_settings(mf) +# else: +# body = self.storage.get(self.id.storage('body'), default='\n', +# revision=revision) +# comment.body = body +# else: +# assert 1==0, 'Unkown type "%s" for id "%s"' % (type, id) +# dbd.storage.readable = False # so we won't read in added bugs, etc. return dbd if libbe.TESTING == True: @@ -350,13 +368,16 @@ if libbe.TESTING == True: ['a', 'b'] >>> bugdir.cleanup() """ - def __init__(self, memory=True): + def __init__(self, memory=True, versioned=False): if memory == True: storage = None else: dir = utility.Dir() self._dir_ref = dir # postpone cleanup since dir.cleanup() removes dir. - storage = libbe.storage.base.Storage(dir.path) + if versioned == False: + storage = libbe.storage.base.Storage(dir.path) + else: + storage = libbe.storage.base.VersionedStorage(dir.path) storage.init() storage.connect() BugDir.__init__(self, storage=storage, uuid='abc123') @@ -384,7 +405,7 @@ if libbe.TESTING == True: self.storage.disconnect() self.storage.connect() self._clear_bugs() - + # class BugDirTestCase(unittest.TestCase): # def setUp(self): # self.dir = utility.Dir() @@ -486,7 +507,7 @@ if libbe.TESTING == True: # "Invalid comment: %d\n%s" % (index, comment)) # def testSyncedComments(self): # self.testComments(sync_with_disk=True) - + class SimpleBugDirTestCase (unittest.TestCase): def setUp(self): # create a pre-existing bugdir in a temporary directory @@ -542,7 +563,7 @@ if libbe.TESTING == True: uuids = sorted([bug.uuid for bug in bugdir]) self.failUnless(uuids == [], uuids) bugdir.cleanup() - + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/command/base.py b/libbe/command/base.py index 9f50632..6a49413 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -263,7 +263,7 @@ class Command (object): self.stdout = codecs.getwriter(output_encoding)(sys.stdout) self.stdout.encoding = output_encoding - def help(self, *args): + def help(self, *args): return '\n\n'.join([self._usage(), self._option_help(), self._long_help().rstrip('\n')]) @@ -345,7 +345,7 @@ class Command (object): def _get_storage(self): """ Callback for use by commands that need it. - + Note that with the current implementation, _get_unconnected_storage() will not work after this method runs, but that shouldn't be an issue for any command I can diff --git a/libbe/command/commit.py b/libbe/command/commit.py index 4ef619c..7d82e7d 100644 --- a/libbe/command/commit.py +++ b/libbe/command/commit.py @@ -27,30 +27,19 @@ import libbe.ui.util.editor class Commit (libbe.command.Command): """Commit the currently pending changes to the repository - >>> import os, sys - >>> import libbe.storage.vcs - >>> import libbe.storage.vcs.base - >>> import libbe.util.utility + >>> import sys + >>> import libbe.bugdir + >>> bd = libbe.bugdir.SimpleBugDir(memory=False, versioned=True) >>> cmd = Commit() + >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout - >>> dir = libbe.util.utility.Dir() - >>> vcs = libbe.storage.vcs.installed_vcs() - >>> vcs.repo = dir.path - >>> vcs.init() - >>> vcs.connect() - >>> cmd._storage = vcs - >>> if vcs.name in libbe.storage.vcs.base.VCS_ORDER: - ... bd = libbe.bugdir.BugDir(vcs, from_storage=False) - ... bd.extra_strings = ['hi there'] - ... cmd.run({'user-id':'Joe'}, ['Making a commit']) # doctest: +ELLIPSIS - ... else: - ... print 'Committed ...' + >>> bd.extra_strings = ['hi there'] + >>> bd.flush_reload() + >>> cmd.run({'user-id':'Joe'}, ['Making a commit']) # doctest: +ELLIPSIS Committed ... - >>> vcs.disconnect() - >>> vcs.destroy() - >>> dir.cleanup() + >>> bd.cleanup() """ name = 'commit' diff --git a/libbe/command/diff.py b/libbe/command/diff.py index c8b5777..d945f96 100644 --- a/libbe/command/diff.py +++ b/libbe/command/diff.py @@ -29,7 +29,7 @@ class Diff (libbe.command.Command): >>> import sys >>> import libbe.bugdir - >>> bd = libbe.bugdir.SimpleBugDir(memory=False) + >>> bd = libbe.bugdir.SimpleBugDir(memory=False, versioned=True) >>> cmd = Diff() >>> cmd._storage = bd.storage >>> cmd._setup_io = lambda i_enc,o_enc : None @@ -39,23 +39,15 @@ class Diff (libbe.command.Command): >>> bug = bd.bug_from_uuid('a') >>> bug.status = 'closed' >>> changed = bd.storage.commit('Closed bug a') - >>> if bd.storage.versioned == True: - ... ret = cmd.run(args=[original]) - ... else: - ... print 'Modified bugs:\\n a:cm: Bug A\\n Changed bug settings:\\n status: open -> closed' + >>> ret = cmd.run(args=[original]) Modified bugs: - a:cm: Bug A + abc/a:cm: Bug A Changed bug settings: status: open -> closed - >>> if bd.storage.versioned == True: - ... ret = cmd.run({'subscribe':'%(bugdir_id)s:mod', 'uuids':True}, [original]) - ... else: - ... print 'a' + >>> ret = cmd.run({'subscribe':'%(bugdir_id)s:mod', 'uuids':True}, [original]) a - >>> if bd.storage.versioned == False: - ... ret = cmd.run(args=[original]) - ... else: - ... raise libbe.command.UserError('This repository not revision-controlled.') + >>> bd.storage.versioned = False + >>> ret = cmd.run(args=[original]) Traceback (most recent call last): ... UserError: This repository is not revision-controlled. @@ -101,7 +93,7 @@ class Diff (libbe.command.Command): if params['repo'] == None: if params['revision'] == None: # get the most recent revision params['revision'] = bugdir.storage.revision_id(-1) - old_bd = bugdir.duplicate_bugdir(params['revision']) # TODO + old_bd = bugdir.duplicate_bugdir(params['revision']) else: old_storage = libbe.storage.get_storage(params['repo']) old_storage.connect() @@ -113,8 +105,8 @@ class Diff (libbe.command.Command): raise libbe.command.UserError( '%s is not revision-controlled.' % storage.repo) - old_bd = old_bd_current.duplicate_bugdir(revision) # TODO - d = libbe.diff.Diff(old_bd, bugir) + old_bd = old_bd_current.duplicate_bugdir(revision) + d = libbe.diff.Diff(old_bd, bugdir) tree = d.report_tree(subscriptions) if params['uuids'] == True: diff --git a/libbe/command/html.py b/libbe/command/html.py index 0f993ae..ec818c0 100644 --- a/libbe/command/html.py +++ b/libbe/command/html.py @@ -60,7 +60,7 @@ class HTML (libbe.command.Command): >>> bd.cleanup() """ name = 'html' - + def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) self.options.extend([ diff --git a/libbe/command/import_xml.py b/libbe/command/import_xml.py index e73d90f..2e96848 100644 --- a/libbe/command/import_xml.py +++ b/libbe/command/import_xml.py @@ -110,9 +110,9 @@ class Import_XML (libbe.command.Command): new.explicit_attrs = [] else: croot_bug,croot_comment = (None, None) - + if params['xml-file'] == '-': - xml = self.stdin.read().encode(self.stdin.encoding) + xml = self.stdin.read().encode(self.stdin.encoding) else: self._check_restricted_access(storage, params['xml-file']) xml = libbe.util.encoding.get_file_contents( @@ -147,7 +147,7 @@ class Import_XML (libbe.command.Command): else: print >> sys.stderr, 'ignoring unknown tag %s in %s' \ % (child.tag, comment_list.tag) - + # merge the new root_comments if params['add-only'] == True: accept_changes = False @@ -172,7 +172,7 @@ class Import_XML (libbe.command.Command): croot_bug.merge(new_croot_bug, accept_changes=accept_changes, accept_extra_strings=accept_extra_strings, accept_comments=accept_comments) - + # merge the new croot_bugs merged_bugs = [] old_bugs = [] @@ -190,7 +190,7 @@ class Import_XML (libbe.command.Command): accept_comments=accept_comments) merged_bugs.append(new) old_bugs.append(old) - + # protect against programmer error causing data loss: if croot_bug != None: comms = [c.uuid for c in croot_comment.traverse()] @@ -201,7 +201,7 @@ class Import_XML (libbe.command.Command): if not new in merged_bugs: assert bugdir.has_bug(new.uuid), \ "bug %s wasn't added" % (new.uuid) - + # save new information bugdir.storage.writeable = writeable if croot_bug != None: diff --git a/libbe/command/list.py b/libbe/command/list.py index 8bdeaae..d48c7ee 100644 --- a/libbe/command/list.py +++ b/libbe/command/list.py @@ -25,7 +25,7 @@ import libbe.bug import libbe.command import libbe.command.util -# get a list of * for cmp_*() comparing two bugs. +# get a list of * for cmp_*() comparing two bugs. AVAILABLE_CMPS = [fn[4:] for fn in dir(libbe.bug) if fn[:4] == 'cmp_'] AVAILABLE_CMPS.remove('attr') # a cmp_* template. @@ -129,7 +129,7 @@ class List (libbe.command.Command): # parser.add_option(short, long, action="store_true", # dest=attr, help=help, default=False) # return parser -# +# # ]) def _run(self, **params): @@ -144,7 +144,7 @@ class List (libbe.command.Command): self.result = bugs if len(bugs) == 0 and params['xml'] == False: print >> self.stdout, "No matching bugs found" - + # sort bugs bugs = self._sort_bugs(bugs, cmp_list) diff --git a/libbe/command/merge.py b/libbe/command/merge.py index e3bf943..447b4ae 100644 --- a/libbe/command/merge.py +++ b/libbe/command/merge.py @@ -63,8 +63,8 @@ class Merge (libbe.command.Command): Short name : abc/a Severity : minor Status : open - Assigned : - Reporter : + Assigned : + Reporter : Creator : John Doe Created : ... Bug A @@ -109,8 +109,8 @@ class Merge (libbe.command.Command): Short name : abc/b Severity : minor Status : closed - Assigned : - Reporter : + Assigned : + Reporter : Creator : Jane Doe Created : ... Bug B @@ -168,7 +168,7 @@ class Merge (libbe.command.Command): comment.storage = None comment.alt_id = comment.uuid comment.storage = bugdir.storage - comment.uuid = libbe.util.id.uuid_gen() + comment.uuid = libbe.util.id.uuid_gen() comment.save() # force onto disk under bugA for comment in newCommTree: # just the child comments diff --git a/libbe/command/set.py b/libbe/command/set.py index aaf2b58..4fe0117 100644 --- a/libbe/command/set.py +++ b/libbe/command/set.py @@ -87,11 +87,11 @@ class Set (libbe.command.Command): def _long_help(self): return """ -Show or change per-tree settings. +Show or change per-tree settings. If name and value are supplied, the name is set to a new value. If no value is specified, the current value is printed. -If no arguments are provided, all names and values are listed. +If no arguments are provided, all names and values are listed. To unset a setting, set it to "none". diff --git a/libbe/command/severity.py b/libbe/command/severity.py index 9289138..7c1d305 100644 --- a/libbe/command/severity.py +++ b/libbe/command/severity.py @@ -59,7 +59,7 @@ class Severity (libbe.command.Command): repeatable=True, completion_callback=libbe.command.util.complete_bug_id), ]) - + def _run(self, **params): bugdir = self._get_bugdir() for bug_id in params['bug-id']: diff --git a/libbe/command/show.py b/libbe/command/show.py index 1a569a6..1b498aa 100644 --- a/libbe/command/show.py +++ b/libbe/command/show.py @@ -45,8 +45,8 @@ class Show (libbe.command.Command): Short name : abc/a Severity : minor Status : open - Assigned : - Reporter : + Assigned : + Reporter : Creator : John Doe Created : ... Bug A diff --git a/libbe/command/status.py b/libbe/command/status.py index 7cf5858..323963a 100644 --- a/libbe/command/status.py +++ b/libbe/command/status.py @@ -56,7 +56,7 @@ class Status (libbe.command.Command): repeatable=True, completion_callback=libbe.command.util.complete_bug_id), ]) - + def _run(self, **params): bugdir = self._get_bugdir() for bug_id in params['bug-id']: diff --git a/libbe/command/subscribe.py b/libbe/command/subscribe.py index 4f72624..a837f99 100644 --- a/libbe/command/subscribe.py +++ b/libbe/command/subscribe.py @@ -131,7 +131,7 @@ class Subscribe (libbe.command.Command): params['types'] = 'all' servers = params['servers'].split(',') types = params['types'].split(',') - + if len(params['id']) == 0: params['id'] = [libbe.diff.BUGDIR_ID] for _id in params['id']: @@ -159,7 +159,7 @@ class Subscribe (libbe.command.Command): else: # add the tag estrs = subscribe(estrs, subscriber, types, servers, type_root) entity.extra_strings = estrs # reassign to notice change - + if params['list-all'] == True: bugdir.load_all_bugs() subscriptions = get_bugdir_subscribers(bugdir, servers[0]) @@ -168,7 +168,7 @@ class Subscribe (libbe.command.Command): for estr in entity.extra_strings: if estr.startswith(TAG): subscriptions.append(estr[len(TAG):]) - + if len(subscriptions) > 0: print >> self.stdout, 'Subscriptions for %s:' % entity_name print >> self.stdout, '\n'.join(subscriptions) diff --git a/libbe/command/tag.py b/libbe/command/tag.py index 26ff1b5..bdb3f31 100644 --- a/libbe/command/tag.py +++ b/libbe/command/tag.py @@ -134,7 +134,7 @@ class Tag (libbe.command.Command): for estr in bug.extra_strings: if estr.startswith(TAG_TAG): tags.append(estr[len(TAG_TAG):]) - + if len(tags) > 0: print "Tags for %s:" % bug.id.user() print '\n'.join(tags) diff --git a/libbe/command/target.py b/libbe/command/target.py index 034c532..df836db 100644 --- a/libbe/command/target.py +++ b/libbe/command/target.py @@ -58,7 +58,7 @@ class Target (libbe.command.Command): >>> bd.cleanup() """ name = 'target' - + def __init__(self, *args, **kwargs): libbe.command.Command.__init__(self, *args, **kwargs) self.options.extend([ diff --git a/libbe/command/util.py b/libbe/command/util.py index a4aaf5f..3bd02d0 100644 --- a/libbe/command/util.py +++ b/libbe/command/util.py @@ -32,7 +32,7 @@ def complete_path(command, argument, fragment=None): if len(comps) == 1 and os.path.isdir(comps[0]): comps.extend(glob.glob(comps[0]+'/*')) return comps - + def complete_status(command, argument, fragment=None): return [fragment] def complete_severity(command, argument, fragment=None): diff --git a/libbe/comment.py b/libbe/comment.py index 3b8a9c7..d899aa8 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -190,18 +190,18 @@ class Comment(Tree, settings_object.SavedSettingsObject): Set from_storage=False to create a new comment. The uuid option is required when from_storage==True. - + The in_reply_to and body options are only used if from_storage==False (the default). When from_storage==True, they are loaded from the bug database. - + in_reply_to should be the uuid string of the parent comment. """ Tree.__init__(self) settings_object.SavedSettingsObject.__init__(self) self.bug = bug self.storage = None - self.uuid = uuid + self.uuid = uuid self.id = libbe.util.id.ID(self, 'comment') if from_storage == False: if uuid == None: @@ -214,7 +214,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): if self.bug != None: self.storage = self.bug.storage if from_storage == False: - if self.storage != None and self.storage.is_writeable(): + if self.storage != None and self.storage.is_writeable(): self.save() def __cmp__(self, other): @@ -368,7 +368,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): self.body = base64.decodestring(body) self.extra_strings = estrs - def merge(self, other, accept_changes=True, + def merge(self, other, accept_changes=True, accept_extra_strings=True, change_exception=False): """ Merge info from other into this comment. Overrides any @@ -448,7 +448,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> print comm.string(indent=2) --------- Comment --------- Name: //abc - From: + From: Date: Thu, 01 Jan 1970 00:00:00 +0000 Some @@ -468,7 +468,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): lines.extend(body.splitlines()) else: lines.append("Content type %s not printable. Try XML output instead" % self.content_type) - + istring = ' '*indent sep = '\n' + istring return istring + sep.join(lines).rstrip('\n') @@ -478,12 +478,12 @@ class Comment(Tree, settings_object.SavedSettingsObject): """ Return a string displaying a thread of comments. bug_shortname is only used if auto_name_map == True. - + string_method_name (defaults to "string") is the name of the Comment method used to generate the output string for each Comment in the thread. The method must take the arguments indent and shortname. - + SIDE-EFFECT: if auto_name_map==True, calls comment_shortnames() which will sort the tree by comment.time. Avoid by calling name_map = {} @@ -507,50 +507,50 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> print a.string_thread(flatten=True) --------- Comment --------- Name: //a - From: + From: Date: Thu, 20 Nov 2008 01:00:00 +0000 Insightful remarks --------- Comment --------- Name: //b - From: + From: Date: Thu, 20 Nov 2008 02:00:00 +0000 Critique original comment --------- Comment --------- Name: //c - From: + From: Date: Thu, 20 Nov 2008 03:00:00 +0000 Begin flamewar :p --------- Comment --------- Name: //d - From: + From: Date: Thu, 20 Nov 2008 04:00:00 +0000 Useful examples >>> print a.string_thread() --------- Comment --------- Name: //a - From: + From: Date: Thu, 20 Nov 2008 01:00:00 +0000 Insightful remarks --------- Comment --------- Name: //b - From: + From: Date: Thu, 20 Nov 2008 02:00:00 +0000 Critique original comment --------- Comment --------- Name: //c - From: + From: Date: Thu, 20 Nov 2008 03:00:00 +0000 Begin flamewar :p --------- Comment --------- Name: //d - From: + From: Date: Thu, 20 Nov 2008 04:00:00 +0000 Useful examples @@ -571,7 +571,11 @@ class Comment(Tree, settings_object.SavedSettingsObject): if settings_mapfile == None: settings_mapfile = \ self.storage.get(self.id.storage("values"), default="\n") - self.settings = mapfile.parse(settings_mapfile) + try: + self.settings = mapfile.parse(settings_mapfile) + except mapfile.InvalidMapfileContents, e: + raise Exception('Invalid settings file for comment %s\n' + '(BE version missmatch?)' % self.id.user()) self._setup_saved_settings() def save_settings(self): @@ -581,7 +585,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): def save(self): """ Save any loaded contents to storage. - + However, if self.storage.is_writeable() == True, then any changes are automatically written to storage as soon as they happen, so calling this method will just waste time (unless @@ -688,7 +692,7 @@ def cmp_attr(comment_1, comment_2, attr, invert=False): val_2 = getattr(comment_2, attr) if val_1 == None: val_1 = None if val_2 == None: val_2 = None - + if invert == True : return -cmp(val_1, val_2) else : @@ -718,7 +722,7 @@ class CommentCompoundComparator (object): if val != 0 : return val return 0 - + cmp_full = CommentCompoundComparator() if libbe.TESTING == True: diff --git a/libbe/diff.py b/libbe/diff.py index 32e8836..f82dbfa 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -90,9 +90,9 @@ class Subscription (object): def __init__(self, id, subscription_type, **kwargs): if 'type_root' not in kwargs: if id == BUGDIR_ID: - kwargs['type_root'] = BUGDIR_TYPE_ALL + kwargs['type_root'] = BUGDIR_TYPE_ALL else: - kwargs['type_root'] = BUG_TYPE_ALL + kwargs['type_root'] = BUG_TYPE_ALL if type(subscription_type) in types.StringTypes: subscription_type = type_from_name(subscription_type, **kwargs) self.id = id diff --git a/libbe/storage/base.py b/libbe/storage/base.py index dd35586..97c8b29 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -256,8 +256,10 @@ class Storage (object): else: decode = False value = self._get(*args, **kwargs) - if decode == True: + if decode == True and type(value) != types.UnicodeType: return unicode(value, self.encoding) + if decode == False and type(value) != types.StringType: + return value.encode(self.encoding) return value def _get(self, id, default=InvalidObject, revision=None): @@ -673,7 +675,7 @@ if TESTING == True: self.failUnless(s == val, "%s.get() returned %s not %s" % (vars(self.Class)['name'], s, self.val)) - + class Storage_persistence_TestCase (StorageTestCase): """Test cases for Storage.disconnect and .connect methods.""" @@ -767,7 +769,7 @@ if TESTING == True: revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), self.commit_body)) for i in range(10): - rev = self.s.revision_id(i+1) + rev = self.s.revision_id(i+1) self.failUnless(rev == revs[i], "%s.revision_id(%d) returned %s not %s" % (vars(self.Class)['name'], i+1, rev, revs[i])) @@ -794,7 +796,7 @@ if TESTING == True: self.failUnless(ret == val(i), "%s.get() returned %s not %s for revision %s" % (vars(self.Class)['name'], ret, val(i), revs[i])) - + def make_storage_testcase_subclasses(storage_class, namespace): """Make StorageTestCase subclasses for storage_class in namespace.""" storage_testcase_classes = [ diff --git a/libbe/storage/util/config.py b/libbe/storage/util/config.py index a0a252e..9f95d14 100644 --- a/libbe/storage/util/config.py +++ b/libbe/storage/util/config.py @@ -46,7 +46,7 @@ def set_val(name, value, section="DEFAULT", encoding=None): if encoding == None: encoding = default_encoding config = ConfigParser.ConfigParser() - if os.path.exists(path()) == False: # touch file or config + if os.path.exists(path()) == False: # touch file or config open(path(), 'w').close() # read chokes on missing file f = codecs.open(path(), 'r', encoding) config.readfp(f, path()) diff --git a/libbe/storage/util/mapfile.py b/libbe/storage/util/mapfile.py index a8d5516..35ae1a0 100644 --- a/libbe/storage/util/mapfile.py +++ b/libbe/storage/util/mapfile.py @@ -24,6 +24,7 @@ independent/conflicting changes. import errno import os.path +import types import yaml import libbe @@ -39,32 +40,37 @@ class IllegalKey(Exception): class IllegalValue(Exception): def __init__(self, value): Exception.__init__(self, 'Illegal value "%s"' % value) - self.value = value + self.value = value + +class InvalidMapfileContents(Exception): + def __init__(self, contents): + Exception.__init__(self, 'Invalid YAML contents') + self.contents = contents def generate(map): """Generate a YAML mapfile content string. - >>> generate({"q":"p"}) + >>> generate({'q':'p'}) 'q: p\\n\\n' - >>> generate({"q":u"Fran\u00e7ais"}) + >>> generate({'q':u'Fran\u00e7ais'}) 'q: Fran\\xc3\\xa7ais\\n\\n' - >>> generate({"q":u"hello"}) + >>> generate({'q':u'hello'}) 'q: hello\\n\\n' - >>> generate({"q=":"p"}) + >>> generate({'q=':'p'}) Traceback (most recent call last): IllegalKey: Illegal key "q=" - >>> generate({"q:":"p"}) + >>> generate({'q:':'p'}) Traceback (most recent call last): IllegalKey: Illegal key "q:" - >>> generate({"q\\n":"p"}) + >>> generate({'q\\n':'p'}) Traceback (most recent call last): IllegalKey: Illegal key "q\\n" - >>> generate({"":"p"}) + >>> generate({'':'p'}) Traceback (most recent call last): IllegalKey: Illegal key "" - >>> generate({">q":"p"}) + >>> generate({'>q':'p'}) Traceback (most recent call last): IllegalKey: Illegal key ">q" - >>> generate({"q":"p\\n"}) + >>> generate({'q':'p\\n'}) Traceback (most recent call last): IllegalValue: Illegal value "p\\n" """ @@ -97,20 +103,28 @@ def parse(contents): 'p' >>> parse('q: \\'p\\'\\n\\n')['q'] 'p' - >>> contents = generate({"a":"b", "c":"d", "e":"f"}) + >>> contents = generate({'a':'b', 'c':'d', 'e':'f'}) >>> dict = parse(contents) - >>> dict["a"] + >>> dict['a'] 'b' - >>> dict["c"] + >>> dict['c'] 'd' - >>> dict["e"] + >>> dict['e'] 'f' - >>> contents = generate({"q":u"Fran\u00e7ais"}) + >>> contents = generate({'q':u'Fran\u00e7ais'}) >>> dict = parse(contents) - >>> dict["q"] + >>> dict['q'] u'Fran\\xe7ais' + >>> dict = parse('a!') + Traceback (most recent call last): + ... + InvalidMapfileContents: Invalid YAML contents """ - return yaml.load(contents) or {} + c = yaml.load(contents) + if type(c) == types.StringType: + raise InvalidMapfileContents( + 'Unable to parse YAML (BE format missmatch?):\n\n%s' % contents) + return c or {} if libbe.TESTING == True: suite = doctest.DocTestSuite() diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py index 760df03..8b86829 100644 --- a/libbe/storage/util/settings_object.py +++ b/libbe/storage/util/settings_object.py @@ -197,9 +197,8 @@ class SavedSettingsObject(object): settings as primed. """ for property in self.settings_properties: - if property not in self.settings: - self.settings[property] = EMPTY - elif self.settings[property] == UNPRIMED: + if property not in self.settings \ + or self.settings[property] == UNPRIMED: self.settings[property] = EMPTY if flag_as_loaded == True: self._settings_loaded = True diff --git a/libbe/storage/util/upgrade.py b/libbe/storage/util/upgrade.py index 7ef760e..c94f171 100644 --- a/libbe/storage/util/upgrade.py +++ b/libbe/storage/util/upgrade.py @@ -24,17 +24,16 @@ import sys import libbe import libbe.bug as bug -import libbe.util.encoding as encoding import libbe.storage.util.mapfile as mapfile +import libbe.util.encoding as encoding +import libbe.util.id -if libbe.TESTING == True: - import doctest # a list of all past versions -BUGDIR_DISK_VERSIONS = ["Bugs Everywhere Tree 1 0", - "Bugs Everywhere Directory v1.1", - "Bugs Everywhere Directory v1.2", - "Bugs Everywhere Directory v1.3"] +BUGDIR_DISK_VERSIONS = ['Bugs Everywhere Tree 1 0', + 'Bugs Everywhere Directory v1.1', + 'Bugs Everywhere Directory v1.2', + 'Bugs Everywhere Directory v1.3'] # the current version BUGDIR_DISK_VERSION = BUGDIR_DISK_VERSIONS[-1] @@ -43,13 +42,17 @@ class Upgrader (object): "Class for converting between different on-disk BE storage formats." initial_version = None final_version = None - def __init__(self, root): - self.root = root + def __init__(self, repo): + self.repo = repo - def get_path(self, *args): + def get_path(self, id): """ - Return a path relative to .root. + Return a path relative to .repo. """ + if id == 'version': + return os.path.join(self.repo, id) + +TODO dir = os.path.join(self.root, '.be') if len(args) == 0: return dir @@ -58,15 +61,15 @@ class Upgrader (object): def check_initial_version(self): path = self.get_path('version') - version = self.vcs.get_file_contents(path).rstrip('\n') + version = encoding.get_file_contents(path).rstrip('\n') assert version == self.initial_version, version def set_version(self): - path = self.get_path("version") - self.vcs.set_file_contents(path, self.final_version+"\n") + path = self.get_path('version') + encoding.set_file_contents(path, self.final_version+'\n') def upgrade(self): - print >> sys.stderr, "upgrading bugdir from '%s' to '%s'" \ + print >> sys.stderr, 'upgrading bugdir from "%s" to "%s"' \ % (self.initial_version, self.final_version) self.check_initial_version() self.set_version() @@ -237,6 +240,3 @@ def upgrade(path, current_version, if version_b == target_version: break i += 1 - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() diff --git a/libbe/storage/vcs/arch.py b/libbe/storage/vcs/arch.py index 8afdca9..f1b5b7b 100644 --- a/libbe/storage/vcs/arch.py +++ b/libbe/storage/vcs/arch.py @@ -167,7 +167,7 @@ class Arch(base.VCS): http://regexps.srparish.net/tutorial-tla/naming-conventions.html Since our bug directory '.be' doesn't satisfy these conventions, we need to adjust them. - + The conventions are specified in project-root/{arch}/=tagging-method """ @@ -211,7 +211,7 @@ class Arch(base.VCS): dirname = path status,output,error = self._u_invoke_client('tree-root', dirname) root = output.rstrip('\n') - + self._get_archive_project_name(root) return root diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 768a85f..3bdb4ac 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -195,6 +195,9 @@ class CachedPathID (object): id = self.id(dirpath) relpath = dirpath[len(self._root)+1:] if id.count('/') == 0: + if id in self._cache: + import sys + print >> sys.stderr, 'Multiple paths for %s: \n %s\n %s' % (id, self._cache[id], relpath) self._cache[id] = relpath except InvalidPath: pass @@ -521,7 +524,7 @@ os.listdir(self.get_path("bugs")): dumping VCS-specific files into the .be directory. If you do need to implement this method (e.g. Arch), set - self.interspersed_vcs_files = True + self.interspersed_vcs_files = True """ assert self.interspersed_vcs_files == False raise NotImplementedError diff --git a/libbe/storage/vcs/bzr.py b/libbe/storage/vcs/bzr.py index 04cc6c1..6f3e840 100644 --- a/libbe/storage/vcs/bzr.py +++ b/libbe/storage/vcs/bzr.py @@ -49,7 +49,7 @@ class Bzr(base.VCS): def _vcs_version(self): status,output,error = self._u_invoke_client('--version') - return output + return output def _vcs_get_user_id(self): status,output,error = self._u_invoke_client('whoami') @@ -88,7 +88,7 @@ class Bzr(base.VCS): return base.VCS._vcs_get_file_contents(self, path, revision) else: status,output,error = \ - self._u_invoke_client('cat', '-r', revision,path) + self._u_invoke_client('cat', '-r', revision, path) return output def _vcs_commit(self, commitfile, allow_empty=False): @@ -123,7 +123,7 @@ class Bzr(base.VCS): return str(index) # bzr commit 0 is the empty tree. return str(current_revision+index+1) - + 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 97e31ff..9a371d9 100644 --- a/libbe/storage/vcs/darcs.py +++ b/libbe/storage/vcs/darcs.py @@ -76,7 +76,7 @@ class Darcs(base.VCS): def _vcs_detect(self, path): if self._u_search_parent_directories(path, "_darcs") != None : return True - return False + return False def _vcs_root(self, path): """Find the root of the deepest repository containing path.""" @@ -129,7 +129,7 @@ class Darcs(base.VCS): 'diff', '--unified', '--patch', revision, path, unicode_output=False) target_patch = output - + # '--output -' to be supported in GNU patch > 2.5.9 # but that hasn't been released as of June 30th, 2009. @@ -206,7 +206,7 @@ class Darcs(base.VCS): except IndexError: return None - + 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 29abda7..8d1b529 100644 --- a/libbe/storage/vcs/git.py +++ b/libbe/storage/vcs/git.py @@ -76,7 +76,7 @@ class Git(base.VCS): def _vcs_detect(self, path): if self._u_search_parent_directories(path, '.git') != None : return True - return False + return False def _vcs_root(self, path): """Find the root of the deepest repository containing path.""" @@ -154,7 +154,7 @@ class Git(base.VCS): except IndexError: return None - + 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 7e0643b..d2d3281 100644 --- a/libbe/storage/vcs/hg.py +++ b/libbe/storage/vcs/hg.py @@ -124,7 +124,7 @@ class Hg(base.VCS): return id return None - + if libbe.TESTING == True: base.make_vcs_testcase_subclasses(Hg, sys.modules[__name__]) diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py index ce0e55e..6eead67 100755 --- a/libbe/ui/command_line.py +++ b/libbe/ui/command_line.py @@ -259,7 +259,7 @@ def main(): if options['no-pager'] == True: paginate = 'never' libbe.ui.util.pager.run_pager(paginate) - + command_name = args[0] try: module = libbe.command.get_command(command_name) diff --git a/libbe/util/encoding.py b/libbe/util/encoding.py index dcc41f8..7706105 100644 --- a/libbe/util/encoding.py +++ b/libbe/util/encoding.py @@ -72,7 +72,7 @@ def get_file_contents(path, mode='r', encoding=None, decode=False): encoding = get_filesystem_encoding() f = codecs.open(path, mode, encoding) else: - f = open(path, mode) + f = open(path, mode) contents = f.read() f.close() return contents diff --git a/libbe/util/id.py b/libbe/util/id.py index 3838259..adc827c 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -50,7 +50,7 @@ except ImportError: q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) else: # win32 don't have os.execvp() so have to run command in a shell - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True, cwd=cwd) except OSError, e : strerror = "%s\nwhile executing %s" % (e.args[1], args) @@ -210,7 +210,7 @@ def child_uuids(child_storage_ids): fields = _split(id) if len(fields) == 1: yield fields[0] - + REGEXP = '#([-a-f0-9]*)(/[-a-g0-9]*)?(/[-a-g0-9]*)?#' @@ -298,7 +298,7 @@ if libbe.TESTING == True: self._siblings = siblings def sibling_uuids(self): return self._siblings - + class IDtestCase(unittest.TestCase): def setUp(self): self.bugdir = DummyObject('1234abcd') @@ -342,11 +342,11 @@ if libbe.TESTING == True: self.bug = DummyObject('abcdef', ['a1234', 'ab9876']) self.bug.bugdir = self.bugdir self.bugdir.bug_from_uuid = lambda uuid: self.bug - self.bugdir.uuids = lambda : self.bug.sibling_uuids() + [self.bug.uuid] + self.bugdir.uuids = lambda : self.bug.sibling_uuids() + [self.bug.uuid] self.comment = DummyObject('12345678', ['1234abcd', '1234cdef']) self.comment.bug = self.bug self.bug.comment_from_uuid = lambda uuid: self.comment - self.bug.uuids = lambda : self.comment.sibling_uuids() + [self.comment.uuid] + self.bug.uuids = lambda : self.comment.sibling_uuids() + [self.comment.uuid] self.bd_id = ID(self.bugdir, 'bugdir') self.b_id = ID(self.bug, 'bug') self.c_id = ID(self.comment, 'comment') diff --git a/libbe/util/plugin.py b/libbe/util/plugin.py index 982c5ca..0326cda 100644 --- a/libbe/util/plugin.py +++ b/libbe/util/plugin.py @@ -57,7 +57,7 @@ def modnames(prefix): >>> 'plugin' in [n for n in modnames('libbe.util')] True """ - components = prefix.split('.') + components = prefix.split('.') modfiles = os.listdir(os.path.join(_PLUGIN_PATH, *components)) modfiles.sort() for modfile in modfiles: diff --git a/libbe/util/subproc.py b/libbe/util/subproc.py index 8806e26..06716b3 100644 --- a/libbe/util/subproc.py +++ b/libbe/util/subproc.py @@ -61,7 +61,7 @@ def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,), else: assert _MSWINDOWS==True, 'invalid platform' # win32 don't have os.execvp() so have to run command in a shell - q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, + q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, shell=True, cwd=cwd) except OSError, e: raise CommandError(args, status=e.args[0], stderr=e) @@ -133,7 +133,7 @@ class Pipe (object): thread.start() threads.append(thread) std_X_arrays.append(stderr_array) - + # also listen to the last processes stdout stdout_array = [] thread = Thread(target=proc._readerthread, @@ -142,11 +142,11 @@ class Pipe (object): thread.start() threads.append(thread) std_X_arrays.append(stdout_array) - + # join threads as they die for thread in threads: thread.join() - + # read output from reader threads std_X_strings = [] for std_X_array in std_X_arrays: diff --git a/libbe/util/utility.py b/libbe/util/utility.py index 779eaa5..31d4c14 100644 --- a/libbe/util/utility.py +++ b/libbe/util/utility.py @@ -51,7 +51,7 @@ def search_parent_directories(path, filename): """ Find the file (or directory) named filename in path or in any of path's parents. - + e.g. search_parent_directories("/a/b/c", ".be") will return the path to the first existing file from @@ -112,7 +112,7 @@ def str_to_time(str_time): time_val = calendar.timegm(time.strptime(str_time, RFC_2822_TIME_FMT)) timesign = -int(timezone_str[0]+"1") # "+" -> time_val ahead of GMT timezone_tuple = time.strptime(timezone_str[1:], "%H%M") - timezone = timezone_tuple.tm_hour*3600 + timezone_tuple.tm_min*60 + timezone = timezone_tuple.tm_hour*3600 + timezone_tuple.tm_min*60 return time_val + timesign*timezone def handy_time(time_val): @@ -153,7 +153,7 @@ def underlined(instring): >>> underlined("Underlined String") 'Underlined String\\n=================' """ - + return "%s\n%s" % (instring, "="*len(instring)) if libbe.TESTING == True: -- cgit From 214c4317bb90684dcfdab4d2402daa66fbad2e77 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 27 Dec 2009 15:58:29 -0500 Subject: Fixed libbe.storage.util.upgrade Note that it only upgrades on-disk versions, so you can't use a non-VCS storage backend whose version isn't your command's current storage version. See #bea/110/bd1# for reasoning. To see the on-disk storage version, look at .be/version To see your command's supported storage version, look at be --full-version I added test_upgrade.sh to exercise the upgrade mechanism on BE's own repository. --- .../bd1207ef-f97e-4078-8c5d-046072012082/body | 45 +++++ .../bd1207ef-f97e-4078-8c5d-046072012082/values | 11 ++ .be/version | 2 +- libbe/bug.py | 4 +- libbe/storage/util/upgrade.py | 207 +++++++++++++++------ libbe/storage/vcs/base.py | 29 ++- libbe/util/id.py | 1 - libbe/version.py | 10 +- test_upgrade.py | 33 ++++ 9 files changed, 264 insertions(+), 78 deletions(-) create mode 100644 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/body create mode 100644 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/values create mode 100755 test_upgrade.py diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/body new file mode 100644 index 0000000..21170a2 --- /dev/null +++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/body @@ -0,0 +1,45 @@ +Some additional thoughts, as I've been developing this idea: + +Different BE storage versions will be difficult to handle. +We currently do disk upgrades via + libbe.storage.util.upgrade +which browses through the .be/ directory, making appropriate changes. + +The new formats know very little about paths, which brought on the +whole libbe.storage.vcs.base.CachedPathID bit. Still, most VCSs +seem to be able to handle renames, e.g. + $ bzr cat -r 200 ./libbe/command/new.py +works, when as of revision 200, the file was + ./becommands/new.py +In fact, bzr recognizes both names: + $ diff <(bzr cat -r 200 ./becommands/new.py) \ + <(bzr cat -r 200 ./libbe/commands/new.py) +returns nothing. Still, I'm not sure this is something we should +require in a storage backend. Which means we'd need to have a +version-dependent id-to-path(version) function. + +We also have the unfortunate situation of duplicate UUIDs from the old + be merge +implemtation. This means that id-to-path is not a well defined +mapping with single-uuid ids. That's ok though, we get a bit uglier +and send the long_user() id into the storage backend instead. While +not so elegant, this will avoid the need for the cached id/path table. + +Ok, you say, we're fine if we have the compound bugdir/bug/comment ids +going out to storage, with the upgrader upgrading the file +appropriately for each file type. Almost. You'll still run into +trouble with upgrades like dir format v1.2 to 1.3 where targets +moved from a per-bug string to a seperate-bugs-with-dependencies. +Now you need to create virtual-target-bugs on the fly when you're +loading the old bugs. Yuck. + +All of this makes me wonder how much we care about being able to +see bug diffs for any repository format older than the current one. +I think that we don't really care ;). After all, the on-disk +format should settle down as BE matures :p. When you _do_ want +to see the long-term history of a particular bug, there's always + bzr log .be/123/bugs/456/values +or the equivalent for your VCS. If access to the raw log ends +up being important, it should be very easy to add + libbe.storage.base.VersionedStorage.log(id) + libbe.command.log diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/values new file mode 100644 index 0000000..f0af48d --- /dev/null +++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082/values @@ -0,0 +1,11 @@ +Author: W. Trevor King + + +Content-type: text/plain + + +Date: Tue, 15 Dec 2009 12:21:11 +0000 + + +In-reply-to: bb406a33-92b6-46dd-950c-c7cfb5440e7b + diff --git a/.be/version b/.be/version index 29baa0e..e7aade4 100644 --- a/.be/version +++ b/.be/version @@ -1 +1 @@ -Bugs Everywhere Directory v1.3 +Bugs Everywhere Directory v1.4 diff --git a/libbe/bug.py b/libbe/bug.py index 6ab4d78..1186ad4 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -628,7 +628,7 @@ 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") + self.storage.get(self.id.storage('values'), default='\n') try: self.settings = mapfile.parse(settings_mapfile) except mapfile.InvalidMapfileContents, e: @@ -638,7 +638,7 @@ class Bug(settings_object.SavedSettingsObject): def save_settings(self): mf = mapfile.generate(self._get_saved_settings()) - self.storage.set(self.id.storage("values"), mf) + self.storage.set(self.id.storage('values'), mf) def save(self): """ diff --git a/libbe/storage/util/upgrade.py b/libbe/storage/util/upgrade.py index c94f171..ce6831d 100644 --- a/libbe/storage/util/upgrade.py +++ b/libbe/storage/util/upgrade.py @@ -23,8 +23,11 @@ import os, os.path import sys import libbe -import libbe.bug as bug +import libbe.bug import libbe.storage.util.mapfile as mapfile +#import libbe.storage.vcs # delay import to avoid cyclic dependency +import libbe.ui.util.editor +import libbe.util import libbe.util.encoding as encoding import libbe.util.id @@ -33,7 +36,9 @@ import libbe.util.id BUGDIR_DISK_VERSIONS = ['Bugs Everywhere Tree 1 0', 'Bugs Everywhere Directory v1.1', 'Bugs Everywhere Directory v1.2', - 'Bugs Everywhere Directory v1.3'] + 'Bugs Everywhere Directory v1.3', + 'Bugs Everywhere Directory v1.4', + ] # the current version BUGDIR_DISK_VERSION = BUGDIR_DISK_VERSIONS[-1] @@ -43,30 +48,37 @@ class Upgrader (object): initial_version = None final_version = None def __init__(self, repo): + import libbe.storage.vcs + self.repo = repo + vcs_name = self._get_vcs_name() + if vcs_name == None: + vcs_name = 'None' + self.vcs = libbe.storage.vcs.vcs_by_name(vcs_name) + self.vcs.repo = self.repo + self.vcs.root() - def get_path(self, id): + def get_path(self, *args): """ - Return a path relative to .repo. + Return the absolute path using args relative to .be. """ - if id == 'version': - return os.path.join(self.repo, id) - -TODO - dir = os.path.join(self.root, '.be') + dir = os.path.join(self.repo, '.be') if len(args) == 0: return dir - assert args[0] in ['version', 'settings', 'bugs'], str(args) return os.path.join(dir, *args) + def _get_vcs_name(self): + return None + def check_initial_version(self): path = self.get_path('version') - version = encoding.get_file_contents(path).rstrip('\n') - assert version == self.initial_version, version + version = encoding.get_file_contents(path, decode=True).rstrip('\n') + assert version == self.initial_version, '%s: %s' % (path, version) def set_version(self): path = self.get_path('version') encoding.set_file_contents(path, self.final_version+'\n') + self.vcs._vcs_update(path) def upgrade(self): print >> sys.stderr, 'upgrading bugdir from "%s" to "%s"' \ @@ -82,11 +94,20 @@ TODO class Upgrade_1_0_to_1_1 (Upgrader): initial_version = "Bugs Everywhere Tree 1 0" final_version = "Bugs Everywhere Directory v1.1" + def _get_vcs_name(self): + path = self.get_path('settings') + settings = encoding.get_file_contents(path) + for line in settings.splitlines(False): + fields = line.split('=') + if len(fields) == 2 and fields[0] == 'rcs_name': + return fields[1] + return None + def _upgrade_mapfile(self, path): - contents = self.vcs.get_file_contents(path) + contents = encoding.get_file_contents(path, decode=True) old_format = False for line in contents.splitlines(): - if len(line.split("=")) == 2: + if len(line.split('=')) == 2: old_format = True break if old_format == True: @@ -105,43 +126,56 @@ class Upgrade_1_0_to_1_1 (Upgrader): contents = '\n'.join(newlines) # load the YAML and save map = mapfile.parse(contents) - mapfile.map_save(self.vcs, path, map) + contents = mapfile.generate(map) + encoding.set_file_contents(path, contents) + self.vcs._vcs_update(path) def _upgrade(self): """ Comment value field "From" -> "Author". Homegrown mapfile -> YAML. """ - path = self.get_path("settings") + path = self.get_path('settings') self._upgrade_mapfile(path) - for bug_uuid in os.listdir(self.get_path("bugs")): - path = self.get_path("bugs", bug_uuid, "values") + for bug_uuid in os.listdir(self.get_path('bugs')): + path = self.get_path('bugs', bug_uuid, 'values') self._upgrade_mapfile(path) - c_path = ["bugs", bug_uuid, "comments"] + c_path = ['bugs', bug_uuid, 'comments'] if not os.path.exists(self.get_path(*c_path)): continue # no comments for this bug for comment_uuid in os.listdir(self.get_path(*c_path)): - path_list = c_path + [comment_uuid, "values"] + path_list = c_path + [comment_uuid, 'values'] path = self.get_path(*path_list) self._upgrade_mapfile(path) - settings = mapfile.map_load(self.vcs, path) - if "From" in settings: - settings["Author"] = settings.pop("From") - mapfile.map_save(self.vcs, path, settings) + settings = mapfile.parse( + encoding.get_file_contents(path)) + if 'From' in settings: + settings['Author'] = settings.pop('From') + encoding.set_file_contents( + path, mapfile.generate(settings)) + self.vcs._vcs_update(path) class Upgrade_1_1_to_1_2 (Upgrader): initial_version = "Bugs Everywhere Directory v1.1" final_version = "Bugs Everywhere Directory v1.2" + def _get_vcs_name(self): + path = self.get_path('settings') + settings = mapfile.parse(encoding.get_file_contents(path)) + if 'rcs_name' in settings: + return settings['rcs_name'] + return None + def _upgrade(self): """ BugDir settings field "rcs_name" -> "vcs_name". """ - path = self.get_path("settings") - settings = mapfile.map_load(self.vcs, path) - if "rcs_name" in settings: - settings["vcs_name"] = settings.pop("rcs_name") - mapfile.map_save(self.vcs, path, settings) + path = self.get_path('settings') + settings = mapfile.parse(encoding.get_file_contents(path)) + if 'rcs_name' in settings: + settings['vcs_name'] = settings.pop('rcs_name') + encoding.set_file_contents(path, mapfile.generate(settings)) + self.vcs._vcs_update(path) class Upgrade_1_2_to_1_3 (Upgrader): initial_version = "Bugs Everywhere Directory v1.2" @@ -149,42 +183,64 @@ class Upgrade_1_2_to_1_3 (Upgrader): def __init__(self, *args, **kwargs): Upgrader.__init__(self, *args, **kwargs) self._targets = {} # key: target text,value: new target bug + + def _get_vcs_name(self): path = self.get_path('settings') - settings = mapfile.map_load(self.vcs, path) + settings = mapfile.parse(encoding.get_file_contents(path)) if 'vcs_name' in settings: - old_vcs = self.vcs - self.vcs = vcs.vcs_by_name(settings['vcs_name']) - self.vcs.root(self.root) - self.vcs.encoding = old_vcs.encoding + return settings['vcs_name'] + return None + + def _save_bug_settings(self, bug): + # The target bugs don't have comments + path = self.get_path('bugs', bug.uuid, 'values') + if not os.path.exists(path): + self.vcs._add_path(path, directory=False) + path = self.get_path('bugs', bug.uuid, 'values') + mf = mapfile.generate(bug._get_saved_settings()) + encoding.set_file_contents(path, mf) + self.vcs._vcs_update(path) def _target_bug(self, target_text): if target_text not in self._targets: - _bug = bug.Bug(bugdir=self, summary=target_text) - # note: we're not a bugdir, but all Bug.save() needs is - # .root, .vcs, and .get_path(), which we have. - _bug.severity = 'target' - self._targets[target_text] = _bug + bug = libbe.bug.Bug(summary=target_text) + bug.severity = 'target' + self._targets[target_text] = bug return self._targets[target_text] def _upgrade_bugdir_mapfile(self): path = self.get_path('settings') - settings = mapfile.map_load(self.vcs, path) + mf = encoding.get_file_contents(path) + if mf == libbe.util.InvalidObject: + return # settings file does not exist + settings = mapfile.parse(mf) if 'target' in settings: settings['target'] = self._target_bug(settings['target']).uuid - mapfile.map_save(self.vcs, path, settings) + mf = mapfile.generate(settings) + encoding.set_file_contents(path, mf) + self.vcs._vcs_update(path) def _upgrade_bug_mapfile(self, bug_uuid): - import becommands.depend + import libbe.command.depend as dep path = self.get_path('bugs', bug_uuid, 'values') - settings = mapfile.map_load(self.vcs, path) + mf = encoding.get_file_contents(path) + if mf == libbe.util.InvalidObject: + return # settings file does not exist + settings = mapfile.parse(mf) if 'target' in settings: target_bug = self._target_bug(settings['target']) - _bug = bug.Bug(bugdir=self, uuid=bug_uuid, from_disk=True) - # note: we're not a bugdir, but all Bug.load_settings() - # needs is .root, .vcs, and .get_path(), which we have. - becommands.depend.add_block(target_bug, _bug) - _bug.settings.pop('target') - _bug.save() + + blocked_by_string = '%s%s' % (dep.BLOCKED_BY_TAG, bug_uuid) + dep._add_remove_extra_string(target_bug, blocked_by_string, add=True) + blocks_string = dep._generate_blocks_string(target_bug) + estrs = settings.get('extra_strings', []) + estrs.append(blocks_string) + settings['extra_strings'] = sorted(estrs) + + settings.pop('target') + mf = mapfile.generate(settings) + encoding.set_file_contents(path, mf) + self.vcs._vcs_update(path) def _upgrade(self): """ @@ -194,12 +250,55 @@ class Upgrade_1_2_to_1_3 (Upgrader): for bug_uuid in os.listdir(self.get_path('bugs')): self._upgrade_bug_mapfile(bug_uuid) self._upgrade_bugdir_mapfile() - for _bug in self._targets.values(): - _bug.save() + for bug in self._targets.values(): + self._save_bug_settings(bug) + +class Upgrade_1_3_to_1_4 (Upgrader): + initial_version = "Bugs Everywhere Directory v1.3" + final_version = "Bugs Everywhere Directory v1.4" + def _get_vcs_name(self): + path = self.get_path('settings') + settings = mapfile.parse(encoding.get_file_contents(path)) + if 'vcs_name' in settings: + return settings['vcs_name'] + return None + + def _upgrade(self): + """ + add new directory "./be/BUGDIR-UUID" + "./be/bugs" -> "./be/BUGDIR-UUID/bugs" + "./be/settings" -> "./be/BUGDIR-UUID/settings" + """ + self.repo = os.path.abspath(self.repo) + basenames = [p for p in os.listdir(self.get_path())] + if not 'bugs' in basenames and not 'settings' in basenames \ + and len([p for p in basenames if len(p)==36]) == 1: + return # the user has upgraded the directory. + basenames = [p for p in basenames if p in ['bugs','settings']] + uuid = libbe.util.id.uuid_gen() + add = [self.get_path(uuid)] + move = [(self.get_path(p), self.get_path(uuid, p)) for p in basenames] + msg = ['Upgrading BE directory version v1.3 to v1.4', + '', + "Because BE's VCS drivers don't support 'move',", + 'please make the following changes with your VCS', + 'and re-run BE. Note that you can choose a different', + 'bugdir UUID to preserve uniformity across branches', + 'of a distributed repository.' + '', + 'add', + ' ' + '\n '.join(add), + 'move', + ' ' + '\n '.join(['%s %s' % (a,b) for a,b in move]), + ] + self.vcs._cached_path_id.destroy() + raise Exception('Need user assistance\n%s' % '\n'.join(msg)) + upgraders = [Upgrade_1_0_to_1_1, Upgrade_1_1_to_1_2, - Upgrade_1_2_to_1_3] + Upgrade_1_2_to_1_3, + Upgrade_1_3_to_1_4] upgrade_classes = {} for upgrader in upgraders: upgrade_classes[(upgrader.initial_version,upgrader.final_version)]=upgrader @@ -213,10 +312,10 @@ def upgrade(path, current_version, """ if current_version not in BUGDIR_DISK_VERSIONS: raise NotImplementedError, \ - "Cannot handle version '%s' yet." % version + "Cannot handle version '%s' yet." % current_version if target_version not in BUGDIR_DISK_VERSIONS: raise NotImplementedError, \ - "Cannot handle version '%s' yet." % version + "Cannot handle version '%s' yet." % current_version if (current_version, target_version) in upgrade_classes: # direct conversion diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 3bdb4ac..a45f1fe 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -40,7 +40,7 @@ from libbe.storage.base import EmptyCommit, InvalidRevision from libbe.util.utility import Dir, search_parent_directories from libbe.util.subproc import CommandError, invoke from libbe.util.plugin import import_by_name -#import libbe.storage.util.upgrade as upgrade +import libbe.storage.util.upgrade as upgrade if libbe.TESTING == True: import unittest @@ -657,8 +657,7 @@ os.listdir(self.get_path("bugs")): def disconnect(self): self._cached_path_id.disconnect() - def _add(self, id, parent=None, directory=False): - path = self._cached_path_id.add_id(id, parent) + def _add_path(self, path, directory=False): relpath = self._u_rel_path(path) reldirs = relpath.split(os.path.sep) if directory == False: @@ -676,6 +675,10 @@ os.listdir(self.get_path("bugs")): open(path, 'w').close() self._vcs_add(self._u_rel_path(path)) + def _add(self, id, parent=None, **kwargs): + path = self._cached_path_id.add_id(id, parent) + self._add_path(path, **kwargs) + def _remove(self, id): path = self._cached_path_id.path(id) if os.path.exists(path): @@ -877,27 +880,17 @@ os.listdir(self.get_path("bugs")): return (summary, body) def check_disk_version(self): - version = self.version() - #if version != upgrade.BUGDIR_DISK_VERSION: - # upgrade.upgrade(self.repo, version) + version = self.disk_version() + if version != upgrade.BUGDIR_DISK_VERSION: + upgrade.upgrade(self.repo, version) def disk_version(self, path=None): """ Requires disk access. """ if path == None: - path = self.get_path('version') - return self.get(path).rstrip('\n') - - def set_disk_version(self): - """ - Requires disk access. - """ - if self.sync_with_disk == False: - raise DiskAccessRequired('set version') - self.vcs.mkdir(self.get_path()) - #self.vcs.set_file_contents(self.get_path("version"), - # upgrade.BUGDIR_DISK_VERSION+"\n") + path = os.path.join(self.repo, '.be', 'version') + return libbe.util.encoding.get_file_contents(path).rstrip('\n') diff --git a/libbe/util/id.py b/libbe/util/id.py index adc827c..6b6b51d 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -172,7 +172,6 @@ class ID (object): assert self._type in HIERARCHY, self._type def storage(self, *args): - import libbe.comment return _assemble(self._object.uuid, *args) def _ancestors(self): diff --git a/libbe/version.py b/libbe/version.py index f8eebbd..1214b3e 100644 --- a/libbe/version.py +++ b/libbe/version.py @@ -23,7 +23,10 @@ be bothered setting version strings" and the "I want complete control over the version strings" workflows. """ +import copy + import libbe._version as _version +import libbe.storage.util.upgrade as upgrade # Manually set a version string (optional, defaults to bzr revision id) #_VERSION = "1.2.3" @@ -39,11 +42,14 @@ def version(verbose=False): else: string = _version.version_info["revision_id"] if verbose == True: + info = copy.copy(_version.version_info) + info['storage'] = upgrade.BUGDIR_DISK_VERSION string += ("\n" "revision: %(revno)d\n" "nick: %(branch_nick)s\n" - "revision id: %(revision_id)s" - % _version.version_info) + "revision id: %(revision_id)s\n" + "storage version: %(storage)s" + % info) return string if __name__ == "__main__": diff --git a/test_upgrade.py b/test_upgrade.py new file mode 100755 index 0000000..40db42a --- /dev/null +++ b/test_upgrade.py @@ -0,0 +1,33 @@ +#!/bin/bash +# +# Test upgrade functionality by checking out revisions with the +# various initial on-disk versions and running `be list` on them to +# force an auto-upgrade. +# +# usage: test_upgrade.sh + +REVS='revid:wking@drexel.edu-20090831063121-85p59rpwoi1mzk3i +revid:wking@drexel.edu-20090831171945-73z3wwt4lrm7zbmu +revid:wking@drexel.edu-20091205224008-z4fed13sd80bj4fe +revid:wking@drexel.edu-20091207123614-okq7i0ahciaupuy9' + +ROOT=$(bzr root) +BE="$ROOT/be" +cd "$ROOT" + +echo "$REVS" | while read REV; do + TMPDIR=$(mktemp --directory --tmpdir "BE-upgrade.XXXXXXXXXX") + REPO="$TMPDIR/repo" + echo "Testing revision: $REV" + echo " Test directory: $REPO" + bzr checkout --lightweight --revision="$REV" "$ROOT" "$TMPDIR/repo" + VERSION=$(cat "$REPO/.be/version") + echo " Version: $VERSION" + $BE --repo "$REPO" list > /dev/null + RET="$?" + rm -rf "$TMPDIR" + if [ $RET -ne 0 ]; then + echo "Error! ($RET)" + exit $RET + fi +done -- cgit From dff704764d77bffbf6cc94c5ba4bb03309da45f8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 27 Dec 2009 16:30:54 -0500 Subject: Added storage.Storage.storage_version() and command.InvalidStorageVersion. Now commands automatically check for storage version compatibility. --- libbe/command/__init__.py | 4 +++- libbe/command/base.py | 13 +++++++++++++ libbe/storage/__init__.py | 14 +++++++++++++- libbe/storage/base.py | 5 +++++ libbe/storage/util/upgrade.py | 24 +++++++----------------- libbe/storage/vcs/base.py | 14 ++++++++------ libbe/version.py | 4 ++-- 7 files changed, 51 insertions(+), 27 deletions(-) diff --git a/libbe/command/__init__.py b/libbe/command/__init__.py index 916b5ce..ab9d2db 100644 --- a/libbe/command/__init__.py +++ b/libbe/command/__init__.py @@ -19,6 +19,7 @@ import base UserError = base.UserError UnknownCommand = base.UnknownCommand +InvalidStorageVersion = base.InvalidStorageVersion get_command = base.get_command get_command_class = base.get_command_class commands = base.commands @@ -26,5 +27,6 @@ Option = base.Option Argument = base.Argument Command = base.Command -__all__ = [UserError, UnknownCommand, get_command, get_command_class, +__all__ = [UserError, UnknownCommand, InvalidStorageVersion, + get_command, get_command_class, commands, Option, Argument, Command] diff --git a/libbe/command/base.py b/libbe/command/base.py index 6a49413..1409c74 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -6,6 +6,7 @@ import os.path import sys import libbe +import libbe.storage import libbe.ui.util.user import libbe.util.encoding import libbe.util.plugin @@ -18,6 +19,15 @@ class UnknownCommand(UserError): Exception.__init__(self, "Unknown command '%s'" % cmd) self.cmd = cmd +class InvalidStorageVersion(UserError): + def __init__(self, active_version, expected_version=None): + if expected_version == None: + expected_version = libbe.storage.STORAGE_VERSION + msg = 'Storage in "%s" not the expected "%s"' \ + % (active_version, expected_version) + UserError.__init__(self, msg) + self.active_version = active_version + self.expected_version = expected_version def get_command(command_name): """Retrieves the module for a user command @@ -354,6 +364,9 @@ class Command (object): if not hasattr(self, '_storage'): self._storage = self._get_unconnected_storage() self._storage.connect() + version = self._storage.storage_version() + if version != libbe.storage.STORAGE_VERSION: + raise InvalidStorageVersion(version) return self._storage def _get_bugdir(self): diff --git a/libbe/storage/__init__.py b/libbe/storage/__init__.py index c58ec34..104b1e1 100644 --- a/libbe/storage/__init__.py +++ b/libbe/storage/__init__.py @@ -10,6 +10,17 @@ NotWriteable = base.NotWriteable NotReadable = base.NotReadable EmptyCommit = base.EmptyCommit +# a list of all past versions +STORAGE_VERSIONS = ['Bugs Everywhere Tree 1 0', + 'Bugs Everywhere Directory v1.1', + 'Bugs Everywhere Directory v1.2', + 'Bugs Everywhere Directory v1.3', + 'Bugs Everywhere Directory v1.4', + ] + +# the current version +STORAGE_VERSION = STORAGE_VERSIONS[-1] + def get_storage(location): """ Return a Storage instance from a repo location string. @@ -21,4 +32,5 @@ def get_storage(location): __all__ = [ConnectionError, InvalidID, InvalidRevision, InvalidDirectory, NotWriteable, NotReadable, - EmptyCommit, get_storage] + EmptyCommit, STORAGE_VERSIONS, STORAGE_VERSION, + get_storage] diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 97c8b29..f32353f 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -10,6 +10,7 @@ import pickle import types from libbe.error import NotSupported +import libbe.storage from libbe.util.tree import Tree from libbe.util import InvalidObject from libbe import TESTING @@ -133,6 +134,10 @@ class Storage (object): """Return a version string for this backend.""" return '0' + def storage_version(self): + """Return the storage format for this backend.""" + return libbe.storage.STORAGE_VERSION + def is_readable(self): return self.readable and self._readable diff --git a/libbe/storage/util/upgrade.py b/libbe/storage/util/upgrade.py index ce6831d..20ef1e4 100644 --- a/libbe/storage/util/upgrade.py +++ b/libbe/storage/util/upgrade.py @@ -25,6 +25,7 @@ import sys import libbe import libbe.bug import libbe.storage.util.mapfile as mapfile +from libbe.storage import STORAGE_VERSIONS, STORAGE_VERSION #import libbe.storage.vcs # delay import to avoid cyclic dependency import libbe.ui.util.editor import libbe.util @@ -32,17 +33,6 @@ import libbe.util.encoding as encoding import libbe.util.id -# a list of all past versions -BUGDIR_DISK_VERSIONS = ['Bugs Everywhere Tree 1 0', - 'Bugs Everywhere Directory v1.1', - 'Bugs Everywhere Directory v1.2', - 'Bugs Everywhere Directory v1.3', - 'Bugs Everywhere Directory v1.4', - ] - -# the current version -BUGDIR_DISK_VERSION = BUGDIR_DISK_VERSIONS[-1] - class Upgrader (object): "Class for converting between different on-disk BE storage formats." initial_version = None @@ -304,16 +294,16 @@ for upgrader in upgraders: upgrade_classes[(upgrader.initial_version,upgrader.final_version)]=upgrader def upgrade(path, current_version, - target_version=BUGDIR_DISK_VERSION): + target_version=STORAGE_VERSION): """ Call the appropriate upgrade function to convert current_version to target_version. If a direct conversion function does not exist, use consecutive conversion functions. """ - if current_version not in BUGDIR_DISK_VERSIONS: + if current_version not in STORAGE_VERSIONS: raise NotImplementedError, \ "Cannot handle version '%s' yet." % current_version - if target_version not in BUGDIR_DISK_VERSIONS: + if target_version not in STORAGE_VERSIONS: raise NotImplementedError, \ "Cannot handle version '%s' yet." % current_version @@ -324,10 +314,10 @@ def upgrade(path, current_version, u.upgrade() else: # consecutive single-step conversion - i = BUGDIR_DISK_VERSIONS.index(current_version) + i = STORAGE_VERSIONS.index(current_version) while True: - version_a = BUGDIR_DISK_VERSIONS[i] - version_b = BUGDIR_DISK_VERSIONS[i+1] + version_a = STORAGE_VERSIONS[i] + version_b = STORAGE_VERSIONS[i+1] try: upgrade_class = upgrade_classes[(version_a, version_b)] except KeyError: diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index a45f1fe..1df08cf 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -34,6 +34,7 @@ import sys import tempfile import libbe +import libbe.storage import libbe.storage.base import libbe.util.encoding from libbe.storage.base import EmptyCommit, InvalidRevision @@ -652,7 +653,7 @@ os.listdir(self.get_path("bugs")): if not os.path.isdir(self.be_dir): raise libbe.storage.base.ConnectionError(self) self._cached_path_id.connect() - self.check_disk_version() + self.check_storage_version() def disconnect(self): self._cached_path_id.disconnect() @@ -879,18 +880,19 @@ os.listdir(self.get_path("bugs")): f.close() return (summary, body) - def check_disk_version(self): - version = self.disk_version() - if version != upgrade.BUGDIR_DISK_VERSION: + def check_storage_version(self): + version = self.storage_version() + if version != libbe.storage.STORAGE_VERSION: upgrade.upgrade(self.repo, version) - def disk_version(self, path=None): + def storage_version(self, path=None): """ Requires disk access. """ if path == None: path = os.path.join(self.repo, '.be', 'version') - return libbe.util.encoding.get_file_contents(path).rstrip('\n') + return libbe.util.encoding.get_file_contents( + path, decode=True).rstrip('\n') diff --git a/libbe/version.py b/libbe/version.py index 1214b3e..ddff5a5 100644 --- a/libbe/version.py +++ b/libbe/version.py @@ -26,7 +26,7 @@ over the version strings" workflows. import copy import libbe._version as _version -import libbe.storage.util.upgrade as upgrade +import libbe.storage # Manually set a version string (optional, defaults to bzr revision id) #_VERSION = "1.2.3" @@ -43,7 +43,7 @@ def version(verbose=False): string = _version.version_info["revision_id"] if verbose == True: info = copy.copy(_version.version_info) - info['storage'] = upgrade.BUGDIR_DISK_VERSION + info['storage'] = libbe.storage.STORAGE_VERSION string += ("\n" "revision: %(revno)d\n" "nick: %(branch_nick)s\n" -- cgit From cfebc238cbda9b6338ec57d5c215c4cbf0246f8b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 27 Dec 2009 16:50:36 -0500 Subject: Moved InvalidStorageVersion from libbe.command to libbe.storage Also added ConnectionError pretty-print to ui.command_line, storage version checking to BugDir.duplicate_bugdir(), and optional revision argument to Storage.storage_version(). --- libbe/bugdir.py | 3 +++ libbe/command/__init__.py | 4 +--- libbe/command/base.py | 12 +----------- libbe/storage/__init__.py | 5 +++-- libbe/storage/base.py | 13 ++++++++++++- libbe/storage/vcs/base.py | 13 +++++++++---- libbe/ui/command_line.py | 4 ++++ 7 files changed, 33 insertions(+), 21 deletions(-) diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 9d90a70..50dc8ba 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -295,6 +295,9 @@ class BugDir (list, settings_object.SavedSettingsObject): Duplicate bugdirs are read-only copies used for generating diffs between revisions. """ + storage_version = self.storage.storage_version(revision) + if storage_version != libbe.storage.STORAGE_VERSION: + raise libbe.storage.InvalidStorageVersion(storage_version) s = copy.deepcopy(self.storage) s.writeable = False class RevisionedStorageGet (object): diff --git a/libbe/command/__init__.py b/libbe/command/__init__.py index ab9d2db..916b5ce 100644 --- a/libbe/command/__init__.py +++ b/libbe/command/__init__.py @@ -19,7 +19,6 @@ import base UserError = base.UserError UnknownCommand = base.UnknownCommand -InvalidStorageVersion = base.InvalidStorageVersion get_command = base.get_command get_command_class = base.get_command_class commands = base.commands @@ -27,6 +26,5 @@ Option = base.Option Argument = base.Argument Command = base.Command -__all__ = [UserError, UnknownCommand, InvalidStorageVersion, - get_command, get_command_class, +__all__ = [UserError, UnknownCommand, get_command, get_command_class, commands, Option, Argument, Command] diff --git a/libbe/command/base.py b/libbe/command/base.py index 1409c74..ac6a58b 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -19,16 +19,6 @@ class UnknownCommand(UserError): Exception.__init__(self, "Unknown command '%s'" % cmd) self.cmd = cmd -class InvalidStorageVersion(UserError): - def __init__(self, active_version, expected_version=None): - if expected_version == None: - expected_version = libbe.storage.STORAGE_VERSION - msg = 'Storage in "%s" not the expected "%s"' \ - % (active_version, expected_version) - UserError.__init__(self, msg) - self.active_version = active_version - self.expected_version = expected_version - def get_command(command_name): """Retrieves the module for a user command @@ -366,7 +356,7 @@ class Command (object): self._storage.connect() version = self._storage.storage_version() if version != libbe.storage.STORAGE_VERSION: - raise InvalidStorageVersion(version) + raise libbe.storage.InvalidStorageVersion(version) return self._storage def _get_bugdir(self): diff --git a/libbe/storage/__init__.py b/libbe/storage/__init__.py index 104b1e1..e99f799 100644 --- a/libbe/storage/__init__.py +++ b/libbe/storage/__init__.py @@ -3,6 +3,7 @@ import base ConnectionError = base.ConnectionError +InvalidStorageVersion = base.InvalidStorageVersion InvalidID = base.InvalidID InvalidRevision = base.InvalidRevision InvalidDirectory = base.InvalidDirectory @@ -30,7 +31,7 @@ def get_storage(location): s.repo = location return s -__all__ = [ConnectionError, InvalidID, InvalidRevision, - InvalidDirectory, NotWriteable, NotReadable, +__all__ = [ConnectionError, InvalidStorageVersion, InvalidID, + InvalidRevision, InvalidDirectory, NotWriteable, NotReadable, EmptyCommit, STORAGE_VERSIONS, STORAGE_VERSION, get_storage] diff --git a/libbe/storage/base.py b/libbe/storage/base.py index f32353f..b43f765 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -26,6 +26,16 @@ if TESTING == True: class ConnectionError (Exception): pass +class InvalidStorageVersion(ConnectionError): + def __init__(self, active_version, expected_version=None): + if expected_version == None: + expected_version = libbe.storage.STORAGE_VERSION + msg = 'Storage in "%s" not the expected "%s"' \ + % (active_version, expected_version) + Exception.__init__(self, msg) + self.active_version = active_version + self.expected_version = expected_version + class InvalidID (KeyError): pass @@ -50,6 +60,7 @@ class EmptyCommit(Exception): def __init__(self): Exception.__init__(self, 'No changes to commit') + class Entry (Tree): def __init__(self, id, value=None, parent=None, directory=False, children=None): @@ -134,7 +145,7 @@ class Storage (object): """Return a version string for this backend.""" return '0' - def storage_version(self): + def storage_version(self, revision=None): """Return the storage format for this backend.""" return libbe.storage.STORAGE_VERSION diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 1df08cf..e96b466 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -32,6 +32,7 @@ import re import shutil import sys import tempfile +import types import libbe import libbe.storage @@ -885,15 +886,19 @@ os.listdir(self.get_path("bugs")): if version != libbe.storage.STORAGE_VERSION: upgrade.upgrade(self.repo, version) - def storage_version(self, path=None): + def storage_version(self, revision=None, path=None): """ Requires disk access. """ if path == None: path = os.path.join(self.repo, '.be', 'version') - return libbe.util.encoding.get_file_contents( - path, decode=True).rstrip('\n') - + if revision == None: # don't require connection + return libbe.util.encoding.get_file_contents( + path, decode=True).rstrip('\n') + contents = self._vcs_get_file_contents(path, revision=revision) + if type(contents) != types.UnicodeType: + contents = unicode(contents, self.encoding) + return contents.strip() if libbe.TESTING == True: diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py index 6eead67..3812789 100755 --- a/libbe/ui/command_line.py +++ b/libbe/ui/command_line.py @@ -284,6 +284,10 @@ def main(): command.cleanup() print 'ERROR:\n', e return 1 + except libbe.storage.ConnectionError, e: + command.cleanup() + print 'Connection Error:\n', e + return 1 command.cleanup() return 0 -- cgit From 83d7624d1deeb73b7f0baddef88069ff27a128ab Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 07:09:18 -0500 Subject: Don't run pager for the 'comment' command. It may need access to the tty for the spawned editor. --- libbe/ui/command_line.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py index 3812789..856f810 100755 --- a/libbe/ui/command_line.py +++ b/libbe/ui/command_line.py @@ -253,13 +253,6 @@ def main(): print 'ERROR:\n', e return 1 - paginate = 'auto' - if options['paginate'] == True: - paginate = 'always' - if options['no-pager'] == True: - paginate = 'never' - libbe.ui.util.pager.run_pager(paginate) - command_name = args[0] try: module = libbe.command.get_command(command_name) @@ -274,6 +267,17 @@ def main(): return libbe.storage.get_storage(self.repo) command = Class(get_unconnected_storage=GUCS(options['repo']), ui=be) parser = CmdOptionParser(command) + + if command.name in ['comment']: + paginate = 'never' + else: + paginate = 'auto' + if options['paginate'] == True: + paginate = 'always' + if options['no-pager'] == True: + paginate = 'never' + libbe.ui.util.pager.run_pager(paginate) + try: options,args = parser.parse_args(args[1:]) command.run(options, args) -- cgit From 292f341109b31c16213146f9184d0b93a425f316 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 07:10:47 -0500 Subject: Add most comments with ignore_missing_references=True. --- libbe/bug.py | 2 +- libbe/comment.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libbe/bug.py b/libbe/bug.py index 1186ad4..66ba579 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -405,7 +405,7 @@ class Bug(settings_object.SavedSettingsObject): if not hasattr(self, 'alt_id') or self.alt_id == None: self.alt_id = uuid self.extra_strings = estrs - self.add_comments(comments) + self.add_comments(comments, ignore_missing_references=True) def add_comment(self, comment, *args, **kwargs): """ diff --git a/libbe/comment.py b/libbe/comment.py index d899aa8..e386796 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -85,7 +85,7 @@ def load_comments(bug, load_full=False): dummy = comm.body # force the body to load comments.append(comm) bug.comment_root = Comment(bug, uuid=INVALID_UUID) - bug.add_comments(comments) + bug.add_comments(comments, ignore_missing_references=True) return bug.comment_root def save_comments(bug): -- cgit From e0c58cc0577fbb1b692e051eabd8597ba35c886a Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 10:06:40 -0500 Subject: libbe.storage.vcs.base.VCS._init() now creates the '.be/version' file. And python test.py libbe.storage.vcs.base passes again. --- libbe/storage/base.py | 9 +++++---- libbe/storage/vcs/base.py | 42 +++++++++++++++++++++++++++++------------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/libbe/storage/base.py b/libbe/storage/base.py index b43f765..9da60ad 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -272,10 +272,11 @@ class Storage (object): else: decode = False value = self._get(*args, **kwargs) - if decode == True and type(value) != types.UnicodeType: - return unicode(value, self.encoding) - if decode == False and type(value) != types.StringType: - return value.encode(self.encoding) + if value != None: + if decode == True and type(value) != types.UnicodeType: + return unicode(value, self.encoding) + elif decode == False and type(value) != types.StringType: + return value.encode(self.encoding) return value def _get(self, id, default=InvalidObject, revision=None): diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index e96b466..cfb39a1 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -38,7 +38,7 @@ import libbe import libbe.storage import libbe.storage.base import libbe.util.encoding -from libbe.storage.base import EmptyCommit, InvalidRevision +from libbe.storage.base import EmptyCommit, InvalidRevision, InvalidID from libbe.util.utility import Dir, search_parent_directories from libbe.util.subproc import CommandError, invoke from libbe.util.plugin import import_by_name @@ -97,11 +97,11 @@ class VCSUnableToRoot (libbe.storage.base.ConnectionError): libbe.storage.base.ConnectionError.__init__(self, msg) self.vcs = vcs -class InvalidPath (libbe.storage.base.InvalidID): +class InvalidPath (InvalidID): def __init__(self, path, root, msg=None): if msg == None: msg = 'Path "%s" not in root "%s"' % (path, root) - libbe.storage.base.InvalidID.__init__(self, msg) + InvalidID.__init__(self, msg) self.path = path self.root = root @@ -111,10 +111,10 @@ class SpacerCollision (InvalidPath): InvalidPath.__init__(self, path, root=None, msg=msg) self.spacer = spacer -class NoSuchFile (libbe.storage.base.InvalidID): +class NoSuchFile (InvalidID): def __init__(self, pathname, root='.'): path = os.path.abspath(os.path.join(root, pathname)) - libbe.storage.base.InvalidID.__init__(self, 'No such file: %s' % path) + InvalidID.__init__(self, 'No such file: %s' % path) class CachedPathID (object): @@ -198,7 +198,6 @@ class CachedPathID (object): relpath = dirpath[len(self._root)+1:] if id.count('/') == 0: if id in self._cache: - import sys print >> sys.stderr, 'Multiple paths for %s: \n %s\n %s' % (id, self._cache[id], relpath) self._cache[id] = relpath except InvalidPath: @@ -240,7 +239,7 @@ class CachedPathID (object): else: extra = fields[1:] if uuid not in self._cache: - raise libbe.storage.base.InvalidID(uuid) + raise InvalidID(uuid) if relpath == True: return os.path.join(self._cache[uuid], *extra) return os.path.join(self._root, self._cache[uuid], *extra) @@ -640,6 +639,7 @@ os.listdir(self.get_path("bugs")): self.root() os.mkdir(self.be_dir) self._vcs_add(self._u_rel_path(self.be_dir)) + self._setup_storage_version() self._cached_path_id.init() def _destroy(self): @@ -723,7 +723,7 @@ os.listdir(self.get_path("bugs")): children[i] = None children.extend([os.path.join(c, c2) for c2 in os.listdir(os.path.join(path, c))]) - elif c == 'id-cache': + elif c in ['id-cache', 'version']: children[i] = None for i,c in enumerate(children): if c == None: continue @@ -738,15 +738,18 @@ os.listdir(self.get_path("bugs")): def _get(self, id, default=libbe.util.InvalidObject, revision=None): try: path = self._cached_path_id.path(id) - except libbe.storage.base.InvalidID, e: + except InvalidID, e: if default == libbe.util.InvalidObject: raise e return default relpath = self._u_rel_path(path) - contents = self._vcs_get_file_contents(relpath,revision) + try: + contents = self._vcs_get_file_contents(relpath,revision) + except InvalidID, e: + raise InvalidID(id) if contents in [libbe.storage.base.InvalidDirectory, libbe.util.InvalidObject]: - raise libbe.storage.base.InvalidID(id) + raise InvalidID(id) elif len(contents) == 0: return None return contents @@ -754,10 +757,10 @@ os.listdir(self.get_path("bugs")): def _set(self, id, value): try: path = self._cached_path_id.path(id) - except libbe.storage.base.InvalidID, e: + except InvalidID, e: raise e if not os.path.exists(path): - raise libbe.storage.base.InvalidID(id) + raise InvalidID(id) if os.path.isdir(path): raise libbe.storage.base.InvalidDirectory(id) f = open(path, "wb") @@ -892,6 +895,8 @@ os.listdir(self.get_path("bugs")): """ if path == None: path = os.path.join(self.repo, '.be', 'version') + if not os.path.exists(path): + raise libbe.storage.InvalidStorageVersion(None) if revision == None: # don't require connection return libbe.util.encoding.get_file_contents( path, decode=True).rstrip('\n') @@ -900,6 +905,17 @@ os.listdir(self.get_path("bugs")): contents = unicode(contents, self.encoding) return contents.strip() + def _setup_storage_version(self): + """ + Requires disk access. + """ + assert self._rooted == True + path = os.path.join(self.be_dir, 'version') + if not os.path.exists(path): + libbe.util.encoding.set_file_contents(path, + libbe.storage.STORAGE_VERSION+'\n') + self._vcs_add(self._u_rel_path(path)) + if libbe.TESTING == True: class VCSTestCase (unittest.TestCase): -- cgit From 7607146d13233dc4ca6c2ed99889ceb43d7298d0 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 10:24:57 -0500 Subject: VersionedStorage_commit_TestCase now allows for versioned files created by self.s.init() --- libbe/storage/base.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 9da60ad..d99c22c 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -737,11 +737,22 @@ if TESTING == True: class VersionedStorage_commit_TestCase (VersionedStorageTestCase): """Test cases for VersionedStorage methods.""" - id = 'I' #unlikely id' - val = 'X' - commit_msg = 'C' #ommitting something interesting' - commit_body = 'B' #ome\nlonger\ndescription\n' + id = 'unlikely id' + val = 'Some value' + commit_msg = 'Committing something interesting' + commit_body = 'Some\nlonger\ndescription\n' + def _setup_for_empty_commit(self): + """ + Initialization might add some files to version control, so + commit those first, before testing the empty commit + functionality. + """ + try: + self.s.commit('Added initialization files') + except EmptyCommit: + pass + def test_revision_id_exception(self): """ Invalid revision id should raise InvalidRevision. @@ -758,6 +769,7 @@ if TESTING == True: """ Empty commit should raise exception. """ + self._setup_for_empty_commit() try: self.s.commit(self.commit_msg, self.commit_body) self.fail( @@ -770,6 +782,7 @@ if TESTING == True: """ Empty commit should _not_ raise exception if allow_empty=True. """ + self._setup_for_empty_commit() self.s.commit(self.commit_msg, self.commit_body, allow_empty=True) -- cgit From f96762deddc0cb6b1380abdcbbe7347ae23f18a1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 10:28:58 -0500 Subject: Bzr storage now based off bzrlib module, not 'bzr' executible. This should make repeated calls to Bzr storage instances _much_ faster, since we avoid repeatedly loading and tearing down a python subprocess. --- libbe/storage/vcs/bzr.py | 114 +++++++++++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 38 deletions(-) diff --git a/libbe/storage/vcs/bzr.py b/libbe/storage/vcs/bzr.py index 6f3e840..4e3f330 100644 --- a/libbe/storage/vcs/bzr.py +++ b/libbe/storage/vcs/bzr.py @@ -22,10 +22,20 @@ Bazaar (bzr) backend. """ +try: + import bzrlib + import bzrlib.branch + import bzrlib.builtins + import bzrlib.config + import bzrlib.errors + import bzrlib.option +except ImportError: + bzrlib = None import os import os.path import re import shutil +import StringIO import libbe import base @@ -41,19 +51,24 @@ def new(): class Bzr(base.VCS): name = 'bzr' - client = 'bzr' + client = None # bzrlib def __init__(self, *args, **kwargs): base.VCS.__init__(self, *args, **kwargs) self.versioned = True def _vcs_version(self): - status,output,error = self._u_invoke_client('--version') - return output + if bzrlib == None: + return None + return bzrlib.__version__ def _vcs_get_user_id(self): - status,output,error = self._u_invoke_client('whoami') - return output.rstrip('\n') + # excerpted from bzrlib.builtins.cmd_whoami.run() + try: + c = bzrlib.branch.Branch.open_containing(self.repo)[0].get_config() + except errors.NotBranchError: + c = bzrlib.config.GlobalConfig() + return c.username() def _vcs_detect(self, path): if self._u_search_parent_directories(path, '.bzr') != None : @@ -62,11 +77,15 @@ class Bzr(base.VCS): def _vcs_root(self, path): """Find the root of the deepest repository containing path.""" - status,output,error = self._u_invoke_client('root', path) - return output.rstrip('\n') + cmd = bzrlib.builtins.cmd_root() + cmd.outf = StringIO.StringIO() + cmd.run(filename=path) + return cmd.outf.getvalue().rstrip('\n') def _vcs_init(self, path): - self._u_invoke_client('init', cwd=path) + cmd = bzrlib.builtins.cmd_init() + cmd.outf = StringIO.StringIO() + cmd.run(location=path) def _vcs_destroy(self): vcs_dir = os.path.join(self.repo, '.bzr') @@ -74,50 +93,69 @@ class Bzr(base.VCS): shutil.rmtree(vcs_dir) def _vcs_add(self, path): - self._u_invoke_client('add', path) + path = os.path.join(self.repo, path) + cmd = bzrlib.builtins.cmd_add() + cmd.outf = StringIO.StringIO() + cmd.run(file_list=[path], file_ids_from=self.repo) def _vcs_remove(self, path): # --force to also remove unversioned files. - self._u_invoke_client('remove', '--force', path) + path = os.path.join(self.repo, path) + cmd = bzrlib.builtins.cmd_remove() + cmd.outf = StringIO.StringIO() + cmd.run(file_list=[path], file_deletion_strategy='force') def _vcs_update(self, path): pass + def _parse_revision_string(self, revision=None): + if revision == None: + return revision + rev_opt = bzrlib.option.Option.OPTIONS['revision'] + try: + rev_spec = rev_opt.type(revision) + except bzrlib.errors.NoSuchRevisionSpec: + raise base.InvalidRevision(revision) + return rev_spec + def _vcs_get_file_contents(self, path, revision=None): if revision == None: return base.VCS._vcs_get_file_contents(self, path, revision) - else: - status,output,error = \ - self._u_invoke_client('cat', '-r', revision, path) - return output + path = os.path.join(self.repo, path) + revision = self._parse_revision_string(revision) + cmd = bzrlib.builtins.cmd_cat() + cmd.outf = StringIO.StringIO() + try: + cmd.run(filename=path, revision=revision) + except bzrlib.errors.BzrCommandError, e: + if 'not present in revision' in str(e): + raise base.InvalidID(path) + raise + return cmd.outf.getvalue() def _vcs_commit(self, commitfile, allow_empty=False): - args = ['commit', '--file', commitfile] - if allow_empty == True: - args.append('--unchanged') - status,output,error = self._u_invoke_client(*args) - else: - kwargs = {'expect':(0,3)} - status,output,error = self._u_invoke_client(*args, **kwargs) - if status != 0: - strings = ['ERROR: no changes to commit.', # bzr 1.3.1 - 'ERROR: No changes to commit.'] # bzr 1.15.1 - if self._u_any_in_string(strings, error) == True: - raise base.EmptyCommit() - else: - raise base.CommandError(args, status, stderr=error) - revision = None - revline = re.compile('Committed revision (.*)[.]') - match = revline.search(error) - assert match != None, output+error - assert len(match.groups()) == 1 - revision = match.groups()[0] - return revision + cmd = bzrlib.builtins.cmd_commit() + cmd.outf = StringIO.StringIO() + cwd = os.getcwd() + os.chdir(self.repo) + try: + cmd.run(file=commitfile, unchanged=allow_empty) + except bzrlib.errors.BzrCommandError, e: + strings = ['no changes to commit.', # bzr 1.3.1 + 'No changes to commit.'] # bzr 1.15.1 + if self._u_any_in_string(strings, str(e)) == True: + raise base.EmptyCommit() + raise + finally: + os.chdir(cwd) + return self._vcs_revision_id(-1) def _vcs_revision_id(self, index): - status,output,error = self._u_invoke_client('revno') - current_revision = int(output) - if index >= current_revision or index < -current_revision: + cmd = bzrlib.builtins.cmd_revno() + cmd.outf = StringIO.StringIO() + cmd.run(location=self.repo) + current_revision = int(cmd.outf.getvalue()) + if index > current_revision or index < -current_revision: return None if index >= 0: return str(index) # bzr commit 0 is the empty tree. -- cgit From 4fbf5d1d222610b0775f95472fe1a60aaedea29f Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 10:40:48 -0500 Subject: Restore comment stripping to libbe.ui.util.editor.editor_string() --- .../3646e056-a2df-46e5-b877-88608c7cc5af/body | 14 + .../3646e056-a2df-46e5-b877-88608c7cc5af/values | 11 + .be/id-cache | 288 +++++++++++++++++++++ libbe/ui/util/editor.py | 4 + 4 files changed, 317 insertions(+) create mode 100644 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/body create mode 100644 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/values create mode 100644 .be/id-cache diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/body new file mode 100644 index 0000000..861fb1d --- /dev/null +++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/body @@ -0,0 +1,14 @@ +> We also have the unfortunate situation of duplicate UUIDs from the old +> be merge +> implemtation. This means that id-to-path is not a well defined +> mapping with single-uuid ids. That's ok though, we get a bit uglier +> and send the long_user() id into the storage backend instead. While +> not so elegant, this will avoid the need for the cached id/path table. + +The situation is worse than just the old `be merge` effects, because +the existence, children, and parents of a particular UUID may be +revision dependent. A UUID will always refer to the same +bugdir/bug/comment, but that bugdir/bug/comment may have different +relatives. Another point in favor of long_user()-style storage ids, +but that just pushes relation-tracking up to the command level. I'm +still figuring out a good way to deal with this... diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/values new file mode 100644 index 0000000..65e4472 --- /dev/null +++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af/values @@ -0,0 +1,11 @@ +Author: W. Trevor King + + +Content-type: text/plain + + +Date: Mon, 28 Dec 2009 12:12:45 +0000 + + +In-reply-to: bd1207ef-f97e-4078-8c5d-046072012082 + diff --git a/.be/id-cache b/.be/id-cache new file mode 100644 index 0000000..e33428c --- /dev/null +++ b/.be/id-cache @@ -0,0 +1,288 @@ +6a0080c4-d684-4c2c-afaa-c15cc43d68ad .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad +d83a5436-85e3-42c7-9a89-a6d50df9d279 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279 +db2c18d9-9573-4d68-88a5-ee47ed24b813 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813 +0cad2ac6-76ef-4a88-abdf-b2e02de76f5c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c +a0e846ed-1549-4ec3-b94d-391e54610f61 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61 +095ade7c-9378-41bd-8137-f2731c6afcac .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac +777182da-a216-45c7-bf4d-42c84e511c66 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66 +8097468f-87a9-4d84-ac20-1772393bb54d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d +bb124fd9-08f5-4f82-a035-6355e8403075 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075 +0c40c13a-3515-4b45-a8c3-142cceab9254 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254 +d8dba78d-f82a-4674-9003-a0ec569b4a96 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96 +7dfdf230-231b-43e0-9b46-58d4d18eded1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1 +f72f8640-2e50-471e-aebe-0ddb8cdd5a2a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a +9a942b1d-a3b5-441d-8aef-b844700e1efa .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa +f376debf-9f7e-4347-807f-00e7263487c7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7 +6dcc910a-ce15-4eeb-b49b-4747719748ed .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed +cb56c990-a757-4aef-9888-a30918a7b3d7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cb56c990-a757-4aef-9888-a30918a7b3d7 +529c290e-b1cf-4800-be7e-68f1ecb9565c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c +0d8af004-8352-4254-b747-d96a40a5d457 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457 +1cb7063f-07ce-4a76-98f9-d184e1ee7282 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282 +a92f97a4-e9fe-43f7-bf56-5862b03a2641 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641 +17921fbc-e7f0-4f31-8cdd-598e5ba7237b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b +c2b78df3-641a-4d4d-ba94-33b26eda6364 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364 +17a2217e-fc1d-4d7a-a569-4fd2a4a2261e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e +22348320-40d3-422c-bdf0-0f6a6bde3fab .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab +dc32aa62-cf56-4171-84a1-8f7d02b23b6d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d +42d57a41-219f-46db-9fda-21b42351da63 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63 +209e2a60-ddd0-4a71-90ef-e57547ed6d76 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76 +5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6 +37650981-1908-4c39-bae2-48e69c771120 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120 +7ec2c071-9630-42b0-b08a-9854616f9144 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144 +da2b09ff-af24-40f3-9b8d-6ffaa5f41164 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/da2b09ff-af24-40f3-9b8d-6ffaa5f41164 +bea86499-824e-4e77-b085-2d581fa9ccab .be/bea86499-824e-4e77-b085-2d581fa9ccab +88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7 +bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3 +7d182ab9-9c0c-4b4f-885e-c5762d7a2437 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7d182ab9-9c0c-4b4f-885e-c5762d7a2437 +028d2e8d-5b0f-4c43-a913-35a1709b2276 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276 +301724b1-3853-4aff-8f23-44373df7cf1c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c +e2f6514c-5f9f-4734-a537-daf3fbe7e9a0 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0 +31beb504-c72b-4304-95ba-a66d2bcbc46a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a +04d71e10-9e44-4006-ab37-b4cc71647671 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671 +9e33512e-e3cb-42ec-bc99-8e77587d0d3f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f +ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68 +4952e1c7-e035-42f1-882b-6b5264481d0a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a +2f6b71c5-45b3-473f-bd14-a1fe41bafcee .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee +576e804a-8b76-4876-8e9d-d7a72b0aef10 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10 +eff20807-07f0-444d-8992-f69ab3f526c5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5 +47c8fd5f-1f5a-4048-bef7-bb4c9a37c411 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411 +b2a333f7-eda6-42b9-8940-177f61ca7f48 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48 +bd1207ef-f97e-4078-8c5d-046072012082 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082 +2aa60b34-2c8d-4f41-bb97-a57309523262 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262 +b9865d8b-46ae-4169-bc83-d75a98164729 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729 +d9959864-ea91-475a-a075-f39aa6760f98 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98 +b3fabbe0-f05d-42a1-9037-e59e628a83e2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2 +68927fef-6ce1-4a1f-a414-28695d913a50 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50 +73a767f4-75e7-4cde-9e24-91bff99ab428 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428 +f65b680b-4309-43a2-ae2d-e65811c9d107 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f65b680b-4309-43a2-ae2d-e65811c9d107 +cb5689f4-7c36-4c44-b380-ca9e06e80bae .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae +8ffc90d7-0be7-4b00-88e6-9ae1b65f7957 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957 +f1479ecf-4154-4cd4-bbd6-0ed6275b9f98 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98 +c129067c-2341-4e7a-92a6-2dcd30d3bbf5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5 +c8283e08-967c-4a7b-b953-3ec62c83fb9f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f +80780fa9-69f8-438c-8fbf-5a702b3badc1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1 +3e7144eb-c934-4b62-94b7-7dbfa90ed6ee .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee +2103f60c-36e5-4b05-b57c-8c6fee2d80d4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4 +9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f +ee681951-f254-43d3-a53a-1b36ae415d5c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c +2929814b-2163-45d0-87ba-f7d1ef0a32a9 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9 +72a519e3-3d6b-4f0f-b412-1310efd255eb .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb +2bb9163c-a2c4-4301-aff5-385f58a14301 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301 +c7ace551-2982-4683-bca3-b5e66056cce5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5 +bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c +4d642e39-a8f3-41d8-93da-bea7e05ef9a6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6 +c592a1e8-f2c8-4dfb-8550-955123073947 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c592a1e8-f2c8-4dfb-8550-955123073947 +b1bc6f39-8166-46c5-a724-4c4a3e1e7d74 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b1bc6f39-8166-46c5-a724-4c4a3e1e7d74 +f5c06914-dc64-4658-8ec7-32a026a53f55 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55 +bd98f525-95ec-446a-84e8-34c7d6fa5b40 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40 +597a7386-643f-4559-8dc4-6871924229b6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6 +cfd7cbc7-27ad-4618-8530-cb4d7323514a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a +cf56e648-3b09-4131-8847-02dff12b4db2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2 +bf0c3752-6338-4919-93ba-4c9252945fb1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1 +2496ccca-130b-4459-bfae-9d9ef0138177 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177 +2f048ac5-5564-4b34-b7f9-605357267ed2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2 +79fb6ef2-176c-45c0-b898-59c3c3e0aafe .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe +13e88b64-117b-4f8b-8cba-8f4a9bc394f5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5 +1182d8e6-5e87-4d0a-b271-c298c36bbc21 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21 +1f40efc1-6efc-4dd8-bdd2-97907e5aa624 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624 +29ad0d9e-c05b-4793-bb8b-e8bf237f51b3 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3 +fd6162f3-7fc1-41d1-a073-a07465802b72 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72 +07fc448f-c42e-4846-929a-8924de485766 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766 +40dac9af-951e-4b98-8779-9ba02c37f8a1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1 +fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a +7e733393-8ba0-4345-a0e3-4140101d32f0 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0 +3646e056-a2df-46e5-b877-88608c7cc5af .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af +4fc71206-4285-417f-8a3c-ed6fb31bbbda .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda +1f25cba2-03ee-43e1-a042-ef6724938ad8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8 +c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e +6eb8141f-b0b1-4d5b-b4e6-d0860d844ada .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada +8e948522-c6a1-4c97-af93-2cf4090f44b5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5 +f1cde826-0506-4b4a-92ab-8499e953fa49 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49 +1f9f60de-ba37-42bc-a1c0-dc062ef255e1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1 +ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42 +4a4609c8-1882-47de-9d30-fee410b8a802 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802 +3bf57ee7-710f-4a01-a8af-8bb9eb9dc937 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937 +0e0c806c-5443-4839-aa60-9615c8c10853 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0e0c806c-5443-4839-aa60-9615c8c10853 +6d7072de-89b6-4c53-a435-6879c644a0e8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8 +e0858b12-0be3-49bb-ad7a-030e488bb2f1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1 +372f8a5c-a1ce-4b07-a7b1-f409033a7eec .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec +49e0425b-3332-4d0e-b371-300eccd55370 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370 +fba8de97-9c61-4a08-b3e7-d8a95d6efe54 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54 +7750d77c-85d2-4810-9d41-cec62b0da885 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885 +85a2d1ac-200a-4ae7-841f-9f4e87795dbf .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf +16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb +c531727a-9d0f-486f-aa0e-d4d2f2236640 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640 +dba25cfd-aa15-457c-903a-b53ecb5a3b2c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c +489397bd-b987-4a08-9589-c5b71661ebb7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7 +27bb8bc2-05c2-417a-9d09-928471380d7a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/27bb8bc2-05c2-417a-9d09-928471380d7a +74cccfbf-069d-4e99-8cab-adaa35f9a2eb .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb +d86e497d-667d-4c2b-9249-76026df56633 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633 +3f556a48-c538-4569-8609-3e829b561d78 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78 +56506b73-36cc-4e32-a578-258a219edba8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8 +1dba8196-654b-4ca0-9a95-fb334af81863 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863 +624a4542-92e9-442e-b71c-a14da4fe55cf .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf +9f910ee0-ff0f-4fa3-b1e3-79a4118e48e9 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9f910ee0-ff0f-4fa3-b1e3-79a4118e48e9 +65776f00-34d8-4b58-874d-333196a5e245 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/65776f00-34d8-4b58-874d-333196a5e245 +a845096e-3cdf-41ed-a0e3-283439665b92 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92 +ae998b27-a11b-4243-abf6-11841e5b8242 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242 +aad59898-8949-44fb-ad0b-2acea6eb2ef8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8 +16ba77d3-dfc9-4732-8d08-0e471f400d85 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85 +00c6f4d8-f965-4d2f-a652-17e58c20ab8c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c +7d7e703f-22f2-4c47-86a3-fcc3c8ead576 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576 +52034fd0-ec50-424d-b25d-2beaf2d2c317 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317 +2b81b428-fc43-4970-9469-b442385b9c0d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2b81b428-fc43-4970-9469-b442385b9c0d +f21bec0d-cad0-44d2-a301-bfb11adce313 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313 +ec133a4e-c9ff-4499-b469-cb0a2ca9a685 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685 +e4ed63f6-9000-4d0b-98c3-487269140141 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141 +950ac308-f3e1-4956-885a-e79ce3025fd5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5 +dcca51b3-bf8f-4482-8f67-662cfbcb9c6c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c +d304f93b-faf2-477e-9ff8-c77e301fd9f9 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9 +46937fd4-b0bc-4eed-8033-d699445441ea .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea +144c238c-75d1-40f1-82c1-647668bcf2bc .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc +30a8b841-98ae-41b7-9ef2-6af7cffca8da .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da +2bb7b4d0-6290-4771-9fff-4aa2e8086b1a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a +7ba4bc51-b251-483a-a67a-f1b89c83f6af .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af +faa686bf-c0eb-48bf-8a0b-d9a2e02bd132 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132 +b8d95763-1825-4e09-bf52-cbd884b916af .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af +f87fd684-6af1-498d-98d5-f915bcee76a9 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9 +074ef29a-3f1d-46dc-8561-7a56af7e6d67 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67 +8e83da06-26f1-4763-a972-dae7e7062233 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233 +64424f05-b42b-4835-8afd-8495ae61345d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d +354dcfc6-5997-4ffe-b7a0-baa852213539 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539 +2c95ee07-462d-42cf-8dc3-8f5389a392cb .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb +427e0ca7-17f5-4a5a-8c68-98cc111a2495 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495 +f51dc5a7-37b7-4ce1-859a-b7cb58be6494 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494 +287d3cc1-1cd0-449a-b280-87c529e33951 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951 +381555eb-f2e3-4ef0-8303-d759c00b390a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a +0f60a148-7024-44bd-bbed-377cbece9d1b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b +09f950d4-9366-4e7b-98b3-9057999f8f38 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38 +1100c966-9671-4bc6-8b68-6d408a910da1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1 +c271a802-d324-48a6-b01d-63e4a72aa43e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e +6e315abe-a080-4369-8729-4aea2dee8494 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494 +8b54e56e-c693-4594-998f-5bd6c1f385d7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7 +e5db7c9b-de48-4302-905b-9570bb6e7ade .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade +a8f35fca-8a15-4833-b568-326f0cc89bfa .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa +12c986be-d19a-4b8b-b1b5-68248ff4d331 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331 +704b37ab-01bb-43d3-9e9f-f0d354f63c7d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d +c454aa67-ca30-43e8-9be4-58cbddd01b63 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63 +4012c6cc-1300-4f6b-af0e-9176eedf8de7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7 +9ce2f015-8ea0-43a5-a03d-fc36f6d202fe .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe +e1ff6c81-37d8-43ee-9dcf-17a89e07556a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a +b17a561a-6100-490e-84eb-d1ae4b617940 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940 +f77fc673-c852-4c81-bfa2-1d59de2661c8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f77fc673-c852-4c81-bfa2-1d59de2661c8 +4d192c6c-a4a8-4844-b083-2dd5926bd2d9 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9 +a63bd76a-cd43-4f97-88ba-2323546d4572 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a63bd76a-cd43-4f97-88ba-2323546d4572 +4f7a4c3b-31e3-4023-8c9d-e67f627a34f0 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0 +ecc91b94-7f3f-44a7-af58-03191d327a7f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ecc91b94-7f3f-44a7-af58-03191d327a7f +202e0dc6-61bf-4b17-a8bd-f8a27482cb68 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68 +e5decfc6-050b-4283-8776-977bf85b2c99 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99 +0ac3c4cb-90e3-4b67-b6cb-1186d5d66240 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240 +d5ed4f87-f1a1-4138-b0ad-190e4a49d820 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820 +b76434a3-5cf9-4d2c-820b-64444289c09f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f +8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8 +c894f10f-197d-4b22-9c5b-19f394df40d4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4 +ea01c122-e629-4d5c-afa7-b180f4a8748b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b +22b6f620-d2f7-42a5-a02e-145733a4e366 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366 +25c67b0b-1afd-4613-a787-e0f018614966 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966 +fd7ab206-5937-4ede-9e78-97aff098b677 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677 +da97e18f-33d6-469e-9d93-6457b9a6bfca .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca +15602c0c-25e4-4c2c-9e24-79bdb90721b1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1 +4c50ca0b-a08f-4723-b00d-4bf342cf86b6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6 +d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00 +06e45775-1c46-4793-a34e-2cc86a8db097 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097 +09f84059-fc8e-4954-b24d-a2b33ef21bf4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4 +a536cee5-cc8d-4b18-b491-657e0c7998b4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4 +f847c981-873e-41ae-b5ce-83dfe60b9afe .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe +6010e186-0260-44e5-8442-8df2269910ce .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce +8d927822-eff9-42c4-9541-8b784b3f7db2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2 +478443b3-dd69-4719-b79a-b1279f75b8e4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4 +3613e6e9-db9e-4775-8914-f31f0b4b81ac .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3613e6e9-db9e-4775-8914-f31f0b4b81ac +acbecd72-988c-4899-a340-fea370ce15a8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8 +4be73baf-e46b-4acb-a58e-4719e57c550b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b +b0e7165b-7099-45ca-9513-412225f7bd52 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52 +4068c833-0c06-475e-8b7e-6701bc416dee .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee +e0155831-499f-421a-ad02-cd15fc3fecf1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0155831-499f-421a-ad02-cd15fc3fecf1 +1847f1f8-525a-42c4-ae2b-e9377459d2a6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6 +0a995544-20dc-42a6-8d3f-348ebbc8921e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e +a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2 +f92c6180-0ed8-4acc-8ced-22995a0c016b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b +6622c06a-ed84-4d45-8011-a082fca219b6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6622c06a-ed84-4d45-8011-a082fca219b6 +9daa72ee-0721-4f68-99ee-f06fec0b340e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9daa72ee-0721-4f68-99ee-f06fec0b340e +c45e5ece-63e3-4fd2-b33f-0bfd06820cf4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4 +b19a8f6a-1d7b-4887-a9df-123d59b0cd9b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b +f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a +fdb615a4-168a-467b-8090-875c998455e5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5 +bb406a33-92b6-46dd-950c-c7cfb5440e7b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b +5e339bac-f4f3-407b-974a-b88795d3573b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b +cf77c72d-b099-413a-802e-a8892ac8c26b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b +0ca2d112-b5bb-4df1-8ac0-e46db6cdd442 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0ca2d112-b5bb-4df1-8ac0-e46db6cdd442 +ae0f9aea-960c-42b4-82df-943bbbe17d58 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58 +a492508e-0be7-4403-bbd0-9cdc0a46b06b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b +942cd941-583d-4020-99e4-80de7e836129 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129 +2ae039de-5b0d-4a4f-aa80-6c81d1345367 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367 +b8bbd433-9017-4c04-a038-2a7370a3adc7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7 +1ba36272-7ae1-4f95-8002-7b45e62e6790 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790 +7fa903a3-f9e6-4e4d-8128-0f26e1ce664b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b +68ba7f0c-ca5f-4f49-a508-e39150c07e13 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13 +ec16300f-529a-4492-8327-f9a72e4447c2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2 +b900f7fd-bab6-48c4-922c-a051f933da58 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58 +508ea95e-7bc6-4b9b-9e36-a3a87014423d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d +2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4 +e757d2ae-085a-4539-99be-096386de5352 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352 +c35835c0-8f9f-4090-ba92-1f616867e486 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486 +520a9829-8d90-43ce-be64-868b8321e5b0 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0 +3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b +00f26f04-9202-4288-8744-b29abc2342d6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6 +7b904395-86e9-4eb1-8534-69cec63801d4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4 +e03b65da-8c31-4a8c-a938-ef1254ef0a80 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/e03b65da-8c31-4a8c-a938-ef1254ef0a80 +16357f68-19c0-4bf9-8220-b88b52b3456d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d +303986f2-0b17-4589-bf76-ed1461699c3e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e +31cd490d-a1c2-4ab3-8284-d80395e34dd2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2 +8385a1fb-63df-4ca6-81cd-28ede83bb0c2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8385a1fb-63df-4ca6-81cd-28ede83bb0c2 +0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1 +744435b7-1521-4059-a55d-f0c403d7b4d8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8 +b3c6da51-3a30-42c9-8c75-587c7a1705c5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3c6da51-3a30-42c9-8c75-587c7a1705c5 +9b1a0e71-4f7d-40b1-ab32-18496bf19a3f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9b1a0e71-4f7d-40b1-ab32-18496bf19a3f +a403de79-8f39-41f2-b9ec-15053b175ee2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2 +a4720227-43cf-49aa-8f9f-f49f46e3e809 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809 +24903c62-f441-496e-9dcf-17e7a581df33 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33 +f05359f6-1bfc-4aa6-9a6d-673516bc0f94 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94 +b187fbce-fb10-4819-ace2-c8b0b4a45c57 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57 +9aa88bbd-71d0-44fa-804d-3562171f9539 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539 +7bfc591e-584a-476e-8e11-b548f1afcaa6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6 +5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc +496edad5-1484-413a-bc68-4b01274a65eb .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb +8e1bbda4-35b6-4579-849d-117b1596ee99 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99 +f925e56f-26f9-4620-82fb-a0f160f27921 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921 +cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c +401950a0-a5ff-46f3-afac-a9cfb300f94b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b +55263144-9775-4b18-ab83-29d66ed91a53 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53 +e520239c-8d69-4ff6-b1bd-0c2f74366200 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200 +83202b83-eea8-452f-8239-d468940bddba .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba +4be35966-373b-438c-a35a-824f5c7a940a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a +13012b22-2d02-444c-87c0-8cf0f17137ae .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae +e173c09a-1b3e-4d8a-a86a-6b8c94a76247 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247 +02223264-e28a-4720-9f20-1e7a27a7041d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/02223264-e28a-4720-9f20-1e7a27a7041d +62a74b85-0d4b-49f5-8794-74bafd871cd4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/62a74b85-0d4b-49f5-8794-74bafd871cd4 +3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e +96abea83-9867-4c21-8eb8-9e1b1093cba4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4 +72dab0c4-f04d-4ff0-9319-f55aafaea627 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627 +dac91856-cb6a-4f69-8c03-38ff0b29aab2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2 +e249e2aa-2029-4a96-bc84-962366e07fd6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6 +0e5fab2a-66eb-4f7d-979f-b50181f604d4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4 +208595bd-35b8-44c2-bf97-fc5ef9e7a58d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d +f2011471-56cb-46e2-813b-1ac336ee7bbc .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc +bcd6e5d4-8d03-43ad-a10d-17619735d077 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077 +e5248100-ea02-4205-a4c1-ac7a577c6362 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362 +6555a651-5a7f-4a8a-9793-47ad1315e9e8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8 +c76d7899-d495-4103-9355-012c0a6fece3 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3 +2628eeca-96c6-4933-8484-d55bb1dbf985 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985 +21c90231-d7f2-49bb-97d9-99e16459d799 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799 +6d87d87e-7974-412f-b9c1-41a87f752d30 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6d87d87e-7974-412f-b9c1-41a87f752d30 +51930348-9ccc-4165-af41-6c7450de050e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e +764b812f-a0bb-4f4d-8e2f-c255c9474a0e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/764b812f-a0bb-4f4d-8e2f-c255c9474a0e +f70dd5df-805b-49f3-a9ce-12e0fae63365 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365 +ae56365e-7a9c-4cc3-ba67-7addbeeeff49 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49 +8015d736-f3ea-4085-940c-552c01a287ef .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef +be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf +d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6 diff --git a/libbe/ui/util/editor.py b/libbe/ui/util/editor.py index 83262e7..1a10fa4 100644 --- a/libbe/ui/util/editor.py +++ b/libbe/ui/util/editor.py @@ -56,6 +56,9 @@ def editor_string(comment=None, encoding=None): >>> os.environ["VISUAL"] = "echo baz > " >>> editor_string() u'baz\\n' + >>> os.environ["VISUAL"] = "echo 'baz\\n== Anything below this line will be ignored\\nHi' > " + >>> editor_string() + u'baz\\n' >>> del os.environ["EDITOR"] >>> del os.environ["VISUAL"] """ @@ -79,6 +82,7 @@ def editor_string(comment=None, encoding=None): os.system("%s %s" % (editor, fname)) output = libbe.util.encoding.get_file_contents( fname, encoding=encoding, decode=True) + output = trimmed_string(output) if output.rstrip('\n') == "": output = None finally: -- cgit From e2b648f2148d7b6550fb3a4bcfde4eff714a7ec6 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 10:56:04 -0500 Subject: Allow external use of Command.usage() and use CmdOptionParser.set_usage() This fixes $ python be diff -2 Usage: be [options] be: error: no such option: -2 and we now get the correct output $ python be diff -2 Usage: be diff [options] [REVISION] be: error: no such option: -2 --- libbe/command/base.py | 4 ++-- libbe/command/target.py | 2 +- libbe/ui/command_line.py | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libbe/command/base.py b/libbe/command/base.py index ac6a58b..cdb4043 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -264,11 +264,11 @@ class Command (object): self.stdout.encoding = output_encoding def help(self, *args): - return '\n\n'.join([self._usage(), + return '\n\n'.join([self.usage(), self._option_help(), self._long_help().rstrip('\n')]) - def _usage(self): + def usage(self): usage = 'usage: be %s [options]' % self.name num_optional = 0 for arg in self.args: diff --git a/libbe/command/target.py b/libbe/command/target.py index df836db..3195f95 100644 --- a/libbe/command/target.py +++ b/libbe/command/target.py @@ -107,7 +107,7 @@ class Target (libbe.command.Command): target = add_target(bugdir, bug, params['target']) return 0 - def _usage(self): + def usage(self): return 'usage: be %(name)s BUG-ID [TARGET]\nor: be %(name)s --resolve [TARGET]' \ % vars(self) diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py index 856f810..b5a3991 100755 --- a/libbe/ui/command_line.py +++ b/libbe/ui/command_line.py @@ -48,6 +48,8 @@ class CmdOptionParser(optparse.OptionParser): self._option_by_name = {} for option in self.command.options: self._add_option(option) + self.set_usage(command.usage()) + def _add_option(self, option): option.validate() @@ -215,7 +217,7 @@ class BE (libbe.command.Command): name='args', optional=True, repeatable=True) ]) - def _usage(self): + def usage(self): return 'usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]' def _long_help(self): -- cgit From 2d6ed9ec7181ef805f305c6c8b7152c1b9ec6ec8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 11:13:49 -0500 Subject: Added VersionedStorage_commit_TestCase.test_commit_revision_ids() --- libbe/storage/base.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/libbe/storage/base.py b/libbe/storage/base.py index d99c22c..9807d86 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -827,6 +827,29 @@ if TESTING == True: "%s.get() returned %s not %s for revision %s" % (vars(self.Class)['name'], ret, val(i), revs[i])) + def test_get_previous_children(self): + """ + Children list should be revision dependent. + """ + self.s.add('parent', directory=True) + revs = [] + cur_children = [] + children = [] + for i in range(10): + new_child = str(i) + self.s.add(new_child, 'parent', directory=False) + self.s.set(new_child, self.val) + revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), + self.commit_body)) + cur_children.append(new_child) + children.append(list(cur_children)) + for i in range(10): + ret = self.s.children('parent', revision=revs[i]) + self.failUnless(ret == children[i], + "%s.get() returned %s not %s for revision %s" + % (vars(self.Class)['name'], ret, + children[i], revs[i])) + def make_storage_testcase_subclasses(storage_class, namespace): """Make StorageTestCase subclasses for storage_class in namespace.""" storage_testcase_classes = [ -- cgit From c90044dff5feaf5f43fee9e8559fecec2ec60091 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 12:30:19 -0500 Subject: Fixed VCS.children() and Bzr.children() for non-None revisions. Now they both pass VersionedStorage_commit_TestCase.test_commit_revision_ids() The .children() implementation for previous revisions lacks the working directory's id<->path cache, so it's fairly slow... --- libbe/storage/base.py | 7 ++++- libbe/storage/vcs/base.py | 78 +++++++++++++++++++++++++++++++++++++++++------ libbe/storage/vcs/bzr.py | 29 +++++++++++++++++- 3 files changed, 102 insertions(+), 12 deletions(-) diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 9807d86..d16c30b 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -37,7 +37,12 @@ class InvalidStorageVersion(ConnectionError): self.expected_version = expected_version class InvalidID (KeyError): - pass + def __init__(self, id=None, revision=None, msg=None): + if msg == None and id != None: + msg = id + KeyError.__init__(self, msg) + self.id = id + self.revision = revision class InvalidRevision (KeyError): pass diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index cfb39a1..3b66019 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -98,10 +98,10 @@ class VCSUnableToRoot (libbe.storage.base.ConnectionError): self.vcs = vcs class InvalidPath (InvalidID): - def __init__(self, path, root, msg=None): + def __init__(self, path, root, msg=None, **kwargs): if msg == None: msg = 'Path "%s" not in root "%s"' % (path, root) - InvalidID.__init__(self, msg) + InvalidID.__init__(self, msg=msg, **kwargs) self.path = path self.root = root @@ -277,9 +277,9 @@ class CachedPathID (object): self._changed = True def id(self, path): - path = os.path.abspath(path) + path = os.path.join(self._root, path) if not path.startswith(self._root + os.path.sep): - raise InvalidPath('Path %s not in root %s' % (path, self._root)) + raise InvalidPath(path, self._root) path = path[len(self._root)+1:] orig_path = path if not path.startswith(self._spacer_dirs[0] + os.path.sep): @@ -548,6 +548,33 @@ os.listdir(self.get_path("bugs")): f.close() return contents + def _vcs_path(self, id, revision): + """ + Return the path to object id as of revision. + + Revision will not be None. + """ + raise NotImplementedError + + def _vcs_isdir(self, path, revision): + """ + Return True if path (as returned by _vcs_path) was a directory + as of revision, False otherwise. + + Revision will not be None. + """ + raise NotImplementedError + + def _vcs_listdir(self, path, revision): + """ + Return a list of the contents of the directory path (as + returned by _vcs_path) as of revision. + + Revision will not be None, and ._vcs_isdir(path, revision) + will be True. + """ + raise NotImplementedError + def _vcs_commit(self, commitfile, allow_empty=False): """ Commit the current working directory, using the contents of @@ -711,28 +738,40 @@ os.listdir(self.get_path("bugs")): self._cached_path_id.remove_id(id) def _children(self, id=None, revision=None): + if revision == None: + id_to_path = self._cached_path_id.path + path_to_id = self._cached_path_id.id + isdir = os.path.isdir + listdir = os.listdir + else: + id_to_path = lambda id : self._vcs_path(id, revision) + path_to_id = self._cached_path_id.id + isdir = lambda path : self._vcs_isdir(path, revision) + listdir = lambda path : self._vcs_listdir(path, revision) if id==None: path = self.be_dir else: - path = self._cached_path_id.path(id) - if os.path.isdir(path) == False: + path = id_to_path(id) + if isdir(path) == False: return [] - children = os.listdir(path) + children = listdir(path) for i,c in enumerate(children): if c in self._cached_path_id._spacer_dirs: children[i] = None children.extend([os.path.join(c, c2) for c2 in - os.listdir(os.path.join(path, c))]) + listdir(os.path.join(path, c))]) elif c in ['id-cache', 'version']: children[i] = None for i,c in enumerate(children): if c == None: continue cpath = os.path.join(path, c) if self.interspersed_vcs_files == True \ + and revision != None \ and self._vcs_is_versioned(cpath) == False: children[i] = None else: - children[i] = self._cached_path_id.id(cpath) + children[i] = path_to_id(cpath) + children[i] return [c for c in children if c != None] def _get(self, id, default=libbe.util.InvalidObject, revision=None): @@ -746,7 +785,7 @@ os.listdir(self.get_path("bugs")): try: contents = self._vcs_get_file_contents(relpath,revision) except InvalidID, e: - raise InvalidID(id) + raise InvalidPath(path=path, root=self.repo, id=id) if contents in [libbe.storage.base.InvalidDirectory, libbe.util.InvalidObject]: raise InvalidID(id) @@ -837,6 +876,25 @@ os.listdir(self.get_path("bugs")): """ return search_parent_directories(path, filename) + def _u_find_id(self, id, revision): + """ + Search for the relative path to id as of revision. + Returns None if the id is not found. + """ + assert self._rooted == True + be_dir = self._cached_path_id._spacer_dirs[0] + stack = [(be_dir, be_dir)] + while len(stack) > 0: + path,long_id = stack.pop() + if long_id.endswith('/'+id): + return path + if self._vcs_isdir(path, revision) == False: + continue + for child in self._vcs_listdir(path, revision): + stack.append((os.path.join(path, child), + '/'.join([long_id, child]))) + return None + def _u_rel_path(self, path, root=None): """ Return the relative path to path from root. diff --git a/libbe/storage/vcs/bzr.py b/libbe/storage/vcs/bzr.py index 4e3f330..d6e7799 100644 --- a/libbe/storage/vcs/bzr.py +++ b/libbe/storage/vcs/bzr.py @@ -129,10 +129,37 @@ class Bzr(base.VCS): cmd.run(filename=path, revision=revision) except bzrlib.errors.BzrCommandError, e: if 'not present in revision' in str(e): - raise base.InvalidID(path) + raise base.InvalidID(path, revision) raise return cmd.outf.getvalue() + def _vcs_path(self, id, revision): + return self._u_find_id(id, revision) + + def _vcs_isdir(self, path, revision): + try: + self._vcs_listdir(path, revision) + except AttributeError, e: + if 'children' in str(e): + return False + raise + return True + + def _vcs_listdir(self, path, revision): + path = os.path.join(self.repo, path) + revision = self._parse_revision_string(revision) + cmd = bzrlib.builtins.cmd_ls() + cmd.outf = StringIO.StringIO() + try: + cmd.run(revision=revision, path=path) + except bzrlib.errors.BzrCommandError, e: + if 'not present in revision' in str(e): + raise base.InvalidID(path, revision) + raise + children = cmd.outf.getvalue().rstrip('\n').splitlines() + children = [self._u_rel_path(c, path) for c in children] + return children + def _vcs_commit(self, commitfile, allow_empty=False): cmd = bzrlib.builtins.cmd_commit() cmd.outf = StringIO.StringIO() -- cgit From e0e7328742b92cb5e08aeec348fce966375d7d52 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 13:22:27 -0500 Subject: Updated Git backend to support .children(revision). + some minor fixes to vcs/base.py and vcs/bzr.py Also removed .be/id-cache, which should never have been versioned in the first place. --- .be/id-cache | 288 ---------------------------------------------- libbe/storage/vcs/base.py | 6 +- libbe/storage/vcs/bzr.py | 4 +- libbe/storage/vcs/git.py | 20 ++++ 4 files changed, 26 insertions(+), 292 deletions(-) delete mode 100644 .be/id-cache diff --git a/.be/id-cache b/.be/id-cache deleted file mode 100644 index e33428c..0000000 --- a/.be/id-cache +++ /dev/null @@ -1,288 +0,0 @@ -6a0080c4-d684-4c2c-afaa-c15cc43d68ad .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/6a0080c4-d684-4c2c-afaa-c15cc43d68ad -d83a5436-85e3-42c7-9a89-a6d50df9d279 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/d83a5436-85e3-42c7-9a89-a6d50df9d279 -db2c18d9-9573-4d68-88a5-ee47ed24b813 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/db2c18d9-9573-4d68-88a5-ee47ed24b813 -0cad2ac6-76ef-4a88-abdf-b2e02de76f5c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c -a0e846ed-1549-4ec3-b94d-391e54610f61 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/a0e846ed-1549-4ec3-b94d-391e54610f61 -095ade7c-9378-41bd-8137-f2731c6afcac .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/095ade7c-9378-41bd-8137-f2731c6afcac -777182da-a216-45c7-bf4d-42c84e511c66 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/777182da-a216-45c7-bf4d-42c84e511c66 -8097468f-87a9-4d84-ac20-1772393bb54d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/8097468f-87a9-4d84-ac20-1772393bb54d -bb124fd9-08f5-4f82-a035-6355e8403075 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/bb124fd9-08f5-4f82-a035-6355e8403075 -0c40c13a-3515-4b45-a8c3-142cceab9254 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/0c40c13a-3515-4b45-a8c3-142cceab9254 -d8dba78d-f82a-4674-9003-a0ec569b4a96 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96 -7dfdf230-231b-43e0-9b46-58d4d18eded1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1 -f72f8640-2e50-471e-aebe-0ddb8cdd5a2a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/f72f8640-2e50-471e-aebe-0ddb8cdd5a2a -9a942b1d-a3b5-441d-8aef-b844700e1efa .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa -f376debf-9f7e-4347-807f-00e7263487c7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/f376debf-9f7e-4347-807f-00e7263487c7 -6dcc910a-ce15-4eeb-b49b-4747719748ed .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/6dcc910a-ce15-4eeb-b49b-4747719748ed -cb56c990-a757-4aef-9888-a30918a7b3d7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cb56c990-a757-4aef-9888-a30918a7b3d7 -529c290e-b1cf-4800-be7e-68f1ecb9565c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c -0d8af004-8352-4254-b747-d96a40a5d457 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457 -1cb7063f-07ce-4a76-98f9-d184e1ee7282 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/1cb7063f-07ce-4a76-98f9-d184e1ee7282 -a92f97a4-e9fe-43f7-bf56-5862b03a2641 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/a92f97a4-e9fe-43f7-bf56-5862b03a2641 -17921fbc-e7f0-4f31-8cdd-598e5ba7237b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b -c2b78df3-641a-4d4d-ba94-33b26eda6364 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c2b78df3-641a-4d4d-ba94-33b26eda6364 -17a2217e-fc1d-4d7a-a569-4fd2a4a2261e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/17a2217e-fc1d-4d7a-a569-4fd2a4a2261e -22348320-40d3-422c-bdf0-0f6a6bde3fab .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/22348320-40d3-422c-bdf0-0f6a6bde3fab -dc32aa62-cf56-4171-84a1-8f7d02b23b6d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/dc32aa62-cf56-4171-84a1-8f7d02b23b6d -42d57a41-219f-46db-9fda-21b42351da63 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/42d57a41-219f-46db-9fda-21b42351da63 -209e2a60-ddd0-4a71-90ef-e57547ed6d76 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76 -5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d8dba78d-f82a-4674-9003-a0ec569b4a96/comments/5b2e1ec8-3bb7-40cd-9f4f-74e5c59838f6 -37650981-1908-4c39-bae2-48e69c771120 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120 -7ec2c071-9630-42b0-b08a-9854616f9144 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144 -da2b09ff-af24-40f3-9b8d-6ffaa5f41164 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/da2b09ff-af24-40f3-9b8d-6ffaa5f41164 -bea86499-824e-4e77-b085-2d581fa9ccab .be/bea86499-824e-4e77-b085-2d581fa9ccab -88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/88d1f2c2-e1af-4f0d-9390-e3c89ae4f7d7 -bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/bb988ed4-d3d5-4e49-b67e-c7ccb8ae44d3 -7d182ab9-9c0c-4b4f-885e-c5762d7a2437 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7d182ab9-9c0c-4b4f-885e-c5762d7a2437 -028d2e8d-5b0f-4c43-a913-35a1709b2276 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276 -301724b1-3853-4aff-8f23-44373df7cf1c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/301724b1-3853-4aff-8f23-44373df7cf1c -e2f6514c-5f9f-4734-a537-daf3fbe7e9a0 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0 -31beb504-c72b-4304-95ba-a66d2bcbc46a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/31beb504-c72b-4304-95ba-a66d2bcbc46a -04d71e10-9e44-4006-ab37-b4cc71647671 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4/comments/04d71e10-9e44-4006-ab37-b4cc71647671 -9e33512e-e3cb-42ec-bc99-8e77587d0d3f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9e33512e-e3cb-42ec-bc99-8e77587d0d3f -ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ae4f8f1e-6f86-4f81-ba9f-4042deb2ee68 -4952e1c7-e035-42f1-882b-6b5264481d0a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4952e1c7-e035-42f1-882b-6b5264481d0a -2f6b71c5-45b3-473f-bd14-a1fe41bafcee .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/2f6b71c5-45b3-473f-bd14-a1fe41bafcee -576e804a-8b76-4876-8e9d-d7a72b0aef10 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10 -eff20807-07f0-444d-8992-f69ab3f526c5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5 -47c8fd5f-1f5a-4048-bef7-bb4c9a37c411 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/47c8fd5f-1f5a-4048-bef7-bb4c9a37c411 -b2a333f7-eda6-42b9-8940-177f61ca7f48 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2/comments/b2a333f7-eda6-42b9-8940-177f61ca7f48 -bd1207ef-f97e-4078-8c5d-046072012082 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bd1207ef-f97e-4078-8c5d-046072012082 -2aa60b34-2c8d-4f41-bb97-a57309523262 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262 -b9865d8b-46ae-4169-bc83-d75a98164729 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/b9865d8b-46ae-4169-bc83-d75a98164729 -d9959864-ea91-475a-a075-f39aa6760f98 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98 -b3fabbe0-f05d-42a1-9037-e59e628a83e2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/b3fabbe0-f05d-42a1-9037-e59e628a83e2 -68927fef-6ce1-4a1f-a414-28695d913a50 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/68927fef-6ce1-4a1f-a414-28695d913a50 -73a767f4-75e7-4cde-9e24-91bff99ab428 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/73a767f4-75e7-4cde-9e24-91bff99ab428 -f65b680b-4309-43a2-ae2d-e65811c9d107 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f65b680b-4309-43a2-ae2d-e65811c9d107 -cb5689f4-7c36-4c44-b380-ca9e06e80bae .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/cb5689f4-7c36-4c44-b380-ca9e06e80bae -8ffc90d7-0be7-4b00-88e6-9ae1b65f7957 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/8ffc90d7-0be7-4b00-88e6-9ae1b65f7957 -f1479ecf-4154-4cd4-bbd6-0ed6275b9f98 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/f1479ecf-4154-4cd4-bbd6-0ed6275b9f98 -c129067c-2341-4e7a-92a6-2dcd30d3bbf5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/c129067c-2341-4e7a-92a6-2dcd30d3bbf5 -c8283e08-967c-4a7b-b953-3ec62c83fb9f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/c8283e08-967c-4a7b-b953-3ec62c83fb9f -80780fa9-69f8-438c-8fbf-5a702b3badc1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/80780fa9-69f8-438c-8fbf-5a702b3badc1 -3e7144eb-c934-4b62-94b7-7dbfa90ed6ee .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/3e7144eb-c934-4b62-94b7-7dbfa90ed6ee -2103f60c-36e5-4b05-b57c-8c6fee2d80d4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4 -9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/9bbe9370-99c7-4d7c-80ee-9ade6b6feb9f -ee681951-f254-43d3-a53a-1b36ae415d5c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ee681951-f254-43d3-a53a-1b36ae415d5c -2929814b-2163-45d0-87ba-f7d1ef0a32a9 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9 -72a519e3-3d6b-4f0f-b412-1310efd255eb .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/72a519e3-3d6b-4f0f-b412-1310efd255eb -2bb9163c-a2c4-4301-aff5-385f58a14301 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/2bb9163c-a2c4-4301-aff5-385f58a14301 -c7ace551-2982-4683-bca3-b5e66056cce5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/c7ace551-2982-4683-bca3-b5e66056cce5 -bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/bd0ebb56-fb46-45bc-af08-1e4a94e8ef3c -4d642e39-a8f3-41d8-93da-bea7e05ef9a6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6 -c592a1e8-f2c8-4dfb-8550-955123073947 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c592a1e8-f2c8-4dfb-8550-955123073947 -b1bc6f39-8166-46c5-a724-4c4a3e1e7d74 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b1bc6f39-8166-46c5-a724-4c4a3e1e7d74 -f5c06914-dc64-4658-8ec7-32a026a53f55 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f5c06914-dc64-4658-8ec7-32a026a53f55 -bd98f525-95ec-446a-84e8-34c7d6fa5b40 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/bd98f525-95ec-446a-84e8-34c7d6fa5b40 -597a7386-643f-4559-8dc4-6871924229b6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6 -cfd7cbc7-27ad-4618-8530-cb4d7323514a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/cfd7cbc7-27ad-4618-8530-cb4d7323514a -cf56e648-3b09-4131-8847-02dff12b4db2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2 -bf0c3752-6338-4919-93ba-4c9252945fb1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1 -2496ccca-130b-4459-bfae-9d9ef0138177 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/2496ccca-130b-4459-bfae-9d9ef0138177 -2f048ac5-5564-4b34-b7f9-605357267ed2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2 -79fb6ef2-176c-45c0-b898-59c3c3e0aafe .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/79fb6ef2-176c-45c0-b898-59c3c3e0aafe -13e88b64-117b-4f8b-8cba-8f4a9bc394f5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/13e88b64-117b-4f8b-8cba-8f4a9bc394f5 -1182d8e6-5e87-4d0a-b271-c298c36bbc21 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2/comments/1182d8e6-5e87-4d0a-b271-c298c36bbc21 -1f40efc1-6efc-4dd8-bdd2-97907e5aa624 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1f40efc1-6efc-4dd8-bdd2-97907e5aa624 -29ad0d9e-c05b-4793-bb8b-e8bf237f51b3 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495/comments/29ad0d9e-c05b-4793-bb8b-e8bf237f51b3 -fd6162f3-7fc1-41d1-a073-a07465802b72 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/fd6162f3-7fc1-41d1-a073-a07465802b72 -07fc448f-c42e-4846-929a-8924de485766 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/07fc448f-c42e-4846-929a-8924de485766 -40dac9af-951e-4b98-8779-9ba02c37f8a1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1 -fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/fa60ce1f-a809-4fb3-a2cd-1a2e0bdd0e0a -7e733393-8ba0-4345-a0e3-4140101d32f0 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/7e733393-8ba0-4345-a0e3-4140101d32f0 -3646e056-a2df-46e5-b877-88608c7cc5af .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/3646e056-a2df-46e5-b877-88608c7cc5af -4fc71206-4285-417f-8a3c-ed6fb31bbbda .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4fc71206-4285-417f-8a3c-ed6fb31bbbda -1f25cba2-03ee-43e1-a042-ef6724938ad8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/1f25cba2-03ee-43e1-a042-ef6724938ad8 -c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e -6eb8141f-b0b1-4d5b-b4e6-d0860d844ada .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada -8e948522-c6a1-4c97-af93-2cf4090f44b5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5 -f1cde826-0506-4b4a-92ab-8499e953fa49 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/f1cde826-0506-4b4a-92ab-8499e953fa49 -1f9f60de-ba37-42bc-a1c0-dc062ef255e1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/1f9f60de-ba37-42bc-a1c0-dc062ef255e1 -ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ffbf5ac9-e2f5-47ab-9c3c-33989c81ad42 -4a4609c8-1882-47de-9d30-fee410b8a802 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802 -3bf57ee7-710f-4a01-a8af-8bb9eb9dc937 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/3bf57ee7-710f-4a01-a8af-8bb9eb9dc937 -0e0c806c-5443-4839-aa60-9615c8c10853 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0e0c806c-5443-4839-aa60-9615c8c10853 -6d7072de-89b6-4c53-a435-6879c644a0e8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2929814b-2163-45d0-87ba-f7d1ef0a32a9/comments/6d7072de-89b6-4c53-a435-6879c644a0e8 -e0858b12-0be3-49bb-ad7a-030e488bb2f1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1 -372f8a5c-a1ce-4b07-a7b1-f409033a7eec .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/372f8a5c-a1ce-4b07-a7b1-f409033a7eec -49e0425b-3332-4d0e-b371-300eccd55370 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/49e0425b-3332-4d0e-b371-300eccd55370 -fba8de97-9c61-4a08-b3e7-d8a95d6efe54 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/fba8de97-9c61-4a08-b3e7-d8a95d6efe54 -7750d77c-85d2-4810-9d41-cec62b0da885 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/7750d77c-85d2-4810-9d41-cec62b0da885 -85a2d1ac-200a-4ae7-841f-9f4e87795dbf .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/85a2d1ac-200a-4ae7-841f-9f4e87795dbf -16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb -c531727a-9d0f-486f-aa0e-d4d2f2236640 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/c531727a-9d0f-486f-aa0e-d4d2f2236640 -dba25cfd-aa15-457c-903a-b53ecb5a3b2c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dba25cfd-aa15-457c-903a-b53ecb5a3b2c -489397bd-b987-4a08-9589-c5b71661ebb7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7 -27bb8bc2-05c2-417a-9d09-928471380d7a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/27bb8bc2-05c2-417a-9d09-928471380d7a -74cccfbf-069d-4e99-8cab-adaa35f9a2eb .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/74cccfbf-069d-4e99-8cab-adaa35f9a2eb -d86e497d-667d-4c2b-9249-76026df56633 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/d86e497d-667d-4c2b-9249-76026df56633 -3f556a48-c538-4569-8609-3e829b561d78 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78 -56506b73-36cc-4e32-a578-258a219edba8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8 -1dba8196-654b-4ca0-9a95-fb334af81863 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/1dba8196-654b-4ca0-9a95-fb334af81863 -624a4542-92e9-442e-b71c-a14da4fe55cf .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/624a4542-92e9-442e-b71c-a14da4fe55cf -9f910ee0-ff0f-4fa3-b1e3-79a4118e48e9 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9f910ee0-ff0f-4fa3-b1e3-79a4118e48e9 -65776f00-34d8-4b58-874d-333196a5e245 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/65776f00-34d8-4b58-874d-333196a5e245 -a845096e-3cdf-41ed-a0e3-283439665b92 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a845096e-3cdf-41ed-a0e3-283439665b92 -ae998b27-a11b-4243-abf6-11841e5b8242 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242 -aad59898-8949-44fb-ad0b-2acea6eb2ef8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/aad59898-8949-44fb-ad0b-2acea6eb2ef8 -16ba77d3-dfc9-4732-8d08-0e471f400d85 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/16ba77d3-dfc9-4732-8d08-0e471f400d85 -00c6f4d8-f965-4d2f-a652-17e58c20ab8c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/00c6f4d8-f965-4d2f-a652-17e58c20ab8c -7d7e703f-22f2-4c47-86a3-fcc3c8ead576 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e948522-c6a1-4c97-af93-2cf4090f44b5/comments/7d7e703f-22f2-4c47-86a3-fcc3c8ead576 -52034fd0-ec50-424d-b25d-2beaf2d2c317 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317 -2b81b428-fc43-4970-9469-b442385b9c0d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2b81b428-fc43-4970-9469-b442385b9c0d -f21bec0d-cad0-44d2-a301-bfb11adce313 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313 -ec133a4e-c9ff-4499-b469-cb0a2ca9a685 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/ec133a4e-c9ff-4499-b469-cb0a2ca9a685 -e4ed63f6-9000-4d0b-98c3-487269140141 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141 -950ac308-f3e1-4956-885a-e79ce3025fd5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/950ac308-f3e1-4956-885a-e79ce3025fd5 -dcca51b3-bf8f-4482-8f67-662cfbcb9c6c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c -d304f93b-faf2-477e-9ff8-c77e301fd9f9 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e/comments/d304f93b-faf2-477e-9ff8-c77e301fd9f9 -46937fd4-b0bc-4eed-8033-d699445441ea .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/46937fd4-b0bc-4eed-8033-d699445441ea -144c238c-75d1-40f1-82c1-647668bcf2bc .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc -30a8b841-98ae-41b7-9ef2-6af7cffca8da .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/30a8b841-98ae-41b7-9ef2-6af7cffca8da -2bb7b4d0-6290-4771-9fff-4aa2e8086b1a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2bb7b4d0-6290-4771-9fff-4aa2e8086b1a -7ba4bc51-b251-483a-a67a-f1b89c83f6af .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af -faa686bf-c0eb-48bf-8a0b-d9a2e02bd132 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/faa686bf-c0eb-48bf-8a0b-d9a2e02bd132 -b8d95763-1825-4e09-bf52-cbd884b916af .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af -f87fd684-6af1-498d-98d5-f915bcee76a9 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/f87fd684-6af1-498d-98d5-f915bcee76a9 -074ef29a-3f1d-46dc-8561-7a56af7e6d67 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/074ef29a-3f1d-46dc-8561-7a56af7e6d67 -8e83da06-26f1-4763-a972-dae7e7062233 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233 -64424f05-b42b-4835-8afd-8495ae61345d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/64424f05-b42b-4835-8afd-8495ae61345d -354dcfc6-5997-4ffe-b7a0-baa852213539 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/354dcfc6-5997-4ffe-b7a0-baa852213539 -2c95ee07-462d-42cf-8dc3-8f5389a392cb .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/2c95ee07-462d-42cf-8dc3-8f5389a392cb -427e0ca7-17f5-4a5a-8c68-98cc111a2495 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/427e0ca7-17f5-4a5a-8c68-98cc111a2495 -f51dc5a7-37b7-4ce1-859a-b7cb58be6494 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f51dc5a7-37b7-4ce1-859a-b7cb58be6494 -287d3cc1-1cd0-449a-b280-87c529e33951 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/287d3cc1-1cd0-449a-b280-87c529e33951 -381555eb-f2e3-4ef0-8303-d759c00b390a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a -0f60a148-7024-44bd-bbed-377cbece9d1b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/0f60a148-7024-44bd-bbed-377cbece9d1b -09f950d4-9366-4e7b-98b3-9057999f8f38 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/09f950d4-9366-4e7b-98b3-9057999f8f38 -1100c966-9671-4bc6-8b68-6d408a910da1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1 -c271a802-d324-48a6-b01d-63e4a72aa43e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e -6e315abe-a080-4369-8729-4aea2dee8494 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/6e315abe-a080-4369-8729-4aea2dee8494 -8b54e56e-c693-4594-998f-5bd6c1f385d7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/8b54e56e-c693-4594-998f-5bd6c1f385d7 -e5db7c9b-de48-4302-905b-9570bb6e7ade .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/e5db7c9b-de48-4302-905b-9570bb6e7ade -a8f35fca-8a15-4833-b568-326f0cc89bfa .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa -12c986be-d19a-4b8b-b1b5-68248ff4d331 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331 -704b37ab-01bb-43d3-9e9f-f0d354f63c7d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/704b37ab-01bb-43d3-9e9f-f0d354f63c7d -c454aa67-ca30-43e8-9be4-58cbddd01b63 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/c454aa67-ca30-43e8-9be4-58cbddd01b63 -4012c6cc-1300-4f6b-af0e-9176eedf8de7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/4012c6cc-1300-4f6b-af0e-9176eedf8de7 -9ce2f015-8ea0-43a5-a03d-fc36f6d202fe .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe -e1ff6c81-37d8-43ee-9dcf-17a89e07556a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/40dac9af-951e-4b98-8779-9ba02c37f8a1/comments/e1ff6c81-37d8-43ee-9dcf-17a89e07556a -b17a561a-6100-490e-84eb-d1ae4b617940 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/b17a561a-6100-490e-84eb-d1ae4b617940 -f77fc673-c852-4c81-bfa2-1d59de2661c8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f77fc673-c852-4c81-bfa2-1d59de2661c8 -4d192c6c-a4a8-4844-b083-2dd5926bd2d9 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/4d192c6c-a4a8-4844-b083-2dd5926bd2d9 -a63bd76a-cd43-4f97-88ba-2323546d4572 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a63bd76a-cd43-4f97-88ba-2323546d4572 -4f7a4c3b-31e3-4023-8c9d-e67f627a34f0 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0 -ecc91b94-7f3f-44a7-af58-03191d327a7f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ecc91b94-7f3f-44a7-af58-03191d327a7f -202e0dc6-61bf-4b17-a8bd-f8a27482cb68 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0cad2ac6-76ef-4a88-abdf-b2e02de76f5c/comments/202e0dc6-61bf-4b17-a8bd-f8a27482cb68 -e5decfc6-050b-4283-8776-977bf85b2c99 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/e5decfc6-050b-4283-8776-977bf85b2c99 -0ac3c4cb-90e3-4b67-b6cb-1186d5d66240 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240 -d5ed4f87-f1a1-4138-b0ad-190e4a49d820 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/d5ed4f87-f1a1-4138-b0ad-190e4a49d820 -b76434a3-5cf9-4d2c-820b-64444289c09f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f -8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/8c1c4f38-a8d4-4cf9-a9f0-e9846ebbcad8 -c894f10f-197d-4b22-9c5b-19f394df40d4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4 -ea01c122-e629-4d5c-afa7-b180f4a8748b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/ea01c122-e629-4d5c-afa7-b180f4a8748b -22b6f620-d2f7-42a5-a02e-145733a4e366 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366 -25c67b0b-1afd-4613-a787-e0f018614966 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966 -fd7ab206-5937-4ede-9e78-97aff098b677 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/fd7ab206-5937-4ede-9e78-97aff098b677 -da97e18f-33d6-469e-9d93-6457b9a6bfca .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/da97e18f-33d6-469e-9d93-6457b9a6bfca -15602c0c-25e4-4c2c-9e24-79bdb90721b1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1 -4c50ca0b-a08f-4723-b00d-4bf342cf86b6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/52034fd0-ec50-424d-b25d-2beaf2d2c317/comments/4c50ca0b-a08f-4723-b00d-4bf342cf86b6 -d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/d81d0df9-e6d9-4fe8-8dbe-989ef2c81f00 -06e45775-1c46-4793-a34e-2cc86a8db097 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c271a802-d324-48a6-b01d-63e4a72aa43e/comments/06e45775-1c46-4793-a34e-2cc86a8db097 -09f84059-fc8e-4954-b24d-a2b33ef21bf4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4 -a536cee5-cc8d-4b18-b491-657e0c7998b4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a536cee5-cc8d-4b18-b491-657e0c7998b4 -f847c981-873e-41ae-b5ce-83dfe60b9afe .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3/comments/f847c981-873e-41ae-b5ce-83dfe60b9afe -6010e186-0260-44e5-8442-8df2269910ce .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/17921fbc-e7f0-4f31-8cdd-598e5ba7237b/comments/6010e186-0260-44e5-8442-8df2269910ce -8d927822-eff9-42c4-9541-8b784b3f7db2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb/comments/8d927822-eff9-42c4-9541-8b784b3f7db2 -478443b3-dd69-4719-b79a-b1279f75b8e4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/478443b3-dd69-4719-b79a-b1279f75b8e4 -3613e6e9-db9e-4775-8914-f31f0b4b81ac .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3613e6e9-db9e-4775-8914-f31f0b4b81ac -acbecd72-988c-4899-a340-fea370ce15a8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/acbecd72-988c-4899-a340-fea370ce15a8 -4be73baf-e46b-4acb-a58e-4719e57c550b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6/comments/4be73baf-e46b-4acb-a58e-4719e57c550b -b0e7165b-7099-45ca-9513-412225f7bd52 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/b0e7165b-7099-45ca-9513-412225f7bd52 -4068c833-0c06-475e-8b7e-6701bc416dee .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/4068c833-0c06-475e-8b7e-6701bc416dee -e0155831-499f-421a-ad02-cd15fc3fecf1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0155831-499f-421a-ad02-cd15fc3fecf1 -1847f1f8-525a-42c4-ae2b-e9377459d2a6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/1847f1f8-525a-42c4-ae2b-e9377459d2a6 -0a995544-20dc-42a6-8d3f-348ebbc8921e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/56506b73-36cc-4e32-a578-258a219edba8/comments/0a995544-20dc-42a6-8d3f-348ebbc8921e -a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2 -f92c6180-0ed8-4acc-8ced-22995a0c016b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f92c6180-0ed8-4acc-8ced-22995a0c016b -6622c06a-ed84-4d45-8011-a082fca219b6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6622c06a-ed84-4d45-8011-a082fca219b6 -9daa72ee-0721-4f68-99ee-f06fec0b340e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9daa72ee-0721-4f68-99ee-f06fec0b340e -c45e5ece-63e3-4fd2-b33f-0bfd06820cf4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c45e5ece-63e3-4fd2-b33f-0bfd06820cf4 -b19a8f6a-1d7b-4887-a9df-123d59b0cd9b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/b19a8f6a-1d7b-4887-a9df-123d59b0cd9b -f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a -fdb615a4-168a-467b-8090-875c998455e5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/fdb615a4-168a-467b-8090-875c998455e5 -bb406a33-92b6-46dd-950c-c7cfb5440e7b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/bb406a33-92b6-46dd-950c-c7cfb5440e7b -5e339bac-f4f3-407b-974a-b88795d3573b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/5e339bac-f4f3-407b-974a-b88795d3573b -cf77c72d-b099-413a-802e-a8892ac8c26b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf77c72d-b099-413a-802e-a8892ac8c26b -0ca2d112-b5bb-4df1-8ac0-e46db6cdd442 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/0ca2d112-b5bb-4df1-8ac0-e46db6cdd442 -ae0f9aea-960c-42b4-82df-943bbbe17d58 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58 -a492508e-0be7-4403-bbd0-9cdc0a46b06b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/a492508e-0be7-4403-bbd0-9cdc0a46b06b -942cd941-583d-4020-99e4-80de7e836129 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129 -2ae039de-5b0d-4a4f-aa80-6c81d1345367 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e83da06-26f1-4763-a972-dae7e7062233/comments/2ae039de-5b0d-4a4f-aa80-6c81d1345367 -b8bbd433-9017-4c04-a038-2a7370a3adc7 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2103f60c-36e5-4b05-b57c-8c6fee2d80d4/comments/b8bbd433-9017-4c04-a038-2a7370a3adc7 -1ba36272-7ae1-4f95-8002-7b45e62e6790 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/1ba36272-7ae1-4f95-8002-7b45e62e6790 -7fa903a3-f9e6-4e4d-8128-0f26e1ce664b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/7fa903a3-f9e6-4e4d-8128-0f26e1ce664b -68ba7f0c-ca5f-4f49-a508-e39150c07e13 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13 -ec16300f-529a-4492-8327-f9a72e4447c2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ba4bc51-b251-483a-a67a-f1b89c83f6af/comments/ec16300f-529a-4492-8327-f9a72e4447c2 -b900f7fd-bab6-48c4-922c-a051f933da58 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/b900f7fd-bab6-48c4-922c-a051f933da58 -508ea95e-7bc6-4b9b-9e36-a3a87014423d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d -2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c4ea43d5-4964-49ea-a1eb-2bab2bde8e2e/comments/2ca25dd6-e9d1-4581-bd29-50f2eaa32fe4 -e757d2ae-085a-4539-99be-096386de5352 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57/comments/e757d2ae-085a-4539-99be-096386de5352 -c35835c0-8f9f-4090-ba92-1f616867e486 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/c35835c0-8f9f-4090-ba92-1f616867e486 -520a9829-8d90-43ce-be64-868b8321e5b0 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e4ed63f6-9000-4d0b-98c3-487269140141/comments/520a9829-8d90-43ce-be64-868b8321e5b0 -3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2/comments/3415fbd7-5a7e-4a7f-af30-82f8ce6ca85b -00f26f04-9202-4288-8744-b29abc2342d6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/00f26f04-9202-4288-8744-b29abc2342d6 -7b904395-86e9-4eb1-8534-69cec63801d4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e0858b12-0be3-49bb-ad7a-030e488bb2f1/comments/7b904395-86e9-4eb1-8534-69cec63801d4 -e03b65da-8c31-4a8c-a938-ef1254ef0a80 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/e03b65da-8c31-4a8c-a938-ef1254ef0a80 -16357f68-19c0-4bf9-8220-b88b52b3456d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/16357f68-19c0-4bf9-8220-b88b52b3456d -303986f2-0b17-4589-bf76-ed1461699c3e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e/comments/303986f2-0b17-4589-bf76-ed1461699c3e -31cd490d-a1c2-4ab3-8284-d80395e34dd2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/31cd490d-a1c2-4ab3-8284-d80395e34dd2 -8385a1fb-63df-4ca6-81cd-28ede83bb0c2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8385a1fb-63df-4ca6-81cd-28ede83bb0c2 -0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1 -744435b7-1521-4059-a55d-f0c403d7b4d8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/744435b7-1521-4059-a55d-f0c403d7b4d8 -b3c6da51-3a30-42c9-8c75-587c7a1705c5 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b3c6da51-3a30-42c9-8c75-587c7a1705c5 -9b1a0e71-4f7d-40b1-ab32-18496bf19a3f .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9b1a0e71-4f7d-40b1-ab32-18496bf19a3f -a403de79-8f39-41f2-b9ec-15053b175ee2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2 -a4720227-43cf-49aa-8f9f-f49f46e3e809 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/a4720227-43cf-49aa-8f9f-f49f46e3e809 -24903c62-f441-496e-9dcf-17e7a581df33 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33 -f05359f6-1bfc-4aa6-9a6d-673516bc0f94 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94 -b187fbce-fb10-4819-ace2-c8b0b4a45c57 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b187fbce-fb10-4819-ace2-c8b0b4a45c57 -9aa88bbd-71d0-44fa-804d-3562171f9539 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539 -7bfc591e-584a-476e-8e11-b548f1afcaa6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6 -5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7bfc591e-584a-476e-8e11-b548f1afcaa6/comments/5a6b44f5-9d1d-4e2e-a42c-f5423c43a1dc -496edad5-1484-413a-bc68-4b01274a65eb .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/496edad5-1484-413a-bc68-4b01274a65eb -8e1bbda4-35b6-4579-849d-117b1596ee99 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99 -f925e56f-26f9-4620-82fb-a0f160f27921 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/f925e56f-26f9-4620-82fb-a0f160f27921 -cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/cdf15bdd-d3fe-4251-9d0b-f1b687e9a26c -401950a0-a5ff-46f3-afac-a9cfb300f94b .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/7ec2c071-9630-42b0-b08a-9854616f9144/comments/401950a0-a5ff-46f3-afac-a9cfb300f94b -55263144-9775-4b18-ab83-29d66ed91a53 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/55263144-9775-4b18-ab83-29d66ed91a53 -e520239c-8d69-4ff6-b1bd-0c2f74366200 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/e520239c-8d69-4ff6-b1bd-0c2f74366200 -83202b83-eea8-452f-8239-d468940bddba .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/83202b83-eea8-452f-8239-d468940bddba -4be35966-373b-438c-a35a-824f5c7a940a .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/9ce2f015-8ea0-43a5-a03d-fc36f6d202fe/comments/4be35966-373b-438c-a35a-824f5c7a940a -13012b22-2d02-444c-87c0-8cf0f17137ae .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/12c986be-d19a-4b8b-b1b5-68248ff4d331/comments/13012b22-2d02-444c-87c0-8cf0f17137ae -e173c09a-1b3e-4d8a-a86a-6b8c94a76247 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/508ea95e-7bc6-4b9b-9e36-a3a87014423d/comments/e173c09a-1b3e-4d8a-a86a-6b8c94a76247 -02223264-e28a-4720-9f20-1e7a27a7041d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/02223264-e28a-4720-9f20-1e7a27a7041d -62a74b85-0d4b-49f5-8794-74bafd871cd4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/62a74b85-0d4b-49f5-8794-74bafd871cd4 -3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/3e331b72-51fd-4408-bc0d-b6c5ac3b9f3e -96abea83-9867-4c21-8eb8-9e1b1093cba4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/529c290e-b1cf-4800-be7e-68f1ecb9565c/comments/96abea83-9867-4c21-8eb8-9e1b1093cba4 -72dab0c4-f04d-4ff0-9319-f55aafaea627 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/576e804a-8b76-4876-8e9d-d7a72b0aef10/comments/72dab0c4-f04d-4ff0-9319-f55aafaea627 -dac91856-cb6a-4f69-8c03-38ff0b29aab2 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dac91856-cb6a-4f69-8c03-38ff0b29aab2 -e249e2aa-2029-4a96-bc84-962366e07fd6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/e249e2aa-2029-4a96-bc84-962366e07fd6 -0e5fab2a-66eb-4f7d-979f-b50181f604d4 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/0e5fab2a-66eb-4f7d-979f-b50181f604d4 -208595bd-35b8-44c2-bf97-fc5ef9e7a58d .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d -f2011471-56cb-46e2-813b-1ac336ee7bbc .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6eb8141f-b0b1-4d5b-b4e6-d0860d844ada/comments/f2011471-56cb-46e2-813b-1ac336ee7bbc -bcd6e5d4-8d03-43ad-a10d-17619735d077 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/comments/bcd6e5d4-8d03-43ad-a10d-17619735d077 -e5248100-ea02-4205-a4c1-ac7a577c6362 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/2f048ac5-5564-4b34-b7f9-605357267ed2/comments/e5248100-ea02-4205-a4c1-ac7a577c6362 -6555a651-5a7f-4a8a-9793-47ad1315e9e8 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/22b6f620-d2f7-42a5-a02e-145733a4e366/comments/6555a651-5a7f-4a8a-9793-47ad1315e9e8 -c76d7899-d495-4103-9355-012c0a6fece3 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/c76d7899-d495-4103-9355-012c0a6fece3 -2628eeca-96c6-4933-8484-d55bb1dbf985 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985 -21c90231-d7f2-49bb-97d9-99e16459d799 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/d9959864-ea91-475a-a075-f39aa6760f98/comments/21c90231-d7f2-49bb-97d9-99e16459d799 -6d87d87e-7974-412f-b9c1-41a87f752d30 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/6d87d87e-7974-412f-b9c1-41a87f752d30 -51930348-9ccc-4165-af41-6c7450de050e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/51930348-9ccc-4165-af41-6c7450de050e -764b812f-a0bb-4f4d-8e2f-c255c9474a0e .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/764b812f-a0bb-4f4d-8e2f-c255c9474a0e -f70dd5df-805b-49f3-a9ce-12e0fae63365 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365 -ae56365e-7a9c-4cc3-ba67-7addbeeeff49 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49 -8015d736-f3ea-4085-940c-552c01a287ef .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef -be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf -d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/dcca51b3-bf8f-4482-8f67-662cfbcb9c6c/comments/d4a87066-c5f4-49f1-9bd9-a872c8e4ffe6 diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 3b66019..8a8b3ca 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -783,9 +783,11 @@ os.listdir(self.get_path("bugs")): return default relpath = self._u_rel_path(path) try: - contents = self._vcs_get_file_contents(relpath,revision) + contents = self._vcs_get_file_contents(relpath, revision) except InvalidID, e: - raise InvalidPath(path=path, root=self.repo, id=id) + if InvalidID == None: + e.id = InvalidID + raise if contents in [libbe.storage.base.InvalidDirectory, libbe.util.InvalidObject]: raise InvalidID(id) diff --git a/libbe/storage/vcs/bzr.py b/libbe/storage/vcs/bzr.py index d6e7799..397267a 100644 --- a/libbe/storage/vcs/bzr.py +++ b/libbe/storage/vcs/bzr.py @@ -129,7 +129,7 @@ class Bzr(base.VCS): cmd.run(filename=path, revision=revision) except bzrlib.errors.BzrCommandError, e: if 'not present in revision' in str(e): - raise base.InvalidID(path, revision) + raise base.InvalidPath(path, root=self.repo, revision=revision) raise return cmd.outf.getvalue() @@ -154,7 +154,7 @@ class Bzr(base.VCS): cmd.run(revision=revision, path=path) except bzrlib.errors.BzrCommandError, e: if 'not present in revision' in str(e): - raise base.InvalidID(path, revision) + raise base.InvalidPath(path, root=self.repo, revision=revision) raise children = cmd.outf.getvalue().rstrip('\n').splitlines() children = [self._u_rel_path(c, path) for c in children] diff --git a/libbe/storage/vcs/git.py b/libbe/storage/vcs/git.py index 8d1b529..35dcd68 100644 --- a/libbe/storage/vcs/git.py +++ b/libbe/storage/vcs/git.py @@ -118,6 +118,26 @@ class Git(base.VCS): status,output,error = self._u_invoke_client('show', arg) return output + + def _vcs_path(self, id, revision): + return self._u_find_id(id, revision) + + def _vcs_isdir(self, path, revision): + arg = '%s:%s' % (revision,path) + args = ['ls-tree', arg] + status,output,error = self._u_invoke_client(*args, expect=(0,128)) + if status != 0: + if 'not a tree object' in error: + return False + raise base.CommandError(args, status, stderr=error) + return True + + def _vcs_listdir(self, path, revision): + arg = '%s:%s' % (revision,path) + status,output,error = self._u_invoke_client( + 'ls-tree', '--name-only', arg) + return output.rstrip('\n').splitlines() + def _vcs_commit(self, commitfile, allow_empty=False): args = ['commit', '--all', '--file', commitfile] if allow_empty == True: -- cgit From d1726c47dccc7a7c7db1f038bc30a5712bb70153 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 04:43:42 -0500 Subject: Adapted BugDir.duplicate_bugdir() to use revision-dependend Storage.children() --- libbe/bugdir.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 50dc8ba..cec1e3b 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -300,17 +300,23 @@ class BugDir (list, settings_object.SavedSettingsObject): raise libbe.storage.InvalidStorageVersion(storage_version) s = copy.deepcopy(self.storage) s.writeable = False - class RevisionedStorageGet (object): + class RevisionedStorage (object): def __init__(self, storage, default_revision): self.s = storage self.sget = self.s.get + self.schildren = self.s.children self.r = default_revision def get(self, *args, **kwargs): if not 'revision' in kwargs or kwargs['revision'] == None: kwargs['revision'] = self.r return self.sget(*args, **kwargs) - rsg = RevisionedStorageGet(s, revision) - s.get = rsg.get + def children(self, *args, **kwargs): + if not 'revision' in kwargs or kwargs['revision'] == None: + kwargs['revision'] = self.r + return self.schildren(*args, **kwargs) + rs = RevisionedStorage(s, revision) + s.get = rs.get + s.children = rs.children dbd = BugDir(s, from_storage=True) # dbd = copy.copy(self) # dbd.storage = copy.copy(self.storage) -- cgit From 0aa80631bd2dc0a5f28f1dd7db2cbda7d14e67fe Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 05:52:27 -0500 Subject: Hg storage now based off mercurial module, not 'hg' executible. This should make repeated calls to Hg storage instances _much_ faster, since we avoid repeatedly loading and tearing down a python subprocess. For example, the testsuite runs ~6x faster on my box. Here's a run with the old Hg implementation: $ python test.py libbe.storage.vcs.hg ... ================================= ERROR: test_get_previous_children --------------------------------- Traceback (most recent call last): ... NotImplementedError --------------------------------- Ran 49 tests in 133.285s FAILED (errors=1) A run with the new implementation gives the same results, except for: Ran 49 tests in 22.328s --- libbe/storage/vcs/bzr.py | 2 +- libbe/storage/vcs/hg.py | 60 +++++++++++++++++++++++++++++++----------------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/libbe/storage/vcs/bzr.py b/libbe/storage/vcs/bzr.py index 397267a..7335861 100644 --- a/libbe/storage/vcs/bzr.py +++ b/libbe/storage/vcs/bzr.py @@ -51,7 +51,7 @@ def new(): class Bzr(base.VCS): name = 'bzr' - client = None # bzrlib + client = None # bzrlib module def __init__(self, *args, **kwargs): base.VCS.__init__(self, *args, **kwargs) diff --git a/libbe/storage/vcs/hg.py b/libbe/storage/vcs/hg.py index d2d3281..373dfd2 100644 --- a/libbe/storage/vcs/hg.py +++ b/libbe/storage/vcs/hg.py @@ -21,10 +21,21 @@ Mercurial (hg) backend. """ +try: + # enable importing on demand to reduce startup time + from mercurial import demandimport; demandimport.enable() + import mercurial + import mercurial.version + import mercurial.dispatch + import mercurial.ui +except ImportError: + mercurial = None import os import os.path import re import shutil +import StringIO +import sys import time # work around http://mercurial.selenic.com/bts/issue618 import libbe @@ -41,7 +52,7 @@ def new(): class Hg(base.VCS): name='hg' - client='hg' + client=None # mercurial module def __init__(self, *args, **kwargs): base.VCS.__init__(self, *args, **kwargs) @@ -49,13 +60,26 @@ class Hg(base.VCS): self.__updated = [] # work around http://mercurial.selenic.com/bts/issue618 def _vcs_version(self): - status,output,error = self._u_invoke_client('--version') - return output + if mercurial == None: + return None + return mercurial.version.get_version() + + def _u_invoke_client(self, *args, **kwargs): + if 'cwd' not in kwargs: + kwargs['cwd'] = self.repo + assert len(kwargs) == 1, kwargs + ui = mercurial.ui.ui(interactive=False) + fullargs = ['--cwd', kwargs['cwd']] + fullargs.extend(args) + stdout = sys.stdout + tmp_stdout = StringIO.StringIO() + sys.stdout = tmp_stdout + mercurial.dispatch._dispatch(ui, fullargs) + sys.stdout = stdout + return tmp_stdout.getvalue().rstrip('\n') def _vcs_get_user_id(self): - status,output,error = self._u_invoke_client( - 'showconfig', 'ui.username') - return output.rstrip('\n') + return self._u_invoke_client('showconfig', 'ui.username') def _vcs_detect(self, path): """Detect whether a directory is revision-controlled using Mercurial""" @@ -64,8 +88,7 @@ class Hg(base.VCS): return False def _vcs_root(self, path): - status,output,error = self._u_invoke_client('root', cwd=path) - return output.rstrip('\n') + return self._u_invoke_client('root', cwd=path) def _vcs_init(self, path): self._u_invoke_client('init', cwd=path) @@ -88,13 +111,11 @@ class Hg(base.VCS): if revision == None: return base.VCS._vcs_get_file_contents(self, path, revision) else: - status,output,error = \ - self._u_invoke_client('cat', '-r', revision, path) - return output + return self._u_invoke_client('cat', '-r', revision, path) def _vcs_commit(self, commitfile, allow_empty=False): args = ['commit', '--logfile', commitfile] - status,output,error = self._u_invoke_client(*args) + output = self._u_invoke_client(*args) # work around http://mercurial.selenic.com/bts/issue618 strings = ['nothing changed'] if self._u_any_in_string(strings, output) == True \ @@ -102,7 +123,7 @@ class Hg(base.VCS): time.sleep(1) for path in self.__updated: os.utime(os.path.join(self.repo, path), None) - status,output,error = self._u_invoke_client(*args) + output = self._u_invoke_client(*args) self.__updated = [] # end work around if allow_empty == False: @@ -115,14 +136,11 @@ class Hg(base.VCS): if index > 0: index -= 1 args = ['identify', '--rev', str(int(index)), '--%s' % style] - kwargs = {'expect': (0,255)} - status,output,error = self._u_invoke_client(*args, **kwargs) - if status == 0: - id = output.strip() - if id == '000000000000': - return None # before initial commit. - return id - return None + output = self._u_invoke_client(*args) + id = output.strip() + if id == '000000000000': + return None # before initial commit. + return id if libbe.TESTING == True: -- cgit From aba3a8b27063a1765c49194cb7f9aba8b277d92f Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 06:36:23 -0500 Subject: Updated Hg backend to support .children(revision). --- libbe/storage/vcs/base.py | 9 +++++---- libbe/storage/vcs/hg.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 8a8b3ca..040c3f9 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -740,12 +740,10 @@ os.listdir(self.get_path("bugs")): def _children(self, id=None, revision=None): if revision == None: id_to_path = self._cached_path_id.path - path_to_id = self._cached_path_id.id isdir = os.path.isdir listdir = os.listdir else: id_to_path = lambda id : self._vcs_path(id, revision) - path_to_id = self._cached_path_id.id isdir = lambda path : self._vcs_isdir(path, revision) listdir = lambda path : self._vcs_listdir(path, revision) if id==None: @@ -770,7 +768,7 @@ os.listdir(self.get_path("bugs")): and self._vcs_is_versioned(cpath) == False: children[i] = None else: - children[i] = path_to_id(cpath) + children[i] = self._u_path_to_id(cpath) children[i] return [c for c in children if c != None] @@ -895,7 +893,10 @@ os.listdir(self.get_path("bugs")): for child in self._vcs_listdir(path, revision): stack.append((os.path.join(path, child), '/'.join([long_id, child]))) - return None + raise InvalidID(id, revision=revision) + + def _u_path_to_id(self, path): + return self._cached_path_id.id(path) def _u_rel_path(self, path, root=None): """ diff --git a/libbe/storage/vcs/hg.py b/libbe/storage/vcs/hg.py index 373dfd2..6baf19c 100644 --- a/libbe/storage/vcs/hg.py +++ b/libbe/storage/vcs/hg.py @@ -43,7 +43,6 @@ import base if libbe.TESTING == True: import doctest - import sys import unittest @@ -113,6 +112,38 @@ class Hg(base.VCS): else: return self._u_invoke_client('cat', '-r', revision, path) + def _vcs_path(self, id, revision): + output = self._u_invoke_client('manifest', '--rev', revision) + be_dir = self._cached_path_id._spacer_dirs[0] + be_dir_sep = self._cached_path_id._spacer_dirs[0] + os.path.sep + files = [f for f in output.splitlines() if f.startswith(be_dir_sep)] + for file in files: + if not file.startswith(be_dir+os.path.sep): + continue + parts = file.split(os.path.sep) + dir = parts.pop(0) # don't add the first spacer dir + for part in parts[:-1]: + dir = os.path.join(dir, part) + if not dir in files: + files.append(dir) + for file in files: + if self._u_path_to_id(file) == id: + return file + raise base.InvalidId(id, revision=revision) + + def _vcs_isdir(self, path, revision): + output = self._u_invoke_client('manifest', '--rev', revision) + files = output.splitlines() + if path in files: + return False + return True + + def _vcs_listdir(self, path, revision): + output = self._u_invoke_client('manifest', '--rev', revision) + files = output.splitlines() + path = path.rstrip(os.path.sep) + os.path.sep + return [self._u_rel_path(f, path) for f in files if f.startswith(path)] + def _vcs_commit(self, commitfile, allow_empty=False): args = ['commit', '--logfile', commitfile] output = self._u_invoke_client(*args) -- cgit From 8bf8e2271f8273bdba3f8327d08b505a0fae11d5 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 06:40:38 -0500 Subject: Adjust Git._vcs_isdir() to Python-2.5-compatible syntax --- libbe/storage/vcs/git.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libbe/storage/vcs/git.py b/libbe/storage/vcs/git.py index 35dcd68..2280665 100644 --- a/libbe/storage/vcs/git.py +++ b/libbe/storage/vcs/git.py @@ -125,7 +125,8 @@ class Git(base.VCS): def _vcs_isdir(self, path, revision): arg = '%s:%s' % (revision,path) args = ['ls-tree', arg] - status,output,error = self._u_invoke_client(*args, expect=(0,128)) + kwargs = {'expect':(0,128)} + status,output,error = self._u_invoke_client(*args, **kwargs) if status != 0: if 'not a tree object' in error: return False -- cgit From 6dafff3ad4a88d8af7b1cd4837b90179ac34a1e3 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 09:46:59 -0500 Subject: Added root directory handling to VCS._u_rel_path(). Now it returns '.' when you ask for the relative path from root to itself. It used to raise AssertionError or InvalidPath. --- libbe/storage/vcs/base.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 040c3f9..7565caf 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -904,6 +904,10 @@ os.listdir(self.get_path("bugs")): >>> vcs = new() >>> vcs._u_rel_path("/a.b/c/.be", "/a.b/c") '.be' + >>> vcs._u_rel_path("/a.b/c/", "/a.b/c") + '.' + >>> vcs._u_rel_path("/a.b/c/", "/a.b/c/") + '.' """ if root == None: if self.repo == None: @@ -912,10 +916,10 @@ os.listdir(self.get_path("bugs")): path = os.path.abspath(path) absRoot = os.path.abspath(root) absRootSlashedDir = os.path.join(absRoot,"") + if path in [absRoot, absRootSlashedDir]: + return '.' if not path.startswith(absRootSlashedDir): raise InvalidPath(path, absRootSlashedDir) - assert path != absRootSlashedDir, \ - "file %s == root directory %s" % (path, absRootSlashedDir) relpath = path[len(absRootSlashedDir):] return relpath @@ -1093,4 +1097,5 @@ if libbe.TESTING == True: make_vcs_testcase_subclasses(VCS, sys.modules[__name__]) unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) - suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + #suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) + suite = unittest.TestSuite([doctest.DocTestSuite()]) -- cgit From b2d5da700c83f693191573c300aee1ffb80a8e98 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 10:18:20 -0500 Subject: Added an additional VCS._u_rel_path() unittest. Also re-enabled the unitsuite in libbe.storage.vcs.base, which I'd disabled while testing the VCS unittests. --- libbe/storage/vcs/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 7565caf..533ef4c 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -908,6 +908,8 @@ os.listdir(self.get_path("bugs")): '.' >>> vcs._u_rel_path("/a.b/c/", "/a.b/c/") '.' + >>> vcs._u_rel_path("./a", ".") + 'a' """ if root == None: if self.repo == None: @@ -1097,5 +1099,4 @@ if libbe.TESTING == True: make_vcs_testcase_subclasses(VCS, sys.modules[__name__]) unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) - #suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) - suite = unittest.TestSuite([doctest.DocTestSuite()]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) -- cgit From 268713c0e2ed76edd84a2196b5c14fe1bc4ff08a Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 10:39:09 -0500 Subject: Updated Darcs backend towards supporting .children(revision). ._vcs_isdir() and ._vcs_listdir() will need to parse the output of darcs show files [options] --patch REVISION PATH but both the --patch option and the PATH argument are new, and I can't get a recent enough version of Darcs to compile on my system. Theoretically they will work, but they remain untested for now. I don't think it's worth rolling my own darcs show files --patch REVISION to support earlier versions of Darcs, since the only solution I can think of now would be to check out the given revision and use os.walk() or some such, and that would be really ugly... Also added .version_cmp() for easy version comparison. Reindented ._vcs_get_file_contents() to remove trailing elses since the if clauses all contain returns. --- libbe/storage/vcs/darcs.py | 154 +++++++++++++++++++++++++++++++++------------ 1 file changed, 114 insertions(+), 40 deletions(-) diff --git a/libbe/storage/vcs/darcs.py b/libbe/storage/vcs/darcs.py index 9a371d9..6d47ea5 100644 --- a/libbe/storage/vcs/darcs.py +++ b/libbe/storage/vcs/darcs.py @@ -53,9 +53,47 @@ class Darcs(base.VCS): def _vcs_version(self): status,output,error = self._u_invoke_client('--version') - num_part = output.split(' ')[0] - self.parsed_version = [int(i) for i in num_part.split('.')] - return output + return output.rstrip('\n') + + def version_cmp(self, *args): + """ + Compare the installed darcs version V_i with another version + V_o (given in *args). Returns + 1 if V_i > V_o, + 0 if V_i == V_o, and + -1 if V_i < V_o + >>> d = Darcs(repo='.') + >>> d._vcs_version = lambda : "2.3.1 (release)" + >>> d.version_cmp(2,3,1) + 0 + >>> d.version_cmp(2,3,2) + -1 + >>> d.version_cmp(2,3,0) + 1 + >>> d.version_cmp(3) + -1 + >>> d._vcs_version = lambda : "2.0.0pre2" + >>> d._parsed_version = None + >>> d.version_cmp(3) + Traceback (most recent call last): + ... + NotImplementedError: Cannot parse "2.0.0pre2" portion of Darcs version "2.0.0pre2" + invalid literal for int() with base 10: '0pre2' + """ + if not hasattr(self, '_parsed_version') \ + or self._parsed_version == None: + num_part = self._vcs_version().split(' ')[0] + try: + self._parsed_version = [int(i) for i in num_part.split('.')] + except ValueError, e: + raise NotImplementedError( + 'Cannot parse "%s" portion of Darcs version "%s"\n %s' + % (num_part, self._vcs_version(), str(e))) + cmps = [cmp(a,b) for a,b in zip(self._parsed_version, args)] + for c in cmps: + if c != 0: + return c + return 0 def _vcs_get_user_id(self): # following http://darcs.net/manual/node4.html#SECTION00410030000000000000 @@ -113,45 +151,81 @@ class Darcs(base.VCS): def _vcs_get_file_contents(self, path, revision=None): if revision == None: return base.VCS._vcs_get_file_contents(self, path, revision) + if self.version_cmp(2, 0, 0) == 1: + status,output,error = self._u_invoke_client( \ + 'show', 'contents', '--patch', revision, path) + return output + # Darcs versions < 2.0.0pre2 lack the 'show contents' command + + status,output,error = self._u_invoke_client( \ + 'diff', '--unified', '--from-patch', revision, path, + unicode_output=False) + major_patch = output + status,output,error = self._u_invoke_client( \ + 'diff', '--unified', '--patch', revision, path, + unicode_output=False) + target_patch = output + + # '--output -' to be supported in GNU patch > 2.5.9 + # but that hasn't been released as of June 30th, 2009. + + # Rewrite path to status before the patch we want + args=['patch', '--reverse', path] + status,output,error = self._u_invoke(args, stdin=major_patch) + # Now apply the patch we want + args=['patch', path] + status,output,error = self._u_invoke(args, stdin=target_patch) + + if os.path.exists(os.path.join(self.repo, path)) == True: + contents = base.VCS._vcs_get_file_contents(self, path) else: - if self.parsed_version[0] >= 2: - status,output,error = self._u_invoke_client( \ - 'show', 'contents', '--patch', revision, path) - return output + contents = '' + + # Now restore path to it's current incarnation + args=['patch', '--reverse', path] + status,output,error = self._u_invoke(args, stdin=target_patch) + args=['patch', path] + status,output,error = self._u_invoke(args, stdin=major_patch) + current_contents = base.VCS._vcs_get_file_contents(self, path) + return contents + + def _vcs_path(self, id, revision): + return self._u_find_id(id, revision) + + def _vcs_isdir(self, path, revision): + if self.version_cmp(2, 3, 1) == 1: + # Sun Nov 15 20:32:06 EST 2009 thomashartman1@gmail.com + # * add versioned show files functionality (darcs show files -p 'some patch') + status,output,error = self._u_invoke_client( \ + 'show', 'files', '--no-files', '--patch', revision) + children = output.rstrip('\n').splitlines() + rpath = '.' + children = [self._u_rel_path(c, rpath) for c in children] + if path in children: + return True + return False + # Darcs versions <= 2.3.1 lack the --patch option for 'show files' + raise NotImplementedError + + def _vcs_listdir(self, path, revision): + if self.version_cmp(2, 3, 1) == 1: + # Sun Nov 15 20:32:06 EST 2009 thomashartman1@gmail.com + # * add versioned show files functionality (darcs show files -p 'some patch') + # Wed Dec 9 05:42:21 EST 2009 Luca Molteni + # * resolve issue835 show file with file directory arguments + path = path.rstrip(os.path.sep) + status,output,error = self._u_invoke_client( \ + 'show', 'files', '--patch', revision, path) + files = output.rstrip('\n').splitlines() + if path == '.': + descendents = [self._u_rel_path(f, path) for f in files + if f != '.'] else: - # Darcs versions < 2.0.0pre2 lack the 'show contents' command - - status,output,error = self._u_invoke_client( \ - 'diff', '--unified', '--from-patch', revision, path, - unicode_output=False) - major_patch = output - status,output,error = self._u_invoke_client( \ - 'diff', '--unified', '--patch', revision, path, - unicode_output=False) - target_patch = output - - # '--output -' to be supported in GNU patch > 2.5.9 - # but that hasn't been released as of June 30th, 2009. - - # Rewrite path to status before the patch we want - args=['patch', '--reverse', path] - status,output,error = self._u_invoke(args, stdin=major_patch) - # Now apply the patch we want - args=['patch', path] - status,output,error = self._u_invoke(args, stdin=target_patch) - - if os.path.exists(os.path.join(self.repo, path)) == True: - contents = base.VCS._vcs_get_file_contents(self, path) - else: - contents = '' - - # Now restore path to it's current incarnation - args=['patch', '--reverse', path] - status,output,error = self._u_invoke(args, stdin=target_patch) - args=['patch', path] - status,output,error = self._u_invoke(args, stdin=major_patch) - current_contents = base.VCS._vcs_get_file_contents(self, path) - return contents + descendents = [self._u_rel_path(f, path) for f in files + if f.startswith(path)] + return [f for f in descendents if f.count(os.path.sep) == 0] + # Darcs versions <= 2.3.1 lack the --patch option for 'show files' + raise NotImplementedError def _vcs_commit(self, commitfile, allow_empty=False): id = self.get_user_id() -- cgit From d0fdc606a0420807cfbde0519d1807bd16f14c37 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 11:28:40 -0500 Subject: Commented #bea/110/781# on why I'm not supporting Arch.child(revision) --- .../comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/body | 14 ++++++++++++++ .../comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/values | 11 +++++++++++ 2 files changed, 25 insertions(+) create mode 100644 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/body create mode 100644 .be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/values diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/body b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/body new file mode 100644 index 0000000..df5b8c5 --- /dev/null +++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/body @@ -0,0 +1,14 @@ +> The situation is worse than just the old `be merge` effects, because +> the existence, children, and parents of a particular UUID may be +> revision dependent. A UUID will always refer to the same +> bugdir/bug/comment, but that bugdir/bug/comment may have different +> relatives. + +I'm not sure how to support .children(revision) in the Arch backend +or the older versions of Darcs without checking out a pristine tree +for the revision in question. That's how we used to support + BugDir.duplicate_bugdir() +but it doesn't fit well with the new Storage system. Since I don't +feel strongly about tla or old Darcs support, I'm leaving that +functionality unimplemented. + diff --git a/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/values b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/values new file mode 100644 index 0000000..d21650d --- /dev/null +++ b/.be/bea86499-824e-4e77-b085-2d581fa9ccab/bugs/1100c966-9671-4bc6-8b68-6d408a910da1/comments/7812d2e5-9d4b-4621-b071-22e91e8757d2/values @@ -0,0 +1,11 @@ +Author: W. Trevor King + + +Content-type: text/plain + + +Date: Tue, 29 Dec 2009 16:20:06 +0000 + + +In-reply-to: 3646e056-a2df-46e5-b877-88608c7cc5af + -- cgit From 4372a17b4215df25b3da0b68daf4d6b490a8955c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 19:00:40 -0500 Subject: Fixed up the completion helpers in libbe.command.util This entailed a fairly thorough cleanup of libbe.util.id. Remaining unimplemented completion helpers: * complete_assigned() * complete_extra_strings() Since these would require scanning all (active?) bugs to compile lists, and I was feeling lazy... --- libbe/bugdir.py | 18 +-- libbe/command/base.py | 6 + libbe/command/util.py | 79 ++++++++++++- libbe/ui/command_line.py | 13 ++- libbe/util/id.py | 286 ++++++++++++++++++++++++++++++++--------------- 5 files changed, 296 insertions(+), 106 deletions(-) diff --git a/libbe/bugdir.py b/libbe/bugdir.py index cec1e3b..737dacf 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -39,6 +39,7 @@ import libbe.storage.util.settings_object as settings_object import libbe.storage.util.mapfile as mapfile import libbe.bug as bug import libbe.util.utility as utility +import libbe.util.id if libbe.TESTING == True: import doctest @@ -73,11 +74,13 @@ class MultipleBugMatches(ValueError): self.shortname = shortname self.matches = matches -class NoBugMatches(KeyError): - def __init__(self, shortname): - msg = "No bug matches %s" % shortname - KeyError.__init__(self, msg) - self.shortname = shortname +class NoBugMatches(libbe.util.id.NoIDMatches): + def __init__(self, *args, **kwargs): + libbe.util.id.NoIDMatches.__init__(self, *args, **kwargs) + def __str__(self): + if self.msg == None: + return 'No bug matches %s' % self.id + return self.msg class DiskAccessRequired (Exception): def __init__(self, goal): @@ -270,8 +273,9 @@ class BugDir (list, settings_object.SavedSettingsObject): def bug_from_uuid(self, uuid): if not self.has_bug(uuid): - raise NoBugMatches('No bug matches %s\n bug map: %s\n repo: %s' \ - % (uuid, self._bug_map, self.storage)) + raise NoBugMatches( + uuid, self.uuids(), + 'No bug matches %s in %s' % (uuid, self.storage)) if self._bug_map[uuid] == None: self._load_bug(uuid) return self._bug_map[uuid] diff --git a/libbe/command/base.py b/libbe/command/base.py index cdb4043..2318aa7 100644 --- a/libbe/command/base.py +++ b/libbe/command/base.py @@ -62,6 +62,12 @@ class CommandInput (object): self.name = name self.help = help + def __str__(self): + return '<%s %s>' % (self.__class__.__name__, self.name) + + def __repr__(self): + return self.__str__() + class Argument (CommandInput): def __init__(self, metavar=None, default=None, type='string', optional=False, repeatable=False, diff --git a/libbe/command/util.py b/libbe/command/util.py index 3bd02d0..a5398cf 100644 --- a/libbe/command/util.py +++ b/libbe/command/util.py @@ -34,17 +34,86 @@ def complete_path(command, argument, fragment=None): return comps def complete_status(command, argument, fragment=None): - return [fragment] + bd = command._get_bugdir() + import libbe.bug + return libbe.bug.status_values + def complete_severity(command, argument, fragment=None): - return [fragment] + bd = command._get_bugdir() + import libbe.bug + return libbe.bug.severity_values + def complete_assigned(command, argument, fragment=None): + if fragment == None: + return [] return [fragment] + def complete_extra_strings(command, argument, fragment=None): + if fragment == None: + return [] return [fragment] + def complete_bug_id(command, argument, fragment=None): - return [fragment] -def complete_bug_comment_id(command, argument, fragment=None): - return [fragment] + return complete_bug_comment_id(command, argument, fragment, + comments=False) + +def complete_bug_comment_id(command, argument, fragment=None, + active_only=True, comments=True): + import libbe.bugdir + import libbe.util.id + bd = command._get_bugdir() + if fragment == None or len(fragment) == 0: + fragment = '/' + try: + p = libbe.util.id.parse_user(bd, fragment) + matches = None + root,residual = (fragment, None) + if not root.endswith('/'): + root += '/' + except libbe.util.id.InvalidIDStructure, e: + return [] + except libbe.util.id.NoIDMatches: + return [] + except libbe.util.id.MultipleIDMatches, e: + if e.common == None: + # choose among bugdirs + return e.matches + common = e.common + matches = e.matches + root,residual = libbe.util.id.residual(common, fragment) + p = libbe.util.id.parse_user(bd, e.common) + bug = None + if matches == None: # fragment was complete, get a list of children uuids + if p['type'] == 'bugdir': + matches = bd.uuids() + common = bd.id.user() + elif p['type'] == 'bug': + if comments == False: + return [fragment] + bug = bd.bug_from_uuid(p['bug']) + matches = bug.uuids() + common = bug.id.user() + else: + assert p['type'] == 'comment', p + return [fragment] + if p['type'] == 'bugdir': + child_fn = bd.bug_from_uuid + elif p['type'] == 'bug': + if comments == False: + return[fragment] + if bug == None: + bug = bd.bug_from_uuid(p['bug']) + child_fn = bug.comment_from_uuid + elif p['type'] == 'comment': + assert matches == None, matches + return [fragment] + possible = [] + common += '/' + for m in matches: + child = child_fn(m) + id = child.id.user() + possible.append(id.replace(common, root)) + return possible def select_values(string, possible_values, name="unkown"): """ diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py index b5a3991..b99f812 100755 --- a/libbe/ui/command_line.py +++ b/libbe/ui/command_line.py @@ -113,15 +113,19 @@ class CmdOptionParser(optparse.OptionParser): self.complete(argument, fragment) for i,arg in enumerate(parsed_args): if arg == '--complete': - if i < len(self.command.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') fragment = None - if i < len(args) - 1: - fragment = args[i+1] + if i < len(parsed_args) - 1: + fragment = parsed_args[i+1] self.complete(argument, fragment) if len(parsed_args) > len(self.command.args) \ and self.command.args[-1].repeatable == False: @@ -149,7 +153,8 @@ class CmdOptionParser(optparse.OptionParser): comps = self.command.complete(argument, fragment) if fragment != None: comps = [c for c in comps if c.startswith(fragment)] - print '\n'.join(comps) + if len(comps) > 0: + print '\n'.join(comps) raise CallbackExit diff --git a/libbe/util/id.py b/libbe/util/id.py index 6b6b51d..f229bef 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -67,33 +67,56 @@ HIERARCHY = ['bugdir', 'bug', 'comment'] class MultipleIDMatches (ValueError): - def __init__(self, id, matches): - msg = ("More than one id matches %s. " - "Please be more specific.\n%s" % (id, matches)) + def __init__(self, id, common, matches): + msg = ('More than one id matches %s. ' + 'Please be more specific (%s/*).\n%s' % (id, common, matches)) ValueError.__init__(self, msg) self.id = id + self.common = common self.matches = matches class NoIDMatches (KeyError): - def __init__(self, id, possible_ids): - msg = "No id matches %s.\n%s" % (id, possible_ids) - KeyError.__init__(self, msg) + def __init__(self, id, possible_ids, msg=None): + KeyError.__init__(self, id) self.id = id self.possible_ids = possible_ids + self.msg = msg + def __str__(self): + if self.msg == None: + return 'No id matches %s.\n%s' % (self.id, self.possible_ids) + return self.msg + +class InvalidIDStructure (KeyError): + def __init__(self, id, msg=None): + KeyError.__init__(self, id) + self.id = id + self.msg = msg + def __str__(self): + if self.msg == None: + return 'Invalid id structure "%s"' % self.id + return self.msg - -def _assemble(*args): +def _assemble(args, check_length=False): args = list(args) for i,arg in enumerate(args): if arg == None: args[i] = '' - return '/'.join(args) - -def _split(id): + id = '/'.join(args) + if check_length == True: + assert len(args) > 0, args + if len(args) > 3: + raise InvalidIDStructure(id, '%d > 3 levels in "%s"' % (len(args), id)) + return id + +def _split(id, check_length=False): args = id.split('/') for i,arg in enumerate(args): if arg == '': args[i] = None + if check_length == True: + assert len(args) > 0, args + if len(args) > 3: + raise InvalidIDStructure(id, '%d > 3 levels in "%s"' % (len(args), id)) return args def _truncate(uuid, other_uuids, min_length=3): @@ -105,14 +128,21 @@ def _truncate(uuid, other_uuids, min_length=3): chars+=1 return uuid[:chars] -def _expand(truncated_id, other_ids): +def _expand(truncated_id, common, other_ids): + other_ids = list(other_ids) + if len(other_ids) == 0: + raise NoIDMatches(truncated_id, other_ids) + if truncated_id == None: + if len(other_ids) == 1: + return other_ids[0] + raise MultipleIDMatches(truncated_id, common, other_ids) matches = [] other_ids = list(other_ids) for id in other_ids: if id.startswith(truncated_id): matches.append(id) if len(matches) > 1: - raise MultipleIDMatches(truncated_id, matches) + raise MultipleIDMatches(truncated_id, common, matches) if len(matches) == 0: raise NoIDMatches(truncated_id, other_ids) return matches[0] @@ -172,7 +202,7 @@ class ID (object): assert self._type in HIERARCHY, self._type def storage(self, *args): - return _assemble(self._object.uuid, *args) + return _assemble([self._object.uuid]+list(args)) def _ancestors(self): ret = [self._object] @@ -187,7 +217,8 @@ class ID (object): return ret def long_user(self): - return _assemble(*[o.uuid for o in self._ancestors()]) + return _assemble([o.uuid for o in self._ancestors()], + check_length=True) def user(self): ids = [] @@ -196,7 +227,7 @@ class ID (object): ids.append(None) else: ids.append(_truncate(o.uuid, o.sibling_uuids())) - return _assemble(*ids) + return _assemble(ids, check_length=True) def child_uuids(child_storage_ids): """ @@ -210,54 +241,74 @@ def child_uuids(child_storage_ids): if len(fields) == 1: yield fields[0] +def long_to_short_user(bugdirs, id): + ids = _split(id, check_length=True) + bugdir = [bd for bd in bugdirs if bd.uuid == ids[0]][0] + objects = [bugdir] + if len(ids) >= 2: + bug = bugdir.bug_from_uuid(ids[1]) + objects.append(bug) + if len(ids) >= 3: + comment = bug.comment_from_uuid(ids[2]) + objects.append(comment) + for i,obj in enumerate(objects): + ids[i] = _truncate(ids[i], obj.sibling_uuids()) + return _assemble(ids) + +def short_to_long_user(bugdirs, id): + ids = _split(id, check_length=True) + ids[0] = _expand(ids[0], common=None, + other_ids=[bd.uuid for bd in bugdirs]) + if len(ids) == 1: + return _assemble(ids) + bugdir = [bd for bd in bugdirs if bd.uuid == ids[0]][0] + ids[1] = _expand(ids[1], common=bugdir.id.user(), + other_ids=bugdir.uuids()) + if len(ids) == 2: + return _assemble(ids) + bug = bugdir.bug_from_uuid(ids[1]) + ids[2] = _expand(ids[2], common=bug.id.user(), + other_ids=bug.uuids()) + return _assemble(ids) + REGEXP = '#([-a-f0-9]*)(/[-a-g0-9]*)?(/[-a-g0-9]*)?#' class IDreplacer (object): - def __init__(self, bugdirs, direction): + def __init__(self, bugdirs, replace_fn): self.bugdirs = bugdirs - self.direction = direction + self.replace_fn = replace_fn def __call__(self, match): - ids = [m.lstrip('/') for m in match.groups() if m != None] - ids = self.switch_ids(ids) - return '#' + '/'.join(ids) + '#' - def switch_id(self, id, sibling_uuids): - if id == None: - return None - if self.direction == 'long_to_short': - return _truncate(id, sibling_uuids) - return _expand(id, sibling_uuids) - def switch_ids(self, ids): - assert ids[0] != None, ids - if self.direction == 'long_to_short': - bugdir = [bd for bd in self.bugdirs if bd.uuid == ids[0]][0] - objects = [bugdir] - if len(ids) >= 2: - bug = bugdir.bug_from_uuid(ids[1]) - objects.append(bug) - if len(ids) >= 3: - comment = bug.comment_from_uuid(ids[2]) - objects.append(comment) - for i,obj in enumerate(objects): - ids[i] = self.switch_id(ids[i], obj.sibling_uuids()) - else: - ids[0] = self.switch_id(ids[0], [bd.uuid for bd in self.bugdirs]) - if len(ids) == 1: - return ids - bugdir = [bd for bd in self.bugdirs if bd.uuid == ids[0]][0] - ids[1] = self.switch_id(ids[1], bugdir.uuids()) - if len(ids) == 2: - return ids - bug = bugdir.bug_from_uuid(ids[1]) - ids[2] = self.switch_id(ids[2], bug.uuids()) - return ids - -def short_to_long_user(bugdirs, text): - return re.sub(REGEXP, IDreplacer(bugdirs, 'short_to_long'), text) - -def long_to_short_user(bugdirs, text): - return re.sub(REGEXP, IDreplacer(bugdirs, 'long_to_short'), text) + ids = [] + for m in match.groups(): + if m == None: + m = '' + ids.append(m) + return '#' + self.replace_fn(self.bugdirs, ''.join(ids)) + '#' + +def short_to_long_text(bugdirs, text): + return re.sub(REGEXP, IDreplacer(bugdirs, short_to_long_user), text) +def long_to_short_text(bugdirs, text): + return re.sub(REGEXP, IDreplacer(bugdirs, long_to_short_user), text) + +def residual(base, fragment): + """ + >>> residual('ABC/DEF/', '//GHI') + ('//', 'GHI') + >>> residual('ABC/DEF/', '/D/GHI') + ('/D/', 'GHI') + >>> residual('ABC/DEF', 'A/D/GHI') + ('A/D/', 'GHI') + >>> residual('ABC/DEF', 'A/D/GHI/JKL') + ('A/D/', 'GHI/JKL') + """ + base = base.rstrip('/') + '/' + ids = fragment.split('/') + base_count = base.count('/') + root_ids = ids[:base_count] + [''] + residual_ids = ids[base_count:] + return ('/'.join(root_ids), '/'.join(residual_ids)) def _parse_user(id): """ @@ -270,21 +321,34 @@ def _parse_user(id): >>> _parse_user('ABC') == \\ ... {'bugdir':'ABC', 'type':'bugdir'} True + >>> _parse_user('') == \\ + ... {'bugdir':None, 'type':'bugdir'} + True + >>> _parse_user('/') == \\ + ... {'bugdir':None, 'bug':None, 'type':'bug'} + True + >>> _parse_user('/DEF/') == \\ + ... {'bugdir':None, 'bug':'DEF', 'comment':None, 'type':'comment'} + True + >>> _parse_user('a/b/c/d') + Traceback (most recent call last): + ... + InvalidIDStructure: 4 > 3 levels in "a/b/c/d" """ ret = {} - args = _split(id) - assert len(args) > 0 and len(args) < 4, 'Invalid id "%s"' % id - for type,arg in zip(HIERARCHY, args): - assert len(arg) > 0, 'Invalid part "%s" of id "%s"' % (arg, id) + args = _split(id, check_length=True) + for i,(type,arg) in enumerate(zip(HIERARCHY, args)): + if arg != None and len(arg) == 0: + raise InvalidIDStructure( + id, 'Invalid %s part %d "%s" of id "%s"' % (type, i, arg, id)) ret['type'] = type ret[type] = arg return ret def parse_user(bugdir, id): - long_id = short_to_long_user([bugdir], '#%s#' % id).strip('#') + long_id = short_to_long_user([bugdir], id) return _parse_user(long_id) - if libbe.TESTING == True: class UUIDtestCase(unittest.TestCase): def testUUID_gen(self): @@ -292,22 +356,28 @@ if libbe.TESTING == True: self.failUnless(len(id) == 36, 'invalid UUID "%s"' % id) class DummyObject (object): - def __init__(self, uuid, siblings=[]): + def __init__(self, uuid, parent=None, siblings=[]): self.uuid = uuid self._siblings = siblings + if parent == None: + type_i = 0 + else: + assert parent.type in HIERARCHY, parent + setattr(self, parent.type, parent) + type_i = HIERARCHY.index(parent.type) + 1 + self.type = HIERARCHY[type_i] + self.id = ID(self, self.type) def sibling_uuids(self): return self._siblings class IDtestCase(unittest.TestCase): def setUp(self): self.bugdir = DummyObject('1234abcd') - self.bug = DummyObject('abcdef', ['a1234', 'ab9876']) - self.bug.bugdir = self.bugdir - self.comment = DummyObject('12345678', ['1234abcd', '1234cdef']) - self.comment.bug = self.bug - self.bd_id = ID(self.bugdir, 'bugdir') - self.b_id = ID(self.bug, 'bug') - self.c_id = ID(self.comment, 'comment') + self.bug = DummyObject('abcdef', self.bugdir, ['a1234', 'ab9876']) + self.comment = DummyObject('12345678', self.bug, ['1234abcd', '1234cdef']) + self.bd_id = self.bugdir.id + self.b_id = self.bug.id + self.c_id = self.comment.id def test_storage(self): self.failUnless(self.bd_id.storage() == self.bugdir.uuid, self.bd_id.storage()) @@ -315,8 +385,9 @@ if libbe.TESTING == True: self.b_id.storage()) self.failUnless(self.c_id.storage() == self.comment.uuid, self.c_id.storage()) - self.failUnless(self.bd_id.storage('x','y','z') == \ - '1234abcd/x/y/z', self.bd_id.storage()) + self.failUnless(self.bd_id.storage('x', 'y', 'z') == \ + '1234abcd/x/y/z', + self.bd_id.storage('x', 'y', 'z')) def test_long_user(self): self.failUnless(self.bd_id.long_user() == self.bugdir.uuid, self.bd_id.long_user()) @@ -338,30 +409,65 @@ if libbe.TESTING == True: class ShortLongParseTestCase(unittest.TestCase): def setUp(self): self.bugdir = DummyObject('1234abcd') - self.bug = DummyObject('abcdef', ['a1234', 'ab9876']) - self.bug.bugdir = self.bugdir + self.bug = DummyObject('abcdef', self.bugdir, ['a1234', 'ab9876']) + self.comment = DummyObject('12345678', self.bug, ['1234abcd', '1234cdef']) + self.bd_id = self.bugdir.id + self.b_id = self.bug.id + self.c_id = self.comment.id self.bugdir.bug_from_uuid = lambda uuid: self.bug self.bugdir.uuids = lambda : self.bug.sibling_uuids() + [self.bug.uuid] - self.comment = DummyObject('12345678', ['1234abcd', '1234cdef']) - self.comment.bug = self.bug self.bug.comment_from_uuid = lambda uuid: self.comment self.bug.uuids = lambda : self.comment.sibling_uuids() + [self.comment.uuid] - self.bd_id = ID(self.bugdir, 'bugdir') - self.b_id = ID(self.bug, 'bug') - self.c_id = ID(self.comment, 'comment') self.short = 'bla bla #123/abc# bla bla #123/abc/12345# bla bla' self.long = 'bla bla #1234abcd/abcdef# bla bla #1234abcd/abcdef/12345678# bla bla' - self.short_id = '123/abc' - def test_short_to_long(self): - self.failUnless(short_to_long_user([self.bugdir], self.short) == self.long, - '\n' + self.short + '\n' + short_to_long_user([self.bugdir], self.short) + '\n' + self.long) - def test_long_to_short(self): - self.failUnless(long_to_short_user([self.bugdir], self.long) == self.short, - '\n' + long_to_short_user([self.bugdir], self.long) + '\n' + self.short) + self.short_id_parse_pairs = [ + ('', {'bugdir':'1234abcd', 'type':'bugdir'}), + ('123/abc', {'bugdir':'1234abcd', 'bug':'abcdef', + 'type':'bug'}), + ('123/abc/12345', {'bugdir':'1234abcd', 'bug':'abcdef', + 'comment':'12345678', 'type':'comment'}), + ] + self.short_id_exception_pairs = [ + ('z', NoIDMatches('z', ['1234abcd'])), + ('///', InvalidIDStructure( + '///', msg='4 > 3 levels in "///"')), + ('/', MultipleIDMatches( + None, '123', ['a1234', 'ab9876', 'abcdef'])), + ('123/', MultipleIDMatches( + None, '123', ['a1234', 'ab9876', 'abcdef'])), + ('123/abc/', MultipleIDMatches( + None, '123/abc', ['1234abcd','1234cdef','12345678'])), + ] + def test_short_to_long_text(self): + self.failUnless(short_to_long_text([self.bugdir], self.short) == self.long, + '\n' + self.short + '\n' + short_to_long_text([self.bugdir], self.short) + '\n' + self.long) + def test_long_to_short_text(self): + self.failUnless(long_to_short_text([self.bugdir], self.long) == self.short, + '\n' + long_to_short_text([self.bugdir], self.long) + '\n' + self.short) def test_parse_user(self): - self.failUnless(parse_user(self.bugdir, self.short_id) == \ - {'bugdir':'1234abcd', 'bug':'abcdef', 'type':'bug'}, - parse_user(self.bugdir, self.short_id)) + for short_id,parsed in self.short_id_parse_pairs: + ret = parse_user(self.bugdir, short_id) + self.failUnless(ret == parsed, + 'got %s\nexpected %s' % (ret, parsed)) + def test_parse_user_exceptions(self): + for short_id,exception in self.short_id_exception_pairs: + try: + ret = parse_user(self.bugdir, short_id) + self.fail('Expected parse_user(bugdir, "%s") to raise %s,' + '\n but it returned %s' + % (short_id, exception.__class__.__name__, ret)) + except exception.__class__, e: + for attr in dir(e): + if attr.startswith('_') or attr == 'args': + continue + value = getattr(e, attr) + expected = getattr(exception, attr) + self.failUnless( + value == expected, + 'Expected parse_user(bugdir, "%s") %s.%s' + '\n to be %s, but it is %s\n\n%s' + % (short_id, exception.__class__.__name__, + attr, expected, value, e)) unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) -- cgit From 9a62c4beea7c89905dc487bdbe2e46fed4b83f21 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 19:19:15 -0500 Subject: Restored post-colon spaces in doctests --- libbe/command/merge.py | 8 ++++---- libbe/command/show.py | 4 ++-- libbe/comment.py | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/libbe/command/merge.py b/libbe/command/merge.py index 447b4ae..328351e 100644 --- a/libbe/command/merge.py +++ b/libbe/command/merge.py @@ -63,8 +63,8 @@ class Merge (libbe.command.Command): Short name : abc/a Severity : minor Status : open - Assigned : - Reporter : + Assigned : + Reporter : Creator : John Doe Created : ... Bug A @@ -109,8 +109,8 @@ class Merge (libbe.command.Command): Short name : abc/b Severity : minor Status : closed - Assigned : - Reporter : + Assigned : + Reporter : Creator : Jane Doe Created : ... Bug B diff --git a/libbe/command/show.py b/libbe/command/show.py index 1b498aa..1a569a6 100644 --- a/libbe/command/show.py +++ b/libbe/command/show.py @@ -45,8 +45,8 @@ class Show (libbe.command.Command): Short name : abc/a Severity : minor Status : open - Assigned : - Reporter : + Assigned : + Reporter : Creator : John Doe Created : ... Bug A diff --git a/libbe/comment.py b/libbe/comment.py index e386796..ad22683 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -448,7 +448,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> print comm.string(indent=2) --------- Comment --------- Name: //abc - From: + From: Date: Thu, 01 Jan 1970 00:00:00 +0000 Some @@ -507,50 +507,50 @@ class Comment(Tree, settings_object.SavedSettingsObject): >>> print a.string_thread(flatten=True) --------- Comment --------- Name: //a - From: + From: Date: Thu, 20 Nov 2008 01:00:00 +0000 Insightful remarks --------- Comment --------- Name: //b - From: + From: Date: Thu, 20 Nov 2008 02:00:00 +0000 Critique original comment --------- Comment --------- Name: //c - From: + From: Date: Thu, 20 Nov 2008 03:00:00 +0000 Begin flamewar :p --------- Comment --------- Name: //d - From: + From: Date: Thu, 20 Nov 2008 04:00:00 +0000 Useful examples >>> print a.string_thread() --------- Comment --------- Name: //a - From: + From: Date: Thu, 20 Nov 2008 01:00:00 +0000 Insightful remarks --------- Comment --------- Name: //b - From: + From: Date: Thu, 20 Nov 2008 02:00:00 +0000 Critique original comment --------- Comment --------- Name: //c - From: + From: Date: Thu, 20 Nov 2008 03:00:00 +0000 Begin flamewar :p --------- Comment --------- Name: //d - From: + From: Date: Thu, 20 Nov 2008 04:00:00 +0000 Useful examples -- cgit From 6ab6b8d0255ccf3557453fc0762d1529d39462ed Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 19:48:33 -0500 Subject: Propogate long_to_short_user() -> long_to_short_text() and inverse I'd missed some calls when I made the changes. --- libbe/comment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libbe/comment.py b/libbe/comment.py index ad22683..fab1f54 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -156,7 +156,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): def _set_comment_body(self, old=None, new=None, force=False): assert self.uuid != INVALID_UUID, self if self.bug != None and self.bug.bugdir != None: - new = libbe.util.id.short_to_long_user([self.bug.bugdir], new) + new = libbe.util.id.short_to_long_text([self.bug.bugdir], new) if (self.storage != None and self.storage.writeable == True) \ or force==True: assert new != None, "Can't save empty comment" @@ -464,7 +464,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): if self.content_type.startswith("text/"): body = (self.body or "") if self.bug != None and self.bug.bugdir != None: - body = libbe.util.id.long_to_short_user([self.bug.bugdir], body) + body = libbe.util.id.long_to_short_text([self.bug.bugdir], body) lines.extend(body.splitlines()) else: lines.append("Content type %s not printable. Try XML output instead" % self.content_type) -- cgit From 1d764100cbe15c8fe26b836ed0e9d494ab14500d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 19:49:50 -0500 Subject: Remove libbe.ui.util.cmdutil All of its functionality has moved off into more focused modules. --- libbe/command/html.py | 15 ++---- libbe/ui/util/cmdutil.py | 129 ----------------------------------------------- 2 files changed, 4 insertions(+), 140 deletions(-) delete mode 100644 libbe/ui/util/cmdutil.py diff --git a/libbe/command/html.py b/libbe/command/html.py index ec818c0..059fe75 100644 --- a/libbe/command/html.py +++ b/libbe/command/html.py @@ -119,14 +119,6 @@ directory. Html = HTML # alias for libbe.command.base.get_command_class() -def help(): - return get_parser().help_str() + longhelp - -def complete(options, args, parser): - for option, value in cmdutil.option_value_pairs(options, parser): - if "--complete" in args: - raise cmdutil.GetCompletions() # no positional arguments for list - class HTMLGen (object): def __init__(self, bd, template=None, title="Site Title", index_header="Index Header", @@ -259,8 +251,8 @@ class HTMLGen (object): % (comment.uuid, comment.content_type), [per_bug_dir, '.htaccess'], mode='a') self._write_file( # TODO: long_to_linked_user() - libbe.util.id.long_to_short_user( - self.bd, comment.body), + libbe.util.id.long_to_short_text( + [self.bd], comment.body), [per_bug_dir, comment.uuid], mode='wb') else: value = self._escape(value) @@ -339,7 +331,8 @@ class HTMLGen (object): try: os.makedirs(dir_path) except: - raise cmdutil.UsageError, 'Cannot create output directory "%s".' % dir_path + raise libbe.command.UserError( + 'Cannot create output directory "%s".' % dir_path) return dir_path def _write_file(self, content, path_array, mode='w'): diff --git a/libbe/ui/util/cmdutil.py b/libbe/ui/util/cmdutil.py deleted file mode 100644 index f2eb5b9..0000000 --- a/libbe/ui/util/cmdutil.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. -# Gianluca Montecchi -# Oleg Romanyshyn -# W. Trevor King -# -# 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., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -""" -Define assorted utilities to make command-line handling easier. -""" - -import glob -import optparse -import os -from textwrap import TextWrapper -from StringIO import StringIO -import sys - -import libbe -import bugdir -import comment -import plugin -import encoding -if libbe.TESTING == True: - import doctest - - - -def iter_commands(): - for name, module in plugin.iter_plugins("becommands"): - yield name.replace("_", "-"), module - -def execute(cmd, args, - manipulate_encodings=True, restrict_file_access=False, - dir="."): - enc = encoding.get_encoding() - cmd = get_command(cmd) - ret = cmd.execute([a.decode(enc) for a in args], - manipulate_encodings=manipulate_encodings, - restrict_file_access=restrict_file_access, - dir=dir) - if ret == None: - ret = 0 - return ret - - - - -def restrict_file_access(bugdir, path): - -def parse_id(id): - """ - Return (bug_id, comment_id) tuple. - Basically inverts Comment.comment_shortnames() - >>> parse_id('XYZ') - ('XYZ', None) - >>> parse_id('XYZ:123') - ('XYZ', ':123') - >>> parse_id('') - Traceback (most recent call last): - ... - UserError: invalid id ''. - >>> parse_id('::') - Traceback (most recent call last): - ... - UserError: invalid id '::'. - """ - if len(id) == 0: - raise UserError("invalid id '%s'." % id) - if id.count(':') > 1: - raise UserError("invalid id '%s'." % id) - elif id.count(':') == 1: - # Split shortname generated by Comment.comment_shortnames() - bug_id,comment_id = id.split(':') - comment_id = ':'+comment_id - else: - bug_id = id - comment_id = None - return (bug_id, comment_id) - -def bug_from_id(bdir, id): - """ - Exception translation for the command-line interface. - id can be either the bug shortname or the full uuid. - """ - try: - bug = bdir.bug_from_shortname(id) - except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: - raise UserError(e.message) - return bug - -def bug_comment_from_id(bdir, id): - """ - Return (bug,comment) tuple matching shortname. id can be either - the bug/comment shortname or the full uuid. If there is no - comment part to the id, the returned comment is the bug's - .comment_root. - """ - bug_id,comment_id = parse_id(id) - try: - bug = bdir.bug_from_shortname(bug_id) - except (bugdir.MultipleBugMatches, bugdir.NoBugMatches), e: - raise UserError(e.message) - if comment_id == None: - comm = bug.comment_root - else: - #bug.load_comments(load_full=False) - try: - comm = bug.comment_root.comment_from_shortname(comment_id) - except comment.InvalidShortname, e: - raise UserError(e.message) - return (bug, comm) - - - -if libbe.TESTING == True: - suite = doctest.DocTestSuite() -- cgit From 182a44a4a284e118da03c1182f2b9a8b76dd0d93 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 19:53:13 -0500 Subject: Don't worry about whitespace in `be --help` --- libbe/ui/command_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py index b99f812..9df474e 100755 --- a/libbe/ui/command_line.py +++ b/libbe/ui/command_line.py @@ -167,7 +167,7 @@ class BE (libbe.command.Command): >>> p = CmdOptionParser(be) >>> p.exit_after_callback = False >>> try: - ... options,args = p.parse_args(['--help']) # doctest: +ELLIPSIS + ... options,args = p.parse_args(['--help']) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE ... except CallbackExit: ... pass usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]] -- cgit From 1a66c3f33c4ccbdf1c430c0d6af3d028ef261d4b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 20:06:27 -0500 Subject: Don't chdir() in libbe/command/html.py doctests. --- libbe/command/html.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/libbe/command/html.py b/libbe/command/html.py index 059fe75..99cd61f 100644 --- a/libbe/command/html.py +++ b/libbe/command/html.py @@ -41,22 +41,19 @@ class HTML (libbe.command.Command): >>> cmd._setup_io = lambda i_enc,o_enc : None >>> cmd.stdout = sys.stdout - >>> cwd = os.getcwd() - >>> os.chdir(bd.storage.repo) - >>> ret = cmd.run() - >>> os.path.exists('./html_export') + >>> ret = cmd.run({'output':os.path.join(bd.storage.repo, 'html_export')}) + >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export')) True - >>> os.path.exists('./html_export/index.html') + >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index.html')) True - >>> os.path.exists('./html_export/index_inactive.html') + >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'index_inactive.html')) True - >>> os.path.exists('./html_export/bugs') + >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs')) True - >>> os.path.exists('./html_export/bugs/a.html') + >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'a.html')) True - >>> os.path.exists('./html_export/bugs/b.html') + >>> os.path.exists(os.path.join(bd.storage.repo, 'html_export', 'bugs', 'b.html')) True - >>> os.chdir(cwd) >>> bd.cleanup() """ name = 'html' -- cgit From d595aba006a39a2d75067fb7fc82956538e7e16d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 20:13:43 -0500 Subject: We don't do much with Mercurial's ui, so _dispatch -> dispatch --- libbe/storage/vcs/hg.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libbe/storage/vcs/hg.py b/libbe/storage/vcs/hg.py index 6baf19c..19e3585 100644 --- a/libbe/storage/vcs/hg.py +++ b/libbe/storage/vcs/hg.py @@ -67,13 +67,12 @@ class Hg(base.VCS): if 'cwd' not in kwargs: kwargs['cwd'] = self.repo assert len(kwargs) == 1, kwargs - ui = mercurial.ui.ui(interactive=False) fullargs = ['--cwd', kwargs['cwd']] fullargs.extend(args) stdout = sys.stdout tmp_stdout = StringIO.StringIO() sys.stdout = tmp_stdout - mercurial.dispatch._dispatch(ui, fullargs) + mercurial.dispatch.dispatch(fullargs) sys.stdout = stdout return tmp_stdout.getvalue().rstrip('\n') -- cgit From b1540a08131173ace920f2d3d0829e54b8f26283 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 21:12:12 -0500 Subject: Fixed make_*_testcase_subclasses() to avoid duplication. Also removed final check for 'parent' existence in Storage_add_remove_TestCase.test_remove_nonrooted() because some VCSs (e.g. Git) don't keep track of blank directories. --- libbe/storage/base.py | 28 +++++++++++++++------------- libbe/storage/vcs/base.py | 3 ++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/libbe/storage/base.py b/libbe/storage/base.py index d16c30b..ffde475 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -501,7 +501,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], directory=False) + self.s.add(ids[-1], directory=(i % 2 == 0)) s = sorted(self.s.children()) self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) @@ -513,7 +513,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent', directory=True) + self.s.add(ids[-1], 'parent', directory=(i % 2 == 0)) s = sorted(self.s.children('parent')) self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) s = self.s.children() @@ -527,7 +527,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append('parent/%s' % str(i)) - self.s.add(ids[-1], 'parent', directory=True) + self.s.add(ids[-1], 'parent', directory=(i % 2 == 0)) s = sorted(self.s.children('parent')) self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) @@ -560,7 +560,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], directory=True) + self.s.add(ids[-1], directory=(i % 2 == 0)) for i in range(10): self.s.remove(ids.pop()) s = sorted(self.s.children()) @@ -574,13 +574,14 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent', directory=False) + self.s.add(ids[-1], 'parent', directory=False)#(i % 2 == 0)) for i in range(10): self.s.remove(ids.pop()) s = sorted(self.s.children('parent')) self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) - s = self.s.children() - self.failUnless(s == ['parent'], s) + if len(s) > 0: + s = self.s.children() + self.failUnless(s == ['parent'], s) def test_remove_directory_not_empty(self): """ @@ -590,7 +591,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent', directory=True) + self.s.add(ids[-1], 'parent', directory=(i % 2 == 0)) self.s.remove(ids.pop()) # empty directory removal succeeds try: self.s.remove('parent') # empty directory removal succeeds @@ -610,7 +611,7 @@ if TESTING == True: ids.append(str(i)) self.s.add(ids[-1], 'parent', directory=True) for j in range(10): # add some grandkids - self.s.add(str(20*(i+1)+j), ids[-1], directory=False) + self.s.add(str(20*(i+1)+j), ids[-1], directory=(i%2 == 0)) self.s.recursive_remove('parent') s = sorted(self.s.children()) self.failUnless(s == [], s) @@ -726,7 +727,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent', directory=False) + self.s.add(ids[-1], 'parent', directory=(i % 2 == 0)) self.s.disconnect() self.s.connect() s = sorted(self.s.children('parent')) @@ -842,7 +843,7 @@ if TESTING == True: children = [] for i in range(10): new_child = str(i) - self.s.add(new_child, 'parent', directory=False) + self.s.add(new_child, 'parent', directory=(i % 2 == 0)) self.s.set(new_child, self.val) revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), self.commit_body)) @@ -861,7 +862,7 @@ if TESTING == True: c for c in ( ob for ob in globals().values() if isinstance(ob, type)) if issubclass(c, StorageTestCase) \ - and not issubclass(c, VersionedStorageTestCase)] + and c.Class == Storage] for base_class in storage_testcase_classes: testcase_class_name = storage_class.__name__ + base_class.__name__ @@ -877,7 +878,8 @@ if TESTING == True: storage_testcase_classes = [ c for c in ( ob for ob in globals().values() if isinstance(ob, type)) - if issubclass(c, StorageTestCase)] + if issubclass(c, StorageTestCase) \ + and c.Class == Storage] for base_class in storage_testcase_classes: testcase_class_name = storage_class.__name__ + base_class.__name__ diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 533ef4c..6ece16d 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -1085,7 +1085,8 @@ if libbe.TESTING == True: vcs_testcase_classes = [ c for c in ( ob for ob in globals().values() if isinstance(ob, type)) - if issubclass(c, VCSTestCase)] + if issubclass(c, VCSTestCase) \ + and c.Class == VCS] for base_class in vcs_testcase_classes: testcase_class_name = vcs_class.__name__ + base_class.__name__ -- cgit From 07ee90254ce64ec734dacebea715a0b7a24599af Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 21:17:39 -0500 Subject: Use ._vcs_is_versioned() in VCS._children() Otherwise Arch will return '.arch-ids' in its list, etc. --- libbe/storage/vcs/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 6ece16d..b47ed2f 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -760,6 +760,9 @@ os.listdir(self.get_path("bugs")): listdir(os.path.join(path, c))]) elif c in ['id-cache', 'version']: children[i] = None + elif self.interspersed_vcs_files \ + and self._vcs_is_versioned(c) == False: + children[i] = None for i,c in enumerate(children): if c == None: continue cpath = os.path.join(path, c) -- cgit From ec2472daa930a46949fcdb3fb9ca715f9bb3f200 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 21:25:33 -0500 Subject: Disable mercurial.demandimport, since it breaks Bzr Running python test.py libbe.storage.vcs.hg libbe.storage.vcs.bzr with the old setup produced lots of Traceback (most recent call last): File ".../libbe/storage/vcs/base.py", line 1010, in setUp self.s.init() File ".../libbe/storage/base.py", line 170, in init return self._init() File ".../libbe/storage/vcs/base.py", line 664, in _init self._vcs_init(self.repo) File ".../libbe/storage/vcs/bzr.py", line 88, in _vcs_init cmd.run(location=path) File ".../python2.5/site-packages/bzrlib/builtins.py", line 1685, in run format = bzrdir.format_registry.make_bzrdir('default') File ".../python2.5/site-packages/bzrlib/bzrdir.py", line 3452, in make_bzrdir return self.get(key)() File ".../python2.5/site-packages/bzrlib/bzrdir.py", line 3398, in helper bd.set_branch_format(_load(branch_format)) File ".../python2.5/site-packages/bzrlib/bzrdir.py", line 3385, in _load [factory_name]) File "/var/lib/python-support/python2.5/mercurial/demandimport.py", line 108, in _demandimport setattr(mod, x, _demandmod(x, mod.__dict__, locals)) File ".../python2.5/site-packages/bzrlib/lazy_import.py", line 106, in __getattribute__ obj = _replace() File ".../python2.5/site-packages/bzrlib/lazy_import.py", line 88, in _replace extra=e) IllegalUseOfScopeReplacer: ScopeReplacer object 'branch' was used incorrectly: Object already cleaned up, did you assign it to another variable?: _factory --- libbe/storage/vcs/hg.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/libbe/storage/vcs/hg.py b/libbe/storage/vcs/hg.py index 19e3585..b280ff2 100644 --- a/libbe/storage/vcs/hg.py +++ b/libbe/storage/vcs/hg.py @@ -22,8 +22,6 @@ Mercurial (hg) backend. """ try: - # enable importing on demand to reduce startup time - from mercurial import demandimport; demandimport.enable() import mercurial import mercurial.version import mercurial.dispatch -- cgit From 16877141d526a5387a0f673b56c1cd6f3b900674 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 21:37:14 -0500 Subject: Correct for possible directory changes in mercurial.dispatch.dispatch() I ran across this when the hg unittests broke the vcs.base unittests: $ python test.py libbe.storage.vcs.base libbe.storage.vcs.hg ... OK $ python test.py libbe.storage.vcs.hg libbe.storage.vcs.base ... File ".../libbe/storage/vcs/base.py", line 914, in libbe.storage.vcs.base.VCSTestCase.Class._u_rel_path Failed example: vcs._u_rel_path("./a", ".") Exception raised: Traceback (most recent call last): File "/usr/lib/python2.5/doctest.py", line 1228, in __run compileflags, 1) in test.globs File "", line 1, in vcs._u_rel_path("./a", ".") File ".../libbe/storage/vcs/base.py", line 921, in _u_rel_path path = os.path.abspath(path) File "/usr/lib/python2.5/posixpath.py", line 403, in abspath path = join(os.getcwd(), path) OSError: [Errno 2] No such file or directory ... FAILED (failures=1) --- libbe/storage/vcs/hg.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libbe/storage/vcs/hg.py b/libbe/storage/vcs/hg.py index b280ff2..11494a9 100644 --- a/libbe/storage/vcs/hg.py +++ b/libbe/storage/vcs/hg.py @@ -70,7 +70,9 @@ class Hg(base.VCS): stdout = sys.stdout tmp_stdout = StringIO.StringIO() sys.stdout = tmp_stdout + cwd = os.getcwd() mercurial.dispatch.dispatch(fullargs) + os.chdir(cwd) sys.stdout = stdout return tmp_stdout.getvalue().rstrip('\n') -- cgit From 072a46eefb66733ae570a9fb9abbc9570461a490 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 21:53:58 -0500 Subject: Emptied interfaces directory Mostly throwing out a bunch of outdated GUIs. The email interface hasn't been moved over to the new 'Command' format yet... --- README.dev | 96 --------- doc/README.dev | 96 +++++++++ doc/SPAM | 34 +++ interfaces/README | 34 --- interfaces/email/catmutt | 59 ----- interfaces/email/interactive/becommands | 1 - interfaces/gui/beg/beg | 12 -- interfaces/gui/beg/table.py | 97 --------- interfaces/gui/wxbe/wxbe | 87 -------- .../Bugs-Everywhere-Web.egg-info/SOURCES.txt | 36 ---- .../Bugs-Everywhere-Web.egg-info/not-zip-safe | 0 .../Bugs-Everywhere-Web.egg-info/requires.txt | 1 - .../Bugs-Everywhere-Web.egg-info/sqlobject.txt | 2 - .../Bugs-Everywhere-Web.egg-info/top_level.txt | 2 - .../Bugs_Everywhere_Web.egg-info/PKG-INFO | 15 -- .../Bugs_Everywhere_Web.egg-info/SOURCES.txt | 44 ---- .../dependency_links.txt | 1 - .../Bugs_Everywhere_Web.egg-info/not-zip-safe | 1 - .../paster_plugins.txt | 2 - .../Bugs_Everywhere_Web.egg-info/requires.txt | 1 - .../Bugs_Everywhere_Web.egg-info/sqlobject.txt | 2 - .../Bugs_Everywhere_Web.egg-info/top_level.txt | 1 - interfaces/web/Bugs-Everywhere-Web/README | 60 ------ .../web/Bugs-Everywhere-Web/beweb/__init__.py | 0 interfaces/web/Bugs-Everywhere-Web/beweb/app.cfg | 120 ----------- .../Bugs-Everywhere-Web/beweb/config.py.example | 10 - .../web/Bugs-Everywhere-Web/beweb/config/app.cfg | 92 -------- .../web/Bugs-Everywhere-Web/beweb/config/log.cfg | 29 --- .../web/Bugs-Everywhere-Web/beweb/controllers.py | 240 --------------------- .../web/Bugs-Everywhere-Web/beweb/formatting.py | 76 ------- interfaces/web/Bugs-Everywhere-Web/beweb/json.py | 13 -- interfaces/web/Bugs-Everywhere-Web/beweb/model.py | 107 --------- interfaces/web/Bugs-Everywhere-Web/beweb/prest.py | 168 --------------- .../web/Bugs-Everywhere-Web/beweb/release.py | 14 -- .../Bugs-Everywhere-Web/beweb/static/css/style.css | 116 ---------- .../beweb/static/images/ds-b.png | Bin 213 -> 0 bytes .../beweb/static/images/ds-bl.png | Bin 327 -> 0 bytes .../beweb/static/images/ds-br.png | Bin 365 -> 0 bytes .../beweb/static/images/ds-l.png | Bin 197 -> 0 bytes .../beweb/static/images/ds-r.png | Bin 214 -> 0 bytes .../beweb/static/images/ds-t.png | Bin 200 -> 0 bytes .../beweb/static/images/ds-tl.png | Bin 240 -> 0 bytes .../beweb/static/images/ds-tr.png | Bin 311 -> 0 bytes .../beweb/static/images/ds2-b.png | Bin 206 -> 0 bytes .../beweb/static/images/ds2-r.png | Bin 204 -> 0 bytes .../beweb/static/images/favicon.ico | Bin 318 -> 0 bytes .../beweb/static/images/favicon.png | Bin 267 -> 0 bytes .../beweb/static/images/half-spiral.png | Bin 1112 -> 0 bytes .../beweb/static/images/header_inner.png | Bin 37537 -> 0 bytes .../beweb/static/images/info.png | Bin 2889 -> 0 bytes .../beweb/static/images/is-b.png | Bin 200 -> 0 bytes .../beweb/static/images/is-bl.png | Bin 408 -> 0 bytes .../beweb/static/images/is-br.png | Bin 304 -> 0 bytes .../beweb/static/images/is-l.png | Bin 214 -> 0 bytes .../beweb/static/images/is-r.png | Bin 197 -> 0 bytes .../beweb/static/images/is-t.png | Bin 213 -> 0 bytes .../beweb/static/images/is-tl.png | Bin 413 -> 0 bytes .../beweb/static/images/is-tr.png | Bin 414 -> 0 bytes .../Bugs-Everywhere-Web/beweb/static/images/ok.png | Bin 25753 -> 0 bytes .../beweb/static/images/shadows.png | Bin 3960 -> 0 bytes .../beweb/static/images/spiral.png | Bin 2120 -> 0 bytes .../beweb/static/images/tg_under_the_hood.png | Bin 4010 -> 0 bytes .../beweb/static/images/under_the_hood_blue.png | Bin 2667 -> 0 bytes .../beweb/templates/__init__.py | 0 .../Bugs-Everywhere-Web/beweb/templates/about.kid | 21 -- .../Bugs-Everywhere-Web/beweb/templates/bugs.kid | 52 ----- .../beweb/templates/edit_bug.kid | 52 ----- .../beweb/templates/edit_comment.kid | 26 --- .../Bugs-Everywhere-Web/beweb/templates/error.kid | 14 -- .../Bugs-Everywhere-Web/beweb/templates/login.kid | 113 ---------- .../Bugs-Everywhere-Web/beweb/templates/master.kid | 71 ------ .../beweb/templates/projects.kid | 32 --- .../beweb/templates/welcome.kid | 50 ----- .../Bugs-Everywhere-Web/beweb/tests/__init__.py | 0 .../beweb/tests/test_controllers.py | 16 -- .../Bugs-Everywhere-Web/beweb/tests/test_model.py | 23 -- interfaces/web/Bugs-Everywhere-Web/dev.cfg | 71 ------ interfaces/web/Bugs-Everywhere-Web/libbe | 1 - interfaces/web/Bugs-Everywhere-Web/prod.cfg | 41 ---- interfaces/web/Bugs-Everywhere-Web/sample-prod.cfg | 71 ------ interfaces/web/Bugs-Everywhere-Web/setup-tables.py | 34 --- interfaces/web/Bugs-Everywhere-Web/setup.py | 62 ------ interfaces/web/Bugs-Everywhere-Web/start-beweb.py | 28 --- interfaces/xml/be-mbox-to-xml | 154 ------------- interfaces/xml/be-xml-to-mbox | 208 ------------------ misc/xml/be-mbox-to-xml | 154 +++++++++++++ misc/xml/be-xml-to-mbox | 208 ++++++++++++++++++ misc/xml/catmutt | 59 +++++ 88 files changed, 551 insertions(+), 2679 deletions(-) delete mode 100644 README.dev create mode 100644 doc/README.dev create mode 100644 doc/SPAM delete mode 100644 interfaces/README delete mode 100755 interfaces/email/catmutt delete mode 120000 interfaces/email/interactive/becommands delete mode 100755 interfaces/gui/beg/beg delete mode 100644 interfaces/gui/beg/table.py delete mode 100755 interfaces/gui/wxbe/wxbe delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/not-zip-safe delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/PKG-INFO delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/SOURCES.txt delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/dependency_links.txt delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/not-zip-safe delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/paster_plugins.txt delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/requires.txt delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/sqlobject.txt delete mode 100644 interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/top_level.txt delete mode 100644 interfaces/web/Bugs-Everywhere-Web/README delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/__init__.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/app.cfg delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/config.py.example delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/config/app.cfg delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/config/log.cfg delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/formatting.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/json.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/model.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/prest.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/release.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/css/style.css delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-b.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-bl.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-br.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-l.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-r.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-t.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tl.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tr.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-b.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-r.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.ico delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/half-spiral.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/header_inner.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/info.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-b.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-bl.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-br.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-l.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-r.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-t.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tl.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tr.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ok.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/shadows.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/spiral.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/tg_under_the_hood.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/static/images/under_the_hood_blue.png delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/templates/__init__.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/templates/about.kid delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/templates/bugs.kid delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/templates/error.kid delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/templates/login.kid delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/templates/master.kid delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/templates/projects.kid delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/templates/welcome.kid delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/tests/__init__.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_controllers.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/beweb/tests/test_model.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/dev.cfg delete mode 120000 interfaces/web/Bugs-Everywhere-Web/libbe delete mode 100644 interfaces/web/Bugs-Everywhere-Web/prod.cfg delete mode 100644 interfaces/web/Bugs-Everywhere-Web/sample-prod.cfg delete mode 100644 interfaces/web/Bugs-Everywhere-Web/setup-tables.py delete mode 100644 interfaces/web/Bugs-Everywhere-Web/setup.py delete mode 100755 interfaces/web/Bugs-Everywhere-Web/start-beweb.py delete mode 100755 interfaces/xml/be-mbox-to-xml delete mode 100755 interfaces/xml/be-xml-to-mbox create mode 100755 misc/xml/be-mbox-to-xml create mode 100755 misc/xml/be-xml-to-mbox create mode 100755 misc/xml/catmutt diff --git a/README.dev b/README.dev deleted file mode 100644 index 2a09463..0000000 --- a/README.dev +++ /dev/null @@ -1,96 +0,0 @@ -Extending BE -============ - -To write a plugin, you simply create a new file in the becommands -directory. Take a look at one of the simpler plugins (e.g. open.py) -for an example of how that looks, and to start getting a feel for the -libbe interface. - -To fit into the current framework, your extension module should -provide the following elements: - __desc__ - A short string describing the purpose of your plugin - execute(args, manipulate_encodings=True, restrict_file_access=False, - dir=".") - The entry function for your plugin. args is everything from - sys.argv after the name of your plugin (e.g. for the command - `be open abc', args=['abc']). - - manipulate_encodings should be passed through to any calls to - bugdir.BugDir(). See the BugDir documentation for details. - - If restrict_file_access==True, you should call - cmdutil.restrict_file_access(bugdir, path) - before attempting to read or write a file. See the - restrict_file_access documentation for details. - - dir is a directory inside the repository of interest. - - Note: be supports command-completion. To avoid raising errors you - need to deal with possible '--complete' options and arguments. - See the 'Command completion' section below for more information. - help() - Return the string to be output by `be help ', - `be --help', etc. - -While that's all that's strictly necessary, many plugins (all the -current ones) use libbe.cmdutil.CmdOptionParser to provide a -consistent interface - get_parser() - Return an instance of CmdOptionParser(""). You can - alter the parser (e.g. add some more options) before returning it. - -Again, you can just browse around in becommands to get a feel for things. - - -Testing -------- - -Run any doctests in your plugin with - be$ python test.py -for example - be$ python test.py merge - - -Command completion ------------------- - -BE implements a general framework to make it easy to support command -completion for arbitrary plugins. In order to support this system, -all becommands should properly handle the '--complete' commandline -argument, returning a list of possible completions. For example - $ be --commands - lists options accepted by be and the names of all available becommands. - $ be list --commands - lists options accepted by becommand/list - $ be list --status --commands - lists arguments accepted by the becommand/list --status option - $ be show -- --commands - lists possible vals for the first positional argument of becommand/show -This is a lot of information, but command-line completion is really -convenient for the user. See becommand/list.py and becommand/show.py -for example implementations. The basic idea is to raise - cmdutil.GetCompletions(['list','of','possible','completions']) -once you've determined what that list should be. - -However, command completion is not critical. The first priority is to -implement the target functionality, with fancy shell sugar coming -later. In recognition of this, cmdutil provides the default_complete -function which ensures that if '--complete' is any one of the -arguments, options, or option-arguments, GetCompletions will be raised -with and empty list. - -Profiling -========= - -Find out which 20 calls take the most cumulative time (time of -execution + childrens' times). - - $ python -m cProfile -o profile be [command] [args] - $ python -c "import pstats; p=pstats.Stats('profile'); p.sort_stats('cumulative').print_stats(20)" - -It's often useful to toss a - import sys, traceback - print >> sys.stderr, '-'*60, '\n', '\n'.join(traceback.format_stack()[-10:]) -into expensive functions (e.g. libbe.util.subproc.invoke()), if you're -not sure why they're being called. diff --git a/doc/README.dev b/doc/README.dev new file mode 100644 index 0000000..2a09463 --- /dev/null +++ b/doc/README.dev @@ -0,0 +1,96 @@ +Extending BE +============ + +To write a plugin, you simply create a new file in the becommands +directory. Take a look at one of the simpler plugins (e.g. open.py) +for an example of how that looks, and to start getting a feel for the +libbe interface. + +To fit into the current framework, your extension module should +provide the following elements: + __desc__ + A short string describing the purpose of your plugin + execute(args, manipulate_encodings=True, restrict_file_access=False, + dir=".") + The entry function for your plugin. args is everything from + sys.argv after the name of your plugin (e.g. for the command + `be open abc', args=['abc']). + + manipulate_encodings should be passed through to any calls to + bugdir.BugDir(). See the BugDir documentation for details. + + If restrict_file_access==True, you should call + cmdutil.restrict_file_access(bugdir, path) + before attempting to read or write a file. See the + restrict_file_access documentation for details. + + dir is a directory inside the repository of interest. + + Note: be supports command-completion. To avoid raising errors you + need to deal with possible '--complete' options and arguments. + See the 'Command completion' section below for more information. + help() + Return the string to be output by `be help ', + `be --help', etc. + +While that's all that's strictly necessary, many plugins (all the +current ones) use libbe.cmdutil.CmdOptionParser to provide a +consistent interface + get_parser() + Return an instance of CmdOptionParser(""). You can + alter the parser (e.g. add some more options) before returning it. + +Again, you can just browse around in becommands to get a feel for things. + + +Testing +------- + +Run any doctests in your plugin with + be$ python test.py +for example + be$ python test.py merge + + +Command completion +------------------ + +BE implements a general framework to make it easy to support command +completion for arbitrary plugins. In order to support this system, +all becommands should properly handle the '--complete' commandline +argument, returning a list of possible completions. For example + $ be --commands + lists options accepted by be and the names of all available becommands. + $ be list --commands + lists options accepted by becommand/list + $ be list --status --commands + lists arguments accepted by the becommand/list --status option + $ be show -- --commands + lists possible vals for the first positional argument of becommand/show +This is a lot of information, but command-line completion is really +convenient for the user. See becommand/list.py and becommand/show.py +for example implementations. The basic idea is to raise + cmdutil.GetCompletions(['list','of','possible','completions']) +once you've determined what that list should be. + +However, command completion is not critical. The first priority is to +implement the target functionality, with fancy shell sugar coming +later. In recognition of this, cmdutil provides the default_complete +function which ensures that if '--complete' is any one of the +arguments, options, or option-arguments, GetCompletions will be raised +with and empty list. + +Profiling +========= + +Find out which 20 calls take the most cumulative time (time of +execution + childrens' times). + + $ python -m cProfile -o profile be [command] [args] + $ python -c "import pstats; p=pstats.Stats('profile'); p.sort_stats('cumulative').print_stats(20)" + +It's often useful to toss a + import sys, traceback + print >> sys.stderr, '-'*60, '\n', '\n'.join(traceback.format_stack()[-10:]) +into expensive functions (e.g. libbe.util.subproc.invoke()), if you're +not sure why they're being called. diff --git a/doc/SPAM b/doc/SPAM new file mode 100644 index 0000000..4d74580 --- /dev/null +++ b/doc/SPAM @@ -0,0 +1,34 @@ +Removing spam commits from the history +====================================== + +arch bzr darcs git hg none + +In the case that some spam or inappropriate comment makes its way +through you interface, you can remove the offending commit XYZ with: + + If the offending commit is the last commit: + + arch: + bzr: bzr uncommit && bzr revert + darcs: darcs obliterate --last=1 + git: git reset --hard HEAD^ + hg: hg rollback && hg revert + + If the offending commit is not the last commit: + + arch: + bzr: bzr rebase -r ..-1 --onto before:XYZ . + (requires bzr-rebase plugin, note, you have to increment XYZ by + hand for , because bzr does not support "after:XYZ".) + darcs: darcs obliterate --matches 'name XYZ' + git: git rebase --onto XYZ~1 XYZ + hg: -not-supported- + (From http://hgbook.red-bean.com/read/finding-and-fixing-mistakes.html#id394667 + "Mercurial also does not provide a way to make a file or + changeset completely disappear from history, because there is no + way to enforce its disappearance") + +Note that all of these _change_the_repo_history_, so only do this on +your interface-specific repo before it interacts with any other repo. +Otherwise, you'll have to survive by cherry-picking only the good +commits. diff --git a/interfaces/README b/interfaces/README deleted file mode 100644 index 4d74580..0000000 --- a/interfaces/README +++ /dev/null @@ -1,34 +0,0 @@ -Removing spam commits from the history -====================================== - -arch bzr darcs git hg none - -In the case that some spam or inappropriate comment makes its way -through you interface, you can remove the offending commit XYZ with: - - If the offending commit is the last commit: - - arch: - bzr: bzr uncommit && bzr revert - darcs: darcs obliterate --last=1 - git: git reset --hard HEAD^ - hg: hg rollback && hg revert - - If the offending commit is not the last commit: - - arch: - bzr: bzr rebase -r ..-1 --onto before:XYZ . - (requires bzr-rebase plugin, note, you have to increment XYZ by - hand for , because bzr does not support "after:XYZ".) - darcs: darcs obliterate --matches 'name XYZ' - git: git rebase --onto XYZ~1 XYZ - hg: -not-supported- - (From http://hgbook.red-bean.com/read/finding-and-fixing-mistakes.html#id394667 - "Mercurial also does not provide a way to make a file or - changeset completely disappear from history, because there is no - way to enforce its disappearance") - -Note that all of these _change_the_repo_history_, so only do this on -your interface-specific repo before it interacts with any other repo. -Otherwise, you'll have to survive by cherry-picking only the good -commits. diff --git a/interfaces/email/catmutt b/interfaces/email/catmutt deleted file mode 100755 index 601f14f..0000000 --- a/interfaces/email/catmutt +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/sh - -# catmutt - wrap mutt allowing mboxes read from stdin. -# -# Copyright (C) 1998-1999 Moritz Barsnick , -# 2009 William Trevor King -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# version 2 as published by the Free Software Foundation. -# -# 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 -# -# developed from grepm-0.6 -# http://www.barsnick.net/sw/grepm.html - -PROGNAME=`basename "$0"` -export TMPDIR="${TMPDIR-/tmp}" # used by mktemp -umask 077 - -if [ $# -gt 0 ] && [ "$1" = "--help" ]; then - echo 1>&2 "Usage: ${PROGNAME} [--help] mutt-arguments" - echo 1>&2 "" - echo 1>&2 "Read a mailbox file from stdin and opens it with mutt." - echo 1>&2 "For example: cat somefile.mbox | ${PROGNAME}" - exit 0 -fi - -# Note: the -t/-p options to mktemp are deprecated for mktemp (GNU -# coreutils) 7.1 in favor of --tmpdir but the --tmpdir option does not -# exist yet for my 6.10-3ubuntu2 coreutils -TMPFILE=`mktemp -t catmutt.XXXXXX` || exit 1 - -trap "rm -f ${TMPFILE}; exit 1" 1 2 3 13 15 - -cat > "${TMPFILE}" || exit 1 - -# Now that we've read in the mailbox file, reopen stdin for mutt/user -# interaction. When in a pipe we're not technically in a tty, so use -# a little hack from "greno" at -# http://www.linuxforums.org/forum/linux-programming-scripting/98607-bash-stdin-problem.html -tty="/dev/`ps -p$$ --no-heading | awk '{print $2}'`" -exec < ${tty} - -if [ `wc -c "${TMPFILE}" | awk '{print $1}'` -gt 0 ]; then - echo 1>&2 "Calling mutt on temporary mailbox file (${TMPFILE})." - mutt -R -f "${TMPFILE}" "$@" -else - echo 1>&2 "Empty mailbox input." -fi - -rm -f "${TMPFILE}" && echo 1>&2 "Deleted temporary mailbox file (${TMPFILE})." diff --git a/interfaces/email/interactive/becommands b/interfaces/email/interactive/becommands deleted file mode 120000 index 8af773c..0000000 --- a/interfaces/email/interactive/becommands +++ /dev/null @@ -1 +0,0 @@ -../../../becommands \ No newline at end of file diff --git a/interfaces/gui/beg/beg b/interfaces/gui/beg/beg deleted file mode 100755 index 55e537d..0000000 --- a/interfaces/gui/beg/beg +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python -import table -from Tkinter import * -from libbe import bugdir - -tk = Tk() -Label(tk, text="Bug list").pack() -mlb = table.MultiListbox(tk, (('Severity', 4), ('Creator', 8), ('Summary', 40))) -for bug in [b for b in bugdir.tree_root(".").list() if b.active]: - mlb.insert(END, (bug.severity, bug.creator, bug.summary)) -mlb.pack(expand=YES,fill=BOTH) -tk.mainloop() diff --git a/interfaces/gui/beg/table.py b/interfaces/gui/beg/table.py deleted file mode 100644 index 2865f28..0000000 --- a/interfaces/gui/beg/table.py +++ /dev/null @@ -1,97 +0,0 @@ -from Tkinter import * - -class MultiListbox(Frame): - def __init__(self, master, lists): - Frame.__init__(self, master) - self.lists = [] - for l,w in lists: - frame = Frame(self); frame.pack(side=LEFT, expand=YES, fill=BOTH) - Label(frame, text=l, borderwidth=1, relief=RAISED).pack(fill=X) - lb = Listbox(frame, width=w, borderwidth=0, selectborderwidth=0, - relief=FLAT, exportselection=FALSE) - lb.pack(expand=YES, fill=BOTH) - self.lists.append(lb) - lb.bind('', lambda e, s=self: s._select(e.y)) - lb.bind('', lambda e, s=self: s._select(e.y)) - lb.bind('', lambda e: 'break') - lb.bind('', lambda e, s=self: s._b2motion(e.x, e.y)) - lb.bind('', lambda e, s=self: s._button2(e.x, e.y)) - frame = Frame(self); frame.pack(side=LEFT, fill=Y) - Label(frame, borderwidth=1, relief=RAISED).pack(fill=X) - sb = Scrollbar(frame, orient=VERTICAL, command=self._scroll) - sb.pack(expand=YES, fill=Y) - self.lists[0]['yscrollcommand']=sb.set - - def _select(self, y): - row = self.lists[0].nearest(y) - self.selection_clear(0, END) - self.selection_set(row) - return 'break' - - def _button2(self, x, y): - for l in self.lists: l.scan_mark(x, y) - return 'break' - - def _b2motion(self, x, y): - for l in self.lists: l.scan_dragto(x, y) - return 'break' - - def _scroll(self, *args): - for l in self.lists: - apply(l.yview, args) - - def curselection(self): - return self.lists[0].curselection() - - def delete(self, first, last=None): - for l in self.lists: - l.delete(first, last) - - def get(self, first, last=None): - result = [] - for l in self.lists: - result.append(l.get(first,last)) - if last: return apply(map, [None] + result) - return result - - def index(self, index): - self.lists[0].index(index) - - def insert(self, index, *elements): - for e in elements: - i = 0 - for l in self.lists: - l.insert(index, e[i]) - i = i + 1 - - def size(self): - return self.lists[0].size() - - def see(self, index): - for l in self.lists: - l.see(index) - - def selection_anchor(self, index): - for l in self.lists: - l.selection_anchor(index) - - def selection_clear(self, first, last=None): - for l in self.lists: - l.selection_clear(first, last) - - def selection_includes(self, index): - return self.lists[0].selection_includes(index) - - def selection_set(self, first, last=None): - for l in self.lists: - l.selection_set(first, last) - -if __name__ == '__main__': - tk = Tk() - Label(tk, text='MultiListbox').pack() - mlb = MultiListbox(tk, (('Subject', 40), ('Sender', 20), ('Date', 10))) - for i in range(1000): - mlb.insert(END, ('Important Message: %d' % i, 'John Doe', '10/10/%04d' % (1900+i))) - mlb.pack(expand=YES,fill=BOTH) - tk.mainloop() - diff --git a/interfaces/gui/wxbe/wxbe b/interfaces/gui/wxbe/wxbe deleted file mode 100755 index e71ae0c..0000000 --- a/interfaces/gui/wxbe/wxbe +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python -import wx -from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin -import sys, os.path -from libbe import bugdir, names -from libbe.bug import cmp_status, cmp_severity, cmp_time, cmp_full - -class MyApp(wx.App): - def OnInit(self): - frame = BugListFrame(None, title="Bug List") - frame.Show(True) - self.SetTopWindow(frame) - return True - -class BugListFrame(wx.Frame): - def __init__(self, *args, **kwargs): - wx.Frame.__init__(self, *args, **kwargs) - bugs = BugList(self) - - # Widgets to display/sort/edit will go in this panel - # for now it is just a placeholder - panel = wx.Panel(self) - panel.SetBackgroundColour("RED") - - vbox = wx.BoxSizer(wx.VERTICAL) - vbox.Add(panel, 0, wx.EXPAND) - vbox.Add(bugs, 1, wx.EXPAND) - - self.SetAutoLayout(True) - self.SetSizer(vbox) - self.Layout() - -class BugList(wx.ListCtrl, ListCtrlAutoWidthMixin): - def __init__(self, parent): - wx.ListCtrl.__init__(self, parent, - style=wx.LC_REPORT) - ListCtrlAutoWidthMixin.__init__(self) - - self.bugdir = bugdir.tree_root(".") - self.buglist = list(self.bugdir.list()) - self.buglist.sort() - self.columns = ("id", "status", "severity", "summary") - - dataIndex = 0 - for x in range(len(self.columns)): - self.InsertColumn(x, self.columns[x].capitalize()) - self.SetColumnWidth(x, wx.LIST_AUTOSIZE_USEHEADER) - for bug in [b for b in self.buglist if b.active]: - name = names.unique_name(bug, self.buglist) - id = self.InsertStringItem(self.GetItemCount(), name) - self.SetStringItem(id, 1, bug.status) - self.SetStringItem(id, 2, bug.severity) - self.SetStringItem(id, 3, bug.summary) - self.SetItemData(id, dataIndex) # set keys for each line - dataIndex += 1 - self.EnsureVisible(id) - for x in range(len(self.columns)): - self.SetColumnWidth(x, wx.LIST_AUTOSIZE) - conts_width = self.GetColumnWidth(x) - self.SetColumnWidth(x, wx.LIST_AUTOSIZE_USEHEADER) - if conts_width > self.GetColumnWidth(x): - self.SetColumnWidth(x, conts_width) - - self.Bind(wx.EVT_LIST_COL_CLICK, self.OnColumnClick) - self.bugcmp_fn = cmp_full - # For reasons I don't understant, sorting is broken... - #self.SortItems(self.Sorter) - #self.Refresh() - def Sorter(self, key1, key2): - """Get bug info from the keys and pass to self.bugcmp_fn""" - bug1 = self.buglist[key1-1] - bug2 = self.buglist[key2-1] - # Another way of getting bug information - #bug1uuid = self.GetItem(key1, 0).GetText() - #bug2uuid = self.GetItem(key2, 0).GetText() - #print bug1uuid, bug2uuid - #bug1 = self.bugdir.get_bug(bug1uuid) - #bug2 = self.bugdir.get_bug(bug1uuid) - print self.bugcmp_fn(bug1,bug2) - return self.bugcmp_fn(bug1,bug2) - def OnColumnClick(self, event): - """Resort bug list depending on which column was clicked""" - print "TODO: sort by column %d" % event.Column - # change self.bugcmp_fn and resort, but I can't get it working - -app = MyApp() -app.MainLoop() diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt deleted file mode 100644 index def18b1..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt +++ /dev/null @@ -1,36 +0,0 @@ -README.txt -setup.py -start-beweb.py -Bugs-Everywhere-Web.egg-info/PKG-INFO -Bugs-Everywhere-Web.egg-info/SOURCES.txt -Bugs-Everywhere-Web.egg-info/not-zip-safe -Bugs-Everywhere-Web.egg-info/requires.txt -Bugs-Everywhere-Web.egg-info/sqlobject.txt -Bugs-Everywhere-Web.egg-info/top_level.txt -beweb/__init__.py -beweb/config.py -beweb/controllers.py -beweb/formatting.py -beweb/model.py -beweb/prest.py -beweb/release.py -beweb/config/__init__.py -beweb/templates/__init__.py -beweb/tests/__init__.py -beweb/tests/test_controllers.py -beweb/tests/test_model.py -libbe/__init__.py -libbe/arch.py -libbe/bugdir.py -libbe/bzr.py -libbe/cmdutil.py -libbe/config.py -libbe/diff.py -libbe/mapfile.py -libbe/names.py -libbe/no_rcs.py -libbe/plugin.py -libbe/rcs.py -libbe/restconvert.py -libbe/tests.py -libbe/utility.py diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/not-zip-safe b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/not-zip-safe deleted file mode 100644 index e69de29..0000000 diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt deleted file mode 100644 index 88b15cb..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt +++ /dev/null @@ -1 +0,0 @@ -TurboGears >= 0.9a4 \ No newline at end of file diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt deleted file mode 100644 index 7f7cbad..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt +++ /dev/null @@ -1,2 +0,0 @@ -db_module=beweb.model -history_dir=$base/beweb/sqlobject-history diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt deleted file mode 100644 index 6455be9..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt +++ /dev/null @@ -1,2 +0,0 @@ -beweb -libbe diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/PKG-INFO b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/PKG-INFO deleted file mode 100644 index 6cb6ad2..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/PKG-INFO +++ /dev/null @@ -1,15 +0,0 @@ -Metadata-Version: 1.0 -Name: Bugs-Everywhere-Web -Version: 1.0 -Summary: UNKNOWN -Home-page: UNKNOWN -Author: UNKNOWN -Author-email: UNKNOWN -License: UNKNOWN -Description: UNKNOWN -Platform: UNKNOWN -Classifier: Development Status :: 3 - Alpha -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Framework :: TurboGears diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/SOURCES.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/SOURCES.txt deleted file mode 100644 index ab62ee4..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/SOURCES.txt +++ /dev/null @@ -1,44 +0,0 @@ -README.txt -setup.py -start-beweb.py -Bugs_Everywhere_Web.egg-info/PKG-INFO -Bugs_Everywhere_Web.egg-info/SOURCES.txt -Bugs_Everywhere_Web.egg-info/dependency_links.txt -Bugs_Everywhere_Web.egg-info/not-zip-safe -Bugs_Everywhere_Web.egg-info/paster_plugins.txt -Bugs_Everywhere_Web.egg-info/requires.txt -Bugs_Everywhere_Web.egg-info/sqlobject.txt -Bugs_Everywhere_Web.egg-info/top_level.txt -Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/SOURCES.txt -Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/not-zip-safe -Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/requires.txt -Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/sqlobject.txt -Bugs_Everywhere_Web.egg-info/Bugs-Everywhere-Web.egg-info/top_level.txt -beweb/__init__.py -beweb/config.py -beweb/controllers.py -beweb/formatting.py -beweb/json.py -beweb/model.py -beweb/prest.py -beweb/release.py -beweb/config/__init__.py -beweb/templates/__init__.py -beweb/tests/__init__.py -beweb/tests/test_controllers.py -beweb/tests/test_model.py -libbe/__init__.py -libbe/arch.py -libbe/bugdir.py -libbe/bzr.py -libbe/cmdutil.py -libbe/config.py -libbe/diff.py -libbe/mapfile.py -libbe/names.py -libbe/no_rcs.py -libbe/plugin.py -libbe/rcs.py -libbe/restconvert.py -libbe/tests.py -libbe/utility.py diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/dependency_links.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/not-zip-safe b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/not-zip-safe deleted file mode 100644 index 8b13789..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/not-zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/paster_plugins.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/paster_plugins.txt deleted file mode 100644 index 14fec70..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/paster_plugins.txt +++ /dev/null @@ -1,2 +0,0 @@ -TurboGears -PasteScript diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/requires.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/requires.txt deleted file mode 100644 index 5fd6f71..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/requires.txt +++ /dev/null @@ -1 +0,0 @@ -TurboGears >= 1.0b1 \ No newline at end of file diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/sqlobject.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/sqlobject.txt deleted file mode 100644 index 7f7cbad..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/sqlobject.txt +++ /dev/null @@ -1,2 +0,0 @@ -db_module=beweb.model -history_dir=$base/beweb/sqlobject-history diff --git a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/top_level.txt b/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/top_level.txt deleted file mode 100644 index 74a8358..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/Bugs_Everywhere_Web.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -beweb diff --git a/interfaces/web/Bugs-Everywhere-Web/README b/interfaces/web/Bugs-Everywhere-Web/README deleted file mode 100644 index c152757..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/README +++ /dev/null @@ -1,60 +0,0 @@ -Using BeWeb, the web UI -======================= -BeWeb uses the Turbogears framework: http://www.turbogears.org/ -Please ensure you have Turbogears 0.8a5 or a compatible release installed. -Because it uses BE data, the web UI does not require a database. - -To use BeWeb, first create a configuration file, telling it which projects -to track, and what to call them. An example configuration file -(beweb/beweb/config.py.example) is provided. - -Next, cd to this directory, and run ./start-beweb.py - -BeWeb allows you to create, view and edit bugs, but it is in an early stage of -development, so some features are missing. - -Configuration file ------------------- - -Configure by creating an appropriate beweb/config.py from -beweb/config.py.example. The server will edit the repositories that -it manages, so you should probably have it running on a seperate -branch than your working repository. You can then merge/push -as you require to keep the branches in sync. - -See - http://docs.turbogears.org/1.0/Configuration -For standard turbogears configuration information. - -Actions -------- - -Currently, you need to login for any methods with a -@identity.require() decorator. The only group in the current -implementation is 'editbugs'. Basically, anyone can browse around, -but only registered 'editbugs' members can change things. - -Anonymous actions: - * See project tree - * See buglist - * See comments -Editbugs required actions: - * Create new comments - * Reply to comments - * Update comment info - -Users ------ - -All login attempts will fail unless you have added some valid users. See - http://docs.turbogears.org/1.0/GettingStartedWithIdentity -For a good intro. For the impatient, try something like - Bugs-Everywhere-Web$ tg-admin toolbox - browse to 'CatWalk' -> 'User' -> 'Add User+' -or - Bugs-Everywhere-Web$ tg-admin sholl - >>> u = User(user_name=u'jdoe', email_address=u'jdoe@example.com', - display_name=u'Jane Doe', password=u'xxx') - >>> g = Group(group_name=u'editbugs', display_name=u'Edit Bugs') - >>> g.addUser(u) # BE-Web uses SQLObject -Exit the tg-admin shell with Ctrl-Z on MS Windows, Ctrl-D on other systems. diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/__init__.py b/interfaces/web/Bugs-Everywhere-Web/beweb/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/app.cfg b/interfaces/web/Bugs-Everywhere-Web/beweb/app.cfg deleted file mode 100644 index 024fa8a..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/app.cfg +++ /dev/null @@ -1,120 +0,0 @@ -[global] -# The settings in this file should not vary depending on the deployment -# environment. devcfg.py and prodcfg.py are the locations for -# the different deployment settings. Settings in this file will -# be overridden by settings in those other files. - -# The commented out values below are the defaults - -# VIEW - -# which view (template engine) to use if one is not specified in the -# template name -# tg.defaultview = "kid" - -# kid.outputformat="html" -# kid.encoding="utf-8" - -# The sitetemplate is used for overall styling of a site that -# includes multiple TurboGears applications -# tg.sitetemplate="" - -# Allow every exposed function to be called as json, -# tg.allow_json = False - -# Set to True if you'd like all of your pages to include MochiKit -# tg.mochikit_all = False - -# VISIT TRACKING -# Each visit to your application will be assigned a unique visit ID tracked via -# a cookie sent to the visitor's browser. -# -------------- - -# Enable Visit tracking -visit.on=True - -# Number of minutes a visit may be idle before it expires. -# visit.timeout=20 - -# The name of the cookie to transmit to the visitor's browser. -# visit.cookie.name="tg-visit" - -# Domain name to specify when setting the cookie (must begin with . according to -# RFC 2109). The default (None) should work for most cases and will default to -# the machine to which the request was made. NOTE: localhost is NEVER a valid -# value and will NOT WORK. -# visit.cookie.domain=None - -# Specific path for the cookie -# visit.cookie.path="/" - -# The name of the VisitManager plugin to use for visitor tracking. -# visit.manager="sqlobject" - - -# IDENTITY -# General configuration of the TurboGears Identity management module -# -------- - -# Switch to turn on or off the Identity management module -identity.on=True - -# [REQUIRED] URL to which CherryPy will internally redirect when an access -# control check fails. If Identity management is turned on, a value for this -# option must be specified. -identity.failure_url="/login" - -# The IdentityProvider to use -- defaults to the SqlObjectIdentityProvider which -# pulls User, Group, and Permission data out of your model database. -identity.provider="sqlobject" - -# The names of the fields on the login form containing the visitor's user ID -# and password. In addition, the submit button is specified simply so its -# existence may be stripped out prior to passing the form data to the target -# controller. -identity.form.user_name="user_name" -identity.form.password="password" -identity.form.submit="login" - -# What sources should the identity provider consider when determining the -# identity associated with a request? Comma separated list of identity sources. -# Valid sources: form, visit, http_auth -identity.source="form,http_auth,visit" - - -# SqlObjectIdentityProvider -# Configuration options for the default IdentityProvider -# ------------------------- - -# The classes you wish to use for your Identity model. Leave these commented out -# to use the default classes for SqlObjectIdentityProvider. Or set them to the -# classes in your model. NOTE: These aren't TG_* because the TG prefix is -# reserved for classes created by TurboGears. -# identity.soprovider.model.user="beweb.model.User" -# identity.soprovider.model.group="beweb.model.Group" -# identity.soprovider.model.permission="beweb.model.Permission" - -# The password encryption algorithm used when comparing passwords against what's -# stored in the database. Valid values are 'md5' or 'sha1'. If you do not -# specify an encryption algorithm, passwords are expected to be clear text. -# -# The SqlObjectProvider *will* encrypt passwords supplied as part of your login -# form. If you set the password through the password property, like: -# my_user.password = 'secret' -# the password will be encrypted in the database, provided identity is up and -# running, or you have loaded the configuration specifying what encryption to -# use (in situations where identity may not yet be running, like tests). - -# identity.soprovider.encryption_algorithm=None - -[/static] -static_filter.on = True -static_filter.dir = "." - -[/favicon.ico] -static_filter.on = True -static_filter.file = "images/favicon.ico" - -[/] -decodingFilter.on = True -static_filter.root = '%(package_dir)s/static' diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/config.py.example b/interfaces/web/Bugs-Everywhere-Web/beweb/config.py.example deleted file mode 100644 index 8745c6d..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/config.py.example +++ /dev/null @@ -1,10 +0,0 @@ -# This is an example beweb configuration file. - -# One thing we need is a map of projects. Projects have a beweb ID, a path, -# and a display name. - -# In this example, the 'be' beweb ID is assigned the display name "Bugs -# Everywhere" and the path "/home/abentley/be" - -projects = {"be": ("Bugs Everywhere","/home/abentley/be"), - } diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/config/app.cfg b/interfaces/web/Bugs-Everywhere-Web/beweb/config/app.cfg deleted file mode 100644 index 15555b7..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/config/app.cfg +++ /dev/null @@ -1,92 +0,0 @@ -[global] -# The settings in this file should not vary depending on the deployment -# environment. dev.cfg and prod.cfg are the locations for -# the different deployment settings. Settings in this file will -# be overridden by settings in those other files. - -# The commented out values below are the defaults - -# VIEW - -# which view (template engine) to use if one is not specified in the -# template name -# tg.defaultview = "kid" - -# The following kid settings determine the settings used by the kid serializer. - -# One of (html|xml|json) -# kid.outputformat="html" - -# kid.encoding="utf-8" - -# The sitetemplate is used for overall styling of a site that -# includes multiple TurboGears applications -# tg.sitetemplate="" - -# Allow every exposed function to be called as json, -# tg.allow_json = False - -# List of Widgets to include on every page. -# for exemple ['turbogears.mochikit'] -# tg.include_widgets = [] - -# Set to True if the scheduler should be started -# tg.scheduler = False - -# IDENTITY -# General configuration of the TurboGears Identity management module -# -------- - -# Switch to turn on or off the Identity management module -identity.on=True - -# [REQUIRED] URL to which CherryPy will internally redirect when an access -# control check fails. If Identity management is turned on, a value for this -# option must be specified. -identity.failure_url="/login" - -# identity.provider='sqlobject' - -# The names of the fields on the login form containing the visitor's user ID -# and password. In addition, the submit button is specified simply so its -# existence may be stripped out prior to passing the form data to the target -# controller. -# identity.form.user_name="user_name" -# identity.form.password="password" -# identity.form.submit="login" - -# What sources should the identity provider consider when determining the -# identity associated with a request? Comma separated list of identity sources. -# Valid sources: form, visit, http_auth -# identity.source="form,http_auth,visit" - -# SqlObjectIdentityProvider -# Configuration options for the default IdentityProvider -# ------------------------- - -# The classes you wish to use for your Identity model. Remember to not use reserved -# SQL keywords for class names (at least unless you specify a different table -# name using sqlmeta). -identity.soprovider.model.user="stfa.model.User" -identity.soprovider.model.group="stfa.model.Group" -identity.soprovider.model.permission="stfa.model.Permission" - -# The password encryption algorithm used when comparing passwords against what's -# stored in the database. Valid values are 'md5' or 'sha1'. If you do not -# specify an encryption algorithm, passwords are expected to be clear text. -# The SqlObjectProvider *will* encrypt passwords supplied as part of your login -# form. If you set the password through the password property, like: -# my_user.password = 'secret' -# the password will be encrypted in the database, provided identity is up and -# running, or you have loaded the configuration specifying what encryption to -# use (in situations where identity may not yet be running, like tests). - -# identity.soprovider.encryption_algorithm=None - -[/static] -static_filter.on = True -static_filter.dir = "%(top_level_dir)s/static" - -[/favicon.ico] -static_filter.on = True -static_filter.file = "%(top_level_dir)s/static/images/favicon.ico" diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/config/log.cfg b/interfaces/web/Bugs-Everywhere-Web/beweb/config/log.cfg deleted file mode 100644 index ce776f8..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/config/log.cfg +++ /dev/null @@ -1,29 +0,0 @@ -# LOGGING -# Logging is often deployment specific, but some handlers and -# formatters can be defined here. - -[logging] -[[formatters]] -[[[message_only]]] -format='*(message)s' - -[[[full_content]]] -format='*(asctime)s *(name)s *(levelname)s *(message)s' - -[[handlers]] -[[[debug_out]]] -class='StreamHandler' -level='DEBUG' -args='(sys.stdout,)' -formatter='full_content' - -[[[access_out]]] -class='StreamHandler' -level='INFO' -args='(sys.stdout,)' -formatter='message_only' - -[[[error_out]]] -class='StreamHandler' -level='ERROR' -args='(sys.stdout,)' diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py b/interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py deleted file mode 100644 index 50cc754..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/controllers.py +++ /dev/null @@ -1,240 +0,0 @@ -import logging - -import cherrypy -import turbogears -from turbogears import controllers, expose, validate, redirect, identity - -from libbe.bugdir import tree_root, NoRootEntry -from config import projects -from prest import PrestHandler, provide_action - - -from beweb import json - -log = logging.getLogger("beweb.controllers") - -def project_tree(project): - try: - return tree_root(projects[project][1]) - except KeyError: - raise Exception("Unknown project %s" % project) - -def comment_url(project, bug, comment, **kwargs): - return turbogears.url("/project/%s/bug/%s/comment/%s" % - (project, bug, comment), kwargs) - -class Comment(PrestHandler): - @identity.require( identity.has_permission("editbugs")) - @provide_action("action", "New comment") - def new_comment(self, comment_data, comment, *args, **kwargs): - bug_tree = project_tree(comment_data['project']) - bug = bug_tree.get_bug(comment_data['bug']) - comment = new_comment(bug, "") - comment.From = identity.current.user.userId - comment.content_type = "text/restructured" - comment.save() - raise cherrypy.HTTPRedirect(comment_url(comment=comment.uuid, - **comment_data)) - - @identity.require( identity.has_permission("editbugs")) - @provide_action("action", "Reply") - def reply_comment(self, comment_data, comment, *args, **kwargs): - bug_tree = project_tree(comment_data['project']) - bug = bug_tree.get_bug(comment_data['bug']) - reply_comment = new_comment(bug, "") - reply_comment.From = identity.current.user.userId - reply_comment.in_reply_to = comment.uuid - reply_comment.save() - reply_data = dict(comment_data) - del reply_data["comment"] - raise cherrypy.HTTPRedirect(comment_url(comment=reply_comment.uuid, - **reply_data)) - - @identity.require( identity.has_permission("editbugs")) - @provide_action("action", "Update") - def update(self, comment_data, comment, comment_body, *args, **kwargs): - comment.body = comment_body - comment.save() - raise cherrypy.HTTPRedirect(bug_url(comment_data['project'], - comment_data['bug'])) - - def instantiate(self, project, bug, comment): - bug_tree = project_tree(project) - bug = bug_tree.get_bug(bug) - return bug.get_comment(comment) - - def dispatch(self, comment_data, comment, *args, **kwargs): - return self.edit_comment(comment_data['project'], comment) - - @turbogears.expose(html="beweb.templates.edit_comment") - def edit_comment(self, project, comment): - return {"comment": comment, "project_id": project} - -class Bug(PrestHandler): - comment = Comment() - @turbogears.expose(html="beweb.templates.edit_bug") - def index(self, project, bug): - return {"bug": bug, "project_id": project} - - def dispatch(self, bug_data, bug, *args, **kwargs): - if bug is None: - return self.list(bug_data['project'], **kwargs) - else: - return self.index(bug_data['project'], bug) - - @turbogears.expose(html="beweb.templates.bugs") - def list(self, project, sort_by=None, show_closed=False, action=None, - search=None): - if action == "New bug": - self.new_bug() - if show_closed == "False": - show_closed = False - bug_tree = project_tree(project) - bugs = list(bug_tree.list()) - if sort_by is None: - bugs.sort() - return {"project_id" : project, - "project_name" : projects[project][0], - "bugs" : bugs, - "show_closed" : show_closed, - "search" : search, - } - - @identity.require( identity.has_permission("editbugs")) - @provide_action("action", "New bug") - def new_bug(self, bug_data, bug, **kwargs): - bug = project_tree(bug_data['project']).new_bug() - bug.creator = identity.current.user.userId - bug.save() - raise cherrypy.HTTPRedirect(bug_url(bug_data['project'], bug.uuid)) - - @identity.require( identity.has_permission("editbugs")) - @provide_action("action", "Update") - def update(self, bug_data, bug, status, severity, summary, assigned, - action): - bug.status = status - bug.severity = severity - bug.summary = summary - if assigned == "": - assigned = None - bug.assigned = assigned - bug.save() -# bug.vcs.precommit(bug.path) -# bug.vcs.commit(bug.path, "Auto-commit") -# bug.vcs.postcommit(bug.path) - raise cherrypy.HTTPRedirect(bug_list_url(bug_data["project"])) - - def instantiate(self, project, bug): - return project_tree(project).get_bug(bug) - - @provide_action("action", "New comment") - def new_comment(self, bug_data, bug, *args, **kwargs): - try: - self.update(bug_data, bug, *args, **kwargs) - except cherrypy.HTTPRedirect: - pass - return self.comment.new_comment(bug_data, comment=None, *args, - **kwargs) - - -def project_url(project_id=None): - project_url = "/project/" - if project_id is not None: - project_url += "%s/" % project_id - return turbogears.url(project_url) - -def bug_url(project_id, bug_uuid=None): - bug_url = "/project/%s/bug/" % project_id - if bug_uuid is not None: - bug_url += "%s/" % bug_uuid - return turbogears.url(bug_url) - -def bug_list_url(project_id, show_closed=False, search=None): - bug_url = "/project/%s/bug/?show_closed=%s" % (project_id, - str(show_closed)) - if search is not None: - bug_url = "%s&search=%s" % (bug_url, search) - return turbogears.url(str(bug_url)) - - -class Project(PrestHandler): - bug = Bug() - @turbogears.expose(html="beweb.templates.projects") - def dispatch(self, project_data, project, *args, **kwargs): - if project is not None: - raise cherrypy.HTTPRedirect(bug_url(project)) - else: - return {"projects": projects} - - def instantiate(self, project): - return project - - -class Root(controllers.Root): - prest = PrestHandler() - prest.project = Project() - @turbogears.expose() - def index(self): - raise cherrypy.HTTPRedirect(project_url()) - - @expose(template="beweb.templates.login") - def login(self, forward_url=None, previous_url=None, *args, **kw): - - if not identity.current.anonymous and identity.was_login_attempted(): - raise redirect(forward_url) - - forward_url=None - previous_url= cherrypy.request.path - - if identity.was_login_attempted(): - msg=_("The credentials you supplied were not correct or "\ - "did not grant access to this resource.") - elif identity.get_identity_errors(): - msg=_("You must provide your credentials before accessing "\ - "this resource.") - else: - msg=_("Please log in.") - forward_url= cherrypy.request.headers.get("Referer", "/") - cherrypy.response.status=403 - return dict(message=msg, previous_url=previous_url, logging_in=True, - original_parameters=cherrypy.request.params, - forward_url=forward_url) - - @expose() - def logout(self): - identity.current.logout() - raise redirect("/") - - @turbogears.expose('beweb.templates.about') - def about(self, *paths, **kwargs): - return {} - - @turbogears.expose() - def default(self, *args, **kwargs): - return self.prest.default(*args, **kwargs) - - def _cp_on_error(self): - import traceback, StringIO - bodyFile = StringIO.StringIO() - traceback.print_exc(file = bodyFile) - trace_text = bodyFile.getvalue() - try: - raise - except cherrypy.NotFound: - self.handle_error('Not Found', str(e), trace_text, '404 Not Found') - - except NoRootEntry, e: - self.handle_error('Project Misconfiguration', str(e), trace_text) - - except Exception, e: - self.handle_error('Internal server error', str(e), trace_text) - - def handle_error(self, heading, body, traceback=None, - status='500 Internal Server Error'): - cherrypy.response.headerMap['Status'] = status - cherrypy.response.body = [self.errorpage(heading, body, traceback)] - - - @turbogears.expose(html='beweb.templates.error') - def errorpage(self, heading, body, traceback): - return {'heading': heading, 'body': body, 'traceback': traceback} diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/formatting.py b/interfaces/web/Bugs-Everywhere-Web/beweb/formatting.py deleted file mode 100644 index 1278414..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/formatting.py +++ /dev/null @@ -1,76 +0,0 @@ -from StringIO import StringIO - -try : - from xml.etree.ElementTree import XML # Python 2.5 (and greater?) -except ImportError : - from elementtree.ElementTree import XML -from libbe.restconvert import rest_xml - -def to_unix(text): - skip_newline = False - for ch in text: - if ch not in ('\r', '\n'): - yield ch - else: - if ch == '\n': - if skip_newline: - continue - else: - skip_newline = True - yield '\n' - - -def soft_text(text): - first_space = False - translations = {'\n': '
\n', '&': '&', '\x3c': '<', - '\x3e': '>'} - for ch in to_unix(text): - if ch == ' ' and first_space is True: - yield ' ' - first_space = ch in (' ') - try: - yield translations[ch] - except KeyError: - yield ch - - -def soft_pre(text): - return XML('
'+ - ''.join(soft_text(text)).encode('utf-8')+'
') - - -def get_rest_body(rest): - xml, warnings = rest_xml(StringIO(rest)) - return xml.find('{http://www.w3.org/1999/xhtml}body'), warnings - - -def comment_body_xhtml(comment): - if comment.content_type == "text/restructured": - return get_rest_body(comment.body)[0] - else: - return soft_pre(comment.body) - - -def select_among(name, options, default, display_names=None): - output = ['") - return XML("".join(output)) diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/json.py b/interfaces/web/Bugs-Everywhere-Web/beweb/json.py deleted file mode 100644 index 6e100c3..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/json.py +++ /dev/null @@ -1,13 +0,0 @@ -# This module provides helper functions for the JSON part of your -# view, if you are providing a JSON-based API for your app. - -# Here's what most rules would look like: -# @jsonify.when("isinstance(obj, YourClass)") -# def jsonify_yourclass(obj): -# return [obj.val1, obj.val2] -# -# The goal is to break your objects down into simple values: -# lists, dicts, numbers and strings - -from turbojson.jsonify import jsonify - diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/model.py b/interfaces/web/Bugs-Everywhere-Web/beweb/model.py deleted file mode 100644 index aa4b6b6..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/model.py +++ /dev/null @@ -1,107 +0,0 @@ -from datetime import datetime - -from sqlobject import * -from turbogears.database import PackageHub -from turbogears import identity - -hub = PackageHub("beweb") -__connection__ = hub - -class Visit(SQLObject): - class sqlmeta: - table = "visit" - - visit_key = StringCol(length=40, alternateID=True, - alternateMethodName="by_visit_key") - created = DateTimeCol(default=datetime.now) - expiry = DateTimeCol() - - def lookup_visit(cls, visit_key): - try: - return cls.by_visit_key(visit_key) - except SQLObjectNotFound: - return None - lookup_visit = classmethod(lookup_visit) - -class VisitIdentity(SQLObject): - visit_key = StringCol(length=40, alternateID=True, - alternateMethodName="by_visit_key") - user_id = IntCol() - - -class Group(SQLObject): - """ - An ultra-simple group definition. - """ - - # names like "Group", "Order" and "User" are reserved words in SQL - # so we set the name to something safe for SQL - class sqlmeta: - table = "tg_group" - - group_name = UnicodeCol(length=16, alternateID=True, - alternateMethodName="by_group_name") - display_name = UnicodeCol(length=255) - created = DateTimeCol(default=datetime.now) - - # collection of all users belonging to this group - users = RelatedJoin("User", intermediateTable="user_group", - joinColumn="group_id", otherColumn="user_id") - - # collection of all permissions for this group - permissions = RelatedJoin("Permission", joinColumn="group_id", - intermediateTable="group_permission", - otherColumn="permission_id") - - -class User(SQLObject): - """ - Reasonably basic User definition. Probably would want additional attributes. - """ - # names like "Group", "Order" and "User" are reserved words in SQL - # so we set the name to something safe for SQL - class sqlmeta: - table = "tg_user" - - child_name = UnicodeCol(length=255) - user_name = UnicodeCol(length=16, alternateID=True, - alternateMethodName="by_user_name") - email_address = UnicodeCol(length=255, alternateID=True, - alternateMethodName="by_email_address") - display_name = UnicodeCol(length=255) - password = UnicodeCol(length=40) - created = DateTimeCol(default=datetime.now) - - # groups this user belongs to - groups = RelatedJoin("Group", intermediateTable="user_group", - joinColumn="user_id", otherColumn="group_id") - - def _get_permissions(self): - perms = set() - for g in self.groups: - perms = perms | set(g.permissions) - return perms - - def _set_password(self, cleartext_password): - "Runs cleartext_password through the hash algorithm before saving." - hash = identity.encrypt_password(cleartext_password) - self._SO_set_password(hash) - - def set_password_raw(self, password): - "Saves the password as-is to the database." - self._SO_set_password(password) - - - -class Permission(SQLObject): - permission_name = UnicodeCol(length=16, alternateID=True, - alternateMethodName="by_permission_name") - description = UnicodeCol(length=255) - - groups = RelatedJoin("Group", - intermediateTable="group_permission", - joinColumn="permission_id", - otherColumn="group_id") - -def people_map(): - return dict((u.user_name, u.display_name) for u in User.select()) diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/prest.py b/interfaces/web/Bugs-Everywhere-Web/beweb/prest.py deleted file mode 100644 index 9a6505d..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/prest.py +++ /dev/null @@ -1,168 +0,0 @@ -from unittest import TestCase -import unittest -from cherrypy import NotFound -"""A pseudo-REST dispatching method in which only the noun comes from the path. -The action performed will depend on kwargs. -""" - -class AmbiguousAction(Exception): - def __init__(self, actions): - Exception.__init__(self, "Supplied action is ambiguous.") - self.actions = actions - - -def provide_action(name, value): - def provider(func): - func._action_desc = (name, value) - return func - return provider - -class PrestHandler(object): - def __init__(self): - object.__init__(self) - self.actions = {} - for member in (getattr(self, m) for m in dir(self)): - if not hasattr(member, '_action_desc'): - continue - name, value = member._action_desc - if name not in self.actions: - self.actions[name] = {} - self.actions[name][value] = member - - @classmethod - def add_action(klass, name, value, function): - if name not in klass.actions: - klass.actions[name] = {} - klass.actions[name][value] = function - - - def decode(self, path, data=None): - """Convert the path into a handler, a resource, data, and extra_path""" - if data is None: - data = {} - if len(path) < 2 or not (hasattr(self, path[1])): - if len(path) == 0: - resource = None - else: - try: - resource = self.instantiate(**data) - except NotImplementedError, e: - if e.args[0] is not PrestHandler.instantiate: - raise NotFound() - - return self, resource, data, path[1:] - if len(path) > 2: - data[path[1]] = path[2] - return getattr(self, path[1]).decode(path[2:], data) - - def instantiate(self, **date): - raise NotImplementedError(PrestHandler.instantiate) - - def default(self, *args, **kwargs): - child, resource, data, extra = self.decode([None,] + list(args)) - action = child.get_action(**kwargs) - new_args = ([data, resource]+extra) - if action is not None: - return action(*new_args, **kwargs) - else: - return child.dispatch(*new_args, **kwargs) - - def get_action(self, **kwargs): - """Return the action requested by kwargs, if any. - - Raises AmbiguousAction if more than one action matches. - """ - actions = [] - for key in kwargs: - if key in self.actions: - if kwargs[key] in self.actions[key]: - actions.append(self.actions[key][kwargs[key]]) - if len(actions) == 0: - return None - elif len(actions) == 1: - return actions[0] - else: - raise AmbiguousAction(actions) - - -class PrestTester(TestCase): - def test_decode(self): - class ProjectHandler(PrestHandler): - actions = {} - def dispatch(self, project_data, project, *args, **kwargs): - self.project_id = project_data['project'] - self.project_data = project_data - self.resource = project - self.args = args - self.kwargs = kwargs - - def instantiate(self, project): - return [project] - - @provide_action('action', 'Save') - def save(self, project_data, project, *args, **kwargs): - self.action = "save" - - @provide_action('behavior', 'Update') - def update(self, project_data, project, *args, **kwargs): - self.action = "update" - - foo = PrestHandler() - foo.project = ProjectHandler() - handler, resource, data, extra = foo.decode([None, 'project', '83', - 'bloop', 'yeah']) - assert handler is foo.project - self.assertEqual({'project': '83'}, data) - self.assertEqual(['bloop', 'yeah'], extra) - foo.default(*['project', '27', 'extra'], **{'a':'b', 'b':'97'}) - self.assertEqual(foo.project.args, ('extra',)) - self.assertEqual(foo.project.kwargs, {'a':'b', 'b':'97'}) - self.assertEqual(foo.project.project_data, {'project': '27'}) - self.assertEqual(foo.project.resource, ['27']) - foo.default(*['project', '27', 'extra'], **{'action':'Save', 'b':'97'}) - self.assertEqual(foo.project.action, 'save') - foo.default(*['project', '27', 'extra'], - **{'behavior':'Update', 'b':'97'}) - self.assertEqual(foo.project.action, 'update') - self.assertRaises(AmbiguousAction, foo.default, - *['project', '27', 'extra'], - **{'behavior':'Update', 'action':'Save', 'b':'97'}) - - class BugHandler(PrestHandler): - actions = {} - def dispatch(self, bug_data, bug, *args, **kwargs): - self.project_id = project_data['project'] - self.project_data = project_data - self.resource = project - self.args = args - self.kwargs = kwargs - - def instantiate(self, project, bug): - return [project, bug] - - @provide_action('action', 'Save') - def save(self, project_data, project, *args, **kwargs): - self.action = "save" - - @provide_action('behavior', 'Update') - def update(self, project_data, project, *args, **kwargs): - self.action = "update" - - foo.project.bug = BugHandler() - handler, resource, data, extra = foo.decode([None, 'project', '83', - 'bug', '92']) - assert handler is foo.project.bug - self.assertEqual(resource[0], '83') - self.assertEqual(resource[1], '92') - self.assertEqual([], extra) - self.assertEqual(data['project'], '83') - self.assertEqual(data['bug'], '92') - -def test(): - patchesTestSuite = unittest.makeSuite(PrestTester,'test') - runner = unittest.TextTestRunner(verbosity=0) - return runner.run(patchesTestSuite) - - -if __name__ == "__main__": - test() diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/release.py b/interfaces/web/Bugs-Everywhere-Web/beweb/release.py deleted file mode 100644 index 9d64bf7..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/release.py +++ /dev/null @@ -1,14 +0,0 @@ -# Release information about Bugs-Everywhere-Web - -version = "1.0" - -# description = "Your plan to rule the world" -# long_description = "More description about your plan" -# author = "Your Name Here" -# email = "YourEmail@YourDomain" -# copyright = "Vintage 2006 - a good year indeed" - -# if it's open source, you might want to specify these -# url = "http://yourcool.site/" -# download_url = "http://yourcool.site/download" -# license = "MIT" diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/css/style.css b/interfaces/web/Bugs-Everywhere-Web/beweb/static/css/style.css deleted file mode 100644 index 6fe197f..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/static/css/style.css +++ /dev/null @@ -1,116 +0,0 @@ -table -{ - background-color: black; -} -td -{ - background-color: white; -} -h1 -{ - font-family: "Verdana"; - font-weight: bold; - font-size: 120%; - margin-bottom:0; - color: #990; -} - -tr.closed td -{ - background-color: #ccc; -} -tr.closedeven td -{ - background-color: #ccc; -} -tr.closedodd td -{ - background-color: #dda; -} - -a:visited, a:link -{ - color: #990; - text-decoration: None; -} -td a:visited, td a:link -{ - display: block; -} -a:visited:hover, a:link:hover -{ - text-decoration: underline; -} -td a:visited:hover, td a:link:hover -{ - color:black; - background-color:#dda; - text-decoration: None; - display: block; -} - -body -{ - font-family: "Verdana"; - font-size:11pt; - background-color: white; -} -.comment -{ -} -.comment table -{ - background-color: transparent; -} -.comment td -{ - background-color: transparent; -} -.comment pre -{ - font-family: "Verdana"; -} -#header -{ - color: black; - font-weight: bold; - background-image: url(/static/images/half-spiral.png); - background-position: right center; - background-repeat: no-repeat; - background-color: #ff0; -} -#header ul.navoption -{ - display: block; - float: right; - margin: 0; - padding-right: 30px; -} -#header li -{ - display: inline; - margin:0; - padding:0; -} -table.insetbox -{ - margin-top: 0.5em; - margin-bottom: 0.5em; -} -.insetbox tr, .insetbox td -{ - margin: 0; - padding: 0; -} -pre.traceback -{ - font-family: Verdana, Ariel, Helvetica, sanserif; -} -tr.even td -{ - background-color: #eee; -} -tr.odd td -{ - background-color: #ffe; -} diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-b.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-b.png deleted file mode 100644 index 790e438..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-b.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-bl.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-bl.png deleted file mode 100644 index 5b43259..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-bl.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-br.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-br.png deleted file mode 100644 index 6cfd62c..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-br.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-l.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-l.png deleted file mode 100644 index a6ce3ce..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-l.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-r.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-r.png deleted file mode 100644 index 1ffd6f8..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-r.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-t.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-t.png deleted file mode 100644 index 0129b0c..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-t.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tl.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tl.png deleted file mode 100644 index d616b77..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tl.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tr.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tr.png deleted file mode 100644 index 18e542e..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds-tr.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-b.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-b.png deleted file mode 100644 index 05a190e..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-b.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-r.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-r.png deleted file mode 100644 index 0c3ea4c..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ds2-r.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.ico b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.ico deleted file mode 100644 index 339d09c..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.ico and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.png deleted file mode 100644 index 6dc53ee..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/favicon.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/half-spiral.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/half-spiral.png deleted file mode 100644 index cb4b56c..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/half-spiral.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/header_inner.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/header_inner.png deleted file mode 100644 index 2b2d87d..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/header_inner.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/info.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/info.png deleted file mode 100644 index 329c523..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/info.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-b.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-b.png deleted file mode 100644 index 25d3cfa..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-b.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-bl.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-bl.png deleted file mode 100644 index f496223..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-bl.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-br.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-br.png deleted file mode 100644 index 74cbd91..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-br.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-l.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-l.png deleted file mode 100644 index dd567fa..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-l.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-r.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-r.png deleted file mode 100644 index 9ac4486..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-r.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-t.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-t.png deleted file mode 100644 index fbb06c8..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-t.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tl.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tl.png deleted file mode 100644 index 9336290..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tl.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tr.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tr.png deleted file mode 100644 index de74808..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/is-tr.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ok.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ok.png deleted file mode 100644 index fee6751..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/ok.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/shadows.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/shadows.png deleted file mode 100644 index 9ddc676..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/shadows.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/spiral.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/spiral.png deleted file mode 100644 index b4bcb1e..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/spiral.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/tg_under_the_hood.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/tg_under_the_hood.png deleted file mode 100644 index bc9c79c..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/tg_under_the_hood.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/under_the_hood_blue.png b/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/under_the_hood_blue.png deleted file mode 100644 index 90e84b7..0000000 Binary files a/interfaces/web/Bugs-Everywhere-Web/beweb/static/images/under_the_hood_blue.png and /dev/null differ diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/__init__.py b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/about.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/about.kid deleted file mode 100644 index fa3548a..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/about.kid +++ /dev/null @@ -1,21 +0,0 @@ - - - - - About Bugs Everywhere - - - -

About Bugs Everywhere

-

Bugs Everywhere is a "distributed bugtracker", designed to complement distributed revision control systems. -

-

-Bugs Everywhere was conceived and written by developers at Panoramic Feedback, primarily Aaron Bentley. Panoramic Feedback is no longer developing BE, and the current maintainer is Chris Ball. -

-

- Bugs Everywhere web site -

-Project List - - diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/bugs.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/bugs.kid deleted file mode 100644 index 198aa94..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/bugs.kid +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - Bugs for $project_name - - - -

Bug list for ${project_name}

- - -
- -
IDStatusSeverityAssigned ToCommentsSummary
${unique_name(bug, bugs[:])}${bug.status}${bug.severity}${people.get(bug.assigned, bug.assigned)}${len(list(bug.iter_comment_ids()))}${bug.summary}
-Project list -Toggle closed -
- -
-
- - - -
- - diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid deleted file mode 100644 index 276f610..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_bug.kid +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - Edit bug - - - -

Edit bug

-
- - - - -
StatusSeverityAssigned ToSummary
${select_among("status", status_values, bug.status)}${select_among("severity", severity_values, bug.severity)}${select_among("assigned", people.keys()+[None], bug.assigned, people)}
-
- - - - -
From${comment.From}
Date${time_to_str(comment.time)}
-
- Edit - Reply -
-
-
- ${show_comment(child, grandchildren)} -
-
-
-
- ${show_comment(comment, children)} -
-

-

-
-Bug List - - diff --git a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid b/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid deleted file mode 100644 index 2b522d4..0000000 --- a/interfaces/web/Bugs-Everywhere-Web/beweb/templates/edit_comment.kid +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - Edit comment - - - -

Edit comment

-
- - - -
From${comment.From}
Date${time_to_str(comment.time)}
-