diff options
author | W. Trevor King <wking@drexel.edu> | 2009-07-29 15:56:10 -0400 |
---|---|---|
committer | W. Trevor King <wking@drexel.edu> | 2009-07-29 15:56:10 -0400 |
commit | dcfe9d5e292fa4a405fafb4bdd6d9e2070f30fa9 (patch) | |
tree | f9e9c8d2ba5190d1a71046cdb5fb3abdc0fcf74f /libbe | |
parent | 4e8882e74aad64859a16f17fa6bef8c04b33913d (diff) | |
parent | 22a38de80ee11ada710bc6766798ca608f938307 (diff) | |
download | bugseverywhere-dcfe9d5e292fa4a405fafb4bdd6d9e2070f30fa9.tar.gz |
Merged interactive email interface
Diffstat (limited to 'libbe')
-rw-r--r-- | libbe/arch.py | 12 | ||||
-rw-r--r-- | libbe/bug.py | 78 | ||||
-rw-r--r-- | libbe/bugdir.py | 283 | ||||
-rw-r--r-- | libbe/bzr.py | 22 | ||||
-rw-r--r-- | libbe/cmdutil.py | 5 | ||||
-rw-r--r-- | libbe/comment.py | 278 | ||||
-rw-r--r-- | libbe/darcs.py | 43 | ||||
-rw-r--r-- | libbe/diff.py | 475 | ||||
-rw-r--r-- | libbe/encoding.py | 4 | ||||
-rw-r--r-- | libbe/git.py | 18 | ||||
-rw-r--r-- | libbe/hg.py | 19 | ||||
-rw-r--r-- | libbe/properties.py | 10 | ||||
-rw-r--r-- | libbe/rcs.py | 55 | ||||
-rw-r--r-- | libbe/settings_object.py | 11 |
14 files changed, 972 insertions, 341 deletions
diff --git a/libbe/arch.py b/libbe/arch.py index 2f45aa9..1bdc8ae 100644 --- a/libbe/arch.py +++ b/libbe/arch.py @@ -176,7 +176,6 @@ class Arch(RCS): 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') @@ -188,7 +187,6 @@ class Arch(RCS): 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", directory=root) @@ -281,6 +279,16 @@ class Arch(RCS): assert revpath.startswith(self._archive_project_name()+'--') revision = revpath[len(self._archive_project_name()+'--'):] return revpath + def _rcs_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): diff --git a/libbe/bug.py b/libbe/bug.py index c1e5481..7554318 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -33,6 +33,11 @@ import comment import utility +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) + ### Define and describe valid bug categories # Use a tuple of (category, description) tuples since we don't have # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/ @@ -245,10 +250,13 @@ class Bug(settings_object.SavedSettingsObject): def __repr__(self): return "Bug(uuid=%r)" % self.uuid - def set_sync_with_disk(self, value): - self.sync_with_disk = value - for comment in self.comments(): - comment.set_sync_with_disk(value) + def __str__(self): + return self.string(shortlist=True) + + def __cmp__(self, other): + return cmp_full(self, other) + + # serializing methods def _setting_attr_string(self, setting): value = getattr(self, setting) @@ -331,11 +339,7 @@ class Bug(settings_object.SavedSettingsObject): output = bugout return output - def __str__(self): - return self.string(shortlist=True) - - def __cmp__(self, other): - return cmp_full(self, other) + # methods for saving/loading/acessing settings and properties. def get_path(self, name=None): my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid) @@ -344,30 +348,25 @@ class Bug(settings_object.SavedSettingsObject): assert name in ["values", "comments"] return os.path.join(my_dir, name) + def set_sync_with_disk(self, value): + self.sync_with_disk = value + for comment in self.comments(): + comment.set_sync_with_disk(value) + def load_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("load settings") self.settings = mapfile.map_load(self.rcs, self.get_path("values")) self._setup_saved_settings() - def load_comments(self, load_full=True): - 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 - # tree to disk. - self.sync_with_disk = False - self.comment_root = None - self.sync_with_disk = True - def save_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("save settings") assert self.summary != None, "Can't save blank bug" - self.rcs.mkdir(self.get_path()) path = self.get_path("values") mapfile.map_save(self.rcs, path, self._get_saved_settings()) - + def save(self): """ Save any loaded contents to disk. Because of lazy loading of @@ -378,15 +377,39 @@ class Bug(settings_object.SavedSettingsObject): calling this method will just waste time (unless something else has been messing with your on-disk files). """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) 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 + # tree to disk. + self.sync_with_disk = False + self.comment_root = None + self.sync_with_disk = True def remove(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("remove") self.comment_root.remove() path = self.get_path() self.rcs.recursive_remove(path) + # methods for managing comments + def comments(self): for comment in self.comment_root.traverse(): yield comment @@ -489,8 +512,12 @@ def cmp_attr(bug_1, bug_2, attr, invert=False): return cmp(val_1, val_2) # alphabetical rankings (a < z) +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") # chronological rankings (newer < older) cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True) @@ -512,7 +539,8 @@ def cmp_comments(bug_1, bug_2): return 0 DEFAULT_CMP_FULL_CMP_LIST = \ - (cmp_status,cmp_severity,cmp_assigned,cmp_time,cmp_creator,cmp_comments) + (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator, + cmp_reporter, cmp_target, cmp_comments, cmp_summary, cmp_uuid) class BugCompoundComparator (object): def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): diff --git a/libbe/bugdir.py b/libbe/bugdir.py index f7345fd..1bb307c 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -17,11 +17,12 @@ # 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 copy +import errno import os import os.path -import errno +import sys import time -import copy import unittest import doctest @@ -68,6 +69,11 @@ class NoBugMatches(KeyError): KeyError.__init__(self, msg) self.shortname = shortname +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) + TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n" @@ -105,7 +111,8 @@ 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 memoryy, a call to save() is a safe move. + 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. @@ -113,9 +120,8 @@ class BugDir (list, settings_object.SavedSettingsObject): changes, this .save() call will be a waste of time. The BugDir will only load information from the file system when it - loads new bugs/comments that it doesn't already have in memory, or - when it explicitly asked to do so (e.g. .load() or - __init__(from_disk=True)). + loads new settings/bugs/comments that it doesn't already have in + memory and .sync_with_disk == True. Allow RCS initialization ======================== @@ -286,8 +292,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and def __init__(self, root=None, sink_to_existing_root=True, assert_new_BugDir=False, allow_rcs_init=False, - manipulate_encodings=True, - from_disk=False, rcs=None): + manipulate_encodings=True, from_disk=False, rcs=None): list.__init__(self) settings_object.SavedSettingsObject.__init__(self) self._manipulate_encodings = manipulate_encodings @@ -316,15 +321,13 @@ settings easy. Don't set this attribute. Set .rcs instead, and self.rcs = rcs self._setup_user_id(self.user_id) - def set_sync_with_disk(self, value): - self.sync_with_disk = value - for bug in self: - bug.set_sync_with_disk(value) + # 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. + return a BugDir rooted there. Only called by __init__, and + then only if sink_to_existing_root == True. """ if not os.path.exists(path): raise NoRootEntry(path) @@ -340,7 +343,77 @@ settings easy. Don't set this attribute. Set .rcs instead, and raise NoBugDir(path) return beroot + def _guess_rcs(self, allow_rcs_init=False): + """ + Only called by __init__. + """ + deepdir = self.get_path() + if not os.path.exists(deepdir): + deepdir = os.path.dirname(deepdir) + new_rcs = rcs.detect_rcs(deepdir) + install = False + if new_rcs.name == "None": + if allow_rcs_init == True: + new_rcs = rcs.installed_rcs() + new_rcs.init(self.root) + return new_rcs + + # methods for saving/loading/accessing settings and properties. + + def get_path(self, *args): + """ + Return a path relative to .root. + """ + my_dir = os.path.join(self.root, ".be") + if len(args) == 0: + return my_dir + assert args[0] in ["version", "settings", "bugs"], str(args) + return os.path.join(my_dir, *args) + + def _get_settings(self, settings_path, for_duplicate_bugdir=False): + allow_no_rcs = not self.rcs.path_in_root(settings_path) + if allow_no_rcs == 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.rcs, settings_path, allow_no_rcs) + except rcs.NoSuchFile: + settings = {"rcs_name": "None"} + return settings + + def _save_settings(self, settings_path, settings, + for_duplicate_bugdir=False): + allow_no_rcs = not self.rcs.path_in_root(settings_path) + if allow_no_rcs == 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.rcs.mkdir(self.get_path(), allow_no_rcs) + mapfile.map_save(self.rcs, settings_path, settings, allow_no_rcs) + + def load_settings(self): + self.settings = self._get_settings(self.get_path("settings")) + 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) + self.rcs = rcs.rcs_by_name(self.rcs_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_rcs=False): + """ + Requires disk access. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("get version") if use_none_rcs == True: RCS = rcs.rcs_by_name("None") RCS.root(self.root) @@ -354,30 +427,31 @@ settings easy. Don't set this attribute. Set .rcs instead, and return tree_version def set_version(self): + """ + Requires disk access. + """ + if self.sync_with_disk == False: + raise DiskAccessRequired("set version") self.rcs.mkdir(self.get_path()) self.rcs.set_file_contents(self.get_path("version"), TREE_VERSION_STRING) - def get_path(self, *args): - my_dir = os.path.join(self.root, ".be") - if len(args) == 0: - return my_dir - assert args[0] in ["version", "settings", "bugs"], str(args) - return os.path.join(my_dir, *args) + # methods controlling disk access - def _guess_rcs(self, allow_rcs_init=False): - deepdir = self.get_path() - if not os.path.exists(deepdir): - deepdir = os.path.dirname(deepdir) - new_rcs = rcs.detect_rcs(deepdir) - install = False - if new_rcs.name == "None": - if allow_rcs_init == True: - new_rcs = rcs.installed_rcs() - new_rcs.init(self.root) - return new_rcs + 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_rcs=True) if version != TREE_VERSION_STRING: raise NotImplementedError, \ @@ -387,59 +461,43 @@ settings easy. Don't set this attribute. Set .rcs instead, and raise NoBugDir(self.get_path()) self.load_settings() - self.rcs = rcs.rcs_by_name(self.rcs_name) - self._setup_user_id(self.user_id) - def load_all_bugs(self): - "Warning: this could take a while." + """ + 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.list_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 self.sync_with_disk = True, then any changes are + 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). + + Requires disk access. """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) self.set_version() self.save_settings() for bug in self: bug.save() + if sync_with_disk == False: + self.set_sync_with_disk(sync_with_disk) - def load_settings(self): - self.settings = self._get_settings(self.get_path("settings")) - 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) - - def _get_settings(self, settings_path): - allow_no_rcs = not self.rcs.path_in_root(settings_path) - # allow_no_rcs=True should only be for the special case of - # configuring duplicate bugdir settings - - try: - settings = mapfile.map_load(self.rcs, settings_path, allow_no_rcs) - except rcs.NoSuchFile: - settings = {"rcs_name": "None"} - return settings - - def save_settings(self): - settings = self._get_saved_settings() - self._save_settings(self.get_path("settings"), settings) - - def _save_settings(self, settings_path, settings): - allow_no_rcs = not self.rcs.path_in_root(settings_path) - # allow_no_rcs=True should only be for the special case of - # configuring duplicate bugdir settings - self.rcs.mkdir(self.get_path(), allow_no_rcs) - mapfile.map_save(self.rcs, settings_path, settings, allow_no_rcs) + # methods for managing duplicate BugDirs def duplicate_bugdir(self, revision): duplicate_path = self.rcs.duplicate_repo(revision) @@ -448,23 +506,27 @@ settings easy. Don't set this attribute. Set .rcs instead, and # initialized for versioning duplicate_settings_path = os.path.join(duplicate_path, ".be", "settings") - duplicate_settings = self._get_settings(duplicate_settings_path) + duplicate_settings = self._get_settings(duplicate_settings_path, + for_duplicate_bugdir=True) if "rcs_name" in duplicate_settings: duplicate_settings["rcs_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) + 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.rcs.remove_duplicate_repo() + # methods for managing bugs + def list_uuids(self): uuids = [] - if os.path.exists(self.get_path()): + if self.sync_with_disk == True and os.path.exists(self.get_path()): # list the uuids on disk for uuid in os.listdir(self.get_path("bugs")): if not (uuid.startswith('.')): @@ -482,6 +544,8 @@ settings easy. Don't set this attribute. Set .rcs 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) self.append(bg) self._bug_map_gen() @@ -498,7 +562,8 @@ settings easy. Don't set this attribute. Set .rcs instead, and def remove_bug(self, bug): self.remove(bug) - bug.remove() + if bug.sync_with_disk == True: + bug.remove() def bug_shortname(self, bug): """ @@ -520,7 +585,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and def bug_from_shortname(self, shortname): """ - >>> bd = simple_bug_dir() + >>> bd = simple_bug_dir(sync_with_disk=False) >>> bug_a = bd.bug_from_shortname('a') >>> print type(bug_a) <class 'libbe.bug.Bug'> @@ -554,20 +619,31 @@ settings easy. Don't set this attribute. Set .rcs instead, and return True -def simple_bug_dir(): +def simple_bug_dir(sync_with_disk=True): """ - For testing + For testing. Set sync_with_disk==False for a memory-only bugdir. >>> bugdir = simple_bug_dir() - >>> ls = list(bugdir.list_uuids()) - >>> ls.sort() - >>> print ls + >>> uuids = list(bugdir.list_uuids()) + >>> uuids.sort() + >>> print uuids ['a', 'b'] """ - dir = utility.Dir() - assert os.path.exists(dir.path) - bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True, + if sync_with_disk == True: + dir = utility.Dir() + assert os.path.exists(dir.path) + root = dir.path + assert_new_BugDir = True + rcs_init = True + else: + root = "/" + assert_new_BugDir = False + rcs_init = False + bugdir = BugDir(root, sink_to_existing_root=False, + assert_new_BugDir=assert_new_BugDir, + allow_rcs_init=rcs_init, manipulate_encodings=False) - bugdir._dir_ref = dir # postpone cleanup since dir.__del__() removes dir. + if sync_with_disk == True: # postpone cleanup since dir.__del__() removes dir. + bugdir._dir_ref = dir bug_a = bugdir.new_bug("a", summary="Bug A") bug_a.creator = "John Doe <jdoe@example.com>" bug_a.time = 0 @@ -575,13 +651,13 @@ def simple_bug_dir(): bug_b.creator = "Jane Doe <jdoe@example.com>" bug_b.time = 0 bug_b.status = "closed" - bugdir.save() + if sync_with_disk == True: + bugdir.save() + bugdir.set_sync_with_disk(True) return bugdir class BugDirTestCase(unittest.TestCase): - def __init__(self, *args, **kwargs): - unittest.TestCase.__init__(self, *args, **kwargs) def setUp(self): self.dir = utility.Dir() self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, @@ -651,9 +727,12 @@ class BugDirTestCase(unittest.TestCase): 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: @@ -661,7 +740,6 @@ class BugDirTestCase(unittest.TestCase): self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid) self.failUnless(comment.sync_with_disk == True, comment.sync_with_disk) - #load_settings() self.failUnless(comment.content_type == "text/plain", comment.content_type) self.failUnless(repLoaded.settings["Content-type"]=="text/plain", @@ -678,5 +756,42 @@ class BugDirTestCase(unittest.TestCase): def testSyncedComments(self): self.testComments(sync_with_disk=True) -unitsuite = unittest.TestLoader().loadTestsFromTestCase(BugDirTestCase) -suite = unittest.TestSuite([unitsuite])#, doctest.DocTestSuite()]) +class SimpleBugDirTestCase (unittest.TestCase): + def setUp(self): + # create a pre-existing bugdir in a temporary directory + self.dir = utility.Dir() + os.chdir(self.dir.path) + self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False, + allow_rcs_init=True) + self.bugdir.new_bug("preexisting", summary="Hopefully not imported") + self.bugdir.save() + def tearDown(self): + self.dir.cleanup() + def testOnDiskCleanLoad(self): + """simple_bug_dir(sync_with_disk==True) should not import preexisting bugs.""" + bugdir = simple_bug_dir(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) + def testInMemoryCleanLoad(self): + """simple_bug_dir(sync_with_disk==False) should not import preexisting bugs.""" + bugdir = simple_bug_dir(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) + + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/bzr.py b/libbe/bzr.py index d7cd1e5..7457815 100644 --- a/libbe/bzr.py +++ b/libbe/bzr.py @@ -93,6 +93,28 @@ class Bzr(RCS): assert len(match.groups()) == 1 revision = match.groups()[0] return revision + def _rcs_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) + def postcommit(self): + try: + self._u_invoke_client('merge') + except rcs.CommandError, e: + if ('No merge branch known or specified' in e.err_str or + 'No merge location known or specified' in e.err_str): + pass + else: + self._u_invoke_client('revert', '--no-backup', + directory=directory) + self._u_invoke_client('resolve', '--all', directory=directory) + raise + if len(self._u_invoke_client('status', directory=directory)[1]) > 0: + self.commit('Merge from upstream') rcs.make_rcs_testcase_subclasses(Bzr, sys.modules[__name__]) diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index 35d2f9e..94a6856 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -70,10 +70,11 @@ def get_command(command_name): return cmd -def execute(cmd, args): +def execute(cmd, args, manipulate_encodings=True): enc = encoding.get_encoding() cmd = get_command(cmd) - ret = cmd.execute([a.decode(enc) for a in args]) + ret = cmd.execute([a.decode(enc) for a in args], + manipulate_encodings=manipulate_encodings) if ret == None: ret = 0 return ret diff --git a/libbe/comment.py b/libbe/comment.py index 3249e8b..c5bec43 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -61,6 +61,11 @@ class MissingReference(ValueError): self.reference = comment.in_reply_to self.comment = comment +class DiskAccessRequired (Exception): + def __init__(self, goal): + msg = "Cannot %s without accessing the disk" % goal + Exception.__init__(self, msg) + INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!" def list_to_root(comments, bug, root=None, @@ -115,6 +120,8 @@ 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.isdir(path): return Comment(bug, uuid=INVALID_UUID) @@ -131,6 +138,8 @@ def loadComments(bug, load_full=False): return list_to_root(comments, bug) def saveComments(bug): + if bug.sync_with_disk == False: + raise DiskAccessRequired("save comments") for comment in bug.comment_root.traverse(): comment.save() @@ -162,9 +171,9 @@ 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 {} - @_versioned_property(name="From", + @_versioned_property(name="Author", doc="The author of the comment") - def From(): return {} + def author(): return {} @_versioned_property(name="In-reply-to", doc="UUID for parent comment or bug") @@ -178,17 +187,17 @@ class Comment(Tree, settings_object.SavedSettingsObject): @_versioned_property(name="Date", doc="An RFC 2822 timestamp for comment creation") - def time_string(): return {} + def date(): return {} def _get_time(self): - if self.time_string == None: + if self.date == None: return None - return utility.str_to_time(self.time_string) + return utility.str_to_time(self.date) def _set_time(self, value): - self.time_string = utility.time_to_str(value) + self.date = utility.time_to_str(value) time = property(fget=_get_time, fset=_set_time, - doc="An integer version of .time_string") + doc="An integer version of .date") def _get_comment_body(self): if self.rcs != None and self.sync_with_disk == True: @@ -258,12 +267,28 @@ class Comment(Tree, settings_object.SavedSettingsObject): self.uuid = uuid_gen() self.time = int(time.time()) # only save to second precision if self.rcs != None: - self.From = self.rcs.get_user_id() + self.author = self.rcs.get_user_id() self.in_reply_to = in_reply_to self.body = body - def set_sync_with_disk(self, value): - self.sync_with_disk = True + def __cmp__(self, other): + return cmp_full(self, other) + + def __str__(self): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> comm.uuid = "com-1" + >>> comm.date = "Thu, 20 Nov 2008 15:55:11 +0000" + >>> comm.author = "Jane Doe <jdoe@example.com>" + >>> print comm + --------- Comment --------- + Name: com-1 + From: Jane Doe <jdoe@example.com> + Date: Thu, 20 Nov 2008 15:55:11 +0000 + <BLANKLINE> + Some insightful remarks + """ + return self.string() def traverse(self, *args, **kwargs): """Avoid working with the possible dummy root comment""" @@ -272,6 +297,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): continue yield comment + # serializing methods + def _setting_attr_string(self, setting): value = getattr(self, setting) if value == None: @@ -282,12 +309,12 @@ class Comment(Tree, settings_object.SavedSettingsObject): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> comm.uuid = "0123" - >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> comm.date = "Thu, 01 Jan 1970 00:00:00 +0000" >>> print comm.xml(indent=2, shortname="com-1") <comment> <uuid>0123</uuid> <short-name>com-1</short-name> - <from></from> + <author></author> <date>Thu, 01 Jan 1970 00:00:00 +0000</date> <content-type>text/plain</content-type> <body>Some @@ -309,8 +336,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): ("alt-id", self.alt_id), ("short-name", shortname), ("in-reply-to", self.in_reply_to), - ("from", self._setting_attr_string("From")), - ("date", self.time_string), + ("author", self._setting_attr_string("author")), + ("date", self.date), ("content-type", self.content_type), ("body", body)] lines = ["<comment>"] @@ -328,11 +355,11 @@ class Comment(Tree, settings_object.SavedSettingsObject): <alt-id> fields. >>> commA = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> commA.uuid = "0123" - >>> commA.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> commA.date = "Thu, 01 Jan 1970 00:00:00 +0000" >>> xml = commA.xml(shortname="com-1") >>> commB = Comment() >>> commB.from_xml(xml) - >>> attrs=['uuid','alt_id','in_reply_to','From','time_string','content_type','body'] + >>> 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'" @@ -342,15 +369,15 @@ class Comment(Tree, settings_object.SavedSettingsObject): Mismatch on alt_id: '0123' should be 'None' >>> print commB.alt_id 0123 - >>> commA.From - >>> commB.From + >>> commA.author + >>> commB.author """ if type(xml_string) == types.UnicodeType: xml_string = xml_string.strip().encode("unicode_escape") comment = ElementTree.XML(xml_string) if comment.tag != "comment": raise InvalidXML(comment, "root element must be <comment>") - tags=['uuid','alt-id','in-reply-to','from','date','content-type','body'] + tags=['uuid','alt-id','in-reply-to','author','date','content-type','body'] uuid = None body = None for child in comment.getchildren(): @@ -368,10 +395,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): if child.tag == "body": body = text continue # don't set the bug's body yet. - elif child.tag == 'from': - attr_name = "From" - elif child.tag == 'date': - attr_name = 'time_string' else: attr_name = child.tag.replace('-','_') setattr(self, attr_name, text) @@ -389,7 +412,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): def string(self, indent=0, shortname=None): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") - >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> comm.date = "Thu, 01 Jan 1970 00:00:00 +0000" >>> print comm.string(indent=2, shortname="com-1") --------- Comment --------- Name: com-1 @@ -405,8 +428,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): lines = [] lines.append("--------- Comment ---------") lines.append("Name: %s" % shortname) - lines.append("From: %s" % (self._setting_attr_string("From"))) - lines.append("Date: %s" % self.time_string) + lines.append("From: %s" % (self._setting_attr_string("author"))) + lines.append("Date: %s" % self.date) lines.append("") if self.content_type.startswith("text/"): lines.extend((self.body or "").splitlines()) @@ -417,78 +440,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): sep = '\n' + istring return istring + sep.join(lines).rstrip('\n') - def __str__(self): - """ - >>> comm = Comment(bug=None, body="Some insightful remarks") - >>> comm.uuid = "com-1" - >>> comm.time_string = "Thu, 20 Nov 2008 15:55:11 +0000" - >>> comm.From = "Jane Doe <jdoe@example.com>" - >>> print comm - --------- Comment --------- - Name: com-1 - From: Jane Doe <jdoe@example.com> - Date: Thu, 20 Nov 2008 15:55:11 +0000 - <BLANKLINE> - Some insightful remarks - """ - return self.string() - - def get_path(self, name=None): - my_dir = os.path.join(self.bug.get_path("comments"), self.uuid) - if name is None: - return my_dir - assert name in ["values", "body"] - return os.path.join(my_dir, name) - - def load_settings(self): - self.settings = mapfile.map_load(self.rcs, self.get_path("values")) - self._setup_saved_settings() - - def save_settings(self): - self.rcs.mkdir(self.get_path()) - path = self.get_path("values") - mapfile.map_save(self.rcs, path, self._get_saved_settings()) - - def save(self): - """ - Save any loaded contents to disk. - - 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). - """ - assert self.body != None, "Can't save blank comment" - self.save_settings() - self._set_comment_body(new=self.body, force=True) - - def remove(self): - for comment in self.traverse(): - path = comment.get_path() - self.rcs.recursive_remove(path) - - def add_reply(self, reply, allow_time_inversion=False): - if self.uuid != INVALID_UUID: - reply.in_reply_to = self.uuid - self.append(reply) - #raise Exception, "adding reply \n%s\n%s" % (self, reply) - - def new_reply(self, body=None): - """ - >>> comm = Comment(bug=None, body="Some insightful remarks") - >>> repA = comm.new_reply("Critique original comment") - >>> repB = repA.new_reply("Begin flamewar :p") - >>> repB.in_reply_to == repA.uuid - True - """ - reply = Comment(self.bug, body=body) - if self.bug != None: - reply.set_sync_with_disk(self.bug.sync_with_disk) - if reply.sync_with_disk == True: - reply.save() - self.add_reply(reply) - return reply - def string_thread(self, string_method_name="string", name_map={}, indent=0, flatten=True, auto_name_map=False, bug_shortname=None): @@ -506,7 +457,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): name_map = {} for shortname,comment in comm.comment_shortnames(bug_shortname): name_map[comment.uuid] = shortname - comm.sort(key=lambda c : c.From) # your sort + comm.sort(key=lambda c : c.author) # your sort comm.string_thread(name_map=name_map) >>> a = Comment(bug=None, uuid="a", body="Insightful remarks") @@ -593,6 +544,80 @@ class Comment(Tree, settings_object.SavedSettingsObject): indent=indent, auto_name_map=auto_name_map, bug_shortname=bug_shortname) + # methods for saving/loading/acessing settings and properties. + + def get_path(self, name=None): + my_dir = os.path.join(self.bug.get_path("comments"), self.uuid) + if name is None: + return my_dir + assert name in ["values", "body"] + return os.path.join(my_dir, name) + + def set_sync_with_disk(self, value): + self.sync_with_disk = value + + def load_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("load settings") + self.settings = mapfile.map_load(self.rcs, self.get_path("values")) + # hack to deal with old BE comments: + if "From" in self.settings: + self.settings["Author"] = self.settings.pop("From") + self._setup_saved_settings() + + def save_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("save settings") + self.rcs.mkdir(self.get_path()) + path = self.get_path("values") + mapfile.map_save(self.rcs, path, self._get_saved_settings()) + + def save(self): + """ + Save any loaded contents to disk. + + 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). + """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) + assert self.body != None, "Can't save blank comment" + 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.rcs.recursive_remove(path) + + def add_reply(self, reply, allow_time_inversion=False): + if self.uuid != INVALID_UUID: + reply.in_reply_to = self.uuid + self.append(reply) + + def new_reply(self, body=None): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> repA = comm.new_reply("Critique original comment") + >>> repB = repA.new_reply("Begin flamewar :p") + >>> repB.in_reply_to == repA.uuid + True + """ + reply = Comment(self.bug, body=body) + if self.bug != None: + reply.set_sync_with_disk(self.bug.sync_with_disk) + if reply.sync_with_disk == True: + reply.save() + self.add_reply(reply) + return reply + def comment_shortnames(self, bug_shortname=None): """ Iterate through (id, comment) pairs, in time order. @@ -659,4 +684,59 @@ class Comment(Tree, settings_object.SavedSettingsObject): return comment raise KeyError(uuid) +def cmp_attr(comment_1, comment_2, attr, invert=False): + """ + Compare a general attribute between two comments using the conventional + comparison rule for that attribute type. If invert == True, sort + *against* that convention. + >>> attr="author" + >>> commentA = Comment() + >>> commentB = Comment() + >>> commentA.author = "John Doe" + >>> commentB.author = "Jane Doe" + >>> cmp_attr(commentA, commentB, attr) < 0 + True + >>> cmp_attr(commentA, commentB, attr, invert=True) > 0 + True + >>> commentB.author = "John Doe" + >>> cmp_attr(commentA, commentB, attr) == 0 + True + """ + if not hasattr(comment_2, attr) : + return 1 + val_1 = getattr(comment_1, attr) + 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 : + return cmp(val_1, val_2) + +# alphabetical rankings (a < z) +cmp_uuid = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "uuid") +cmp_author = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "author") +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") +# 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) + +class CommentCompoundComparator (object): + def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): + self.cmp_list = cmp_list + def __call__(self, comment_1, comment_2): + for comparison in self.cmp_list : + val = comparison(comment_1, comment_2) + if val != 0 : + return val + return 0 + +cmp_full = CommentCompoundComparator() + suite = doctest.DocTestSuite() diff --git a/libbe/darcs.py b/libbe/darcs.py index e7132c0..0720ed9 100644 --- a/libbe/darcs.py +++ b/libbe/darcs.py @@ -18,8 +18,13 @@ import codecs import os import re import sys -import unittest +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 doctest +import unittest import rcs from rcs import RCS @@ -138,24 +143,36 @@ class Darcs(RCS): args = ['record', '--all', '--author', id, '--logfile', commitfile] status,output,error = self._u_invoke_client(*args) empty_strings = ["No changes!"] - revision = None if self._u_any_in_string(empty_strings, output) == True: if allow_empty == False: raise rcs.EmptyCommit() - else: # we need a extra call to get the current revision - args = ["changes", "--last=1", "--xml"] - status,output,error = self._u_invoke_client(*args) - revline = re.compile("[ \t]*<name>(.*)</name>") - # note that darcs does _not_ make an empty revision. - # this returns the last non-empty revision id... + # note that darcs does _not_ make an empty revision. + # this returns the last non-empty revision id... + revision = self._rcs_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] + match = revline.search(output) + assert match != None, output+error + assert len(match.groups()) == 1 + revision = match.groups()[0] return revision - + def _rcs_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 rcs.make_rcs_testcase_subclasses(Darcs, sys.modules[__name__]) diff --git a/libbe/diff.py b/libbe/diff.py index ba48efc..69ccff8 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -15,111 +15,398 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Compare two bug trees""" -from libbe import cmdutil, bugdir, bug +from libbe import bugdir, bug, settings_object, tree from libbe.utility import time_to_str +import difflib import doctest -def bug_diffs(old_bugdir, new_bugdir): - added = [] - removed = [] - modified = [] - for uuid in old_bugdir.list_uuids(): - old_bug = old_bugdir.bug_from_uuid(uuid) - try: - new_bug = new_bugdir.bug_from_uuid(uuid) - old_bug.load_comments() - new_bug.load_comments() - if old_bug != new_bug: - modified.append((old_bug, new_bug)) - except KeyError: - removed.append(old_bug) - for uuid in new_bugdir.list_uuids(): - if not old_bugdir.has_bug(uuid): - new_bug = new_bugdir.bug_from_uuid(uuid) - added.append(new_bug) - return (removed, modified, added) +class DiffTree (tree.Tree): + """ + A tree holding difference data for easy report generation. + >>> all = DiffTree("all") + >>> bugdir = DiffTree("bugdir", data="target: None -> 1.0") + >>> all.append(bugdir) + >>> bugs = DiffTree("bugs", "bug-count: 5 -> 6") + >>> all.append(bugs) + >>> new = DiffTree("new", "new bugs: ABC, DEF") + >>> bugs.append(new) + >>> rem = DiffTree("rem", "removed bugs: RST, UVW") + >>> bugs.append(rem) + >>> print all.report_string() + target: None -> 1.0 + bug-count: 5 -> 6 + new bugs: ABC, DEF + removed bugs: RST, UVW + >>> print "\\n".join(all.paths()) + all + all/bugdir + all/bugs + all/bugs/new + all/bugs/rem + >>> all.child_by_path("/") == all + True + >>> all.child_by_path("/bugs") == bugs + True + >>> all.child_by_path("/bugs/rem") == rem + True + >>> all.child_by_path("all") == all + True + >>> all.child_by_path("all/") == all + True + >>> all.child_by_path("all/bugs") == bugs + True + >>> all.child_by_path("/bugs").masked = True + >>> print all.report_string() + target: None -> 1.0 + """ + def __init__(self, name, data=None, data_part_fn=str, + requires_children=False, masked=False): + tree.Tree.__init__(self) + self.name = name + self.data = data + self.data_part_fn = data_part_fn + self.requires_children = requires_children + self.masked = masked + def paths(self, parent_path=None): + paths = [] + if parent_path == None: + path = self.name + else: + 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] == "": + names[0] = self.name # replace root with self + if len(names) > 1 and names[-1] == "": + names = names[:-1] # strip empty tail + else: # it was already an array + names = path + assert len(names) > 0, path + if names[0] == self.name: + if len(names) == 1: + return self + for child in self: + if names[1] == child.name: + 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]) + def report_string(self): + return "\n".join(self.report()) + def report(self, root=None, parent=None, depth=0): + if root == None: + root = self.make_root() + if self.masked == True: + return None + data_part = self.data_part(depth) + if self.requires_children == True and len(self) == 0: + pass + else: + self.join(root, parent, data_part) + depth += 1 + for child in self: + child.report(root, self, depth) + return root + def make_root(self): + return [] + def join(self, root, parent, data_part): + if data_part != None: + root.append(data_part) + def data_part(self, depth, indent=True): + if self.data == None: + return None + 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 + data_part = indent+line_sep.join(data_part_lines) + self._cached_data_part = data_part + return data_part -def diff_report(bug_diffs_data, old_bugdir, new_bugdir): - bugs_removed,bugs_modified,bugs_added = bug_diffs_data - def modified_cmp(left, right): - return bug.cmp_severity(left[1], right[1]) +class Diff (object): + """ + Difference tree generator for BugDirs. + >>> import copy + >>> bd = bugdir.simple_bug_dir(sync_with_disk=False) + >>> bd.user_id = "John Doe <j@doe.com>" + >>> bd_new = copy.deepcopy(bd) + >>> 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") + >>> bd_new.remove_bug(b) + >>> c = bd_new.new_bug("c", "Bug C") + >>> d = Diff(bd, bd_new) + >>> r = d.report_tree() + >>> print "\\n".join(r.paths()) + bugdir + bugdir/settings + bugdir/bugs + bugdir/bugs/new + bugdir/bugs/new/c + bugdir/bugs/rem + bugdir/bugs/rem/b + bugdir/bugs/mod + bugdir/bugs/mod/a + bugdir/bugs/mod/a/settings + bugdir/bugs/mod/a/comments + bugdir/bugs/mod/a/comments/new + bugdir/bugs/mod/a/comments/new/acom + bugdir/bugs/mod/a/comments/rem + bugdir/bugs/mod/a/comments/mod + >>> print r.report_string() + Changed bug directory settings: + target: None -> 1.0 + New bugs: + c:om: Bug C + Removed bugs: + b:cm: Bug B + Modified bugs: + a:cm: Bug A + Changed bug settings: + status: open -> closed + New comments: + from John Doe <j@doe.com> on Thu, 01 Jan 1970 00:00:00 +0000 + """ + def __init__(self, old_bugdir, new_bugdir): + self.old_bugdir = old_bugdir + self.new_bugdir = new_bugdir - bugs_added.sort(bug.cmp_severity) - bugs_removed.sort(bug.cmp_severity) - bugs_modified.sort(modified_cmp) - lines = [] - - if old_bugdir.settings != new_bugdir.settings: - bugdir_settings = sorted(new_bugdir.settings_properties) - bugdir_settings.remove("rcs_name") # tweaked by bugdir.duplicate_bugdir - change_list = change_lines(old_bugdir, new_bugdir, bugdir_settings) - if len(change_list) > 0: - lines.append("Modified bug directory:") - change_strings = ["%s: %s -> %s" % f for f in change_list] - lines.extend(change_strings) - lines.append("") - if len(bugs_added) > 0: - lines.append("New bug reports:") - for bg in bugs_added: - lines.extend(bg.string(shortlist=True).splitlines()) - lines.append("") - if len(bugs_modified) > 0: - printed = False - for old_bug, new_bug in bugs_modified: - change_str = bug_changes(old_bug, new_bug) - if change_str is None: - continue - if not printed: - printed = True - lines.append("Modified bug reports:") - lines.extend(change_str.splitlines()) - if printed == True: - lines.append("") - if len(bugs_removed) > 0: - lines.append("Removed bug reports:") - for bg in bugs_removed: - lines.extend(bg.string(shortlist=True).splitlines()) - lines.append("") - - return "\n".join(lines).rstrip("\n") + # data assembly methods -def change_lines(old, new, attributes): - change_list = [] - for attr in attributes: - old_attr = getattr(old, attr) - new_attr = getattr(new, attr) - if old_attr != new_attr: - change_list.append((attr, old_attr, new_attr)) - if len(change_list) >= 0: - return change_list - else: + def _changed_bugs(self): + """ + Search for differences in all bugs between .old_bugdir and + .new_bugdir. Returns + (added_bugs, modified_bugs, removed_bugs) + where added_bugs and removed_bugs are lists of added and + removed bugs respectively. modified_bugs is a list of + (old_bug,new_bug) pairs. + """ + if hasattr(self, "__changed_bugs"): + return self.__changed_bugs + added = [] + removed = [] + modified = [] + for uuid in self.new_bugdir.list_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: + 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.list_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 + def _bug_modified_cmp(self, left, right): + return cmp(left[1], right[1]) + def _changed_comments(self, old, new): + """ + Search for differences in all loaded comments between the bugs + old and new. Returns + (added_comments, modified_comments, removed_comments) + analogous to ._changed_bugs. + """ + if hasattr(self, "__changed_comments"): + if new.uuid in self.__changed_comments: + return self.__changed_comments[new.uuid] + else: + self.__changed_comments = {} + added = [] + removed = [] + modified = [] + old.comment_root.sort(key=lambda comm : comm.time) + new.comment_root.sort(key=lambda comm : comm.time) + old_comment_ids = [c.uuid for c in old.comments()] + new_comment_ids = [c.uuid for c in new.comments()] + for uuid in new_comment_ids: + new_comment = new.comment_from_uuid(uuid) + try: + old_comment = old.comment_from_uuid(uuid) + except KeyError: + added.append(new_comment) + else: + if old_comment != new_comment: + 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) + self.__changed_comments[new.uuid] = (added, modified, removed) + return self.__changed_comments[new.uuid] + def _attribute_changes(self, old, new, attributes): + """ + Take two objects old and new, and compare the value of *.attr + for attr in the list attribute names. Returns a list of + (attr_name, old_value, new_value) + tuples. + """ + change_list = [] + for attr in attributes: + old_value = getattr(old, attr) + new_value = getattr(new, attr) + if old_value != new_value: + change_list.append((attr, old_value, new_value)) + if len(change_list) >= 0: + return change_list return None + def _settings_properties_attribute_changes(self, old, new, + hidden_properties=[]): + properties = sorted(new.settings_properties) + for p in hidden_properties: + properties.remove(p) + attributes = [settings_object.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, + ["rcs_name"]) # tweaked by bugdir.duplicate_bugdir + def _bug_attribute_changes(self, old, new): + return self._settings_properties_attribute_changes(old, new) + def _comment_attribute_changes(self, old, new): + return self._settings_properties_attribute_changes(old, new) -def bug_changes(old, new): - bug_settings = sorted(new.settings_properties) - change_list = change_lines(old, new, bug_settings) - change_strings = ["%s: %s -> %s" % f for f in change_list] + # report generation methods - old_comment_ids = [c.uuid for c in old.comments()] - new_comment_ids = [c.uuid for c in new.comments()] - for comment_id in new_comment_ids: - if comment_id not in old_comment_ids: - summary = comment_summary(new.comment_from_uuid(comment_id), "new") - change_strings.append(summary) - for comment_id in old_comment_ids: - if comment_id not in new_comment_ids: - summary = comment_summary(new.comment_from_uuid(comment_id), - "removed") - change_strings.append(summary) + def report_tree(self, 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 + bugdir_settings = sorted(self.new_bugdir.settings_properties) + bugdir_settings.remove("rcs_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) + bug_root = diff_tree("bugs") + root.append(bug_root) + add,mod,rem = self._changed_bugs() + 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) + 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) + 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, + 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) + b.append(bsum) + cr = diff_tree("comments") + b.append(cr) + a,m,d = self._changed_comments(old, new) + 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) + 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) + 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, + self.comment_attribute_change_string) + if o.body != n.body: + data = (o.body, n.body) + cbody = diff_tree("cbody", data, + self.comment_body_change_string) + c.append(cbody) + cr.extend([cnew, crem, cmod]) + self.__report_tree = root + return self.__report_tree - if len(change_strings) == 0: - return None - return "%s\n %s" % (new.string(shortlist=True), - " \n".join(change_strings)) + # change data -> string methods. + # 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] + for i,change_string in enumerate(change_strings): + change_strings[i] = indent_string+change_string + return u"\n".join(change_strings) + def bugdir_attribute_change_string(self, attribute_changes): + 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" % \ + self.attribute_change_string(attribute_changes, indent=1) + def comment_attribute_change_string(self, attribute_changes): + 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) + def bug_rem_string(self, bug): + return bug.string(shortlist=True) + def bug_mod_string(self, bugs): + old_bug,new_bug = bugs + 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) + def _comment_summary_string(self, comment): + 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) + 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) + def comment_mod_string(self, comments): + old_comment,new_comment = comments + return self._comment_summary_string(new_comment) + def comment_body_change_string(self, bodies): + old_body,new_body = bodies + return difflib.unified_diff(old_body, new_body) -def comment_summary(comment, status): - return "%8s comment from %s on %s" % (status, comment.From, - time_to_str(comment.time)) suite = doctest.DocTestSuite() diff --git a/libbe/encoding.py b/libbe/encoding.py index d603602..4af864e 100644 --- a/libbe/encoding.py +++ b/libbe/encoding.py @@ -19,11 +19,15 @@ import locale import sys 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 diff --git a/libbe/git.py b/libbe/git.py index 2f9ffa9..f7e9646 100644 --- a/libbe/git.py +++ b/libbe/git.py @@ -111,7 +111,23 @@ class Git(RCS): assert match != None, output+error assert len(match.groups()) == 3 revision = match.groups()[1] - return revision + full_revision = self._rcs_revision_id(-1) + assert full_revision.startswith(revision), \ + "Mismatched revisions:\n%s\n%s" % (revision, full_revision) + return full_revision + def _rcs_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 rcs.CommandError(args, status, error) + commits = output.splitlines() + try: + return commits[index] + except IndexError: + return None rcs.make_rcs_testcase_subclasses(Git, sys.modules[__name__]) diff --git a/libbe/hg.py b/libbe/hg.py index a20eeb5..31df1d0 100644 --- a/libbe/hg.py +++ b/libbe/hg.py @@ -80,14 +80,17 @@ class Hg(RCS): strings = ["nothing changed"] if self._u_any_in_string(strings, output) == True: raise rcs.EmptyCommit() - status,output,error = self._u_invoke_client('identify') - revision = None - revline = re.compile("(.*) tip") - match = revline.search(output) - assert match != None, output+error - assert len(match.groups()) == 1 - revision = match.groups()[0] - return revision + return self._rcs_revision_id(-1) + def _rcs_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 rcs.make_rcs_testcase_subclasses(Hg, sys.modules[__name__]) diff --git a/libbe/properties.py b/libbe/properties.py index 144220b..09dd20e 100644 --- a/libbe/properties.py +++ b/libbe/properties.py @@ -160,10 +160,10 @@ def _get_cached_mutable_property(self, cacher_name, property_name, default=None) 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): +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: - return 1 # any value > non-existant old 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) @@ -327,7 +327,7 @@ def primed_property(primer, initVal=None): return funcs return decorator -def change_hook_property(hook, mutable=False): +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 @@ -359,9 +359,9 @@ def change_hook_property(hook, mutable=False): 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) != 0: + 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) + 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 diff --git a/libbe/rcs.py b/libbe/rcs.py index 294b8e0..0206bf6 100644 --- a/libbe/rcs.py +++ b/libbe/rcs.py @@ -214,6 +214,16 @@ class RCS(object): changes to commit. """ return None + def _rcs_revision_id(self, index): + """ + Return the name of the <index>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 installed(self): try: self._rcs_help() @@ -415,6 +425,18 @@ class RCS(object): Only executed after successful commits. """ pass + def revision_id(self, index=None): + """ + Return the name of the <index>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._rcs_revision_id(index) def _u_any_in_string(self, list, string): """ Return True if any of the strings in list are in string. @@ -822,6 +844,39 @@ class RCS_commit_TestCase(RCSTestCase): 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.rcs.versioned: + self.failUnlessEqual(self.rcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + full_path = self.full_path(path) + self.rcs.set_file_contents( + full_path, self.test_contents['rev_1']) + revision = self.rcs.commit("Initial %s contents." % path) + committed_revisions.append(revision) + self.rcs.set_file_contents( + full_path, self.test_contents['uncommitted']) + revision = self.rcs.commit("Altered %s contents." % path) + committed_revisions.append(revision) + for i,revision in enumerate(committed_revisions): + self.failUnlessEqual(self.rcs.revision_id(i), revision) + i += -len(committed_revisions) # check negative indices + self.failUnlessEqual(self.rcs.revision_id(i), revision) + i = len(committed_revisions) + self.failUnlessEqual(self.rcs.revision_id(i), None) + self.failUnlessEqual(self.rcs.revision_id(-i-1), None) + + def test_revision_id_as_committed(self): + """Check revision id before first commit""" + if not self.rcs.versioned: + self.failUnlessEqual(self.rcs.revision_id(5), None) + return + committed_revisions = [] + for path in self.test_files: + self.failUnlessEqual(self.rcs.revision_id(0), None) + class RCS_duplicate_repo_TestCase(RCSTestCase): """Test cases for RCS.duplicate_repo method.""" diff --git a/libbe/settings_object.py b/libbe/settings_object.py index dde247f..ceea9d5 100644 --- a/libbe/settings_object.py +++ b/libbe/settings_object.py @@ -148,7 +148,8 @@ def versioned_property(name, doc, 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) + 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) @@ -385,30 +386,24 @@ class SavedSettingsObjectTests(unittest.TestCase): 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 == [ - "'None' -> '<class 'libbe.settings_object.EMPTY'>'" - ], SAVES) + 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 == [ - "'None' -> '<class 'libbe.settings_object.EMPTY'>'", "'<class 'libbe.settings_object.EMPTY'>' -> '[]'" ], SAVES) t.list_type.append(5) self.failUnless(SAVES == [ - "'None' -> '<class 'libbe.settings_object.EMPTY'>'", "'<class 'libbe.settings_object.EMPTY'>' -> '[]'", ], SAVES) self.failUnless(t.settings["List-type"] == [5],t.settings["List-type"]) self.failUnless(SAVES == [ # the append(5) has not yet been saved - "'None' -> '<class 'libbe.settings_object.EMPTY'>'", "'<class 'libbe.settings_object.EMPTY'>' -> '[]'", ], SAVES) self.failUnless(t.list_type == [5], t.list_type) # <-get triggers saved self.failUnless(SAVES == [ # now the append(5) has been saved. - "'None' -> '<class 'libbe.settings_object.EMPTY'>'", "'<class 'libbe.settings_object.EMPTY'>' -> '[]'", "'[]' -> '[5]'" ], SAVES) |