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