diff options
Diffstat (limited to 'libbe/storage')
-rw-r--r-- | libbe/storage/base.py | 224 | ||||
-rw-r--r-- | libbe/storage/http.py | 16 | ||||
-rw-r--r-- | libbe/storage/vcs/arch.py | 27 | ||||
-rw-r--r-- | libbe/storage/vcs/base.py | 91 | ||||
-rw-r--r-- | libbe/storage/vcs/bzr.py | 91 | ||||
-rw-r--r-- | libbe/storage/vcs/darcs.py | 127 | ||||
-rw-r--r-- | libbe/storage/vcs/git.py | 81 | ||||
-rw-r--r-- | libbe/storage/vcs/hg.py | 91 |
8 files changed, 636 insertions, 112 deletions
diff --git a/libbe/storage/base.py b/libbe/storage/base.py index aa32ea9..202305b 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -53,11 +53,15 @@ class InvalidStorageVersion(ConnectionError): class InvalidID (KeyError): def __init__(self, id=None, revision=None, msg=None): - if msg == None and id != None: - msg = id - KeyError.__init__(self, msg) + KeyError.__init__(self, id) + self.msg = msg self.id = id self.revision = revision + def __str__(self): + if self.msg == None: + return '%s in revision %s' % (self.id, self.revision) + return self.msg + class InvalidRevision (KeyError): pass @@ -274,6 +278,26 @@ class Storage (object): for entry in reversed(list(self._data[id].traverse())): self._remove(entry.id) + def ancestors(self, *args, **kwargs): + """Return a list of the specified entry's ancestors' ids.""" + if self.is_readable() == False: + raise NotReadable('Cannot list parents with unreadable storage.') + return self._ancestors(*args, **kwargs) + + def _ancestors(self, id=None, revision=None): + if id == None: + return [] + ancestors = [] + stack = [id] + while len(stack) > 0: + id = stack.pop(0) + parent = self._data[id].parent + if parent != None and not parent.id.startswith('__'): + ancestor = parent.id + ancestors.append(ancestor) + stack.append(ancestor) + return ancestors + def children(self, *args, **kwargs): """Return a list of specified entry's children's ids.""" if self.is_readable() == False: @@ -389,17 +413,39 @@ class VersionedStorage (Storage): for entry in reversed(list(self._data[-1][id].traverse())): self._remove(entry.id) + def _ancestors(self, id=None, revision=None): + if id == None: + return [] + if revision == None: + revision = -1 + else: + revision = int(revision) + ancestors = [] + stack = [id] + while len(stack) > 0: + id = stack.pop(0) + parent = self._data[revision][id].parent + if parent != None and not parent.id.startswith('__'): + ancestor = parent.id + ancestors.append(ancestor) + stack.append(ancestor) + return ancestors + def _children(self, id=None, revision=None): if id == None: id = '__ROOT__' if revision == None: revision = -1 + else: + revision = int(revision) return [c.id for c in self._data[revision][id] if not c.id.startswith('__')] def _get(self, id, default=InvalidObject, revision=None): if revision == None: revision = -1 + else: + revision = int(revision) if id in self._data[revision]: return self._data[revision][id].value elif default == InvalidObject: @@ -428,7 +474,7 @@ class VersionedStorage (Storage): raise EmptyCommit self._data[-1]["__COMMIT__SUMMARY__"].value = summary self._data[-1]["__COMMIT__BODY__"].value = body - rev = len(self._data)-1 + rev = str(len(self._data)-1) self._data.append(copy.deepcopy(self._data[-1])) return rev @@ -452,12 +498,34 @@ class VersionedStorage (Storage): raise InvalidRevision(index) L = len(self._data) - 1 # -1 b/c of initial commit if index >= -L and index <= L: - return index % L + return str(index % L) raise InvalidRevision(i) + def changed(self, revision): + """ + Return a tuple of lists of ids + (new, modified, removed) + from the specified revision to the current situation. + """ + new = [] + modified = [] + removed = [] + for id,value in self._data[int(revision)].items(): + if id.startswith('__'): + continue + if not id in self._data[-1]: + removed.append(id) + elif value.value != self._data[-1][id].value: + modified.append(id) + for id in self._data[-1]: + if not id in self._data[int(revision)]: + new.append(id) + return (new, modified, removed) + + if TESTING == True: class StorageTestCase (unittest.TestCase): - """Test cases for base Storage class.""" + """Test cases for Storage class.""" Class = Storage @@ -465,6 +533,23 @@ if TESTING == True: super(StorageTestCase, self).__init__(*args, **kwargs) self.dirname = None + # this class will be the basis of tests for several classes, + # so make sure we print the name of the class we're dealing with. + def fail(self, msg=None): + """Fail immediately, with the given message.""" + raise self.failureException, \ + '(%s) %s' % (self.Class.__name__, msg) + + def failIf(self, expr, msg=None): + "Fail the test if the expression is true." + if expr: raise self.failureException, \ + '(%s) %s' % (self.Class.__name__, msg) + + def failUnless(self, expr, msg=None): + """Fail the test unless the expression is true.""" + if not expr: raise self.failureException, \ + '(%s) %s' % (self.Class.__name__, msg) + def setUp(self): """Set up test fixtures for Storage test case.""" super(StorageTestCase, self).setUp() @@ -514,8 +599,7 @@ if TESTING == True: self.failUnless(len(self.s.children()) == 0, self.s.children()) def test_add_identical_rooted(self): - """ - Adding entries with the same ID should not increase the number of children. + """Adding entries with the same ID should not increase the number of children. """ for i in range(10): self.s.add('some id', directory=False) @@ -523,8 +607,7 @@ if TESTING == True: self.failUnless(s == ['some id'], s) def test_add_rooted(self): - """ - Adding entries should increase the number of children (rooted). + """Adding entries should increase the number of children (rooted). """ ids = [] for i in range(10): @@ -534,8 +617,7 @@ if TESTING == True: 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). + """Adding entries should increase the number of children (nonrooted). """ self.s.add('parent', directory=True) ids = [] @@ -547,9 +629,23 @@ if TESTING == True: s = self.s.children() self.failUnless(s == ['parent'], s) - def test_children(self): + def test_ancestors(self): + """Check ancestors lists. """ - Non-UUID ids should be returned as such. + self.s.add('parent', directory=True) + for i in range(10): + i_id = str(i) + self.s.add(i_id, 'parent', directory=True) + for j in range(10): # add some grandkids + j_id = str(20*(i+1)+j) + self.s.add(j_id, i_id, directory=(i%2 == 0)) + ancestors = sorted(self.s.ancestors(j_id)) + self.failUnless(ancestors == [i_id, 'parent'], + 'Unexpected ancestors for %s/%s, "%s"' + % (i_id, j_id, ancestors)) + + def test_children(self): + """Non-UUID ids should be returned as such. """ self.s.add('parent', directory=True) ids = [] @@ -560,8 +656,7 @@ if TESTING == True: self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) def test_add_invalid_directory(self): - """ - Should not be able to add children to non-directories. + """Should not be able to add children to non-directories. """ self.s.add('parent', directory=False) try: @@ -582,8 +677,7 @@ if TESTING == True: self.s.children('parent')) def test_remove_rooted(self): - """ - Removing entries should decrease the number of children (rooted). + """Removing entries should decrease the number of children (rooted). """ ids = [] for i in range(10): @@ -595,8 +689,7 @@ if TESTING == True: 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). + """Removing entries should decrease the number of children (nonrooted). """ self.s.add('parent', directory=True) ids = [] @@ -612,8 +705,7 @@ if TESTING == True: self.failUnless(s == ['parent'], s) def test_remove_directory_not_empty(self): - """ - Removing a non-empty directory entry should raise exception. + """Removing a non-empty directory entry should raise exception. """ self.s.add('parent', directory=True) ids = [] @@ -630,9 +722,7 @@ if TESTING == True: pass def test_recursive_remove(self): - """ - Recursive remove should empty the tree. - """ + """Recursive remove should empty the tree.""" self.s.add('parent', directory=True) ids = [] for i in range(10): @@ -651,8 +741,7 @@ if TESTING == True: val = 'unlikely value' def test_get_default(self): - """ - Get should return specified default if id not in Storage. + """Get should return specified default if id not in Storage. """ ret = self.s.get(self.id, default=self.val) self.failUnless(ret == self.val, @@ -660,8 +749,7 @@ if TESTING == True: % (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. + """Get should raise exception if id not in Storage and no default. """ try: ret = self.s.get(self.id) @@ -672,8 +760,7 @@ if TESTING == True: pass def test_get_initial_value(self): - """ - Data value should be None before any value has been set. + """Data value should be None before any value has been set. """ self.s.add(self.id, directory=False) ret = self.s.get(self.id) @@ -682,8 +769,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret)) def test_set_exception(self): - """ - Set should raise exception if id not in Storage. + """Set should raise exception if id not in Storage. """ try: self.s.set(self.id, self.val) @@ -694,8 +780,7 @@ if TESTING == True: pass def test_set(self): - """ - Set should define the value returned by get. + """Set should define the value returned by get. """ self.s.add(self.id, directory=False) self.s.set(self.id, self.val) @@ -705,8 +790,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret, self.val)) def test_unicode_set(self): - """ - Set should define the value returned by get. + """Set should define the value returned by get. """ val = u'Fran\xe7ois' self.s.add(self.id, directory=False) @@ -735,8 +819,7 @@ if TESTING == True: val = 'unlikely value' def test_get_set_persistence(self): - """ - Set should define the value returned by get after reconnect. + """Set should define the value returned by get after reconnect. """ self.s.add(self.id, directory=False) self.s.set(self.id, self.val) @@ -748,8 +831,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret, self.val)) def test_add_nonrooted_persistence(self): - """ - Adding entries should increase the number of children after reconnect. + """Adding entries should increase the number of children after reconnect. """ self.s.add('parent', directory=True) ids = [] @@ -764,12 +846,12 @@ if TESTING == True: self.failUnless(s == ['parent'], s) class VersionedStorageTestCase (StorageTestCase): - """Test cases for base VersionedStorage class.""" + """Test cases for VersionedStorage methods.""" Class = VersionedStorage class VersionedStorage_commit_TestCase (VersionedStorageTestCase): - """Test cases for VersionedStorage methods.""" + """Test cases for VersionedStorage.commit and revision_ids methods.""" id = 'unlikely id' val = 'Some value' @@ -788,8 +870,7 @@ if TESTING == True: pass def test_revision_id_exception(self): - """ - Invalid revision id should raise InvalidRevision. + """Invalid revision id should raise InvalidRevision. """ try: rev = self.s.revision_id('highly unlikely revision id') @@ -800,8 +881,7 @@ if TESTING == True: pass def test_empty_commit_raises_exception(self): - """ - Empty commit should raise exception. + """Empty commit should raise exception. """ self._setup_for_empty_commit() try: @@ -813,16 +893,14 @@ if TESTING == True: pass def test_empty_commit_allowed(self): - """ - Empty commit should _not_ raise exception if allow_empty=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) def test_commit_revision_ids(self): - """ - Commit / revision_id should agree on revision ids. + """Commit / revision_id should agree on revision ids. """ def val(i): return '%s:%d' % (self.val, i+1) @@ -844,8 +922,7 @@ if TESTING == True: % (vars(self.Class)['name'], i, rev, revs[i])) def test_get_previous_version(self): - """ - Get should be able to return the previous version. + """Get should be able to return the previous version. """ def val(i): return '%s:%d' % (self.val, i+1) @@ -862,8 +939,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret, val(i), revs[i])) def test_get_previous_children(self): - """ - Children list should be revision dependent. + """Children list should be revision dependent. """ self.s.add('parent', directory=True) revs = [] @@ -871,7 +947,7 @@ if TESTING == True: children = [] for i in range(10): new_child = str(i) - self.s.add(new_child, 'parent', directory=(i % 2 == 0)) + self.s.add(new_child, 'parent') self.s.set(new_child, self.val) revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), self.commit_body)) @@ -884,6 +960,35 @@ if TESTING == True: % (vars(self.Class)['name'], ret, children[i], revs[i])) + class VersionedStorage_changed_TestCase (VersionedStorageTestCase): + """Test cases for VersionedStorage.changed() method.""" + + def test_changed(self): + """Changed lists should reflect past activity""" + self.s.add('dir', directory=True) + self.s.add('modified', parent='dir') + self.s.set('modified', 'some value to be modified') + self.s.add('moved', parent='dir') + self.s.set('moved', 'this entry will be moved') + self.s.add('removed', parent='dir') + self.s.set('removed', 'this entry will be deleted') + revA = self.s.commit('Initial state') + self.s.add('new', parent='dir') + self.s.set('new', 'this entry is new') + self.s.set('modified', 'a new value') + self.s.remove('moved') + self.s.add('moved2', parent='dir') + self.s.set('moved2', 'this entry will be moved') + self.s.remove('removed') + revB = self.s.commit('Final state') + new,mod,rem = self.s.changed(revA) + self.failUnless(sorted(new) == ['moved2', 'new'], + 'Unexpected new: %s' % new) + self.failUnless(mod == ['modified'], + 'Unexpected modified: %s' % mod) + self.failUnless(sorted(rem) == ['moved', 'removed'], + 'Unexpected removed: %s' % rem) + def make_storage_testcase_subclasses(storage_class, namespace): """Make StorageTestCase subclasses for storage_class in namespace.""" storage_testcase_classes = [ @@ -906,8 +1011,11 @@ if TESTING == True: storage_testcase_classes = [ c for c in ( ob for ob in globals().values() if isinstance(ob, type)) - if issubclass(c, StorageTestCase) \ - and c.Class == Storage] + if ((issubclass(c, StorageTestCase) \ + and c.Class == Storage) + or + (issubclass(c, VersionedStorageTestCase) \ + and c.Class == VersionedStorage))] for base_class in storage_testcase_classes: testcase_class_name = storage_class.__name__ + base_class.__name__ diff --git a/libbe/storage/http.py b/libbe/storage/http.py index 0792b1e..2de2aff 100644 --- a/libbe/storage/http.py +++ b/libbe/storage/http.py @@ -142,6 +142,13 @@ class HTTP (base.VersionedStorage): url, get=False, data_dict={'id':id, 'recursive':True}) + def _ancestors(self, id=None, revision=None): + url = urlparse.urljoin(self.repo, 'ancestors') + page,final_url,info = get_post_url( + url, get=True, + data_dict={'id':id, 'revision':revision}) + return page.strip('\n').splitlines() + def _children(self, id=None, revision=None): url = urlparse.urljoin(self.repo, 'children') page,final_url,info = get_post_url( @@ -227,6 +234,15 @@ class HTTP (base.VersionedStorage): raise base.InvalidID(id) return page.rstrip('\n') + def changed(self, revision=None): + url = urlparse.urljoin(self.repo, 'changed') + page,final_url,info = get_post_url( + url, get=True, + data_dict={'revision':revision}) + lines = page.strip('\n') + new,mod,rem = [p.splitlines() for p in page.split('\n\n')] + return (new, mod, rem) + def check_storage_version(self): version = self.storage_version() if version != libbe.storage.STORAGE_VERSION: diff --git a/libbe/storage/vcs/arch.py b/libbe/storage/vcs/arch.py index f9ae32b..f9b01fd 100644 --- a/libbe/storage/vcs/arch.py +++ b/libbe/storage/vcs/arch.py @@ -27,7 +27,7 @@ import os import re import shutil import sys -import time +import time # work around http://mercurial.selenic.com/bts/issue618 import libbe import libbe.ui.util.user @@ -68,6 +68,7 @@ class Arch(base.VCS): self.versioned = True self.interspersed_vcs_files = True self.paranoid = False + self.__updated = [] # work around http://mercurial.selenic.com/bts/issue618 def _vcs_version(self): status,output,error = self._u_invoke_client('--version') @@ -288,7 +289,7 @@ class Arch(base.VCS): shutil.rmtree(arch_ids) def _vcs_update(self, path): - pass + self.__updated.append(path) # work around http://mercurial.selenic.com/bts/issue618 def _vcs_is_versioned(self, path): if '.arch-ids' in path: @@ -300,16 +301,29 @@ class Arch(base.VCS): return base.VCS._vcs_get_file_contents(self, path, revision) else: status,output,error = \ - self._invoke_client('file-find', path, revision) - relpath = output.rstrip('\n') + self._invoke_client( + 'file-find', '--unescaped', path, revision) + relpath = output.rstrip('\n').splitlines()[-1] + print >> sys.stderr, 'getting', relpath return base.VCS._vcs_get_file_contents(self, relpath) + def _vcs_path(self, id, revision): + raise NotImplementedError + 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 base.EmptyCommit() + # work around http://mercurial.selenic.com/bts/issue618 + time.sleep(1) + for path in self.__updated: + os.utime(os.path.join(self.repo, path), None) + self.__updated = [] + status,output,error = self._u_invoke_client('changes',expect=(0,1)) + if status == 0: + # end work around + raise base.EmptyCommit() summary,body = self._u_parse_commitfile(commitfile) args = ['commit', '--summary', summary] if body != None: @@ -342,6 +356,9 @@ class Arch(base.VCS): return None return '%s--%s' % (self._archive_project_name(), log) + def _vcs_changed(self, revision): + raise NotImplementedError + if libbe.TESTING == True: base.make_vcs_testcase_subclasses(Arch, sys.modules[__name__]) diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 8390cbc..15460b0 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -168,7 +168,7 @@ class CachedPathID (object): >>> c.path('qrs') Traceback (most recent call last): ... - InvalidID: 'qrs' + InvalidID: qrs in revision None >>> c.disconnect() >>> c.destroy() >>> dir.cleanup() @@ -363,7 +363,7 @@ class VCS (libbe.storage.base.VersionedStorage): 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). + (e.g. RevisionedBugDir). Disable encoding manipulation ============================= @@ -597,6 +597,14 @@ os.listdir(self.get_path("bugs")): """ return None + def _vcs_changed(self, revision): + """ + Return a tuple of lists of ids + (new, modified, removed) + from the specified revision to the current situation. + """ + return ([], [], []) + def version(self): # Cache version string for efficiency. if not hasattr(self, '_version'): @@ -737,6 +745,27 @@ os.listdir(self.get_path("bugs")): if p.startswith(path): self._cached_path_id.remove_id(id) + def _ancestors(self, id=None, revision=None): + if revision == None: + id_to_path = self._cached_path_id.path + else: + id_to_path = lambda id : self._vcs_path(id, revision) + if id==None: + path = self.be_dir + else: + path = id_to_path(id) + ancestors = [] + while True: + if not path.startswith(self.repo + os.path.sep): + break + path = os.path.dirname(path) + try: + id = self._u_path_to_id(path) + ancestors.append(id) + except (SpacerCollision, InvalidPath): + pass + return ancestors + def _children(self, id=None, revision=None): if revision == None: id_to_path = self._cached_path_id.path @@ -786,12 +815,14 @@ os.listdir(self.get_path("bugs")): try: contents = self._vcs_get_file_contents(relpath, revision) except InvalidID, e: - if InvalidID == None: - e.id = InvalidID + if e.id == None: + e.id = id + if e.revision == None: + e.revision = revision raise if contents in [libbe.storage.base.InvalidDirectory, libbe.util.InvalidObject]: - raise InvalidID(id) + raise InvalidID(id, revision) elif len(contents) == 0: return None return contents @@ -839,6 +870,20 @@ os.listdir(self.get_path("bugs")): raise libbe.storage.base.InvalidRevision(index) return revid + def changed(self, revision): + new,mod,rem = self._vcs_changed(revision) + def paths_to_ids(paths): + for p in paths: + try: + id = self._u_path_to_id(p) + yield id + except (SpacerCollision, InvalidPath): + pass + new_id = list(paths_to_ids(new)) + mod_id = list(paths_to_ids(mod)) + rem_id = list(paths_to_ids(rem)) + return (new_id, mod_id, rem_id) + def _u_any_in_string(self, list, string): """ Return True if any of the strings in list are in string. @@ -883,6 +928,33 @@ os.listdir(self.get_path("bugs")): return None return ret + def _u_find_id_from_manifest(self, id, manifest, revision=None): + """ + Search for the relative path to id using manifest, a list of all files. + + Returns None if the id is not found. + """ + 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 manifest 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: + try: + p_id = self._u_path_to_id(file) + if p_id == id: + return file + except (SpacerCollision, InvalidPath): + pass + raise InvalidID(id, revision=revision) + def _u_find_id(self, id, revision): """ Search for the relative path to id as of revision. @@ -1023,8 +1095,7 @@ if libbe.TESTING == True: class VCS_installed_TestCase (VCSTestCase): def test_installed(self): - """ - See if the VCS is installed. + """See if the VCS is installed. """ self.failUnless(self.s.installed() == True, '%(name)s VCS not found' % vars(self.Class)) @@ -1032,8 +1103,7 @@ if libbe.TESTING == True: class VCS_detection_TestCase (VCSTestCase): def test_detection(self): - """ - See if the VCS detects its installed repository + """See if the VCS detects its installed repository """ if self.s.installed(): self.s.disconnect() @@ -1043,8 +1113,7 @@ if libbe.TESTING == True: self.s.connect() def test_no_detection(self): - """ - See if the VCS detects its installed repository + """See if the VCS detects its installed repository """ if self.s.installed() and self.Class.name != 'None': self.s.disconnect() diff --git a/libbe/storage/vcs/bzr.py b/libbe/storage/vcs/bzr.py index 4d72fd0..e1cd2e5 100644 --- a/libbe/storage/vcs/bzr.py +++ b/libbe/storage/vcs/bzr.py @@ -36,13 +36,13 @@ import os.path import re import shutil import StringIO +import sys import libbe import base if libbe.TESTING == True: import doctest - import sys import unittest @@ -134,7 +134,9 @@ class Bzr(base.VCS): return cmd.outf.getvalue() def _vcs_path(self, id, revision): - return self._u_find_id(id, revision) + manifest = self._vcs_listdir( + self.repo, revision=revision, recursive=True) + return self._u_find_id_from_manifest(id, manifest, revision=revision) def _vcs_isdir(self, path, revision): try: @@ -145,13 +147,13 @@ class Bzr(base.VCS): raise return True - def _vcs_listdir(self, path, revision): + def _vcs_listdir(self, path, revision, recursive=False): 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) + cmd.run(revision=revision, path=path, recursive=recursive) except bzrlib.errors.BzrCommandError, e: if 'not present in revision' in str(e): raise base.InvalidPath(path, root=self.repo, revision=revision) @@ -188,6 +190,87 @@ class Bzr(base.VCS): return str(index) # bzr commit 0 is the empty tree. return str(current_revision+index+1) + def _diff(self, revision): + revision = self._parse_revision_string(revision) + cmd = bzrlib.builtins.cmd_diff() + cmd.outf = StringIO.StringIO() + # for some reason, cmd_diff uses sys.stdout not self.outf for output. + stdout = sys.stdout + sys.stdout = cmd.outf + try: + status = cmd.run(revision=revision, file_list=[self.repo]) + finally: + sys.stdout = stdout + assert status in [0,1], "Invalid status %d" % status + return cmd.outf.getvalue() + + def _parse_diff(self, diff_text): + """ + Example diff text: + + === modified file 'dir/changed' + --- dir/changed 2010-01-16 01:54:53 +0000 + +++ dir/changed 2010-01-16 01:54:54 +0000 + @@ -1,3 +1,3 @@ + hi + -there + +everyone and + joe + + === removed file 'dir/deleted' + --- dir/deleted 2010-01-16 01:54:53 +0000 + +++ dir/deleted 1970-01-01 00:00:00 +0000 + @@ -1,3 +0,0 @@ + -in + -the + -beginning + + === removed file 'dir/moved' + --- dir/moved 2010-01-16 01:54:53 +0000 + +++ dir/moved 1970-01-01 00:00:00 +0000 + @@ -1,4 +0,0 @@ + -the + -ants + -go + -marching + + === added file 'dir/moved2' + --- dir/moved2 1970-01-01 00:00:00 +0000 + +++ dir/moved2 2010-01-16 01:54:34 +0000 + @@ -0,0 +1,4 @@ + +the + +ants + +go + +marching + + === added file 'dir/new' + --- dir/new 1970-01-01 00:00:00 +0000 + +++ dir/new 2010-01-16 01:54:54 +0000 + @@ -0,0 +1,2 @@ + +hello + +world + + """ + new = [] + modified = [] + removed = [] + for line in diff_text.splitlines(): + if not line.startswith('=== '): + continue + fields = line.split() + action = fields[1] + file = fields[-1].strip("'") + if action == 'added': + new.append(file) + elif action == 'modified': + modified.append(file) + elif action == 'removed': + removed.append(file) + return (new,modified,removed) + + def _vcs_changed(self, revision): + return self._parse_diff(self._diff(revision)) + if libbe.TESTING == True: base.make_vcs_testcase_subclasses(Bzr, sys.modules[__name__]) diff --git a/libbe/storage/vcs/darcs.py b/libbe/storage/vcs/darcs.py index 92dc1a7..c6892b4 100644 --- a/libbe/storage/vcs/darcs.py +++ b/libbe/storage/vcs/darcs.py @@ -157,24 +157,14 @@ class Darcs(base.VCS): 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 + patch = self._diff(revision, path=path, unicode_output=False) # '--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) + status,output,error = self._u_invoke(args, stdin=patch) if os.path.exists(os.path.join(self.repo, path)) == True: contents = base.VCS._vcs_get_file_contents(self, path) @@ -182,11 +172,8 @@ class Darcs(base.VCS): 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) + status,output,error = self._u_invoke(args, stdin=patch) return contents def _vcs_path(self, id, revision): @@ -257,7 +244,10 @@ class Darcs(base.VCS): revision = match.groups()[0] return revision - def _vcs_revision_id(self, index): + def _revisions(self): + """ + Return a list of revisions in the repository. + """ status,output,error = self._u_invoke_client('changes', '--xml') revisions = [] xml_str = output.encode('unicode_escape').replace(r'\n', '\n') @@ -270,6 +260,10 @@ class Darcs(base.VCS): text = unescape(unicode(child.text).decode('unicode_escape').strip()) revisions.append(text) revisions.reverse() + return revisions + + def _vcs_revision_id(self, index): + revisions = self._revisions() try: if index > 0: return revisions[index-1] @@ -280,6 +274,105 @@ class Darcs(base.VCS): except IndexError: return None + def _diff(self, revision, path=None, unicode_output=True): + revisions = self._revisions() + i = revisions.index(revision) + args = ['diff', '--unified'] + if i+1 < len(revisions): + next_rev = revisions[i+1] + args.extend(['--from-patch', next_rev]) + if path != None: + args.append(path) + kwargs = {'unicode_output':unicode_output} + status,output,error = self._u_invoke_client( + *args, **kwargs) + return output + + def _parse_diff(self, diff_text): + """ + Example diff text: + + Mon Jan 18 15:19:30 EST 2010 None <None@invalid.com> + * Final state + diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/modified new-BEtestgQtDuD/.be/dir/bugs/modified + --- old-BEtestgQtDuD/.be/dir/bugs/modified 2010-01-18 15:19:30.000000000 -0500 + +++ new-BEtestgQtDuD/.be/dir/bugs/modified 2010-01-18 15:19:30.000000000 -0500 + @@ -1 +1 @@ + -some value to be modified + \ No newline at end of file + +a new value + \ No newline at end of file + diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved new-BEtestgQtDuD/.be/dir/bugs/moved + --- old-BEtestgQtDuD/.be/dir/bugs/moved 2010-01-18 15:19:30.000000000 -0500 + +++ new-BEtestgQtDuD/.be/dir/bugs/moved 1969-12-31 19:00:00.000000000 -0500 + @@ -1 +0,0 @@ + -this entry will be moved + \ No newline at end of file + diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/moved2 new-BEtestgQtDuD/.be/dir/bugs/moved2 + --- old-BEtestgQtDuD/.be/dir/bugs/moved2 1969-12-31 19:00:00.000000000 -0500 + +++ new-BEtestgQtDuD/.be/dir/bugs/moved2 2010-01-18 15:19:30.000000000 -0500 + @@ -0,0 +1 @@ + +this entry will be moved + \ No newline at end of file + diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/new new-BEtestgQtDuD/.be/dir/bugs/new + --- old-BEtestgQtDuD/.be/dir/bugs/new 1969-12-31 19:00:00.000000000 -0500 + +++ new-BEtestgQtDuD/.be/dir/bugs/new 2010-01-18 15:19:30.000000000 -0500 + @@ -0,0 +1 @@ + +this entry is new + \ No newline at end of file + diff -rN --unified old-BEtestgQtDuD/.be/dir/bugs/removed new-BEtestgQtDuD/.be/dir/bugs/removed + --- old-BEtestgQtDuD/.be/dir/bugs/removed 2010-01-18 15:19:30.000000000 -0500 + +++ new-BEtestgQtDuD/.be/dir/bugs/removed 1969-12-31 19:00:00.000000000 -0500 + @@ -1 +0,0 @@ + -this entry will be deleted + \ No newline at end of file + + """ + new = [] + modified = [] + removed = [] + lines = diff_text.splitlines() + repodir = os.path.basename(self.repo) + os.path.sep + i = 0 + while i < len(lines): + line = lines[i]; i += 1 + if not line.startswith('diff '): + continue + file_a,file_b = line.split()[-2:] + assert file_a.startswith('old-'), \ + 'missformed file_a %s' % file_a + assert file_b.startswith('new-'), \ + 'missformed file_a %s' % file_b + file = file_a[4:] + assert file_b[4:] == file, \ + 'diff file missmatch %s != %s' % (file_a, file_b) + assert file.startswith(repodir), \ + 'missformed file_a %s' % file_a + file = file[len(repodir):] + lines_added = 0 + lines_removed = 0 + line = lines[i]; i += 1 + assert line.startswith('--- old-'), \ + 'missformed "---" line %s' % line + time_a = line.split('\t')[1] + line = lines[i]; i += 1 + assert line.startswith('+++ new-'), \ + 'missformed "+++" line %s' % line + time_b = line.split('\t')[1] + zero_time = time.strftime('%Y-%m-%d %H:%M:%S.000000000 ', + time.localtime(0)) + # note that zero_time is missing the trailing timezone offset + if time_a.startswith(zero_time): + new.append(file) + elif time_b.startswith(zero_time): + removed.append(file) + else: + modified.append(file) + return (new,modified,removed) + + def _vcs_changed(self, revision): + return self._parse_diff(self._diff(revision)) + if libbe.TESTING == True: base.make_vcs_testcase_subclasses(Darcs, sys.modules[__name__]) diff --git a/libbe/storage/vcs/git.py b/libbe/storage/vcs/git.py index 5382c7e..6d3aa87 100644 --- a/libbe/storage/vcs/git.py +++ b/libbe/storage/vcs/git.py @@ -175,6 +175,87 @@ class Git(base.VCS): except IndexError: return None + def _diff(self, revision): + status,output,error = self._u_invoke_client('diff', revision) + return output + + def _parse_diff(self, diff_text): + """ + Example diff text: + + diff --git a/dir/changed b/dir/changed + index 6c3ea8c..2f2f7c7 100644 + --- a/dir/changed + +++ b/dir/changed + @@ -1,3 +1,3 @@ + hi + -there + +everyone and + joe + diff --git a/dir/deleted b/dir/deleted + deleted file mode 100644 + index 225ec04..0000000 + --- a/dir/deleted + +++ /dev/null + @@ -1,3 +0,0 @@ + -in + -the + -beginning + diff --git a/dir/moved b/dir/moved + deleted file mode 100644 + index 5ef102f..0000000 + --- a/dir/moved + +++ /dev/null + @@ -1,4 +0,0 @@ + -the + -ants + -go + -marching + diff --git a/dir/moved2 b/dir/moved2 + new file mode 100644 + index 0000000..5ef102f + --- /dev/null + +++ b/dir/moved2 + @@ -0,0 +1,4 @@ + +the + +ants + +go + +marching + diff --git a/dir/new b/dir/new + new file mode 100644 + index 0000000..94954ab + --- /dev/null + +++ b/dir/new + @@ -0,0 +1,2 @@ + +hello + +world + """ + new = [] + modified = [] + removed = [] + lines = diff_text.splitlines() + for i,line in enumerate(lines): + if not line.startswith('diff '): + continue + file_a,file_b = line.split()[-2:] + assert file_a.startswith('a/'), \ + 'missformed file_a %s' % file_a + assert file_b.startswith('b/'), \ + 'missformed file_a %s' % file_b + file = file_a[2:] + assert file_b[2:] == file, \ + 'diff file missmatch %s != %s' % (file_a, file_b) + if lines[i+1].startswith('new '): + new.append(file) + elif lines[i+1].startswith('index '): + modified.append(file) + elif lines[i+1].startswith('deleted '): + removed.append(file) + return (new,modified,removed) + + def _vcs_changed(self, revision): + return self._parse_diff(self._diff(revision)) + if libbe.TESTING == True: base.make_vcs_testcase_subclasses(Git, sys.modules[__name__]) diff --git a/libbe/storage/vcs/hg.py b/libbe/storage/vcs/hg.py index 824f687..076943a 100644 --- a/libbe/storage/vcs/hg.py +++ b/libbe/storage/vcs/hg.py @@ -112,23 +112,9 @@ class Hg(base.VCS): 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) + manifest = self._u_invoke_client( + 'manifest', '--rev', revision).splitlines() + return self._u_find_id_from_manifest(id, manifest, revision=revision) def _vcs_isdir(self, path, revision): output = self._u_invoke_client('manifest', '--rev', revision) @@ -172,6 +158,77 @@ class Hg(base.VCS): return None # before initial commit. return id + def _diff(self, revision): + return self._u_invoke_client( + 'diff', '-r', revision, '--git') + + def _parse_diff(self, diff_text): + """ + Example diff text: + + diff --git a/.be/dir/bugs/modified b/.be/dir/bugs/modified + --- a/.be/dir/bugs/modified + +++ b/.be/dir/bugs/modified + @@ -1,1 +1,1 @@ some value to be modified + -some value to be modified + \ No newline at end of file + +a new value + \ No newline at end of file + diff --git a/.be/dir/bugs/moved b/.be/dir/bugs/moved + deleted file mode 100644 + --- a/.be/dir/bugs/moved + +++ /dev/null + @@ -1,1 +0,0 @@ + -this entry will be moved + \ No newline at end of file + diff --git a/.be/dir/bugs/moved2 b/.be/dir/bugs/moved2 + new file mode 100644 + --- /dev/null + +++ b/.be/dir/bugs/moved2 + @@ -0,0 +1,1 @@ + +this entry will be moved + \ No newline at end of file + diff --git a/.be/dir/bugs/new b/.be/dir/bugs/new + new file mode 100644 + --- /dev/null + +++ b/.be/dir/bugs/new + @@ -0,0 +1,1 @@ + +this entry is new + \ No newline at end of file + diff --git a/.be/dir/bugs/removed b/.be/dir/bugs/removed + deleted file mode 100644 + --- a/.be/dir/bugs/removed + +++ /dev/null + @@ -1,1 +0,0 @@ + -this entry will be deleted + \ No newline at end of file + """ + new = [] + modified = [] + removed = [] + lines = diff_text.splitlines() + for i,line in enumerate(lines): + if not line.startswith('diff '): + continue + file_a,file_b = line.split()[-2:] + assert file_a.startswith('a/'), \ + 'missformed file_a %s' % file_a + assert file_b.startswith('b/'), \ + 'missformed file_a %s' % file_b + file = file_a[2:] + assert file_b[2:] == file, \ + 'diff file missmatch %s != %s' % (file_a, file_b) + if lines[i+1].startswith('new '): + new.append(file) + elif lines[i+1].startswith('deleted '): + removed.append(file) + else: + modified.append(file) + return (new,modified,removed) + + def _vcs_changed(self, revision): + return self._parse_diff(self._diff(revision)) + if libbe.TESTING == True: base.make_vcs_testcase_subclasses(Hg, sys.modules[__name__]) |