Always use a branching scheme.
[jelmer/subvertpy.git] / repository.py
1 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16 """Subversion repository access."""
17
18 import bzrlib
19 from bzrlib import osutils
20 from bzrlib.branch import BranchCheckResult
21 from bzrlib.errors import (InvalidRevisionId, NoSuchRevision, 
22                            NotBranchError, UninitializableFormat, BzrError)
23 from bzrlib.inventory import Inventory
24 from bzrlib.lockable_files import LockableFiles, TransportLock
25 from bzrlib.repository import Repository, RepositoryFormat
26 from bzrlib.revisiontree import RevisionTree
27 from bzrlib.revision import Revision, NULL_REVISION
28 from bzrlib.transport import Transport
29 from bzrlib.timestamp import unpack_highres_date, format_highres_date
30 from bzrlib.trace import mutter
31
32 from svn.core import SubversionException, Pool
33 import svn.core
34
35 import os
36
37 from branchprops import BranchPropertyList
38 from cache import create_cache_dir, sqlite3
39 from config import SvnRepositoryConfig
40 import errors
41 import logwalker
42 from revids import (generate_svn_revision_id, parse_svn_revision_id, 
43                     MAPPING_VERSION, RevidMap)
44 from scheme import BranchingScheme, ListBranchingScheme, parse_list_scheme_text
45 from tree import SvnRevisionTree
46
47 SVN_PROP_BZR_PREFIX = 'bzr:'
48 SVN_PROP_BZR_MERGE = 'bzr:merge'
49 SVN_PROP_BZR_FILEIDS = 'bzr:file-ids'
50 SVN_PROP_SVK_MERGE = 'svk:merge'
51 SVN_PROP_BZR_REVISION_INFO = 'bzr:revision-info'
52 SVN_REVPROP_BZR_SIGNATURE = 'bzr:gpg-signature'
53 SVN_PROP_BZR_REVISION_ID = 'bzr:revision-id-v%d:' % MAPPING_VERSION
54 SVN_PROP_BZR_BRANCHING_SCHEME = 'bzr:branching-scheme'
55
56 def parse_revid_property(line):
57     """Parse a (revnum, revid) tuple as set in revision id properties.
58     :param line: line to parse
59     :return: tuple with (bzr_revno, revid)
60     """
61     assert not '\n' in line
62     try:
63         (revno, revid) = line.split(' ', 1)
64     except ValueError:
65         raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_ID, 
66                 "missing space")
67     if revid == "":
68         raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_ID,
69                 "empty revision id")
70     return (int(revno), revid)
71
72
73 def parse_revision_metadata(text, rev):
74     """Parse a revision info text (as set in bzr:revision-info).
75
76     :param text: text to parse
77     :param rev: Revision object to apply read parameters to
78     """
79     in_properties = False
80     for l in text.splitlines():
81         try:
82             key, value = l.split(": ", 2)
83         except ValueError:
84             raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_INFO, 
85                     "Missing : in revision metadata")
86         if key == "committer":
87             rev.committer = str(value)
88         elif key == "timestamp":
89             (rev.timestamp, rev.timezone) = unpack_highres_date(value)
90         elif key == "properties":
91             in_properties = True
92         elif key[0] == "\t" and in_properties:
93             rev.properties[str(key[1:])] = str(value)
94         else:
95             raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_INFO, 
96                     "Invalid key %r" % key)
97
98
99 def generate_revision_metadata(timestamp, timezone, committer, revprops):
100     """Generate revision metadata text for the specified revision 
101     properties.
102
103     :param timestamp: timestamp of the revision, in seconds since epoch
104     :param timezone: timezone, specified by offset from GMT in seconds
105     :param committer: name/email of the committer
106     :param revprops: dictionary with custom revision properties
107     :return: text with data to set bzr:revision-info to.
108     """
109     assert timestamp is None or isinstance(timestamp, float)
110     text = ""
111     if timestamp is not None:
112         text += "timestamp: %s\n" % format_highres_date(timestamp, timezone) 
113     if committer is not None:
114         text += "committer: %s\n" % committer
115     if revprops is not None and revprops != {}:
116         text += "properties: \n"
117         for k, v in sorted(revprops.items()):
118             text += "\t%s: %s\n" % (k, v)
119     return text
120
121
122 def svk_feature_to_revision_id(feature, scheme):
123     """Create a revision id from a svk feature identifier.
124
125     :param feature: The feature identifier as string.
126     :param scheme: Branching scheme name
127     :return: Matching revision id.
128     """
129     (uuid, branch, revnum) = feature.split(":")
130     return generate_svn_revision_id(uuid, int(revnum), branch.strip("/"), 
131                                     scheme)
132
133
134 def revision_id_to_svk_feature(revid):
135     """Create a SVK feature identifier from a revision id.
136
137     :param revid: Revision id to convert.
138     :return: Matching SVK feature identifier.
139     """
140     (uuid, branch, revnum, scheme) = parse_svn_revision_id(revid)
141     return "%s:/%s:%d" % (uuid, branch, revnum)
142
143
144 class SvnRepositoryFormat(RepositoryFormat):
145     """Repository format for Subversion repositories (accessed using svn_ra).
146     """
147     rich_root_data = True
148
149     def __init__(self):
150         super(SvnRepositoryFormat, self).__init__()
151
152     def get_format_description(self):
153         return "Subversion Repository"
154
155     def initialize(self, url, shared=False, _internal=False):
156         """Svn repositories cannot be created (yet)."""
157         raise UninitializableFormat(self)
158
159     def check_conversion_target(self, target_repo_format):
160         return target_repo_format.rich_root_data
161
162 cachedbs = {}
163
164 class SvnRepository(Repository):
165     """
166     Provides a simplified interface to a Subversion repository 
167     by using the RA (remote access) API from subversion
168     """
169     def __init__(self, bzrdir, transport, guessed_scheme):
170         from fileids import SimpleFileIdMap
171         _revision_store = None
172
173         assert isinstance(transport, Transport)
174
175         control_files = LockableFiles(transport, '', TransportLock)
176         Repository.__init__(self, SvnRepositoryFormat(), bzrdir, 
177             control_files, None, None, None)
178
179         self.transport = transport
180         self.uuid = transport.get_uuid()
181         assert self.uuid is not None
182         self.base = transport.base
183         assert self.base is not None
184         self._serializer = None
185         self.dir_cache = {}
186         self.pool = Pool()
187         self.config = SvnRepositoryConfig(self.uuid)
188         self.config.add_location(self.base)
189         cache_file = os.path.join(self.create_cache_dir(), 
190                                   'cache-v%d' % MAPPING_VERSION)
191         if not cachedbs.has_key(cache_file):
192             cachedbs[cache_file] = sqlite3.connect(cache_file)
193         self.cachedb = cachedbs[cache_file]
194
195         self._latest_revnum = transport.get_latest_revnum()
196         self._log = logwalker.LogWalker(transport=transport, 
197                                         cache_db=self.cachedb, 
198                                         last_revnum=self._latest_revnum)
199
200         self.branchprop_list = BranchPropertyList(self._log, self.cachedb)
201         self.fileid_map = SimpleFileIdMap(self, self.cachedb)
202         self.revmap = RevidMap(self.cachedb)
203         if self.config.get_branching_scheme() is not None:
204             self.scheme = self.config.get_branching_scheme()
205         else:
206             text = self.branchprop_list.get_property("", self._latest_revnum, 
207                                              SVN_PROP_BZR_BRANCHING_SCHEME, None)
208             if text is not None:
209                 self.set_branching_scheme(
210                         ListBranchingScheme(parse_list_scheme_text(text)))
211             else:
212                 self.scheme = guessed_scheme
213             assert self.scheme is not None
214
215     def set_branching_scheme(self, scheme):
216         self.scheme = scheme
217         self.config.set_branching_scheme(str(scheme))
218
219     def _warn_if_deprecated(self):
220         # This class isn't deprecated
221         pass
222
223     def __repr__(self):
224         return '%s(%r)' % (self.__class__.__name__, 
225                            self.base)
226
227     def create_cache_dir(self):
228         cache_dir = create_cache_dir()
229         dir = os.path.join(cache_dir, self.uuid)
230         if not os.path.exists(dir):
231             os.mkdir(dir)
232         return dir
233
234     def _check(self, revision_ids):
235         return BranchCheckResult(self)
236
237     def get_inventory(self, revision_id):
238         assert revision_id != None
239         return self.revision_tree(revision_id).inventory
240
241     def get_fileid_map(self, revnum, path, scheme):
242         return self.fileid_map.get_map(self.uuid, revnum, path, 
243                                        self.revision_fileid_renames, scheme)
244
245     def transform_fileid_map(self, uuid, revnum, branch, changes, renames, 
246                              scheme):
247         return self.fileid_map.apply_changes(uuid, revnum, branch, changes, 
248                                              renames, scheme)
249
250     def all_revision_ids(self, scheme=None):
251         if scheme is None:
252             scheme = self.scheme
253         for (bp, rev) in self.follow_history(
254                 self.transport.get_latest_revnum(), scheme):
255             yield self.generate_revision_id(rev, bp, str(scheme))
256
257     def get_inventory_weave(self):
258         raise NotImplementedError(self.get_inventory_weave)
259
260     def set_make_working_trees(self, new_value):
261         """See Repository.set_make_working_trees()."""
262         pass # FIXME: ignored, nowhere to store it... 
263
264     def make_working_trees(self):
265         """See Repository.make_working_trees().
266
267         Always returns False, as working trees are never created inside 
268         Subversion repositories.
269         """
270         return False
271
272     def get_ancestry(self, revision_id):
273         """See Repository.get_ancestry().
274         
275         Note: only the first bit is topologically ordered!
276         """
277         if revision_id is None: 
278             return [None]
279
280         (path, revnum, scheme) = self.lookup_revision_id(revision_id)
281
282         ancestry = [revision_id]
283
284         for l in self.branchprop_list.get_property(path, revnum, 
285                                     SVN_PROP_BZR_MERGE, "").splitlines():
286             ancestry.extend(l.split("\n"))
287
288         if revnum > 0:
289             for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
290                 ancestry.append(self.generate_revision_id(rev, branch, scheme))
291
292         ancestry.append(None)
293         ancestry.reverse()
294         return ancestry
295
296     def has_revision(self, revision_id):
297         """See Repository.has_revision()."""
298         if revision_id is None:
299             return True
300
301         try:
302             (path, revnum, scheme) = self.lookup_revision_id(revision_id)
303         except NoSuchRevision:
304             return False
305
306         try:
307             return (svn.core.svn_node_dir == self.transport.check_path(path, revnum))
308         except SubversionException, (_, num):
309             if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
310                 return False
311             raise
312
313
314     def revision_trees(self, revids):
315         """See Repository.revision_trees()."""
316         for revid in revids:
317             yield self.revision_tree(revid)
318
319     def revision_tree(self, revision_id):
320         """See Repository.revision_tree()."""
321         if revision_id is None:
322             revision_id = NULL_REVISION
323
324         if revision_id == NULL_REVISION:
325             inventory = Inventory(root_id=None)
326             inventory.revision_id = revision_id
327             return RevisionTree(self, inventory, revision_id)
328
329         return SvnRevisionTree(self, revision_id)
330
331     def revision_fileid_renames(self, revid):
332         """Check which files were renamed in a particular revision.
333         
334         :param revid: Id of revision to look up.
335         :return: dictionary with paths as keys, file ids as values
336         """
337         (path, revnum, scheme) = self.lookup_revision_id(revid)
338         ret = {}
339         for line in self.branchprop_list.get_property_diff(path, revnum, 
340                 SVN_PROP_BZR_FILEIDS).splitlines():
341             (path, key) = line.split("\t", 2)
342             ret[path] = osutils.safe_file_id(key)
343         return ret
344
345     def _mainline_revision_parent(self, path, revnum, scheme):
346         """Find the mainline parent of the specified revision.
347
348         :param path: Path of the revision in Subversion
349         :param revnum: Subversion revision number
350         :param scheme: Name of branching scheme to use
351         :return: Revision id of the left-hand-side parent or None if 
352                   this is the first revision
353         """
354         assert isinstance(path, basestring)
355         assert isinstance(revnum, int)
356
357         if not scheme.is_branch(path) and \
358            not scheme.is_tag(path):
359             raise NoSuchRevision(self, 
360                     self.generate_revision_id(revnum, path, scheme))
361
362         it = self.follow_branch(path, revnum, scheme)
363         # the first tuple returned should match the one specified. 
364         # if it's not, then the branch, revnum didn't change in the specified 
365         # revision and so it is invalid
366         if (path, revnum) != it.next():
367             raise NoSuchRevision(self, 
368                     self.generate_revision_id(revnum, path, scheme))
369         try:
370             (branch, rev) = it.next()
371             return self.generate_revision_id(rev, branch, scheme)
372         except StopIteration:
373             # The specified revision was the first one in the branch
374             return None
375
376     def revision_parents(self, revision_id, merged_data=None):
377         parent_ids = []
378         (branch, revnum, scheme) = self.lookup_revision_id(revision_id)
379         mainline_parent = self._mainline_revision_parent(branch, revnum, scheme)
380         if mainline_parent is not None:
381             parent_ids.append(mainline_parent)
382             (parent_path, parent_revnum, scheme) = self.lookup_revision_id(mainline_parent)
383         else:
384             parent_path = None
385
386         # if the branch didn't change, bzr:merge can't have changed
387         if not self._log.touches_path(branch, revnum):
388             return parent_ids
389        
390         if merged_data is None:
391             new_merge = self.branchprop_list.get_property(branch, revnum, 
392                                            SVN_PROP_BZR_MERGE, "").splitlines()
393
394             if len(new_merge) == 0 or parent_path is None:
395                 old_merge = ""
396             else:
397                 old_merge = self.branchprop_list.get_property(parent_path, parent_revnum, 
398                         SVN_PROP_BZR_MERGE, "").splitlines()
399
400             assert (len(old_merge) == len(new_merge) or 
401                     len(old_merge) + 1 == len(new_merge))
402
403             if len(old_merge) < len(new_merge):
404                 merged_data = new_merge[-1]
405             else:
406                 merged_data = ""
407
408         if ' ' in merged_data:
409             mutter('invalid revision id %r in merged property, skipping' % merged_data)
410             merged_data = ""
411
412         if merged_data != "":
413             parent_ids.extend(merged_data.split("\t"))
414
415         return parent_ids
416
417     def get_revision(self, revision_id):
418         """See Repository.get_revision."""
419         if not revision_id or not isinstance(revision_id, basestring):
420             raise InvalidRevisionId(revision_id=revision_id, branch=self)
421
422         (path, revnum, scheme) = self.lookup_revision_id(revision_id)
423         
424         parent_ids = self.revision_parents(revision_id)
425
426         # Commit SVN revision properties to a Revision object
427         rev = Revision(revision_id=revision_id, parent_ids=parent_ids)
428
429         (rev.committer, rev.message, date) = self._log.get_revision_info(revnum)
430         if rev.committer is None:
431             rev.committer = ""
432
433         if date is not None:
434             rev.timestamp = 1.0 * svn.core.secs_from_timestr(date, None)
435         else:
436             rev.timestamp = 0.0 # FIXME: Obtain repository creation time
437         rev.timezone = None
438         rev.properties = {}
439         parse_revision_metadata(
440                 self.branchprop_list.get_property(path, revnum, 
441                      SVN_PROP_BZR_REVISION_INFO, ""), rev)
442
443         rev.inventory_sha1 = property(lambda: self.get_inventory_sha1(revision_id))
444
445         return rev
446
447     def get_revisions(self, revision_ids):
448         # TODO: More efficient implementation?
449         return map(self.get_revision, revision_ids)
450
451     def add_revision(self, rev_id, rev, inv=None, config=None):
452         raise NotImplementedError(self.add_revision)
453
454     def generate_revision_id(self, revnum, path, scheme):
455         """Generate an unambiguous revision id. 
456         
457         :param revnum: Subversion revision number.
458         :param path: Branch path.
459         :param scheme: Branching scheme name
460
461         :return: New revision id.
462         """
463         assert isinstance(path, str)
464         assert isinstance(revnum, int)
465
466         # Look in the cache to see if it already has a revision id
467         revid = self.revmap.lookup_branch_revnum(revnum, path, scheme)
468         if revid is not None:
469             return revid
470
471         # Lookup the revision from the bzr:revision-id-vX property
472         line = self.branchprop_list.get_property_diff(path, revnum, 
473                 SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n")
474         # Or generate it
475         if line == "":
476             revid = generate_svn_revision_id(self.uuid, revnum, path, 
477                                              scheme)
478         else:
479             try:
480                 (bzr_revno, revid) = parse_revid_property(line)
481                 self.revmap.insert_revid(revid, path, revnum, revnum, 
482                         scheme, bzr_revno)
483             except errors.InvalidPropertyValue, e:
484                 mutter(str(e))
485                 revid = generate_svn_revision_id(self.uuid, revnum, path, 
486                                                  scheme)
487                 self.revmap.insert_revid(revid, path, revnum, revnum, 
488                         scheme)
489
490         return revid
491
492     def lookup_revision_id(self, revid, scheme=None):
493         """Parse an existing Subversion-based revision id.
494
495         :param revid: The revision id.
496         :param scheme: Optional branching scheme to use when searching for 
497                        revisions
498         :raises: NoSuchRevision
499         :return: Tuple with branch path, revision number and scheme.
500         """
501         def get_scheme(name):
502             assert isinstance(name, basestring)
503             return BranchingScheme.find_scheme(name)
504
505         # Try a simple parse
506         try:
507             (uuid, branch_path, revnum, schemen) = parse_svn_revision_id(revid)
508             assert isinstance(branch_path, str)
509             if uuid == self.uuid:
510                 return (branch_path, revnum, get_scheme(schemen))
511             # If the UUID doesn't match, this may still be a valid revision
512             # id; a revision from another SVN repository may be pushed into 
513             # this one.
514         except InvalidRevisionId:
515             pass
516
517         # Check the record out of the revmap, if it exists
518         try:
519             (branch_path, min_revnum, max_revnum, \
520                     scheme) = self.revmap.lookup_revid(revid)
521             assert isinstance(branch_path, str)
522             # Entry already complete?
523             if min_revnum == max_revnum:
524                 return (branch_path, min_revnum, get_scheme(scheme))
525         except NoSuchRevision:
526             # If there is no entry in the map, walk over all branches:
527             if scheme is None:
528                 scheme = self.scheme
529             for (branch, revno, exists) in self.find_branches(scheme):
530                 # Look at their bzr:revision-id-vX
531                 revids = []
532                 for line in self.branchprop_list.get_property(branch, revno, 
533                         SVN_PROP_BZR_REVISION_ID+str(scheme), "").splitlines():
534                     try:
535                         revids.append(parse_revid_property(line))
536                     except errors.InvalidPropertyValue, e:
537                         mutter(str(e))
538
539                 # If there are any new entries that are not yet in the cache, 
540                 # add them
541                 for (entry_revno, entry_revid) in revids:
542                     self.revmap.insert_revid(entry_revid, branch, 0, revno, 
543                             str(scheme), entry_revno)
544
545                 if revid in revids:
546                     break
547                 
548             (branch_path, min_revnum, max_revnum, scheme) = self.revmap.lookup_revid(revid)
549             assert isinstance(branch_path, str)
550
551         # Find the branch property between min_revnum and max_revnum that 
552         # added revid
553         i = min_revnum
554         for (bp, rev) in self.follow_branch(branch_path, max_revnum, 
555                                             get_scheme(scheme)):
556             try:
557                 (entry_revno, entry_revid) = parse_revid_property(
558                  self.branchprop_list.get_property_diff(bp, rev, 
559                      SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n"))
560             except errors.InvalidPropertyValue:
561                 # Don't warn about encountering an invalid property, 
562                 # that will already have happened earlier
563                 continue
564             if entry_revid == revid:
565                 self.revmap.insert_revid(revid, bp, rev, rev, scheme, 
566                                          entry_revno)
567                 return (bp, rev, get_scheme(scheme))
568
569         raise AssertionError("Revision id %s was added incorrectly" % revid)
570
571     def get_inventory_xml(self, revision_id):
572         return bzrlib.xml5.serializer_v5.write_inventory_to_string(
573             self.get_inventory(revision_id))
574
575     def get_inventory_sha1(self, revision_id):
576         """Get the sha1 for the XML representation of an inventory.
577
578         :param revision_id: Revision id of the inventory for which to return 
579          the SHA1.
580         :return: XML string
581         """
582
583         return osutils.sha_string(self.get_inventory_xml(revision_id))
584
585     def get_revision_xml(self, revision_id):
586         """Return the XML representation of a revision.
587
588         :param revision_id: Revision for which to return the XML.
589         :return: XML string
590         """
591         return bzrlib.xml5.serializer_v5.write_revision_to_string(
592             self.get_revision(revision_id))
593
594     def follow_history(self, revnum, scheme):
595         """Yield all the branches found between the start of history 
596         and a specified revision number.
597
598         :param revnum: Revision number up to which to search.
599         :return: iterator over branches in the range 0..revnum
600         """
601         assert scheme is not None
602
603         while revnum >= 0:
604             yielded_paths = []
605             paths = self._log.get_revision_paths(revnum)
606             for p in paths:
607                 try:
608                     bp = scheme.unprefix(p)[0]
609                     if not bp in yielded_paths:
610                         if not paths.has_key(bp) or paths[bp][0] != 'D':
611                             assert revnum > 0 or bp == ""
612                             yield (bp, revnum)
613                         yielded_paths.append(bp)
614                 except NotBranchError:
615                     pass
616             revnum -= 1
617
618     def follow_branch(self, branch_path, revnum, scheme):
619         """Follow the history of a branch. Will yield all the 
620         left-hand side ancestors of a specified revision.
621     
622         :param branch_path: Subversion path to search.
623         :param revnum: Revision number in Subversion to start.
624         :param scheme: Name of the branching scheme to use
625         :return: iterator over the ancestors
626         """
627         assert branch_path is not None
628         assert isinstance(branch_path, str)
629         assert isinstance(revnum, int) and revnum >= 0
630         if not scheme.is_branch(branch_path) and \
631            not scheme.is_tag(branch_path):
632             raise errors.NotSvnBranchPath(branch_path, revnum)
633         branch_path = branch_path.strip("/")
634
635         while revnum >= 0:
636             paths = self._log.get_revision_paths(revnum)
637
638             yielded = False
639             # If something underneath branch_path changed, there is a 
640             # revision there, so yield it.
641             for p in paths:
642                 assert isinstance(p, str)
643                 if p == branch_path or p.startswith(branch_path+"/") or branch_path == "":
644                     yield (branch_path, revnum)
645                     yielded = True
646                     break
647             
648             # If there are no special cases, just go try the 
649             # next revnum in history
650             revnum -= 1
651
652             # Make sure we get the right location for next time, if 
653             # the branch itself was copied
654             if (paths.has_key(branch_path) and 
655                 paths[branch_path][0] in ('R', 'A')):
656                 if not yielded:
657                     yield (branch_path, revnum+1)
658                 if paths[branch_path][1] is None:
659                     return
660                 if not scheme.is_branch(paths[branch_path][1]) and \
661                    not scheme.is_tag(paths[branch_path][1]):
662                     # FIXME: if copyfrom_path is not a branch path, 
663                     # should simulate a reverse "split" of a branch
664                     # for now, just make it look like the branch ended here
665                     return
666                 revnum = paths[branch_path][2]
667                 branch_path = paths[branch_path][1].encode("utf-8")
668                 continue
669             
670             # Make sure we get the right location for the next time if 
671             # one of the parents changed
672
673             # Path names need to be sorted so the longer paths 
674             # override the shorter ones
675             path_names = paths.keys()
676             path_names.sort()
677             for p in path_names:
678                 if branch_path.startswith(p+"/"):
679                     assert paths[p][1] is not None and paths[p][0] in ('A', 'R'), "Parent didn't exist yet, but child wasn't added !?"
680
681                     revnum = paths[p][2]
682                     branch_path = paths[p][1].encode("utf-8") + branch_path[len(p):]
683
684     """Return all the changes that happened in a branch 
685     between branch_path and revnum. 
686
687     :return: iterator that returns tuples with branch path, 
688     changed paths and revision number.
689     """
690     def follow_branch_history(self, branch_path, revnum, scheme):
691         assert branch_path is not None
692         if not scheme.is_branch(branch_path) and \
693            not scheme.is_tag(branch_path):
694             raise errors.NotSvnBranchPath(branch_path, revnum)
695
696         for (bp, paths, revnum) in self._log.follow_path(branch_path, revnum):
697             if (paths.has_key(bp) and 
698                 paths[bp][1] is not None and 
699                 not scheme.is_branch(paths[bp][1]) and
700                 not scheme.is_tag(paths[bp][1])):
701                 # FIXME: if copyfrom_path is not a branch path, 
702                 # should simulate a reverse "split" of a branch
703                 # for now, just make it look like the branch ended here
704                 for c in self._log.find_children(paths[bp][1], paths[bp][2]):
705                     path = c.replace(paths[bp][1], bp+"/", 1).replace("//", "/")
706                     paths[path] = ('A', None, -1)
707                 paths[bp] = ('A', None, -1)
708
709                 yield (bp, paths, revnum)
710                 return
711                      
712             yield (bp, paths, revnum)
713
714     """Check whether a signature exists for a particular revision id.
715
716     :param revision_id: Revision id for which the signatures should be looked up.
717     :return: False, as no signatures are stored for revisions in Subversion 
718         at the moment.
719     """
720     def has_signature_for_revision_id(self, revision_id):
721         # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE 
722         return False # SVN doesn't store GPG signatures. Perhaps 
723                      # store in SVN revision property?
724
725
726     def get_signature_text(self, revision_id):
727         """Return the signature text for a particular revision.
728
729         :param revision_id: Id of the revision for which to return the 
730                             signature.
731         :raises NoSuchRevision: Always
732         """
733         # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE 
734         # SVN doesn't store GPG signatures
735         raise NoSuchRevision(self, revision_id)
736
737     def _full_revision_graph(self, scheme):
738         graph = {}
739         for (branch, revnum) in self.follow_history(self._latest_revnum, 
740                                                     scheme):
741             mutter('%r, %r' % (branch, revnum))
742             revid = self.generate_revision_id(revnum, branch, scheme)
743             graph[revid] = self.revision_parents(revid)
744         return graph
745
746     def get_revision_graph(self, revision_id=None):
747         if revision_id == NULL_REVISION:
748             return {}
749
750         if revision_id is None:
751             return self._full_revision_graph(self.scheme)
752
753         (path, revnum, scheme) = self.lookup_revision_id(revision_id)
754
755         _previous = revision_id
756         self._ancestry = {}
757         
758         if revnum > 0:
759             for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
760                 revid = self.generate_revision_id(rev, branch, scheme)
761                 self._ancestry[_previous] = [revid]
762                 _previous = revid
763
764         self._ancestry[_previous] = []
765
766         return self._ancestry
767
768     def find_branches(self, scheme, revnum=None, pb=None):
769         """Find all branches that were changed in the specified revision number.
770
771         :param revnum: Revision to search for branches.
772         :return: iterator that returns tuples with (path, revision number, still exists). The revision number is the revision in which the branch last existed.
773         """
774         assert scheme is not None
775         if revnum is None:
776             revnum = self.transport.get_latest_revnum()
777
778         created_branches = {}
779
780         for i in range(revnum+1):
781             if pb is not None:
782                 pb.update("finding branches", i, revnum+1)
783             paths = self._log.get_revision_paths(i)
784             names = paths.keys()
785             names.sort()
786             for p in names:
787                 if scheme.is_branch(p) or scheme.is_tag(p):
788                     if paths[p][0] in ('R', 'D'):
789                         del created_branches[p]
790                         j = self._log.find_latest_change(p, i-1, recurse=True)
791                         yield (p, j, False)
792
793                     if paths[p][0] in ('A', 'R'): 
794                         created_branches[p] = i
795                 elif scheme.is_branch_parent(p) or \
796                         scheme.is_tag_parent(p):
797                     if paths[p][0] in ('R', 'D'):
798                         k = created_branches.keys()
799                         for c in k:
800                             if c.startswith(p+"/"):
801                                 del created_branches[c] 
802                                 j = self._log.find_latest_change(c, i-1, 
803                                         recurse=True)
804                                 yield (c, j, False)
805                     if paths[p][0] in ('A', 'R'):
806                         parents = [p]
807                         while parents:
808                             p = parents.pop()
809                             for c in self.transport.get_dir(p, i)[0].keys():
810                                 n = p+"/"+c
811                                 if scheme.is_branch(n) or scheme.is_tag(n):
812                                     created_branches[n] = i
813                                 elif scheme.is_branch_parent(n) or scheme.is_tag_parent(n):
814                                     parents.append(n)
815
816         for p in created_branches:
817             j = self._log.find_latest_change(p, revnum, recurse=True)
818             if j is None:
819                 j = created_branches[p]
820             yield (p, j, True)
821
822     def is_shared(self):
823         """Return True if this repository is flagged as a shared repository."""
824         return True
825
826     def get_physical_lock_status(self):
827         return False
828
829     def get_commit_builder(self, branch, parents, config, timestamp=None, 
830                            timezone=None, committer=None, revprops=None, 
831                            revision_id=None):
832         from commit import SvnCommitBuilder
833         return SvnCommitBuilder(self, branch, parents, config, timestamp, 
834                 timezone, committer, revprops, revision_id)
835
836
837