aboutsummaryrefslogtreecommitdiffstats
path: root/libbe
diff options
context:
space:
mode:
Diffstat (limited to 'libbe')
-rw-r--r--libbe/bugdir.py85
-rw-r--r--libbe/command/diff.py7
-rw-r--r--libbe/command/serve.py45
-rw-r--r--libbe/diff.py78
-rw-r--r--libbe/storage/base.py224
-rw-r--r--libbe/storage/http.py16
-rw-r--r--libbe/storage/vcs/arch.py27
-rw-r--r--libbe/storage/vcs/base.py91
-rw-r--r--libbe/storage/vcs/bzr.py91
-rw-r--r--libbe/storage/vcs/darcs.py127
-rw-r--r--libbe/storage/vcs/git.py81
-rw-r--r--libbe/storage/vcs/hg.py91
-rw-r--r--libbe/util/subproc.py2
13 files changed, 768 insertions, 197 deletions
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index 951a4d5..5967a7e 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -292,83 +292,54 @@ class BugDir (list, settings_object.SavedSettingsObject):
def sibling_uuids(self):
return []
- # methods for managing duplicate BugDirs
-
- def duplicate_bugdir(self, revision):
- """
- Duplicate bugdirs are read-only copies used for generating
- diffs between revisions.
- """
- storage_version = self.storage.storage_version(revision)
+class RevisionedBugDir (BugDir):
+ """
+ RevisionedBugDirs are read-only copies used for generating
+ diffs between revisions.
+ """
+ def __init__(self, bugdir, revision):
+ storage_version = bugdir.storage.storage_version(revision)
if storage_version != libbe.storage.STORAGE_VERSION:
raise libbe.storage.InvalidStorageVersion(storage_version)
- s = copy.deepcopy(self.storage)
+ s = copy.deepcopy(bugdir.storage)
s.writeable = False
class RevisionedStorage (object):
def __init__(self, storage, default_revision):
self.s = storage
self.sget = self.s.get
+ self.sancestors = self.s.ancestors
self.schildren = self.s.children
+ self.schanged = self.s.changed
self.r = default_revision
def get(self, *args, **kwargs):
if not 'revision' in kwargs or kwargs['revision'] == None:
kwargs['revision'] = self.r
return self.sget(*args, **kwargs)
+ def ancestors(self, *args, **kwargs):
+ print 'getting ancestors', args, kwargs
+ if not 'revision' in kwargs or kwargs['revision'] == None:
+ kwargs['revision'] = self.r
+ ret = self.sancestors(*args, **kwargs)
+ print 'got ancestors', ret
+ return ret
def children(self, *args, **kwargs):
if not 'revision' in kwargs or kwargs['revision'] == None:
kwargs['revision'] = self.r
return self.schildren(*args, **kwargs)
+ def changed(self, *args, **kwargs):
+ if not 'revision' in kwargs or kwargs['revision'] == None:
+ kwargs['revision'] = self.r
+ return self.schanged(*args, **kwargs)
rs = RevisionedStorage(s, revision)
s.get = rs.get
+ s.ancestors = rs.ancestors
s.children = rs.children
- dbd = BugDir(s, from_storage=True)
-# dbd = copy.copy(self)
-# dbd.storage = copy.copy(self.storage)
-# dbd._bug_map = copy.copy(self._bug_map)
-# dbd.storage.writeable = False
-# added,changed,removed = self.storage.changed_since(revision)
-# for id in added:
-# pass
-# for id in removed:
-# pass
-# for id in changed:
-# parsed = libbe.util.id.parse_id(id)
-# if parsed['type'] == 'bugdir':
-# assert parsed['remaining'] == ['settings'], parsed['remaining']
-# dbd._settings = copy.copy(self._settings)
-# mf = self.storage.get(self.id.storage('settings'), default='\n',
-# revision=revision)
-# dbd.load_settings(mf)
-# else:
-# if parsed['bug'] not in self:
-# self._load_bug(parsed['bug'])
-# dbd._load_bug(parsed['bug'])
-# else:
-# bug = copy.copy(self._bug_map[parsed['bug']])
-# bug.settings = copy.copy(bug.settings)
-# dbd._bug_map[parsed['bug']] = bug
-# if parsed['type'] == 'bug':
-# assert parsed['remaining'] == ['values'], parsed['remaining']
-# mf = self.storage.get(self.id.storage('values'), default='\n',
-# revision=revision)
-# bug.load_settings(mf)
-# elif parsed['type'] == 'comment':
-# assert parsed['remaining'] in [['values'], ['body']], \
-# parsed['remaining']
-# bug.comment_root = copy.deepcopy(bug.comment_root)
-# comment = bug.comment_from_uuid(parsed['comment'])
-# if parsed['remaining'] == ['values']:
-# mf = self.storage.get(self.id.storage('values'), default='\n',
-# revision=revision)
-# comment.load_settings(mf)
-# else:
-# body = self.storage.get(self.id.storage('body'), default='\n',
-# revision=revision)
-# comment.body = body
-# else:
-# assert 1==0, 'Unkown type "%s" for id "%s"' % (type, id)
-# dbd.storage.readable = False # so we won't read in added bugs, etc.
- return dbd
+ s.changed = rs.changed
+ BugDir.__init__(self, s, from_storage=True)
+ self.revision = revision
+ def changed(self):
+ return self.storage.changed()
+
if libbe.TESTING == True:
class SimpleBugDir (BugDir):
diff --git a/libbe/command/diff.py b/libbe/command/diff.py
index 4db7c17..967ab14 100644
--- a/libbe/command/diff.py
+++ b/libbe/command/diff.py
@@ -17,6 +17,7 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import libbe
+import libbe.bugdir
import libbe.bug
import libbe.command
import libbe.command.util
@@ -95,11 +96,11 @@ class Diff (libbe.command.Command):
if params['repo'] == None:
if params['revision'] == None: # get the most recent revision
params['revision'] = bugdir.storage.revision_id(-1)
- old_bd = bugdir.duplicate_bugdir(params['revision'])
+ old_bd = libbe.bugdir.RevisionedBugDir(bugdir, params['revision'])
else:
old_storage = libbe.storage.get_storage(params['repo'])
old_storage.connect()
- old_bd_current = bugdir.BugDir(old_storage, from_disk=True)
+ old_bd_current = libbe.bugdir.BugDir(old_storage, from_disk=True)
if params['revision'] == None: # use the current working state
old_bd = old_bd_current
else:
@@ -107,7 +108,7 @@ class Diff (libbe.command.Command):
raise libbe.command.UserError(
'%s is not revision-controlled.'
% storage.repo)
- old_bd = old_bd_current.duplicate_bugdir(revision)
+ old_bd = libbe.bugdir.RevisionedBugDir(old_bd_current,revision)
d = libbe.diff.Diff(old_bd, bugdir)
tree = d.report_tree(subscriptions)
diff --git a/libbe/command/serve.py b/libbe/command/serve.py
index 5dbd2b4..ec25486 100644
--- a/libbe/command/serve.py
+++ b/libbe/command/serve.py
@@ -19,6 +19,13 @@ import posixpath
import urllib
import urlparse
+try:
+ # Python >= 2.6
+ from urlparse import parse_qs
+except ImportError:
+ # Python <= 2.5
+ from cgi import parse_qs
+
import libbe
import libbe.command
import libbe.command.util
@@ -66,12 +73,16 @@ class BERequestHandler (server.BaseHTTPRequestHandler):
data = self.parse_query(query)
try:
- if path == ['children']:
+ if path == ['ancestors']:
+ content,ctype = self.handle_ancestors(data)
+ elif path == ['children']:
content,ctype = self.handle_children(data)
elif len(path) > 1 and path[0] == 'get':
content,ctype = self.handle_get('/'.join(path[1:]), data)
elif path == ['revision-id']:
content,ctype = self.handle_revision_id(data)
+ elif path == ['changed']:
+ content,ctype = self.handle_changed(data)
elif path == ['version']:
content,ctype = self.handle_version(data)
else:
@@ -180,6 +191,21 @@ class BERequestHandler (server.BaseHTTPRequestHandler):
self.send_response(200)
return (None,None)
+ def handle_ancestors(self, data):
+ if not 'id' in data:
+ self.send_error(406, 'Missing query key id')
+ raise _HandlerError()
+ elif data['id'] == 'None':
+ data['id'] = None
+ id = data['id']
+ if not 'revision' in data or data['revision'] == 'None':
+ data['revision'] = None
+ revision = data['revision']
+ content = '\n'.join(self.s.ancestors(id, revision))
+ ctype = 'application/octet-stream'
+ self.send_response(200)
+ return content,ctype
+
def handle_children(self, data):
if not 'id' in data:
self.send_error(406, 'Missing query key id')
@@ -246,6 +272,16 @@ class BERequestHandler (server.BaseHTTPRequestHandler):
self.send_response(200)
return content,ctype
+ def handle_changed(self, data):
+ if not 'revision' in data or data['revision'] == 'None':
+ data['revision'] = None
+ revision = data['revision']
+ add,mod,rem = self.s.changed(revision)
+ content = '\n\n'.join(['\n'.join(p) for p in (add,mod,rem)])
+ ctype = 'application/octet-stream'
+ self.send_response(200)
+ return content,ctype
+
def handle_version(self, data):
if not 'revision' in data or data['revision'] == 'None':
data['revision'] = None
@@ -270,7 +306,7 @@ class BERequestHandler (server.BaseHTTPRequestHandler):
def parse_query(self, query):
if len(query) == 0:
return {}
- data = urlparse.parse_qs(
+ data = parse_qs(
query, keep_blank_values=True, strict_parsing=True)
for k,v in data.items():
if len(v) == 1:
@@ -296,13 +332,16 @@ class BERequestHandler (server.BaseHTTPRequestHandler):
class Serve (libbe.command.Command):
"""Serve a Storage backend for the HTTP storage client
+ >>> raise NotImplementedError, "Serve tests not yet implemented"
+ >>> import sys
>>> import libbe.bugdir
+ >>> import libbe.command.list
>>> bd = libbe.bugdir.SimpleBugDir(memory=False)
>>> io = libbe.command.StringInputOutput()
>>> io.stdout = sys.stdout
>>> ui = libbe.command.UserInterface(io=io)
>>> ui.storage_callbacks.set_storage(bd.storage)
- >>> cmd = List(ui=ui)
+ >>> cmd = libbe.command.list.List(ui=ui)
>>> ret = ui.run(cmd)
abc/a:om: Bug A
diff --git a/libbe/diff.py b/libbe/diff.py
index b325c5d..94a2dc3 100644
--- a/libbe/diff.py
+++ b/libbe/diff.py
@@ -367,32 +367,68 @@ class Diff (object):
old_uuids.extend([s for s in subscribed_bugs
if self.old_bugdir.has_bug(s)])
old_uuids = sorted(set(old_uuids))
+
added = []
removed = []
modified = []
- for uuid in new_uuids:
- new_bug = self.new_bugdir.bug_from_uuid(uuid)
- try:
- old_bug = self.old_bugdir.bug_from_uuid(uuid)
- except KeyError:
+ if hasattr(self.old_bugdir, 'changed'):
+ # take advantage of a RevisionedBugDir-style changed() method
+ new_ids,mod_ids,rem_ids = self.old_bugdir.changed()
+ for id in new_ids:
+ for a_id in self.new_bugdir.storage.ancestors(id):
+ if a_id.count('/') == 0:
+ if a_id in [b.id.storage() for b in added]:
+ break
+ try:
+ bug = self.new_bugdir.bug_from_uuid(a_id)
+ added.append(bug)
+ except libbe.bugdir.NoBugMatches:
+ pass
+ for id in rem_ids:
+ for a_id in self.old_bugdir.storage.ancestors(id):
+ if a_id.count('/') == 0:
+ if a_id in [b.id.storage() for b in removed]:
+ break
+ try:
+ bug = self.old_bugdir.bug_from_uuid(a_id)
+ removed.append(bug)
+ except libbe.bugdir.NoBugMatches:
+ pass
+ for id in mod_ids:
+ for a_id in self.new_bugdir.storage.ancestors(id):
+ if a_id.count('/') == 0:
+ if a_id in [b[0].id.storage() for b in modified]:
+ break
+ try:
+ new_bug = self.new_bugdir.bug_from_uuid(a_id)
+ old_bug = self.old_bugdir.bug_from_uuid(a_id)
+ modified.append((old_bug, new_bug))
+ except libbe.bugdir.NoBugMatches:
+ pass
+ else:
+ for uuid in new_uuids:
+ new_bug = self.new_bugdir.bug_from_uuid(uuid)
+ try:
+ old_bug = self.old_bugdir.bug_from_uuid(uuid)
+ except KeyError:
+ if BUGDIR_TYPE_ALL in bugdir_types \
+ or BUGDIR_TYPE_NEW in bugdir_types \
+ or uuid in subscribed_bugs:
+ added.append(new_bug)
+ continue
if BUGDIR_TYPE_ALL in bugdir_types \
- or BUGDIR_TYPE_NEW in bugdir_types \
+ or BUGDIR_TYPE_MOD in bugdir_types \
or uuid in subscribed_bugs:
- added.append(new_bug)
- continue
- if BUGDIR_TYPE_ALL in bugdir_types \
- or BUGDIR_TYPE_MOD in bugdir_types \
- or uuid in subscribed_bugs:
- if old_bug.storage != None and old_bug.storage.is_readable():
- old_bug.load_comments()
- if new_bug.storage != None and new_bug.storage.is_readable():
- new_bug.load_comments()
- if old_bug != new_bug:
- modified.append((old_bug, new_bug))
- for uuid in old_uuids:
- if not self.new_bugdir.has_bug(uuid):
- old_bug = self.old_bugdir.bug_from_uuid(uuid)
- removed.append(old_bug)
+ if old_bug.storage != None and old_bug.storage.is_readable():
+ old_bug.load_comments()
+ if new_bug.storage != None and new_bug.storage.is_readable():
+ new_bug.load_comments()
+ if old_bug != new_bug:
+ modified.append((old_bug, new_bug))
+ for uuid in old_uuids:
+ if not self.new_bugdir.has_bug(uuid):
+ old_bug = self.old_bugdir.bug_from_uuid(uuid)
+ removed.append(old_bug)
added.sort()
removed.sort()
modified.sort(self._bug_modified_cmp)
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__])
diff --git a/libbe/util/subproc.py b/libbe/util/subproc.py
index 6ca1e80..b02b8e8 100644
--- a/libbe/util/subproc.py
+++ b/libbe/util/subproc.py
@@ -36,7 +36,7 @@ if _POSIX == True:
class CommandError(Exception):
def __init__(self, command, status, stdout=None, stderr=None):
strerror = ['Command failed (%d):\n %s\n' % (status, stderr),
- 'while executing\n %s' % command]
+ 'while executing\n %s' % str(command)]
Exception.__init__(self, '\n'.join(strerror))
self.command = command
self.status = status